diff --git a/.ai/mcp/mcp.json b/.ai/mcp/mcp.json new file mode 100644 index 00000000..0bb6a0f1 --- /dev/null +++ b/.ai/mcp/mcp.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "laravel-boost": { + "command": "php", + "args": [ + "./artisan", + "boost:mcp" + ] + } + } +} \ No newline at end of file diff --git a/.cursor/mcp.json b/.cursor/mcp.json new file mode 100644 index 00000000..ea301954 --- /dev/null +++ b/.cursor/mcp.json @@ -0,0 +1,11 @@ +{ + "mcpServers": { + "laravel-boost": { + "command": "php", + "args": [ + "./artisan", + "boost:mcp" + ] + } + } +} \ No newline at end of file diff --git a/.cursor/rules/laravel-boost.mdc b/.cursor/rules/laravel-boost.mdc new file mode 100644 index 00000000..da9e12e7 --- /dev/null +++ b/.cursor/rules/laravel-boost.mdc @@ -0,0 +1,367 @@ +--- +alwaysApply: true +--- + +=== foundation rules === + +# Laravel Boost Guidelines + +The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications. + +## Foundational Context +This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. + +- php - 8.4.19 +- filament/filament (FILAMENT) - v5 +- laravel/cashier (CASHIER) - v15 +- laravel/framework (LARAVEL) - v12 +- laravel/horizon (HORIZON) - v5 +- laravel/nightwatch (NIGHTWATCH) - v1 +- laravel/pennant (PENNANT) - v1 +- laravel/prompts (PROMPTS) - v0 +- laravel/sanctum (SANCTUM) - v4 +- laravel/socialite (SOCIALITE) - v5 +- livewire/flux (FLUXUI_FREE) - v2 +- livewire/flux-pro (FLUXUI_PRO) - v2 +- livewire/livewire (LIVEWIRE) - v4 +- laravel/mcp (MCP) - v0 +- laravel/pint (PINT) - v1 +- laravel/sail (SAIL) - v1 +- phpunit/phpunit (PHPUNIT) - v11 +- rector/rector (RECTOR) - v2 +- alpinejs (ALPINEJS) - v3 +- prettier (PRETTIER) - v3 +- tailwindcss (TAILWINDCSS) - v4 + +## Conventions +- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming. +- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. +- Check for existing components to reuse before writing a new one. + +## Verification Scripts +- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important. + +## Application Structure & Architecture +- Stick to existing directory structure; don't create new base folders without approval. +- Do not change the application's dependencies without approval. + +## Frontend Bundling +- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them. + +## Replies +- Be concise in your explanations - focus on what's important rather than explaining obvious details. + +## Documentation Files +- You must only create documentation files if explicitly requested by the user. + +=== boost rules === + +## Laravel Boost +- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them. + +## Artisan +- Use the `list-artisan-commands` tool when you need to call an Artisan command to double-check the available parameters. + +## URLs +- Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port. + +## Tinker / Debugging +- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly. +- Use the `database-query` tool when you only need to read from the database. + +## Reading Browser Logs With the `browser-logs` Tool +- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost. +- Only recent browser logs will be useful - ignore old logs. + +## Searching Documentation (Critically Important) +- Boost comes with a powerful `search-docs` tool you should use before any other approaches when dealing with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages. +- The `search-docs` tool is perfect for all Laravel-related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc. +- You must use this tool to search for Laravel ecosystem documentation before falling back to other approaches. +- Search the documentation before making code changes to ensure we are taking the correct approach. +- Use multiple, broad, simple, topic-based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`. +- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`. + +### Available Search Syntax +- You can and should pass multiple queries at once. The most relevant results will be returned first. + +1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'. +2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit". +3. Quoted Phrases (Exact Position) - query="infinite scroll" - words must be adjacent and in that order. +4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit". +5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms. + +=== php rules === + +## PHP + +- Always use curly braces for control structures, even if it has one line. + +### Constructors +- Use PHP 8 constructor property promotion in `__construct()`. + - public function __construct(public GitHub $github) { } +- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private. + +### Type Declarations +- Always use explicit return type declarations for methods and functions. +- Use appropriate PHP type hints for method parameters. + + +protected function isAccessible(User $user, ?string $path = null): bool +{ + ... +} + + +## Comments +- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless there is something very complex going on. + +## PHPDoc Blocks +- Add useful array shape type definitions for arrays when appropriate. + +## Enums +- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`. + +=== herd rules === + +## Laravel Herd + +- The application is served by Laravel Herd and will be available at: `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate URLs for the user to ensure valid URLs. +- You must not run any commands to make the site available via HTTP(S). It is always available through Laravel Herd. + +=== tests rules === + +## Test Enforcement + +- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. +- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter. + +=== laravel/core rules === + +## Do Things the Laravel Way + +- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool. +- If you're creating a generic PHP class, use `php artisan make:class`. +- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. + +### Database +- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins. +- Use Eloquent models and relationships before suggesting raw database queries. +- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them. +- Generate code that prevents N+1 query problems by using eager loading. +- Use Laravel's query builder for very complex database operations. + +### Model Creation +- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`. + +### APIs & Eloquent Resources +- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. + +### Controllers & Validation +- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages. +- Check sibling Form Requests to see if the application uses array or string based validation rules. + +### Queues +- Use queued jobs for time-consuming operations with the `ShouldQueue` interface. + +### Authentication & Authorization +- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.). + +### URL Generation +- When generating links to other pages, prefer named routes and the `route()` function. + +### Configuration +- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`. + +### Testing +- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model. +- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`. +- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. + +### Vite Error +- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`. + +=== laravel/v12 rules === + +## Laravel 12 + +- Use the `search-docs` tool to get version-specific documentation. +- This project upgraded from Laravel 10 without migrating to the new streamlined Laravel file structure. +- This is **perfectly fine** and recommended by Laravel. Follow the existing structure from Laravel 10. We do not need to migrate to the new Laravel structure unless the user explicitly requests it. + +### Laravel 10 Structure +- Middleware typically lives in `app/Http/Middleware/` and service providers in `app/Providers/`. +- There is no `bootstrap/app.php` application configuration in a Laravel 10 structure: + - Middleware registration happens in `app/Http/Kernel.php` + - Exception handling is in `app/Exceptions/Handler.php` + - Console commands and schedule register in `app/Console/Kernel.php` + - Rate limits likely exist in `RouteServiceProvider` or `app/Http/Kernel.php` + +### Database +- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost. +- Laravel 12 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`. + +### Models +- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models. + +=== pennant/core rules === + +## Laravel Pennant + +- This application uses Laravel Pennant for feature flag management, providing a flexible system for controlling feature availability across different organizations and user types. +- Use the `search-docs` tool, in combination with existing codebase conventions, to assist the user effectively with feature flags. + +=== fluxui-pro/core rules === + +## Flux UI Pro + +- This project is using the Pro version of Flux UI. It has full access to the free components and variants, as well as full access to the Pro components and variants. +- Flux UI is a component library for Livewire. Flux is a robust, hand-crafted UI component library for your Livewire applications. It's built using Tailwind CSS and provides a set of components that are easy to use and customize. +- You should use Flux UI components when available. +- Fallback to standard Blade components if Flux is unavailable. +- If available, use the `search-docs` tool to get the exact documentation and code snippets available for this project. +- Flux UI components look like this: + + + + + +### Available Components +This is correct as of Boost installation, but there may be additional components within the codebase. + + +accordion, autocomplete, avatar, badge, brand, breadcrumbs, button, calendar, callout, card, chart, checkbox, command, composer, context, date-picker, dropdown, editor, field, file-upload, heading, icon, input, kanban, modal, navbar, otp-input, pagination, pillbox, popover, profile, radio, select, separator, skeleton, slider, switch, table, tabs, text, textarea, time-picker, toast, tooltip + + +=== livewire/core rules === + +## Livewire + +- Use the `search-docs` tool to find exact version-specific documentation for how to write Livewire and Livewire tests. +- Use the `php artisan make:livewire [Posts\CreatePost]` Artisan command to create new components. +- State should live on the server, with the UI reflecting it. +- All Livewire requests hit the Laravel backend; they're like regular HTTP requests. Always validate form data and run authorization checks in Livewire actions. + +## Livewire Best Practices +- Livewire components require a single root element. +- Use `wire:loading` and `wire:dirty` for delightful loading states. +- Add `wire:key` in loops: + + ```blade + @foreach ($items as $item) +
+ {{ $item->name }} +
+ @endforeach + ``` + +- Prefer lifecycle hooks like `mount()`, `updatedFoo()` for initialization and reactive side effects: + + + public function mount(User $user) { $this->user = $user; } + public function updatedSearch() { $this->resetPage(); } + + +## Testing Livewire + + + Livewire::test(Counter::class) + ->assertSet('count', 0) + ->call('increment') + ->assertSet('count', 1) + ->assertSee(1) + ->assertStatus(200); + + + + $this->get('/posts/create') + ->assertSeeLivewire(CreatePost::class); + + +=== pint/core rules === + +## Laravel Pint Code Formatter + +- You must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style. +- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues. + +=== phpunit/core rules === + +## PHPUnit + +- This application uses PHPUnit for testing. All tests must be written as PHPUnit classes. Use `php artisan make:test --phpunit {name}` to create a new test. +- If you see a test using "Pest", convert it to PHPUnit. +- Every time a test has been updated, run that singular test. +- When the tests relating to your feature are passing, ask the user if they would like to also run the entire test suite to make sure everything is still passing. +- Tests should test all of the happy paths, failure paths, and weird paths. +- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files; these are core to the application. + +### Running Tests +- Run the minimal number of tests, using an appropriate filter, before finalizing. +- To run all tests: `php artisan test --compact`. +- To run all tests in a file: `php artisan test --compact tests/Feature/ExampleTest.php`. +- To filter on a particular test name: `php artisan test --compact --filter=testName` (recommended after making a change to a related file). + +=== tailwindcss/core rules === + +## Tailwind CSS + +- Use Tailwind CSS classes to style HTML; check and use existing Tailwind conventions within the project before writing your own. +- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc.). +- Think through class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child carefully to limit repetition, and group elements logically. +- You can use the `search-docs` tool to get exact examples from the official documentation when needed. + +### Spacing +- When listing items, use gap utilities for spacing; don't use margins. + + +
+
Superior
+
Michigan
+
Erie
+
+
+ +### Dark Mode +- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`. + +=== tailwindcss/v4 rules === + +## Tailwind CSS 4 + +- Always use Tailwind CSS v4; do not use the deprecated utilities. +- `corePlugins` is not supported in Tailwind v4. +- In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed. + + +@theme { + --color-brand: oklch(0.72 0.11 178); +} + + +- In Tailwind v4, you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3: + + + - @tailwind base; + - @tailwind components; + - @tailwind utilities; + + @import "tailwindcss"; + + +### Replaced Utilities +- Tailwind v4 removed deprecated utilities. Do not use the deprecated option; use the replacement. +- Opacity values are still numeric. + +| Deprecated | Replacement | +|------------+--------------| +| bg-opacity-* | bg-black/* | +| text-opacity-* | text-black/* | +| border-opacity-* | border-black/* | +| divide-opacity-* | divide-black/* | +| ring-opacity-* | ring-black/* | +| placeholder-opacity-* | placeholder-black/* | +| flex-shrink-* | shrink-* | +| flex-grow-* | grow-* | +| overflow-ellipsis | text-ellipsis | +| decoration-slice | box-decoration-slice | +| decoration-clone | box-decoration-clone | +
diff --git a/.env.example b/.env.example index 898fd326..6f3fc12a 100644 --- a/.env.example +++ b/.env.example @@ -56,3 +56,38 @@ VITE_PUSHER_HOST="${PUSHER_HOST}" VITE_PUSHER_PORT="${PUSHER_PORT}" VITE_PUSHER_SCHEME="${PUSHER_SCHEME}" VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}" + +TORCHLIGHT_TOKEN= + +STRIPE_KEY= +STRIPE_SECRET= +STRIPE_WEBHOOK_SECRET= +STRIPE_MINI_PRICE_ID= +STRIPE_MINI_PRICE_ID_EAP= +STRIPE_PRO_PRICE_ID= +STRIPE_PRO_PRICE_ID_EAP= +STRIPE_MAX_PRICE_ID= +STRIPE_MAX_PRICE_ID_MONTHLY= +STRIPE_MAX_PRICE_ID_EAP= +STRIPE_ULTRA_COMP_PRICE_ID= +STRIPE_EXTRA_SEAT_PRICE_ID= +STRIPE_EXTRA_SEAT_PRICE_ID_MONTHLY= +STRIPE_FOREVER_PRICE_ID= +STRIPE_TRIAL_PRICE_ID= +STRIPE_MINI_PAYMENT_LINK= +STRIPE_PRO_PAYMENT_LINK= +STRIPE_MAX_PAYMENT_LINK= +STRIPE_FOREVER_PAYMENT_LINK= +STRIPE_TRIAL_PAYMENT_LINK= + +ANYSTACK_API_KEY= +ANYSTACK_PRODUCT_ID= +ANYSTACK_MINI_POLICY_ID= +ANYSTACK_PRO_POLICY_ID= +ANYSTACK_MAX_POLICY_ID= +ANYSTACK_FOREVER_POLICY_ID= +ANYSTACK_TRIAL_POLICY_ID= + +FILAMENT_USERS= + +BIFROST_API_KEY=your-secure-api-key-here diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 00000000..d5a3f494 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,364 @@ + +=== foundation rules === + +# Laravel Boost Guidelines + +The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications. + +## Foundational Context +This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. + +- php - 8.4.19 +- filament/filament (FILAMENT) - v5 +- laravel/cashier (CASHIER) - v15 +- laravel/framework (LARAVEL) - v12 +- laravel/horizon (HORIZON) - v5 +- laravel/nightwatch (NIGHTWATCH) - v1 +- laravel/pennant (PENNANT) - v1 +- laravel/prompts (PROMPTS) - v0 +- laravel/sanctum (SANCTUM) - v4 +- laravel/socialite (SOCIALITE) - v5 +- livewire/flux (FLUXUI_FREE) - v2 +- livewire/flux-pro (FLUXUI_PRO) - v2 +- livewire/livewire (LIVEWIRE) - v4 +- laravel/mcp (MCP) - v0 +- laravel/pint (PINT) - v1 +- laravel/sail (SAIL) - v1 +- phpunit/phpunit (PHPUNIT) - v11 +- rector/rector (RECTOR) - v2 +- alpinejs (ALPINEJS) - v3 +- prettier (PRETTIER) - v3 +- tailwindcss (TAILWINDCSS) - v4 + +## Conventions +- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming. +- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. +- Check for existing components to reuse before writing a new one. + +## Verification Scripts +- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important. + +## Application Structure & Architecture +- Stick to existing directory structure; don't create new base folders without approval. +- Do not change the application's dependencies without approval. + +## Frontend Bundling +- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them. + +## Replies +- Be concise in your explanations - focus on what's important rather than explaining obvious details. + +## Documentation Files +- You must only create documentation files if explicitly requested by the user. + +=== boost rules === + +## Laravel Boost +- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them. + +## Artisan +- Use the `list-artisan-commands` tool when you need to call an Artisan command to double-check the available parameters. + +## URLs +- Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port. + +## Tinker / Debugging +- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly. +- Use the `database-query` tool when you only need to read from the database. + +## Reading Browser Logs With the `browser-logs` Tool +- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost. +- Only recent browser logs will be useful - ignore old logs. + +## Searching Documentation (Critically Important) +- Boost comes with a powerful `search-docs` tool you should use before any other approaches when dealing with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages. +- The `search-docs` tool is perfect for all Laravel-related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc. +- You must use this tool to search for Laravel ecosystem documentation before falling back to other approaches. +- Search the documentation before making code changes to ensure we are taking the correct approach. +- Use multiple, broad, simple, topic-based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`. +- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`. + +### Available Search Syntax +- You can and should pass multiple queries at once. The most relevant results will be returned first. + +1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'. +2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit". +3. Quoted Phrases (Exact Position) - query="infinite scroll" - words must be adjacent and in that order. +4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit". +5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms. + +=== php rules === + +## PHP + +- Always use curly braces for control structures, even if it has one line. + +### Constructors +- Use PHP 8 constructor property promotion in `__construct()`. + - public function __construct(public GitHub $github) { } +- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private. + +### Type Declarations +- Always use explicit return type declarations for methods and functions. +- Use appropriate PHP type hints for method parameters. + + +protected function isAccessible(User $user, ?string $path = null): bool +{ + ... +} + + +## Comments +- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless there is something very complex going on. + +## PHPDoc Blocks +- Add useful array shape type definitions for arrays when appropriate. + +## Enums +- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`. + +=== herd rules === + +## Laravel Herd + +- The application is served by Laravel Herd and will be available at: `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate URLs for the user to ensure valid URLs. +- You must not run any commands to make the site available via HTTP(S). It is always available through Laravel Herd. + +=== tests rules === + +## Test Enforcement + +- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. +- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter. + +=== laravel/core rules === + +## Do Things the Laravel Way + +- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool. +- If you're creating a generic PHP class, use `php artisan make:class`. +- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. + +### Database +- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins. +- Use Eloquent models and relationships before suggesting raw database queries. +- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them. +- Generate code that prevents N+1 query problems by using eager loading. +- Use Laravel's query builder for very complex database operations. + +### Model Creation +- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`. + +### APIs & Eloquent Resources +- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. + +### Controllers & Validation +- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages. +- Check sibling Form Requests to see if the application uses array or string based validation rules. + +### Queues +- Use queued jobs for time-consuming operations with the `ShouldQueue` interface. + +### Authentication & Authorization +- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.). + +### URL Generation +- When generating links to other pages, prefer named routes and the `route()` function. + +### Configuration +- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`. + +### Testing +- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model. +- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`. +- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. + +### Vite Error +- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`. + +=== laravel/v12 rules === + +## Laravel 12 + +- Use the `search-docs` tool to get version-specific documentation. +- This project upgraded from Laravel 10 without migrating to the new streamlined Laravel file structure. +- This is **perfectly fine** and recommended by Laravel. Follow the existing structure from Laravel 10. We do not need to migrate to the new Laravel structure unless the user explicitly requests it. + +### Laravel 10 Structure +- Middleware typically lives in `app/Http/Middleware/` and service providers in `app/Providers/`. +- There is no `bootstrap/app.php` application configuration in a Laravel 10 structure: + - Middleware registration happens in `app/Http/Kernel.php` + - Exception handling is in `app/Exceptions/Handler.php` + - Console commands and schedule register in `app/Console/Kernel.php` + - Rate limits likely exist in `RouteServiceProvider` or `app/Http/Kernel.php` + +### Database +- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost. +- Laravel 12 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`. + +### Models +- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models. + +=== pennant/core rules === + +## Laravel Pennant + +- This application uses Laravel Pennant for feature flag management, providing a flexible system for controlling feature availability across different organizations and user types. +- Use the `search-docs` tool, in combination with existing codebase conventions, to assist the user effectively with feature flags. + +=== fluxui-pro/core rules === + +## Flux UI Pro + +- This project is using the Pro version of Flux UI. It has full access to the free components and variants, as well as full access to the Pro components and variants. +- Flux UI is a component library for Livewire. Flux is a robust, hand-crafted UI component library for your Livewire applications. It's built using Tailwind CSS and provides a set of components that are easy to use and customize. +- You should use Flux UI components when available. +- Fallback to standard Blade components if Flux is unavailable. +- If available, use the `search-docs` tool to get the exact documentation and code snippets available for this project. +- Flux UI components look like this: + + + + + +### Available Components +This is correct as of Boost installation, but there may be additional components within the codebase. + + +accordion, autocomplete, avatar, badge, brand, breadcrumbs, button, calendar, callout, card, chart, checkbox, command, composer, context, date-picker, dropdown, editor, field, file-upload, heading, icon, input, kanban, modal, navbar, otp-input, pagination, pillbox, popover, profile, radio, select, separator, skeleton, slider, switch, table, tabs, text, textarea, time-picker, toast, tooltip + + +=== livewire/core rules === + +## Livewire + +- Use the `search-docs` tool to find exact version-specific documentation for how to write Livewire and Livewire tests. +- Use the `php artisan make:livewire [Posts\CreatePost]` Artisan command to create new components. +- State should live on the server, with the UI reflecting it. +- All Livewire requests hit the Laravel backend; they're like regular HTTP requests. Always validate form data and run authorization checks in Livewire actions. + +## Livewire Best Practices +- Livewire components require a single root element. +- Use `wire:loading` and `wire:dirty` for delightful loading states. +- Add `wire:key` in loops: + + ```blade + @foreach ($items as $item) +
+ {{ $item->name }} +
+ @endforeach + ``` + +- Prefer lifecycle hooks like `mount()`, `updatedFoo()` for initialization and reactive side effects: + + + public function mount(User $user) { $this->user = $user; } + public function updatedSearch() { $this->resetPage(); } + + +## Testing Livewire + + + Livewire::test(Counter::class) + ->assertSet('count', 0) + ->call('increment') + ->assertSet('count', 1) + ->assertSee(1) + ->assertStatus(200); + + + + $this->get('/posts/create') + ->assertSeeLivewire(CreatePost::class); + + +=== pint/core rules === + +## Laravel Pint Code Formatter + +- You must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style. +- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues. + +=== phpunit/core rules === + +## PHPUnit + +- This application uses PHPUnit for testing. All tests must be written as PHPUnit classes. Use `php artisan make:test --phpunit {name}` to create a new test. +- If you see a test using "Pest", convert it to PHPUnit. +- Every time a test has been updated, run that singular test. +- When the tests relating to your feature are passing, ask the user if they would like to also run the entire test suite to make sure everything is still passing. +- Tests should test all of the happy paths, failure paths, and weird paths. +- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files; these are core to the application. + +### Running Tests +- Run the minimal number of tests, using an appropriate filter, before finalizing. +- To run all tests: `php artisan test --compact`. +- To run all tests in a file: `php artisan test --compact tests/Feature/ExampleTest.php`. +- To filter on a particular test name: `php artisan test --compact --filter=testName` (recommended after making a change to a related file). + +=== tailwindcss/core rules === + +## Tailwind CSS + +- Use Tailwind CSS classes to style HTML; check and use existing Tailwind conventions within the project before writing your own. +- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc.). +- Think through class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child carefully to limit repetition, and group elements logically. +- You can use the `search-docs` tool to get exact examples from the official documentation when needed. + +### Spacing +- When listing items, use gap utilities for spacing; don't use margins. + + +
+
Superior
+
Michigan
+
Erie
+
+
+ +### Dark Mode +- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`. + +=== tailwindcss/v4 rules === + +## Tailwind CSS 4 + +- Always use Tailwind CSS v4; do not use the deprecated utilities. +- `corePlugins` is not supported in Tailwind v4. +- In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed. + + +@theme { + --color-brand: oklch(0.72 0.11 178); +} + + +- In Tailwind v4, you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3: + + + - @tailwind base; + - @tailwind components; + - @tailwind utilities; + + @import "tailwindcss"; + + +### Replaced Utilities +- Tailwind v4 removed deprecated utilities. Do not use the deprecated option; use the replacement. +- Opacity values are still numeric. + +| Deprecated | Replacement | +|------------+--------------| +| bg-opacity-* | bg-black/* | +| text-opacity-* | text-black/* | +| border-opacity-* | border-black/* | +| divide-opacity-* | divide-black/* | +| ring-opacity-* | ring-black/* | +| placeholder-opacity-* | placeholder-black/* | +| flex-shrink-* | shrink-* | +| flex-grow-* | grow-* | +| overflow-ellipsis | text-ellipsis | +| decoration-slice | box-decoration-slice | +| decoration-clone | box-decoration-clone | +
diff --git a/.github/funding.yml b/.github/funding.yml index 23b9601c..5117700c 100644 --- a/.github/funding.yml +++ b/.github/funding.yml @@ -1,2 +1,2 @@ -github: simonhamp +github: nativephp open_collective: nativephp diff --git a/.github/workflows/linting.yaml b/.github/workflows/linting.yaml index 9313ab46..1cd305a9 100644 --- a/.github/workflows/linting.yaml +++ b/.github/workflows/linting.yaml @@ -11,7 +11,7 @@ jobs: strategy: matrix: os: [ubuntu-latest] - php: [8.3] + php: [8.4] steps: - name: Checkout code @@ -20,7 +20,7 @@ jobs: - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: '8.3' + php-version: ${{ matrix.php }} tools: composer:v2 - name: Copy .env @@ -37,6 +37,9 @@ jobs: key: ${{ runner.os }}-${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }} restore-keys: ${{ runner.os }}-${{ matrix.php }}-composer- + - name: Configure Flux Pro auth + run: composer config http-basic.composer.fluxui.dev "${{ secrets.FLUX_EMAIL }}" "${{ secrets.FLUX_LICENSE_KEY }}" + - name: Install Dependencies run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml new file mode 100644 index 00000000..eedef6a6 --- /dev/null +++ b/.github/workflows/tests.yaml @@ -0,0 +1,67 @@ +# Unit and features tests +# also useful for testing new php versions +name: Tests +on: + workflow_dispatch: + push: + branches-ignore: + - 'dependabot/npm_and_yarn/*' + +jobs: + tests: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + php: [8.4] +# services: +# redis: +# image: redis +# ports: +# - 6379:6379 +# options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3 + + steps: + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer:v2 + coverage: xdebug + + - name: Copy .env + run: cp .env.example .env + + - name: Get composer cache directory + id: composer-cache + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache Composer dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-${{ matrix.php }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: ${{ runner.os }}-${{ matrix.php }}-composer- + + - name: Configure Flux Pro auth + run: composer config http-basic.composer.fluxui.dev "${{ secrets.FLUX_EMAIL }}" "${{ secrets.FLUX_LICENSE_KEY }}" + + - name: Install Dependencies + run: composer install -q --no-ansi --no-interaction --no-scripts --no-progress --prefer-dist + + - name: Generate Application Key + run: php artisan key:generate + + - name: Compile Assets + run: npm install && npm run build + + - name: Run tests + env: + DB_CONNECTION: sqlite + DB_DATABASE: ":memory:" + QUEUE_CONNECTION: sync + run: ./vendor/bin/phpunit --testdox diff --git a/.gitignore b/.gitignore index 7fe978f8..9713846c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,19 +1,55 @@ +# php unit /.phpunit.cache +.phpunit.result.cache + +# auth +auth.json + +# node_modules /node_modules -/public/build + +# public /public/hot /public/storage +/public/build +/public/sitemap.xml + +# storage /storage/*.key + +# vendor /vendor -.env -.env.backup -.env.production -.phpunit.result.cache + +# ide +/.fleet +/.idea +/.vscode +_ide_helper* +.phpstorm.meta.php + +# .env +.env* + +# cache +*.cache + +# homestead Homestead.json Homestead.yaml -auth.json + +# docker +docker-compose.override.yml + +# package managers npm-debug.log yarn-error.log -/.fleet -/.idea -/.vscode + +# logs +worker.log + +# windows +*:Zone.Identifier +Thumbs.db + +# mac +.DS_Store \ No newline at end of file diff --git a/.junie/guidelines.md b/.junie/guidelines.md new file mode 100644 index 00000000..d5a3f494 --- /dev/null +++ b/.junie/guidelines.md @@ -0,0 +1,364 @@ + +=== foundation rules === + +# Laravel Boost Guidelines + +The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications. + +## Foundational Context +This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. + +- php - 8.4.19 +- filament/filament (FILAMENT) - v5 +- laravel/cashier (CASHIER) - v15 +- laravel/framework (LARAVEL) - v12 +- laravel/horizon (HORIZON) - v5 +- laravel/nightwatch (NIGHTWATCH) - v1 +- laravel/pennant (PENNANT) - v1 +- laravel/prompts (PROMPTS) - v0 +- laravel/sanctum (SANCTUM) - v4 +- laravel/socialite (SOCIALITE) - v5 +- livewire/flux (FLUXUI_FREE) - v2 +- livewire/flux-pro (FLUXUI_PRO) - v2 +- livewire/livewire (LIVEWIRE) - v4 +- laravel/mcp (MCP) - v0 +- laravel/pint (PINT) - v1 +- laravel/sail (SAIL) - v1 +- phpunit/phpunit (PHPUNIT) - v11 +- rector/rector (RECTOR) - v2 +- alpinejs (ALPINEJS) - v3 +- prettier (PRETTIER) - v3 +- tailwindcss (TAILWINDCSS) - v4 + +## Conventions +- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming. +- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. +- Check for existing components to reuse before writing a new one. + +## Verification Scripts +- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important. + +## Application Structure & Architecture +- Stick to existing directory structure; don't create new base folders without approval. +- Do not change the application's dependencies without approval. + +## Frontend Bundling +- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them. + +## Replies +- Be concise in your explanations - focus on what's important rather than explaining obvious details. + +## Documentation Files +- You must only create documentation files if explicitly requested by the user. + +=== boost rules === + +## Laravel Boost +- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them. + +## Artisan +- Use the `list-artisan-commands` tool when you need to call an Artisan command to double-check the available parameters. + +## URLs +- Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port. + +## Tinker / Debugging +- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly. +- Use the `database-query` tool when you only need to read from the database. + +## Reading Browser Logs With the `browser-logs` Tool +- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost. +- Only recent browser logs will be useful - ignore old logs. + +## Searching Documentation (Critically Important) +- Boost comes with a powerful `search-docs` tool you should use before any other approaches when dealing with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages. +- The `search-docs` tool is perfect for all Laravel-related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc. +- You must use this tool to search for Laravel ecosystem documentation before falling back to other approaches. +- Search the documentation before making code changes to ensure we are taking the correct approach. +- Use multiple, broad, simple, topic-based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`. +- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`. + +### Available Search Syntax +- You can and should pass multiple queries at once. The most relevant results will be returned first. + +1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'. +2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit". +3. Quoted Phrases (Exact Position) - query="infinite scroll" - words must be adjacent and in that order. +4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit". +5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms. + +=== php rules === + +## PHP + +- Always use curly braces for control structures, even if it has one line. + +### Constructors +- Use PHP 8 constructor property promotion in `__construct()`. + - public function __construct(public GitHub $github) { } +- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private. + +### Type Declarations +- Always use explicit return type declarations for methods and functions. +- Use appropriate PHP type hints for method parameters. + + +protected function isAccessible(User $user, ?string $path = null): bool +{ + ... +} + + +## Comments +- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless there is something very complex going on. + +## PHPDoc Blocks +- Add useful array shape type definitions for arrays when appropriate. + +## Enums +- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`. + +=== herd rules === + +## Laravel Herd + +- The application is served by Laravel Herd and will be available at: `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate URLs for the user to ensure valid URLs. +- You must not run any commands to make the site available via HTTP(S). It is always available through Laravel Herd. + +=== tests rules === + +## Test Enforcement + +- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. +- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter. + +=== laravel/core rules === + +## Do Things the Laravel Way + +- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool. +- If you're creating a generic PHP class, use `php artisan make:class`. +- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. + +### Database +- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins. +- Use Eloquent models and relationships before suggesting raw database queries. +- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them. +- Generate code that prevents N+1 query problems by using eager loading. +- Use Laravel's query builder for very complex database operations. + +### Model Creation +- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`. + +### APIs & Eloquent Resources +- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. + +### Controllers & Validation +- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages. +- Check sibling Form Requests to see if the application uses array or string based validation rules. + +### Queues +- Use queued jobs for time-consuming operations with the `ShouldQueue` interface. + +### Authentication & Authorization +- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.). + +### URL Generation +- When generating links to other pages, prefer named routes and the `route()` function. + +### Configuration +- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`. + +### Testing +- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model. +- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`. +- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. + +### Vite Error +- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`. + +=== laravel/v12 rules === + +## Laravel 12 + +- Use the `search-docs` tool to get version-specific documentation. +- This project upgraded from Laravel 10 without migrating to the new streamlined Laravel file structure. +- This is **perfectly fine** and recommended by Laravel. Follow the existing structure from Laravel 10. We do not need to migrate to the new Laravel structure unless the user explicitly requests it. + +### Laravel 10 Structure +- Middleware typically lives in `app/Http/Middleware/` and service providers in `app/Providers/`. +- There is no `bootstrap/app.php` application configuration in a Laravel 10 structure: + - Middleware registration happens in `app/Http/Kernel.php` + - Exception handling is in `app/Exceptions/Handler.php` + - Console commands and schedule register in `app/Console/Kernel.php` + - Rate limits likely exist in `RouteServiceProvider` or `app/Http/Kernel.php` + +### Database +- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost. +- Laravel 12 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`. + +### Models +- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models. + +=== pennant/core rules === + +## Laravel Pennant + +- This application uses Laravel Pennant for feature flag management, providing a flexible system for controlling feature availability across different organizations and user types. +- Use the `search-docs` tool, in combination with existing codebase conventions, to assist the user effectively with feature flags. + +=== fluxui-pro/core rules === + +## Flux UI Pro + +- This project is using the Pro version of Flux UI. It has full access to the free components and variants, as well as full access to the Pro components and variants. +- Flux UI is a component library for Livewire. Flux is a robust, hand-crafted UI component library for your Livewire applications. It's built using Tailwind CSS and provides a set of components that are easy to use and customize. +- You should use Flux UI components when available. +- Fallback to standard Blade components if Flux is unavailable. +- If available, use the `search-docs` tool to get the exact documentation and code snippets available for this project. +- Flux UI components look like this: + + + + + +### Available Components +This is correct as of Boost installation, but there may be additional components within the codebase. + + +accordion, autocomplete, avatar, badge, brand, breadcrumbs, button, calendar, callout, card, chart, checkbox, command, composer, context, date-picker, dropdown, editor, field, file-upload, heading, icon, input, kanban, modal, navbar, otp-input, pagination, pillbox, popover, profile, radio, select, separator, skeleton, slider, switch, table, tabs, text, textarea, time-picker, toast, tooltip + + +=== livewire/core rules === + +## Livewire + +- Use the `search-docs` tool to find exact version-specific documentation for how to write Livewire and Livewire tests. +- Use the `php artisan make:livewire [Posts\CreatePost]` Artisan command to create new components. +- State should live on the server, with the UI reflecting it. +- All Livewire requests hit the Laravel backend; they're like regular HTTP requests. Always validate form data and run authorization checks in Livewire actions. + +## Livewire Best Practices +- Livewire components require a single root element. +- Use `wire:loading` and `wire:dirty` for delightful loading states. +- Add `wire:key` in loops: + + ```blade + @foreach ($items as $item) +
+ {{ $item->name }} +
+ @endforeach + ``` + +- Prefer lifecycle hooks like `mount()`, `updatedFoo()` for initialization and reactive side effects: + + + public function mount(User $user) { $this->user = $user; } + public function updatedSearch() { $this->resetPage(); } + + +## Testing Livewire + + + Livewire::test(Counter::class) + ->assertSet('count', 0) + ->call('increment') + ->assertSet('count', 1) + ->assertSee(1) + ->assertStatus(200); + + + + $this->get('/posts/create') + ->assertSeeLivewire(CreatePost::class); + + +=== pint/core rules === + +## Laravel Pint Code Formatter + +- You must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style. +- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues. + +=== phpunit/core rules === + +## PHPUnit + +- This application uses PHPUnit for testing. All tests must be written as PHPUnit classes. Use `php artisan make:test --phpunit {name}` to create a new test. +- If you see a test using "Pest", convert it to PHPUnit. +- Every time a test has been updated, run that singular test. +- When the tests relating to your feature are passing, ask the user if they would like to also run the entire test suite to make sure everything is still passing. +- Tests should test all of the happy paths, failure paths, and weird paths. +- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files; these are core to the application. + +### Running Tests +- Run the minimal number of tests, using an appropriate filter, before finalizing. +- To run all tests: `php artisan test --compact`. +- To run all tests in a file: `php artisan test --compact tests/Feature/ExampleTest.php`. +- To filter on a particular test name: `php artisan test --compact --filter=testName` (recommended after making a change to a related file). + +=== tailwindcss/core rules === + +## Tailwind CSS + +- Use Tailwind CSS classes to style HTML; check and use existing Tailwind conventions within the project before writing your own. +- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc.). +- Think through class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child carefully to limit repetition, and group elements logically. +- You can use the `search-docs` tool to get exact examples from the official documentation when needed. + +### Spacing +- When listing items, use gap utilities for spacing; don't use margins. + + +
+
Superior
+
Michigan
+
Erie
+
+
+ +### Dark Mode +- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`. + +=== tailwindcss/v4 rules === + +## Tailwind CSS 4 + +- Always use Tailwind CSS v4; do not use the deprecated utilities. +- `corePlugins` is not supported in Tailwind v4. +- In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed. + + +@theme { + --color-brand: oklch(0.72 0.11 178); +} + + +- In Tailwind v4, you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3: + + + - @tailwind base; + - @tailwind components; + - @tailwind utilities; + + @import "tailwindcss"; + + +### Replaced Utilities +- Tailwind v4 removed deprecated utilities. Do not use the deprecated option; use the replacement. +- Opacity values are still numeric. + +| Deprecated | Replacement | +|------------+--------------| +| bg-opacity-* | bg-black/* | +| text-opacity-* | text-black/* | +| border-opacity-* | border-black/* | +| divide-opacity-* | divide-black/* | +| ring-opacity-* | ring-black/* | +| placeholder-opacity-* | placeholder-black/* | +| flex-shrink-* | shrink-* | +| flex-grow-* | grow-* | +| overflow-ellipsis | text-ellipsis | +| decoration-slice | box-decoration-slice | +| decoration-clone | box-decoration-clone | +
diff --git a/.junie/mcp/mcp.json b/.junie/mcp/mcp.json new file mode 100644 index 00000000..0fd7d4ef --- /dev/null +++ b/.junie/mcp/mcp.json @@ -0,0 +1,8 @@ +{ + "mcpServers": { + "laravel-boost": { + "command": "php", + "args": ["./artisan", "boost:mcp"] + } + } +} diff --git a/.mcp.json b/.mcp.json new file mode 100644 index 00000000..a68c0f44 --- /dev/null +++ b/.mcp.json @@ -0,0 +1,15 @@ +{ + "mcpServers": { + "laravel-boost": { + "command": "php", + "args": [ + "./artisan", + "boost:mcp" + ] + }, + "nightwatch": { + "type": "http", + "url": "https://nightwatch.laravel.com/mcp" + } + } +} \ No newline at end of file diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..705d41a7 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +**/dist +**/package-lock.json +public/build \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 00000000..35a144fa --- /dev/null +++ b/.prettierrc @@ -0,0 +1,16 @@ +{ + "plugins": ["prettier-plugin-blade", "prettier-plugin-tailwindcss"], + "overrides": [ + { + "files": ["*.blade.php"], + "options": { + "parser": "blade" + } + } + ], + "tailwindStylesheet": "./resources/css/app.css", + "singleQuote": true, + "semi": false, + "trailingComma": "all", + "singleAttributePerLine": true +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..d5a3f494 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,364 @@ + +=== foundation rules === + +# Laravel Boost Guidelines + +The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications. + +## Foundational Context +This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. + +- php - 8.4.19 +- filament/filament (FILAMENT) - v5 +- laravel/cashier (CASHIER) - v15 +- laravel/framework (LARAVEL) - v12 +- laravel/horizon (HORIZON) - v5 +- laravel/nightwatch (NIGHTWATCH) - v1 +- laravel/pennant (PENNANT) - v1 +- laravel/prompts (PROMPTS) - v0 +- laravel/sanctum (SANCTUM) - v4 +- laravel/socialite (SOCIALITE) - v5 +- livewire/flux (FLUXUI_FREE) - v2 +- livewire/flux-pro (FLUXUI_PRO) - v2 +- livewire/livewire (LIVEWIRE) - v4 +- laravel/mcp (MCP) - v0 +- laravel/pint (PINT) - v1 +- laravel/sail (SAIL) - v1 +- phpunit/phpunit (PHPUNIT) - v11 +- rector/rector (RECTOR) - v2 +- alpinejs (ALPINEJS) - v3 +- prettier (PRETTIER) - v3 +- tailwindcss (TAILWINDCSS) - v4 + +## Conventions +- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming. +- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. +- Check for existing components to reuse before writing a new one. + +## Verification Scripts +- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important. + +## Application Structure & Architecture +- Stick to existing directory structure; don't create new base folders without approval. +- Do not change the application's dependencies without approval. + +## Frontend Bundling +- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them. + +## Replies +- Be concise in your explanations - focus on what's important rather than explaining obvious details. + +## Documentation Files +- You must only create documentation files if explicitly requested by the user. + +=== boost rules === + +## Laravel Boost +- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them. + +## Artisan +- Use the `list-artisan-commands` tool when you need to call an Artisan command to double-check the available parameters. + +## URLs +- Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port. + +## Tinker / Debugging +- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly. +- Use the `database-query` tool when you only need to read from the database. + +## Reading Browser Logs With the `browser-logs` Tool +- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost. +- Only recent browser logs will be useful - ignore old logs. + +## Searching Documentation (Critically Important) +- Boost comes with a powerful `search-docs` tool you should use before any other approaches when dealing with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages. +- The `search-docs` tool is perfect for all Laravel-related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc. +- You must use this tool to search for Laravel ecosystem documentation before falling back to other approaches. +- Search the documentation before making code changes to ensure we are taking the correct approach. +- Use multiple, broad, simple, topic-based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`. +- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`. + +### Available Search Syntax +- You can and should pass multiple queries at once. The most relevant results will be returned first. + +1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'. +2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit". +3. Quoted Phrases (Exact Position) - query="infinite scroll" - words must be adjacent and in that order. +4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit". +5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms. + +=== php rules === + +## PHP + +- Always use curly braces for control structures, even if it has one line. + +### Constructors +- Use PHP 8 constructor property promotion in `__construct()`. + - public function __construct(public GitHub $github) { } +- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private. + +### Type Declarations +- Always use explicit return type declarations for methods and functions. +- Use appropriate PHP type hints for method parameters. + + +protected function isAccessible(User $user, ?string $path = null): bool +{ + ... +} + + +## Comments +- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless there is something very complex going on. + +## PHPDoc Blocks +- Add useful array shape type definitions for arrays when appropriate. + +## Enums +- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`. + +=== herd rules === + +## Laravel Herd + +- The application is served by Laravel Herd and will be available at: `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate URLs for the user to ensure valid URLs. +- You must not run any commands to make the site available via HTTP(S). It is always available through Laravel Herd. + +=== tests rules === + +## Test Enforcement + +- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. +- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter. + +=== laravel/core rules === + +## Do Things the Laravel Way + +- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool. +- If you're creating a generic PHP class, use `php artisan make:class`. +- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. + +### Database +- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins. +- Use Eloquent models and relationships before suggesting raw database queries. +- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them. +- Generate code that prevents N+1 query problems by using eager loading. +- Use Laravel's query builder for very complex database operations. + +### Model Creation +- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`. + +### APIs & Eloquent Resources +- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. + +### Controllers & Validation +- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages. +- Check sibling Form Requests to see if the application uses array or string based validation rules. + +### Queues +- Use queued jobs for time-consuming operations with the `ShouldQueue` interface. + +### Authentication & Authorization +- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.). + +### URL Generation +- When generating links to other pages, prefer named routes and the `route()` function. + +### Configuration +- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`. + +### Testing +- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model. +- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`. +- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. + +### Vite Error +- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`. + +=== laravel/v12 rules === + +## Laravel 12 + +- Use the `search-docs` tool to get version-specific documentation. +- This project upgraded from Laravel 10 without migrating to the new streamlined Laravel file structure. +- This is **perfectly fine** and recommended by Laravel. Follow the existing structure from Laravel 10. We do not need to migrate to the new Laravel structure unless the user explicitly requests it. + +### Laravel 10 Structure +- Middleware typically lives in `app/Http/Middleware/` and service providers in `app/Providers/`. +- There is no `bootstrap/app.php` application configuration in a Laravel 10 structure: + - Middleware registration happens in `app/Http/Kernel.php` + - Exception handling is in `app/Exceptions/Handler.php` + - Console commands and schedule register in `app/Console/Kernel.php` + - Rate limits likely exist in `RouteServiceProvider` or `app/Http/Kernel.php` + +### Database +- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost. +- Laravel 12 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`. + +### Models +- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models. + +=== pennant/core rules === + +## Laravel Pennant + +- This application uses Laravel Pennant for feature flag management, providing a flexible system for controlling feature availability across different organizations and user types. +- Use the `search-docs` tool, in combination with existing codebase conventions, to assist the user effectively with feature flags. + +=== fluxui-pro/core rules === + +## Flux UI Pro + +- This project is using the Pro version of Flux UI. It has full access to the free components and variants, as well as full access to the Pro components and variants. +- Flux UI is a component library for Livewire. Flux is a robust, hand-crafted UI component library for your Livewire applications. It's built using Tailwind CSS and provides a set of components that are easy to use and customize. +- You should use Flux UI components when available. +- Fallback to standard Blade components if Flux is unavailable. +- If available, use the `search-docs` tool to get the exact documentation and code snippets available for this project. +- Flux UI components look like this: + + + + + +### Available Components +This is correct as of Boost installation, but there may be additional components within the codebase. + + +accordion, autocomplete, avatar, badge, brand, breadcrumbs, button, calendar, callout, card, chart, checkbox, command, composer, context, date-picker, dropdown, editor, field, file-upload, heading, icon, input, kanban, modal, navbar, otp-input, pagination, pillbox, popover, profile, radio, select, separator, skeleton, slider, switch, table, tabs, text, textarea, time-picker, toast, tooltip + + +=== livewire/core rules === + +## Livewire + +- Use the `search-docs` tool to find exact version-specific documentation for how to write Livewire and Livewire tests. +- Use the `php artisan make:livewire [Posts\CreatePost]` Artisan command to create new components. +- State should live on the server, with the UI reflecting it. +- All Livewire requests hit the Laravel backend; they're like regular HTTP requests. Always validate form data and run authorization checks in Livewire actions. + +## Livewire Best Practices +- Livewire components require a single root element. +- Use `wire:loading` and `wire:dirty` for delightful loading states. +- Add `wire:key` in loops: + + ```blade + @foreach ($items as $item) +
+ {{ $item->name }} +
+ @endforeach + ``` + +- Prefer lifecycle hooks like `mount()`, `updatedFoo()` for initialization and reactive side effects: + + + public function mount(User $user) { $this->user = $user; } + public function updatedSearch() { $this->resetPage(); } + + +## Testing Livewire + + + Livewire::test(Counter::class) + ->assertSet('count', 0) + ->call('increment') + ->assertSet('count', 1) + ->assertSee(1) + ->assertStatus(200); + + + + $this->get('/posts/create') + ->assertSeeLivewire(CreatePost::class); + + +=== pint/core rules === + +## Laravel Pint Code Formatter + +- You must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style. +- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues. + +=== phpunit/core rules === + +## PHPUnit + +- This application uses PHPUnit for testing. All tests must be written as PHPUnit classes. Use `php artisan make:test --phpunit {name}` to create a new test. +- If you see a test using "Pest", convert it to PHPUnit. +- Every time a test has been updated, run that singular test. +- When the tests relating to your feature are passing, ask the user if they would like to also run the entire test suite to make sure everything is still passing. +- Tests should test all of the happy paths, failure paths, and weird paths. +- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files; these are core to the application. + +### Running Tests +- Run the minimal number of tests, using an appropriate filter, before finalizing. +- To run all tests: `php artisan test --compact`. +- To run all tests in a file: `php artisan test --compact tests/Feature/ExampleTest.php`. +- To filter on a particular test name: `php artisan test --compact --filter=testName` (recommended after making a change to a related file). + +=== tailwindcss/core rules === + +## Tailwind CSS + +- Use Tailwind CSS classes to style HTML; check and use existing Tailwind conventions within the project before writing your own. +- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc.). +- Think through class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child carefully to limit repetition, and group elements logically. +- You can use the `search-docs` tool to get exact examples from the official documentation when needed. + +### Spacing +- When listing items, use gap utilities for spacing; don't use margins. + + +
+
Superior
+
Michigan
+
Erie
+
+
+ +### Dark Mode +- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`. + +=== tailwindcss/v4 rules === + +## Tailwind CSS 4 + +- Always use Tailwind CSS v4; do not use the deprecated utilities. +- `corePlugins` is not supported in Tailwind v4. +- In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed. + + +@theme { + --color-brand: oklch(0.72 0.11 178); +} + + +- In Tailwind v4, you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3: + + + - @tailwind base; + - @tailwind components; + - @tailwind utilities; + + @import "tailwindcss"; + + +### Replaced Utilities +- Tailwind v4 removed deprecated utilities. Do not use the deprecated option; use the replacement. +- Opacity values are still numeric. + +| Deprecated | Replacement | +|------------+--------------| +| bg-opacity-* | bg-black/* | +| text-opacity-* | text-black/* | +| border-opacity-* | border-black/* | +| divide-opacity-* | divide-black/* | +| ring-opacity-* | ring-black/* | +| placeholder-opacity-* | placeholder-black/* | +| flex-shrink-* | shrink-* | +| flex-grow-* | grow-* | +| overflow-ellipsis | text-ellipsis | +| decoration-slice | box-decoration-slice | +| decoration-clone | box-decoration-clone | +
diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..d5a3f494 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,364 @@ + +=== foundation rules === + +# Laravel Boost Guidelines + +The Laravel Boost guidelines are specifically curated by Laravel maintainers for this application. These guidelines should be followed closely to enhance the user's satisfaction building Laravel applications. + +## Foundational Context +This application is a Laravel application and its main Laravel ecosystems package & versions are below. You are an expert with them all. Ensure you abide by these specific packages & versions. + +- php - 8.4.19 +- filament/filament (FILAMENT) - v5 +- laravel/cashier (CASHIER) - v15 +- laravel/framework (LARAVEL) - v12 +- laravel/horizon (HORIZON) - v5 +- laravel/nightwatch (NIGHTWATCH) - v1 +- laravel/pennant (PENNANT) - v1 +- laravel/prompts (PROMPTS) - v0 +- laravel/sanctum (SANCTUM) - v4 +- laravel/socialite (SOCIALITE) - v5 +- livewire/flux (FLUXUI_FREE) - v2 +- livewire/flux-pro (FLUXUI_PRO) - v2 +- livewire/livewire (LIVEWIRE) - v4 +- laravel/mcp (MCP) - v0 +- laravel/pint (PINT) - v1 +- laravel/sail (SAIL) - v1 +- phpunit/phpunit (PHPUNIT) - v11 +- rector/rector (RECTOR) - v2 +- alpinejs (ALPINEJS) - v3 +- prettier (PRETTIER) - v3 +- tailwindcss (TAILWINDCSS) - v4 + +## Conventions +- You must follow all existing code conventions used in this application. When creating or editing a file, check sibling files for the correct structure, approach, and naming. +- Use descriptive names for variables and methods. For example, `isRegisteredForDiscounts`, not `discount()`. +- Check for existing components to reuse before writing a new one. + +## Verification Scripts +- Do not create verification scripts or tinker when tests cover that functionality and prove it works. Unit and feature tests are more important. + +## Application Structure & Architecture +- Stick to existing directory structure; don't create new base folders without approval. +- Do not change the application's dependencies without approval. + +## Frontend Bundling +- If the user doesn't see a frontend change reflected in the UI, it could mean they need to run `npm run build`, `npm run dev`, or `composer run dev`. Ask them. + +## Replies +- Be concise in your explanations - focus on what's important rather than explaining obvious details. + +## Documentation Files +- You must only create documentation files if explicitly requested by the user. + +=== boost rules === + +## Laravel Boost +- Laravel Boost is an MCP server that comes with powerful tools designed specifically for this application. Use them. + +## Artisan +- Use the `list-artisan-commands` tool when you need to call an Artisan command to double-check the available parameters. + +## URLs +- Whenever you share a project URL with the user, you should use the `get-absolute-url` tool to ensure you're using the correct scheme, domain/IP, and port. + +## Tinker / Debugging +- You should use the `tinker` tool when you need to execute PHP to debug code or query Eloquent models directly. +- Use the `database-query` tool when you only need to read from the database. + +## Reading Browser Logs With the `browser-logs` Tool +- You can read browser logs, errors, and exceptions using the `browser-logs` tool from Boost. +- Only recent browser logs will be useful - ignore old logs. + +## Searching Documentation (Critically Important) +- Boost comes with a powerful `search-docs` tool you should use before any other approaches when dealing with Laravel or Laravel ecosystem packages. This tool automatically passes a list of installed packages and their versions to the remote Boost API, so it returns only version-specific documentation for the user's circumstance. You should pass an array of packages to filter on if you know you need docs for particular packages. +- The `search-docs` tool is perfect for all Laravel-related packages, including Laravel, Inertia, Livewire, Filament, Tailwind, Pest, Nova, Nightwatch, etc. +- You must use this tool to search for Laravel ecosystem documentation before falling back to other approaches. +- Search the documentation before making code changes to ensure we are taking the correct approach. +- Use multiple, broad, simple, topic-based queries to start. For example: `['rate limiting', 'routing rate limiting', 'routing']`. +- Do not add package names to queries; package information is already shared. For example, use `test resource table`, not `filament 4 test resource table`. + +### Available Search Syntax +- You can and should pass multiple queries at once. The most relevant results will be returned first. + +1. Simple Word Searches with auto-stemming - query=authentication - finds 'authenticate' and 'auth'. +2. Multiple Words (AND Logic) - query=rate limit - finds knowledge containing both "rate" AND "limit". +3. Quoted Phrases (Exact Position) - query="infinite scroll" - words must be adjacent and in that order. +4. Mixed Queries - query=middleware "rate limit" - "middleware" AND exact phrase "rate limit". +5. Multiple Queries - queries=["authentication", "middleware"] - ANY of these terms. + +=== php rules === + +## PHP + +- Always use curly braces for control structures, even if it has one line. + +### Constructors +- Use PHP 8 constructor property promotion in `__construct()`. + - public function __construct(public GitHub $github) { } +- Do not allow empty `__construct()` methods with zero parameters unless the constructor is private. + +### Type Declarations +- Always use explicit return type declarations for methods and functions. +- Use appropriate PHP type hints for method parameters. + + +protected function isAccessible(User $user, ?string $path = null): bool +{ + ... +} + + +## Comments +- Prefer PHPDoc blocks over inline comments. Never use comments within the code itself unless there is something very complex going on. + +## PHPDoc Blocks +- Add useful array shape type definitions for arrays when appropriate. + +## Enums +- Typically, keys in an Enum should be TitleCase. For example: `FavoritePerson`, `BestLake`, `Monthly`. + +=== herd rules === + +## Laravel Herd + +- The application is served by Laravel Herd and will be available at: `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate URLs for the user to ensure valid URLs. +- You must not run any commands to make the site available via HTTP(S). It is always available through Laravel Herd. + +=== tests rules === + +## Test Enforcement + +- Every change must be programmatically tested. Write a new test or update an existing test, then run the affected tests to make sure they pass. +- Run the minimum number of tests needed to ensure code quality and speed. Use `php artisan test --compact` with a specific filename or filter. + +=== laravel/core rules === + +## Do Things the Laravel Way + +- Use `php artisan make:` commands to create new files (i.e. migrations, controllers, models, etc.). You can list available Artisan commands using the `list-artisan-commands` tool. +- If you're creating a generic PHP class, use `php artisan make:class`. +- Pass `--no-interaction` to all Artisan commands to ensure they work without user input. You should also pass the correct `--options` to ensure correct behavior. + +### Database +- Always use proper Eloquent relationship methods with return type hints. Prefer relationship methods over raw queries or manual joins. +- Use Eloquent models and relationships before suggesting raw database queries. +- Avoid `DB::`; prefer `Model::query()`. Generate code that leverages Laravel's ORM capabilities rather than bypassing them. +- Generate code that prevents N+1 query problems by using eager loading. +- Use Laravel's query builder for very complex database operations. + +### Model Creation +- When creating new models, create useful factories and seeders for them too. Ask the user if they need any other things, using `list-artisan-commands` to check the available options to `php artisan make:model`. + +### APIs & Eloquent Resources +- For APIs, default to using Eloquent API Resources and API versioning unless existing API routes do not, then you should follow existing application convention. + +### Controllers & Validation +- Always create Form Request classes for validation rather than inline validation in controllers. Include both validation rules and custom error messages. +- Check sibling Form Requests to see if the application uses array or string based validation rules. + +### Queues +- Use queued jobs for time-consuming operations with the `ShouldQueue` interface. + +### Authentication & Authorization +- Use Laravel's built-in authentication and authorization features (gates, policies, Sanctum, etc.). + +### URL Generation +- When generating links to other pages, prefer named routes and the `route()` function. + +### Configuration +- Use environment variables only in configuration files - never use the `env()` function directly outside of config files. Always use `config('app.name')`, not `env('APP_NAME')`. + +### Testing +- When creating models for tests, use the factories for the models. Check if the factory has custom states that can be used before manually setting up the model. +- Faker: Use methods such as `$this->faker->word()` or `fake()->randomDigit()`. Follow existing conventions whether to use `$this->faker` or `fake()`. +- When creating tests, make use of `php artisan make:test [options] {name}` to create a feature test, and pass `--unit` to create a unit test. Most tests should be feature tests. + +### Vite Error +- If you receive an "Illuminate\Foundation\ViteException: Unable to locate file in Vite manifest" error, you can run `npm run build` or ask the user to run `npm run dev` or `composer run dev`. + +=== laravel/v12 rules === + +## Laravel 12 + +- Use the `search-docs` tool to get version-specific documentation. +- This project upgraded from Laravel 10 without migrating to the new streamlined Laravel file structure. +- This is **perfectly fine** and recommended by Laravel. Follow the existing structure from Laravel 10. We do not need to migrate to the new Laravel structure unless the user explicitly requests it. + +### Laravel 10 Structure +- Middleware typically lives in `app/Http/Middleware/` and service providers in `app/Providers/`. +- There is no `bootstrap/app.php` application configuration in a Laravel 10 structure: + - Middleware registration happens in `app/Http/Kernel.php` + - Exception handling is in `app/Exceptions/Handler.php` + - Console commands and schedule register in `app/Console/Kernel.php` + - Rate limits likely exist in `RouteServiceProvider` or `app/Http/Kernel.php` + +### Database +- When modifying a column, the migration must include all of the attributes that were previously defined on the column. Otherwise, they will be dropped and lost. +- Laravel 12 allows limiting eagerly loaded records natively, without external packages: `$query->latest()->limit(10);`. + +### Models +- Casts can and likely should be set in a `casts()` method on a model rather than the `$casts` property. Follow existing conventions from other models. + +=== pennant/core rules === + +## Laravel Pennant + +- This application uses Laravel Pennant for feature flag management, providing a flexible system for controlling feature availability across different organizations and user types. +- Use the `search-docs` tool, in combination with existing codebase conventions, to assist the user effectively with feature flags. + +=== fluxui-pro/core rules === + +## Flux UI Pro + +- This project is using the Pro version of Flux UI. It has full access to the free components and variants, as well as full access to the Pro components and variants. +- Flux UI is a component library for Livewire. Flux is a robust, hand-crafted UI component library for your Livewire applications. It's built using Tailwind CSS and provides a set of components that are easy to use and customize. +- You should use Flux UI components when available. +- Fallback to standard Blade components if Flux is unavailable. +- If available, use the `search-docs` tool to get the exact documentation and code snippets available for this project. +- Flux UI components look like this: + + + + + +### Available Components +This is correct as of Boost installation, but there may be additional components within the codebase. + + +accordion, autocomplete, avatar, badge, brand, breadcrumbs, button, calendar, callout, card, chart, checkbox, command, composer, context, date-picker, dropdown, editor, field, file-upload, heading, icon, input, kanban, modal, navbar, otp-input, pagination, pillbox, popover, profile, radio, select, separator, skeleton, slider, switch, table, tabs, text, textarea, time-picker, toast, tooltip + + +=== livewire/core rules === + +## Livewire + +- Use the `search-docs` tool to find exact version-specific documentation for how to write Livewire and Livewire tests. +- Use the `php artisan make:livewire [Posts\CreatePost]` Artisan command to create new components. +- State should live on the server, with the UI reflecting it. +- All Livewire requests hit the Laravel backend; they're like regular HTTP requests. Always validate form data and run authorization checks in Livewire actions. + +## Livewire Best Practices +- Livewire components require a single root element. +- Use `wire:loading` and `wire:dirty` for delightful loading states. +- Add `wire:key` in loops: + + ```blade + @foreach ($items as $item) +
+ {{ $item->name }} +
+ @endforeach + ``` + +- Prefer lifecycle hooks like `mount()`, `updatedFoo()` for initialization and reactive side effects: + + + public function mount(User $user) { $this->user = $user; } + public function updatedSearch() { $this->resetPage(); } + + +## Testing Livewire + + + Livewire::test(Counter::class) + ->assertSet('count', 0) + ->call('increment') + ->assertSet('count', 1) + ->assertSee(1) + ->assertStatus(200); + + + + $this->get('/posts/create') + ->assertSeeLivewire(CreatePost::class); + + +=== pint/core rules === + +## Laravel Pint Code Formatter + +- You must run `vendor/bin/pint --dirty --format agent` before finalizing changes to ensure your code matches the project's expected style. +- Do not run `vendor/bin/pint --test --format agent`, simply run `vendor/bin/pint --format agent` to fix any formatting issues. + +=== phpunit/core rules === + +## PHPUnit + +- This application uses PHPUnit for testing. All tests must be written as PHPUnit classes. Use `php artisan make:test --phpunit {name}` to create a new test. +- If you see a test using "Pest", convert it to PHPUnit. +- Every time a test has been updated, run that singular test. +- When the tests relating to your feature are passing, ask the user if they would like to also run the entire test suite to make sure everything is still passing. +- Tests should test all of the happy paths, failure paths, and weird paths. +- You must not remove any tests or test files from the tests directory without approval. These are not temporary or helper files; these are core to the application. + +### Running Tests +- Run the minimal number of tests, using an appropriate filter, before finalizing. +- To run all tests: `php artisan test --compact`. +- To run all tests in a file: `php artisan test --compact tests/Feature/ExampleTest.php`. +- To filter on a particular test name: `php artisan test --compact --filter=testName` (recommended after making a change to a related file). + +=== tailwindcss/core rules === + +## Tailwind CSS + +- Use Tailwind CSS classes to style HTML; check and use existing Tailwind conventions within the project before writing your own. +- Offer to extract repeated patterns into components that match the project's conventions (i.e. Blade, JSX, Vue, etc.). +- Think through class placement, order, priority, and defaults. Remove redundant classes, add classes to parent or child carefully to limit repetition, and group elements logically. +- You can use the `search-docs` tool to get exact examples from the official documentation when needed. + +### Spacing +- When listing items, use gap utilities for spacing; don't use margins. + + +
+
Superior
+
Michigan
+
Erie
+
+
+ +### Dark Mode +- If existing pages and components support dark mode, new pages and components must support dark mode in a similar way, typically using `dark:`. + +=== tailwindcss/v4 rules === + +## Tailwind CSS 4 + +- Always use Tailwind CSS v4; do not use the deprecated utilities. +- `corePlugins` is not supported in Tailwind v4. +- In Tailwind v4, configuration is CSS-first using the `@theme` directive — no separate `tailwind.config.js` file is needed. + + +@theme { + --color-brand: oklch(0.72 0.11 178); +} + + +- In Tailwind v4, you import Tailwind using a regular CSS `@import` statement, not using the `@tailwind` directives used in v3: + + + - @tailwind base; + - @tailwind components; + - @tailwind utilities; + + @import "tailwindcss"; + + +### Replaced Utilities +- Tailwind v4 removed deprecated utilities. Do not use the deprecated option; use the replacement. +- Opacity values are still numeric. + +| Deprecated | Replacement | +|------------+--------------| +| bg-opacity-* | bg-black/* | +| text-opacity-* | text-black/* | +| border-opacity-* | border-black/* | +| divide-opacity-* | divide-black/* | +| ring-opacity-* | ring-black/* | +| placeholder-opacity-* | placeholder-black/* | +| flex-shrink-* | shrink-* | +| flex-grow-* | grow-* | +| overflow-ellipsis | text-ellipsis | +| decoration-slice | box-decoration-slice | +| decoration-clone | box-decoration-clone | +
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..cc8a1a84 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,22 @@ +Thank you for contributing! + +## Getting Started + +Be sure to fork and clone the repo: `gh repo clone NativePHP/nativephp.com` + +Make a branch for what change you want to contribute and open a pull request to the upstream main branch. + +## Torchlight + +The documentation uses [torchlight.dev](torchlight.dev) for syntax highlighting. + +Without a token, you will run into an error on pages like "/docs/desktop/1/getting-started/introduction". + +To get set up: + +1. Create an account and then make a new API token [here](https://torchlight.dev/account/api-tokens). +2. Then set the API token in the .env. + +``` +TORCHLIGHT_TOKEN= +``` diff --git a/README.md b/README.md index a8e3fadc..265f003b 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,8 @@ This repo contains the content of the nativephp.com website. We welcome any contributions to improve the documentation. Please feel free to open a PR. +Also, be sure to read our [contributing guidelines](CONTRIBUTING.md) for help getting set up. + ## Issues -Please raise any issues on the [NativePHP/laravel](https://github.com/NativePHP/laravel/issues) repo. +Please raise any issues on the [NativePHP/desktop](https://github.com/nativephp/desktop/issues) repo. diff --git a/app/Actions/Licenses/DeleteLicense.php b/app/Actions/Licenses/DeleteLicense.php new file mode 100644 index 00000000..b591999b --- /dev/null +++ b/app/Actions/Licenses/DeleteLicense.php @@ -0,0 +1,23 @@ +license($license->anystack_id, $license->anystack_product_id) + ->delete(); + } + + return $license->delete(); + } +} diff --git a/app/Actions/Licenses/RotateLicenseKey.php b/app/Actions/Licenses/RotateLicenseKey.php new file mode 100644 index 00000000..b7e9584b --- /dev/null +++ b/app/Actions/Licenses/RotateLicenseKey.php @@ -0,0 +1,47 @@ +createNewAnystackLicense($license); + + $oldAnystackId = $license->anystack_id; + + $license->update([ + 'anystack_id' => $newLicenseData['id'], + 'key' => $newLicenseData['key'], + ]); + + $this->suspendOldAnystackLicense($license, $oldAnystackId); + + return $license; + } + + private function createNewAnystackLicense(License $license): array + { + return Anystack::api() + ->licenses($license->anystack_product_id) + ->create([ + 'policy_id' => $license->subscriptionType->anystackPolicyId(), + 'contact_id' => $license->user->anystack_contact_id, + ]) + ->json('data'); + } + + private function suspendOldAnystackLicense(License $license, string $oldAnystackId): void + { + Anystack::api() + ->license($oldAnystackId, $license->anystack_product_id) + ->suspend(); + } +} diff --git a/app/Actions/Licenses/SuspendLicense.php b/app/Actions/Licenses/SuspendLicense.php new file mode 100644 index 00000000..8fe4383a --- /dev/null +++ b/app/Actions/Licenses/SuspendLicense.php @@ -0,0 +1,25 @@ +license($license->anystack_id, $license->anystack_product_id) + ->suspend(); + + $license->update([ + 'is_suspended' => true, + ]); + + return $license; + } +} diff --git a/app/Actions/Licenses/UnsuspendLicense.php b/app/Actions/Licenses/UnsuspendLicense.php new file mode 100644 index 00000000..a35d5c21 --- /dev/null +++ b/app/Actions/Licenses/UnsuspendLicense.php @@ -0,0 +1,25 @@ +license($license->anystack_id, $license->anystack_product_id) + ->suspend(false); + + $license->update([ + 'is_suspended' => false, + ]); + + return $license; + } +} diff --git a/app/Actions/SubLicenses/DeleteSubLicense.php b/app/Actions/SubLicenses/DeleteSubLicense.php new file mode 100644 index 00000000..e413fe45 --- /dev/null +++ b/app/Actions/SubLicenses/DeleteSubLicense.php @@ -0,0 +1,21 @@ +license($subLicense->anystack_id, $subLicense->parentLicense->subscriptionType->anystackProductId()) + ->delete(); + + return $subLicense->delete(); + } +} diff --git a/app/Actions/SubLicenses/SuspendSubLicense.php b/app/Actions/SubLicenses/SuspendSubLicense.php new file mode 100644 index 00000000..13b81dfe --- /dev/null +++ b/app/Actions/SubLicenses/SuspendSubLicense.php @@ -0,0 +1,25 @@ +license($subLicense->anystack_id, $subLicense->parentLicense->subscriptionType->anystackProductId()) + ->suspend(); + + $subLicense->update([ + 'is_suspended' => true, + ]); + + return $subLicense; + } +} diff --git a/app/Actions/SubLicenses/UnsuspendSubLicense.php b/app/Actions/SubLicenses/UnsuspendSubLicense.php new file mode 100644 index 00000000..f15c5345 --- /dev/null +++ b/app/Actions/SubLicenses/UnsuspendSubLicense.php @@ -0,0 +1,25 @@ +license($subLicense->anystack_id, $subLicense->parentLicense->subscriptionType->anystackProductId()) + ->suspend(false); + + $subLicense->update([ + 'is_suspended' => false, + ]); + + return $subLicense; + } +} diff --git a/app/Console/Commands/BackfillSubscriptionPrices.php b/app/Console/Commands/BackfillSubscriptionPrices.php new file mode 100644 index 00000000..dfa89fa6 --- /dev/null +++ b/app/Console/Commands/BackfillSubscriptionPrices.php @@ -0,0 +1,66 @@ +get(); + + if ($subscriptions->isEmpty()) { + $this->info('No subscriptions need backfilling.'); + + return self::SUCCESS; + } + + $this->info("Backfilling {$subscriptions->count()} subscriptions..."); + + $bar = $this->output->createProgressBar($subscriptions->count()); + $bar->start(); + + $errors = 0; + + foreach ($subscriptions as $subscription) { + try { + $invoices = Cashier::stripe()->invoices->all([ + 'subscription' => $subscription->stripe_id, + 'limit' => 1, + ]); + + if (! empty($invoices->data)) { + $subscription->update([ + 'price_paid' => max(0, $invoices->data[0]->total), + ]); + } + } catch (\Exception $e) { + $errors++; + $this->newLine(); + $this->error("Failed for subscription {$subscription->stripe_id}: {$e->getMessage()}"); + } + + $bar->advance(); + Sleep::usleep(100_000); + } + + $bar->finish(); + $this->newLine(2); + + if ($errors > 0) { + $this->warn("{$errors} subscription(s) failed to backfill."); + } + + $this->info('Backfill complete.'); + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/CompUltraSubscription.php b/app/Console/Commands/CompUltraSubscription.php new file mode 100644 index 00000000..170f339f --- /dev/null +++ b/app/Console/Commands/CompUltraSubscription.php @@ -0,0 +1,59 @@ +error('STRIPE_ULTRA_COMP_PRICE_ID is not configured.'); + + return self::FAILURE; + } + + $email = $this->argument('email'); + $user = User::where('email', $email)->first(); + + if (! $user) { + $this->error("User not found: {$email}"); + + return self::FAILURE; + } + + $existingSubscription = $user->subscription('default'); + + if ($existingSubscription && $existingSubscription->active()) { + $currentPlan = 'unknown'; + + try { + $currentPlan = Subscription::fromStripePriceId( + $existingSubscription->items->first()?->stripe_price ?? $existingSubscription->stripe_price + )->name(); + } catch (\Exception) { + } + + $this->error("User already has an active {$currentPlan} subscription. Cancel it first or use swap."); + + return self::FAILURE; + } + + $user->createOrGetStripeCustomer(); + + $user->newSubscription('default', $compedPriceId)->create(); + + $this->info("Comped Ultra subscription created for {$email}."); + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/ExtendLicenseExpiryCommand.php b/app/Console/Commands/ExtendLicenseExpiryCommand.php new file mode 100644 index 00000000..8bc3de90 --- /dev/null +++ b/app/Console/Commands/ExtendLicenseExpiryCommand.php @@ -0,0 +1,47 @@ +argument('license_id'); + + // Find the license + $license = License::find($licenseId); + if (! $license) { + $this->error("License with ID {$licenseId} not found"); + + return Command::FAILURE; + } + + // Dispatch the job to update the license expiry + dispatch(new UpdateAnystackLicenseExpiryJob($license)); + + $this->info("License expiry updated to {$license->expires_at->format('Y-m-d')}"); + + return Command::SUCCESS; + } +} diff --git a/app/Console/Commands/GeneratePluginLicenseKeys.php b/app/Console/Commands/GeneratePluginLicenseKeys.php new file mode 100644 index 00000000..6045af79 --- /dev/null +++ b/app/Console/Commands/GeneratePluginLicenseKeys.php @@ -0,0 +1,65 @@ +option('force'); + + $query = User::query(); + + if (! $force) { + $query->whereNull('plugin_license_key'); + } + + $count = $query->count(); + + if ($count === 0) { + $this->info('All users already have plugin license keys.'); + + return self::SUCCESS; + } + + $this->info("Generating plugin license keys for {$count} users..."); + + $bar = $this->output->createProgressBar($count); + $bar->start(); + + $query->chunkById(100, function ($users) use ($bar): void { + foreach ($users as $user) { + $user->getPluginLicenseKey(); + $bar->advance(); + } + }); + + $bar->finish(); + $this->newLine(); + + $this->info("Generated plugin license keys for {$count} users."); + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/GrantPluginToBundleOwners.php b/app/Console/Commands/GrantPluginToBundleOwners.php new file mode 100644 index 00000000..07acb0d6 --- /dev/null +++ b/app/Console/Commands/GrantPluginToBundleOwners.php @@ -0,0 +1,118 @@ +argument('bundle'))->first(); + + if (! $bundle) { + $this->error("Bundle not found: {$this->argument('bundle')}"); + + return Command::FAILURE; + } + + $plugin = Plugin::where('name', $this->argument('plugin'))->first(); + + if (! $plugin) { + $this->error("Plugin not found: {$this->argument('plugin')}"); + + return Command::FAILURE; + } + + $dryRun = $this->option('dry-run'); + $noEmail = $this->option('no-email'); + + // Find all unique users who have purchased this bundle + // (they have at least one active PluginLicense linked to this bundle) + $userIds = PluginLicense::where('plugin_bundle_id', $bundle->id) + ->active() + ->distinct() + ->pluck('user_id'); + + $users = User::whereIn('id', $userIds)->get(); + + if ($users->isEmpty()) { + $this->warn('No users found who have purchased this bundle.'); + + return Command::SUCCESS; + } + + $this->info("Bundle: {$bundle->name} (slug: {$bundle->slug})"); + $this->info("Plugin: {$plugin->name}"); + $this->info("Users found: {$users->count()}"); + + if ($dryRun) { + $this->warn('[DRY RUN] No changes will be made.'); + } + + $this->newLine(); + + $granted = 0; + $skipped = 0; + + foreach ($users as $user) { + // Check if user already has an active license for this plugin + $existingLicense = PluginLicense::where('user_id', $user->id) + ->where('plugin_id', $plugin->id) + ->active() + ->exists(); + + if ($existingLicense) { + $this->line(" Skipped {$user->email} — already has an active license"); + $skipped++; + + continue; + } + + if (! $dryRun) { + PluginLicense::create([ + 'user_id' => $user->id, + 'plugin_id' => $plugin->id, + 'plugin_bundle_id' => $bundle->id, + 'price_paid' => 0, + 'currency' => 'USD', + 'is_grandfathered' => true, + 'purchased_at' => now(), + ]); + + if (! $noEmail) { + $user->notify( + (new BundlePluginAdded($plugin, $bundle)) + ->delay(now()->addSeconds($granted * 2)) + ); + } + } + + $this->line(" Granted to {$user->email}"); + $granted++; + } + + $this->newLine(); + $this->info("Granted: {$granted}"); + $this->info("Skipped (already licensed): {$skipped}"); + + if ($dryRun) { + $this->warn('This was a dry run. Run again without --dry-run to apply changes.'); + } + + return Command::SUCCESS; + } +} diff --git a/app/Console/Commands/GrantProduct.php b/app/Console/Commands/GrantProduct.php new file mode 100644 index 00000000..e08ab13e --- /dev/null +++ b/app/Console/Commands/GrantProduct.php @@ -0,0 +1,85 @@ +argument('product'))->first(); + + if (! $product) { + $this->error("Product not found: {$this->argument('product')}"); + + return Command::FAILURE; + } + + $user = User::where('email', $this->argument('user'))->first(); + + if (! $user) { + $this->error("User not found: {$this->argument('user')}"); + + return Command::FAILURE; + } + + $dryRun = $this->option('dry-run'); + $noEmail = $this->option('no-email'); + + $this->info("Product: {$product->name} (slug: {$product->slug})"); + $this->info("User: {$user->email}"); + + if ($dryRun) { + $this->warn('[DRY RUN] No changes will be made.'); + } + + $this->newLine(); + + // Check if user already has a license for this product + $existingLicense = ProductLicense::where('user_id', $user->id) + ->where('product_id', $product->id) + ->exists(); + + if ($existingLicense) { + $this->warn("User {$user->email} already has a license for this product."); + + return Command::SUCCESS; + } + + if (! $dryRun) { + ProductLicense::create([ + 'user_id' => $user->id, + 'product_id' => $product->id, + 'price_paid' => 0, + 'currency' => 'USD', + 'is_comped' => true, + 'purchased_at' => now(), + ]); + + if (! $noEmail) { + $user->notify(new ProductGranted($product)); + } + } + + $this->info("Granted to {$user->email}"); + + if ($dryRun) { + $this->warn('This was a dry run. Run again without --dry-run to apply changes.'); + } + + return Command::SUCCESS; + } +} diff --git a/app/Console/Commands/ImportAnystackContactsCommand.php b/app/Console/Commands/ImportAnystackContactsCommand.php new file mode 100644 index 00000000..f6a96e81 --- /dev/null +++ b/app/Console/Commands/ImportAnystackContactsCommand.php @@ -0,0 +1,23 @@ +name('import-anystack-contacts') + ->dispatch(); + } +} diff --git a/app/Console/Commands/ImportAnystackLicensesCommand.php b/app/Console/Commands/ImportAnystackLicensesCommand.php new file mode 100644 index 00000000..5793e6d8 --- /dev/null +++ b/app/Console/Commands/ImportAnystackLicensesCommand.php @@ -0,0 +1,23 @@ +name('import-anystack-licenses') + ->dispatch(); + } +} diff --git a/app/Console/Commands/MarkCompedSubscriptions.php b/app/Console/Commands/MarkCompedSubscriptions.php new file mode 100644 index 00000000..11a67e7e --- /dev/null +++ b/app/Console/Commands/MarkCompedSubscriptions.php @@ -0,0 +1,125 @@ +argument('file'); + + if (! file_exists($path)) { + $this->error("File not found: {$path}"); + + return self::FAILURE; + } + + $emails = $this->parseEmails($path); + + if (empty($emails)) { + $this->error('No valid email addresses found in the file.'); + + return self::FAILURE; + } + + $this->info('Found '.count($emails).' email(s) to process.'); + + $updated = 0; + $skipped = []; + + foreach ($emails as $email) { + $user = User::where('email', $email)->first(); + + if (! $user) { + $skipped[] = "{$email} — user not found"; + + continue; + } + + $subscription = Subscription::where('user_id', $user->id) + ->where('stripe_status', 'active') + ->first(); + + if (! $subscription) { + $skipped[] = "{$email} — no active subscription"; + + continue; + } + + if ($subscription->is_comped) { + $skipped[] = "{$email} — already marked as comped"; + + continue; + } + + $subscription->update(['is_comped' => true]); + $updated++; + $this->info("Marked {$email} as comped (subscription #{$subscription->id})"); + } + + if (count($skipped) > 0) { + $this->warn('Skipped:'); + foreach ($skipped as $reason) { + $this->warn(" - {$reason}"); + } + } + + $this->info("Done. {$updated} subscription(s) marked as comped."); + + return self::SUCCESS; + } + + /** + * Parse email addresses from a CSV file. + * Supports: plain list (one email per line), or CSV with an "email" column header. + * + * @return array + */ + private function parseEmails(string $path): array + { + $handle = fopen($path, 'r'); + + if (! $handle) { + return []; + } + + $emails = []; + $emailColumnIndex = null; + $isFirstRow = true; + + while (($row = fgetcsv($handle)) !== false) { + if ($isFirstRow) { + $isFirstRow = false; + $headers = array_map(fn ($h) => strtolower(trim($h)), $row); + $emailColumnIndex = array_search('email', $headers); + + // If the first row looks like an email itself (no header), treat it as data + if ($emailColumnIndex === false && filter_var(trim($row[0]), FILTER_VALIDATE_EMAIL)) { + $emailColumnIndex = 0; + $emails[] = strtolower(trim($row[0])); + } + + continue; + } + + $value = trim($row[$emailColumnIndex] ?? ''); + + if (filter_var($value, FILTER_VALIDATE_EMAIL)) { + $emails[] = strtolower($value); + } + } + + fclose($handle); + + return array_unique($emails); + } +} diff --git a/app/Console/Commands/MatchUsersWithStripeCustomers.php b/app/Console/Commands/MatchUsersWithStripeCustomers.php new file mode 100644 index 00000000..71ea2575 --- /dev/null +++ b/app/Console/Commands/MatchUsersWithStripeCustomers.php @@ -0,0 +1,110 @@ +count(); + + if ($totalUsers === 0) { + $this->info('No users found without Stripe IDs.'); + + return self::SUCCESS; + } + + $this->info("Found {$totalUsers} users without Stripe IDs."); + + $limit = $this->option('limit'); + if ($limit) { + $query->limit((int) $limit); + $this->info("Processing first {$limit} users..."); + } + + $dryRun = $this->option('dry-run'); + if ($dryRun) { + $this->warn('DRY RUN MODE - No changes will be made'); + } + + $users = $query->get(); + + $matched = 0; + $notFound = 0; + $errors = 0; + + $progressBar = $this->output->createProgressBar($users->count()); + $progressBar->start(); + + /** @var User $user */ + foreach ($users as $user) { + try { + /** @var Customer $customer */ + $customer = $user->findStripeCustomerRecords()->first(fn (Customer $result) => $result->next_invoice_sequence === 1); + + if ($customer) { + $matched++; + + if (! $dryRun) { + $user->update(['stripe_id' => $customer->id]); + } + + $this->newLine(); + $this->line(" ✓ Matched: {$user->email} → {$customer->id}"); + } else { + $notFound++; + $this->newLine(); + $this->line(" - No match: {$user->email}"); + } + } catch (ApiErrorException $e) { + $errors++; + $this->newLine(); + $this->error(" ✗ Error for {$user->email}: {$e->getMessage()}"); + } catch (\Exception $e) { + $errors++; + $this->newLine(); + $this->error(" ✗ Unexpected error for {$user->email}: {$e->getMessage()}"); + } + + $progressBar->advance(); + + // Add a small delay to avoid rate limiting + Sleep::usleep(100000); // 0.1 seconds + } + + $progressBar->finish(); + $this->newLine(2); + + // Summary + $this->info('Summary:'); + $this->table( + ['Status', 'Count'], + [ + ['Matched', $matched], + ['Not Found', $notFound], + ['Errors', $errors], + ['Total Processed', $users->count()], + ] + ); + + if ($dryRun) { + $this->warn('This was a dry run. Run without --dry-run to apply changes.'); + } + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/ProcessEligiblePayouts.php b/app/Console/Commands/ProcessEligiblePayouts.php new file mode 100644 index 00000000..546ea0d9 --- /dev/null +++ b/app/Console/Commands/ProcessEligiblePayouts.php @@ -0,0 +1,35 @@ +where('eligible_for_payout_at', '<=', now()) + ->get(); + + if ($eligiblePayouts->isEmpty()) { + $this->info('No eligible payouts to process.'); + + return self::SUCCESS; + } + + foreach ($eligiblePayouts as $payout) { + ProcessPayoutTransfer::dispatch($payout); + } + + $this->info("Dispatched {$eligiblePayouts->count()} payout transfer job(s)."); + + return self::SUCCESS; + } +} diff --git a/app/Console/Commands/RemoveExpiredDiscordRoles.php b/app/Console/Commands/RemoveExpiredDiscordRoles.php new file mode 100644 index 00000000..c1dbf304 --- /dev/null +++ b/app/Console/Commands/RemoveExpiredDiscordRoles.php @@ -0,0 +1,46 @@ +whereNotNull('discord_role_granted_at') + ->whereNotNull('discord_id') + ->get(); + + foreach ($users as $user) { + if (! $user->hasMaxAccess()) { + $success = $discord->removeMaxRole($user->discord_id); + + if ($success) { + $user->update([ + 'discord_role_granted_at' => null, + ]); + + $this->info("Removed Discord role for user: {$user->email} ({$user->discord_username})"); + $removed++; + } else { + $this->error("Failed to remove Discord role for user: {$user->email} ({$user->discord_username})"); + } + } + } + + $this->info("Total users with Discord role removed: {$removed}"); + + return Command::SUCCESS; + } +} diff --git a/app/Console/Commands/RemoveExpiredGitHubAccess.php b/app/Console/Commands/RemoveExpiredGitHubAccess.php new file mode 100644 index 00000000..5afbd1cc --- /dev/null +++ b/app/Console/Commands/RemoveExpiredGitHubAccess.php @@ -0,0 +1,50 @@ +whereNotNull('mobile_repo_access_granted_at') + ->whereNotNull('github_username') + ->get(); + + foreach ($users as $user) { + // Check if user still qualifies for mobile repo access + if (! $user->hasMobileRepoAccess()) { + // Remove from repository + $success = $github->removeFromMobileRepo($user->github_username); + + if ($success) { + // Clear the access timestamp + $user->update([ + 'mobile_repo_access_granted_at' => null, + ]); + + $this->info("Removed access for user: {$user->email} (@{$user->github_username})"); + $removed++; + } else { + $this->error("Failed to remove access for user: {$user->email} (@{$user->github_username})"); + } + } + } + + $this->info("Total users with access removed: {$removed}"); + + return Command::SUCCESS; + } +} diff --git a/app/Console/Commands/ResendLeadNotifications.php b/app/Console/Commands/ResendLeadNotifications.php new file mode 100644 index 00000000..2af6e5c3 --- /dev/null +++ b/app/Console/Commands/ResendLeadNotifications.php @@ -0,0 +1,43 @@ +argument('date'))->startOfDay(); + + $leads = Lead::query() + ->where('created_at', '>=', $date) + ->orderBy('created_at') + ->get(); + + if ($leads->isEmpty()) { + $this->info('No leads found from '.$date->toDateString().' onwards.'); + + return; + } + + $this->info("Found {$leads->count()} lead(s) from {$date->toDateString()} onwards."); + + foreach ($leads as $lead) { + Notification::route('mail', 'sales@nativephp.com') + ->notify(new NewLeadSubmitted($lead)); + + $this->line("Sent notification for lead: {$lead->company} ({$lead->email})"); + } + + $this->info('Done.'); + } +} diff --git a/app/Console/Commands/RetryFailedPayouts.php b/app/Console/Commands/RetryFailedPayouts.php new file mode 100644 index 00000000..78ed1517 --- /dev/null +++ b/app/Console/Commands/RetryFailedPayouts.php @@ -0,0 +1,89 @@ +option('payout-id'); + + if ($payoutId) { + $payout = PluginPayout::find($payoutId); + + if (! $payout) { + $this->error("Payout #{$payoutId} not found."); + + return self::FAILURE; + } + + if (! $payout->isFailed()) { + $this->error("Payout #{$payoutId} is not in failed status."); + + return self::FAILURE; + } + + return $this->retryPayout($payout, $stripeConnectService); + } + + $failedPayouts = PluginPayout::failed() + ->with(['pluginLicense', 'developerAccount']) + ->get(); + + if ($failedPayouts->isEmpty()) { + $this->info('No failed payouts to retry.'); + + return self::SUCCESS; + } + + $this->info("Found {$failedPayouts->count()} failed payout(s) to retry."); + + $succeeded = 0; + $failed = 0; + + foreach ($failedPayouts as $payout) { + // Reset status to pending before retrying + $payout->update(['status' => PayoutStatus::Pending]); + + if ($stripeConnectService->processTransfer($payout)) { + $this->info("Payout #{$payout->id} succeeded."); + $succeeded++; + } else { + $this->error("Payout #{$payout->id} failed again."); + $failed++; + } + } + + $this->newLine(); + $this->info("Results: {$succeeded} succeeded, {$failed} failed."); + + return $failed > 0 ? self::FAILURE : self::SUCCESS; + } + + protected function retryPayout(PluginPayout $payout, StripeConnectService $stripeConnectService): int + { + $this->info("Retrying payout #{$payout->id}..."); + + // Reset status to pending before retrying + $payout->update(['status' => PayoutStatus::Pending]); + + if ($stripeConnectService->processTransfer($payout)) { + $this->info('Payout succeeded!'); + + return self::SUCCESS; + } + + $this->error('Payout failed again.'); + + return self::FAILURE; + } +} diff --git a/app/Console/Commands/SatisBuild.php b/app/Console/Commands/SatisBuild.php new file mode 100644 index 00000000..d7b06650 --- /dev/null +++ b/app/Console/Commands/SatisBuild.php @@ -0,0 +1,77 @@ +option('plugin'); + + if ($pluginName) { + $plugin = Plugin::where('name', $pluginName)->first(); + + if (! $plugin) { + $this->error("Plugin '{$pluginName}' not found."); + + return self::FAILURE; + } + + if (! $plugin->isApproved()) { + $this->error("Plugin '{$pluginName}' is not approved."); + + return self::FAILURE; + } + + $this->info("Triggering Satis build for: {$pluginName}"); + $result = $satisService->build([$plugin]); + } else { + $this->info('Triggering Satis build for all approved plugins...'); + $result = $satisService->buildAll(); + } + + if ($result['success']) { + $this->info('Build triggered successfully!'); + $this->line("Job ID: {$result['job_id']}"); + + if (isset($result['plugins_count'])) { + $this->line("Plugins: {$result['plugins_count']}"); + } + + return self::SUCCESS; + } + + $this->error('Build trigger failed: '.$result['error']); + + if (isset($result['status'])) { + $this->line("HTTP Status: {$result['status']}"); + } + + $this->line('API URL: '.config('services.satis.url')); + $this->line('API Key configured: '.(config('services.satis.api_key') ? 'Yes' : 'No')); + + return self::FAILURE; + } +} diff --git a/app/Console/Commands/SendLegacyLicenseThankYou.php b/app/Console/Commands/SendLegacyLicenseThankYou.php new file mode 100644 index 00000000..74c7b81b --- /dev/null +++ b/app/Console/Commands/SendLegacyLicenseThankYou.php @@ -0,0 +1,83 @@ +option('dry-run'); + + if ($dryRun) { + $this->info('DRY RUN - No emails will be sent'); + } + + // Find all legacy licenses (no subscription, created before May 8, 2025) + // that have a user and haven't been converted to a subscription + $legacyLicenses = License::query() + ->whereNull('subscription_item_id') + ->where('created_at', '<', Date::create(2025, 5, 8)) + ->whereHas('user') + ->with('user') + ->get(); + + $this->info("Found {$legacyLicenses->count()} legacy license(s)"); + + // Group by user to avoid sending multiple emails to the same person + $userLicenses = $legacyLicenses->groupBy('user_id'); + + $sent = 0; + $skipped = 0; + + foreach ($userLicenses as $userId => $licenses) { + $user = $licenses->first()->user; + + if (! $user) { + $this->warn("Skipping license(s) for user ID {$userId} - user not found"); + $skipped++; + + continue; + } + + // Check if user has any active subscription (meaning they've already renewed) + $hasActiveSubscription = $user->subscriptions() + ->where('stripe_status', 'active') + ->exists(); + + if ($hasActiveSubscription) { + $this->line("Skipping {$user->email} - already has active subscription"); + $skipped++; + + continue; + } + + // Use the first (or oldest) legacy license for the email + $license = $licenses->sortBy('created_at')->first(); + + if ($dryRun) { + $this->line("Would send to: {$user->email} (License: {$license->key})"); + } else { + $user->notify(new LegacyLicenseThankYou($license)); + $this->line("Sent to: {$user->email}"); + } + + $sent++; + } + + $this->newLine(); + $this->info($dryRun ? "Would send: {$sent} email(s)" : "Sent: {$sent} email(s)"); + $this->info("Skipped: {$skipped} user(s)"); + + return Command::SUCCESS; + } +} diff --git a/app/Console/Commands/SendLicenseExpiryWarnings.php b/app/Console/Commands/SendLicenseExpiryWarnings.php new file mode 100644 index 00000000..e9241933 --- /dev/null +++ b/app/Console/Commands/SendLicenseExpiryWarnings.php @@ -0,0 +1,88 @@ +option('catch-up'); + + if ($catchUp) { + $this->info('Running in catch-up mode - sending missed warnings...'); + } + + foreach ($warningDays as $days) { + $sent = $this->sendWarningsForDays($days, $catchUp); + $totalSent += $sent; + + $this->info("Sent {$sent} warning emails for licenses expiring in {$days} day(s)"); + } + + $this->info("Total warning emails sent: {$totalSent}"); + + return Command::SUCCESS; + } + + private function sendWarningsForDays(int $days, bool $catchUp = false): int + { + $sent = 0; + + $query = License::query() + ->whereNull('subscription_item_id') // Legacy licenses without subscriptions + ->with('user'); + + if ($catchUp) { + // Catch-up mode: find licenses that are within the warning window but haven't received this warning yet + // For 30-day: expires within 30 days (but more than 7 days to avoid overlap) + // For 7-day: expires within 7 days (but more than 1 day) + // For 1-day: expires within 1 day (but hasn't expired yet) + $warningThresholds = [30 => 7, 7 => 1, 1 => 0]; + $lowerBound = $warningThresholds[$days] ?? 0; + + $query->where('expires_at', '>', now()->addDays($lowerBound)->startOfDay()) + ->where('expires_at', '<=', now()->addDays($days)->endOfDay()) + ->whereDoesntHave('expiryWarnings', function ($q) use ($days): void { + $q->where('warning_days', $days); + }); + } else { + // Normal mode: only licenses expiring on the exact target date + $targetDate = now()->addDays($days)->startOfDay(); + $query->whereDate('expires_at', $targetDate) + ->whereDoesntHave('expiryWarnings', function ($q) use ($days): void { + $q->where('warning_days', $days) + ->where('sent_at', '>=', now()->subHours(23)); // Prevent duplicate emails within 23 hours + }); + } + + $licenses = $query->get(); + + foreach ($licenses as $license) { + if ($license->user) { + $license->user->notify(new LicenseExpiryWarning($license, $days)); + + // Track that we sent this warning + $license->expiryWarnings()->create([ + 'warning_days' => $days, + 'sent_at' => now(), + ]); + + $sent++; + + $this->line("Sent {$days}-day warning to {$license->user->email} for license {$license->key}"); + } + } + + return $sent; + } +} diff --git a/app/Console/Commands/SendMaxToUltraAnnouncement.php b/app/Console/Commands/SendMaxToUltraAnnouncement.php new file mode 100644 index 00000000..1515b5a9 --- /dev/null +++ b/app/Console/Commands/SendMaxToUltraAnnouncement.php @@ -0,0 +1,59 @@ +option('dry-run'); + + if ($dryRun) { + $this->info('DRY RUN - No emails will be sent'); + } + + $maxPriceIds = array_filter([ + config('subscriptions.plans.max.stripe_price_id'), + config('subscriptions.plans.max.stripe_price_id_monthly'), + config('subscriptions.plans.max.stripe_price_id_eap'), + config('subscriptions.plans.max.stripe_price_id_discounted'), + ]); + + $users = User::query() + ->whereHas('subscriptions', function ($query) use ($maxPriceIds) { + $query->where('stripe_status', 'active') + ->where('is_comped', false) + ->whereIn('stripe_price', $maxPriceIds); + }) + ->get(); + + $this->info("Found {$users->count()} paying Max subscriber(s)"); + + $sent = 0; + + foreach ($users as $user) { + if ($dryRun) { + $this->line("Would send to: {$user->email}"); + } else { + $user->notify(new MaxToUltraAnnouncement); + $this->line("Sent to: {$user->email}"); + } + + $sent++; + } + + $this->newLine(); + $this->info($dryRun ? "Would send: {$sent} email(s)" : "Sent: {$sent} email(s)"); + + return Command::SUCCESS; + } +} diff --git a/app/Console/Commands/SendUltraLicenseHolderPromotion.php b/app/Console/Commands/SendUltraLicenseHolderPromotion.php new file mode 100644 index 00000000..29a03243 --- /dev/null +++ b/app/Console/Commands/SendUltraLicenseHolderPromotion.php @@ -0,0 +1,78 @@ +option('dry-run'); + + if ($dryRun) { + $this->info('DRY RUN - No emails will be sent'); + } + + $legacyLicenses = License::query() + ->whereNull('subscription_item_id') + ->whereHas('user') + ->with('user') + ->get(); + + // Group by user to avoid sending multiple emails to the same person + $userLicenses = $legacyLicenses->groupBy('user_id'); + + $eligible = 0; + $skipped = 0; + + foreach ($userLicenses as $userId => $licenses) { + $user = $licenses->first()->user; + + if (! $user) { + $skipped++; + + continue; + } + + // Skip users who already have an active subscription + $hasActiveSubscription = $user->subscriptions() + ->where('stripe_status', 'active') + ->exists(); + + if ($hasActiveSubscription) { + $this->line("Skipping {$user->email} - already has active subscription"); + $skipped++; + + continue; + } + + $license = $licenses->sortBy('created_at')->first(); + $planName = Subscription::from($license->policy_name)->name(); + + if ($dryRun) { + $this->line("Would send to: {$user->email} ({$planName})"); + } else { + $user->notify(new UltraLicenseHolderPromotion($planName)); + $this->line("Sent to: {$user->email} ({$planName})"); + } + + $eligible++; + } + + $this->newLine(); + $this->info("Found {$eligible} eligible license holder(s)"); + $this->info($dryRun ? "Would send: {$eligible} email(s)" : "Sent: {$eligible} email(s)"); + $this->info("Skipped: {$skipped} user(s)"); + + return Command::SUCCESS; + } +} diff --git a/app/Console/Commands/SendUltraUpgradePromotion.php b/app/Console/Commands/SendUltraUpgradePromotion.php new file mode 100644 index 00000000..4b1fcbfb --- /dev/null +++ b/app/Console/Commands/SendUltraUpgradePromotion.php @@ -0,0 +1,74 @@ +option('dry-run'); + + if ($dryRun) { + $this->info('DRY RUN - No emails will be sent'); + } + + $miniPriceIds = array_filter([ + config('subscriptions.plans.mini.stripe_price_id'), + config('subscriptions.plans.mini.stripe_price_id_eap'), + ]); + + $proPriceIds = array_filter([ + config('subscriptions.plans.pro.stripe_price_id'), + config('subscriptions.plans.pro.stripe_price_id_eap'), + config('subscriptions.plans.pro.stripe_price_id_discounted'), + ]); + + $eligiblePriceIds = array_merge($miniPriceIds, $proPriceIds); + + $users = User::query() + ->whereHas('subscriptions', function ($query) use ($eligiblePriceIds) { + $query->where('stripe_status', 'active') + ->where('is_comped', false) + ->whereIn('stripe_price', $eligiblePriceIds); + }) + ->get(); + + $this->info("Found {$users->count()} eligible subscriber(s)"); + + $sent = 0; + + foreach ($users as $user) { + $priceId = $user->subscriptions() + ->where('stripe_status', 'active') + ->where('is_comped', false) + ->whereIn('stripe_price', $eligiblePriceIds) + ->value('stripe_price'); + + $planName = Subscription::fromStripePriceId($priceId)->name(); + + if ($dryRun) { + $this->line("Would send to: {$user->email} ({$planName})"); + } else { + $user->notify(new UltraUpgradePromotion($planName)); + $this->line("Sent to: {$user->email} ({$planName})"); + } + + $sent++; + } + + $this->newLine(); + $this->info($dryRun ? "Would send: {$sent} email(s)" : "Sent: {$sent} email(s)"); + + return Command::SUCCESS; + } +} diff --git a/app/Console/Commands/SyncPlugins.php b/app/Console/Commands/SyncPlugins.php new file mode 100644 index 00000000..8406da10 --- /dev/null +++ b/app/Console/Commands/SyncPlugins.php @@ -0,0 +1,35 @@ +count(); + + if ($count === 0) { + $this->info('No plugins to sync.'); + + return self::SUCCESS; + } + + $this->info("Dispatching sync jobs for {$count} plugins..."); + + $plugins->each(fn (Plugin $plugin) => dispatch(new SyncPlugin($plugin))); + + $this->info('Done. Jobs have been dispatched to the queue.'); + + return self::SUCCESS; + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index e6b9960e..6600f5e4 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -12,7 +12,22 @@ class Kernel extends ConsoleKernel */ protected function schedule(Schedule $schedule): void { - // $schedule->command('inspire')->hourly(); + // Remove GitHub access for users with expired Max licenses + $schedule->command('github:remove-expired-access') + ->dailyAt('10:00') + ->onOneServer() + ->runInBackground(); + + // Remove Discord Max role for users with expired Max licenses + $schedule->command('discord:remove-expired-roles') + ->dailyAt('10:30') + ->onOneServer() + ->runInBackground(); + + // Process developer payouts that have passed the 15-day holding period + $schedule->command('payouts:process-eligible') + ->dailyAt('11:00') + ->onOneServer(); } /** diff --git a/app/Enums/LicenseSource.php b/app/Enums/LicenseSource.php new file mode 100644 index 00000000..9d1a7150 --- /dev/null +++ b/app/Enums/LicenseSource.php @@ -0,0 +1,11 @@ + 'Pending', + self::Transferred => 'Transferred', + self::Failed => 'Failed', + }; + } + + public function color(): string + { + return match ($this) { + self::Pending => 'yellow', + self::Transferred => 'green', + self::Failed => 'red', + }; + } +} diff --git a/app/Enums/PluginActivityType.php b/app/Enums/PluginActivityType.php new file mode 100644 index 00000000..56a26126 --- /dev/null +++ b/app/Enums/PluginActivityType.php @@ -0,0 +1,45 @@ + 'Submitted', + self::Resubmitted => 'Resubmitted', + self::Approved => 'Approved', + self::Rejected => 'Rejected', + self::DescriptionUpdated => 'Description Updated', + }; + } + + public function color(): string + { + return match ($this) { + self::Submitted => 'info', + self::Resubmitted => 'info', + self::Approved => 'success', + self::Rejected => 'danger', + self::DescriptionUpdated => 'gray', + }; + } + + public function icon(): string + { + return match ($this) { + self::Submitted => 'heroicon-o-paper-airplane', + self::Resubmitted => 'heroicon-o-arrow-path', + self::Approved => 'heroicon-o-check-circle', + self::Rejected => 'heroicon-o-x-circle', + self::DescriptionUpdated => 'heroicon-o-pencil-square', + }; + } +} diff --git a/app/Enums/PluginStatus.php b/app/Enums/PluginStatus.php new file mode 100644 index 00000000..4fd44020 --- /dev/null +++ b/app/Enums/PluginStatus.php @@ -0,0 +1,28 @@ + 'Pending Review', + self::Approved => 'Approved', + self::Rejected => 'Rejected', + }; + } + + public function color(): string + { + return match ($this) { + self::Pending => 'yellow', + self::Approved => 'green', + self::Rejected => 'red', + }; + } +} diff --git a/app/Enums/PluginTier.php b/app/Enums/PluginTier.php new file mode 100644 index 00000000..30b952ad --- /dev/null +++ b/app/Enums/PluginTier.php @@ -0,0 +1,60 @@ + 'Bronze', + self::Silver => 'Silver', + self::Gold => 'Gold', + }; + } + + public function description(): string + { + return match ($this) { + self::Bronze => 'Entry-level plugins', + self::Silver => 'Standard plugins', + self::Gold => 'Premium plugins', + }; + } + + public function color(): string + { + return match ($this) { + self::Bronze => 'warning', + self::Silver => 'gray', + self::Gold => 'success', + }; + } + + /** + * Get the price amounts (in cents) for each PriceTier. + * + * @return array + */ + public function getPrices(): array + { + return match ($this) { + self::Bronze => [ + PriceTier::Regular->value => 2900, + PriceTier::Subscriber->value => 1100, + ], + self::Silver => [ + PriceTier::Regular->value => 4900, + PriceTier::Subscriber->value => 1700, + ], + self::Gold => [ + PriceTier::Regular->value => 9900, + PriceTier::Subscriber->value => 3100, + ], + }; + } +} diff --git a/app/Enums/PluginType.php b/app/Enums/PluginType.php new file mode 100644 index 00000000..27df3858 --- /dev/null +++ b/app/Enums/PluginType.php @@ -0,0 +1,17 @@ + 'Free', + self::Paid => 'Paid', + }; + } +} diff --git a/app/Enums/PriceTier.php b/app/Enums/PriceTier.php new file mode 100644 index 00000000..b982fb42 --- /dev/null +++ b/app/Enums/PriceTier.php @@ -0,0 +1,42 @@ + 'Regular', + self::Subscriber => 'Subscriber', + self::Eap => 'Early Access', + }; + } + + public function description(): string + { + return match ($this) { + self::Regular => 'Standard pricing for all customers', + self::Subscriber => 'Discounted pricing for subscribers', + self::Eap => 'Special pricing for Early Access Program customers', + }; + } + + /** + * Get the priority order for tier selection (lower = higher priority). + * When a user qualifies for multiple tiers, the lowest priced tier wins, + * but this priority helps with display/sorting. + */ + public function priority(): int + { + return match ($this) { + self::Eap => 1, + self::Subscriber => 2, + self::Regular => 3, + }; + } +} diff --git a/app/Enums/StripeConnectStatus.php b/app/Enums/StripeConnectStatus.php new file mode 100644 index 00000000..8af1617d --- /dev/null +++ b/app/Enums/StripeConnectStatus.php @@ -0,0 +1,33 @@ + 'Pending Onboarding', + self::Active => 'Active', + self::Disabled => 'Disabled', + }; + } + + public function color(): string + { + return match ($this) { + self::Pending => 'yellow', + self::Active => 'green', + self::Disabled => 'red', + }; + } + + public function canReceivePayouts(): bool + { + return $this === self::Active; + } +} diff --git a/app/Enums/Subscription.php b/app/Enums/Subscription.php new file mode 100644 index 00000000..760caff0 --- /dev/null +++ b/app/Enums/Subscription.php @@ -0,0 +1,132 @@ +items as $item) { + $priceId = $item->price->id; + + if (self::isExtraSeatPrice($priceId)) { + continue; + } + + return self::fromStripePriceId($priceId); + } + + throw new RuntimeException('Could not resolve a plan price id from subscription items.'); + } + + public static function isExtraSeatPrice(string $priceId): bool + { + return in_array($priceId, array_filter([ + config('subscriptions.plans.max.stripe_extra_seat_price_id'), + config('subscriptions.plans.max.stripe_extra_seat_price_id_monthly'), + ])); + } + + public static function extraSeatStripePriceId(string $interval): ?string + { + return match ($interval) { + 'year' => config('subscriptions.plans.max.stripe_extra_seat_price_id'), + 'month' => config('subscriptions.plans.max.stripe_extra_seat_price_id_monthly'), + default => null, + }; + } + + public static function fromStripePriceId(string $priceId): self + { + return match ($priceId) { + config('subscriptions.plans.mini.stripe_price_id'), + config('subscriptions.plans.mini.stripe_price_id_eap') => self::Mini, + 'price_1RoZeVAyFo6rlwXqtnOViUCf', + config('subscriptions.plans.pro.stripe_price_id'), + config('subscriptions.plans.pro.stripe_price_id_discounted'), + config('subscriptions.plans.pro.stripe_price_id_eap') => self::Pro, + 'price_1RoZk0AyFo6rlwXqjkLj4hZ0', + config('subscriptions.plans.max.stripe_price_id'), + config('subscriptions.plans.max.stripe_price_id_monthly'), + config('subscriptions.plans.max.stripe_price_id_discounted'), + config('subscriptions.plans.max.stripe_price_id_eap'), + config('subscriptions.plans.max.stripe_price_id_comped') => self::Max, + default => throw new RuntimeException("Unknown Stripe price id: {$priceId}"), + }; + } + + public static function fromAnystackPolicy(string $policyId): self + { + return match ($policyId) { + config('subscriptions.plans.mini.anystack_policy_id') => self::Mini, + config('subscriptions.plans.pro.anystack_policy_id') => self::Pro, + config('subscriptions.plans.max.anystack_policy_id') => self::Max, + config('subscriptions.plans.forever.anystack_policy_id') => self::Forever, + config('subscriptions.plans.trial.anystack_policy_id') => self::Trial, + default => throw new RuntimeException("Unknown Anystack policy id: {$policyId}"), + }; + } + + public function name(): string + { + return config("subscriptions.plans.{$this->value}.name"); + } + + public function stripePriceId(bool $forceEap = false, bool $discounted = false, string $interval = 'year'): string + { + // Monthly billing uses the regular monthly price (no EAP/discounted monthly prices exist) + if ($interval === 'month') { + return config("subscriptions.plans.{$this->value}.stripe_price_id_monthly") + ?? config("subscriptions.plans.{$this->value}.stripe_price_id"); + } + + // EAP ends June 1st at midnight UTC + if (now()->isBefore('2025-06-01 00:00:00') || $forceEap) { + return config("subscriptions.plans.{$this->value}.stripe_price_id_eap"); + } + + if ($discounted) { + return config("subscriptions.plans.{$this->value}.stripe_price_id_discounted"); + } + + return config("subscriptions.plans.{$this->value}.stripe_price_id"); + } + + public function stripePaymentLink(): string + { + return config("subscriptions.plans.{$this->value}.stripe_payment_link"); + } + + public function anystackProductId(): string + { + return config("subscriptions.plans.{$this->value}.anystack_product_id"); + } + + public function anystackPolicyId(): string + { + return config("subscriptions.plans.{$this->value}.anystack_policy_id"); + } + + public function supportsSubLicenses(): bool + { + return in_array($this, [self::Pro, self::Max, self::Forever]); + } + + public function subLicenseLimit(): ?int + { + return match ($this) { + self::Pro => 9, + self::Max, self::Forever => null, // Unlimited + default => 0, + }; + } +} diff --git a/app/Enums/TeamUserRole.php b/app/Enums/TeamUserRole.php new file mode 100644 index 00000000..95fa8f29 --- /dev/null +++ b/app/Enums/TeamUserRole.php @@ -0,0 +1,9 @@ +reportable(function (Throwable $e) { - // + $this->reportable(function (Throwable $e): void { + Integration::captureUnhandledException($e); }); } } diff --git a/app/Exceptions/InvalidStateException.php b/app/Exceptions/InvalidStateException.php new file mode 100644 index 00000000..28ec36a0 --- /dev/null +++ b/app/Exceptions/InvalidStateException.php @@ -0,0 +1,10 @@ + +
-
Copied!
+
Copied!
',jp=Number.isNaN||De.isNaN;function Y(e){return typeof e=="number"&&!jp(e)}var Tl=function(t){return t>0&&t<1/0};function la(e){return typeof e>"u"}function ot(e){return ra(e)==="object"&&e!==null}var Yp=Object.prototype.hasOwnProperty;function vt(e){if(!ot(e))return!1;try{var t=e.constructor,i=t.prototype;return t&&i&&Yp.call(i,"isPrototypeOf")}catch{return!1}}function be(e){return typeof e=="function"}var qp=Array.prototype.slice;function Pl(e){return Array.from?Array.from(e):qp.call(e)}function oe(e,t){return e&&be(t)&&(Array.isArray(e)||Y(e.length)?Pl(e).forEach(function(i,a){t.call(e,i,a,e)}):ot(e)&&Object.keys(e).forEach(function(i){t.call(e,e[i],i,e)})),e}var J=Object.assign||function(t){for(var i=arguments.length,a=new Array(i>1?i-1:0),n=1;n0&&a.forEach(function(l){ot(l)&&Object.keys(l).forEach(function(o){t[o]=l[o]})}),t},$p=/\.\d*(?:0|9){12}\d*$/;function yt(e){var t=arguments.length>1&&arguments[1]!==void 0?arguments[1]:1e11;return $p.test(e)?Math.round(e*t)/t:e}var Xp=/^width|height|left|top|marginLeft|marginTop$/;function je(e,t){var i=e.style;oe(t,function(a,n){Xp.test(n)&&Y(a)&&(a="".concat(a,"px")),i[n]=a})}function Kp(e,t){return e.classList?e.classList.contains(t):e.className.indexOf(t)>-1}function pe(e,t){if(t){if(Y(e.length)){oe(e,function(a){pe(a,t)});return}if(e.classList){e.classList.add(t);return}var i=e.className.trim();i?i.indexOf(t)<0&&(e.className="".concat(i," ").concat(t)):e.className=t}}function Oe(e,t){if(t){if(Y(e.length)){oe(e,function(i){Oe(i,t)});return}if(e.classList){e.classList.remove(t);return}e.className.indexOf(t)>=0&&(e.className=e.className.replace(t,""))}}function xt(e,t,i){if(t){if(Y(e.length)){oe(e,function(a){xt(a,t,i)});return}i?pe(e,t):Oe(e,t)}}var Zp=/([a-z\d])([A-Z])/g;function va(e){return e.replace(Zp,"$1-$2").toLowerCase()}function ha(e,t){return ot(e[t])?e[t]:e.dataset?e.dataset[t]:e.getAttribute("data-".concat(va(t)))}function Wt(e,t,i){ot(i)?e[t]=i:e.dataset?e.dataset[t]=i:e.setAttribute("data-".concat(va(t)),i)}function Qp(e,t){if(ot(e[t]))try{delete e[t]}catch{e[t]=void 0}else if(e.dataset)try{delete e.dataset[t]}catch{e.dataset[t]=void 0}else e.removeAttribute("data-".concat(va(t)))}var Fl=/\s\s*/,Ol=(function(){var e=!1;if(Ti){var t=!1,i=function(){},a=Object.defineProperty({},"once",{get:function(){return e=!0,t},set:function(l){t=l}});De.addEventListener("test",i,a),De.removeEventListener("test",i,a)}return e})();function Fe(e,t,i){var a=arguments.length>3&&arguments[3]!==void 0?arguments[3]:{},n=i;t.trim().split(Fl).forEach(function(l){if(!Ol){var o=e.listeners;o&&o[l]&&o[l][i]&&(n=o[l][i],delete o[l][i],Object.keys(o[l]).length===0&&delete o[l],Object.keys(o).length===0&&delete e.listeners)}e.removeEventListener(l,n,a)})}function Se(e,t,i){var a=arguments.length>3&&arguments[3]!==void 0?arguments[3]:{},n=i;t.trim().split(Fl).forEach(function(l){if(a.once&&!Ol){var o=e.listeners,r=o===void 0?{}:o;n=function(){delete r[l][i],e.removeEventListener(l,n,a);for(var p=arguments.length,c=new Array(p),d=0;dMath.abs(i)&&(i=m)})}),i}function bi(e,t){var i=e.pageX,a=e.pageY,n={endX:i,endY:a};return t?n:xl({startX:i,startY:a},n)}function tm(e){var t=0,i=0,a=0;return oe(e,function(n){var l=n.startX,o=n.startY;t+=l,i+=o,a+=1}),t/=a,i/=a,{pageX:t,pageY:i}}function Ye(e){var t=e.aspectRatio,i=e.height,a=e.width,n=arguments.length>1&&arguments[1]!==void 0?arguments[1]:"contain",l=Tl(a),o=Tl(i);if(l&&o){var r=i*t;n==="contain"&&r>a||n==="cover"&&r90?{width:s,height:r}:{width:r,height:s}}function am(e,t,i,a){var n=t.aspectRatio,l=t.naturalWidth,o=t.naturalHeight,r=t.rotate,s=r===void 0?0:r,p=t.scaleX,c=p===void 0?1:p,d=t.scaleY,m=d===void 0?1:d,u=i.aspectRatio,g=i.naturalWidth,f=i.naturalHeight,h=a.fillColor,I=h===void 0?"transparent":h,b=a.imageSmoothingEnabled,E=b===void 0?!0:b,v=a.imageSmoothingQuality,y=v===void 0?"low":v,T=a.maxWidth,_=T===void 0?1/0:T,x=a.maxHeight,R=x===void 0?1/0:x,P=a.minWidth,z=P===void 0?0:P,A=a.minHeight,B=A===void 0?0:A,w=document.createElement("canvas"),F=w.getContext("2d"),S=Ye({aspectRatio:u,width:_,height:R}),L=Ye({aspectRatio:u,width:z,height:B},"cover"),D=Math.min(S.width,Math.max(L.width,g)),O=Math.min(S.height,Math.max(L.height,f)),U=Ye({aspectRatio:n,width:_,height:R}),C=Ye({aspectRatio:n,width:z,height:B},"cover"),X=Math.min(U.width,Math.max(C.width,l)),K=Math.min(U.height,Math.max(C.height,o)),Z=[-X/2,-K/2,X,K];return w.width=yt(D),w.height=yt(O),F.fillStyle=I,F.fillRect(0,0,D,O),F.save(),F.translate(D/2,O/2),F.rotate(s*Math.PI/180),F.scale(c,m),F.imageSmoothingEnabled=E,F.imageSmoothingQuality=y,F.drawImage.apply(F,[e].concat(Rl(Z.map(function(ce){return Math.floor(yt(ce))})))),F.restore(),w}var Cl=String.fromCharCode;function nm(e,t,i){var a="";i+=t;for(var n=t;n0;)i.push(Cl.apply(null,Pl(n.subarray(0,a)))),n=n.subarray(a);return"data:".concat(t,";base64,").concat(btoa(i.join("")))}function sm(e){var t=new DataView(e),i;try{var a,n,l;if(t.getUint8(0)===255&&t.getUint8(1)===216)for(var o=t.byteLength,r=2;r+1=8&&(l=p+d)}}}if(l){var m=t.getUint16(l,a),u,g;for(g=0;g=0?l:Al),height:Math.max(a.offsetHeight,o>=0?o:zl)};this.containerData=r,je(n,{width:r.width,height:r.height}),pe(t,Ee),Oe(n,Ee)},initCanvas:function(){var t=this.containerData,i=this.imageData,a=this.options.viewMode,n=Math.abs(i.rotate)%180===90,l=n?i.naturalHeight:i.naturalWidth,o=n?i.naturalWidth:i.naturalHeight,r=l/o,s=t.width,p=t.height;t.height*r>t.width?a===3?s=t.height*r:p=t.width/r:a===3?p=t.width/r:s=t.height*r;var c={aspectRatio:r,naturalWidth:l,naturalHeight:o,width:s,height:p};this.canvasData=c,this.limited=a===1||a===2,this.limitCanvas(!0,!0),c.width=Math.min(Math.max(c.width,c.minWidth),c.maxWidth),c.height=Math.min(Math.max(c.height,c.minHeight),c.maxHeight),c.left=(t.width-c.width)/2,c.top=(t.height-c.height)/2,c.oldLeft=c.left,c.oldTop=c.top,this.initialCanvasData=J({},c)},limitCanvas:function(t,i){var a=this.options,n=this.containerData,l=this.canvasData,o=this.cropBoxData,r=a.viewMode,s=l.aspectRatio,p=this.cropped&&o;if(t){var c=Number(a.minCanvasWidth)||0,d=Number(a.minCanvasHeight)||0;r>1?(c=Math.max(c,n.width),d=Math.max(d,n.height),r===3&&(d*s>c?c=d*s:d=c/s)):r>0&&(c?c=Math.max(c,p?o.width:0):d?d=Math.max(d,p?o.height:0):p&&(c=o.width,d=o.height,d*s>c?c=d*s:d=c/s));var m=Ye({aspectRatio:s,width:c,height:d});c=m.width,d=m.height,l.minWidth=c,l.minHeight=d,l.maxWidth=1/0,l.maxHeight=1/0}if(i)if(r>(p?0:1)){var u=n.width-l.width,g=n.height-l.height;l.minLeft=Math.min(0,u),l.minTop=Math.min(0,g),l.maxLeft=Math.max(0,u),l.maxTop=Math.max(0,g),p&&this.limited&&(l.minLeft=Math.min(o.left,o.left+(o.width-l.width)),l.minTop=Math.min(o.top,o.top+(o.height-l.height)),l.maxLeft=o.left,l.maxTop=o.top,r===2&&(l.width>=n.width&&(l.minLeft=Math.min(0,u),l.maxLeft=Math.max(0,u)),l.height>=n.height&&(l.minTop=Math.min(0,g),l.maxTop=Math.max(0,g))))}else l.minLeft=-l.width,l.minTop=-l.height,l.maxLeft=n.width,l.maxTop=n.height},renderCanvas:function(t,i){var a=this.canvasData,n=this.imageData;if(i){var l=im({width:n.naturalWidth*Math.abs(n.scaleX||1),height:n.naturalHeight*Math.abs(n.scaleY||1),degree:n.rotate||0}),o=l.width,r=l.height,s=a.width*(o/a.naturalWidth),p=a.height*(r/a.naturalHeight);a.left-=(s-a.width)/2,a.top-=(p-a.height)/2,a.width=s,a.height=p,a.aspectRatio=o/r,a.naturalWidth=o,a.naturalHeight=r,this.limitCanvas(!0,!1)}(a.width>a.maxWidth||a.widtha.maxHeight||a.heighti.width?l.height=l.width/a:l.width=l.height*a),this.cropBoxData=l,this.limitCropBox(!0,!0),l.width=Math.min(Math.max(l.width,l.minWidth),l.maxWidth),l.height=Math.min(Math.max(l.height,l.minHeight),l.maxHeight),l.width=Math.max(l.minWidth,l.width*n),l.height=Math.max(l.minHeight,l.height*n),l.left=i.left+(i.width-l.width)/2,l.top=i.top+(i.height-l.height)/2,l.oldLeft=l.left,l.oldTop=l.top,this.initialCropBoxData=J({},l)},limitCropBox:function(t,i){var a=this.options,n=this.containerData,l=this.canvasData,o=this.cropBoxData,r=this.limited,s=a.aspectRatio;if(t){var p=Number(a.minCropBoxWidth)||0,c=Number(a.minCropBoxHeight)||0,d=r?Math.min(n.width,l.width,l.width+l.left,n.width-l.left):n.width,m=r?Math.min(n.height,l.height,l.height+l.top,n.height-l.top):n.height;p=Math.min(p,n.width),c=Math.min(c,n.height),s&&(p&&c?c*s>p?c=p/s:p=c*s:p?c=p/s:c&&(p=c*s),m*s>d?m=d/s:d=m*s),o.minWidth=Math.min(p,d),o.minHeight=Math.min(c,m),o.maxWidth=d,o.maxHeight=m}i&&(r?(o.minLeft=Math.max(0,l.left),o.minTop=Math.max(0,l.top),o.maxLeft=Math.min(n.width,l.left+l.width)-o.width,o.maxTop=Math.min(n.height,l.top+l.height)-o.height):(o.minLeft=0,o.minTop=0,o.maxLeft=n.width-o.width,o.maxTop=n.height-o.height))},renderCropBox:function(){var t=this.options,i=this.containerData,a=this.cropBoxData;(a.width>a.maxWidth||a.widtha.maxHeight||a.height=i.width&&a.height>=i.height?_l:Ta),je(this.cropBox,J({width:a.width,height:a.height},Ut({translateX:a.left,translateY:a.top}))),this.cropped&&this.limited&&this.limitCanvas(!0,!0),this.disabled||this.output()},output:function(){this.preview(),Rt(this.element,pa,this.getData())}},pm={initPreview:function(){var t=this.element,i=this.crossOrigin,a=this.options.preview,n=i?this.crossOriginUrl:this.url,l=t.alt||"The image to preview",o=document.createElement("img");if(i&&(o.crossOrigin=i),o.src=n,o.alt=l,this.viewBox.appendChild(o),this.viewBoxImage=o,!!a){var r=a;typeof a=="string"?r=t.ownerDocument.querySelectorAll(a):a.querySelector&&(r=[a]),this.previews=r,oe(r,function(s){var p=document.createElement("img");Wt(s,hi,{width:s.offsetWidth,height:s.offsetHeight,html:s.innerHTML}),i&&(p.crossOrigin=i),p.src=n,p.alt=l,p.style.cssText='display:block;width:100%;height:auto;min-width:0!important;min-height:0!important;max-width:none!important;max-height:none!important;image-orientation:0deg!important;"',s.innerHTML="",s.appendChild(p)})}},resetPreview:function(){oe(this.previews,function(t){var i=ha(t,hi);je(t,{width:i.width,height:i.height}),t.innerHTML=i.html,Qp(t,hi)})},preview:function(){var t=this.imageData,i=this.canvasData,a=this.cropBoxData,n=a.width,l=a.height,o=t.width,r=t.height,s=a.left-i.left-t.left,p=a.top-i.top-t.top;!this.cropped||this.disabled||(je(this.viewBoxImage,J({width:o,height:r},Ut(J({translateX:-s,translateY:-p},t)))),oe(this.previews,function(c){var d=ha(c,hi),m=d.width,u=d.height,g=m,f=u,h=1;n&&(h=m/n,f=l*h),l&&f>u&&(h=u/l,g=n*h,f=u),je(c,{width:g,height:f}),je(c.getElementsByTagName("img")[0],J({width:o*h,height:r*h},Ut(J({translateX:-s*h,translateY:-p*h},t))))}))}},mm={bind:function(){var t=this.element,i=this.options,a=this.cropper;be(i.cropstart)&&Se(t,ga,i.cropstart),be(i.cropmove)&&Se(t,ua,i.cropmove),be(i.cropend)&&Se(t,ma,i.cropend),be(i.crop)&&Se(t,pa,i.crop),be(i.zoom)&&Se(t,fa,i.zoom),Se(a,pl,this.onCropStart=this.cropStart.bind(this)),i.zoomable&&i.zoomOnWheel&&Se(a,hl,this.onWheel=this.wheel.bind(this),{passive:!1,capture:!0}),i.toggleDragModeOnDblclick&&Se(a,dl,this.onDblclick=this.dblclick.bind(this)),Se(t.ownerDocument,ml,this.onCropMove=this.cropMove.bind(this)),Se(t.ownerDocument,ul,this.onCropEnd=this.cropEnd.bind(this)),i.responsive&&Se(window,fl,this.onResize=this.resize.bind(this))},unbind:function(){var t=this.element,i=this.options,a=this.cropper;be(i.cropstart)&&Fe(t,ga,i.cropstart),be(i.cropmove)&&Fe(t,ua,i.cropmove),be(i.cropend)&&Fe(t,ma,i.cropend),be(i.crop)&&Fe(t,pa,i.crop),be(i.zoom)&&Fe(t,fa,i.zoom),Fe(a,pl,this.onCropStart),i.zoomable&&i.zoomOnWheel&&Fe(a,hl,this.onWheel,{passive:!1,capture:!0}),i.toggleDragModeOnDblclick&&Fe(a,dl,this.onDblclick),Fe(t.ownerDocument,ml,this.onCropMove),Fe(t.ownerDocument,ul,this.onCropEnd),i.responsive&&Fe(window,fl,this.onResize)}},um={resize:function(){if(!this.disabled){var t=this.options,i=this.container,a=this.containerData,n=i.offsetWidth/a.width,l=i.offsetHeight/a.height,o=Math.abs(n-1)>Math.abs(l-1)?n:l;if(o!==1){var r,s;t.restore&&(r=this.getCanvasData(),s=this.getCropBoxData()),this.render(),t.restore&&(this.setCanvasData(oe(r,function(p,c){r[c]=p*o})),this.setCropBoxData(oe(s,function(p,c){s[c]=p*o})))}}},dblclick:function(){this.disabled||this.options.dragMode===Ml||this.setDragMode(Kp(this.dragBox,ca)?Ll:Ia)},wheel:function(t){var i=this,a=Number(this.options.wheelZoomRatio)||.1,n=1;this.disabled||(t.preventDefault(),!this.wheeling&&(this.wheeling=!0,setTimeout(function(){i.wheeling=!1},50),t.deltaY?n=t.deltaY>0?1:-1:t.wheelDelta?n=-t.wheelDelta/120:t.detail&&(n=t.detail>0?1:-1),this.zoom(-n*a,t)))},cropStart:function(t){var i=t.buttons,a=t.button;if(!(this.disabled||(t.type==="mousedown"||t.type==="pointerdown"&&t.pointerType==="mouse")&&(Y(i)&&i!==1||Y(a)&&a!==0||t.ctrlKey))){var n=this.options,l=this.pointers,o;t.changedTouches?oe(t.changedTouches,function(r){l[r.identifier]=bi(r)}):l[t.pointerId||0]=bi(t),Object.keys(l).length>1&&n.zoomable&&n.zoomOnTouch?o=wl:o=ha(t.target,Ht),Vp.test(o)&&Rt(this.element,ga,{originalEvent:t,action:o})!==!1&&(t.preventDefault(),this.action=o,this.cropping=!1,o===Sl&&(this.cropping=!0,pe(this.dragBox,Ei)))}},cropMove:function(t){var i=this.action;if(!(this.disabled||!i)){var a=this.pointers;t.preventDefault(),Rt(this.element,ua,{originalEvent:t,action:i})!==!1&&(t.changedTouches?oe(t.changedTouches,function(n){J(a[n.identifier]||{},bi(n,!0))}):J(a[t.pointerId||0]||{},bi(t,!0)),this.change(t))}},cropEnd:function(t){if(!this.disabled){var i=this.action,a=this.pointers;t.changedTouches?oe(t.changedTouches,function(n){delete a[n.identifier]}):delete a[t.pointerId||0],i&&(t.preventDefault(),Object.keys(a).length||(this.action=""),this.cropping&&(this.cropping=!1,xt(this.dragBox,Ei,this.cropped&&this.options.modal)),Rt(this.element,ma,{originalEvent:t,action:i}))}}},gm={change:function(t){var i=this.options,a=this.canvasData,n=this.containerData,l=this.cropBoxData,o=this.pointers,r=this.action,s=i.aspectRatio,p=l.left,c=l.top,d=l.width,m=l.height,u=p+d,g=c+m,f=0,h=0,I=n.width,b=n.height,E=!0,v;!s&&t.shiftKey&&(s=d&&m?d/m:1),this.limited&&(f=l.minLeft,h=l.minTop,I=f+Math.min(n.width,a.width,a.left+a.width),b=h+Math.min(n.height,a.height,a.top+a.height));var y=o[Object.keys(o)[0]],T={x:y.endX-y.startX,y:y.endY-y.startY},_=function(R){switch(R){case nt:u+T.x>I&&(T.x=I-u);break;case lt:p+T.xb&&(T.y=b-g);break}};switch(r){case Ta:p+=T.x,c+=T.y;break;case nt:if(T.x>=0&&(u>=I||s&&(c<=h||g>=b))){E=!1;break}_(nt),d+=T.x,d<0&&(r=lt,d=-d,p-=d),s&&(m=d/s,c+=(l.height-m)/2);break;case We:if(T.y<=0&&(c<=h||s&&(p<=f||u>=I))){E=!1;break}_(We),m-=T.y,c+=T.y,m<0&&(r=It,m=-m,c-=m),s&&(d=m*s,p+=(l.width-d)/2);break;case lt:if(T.x<=0&&(p<=f||s&&(c<=h||g>=b))){E=!1;break}_(lt),d-=T.x,p+=T.x,d<0&&(r=nt,d=-d,p-=d),s&&(m=d/s,c+=(l.height-m)/2);break;case It:if(T.y>=0&&(g>=b||s&&(p<=f||u>=I))){E=!1;break}_(It),m+=T.y,m<0&&(r=We,m=-m,c-=m),s&&(d=m*s,p+=(l.width-d)/2);break;case kt:if(s){if(T.y<=0&&(c<=h||u>=I)){E=!1;break}_(We),m-=T.y,c+=T.y,d=m*s}else _(We),_(nt),T.x>=0?uh&&(m-=T.y,c+=T.y):(m-=T.y,c+=T.y);d<0&&m<0?(r=Gt,m=-m,d=-d,c-=m,p-=d):d<0?(r=Nt,d=-d,p-=d):m<0&&(r=Vt,m=-m,c-=m);break;case Nt:if(s){if(T.y<=0&&(c<=h||p<=f)){E=!1;break}_(We),m-=T.y,c+=T.y,d=m*s,p+=l.width-d}else _(We),_(lt),T.x<=0?p>f?(d-=T.x,p+=T.x):T.y<=0&&c<=h&&(E=!1):(d-=T.x,p+=T.x),T.y<=0?c>h&&(m-=T.y,c+=T.y):(m-=T.y,c+=T.y);d<0&&m<0?(r=Vt,m=-m,d=-d,c-=m,p-=d):d<0?(r=kt,d=-d,p-=d):m<0&&(r=Gt,m=-m,c-=m);break;case Gt:if(s){if(T.x<=0&&(p<=f||g>=b)){E=!1;break}_(lt),d-=T.x,p+=T.x,m=d/s}else _(It),_(lt),T.x<=0?p>f?(d-=T.x,p+=T.x):T.y>=0&&g>=b&&(E=!1):(d-=T.x,p+=T.x),T.y>=0?g=0&&(u>=I||g>=b)){E=!1;break}_(nt),d+=T.x,m=d/s}else _(It),_(nt),T.x>=0?u=0&&g>=b&&(E=!1):d+=T.x,T.y>=0?g0?r=T.y>0?Vt:kt:T.x<0&&(p-=d,r=T.y>0?Gt:Nt),T.y<0&&(c-=m),this.cropped||(Oe(this.cropBox,Ee),this.cropped=!0,this.limited&&this.limitCropBox(!0,!0));break}E&&(l.width=d,l.height=m,l.left=p,l.top=c,this.action=r,this.renderCropBox()),oe(o,function(x){x.startX=x.endX,x.startY=x.endY})}},fm={crop:function(){return this.ready&&!this.cropped&&!this.disabled&&(this.cropped=!0,this.limitCropBox(!0,!0),this.options.modal&&pe(this.dragBox,Ei),Oe(this.cropBox,Ee),this.setCropBoxData(this.initialCropBoxData)),this},reset:function(){return this.ready&&!this.disabled&&(this.imageData=J({},this.initialImageData),this.canvasData=J({},this.initialCanvasData),this.cropBoxData=J({},this.initialCropBoxData),this.renderCanvas(),this.cropped&&this.renderCropBox()),this},clear:function(){return this.cropped&&!this.disabled&&(J(this.cropBoxData,{left:0,top:0,width:0,height:0}),this.cropped=!1,this.renderCropBox(),this.limitCanvas(!0,!0),this.renderCanvas(),Oe(this.dragBox,Ei),pe(this.cropBox,Ee)),this},replace:function(t){var i=arguments.length>1&&arguments[1]!==void 0?arguments[1]:!1;return!this.disabled&&t&&(this.isImg&&(this.element.src=t),i?(this.url=t,this.image.src=t,this.ready&&(this.viewBoxImage.src=t,oe(this.previews,function(a){a.getElementsByTagName("img")[0].src=t}))):(this.isImg&&(this.replaced=!0),this.options.data=null,this.uncreate(),this.load(t))),this},enable:function(){return this.ready&&this.disabled&&(this.disabled=!1,Oe(this.cropper,sl)),this},disable:function(){return this.ready&&!this.disabled&&(this.disabled=!0,pe(this.cropper,sl)),this},destroy:function(){var t=this.element;return t[Q]?(t[Q]=void 0,this.isImg&&this.replaced&&(t.src=this.originalUrl),this.uncreate(),this):this},move:function(t){var i=arguments.length>1&&arguments[1]!==void 0?arguments[1]:t,a=this.canvasData,n=a.left,l=a.top;return this.moveTo(la(t)?t:n+Number(t),la(i)?i:l+Number(i))},moveTo:function(t){var i=arguments.length>1&&arguments[1]!==void 0?arguments[1]:t,a=this.canvasData,n=!1;return t=Number(t),i=Number(i),this.ready&&!this.disabled&&this.options.movable&&(Y(t)&&(a.left=t,n=!0),Y(i)&&(a.top=i,n=!0),n&&this.renderCanvas(!0)),this},zoom:function(t,i){var a=this.canvasData;return t=Number(t),t<0?t=1/(1-t):t=1+t,this.zoomTo(a.width*t/a.naturalWidth,null,i)},zoomTo:function(t,i,a){var n=this.options,l=this.canvasData,o=l.width,r=l.height,s=l.naturalWidth,p=l.naturalHeight;if(t=Number(t),t>=0&&this.ready&&!this.disabled&&n.zoomable){var c=s*t,d=p*t;if(Rt(this.element,fa,{ratio:t,oldRatio:o/s,originalEvent:a})===!1)return this;if(a){var m=this.pointers,u=Dl(this.cropper),g=m&&Object.keys(m).length?tm(m):{pageX:a.pageX,pageY:a.pageY};l.left-=(c-o)*((g.pageX-u.left-l.left)/o),l.top-=(d-r)*((g.pageY-u.top-l.top)/r)}else vt(i)&&Y(i.x)&&Y(i.y)?(l.left-=(c-o)*((i.x-l.left)/o),l.top-=(d-r)*((i.y-l.top)/r)):(l.left-=(c-o)/2,l.top-=(d-r)/2);l.width=c,l.height=d,this.renderCanvas(!0)}return this},rotate:function(t){return this.rotateTo((this.imageData.rotate||0)+Number(t))},rotateTo:function(t){return t=Number(t),Y(t)&&this.ready&&!this.disabled&&this.options.rotatable&&(this.imageData.rotate=t%360,this.renderCanvas(!0,!0)),this},scaleX:function(t){var i=this.imageData.scaleY;return this.scale(t,Y(i)?i:1)},scaleY:function(t){var i=this.imageData.scaleX;return this.scale(Y(i)?i:1,t)},scale:function(t){var i=arguments.length>1&&arguments[1]!==void 0?arguments[1]:t,a=this.imageData,n=!1;return t=Number(t),i=Number(i),this.ready&&!this.disabled&&this.options.scalable&&(Y(t)&&(a.scaleX=t,n=!0),Y(i)&&(a.scaleY=i,n=!0),n&&this.renderCanvas(!0,!0)),this},getData:function(){var t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:!1,i=this.options,a=this.imageData,n=this.canvasData,l=this.cropBoxData,o;if(this.ready&&this.cropped){o={x:l.left-n.left,y:l.top-n.top,width:l.width,height:l.height};var r=a.width/a.naturalWidth;if(oe(o,function(c,d){o[d]=c/r}),t){var s=Math.round(o.y+o.height),p=Math.round(o.x+o.width);o.x=Math.round(o.x),o.y=Math.round(o.y),o.width=p-o.x,o.height=s-o.y}}else o={x:0,y:0,width:0,height:0};return i.rotatable&&(o.rotate=a.rotate||0),i.scalable&&(o.scaleX=a.scaleX||1,o.scaleY=a.scaleY||1),o},setData:function(t){var i=this.options,a=this.imageData,n=this.canvasData,l={};if(this.ready&&!this.disabled&&vt(t)){var o=!1;i.rotatable&&Y(t.rotate)&&t.rotate!==a.rotate&&(a.rotate=t.rotate,o=!0),i.scalable&&(Y(t.scaleX)&&t.scaleX!==a.scaleX&&(a.scaleX=t.scaleX,o=!0),Y(t.scaleY)&&t.scaleY!==a.scaleY&&(a.scaleY=t.scaleY,o=!0)),o&&this.renderCanvas(!0,!0);var r=a.width/a.naturalWidth;Y(t.x)&&(l.left=t.x*r+n.left),Y(t.y)&&(l.top=t.y*r+n.top),Y(t.width)&&(l.width=t.width*r),Y(t.height)&&(l.height=t.height*r),this.setCropBoxData(l)}return this},getContainerData:function(){return this.ready?J({},this.containerData):{}},getImageData:function(){return this.sized?J({},this.imageData):{}},getCanvasData:function(){var t=this.canvasData,i={};return this.ready&&oe(["left","top","width","height","naturalWidth","naturalHeight"],function(a){i[a]=t[a]}),i},setCanvasData:function(t){var i=this.canvasData,a=i.aspectRatio;return this.ready&&!this.disabled&&vt(t)&&(Y(t.left)&&(i.left=t.left),Y(t.top)&&(i.top=t.top),Y(t.width)?(i.width=t.width,i.height=t.width/a):Y(t.height)&&(i.height=t.height,i.width=t.height*a),this.renderCanvas(!0)),this},getCropBoxData:function(){var t=this.cropBoxData,i;return this.ready&&this.cropped&&(i={left:t.left,top:t.top,width:t.width,height:t.height}),i||{}},setCropBoxData:function(t){var i=this.cropBoxData,a=this.options.aspectRatio,n,l;return this.ready&&this.cropped&&!this.disabled&&vt(t)&&(Y(t.left)&&(i.left=t.left),Y(t.top)&&(i.top=t.top),Y(t.width)&&t.width!==i.width&&(n=!0,i.width=t.width),Y(t.height)&&t.height!==i.height&&(l=!0,i.height=t.height),a&&(n?i.height=i.width/a:l&&(i.width=i.height*a)),this.renderCropBox()),this},getCroppedCanvas:function(){var t=arguments.length>0&&arguments[0]!==void 0?arguments[0]:{};if(!this.ready||!window.HTMLCanvasElement)return null;var i=this.canvasData,a=am(this.image,this.imageData,i,t);if(!this.cropped)return a;var n=this.getData(t.rounded),l=n.x,o=n.y,r=n.width,s=n.height,p=a.width/Math.floor(i.naturalWidth);p!==1&&(l*=p,o*=p,r*=p,s*=p);var c=r/s,d=Ye({aspectRatio:c,width:t.maxWidth||1/0,height:t.maxHeight||1/0}),m=Ye({aspectRatio:c,width:t.minWidth||0,height:t.minHeight||0},"cover"),u=Ye({aspectRatio:c,width:t.width||(p!==1?a.width:r),height:t.height||(p!==1?a.height:s)}),g=u.width,f=u.height;g=Math.min(d.width,Math.max(m.width,g)),f=Math.min(d.height,Math.max(m.height,f));var h=document.createElement("canvas"),I=h.getContext("2d");h.width=yt(g),h.height=yt(f),I.fillStyle=t.fillColor||"transparent",I.fillRect(0,0,g,f);var b=t.imageSmoothingEnabled,E=b===void 0?!0:b,v=t.imageSmoothingQuality;I.imageSmoothingEnabled=E,v&&(I.imageSmoothingQuality=v);var y=a.width,T=a.height,_=l,x=o,R,P,z,A,B,w;_<=-r||_>y?(_=0,R=0,z=0,B=0):_<=0?(z=-_,_=0,R=Math.min(y,r+_),B=R):_<=y&&(z=0,R=Math.min(r,y-_),B=R),R<=0||x<=-s||x>T?(x=0,P=0,A=0,w=0):x<=0?(A=-x,x=0,P=Math.min(T,s+x),w=P):x<=T&&(A=0,P=Math.min(s,T-x),w=P);var F=[_,x,R,P];if(B>0&&w>0){var S=g/r;F.push(z*S,A*S,B*S,w*S)}return I.drawImage.apply(I,[a].concat(Rl(F.map(function(L){return Math.floor(yt(L))})))),h},setAspectRatio:function(t){var i=this.options;return!this.disabled&&!la(t)&&(i.aspectRatio=Math.max(0,t)||NaN,this.ready&&(this.initCropBox(),this.cropped&&this.renderCropBox())),this},setDragMode:function(t){var i=this.options,a=this.dragBox,n=this.face;if(this.ready&&!this.disabled){var l=t===Ia,o=i.movable&&t===Ll;t=l||o?t:Ml,i.dragMode=t,Wt(a,Ht,t),xt(a,ca,l),xt(a,da,o),i.cropBoxMovable||(Wt(n,Ht,t),xt(n,ca,l),xt(n,da,o))}return this}},hm=De.Cropper,xa=(function(){function e(t){var i=arguments.length>1&&arguments[1]!==void 0?arguments[1]:{};if(Mp(this,e),!t||!Hp.test(t.tagName))throw new Error("The first argument is required and must be an or element.");this.element=t,this.options=J({},El,vt(i)&&i),this.cropped=!1,this.disabled=!1,this.pointers={},this.ready=!1,this.reloading=!1,this.replaced=!1,this.sized=!1,this.sizing=!1,this.init()}return Ap(e,[{key:"init",value:function(){var i=this.element,a=i.tagName.toLowerCase(),n;if(!i[Q]){if(i[Q]=this,a==="img"){if(this.isImg=!0,n=i.getAttribute("src")||"",this.originalUrl=n,!n)return;n=i.src}else a==="canvas"&&window.HTMLCanvasElement&&(n=i.toDataURL());this.load(n)}}},{key:"load",value:function(i){var a=this;if(i){this.url=i,this.imageData={};var n=this.element,l=this.options;if(!l.rotatable&&!l.scalable&&(l.checkOrientation=!1),!l.checkOrientation||!window.ArrayBuffer){this.clone();return}if(Gp.test(i)){Up.test(i)?this.read(om(i)):this.clone();return}var o=new XMLHttpRequest,r=this.clone.bind(this);this.reloading=!0,this.xhr=o,o.onabort=r,o.onerror=r,o.ontimeout=r,o.onprogress=function(){o.getResponseHeader("content-type")!==bl&&o.abort()},o.onload=function(){a.read(o.response)},o.onloadend=function(){a.reloading=!1,a.xhr=null},l.checkCrossOrigin&&Il(i)&&n.crossOrigin&&(i=vl(i)),o.open("GET",i,!0),o.responseType="arraybuffer",o.withCredentials=n.crossOrigin==="use-credentials",o.send()}}},{key:"read",value:function(i){var a=this.options,n=this.imageData,l=sm(i),o=0,r=1,s=1;if(l>1){this.url=rm(i,bl);var p=cm(l);o=p.rotate,r=p.scaleX,s=p.scaleY}a.rotatable&&(n.rotate=o),a.scalable&&(n.scaleX=r,n.scaleY=s),this.clone()}},{key:"clone",value:function(){var i=this.element,a=this.url,n=i.crossOrigin,l=a;this.options.checkCrossOrigin&&Il(a)&&(n||(n="anonymous"),l=vl(a)),this.crossOrigin=n,this.crossOriginUrl=l;var o=document.createElement("img");n&&(o.crossOrigin=n),o.src=l||a,o.alt=i.alt||"The image to crop",this.image=o,o.onload=this.start.bind(this),o.onerror=this.stop.bind(this),pe(o,cl),i.parentNode.insertBefore(o,i.nextSibling)}},{key:"start",value:function(){var i=this,a=this.image;a.onload=null,a.onerror=null,this.sizing=!0;var n=De.navigator&&/(?:iPad|iPhone|iPod).*?AppleWebKit/i.test(De.navigator.userAgent),l=function(p,c){J(i.imageData,{naturalWidth:p,naturalHeight:c,aspectRatio:p/c}),i.initialImageData=J({},i.imageData),i.sizing=!1,i.sized=!0,i.build()};if(a.naturalWidth&&!n){l(a.naturalWidth,a.naturalHeight);return}var o=document.createElement("img"),r=document.body||document.documentElement;this.sizingImage=o,o.onload=function(){l(o.width,o.height),n||r.removeChild(o)},o.src=a.src,n||(o.style.cssText="left:0;max-height:none!important;max-width:none!important;min-height:0!important;min-width:0!important;opacity:0;position:absolute;top:0;z-index:-1;",r.appendChild(o))}},{key:"stop",value:function(){var i=this.image;i.onload=null,i.onerror=null,i.parentNode.removeChild(i),this.image=null}},{key:"build",value:function(){if(!(!this.sized||this.ready)){var i=this.element,a=this.options,n=this.image,l=i.parentNode,o=document.createElement("div");o.innerHTML=Wp;var r=o.querySelector(".".concat(Q,"-container")),s=r.querySelector(".".concat(Q,"-canvas")),p=r.querySelector(".".concat(Q,"-drag-box")),c=r.querySelector(".".concat(Q,"-crop-box")),d=c.querySelector(".".concat(Q,"-face"));this.container=l,this.cropper=r,this.canvas=s,this.dragBox=p,this.cropBox=c,this.viewBox=r.querySelector(".".concat(Q,"-view-box")),this.face=d,s.appendChild(n),pe(i,Ee),l.insertBefore(r,i.nextSibling),Oe(n,cl),this.initPreview(),this.bind(),a.initialAspectRatio=Math.max(0,a.initialAspectRatio)||NaN,a.aspectRatio=Math.max(0,a.aspectRatio)||NaN,a.viewMode=Math.max(0,Math.min(3,Math.round(a.viewMode)))||0,pe(c,Ee),a.guides||pe(c.getElementsByClassName("".concat(Q,"-dashed")),Ee),a.center||pe(c.getElementsByClassName("".concat(Q,"-center")),Ee),a.background&&pe(r,"".concat(Q,"-bg")),a.highlight||pe(d,Cp),a.cropBoxMovable&&(pe(d,da),Wt(d,Ht,Ta)),a.cropBoxResizable||(pe(c.getElementsByClassName("".concat(Q,"-line")),Ee),pe(c.getElementsByClassName("".concat(Q,"-point")),Ee)),this.render(),this.ready=!0,this.setDragMode(a.dragMode),a.autoCrop&&this.crop(),this.setData(a.data),be(a.ready)&&Se(i,gl,a.ready,{once:!0}),Rt(i,gl)}}},{key:"unbuild",value:function(){if(this.ready){this.ready=!1,this.unbind(),this.resetPreview();var i=this.cropper.parentNode;i&&i.removeChild(this.cropper),Oe(this.element,Ee)}}},{key:"uncreate",value:function(){this.ready?(this.unbuild(),this.ready=!1,this.cropped=!1):this.sizing?(this.sizingImage.onload=null,this.sizing=!1,this.sized=!1):this.reloading?(this.xhr.onabort=null,this.xhr.abort()):this.image&&this.stop()}}],[{key:"noConflict",value:function(){return window.Cropper=hm,e}},{key:"setDefaults",value:function(i){J(El,vt(i)&&i)}}])})();J(xa.prototype,dm,pm,mm,um,gm,fm);var Bl={"application/prs.cww":["cww"],"application/prs.xsf+xml":["xsf"],"application/vnd.1000minds.decision-model+xml":["1km"],"application/vnd.3gpp.pic-bw-large":["plb"],"application/vnd.3gpp.pic-bw-small":["psb"],"application/vnd.3gpp.pic-bw-var":["pvb"],"application/vnd.3gpp2.tcap":["tcap"],"application/vnd.3m.post-it-notes":["pwn"],"application/vnd.accpac.simply.aso":["aso"],"application/vnd.accpac.simply.imp":["imp"],"application/vnd.acucobol":["acu"],"application/vnd.acucorp":["atc","acutc"],"application/vnd.adobe.air-application-installer-package+zip":["air"],"application/vnd.adobe.formscentral.fcdt":["fcdt"],"application/vnd.adobe.fxp":["fxp","fxpl"],"application/vnd.adobe.xdp+xml":["xdp"],"application/vnd.adobe.xfdf":["*xfdf"],"application/vnd.age":["age"],"application/vnd.ahead.space":["ahead"],"application/vnd.airzip.filesecure.azf":["azf"],"application/vnd.airzip.filesecure.azs":["azs"],"application/vnd.amazon.ebook":["azw"],"application/vnd.americandynamics.acc":["acc"],"application/vnd.amiga.ami":["ami"],"application/vnd.android.package-archive":["apk"],"application/vnd.anser-web-certificate-issue-initiation":["cii"],"application/vnd.anser-web-funds-transfer-initiation":["fti"],"application/vnd.antix.game-component":["atx"],"application/vnd.apple.installer+xml":["mpkg"],"application/vnd.apple.keynote":["key"],"application/vnd.apple.mpegurl":["m3u8"],"application/vnd.apple.numbers":["numbers"],"application/vnd.apple.pages":["pages"],"application/vnd.apple.pkpass":["pkpass"],"application/vnd.aristanetworks.swi":["swi"],"application/vnd.astraea-software.iota":["iota"],"application/vnd.audiograph":["aep"],"application/vnd.autodesk.fbx":["fbx"],"application/vnd.balsamiq.bmml+xml":["bmml"],"application/vnd.blueice.multipass":["mpm"],"application/vnd.bmi":["bmi"],"application/vnd.businessobjects":["rep"],"application/vnd.chemdraw+xml":["cdxml"],"application/vnd.chipnuts.karaoke-mmd":["mmd"],"application/vnd.cinderella":["cdy"],"application/vnd.citationstyles.style+xml":["csl"],"application/vnd.claymore":["cla"],"application/vnd.cloanto.rp9":["rp9"],"application/vnd.clonk.c4group":["c4g","c4d","c4f","c4p","c4u"],"application/vnd.cluetrust.cartomobile-config":["c11amc"],"application/vnd.cluetrust.cartomobile-config-pkg":["c11amz"],"application/vnd.commonspace":["csp"],"application/vnd.contact.cmsg":["cdbcmsg"],"application/vnd.cosmocaller":["cmc"],"application/vnd.crick.clicker":["clkx"],"application/vnd.crick.clicker.keyboard":["clkk"],"application/vnd.crick.clicker.palette":["clkp"],"application/vnd.crick.clicker.template":["clkt"],"application/vnd.crick.clicker.wordbank":["clkw"],"application/vnd.criticaltools.wbs+xml":["wbs"],"application/vnd.ctc-posml":["pml"],"application/vnd.cups-ppd":["ppd"],"application/vnd.curl.car":["car"],"application/vnd.curl.pcurl":["pcurl"],"application/vnd.dart":["dart"],"application/vnd.data-vision.rdz":["rdz"],"application/vnd.dbf":["dbf"],"application/vnd.dcmp+xml":["dcmp"],"application/vnd.dece.data":["uvf","uvvf","uvd","uvvd"],"application/vnd.dece.ttml+xml":["uvt","uvvt"],"application/vnd.dece.unspecified":["uvx","uvvx"],"application/vnd.dece.zip":["uvz","uvvz"],"application/vnd.denovo.fcselayout-link":["fe_launch"],"application/vnd.dna":["dna"],"application/vnd.dolby.mlp":["mlp"],"application/vnd.dpgraph":["dpg"],"application/vnd.dreamfactory":["dfac"],"application/vnd.ds-keypoint":["kpxx"],"application/vnd.dvb.ait":["ait"],"application/vnd.dvb.service":["svc"],"application/vnd.dynageo":["geo"],"application/vnd.ecowin.chart":["mag"],"application/vnd.enliven":["nml"],"application/vnd.epson.esf":["esf"],"application/vnd.epson.msf":["msf"],"application/vnd.epson.quickanime":["qam"],"application/vnd.epson.salt":["slt"],"application/vnd.epson.ssf":["ssf"],"application/vnd.eszigno3+xml":["es3","et3"],"application/vnd.ezpix-album":["ez2"],"application/vnd.ezpix-package":["ez3"],"application/vnd.fdf":["*fdf"],"application/vnd.fdsn.mseed":["mseed"],"application/vnd.fdsn.seed":["seed","dataless"],"application/vnd.flographit":["gph"],"application/vnd.fluxtime.clip":["ftc"],"application/vnd.framemaker":["fm","frame","maker","book"],"application/vnd.frogans.fnc":["fnc"],"application/vnd.frogans.ltf":["ltf"],"application/vnd.fsc.weblaunch":["fsc"],"application/vnd.fujitsu.oasys":["oas"],"application/vnd.fujitsu.oasys2":["oa2"],"application/vnd.fujitsu.oasys3":["oa3"],"application/vnd.fujitsu.oasysgp":["fg5"],"application/vnd.fujitsu.oasysprs":["bh2"],"application/vnd.fujixerox.ddd":["ddd"],"application/vnd.fujixerox.docuworks":["xdw"],"application/vnd.fujixerox.docuworks.binder":["xbd"],"application/vnd.fuzzysheet":["fzs"],"application/vnd.genomatix.tuxedo":["txd"],"application/vnd.geogebra.file":["ggb"],"application/vnd.geogebra.slides":["ggs"],"application/vnd.geogebra.tool":["ggt"],"application/vnd.geometry-explorer":["gex","gre"],"application/vnd.geonext":["gxt"],"application/vnd.geoplan":["g2w"],"application/vnd.geospace":["g3w"],"application/vnd.gmx":["gmx"],"application/vnd.google-apps.document":["gdoc"],"application/vnd.google-apps.drawing":["gdraw"],"application/vnd.google-apps.form":["gform"],"application/vnd.google-apps.jam":["gjam"],"application/vnd.google-apps.map":["gmap"],"application/vnd.google-apps.presentation":["gslides"],"application/vnd.google-apps.script":["gscript"],"application/vnd.google-apps.site":["gsite"],"application/vnd.google-apps.spreadsheet":["gsheet"],"application/vnd.google-earth.kml+xml":["kml"],"application/vnd.google-earth.kmz":["kmz"],"application/vnd.gov.sk.xmldatacontainer+xml":["xdcf"],"application/vnd.grafeq":["gqf","gqs"],"application/vnd.groove-account":["gac"],"application/vnd.groove-help":["ghf"],"application/vnd.groove-identity-message":["gim"],"application/vnd.groove-injector":["grv"],"application/vnd.groove-tool-message":["gtm"],"application/vnd.groove-tool-template":["tpl"],"application/vnd.groove-vcard":["vcg"],"application/vnd.hal+xml":["hal"],"application/vnd.handheld-entertainment+xml":["zmm"],"application/vnd.hbci":["hbci"],"application/vnd.hhe.lesson-player":["les"],"application/vnd.hp-hpgl":["hpgl"],"application/vnd.hp-hpid":["hpid"],"application/vnd.hp-hps":["hps"],"application/vnd.hp-jlyt":["jlt"],"application/vnd.hp-pcl":["pcl"],"application/vnd.hp-pclxl":["pclxl"],"application/vnd.hydrostatix.sof-data":["sfd-hdstx"],"application/vnd.ibm.minipay":["mpy"],"application/vnd.ibm.modcap":["afp","listafp","list3820"],"application/vnd.ibm.rights-management":["irm"],"application/vnd.ibm.secure-container":["sc"],"application/vnd.iccprofile":["icc","icm"],"application/vnd.igloader":["igl"],"application/vnd.immervision-ivp":["ivp"],"application/vnd.immervision-ivu":["ivu"],"application/vnd.insors.igm":["igm"],"application/vnd.intercon.formnet":["xpw","xpx"],"application/vnd.intergeo":["i2g"],"application/vnd.intu.qbo":["qbo"],"application/vnd.intu.qfx":["qfx"],"application/vnd.ipunplugged.rcprofile":["rcprofile"],"application/vnd.irepository.package+xml":["irp"],"application/vnd.is-xpr":["xpr"],"application/vnd.isac.fcs":["fcs"],"application/vnd.jam":["jam"],"application/vnd.jcp.javame.midlet-rms":["rms"],"application/vnd.jisp":["jisp"],"application/vnd.joost.joda-archive":["joda"],"application/vnd.kahootz":["ktz","ktr"],"application/vnd.kde.karbon":["karbon"],"application/vnd.kde.kchart":["chrt"],"application/vnd.kde.kformula":["kfo"],"application/vnd.kde.kivio":["flw"],"application/vnd.kde.kontour":["kon"],"application/vnd.kde.kpresenter":["kpr","kpt"],"application/vnd.kde.kspread":["ksp"],"application/vnd.kde.kword":["kwd","kwt"],"application/vnd.kenameaapp":["htke"],"application/vnd.kidspiration":["kia"],"application/vnd.kinar":["kne","knp"],"application/vnd.koan":["skp","skd","skt","skm"],"application/vnd.kodak-descriptor":["sse"],"application/vnd.las.las+xml":["lasxml"],"application/vnd.llamagraphics.life-balance.desktop":["lbd"],"application/vnd.llamagraphics.life-balance.exchange+xml":["lbe"],"application/vnd.lotus-1-2-3":["123"],"application/vnd.lotus-approach":["apr"],"application/vnd.lotus-freelance":["pre"],"application/vnd.lotus-notes":["nsf"],"application/vnd.lotus-organizer":["org"],"application/vnd.lotus-screencam":["scm"],"application/vnd.lotus-wordpro":["lwp"],"application/vnd.macports.portpkg":["portpkg"],"application/vnd.mapbox-vector-tile":["mvt"],"application/vnd.mcd":["mcd"],"application/vnd.medcalcdata":["mc1"],"application/vnd.mediastation.cdkey":["cdkey"],"application/vnd.mfer":["mwf"],"application/vnd.mfmp":["mfm"],"application/vnd.micrografx.flo":["flo"],"application/vnd.micrografx.igx":["igx"],"application/vnd.mif":["mif"],"application/vnd.mobius.daf":["daf"],"application/vnd.mobius.dis":["dis"],"application/vnd.mobius.mbk":["mbk"],"application/vnd.mobius.mqy":["mqy"],"application/vnd.mobius.msl":["msl"],"application/vnd.mobius.plc":["plc"],"application/vnd.mobius.txf":["txf"],"application/vnd.mophun.application":["mpn"],"application/vnd.mophun.certificate":["mpc"],"application/vnd.mozilla.xul+xml":["xul"],"application/vnd.ms-artgalry":["cil"],"application/vnd.ms-cab-compressed":["cab"],"application/vnd.ms-excel":["xls","xlm","xla","xlc","xlt","xlw"],"application/vnd.ms-excel.addin.macroenabled.12":["xlam"],"application/vnd.ms-excel.sheet.binary.macroenabled.12":["xlsb"],"application/vnd.ms-excel.sheet.macroenabled.12":["xlsm"],"application/vnd.ms-excel.template.macroenabled.12":["xltm"],"application/vnd.ms-fontobject":["eot"],"application/vnd.ms-htmlhelp":["chm"],"application/vnd.ms-ims":["ims"],"application/vnd.ms-lrm":["lrm"],"application/vnd.ms-officetheme":["thmx"],"application/vnd.ms-outlook":["msg"],"application/vnd.ms-pki.seccat":["cat"],"application/vnd.ms-pki.stl":["*stl"],"application/vnd.ms-powerpoint":["ppt","pps","pot"],"application/vnd.ms-powerpoint.addin.macroenabled.12":["ppam"],"application/vnd.ms-powerpoint.presentation.macroenabled.12":["pptm"],"application/vnd.ms-powerpoint.slide.macroenabled.12":["sldm"],"application/vnd.ms-powerpoint.slideshow.macroenabled.12":["ppsm"],"application/vnd.ms-powerpoint.template.macroenabled.12":["potm"],"application/vnd.ms-project":["*mpp","mpt"],"application/vnd.ms-visio.viewer":["vdx"],"application/vnd.ms-word.document.macroenabled.12":["docm"],"application/vnd.ms-word.template.macroenabled.12":["dotm"],"application/vnd.ms-works":["wps","wks","wcm","wdb"],"application/vnd.ms-wpl":["wpl"],"application/vnd.ms-xpsdocument":["xps"],"application/vnd.mseq":["mseq"],"application/vnd.musician":["mus"],"application/vnd.muvee.style":["msty"],"application/vnd.mynfc":["taglet"],"application/vnd.nato.bindingdataobject+xml":["bdo"],"application/vnd.neurolanguage.nlu":["nlu"],"application/vnd.nitf":["ntf","nitf"],"application/vnd.noblenet-directory":["nnd"],"application/vnd.noblenet-sealer":["nns"],"application/vnd.noblenet-web":["nnw"],"application/vnd.nokia.n-gage.ac+xml":["*ac"],"application/vnd.nokia.n-gage.data":["ngdat"],"application/vnd.nokia.n-gage.symbian.install":["n-gage"],"application/vnd.nokia.radio-preset":["rpst"],"application/vnd.nokia.radio-presets":["rpss"],"application/vnd.novadigm.edm":["edm"],"application/vnd.novadigm.edx":["edx"],"application/vnd.novadigm.ext":["ext"],"application/vnd.oasis.opendocument.chart":["odc"],"application/vnd.oasis.opendocument.chart-template":["otc"],"application/vnd.oasis.opendocument.database":["odb"],"application/vnd.oasis.opendocument.formula":["odf"],"application/vnd.oasis.opendocument.formula-template":["odft"],"application/vnd.oasis.opendocument.graphics":["odg"],"application/vnd.oasis.opendocument.graphics-template":["otg"],"application/vnd.oasis.opendocument.image":["odi"],"application/vnd.oasis.opendocument.image-template":["oti"],"application/vnd.oasis.opendocument.presentation":["odp"],"application/vnd.oasis.opendocument.presentation-template":["otp"],"application/vnd.oasis.opendocument.spreadsheet":["ods"],"application/vnd.oasis.opendocument.spreadsheet-template":["ots"],"application/vnd.oasis.opendocument.text":["odt"],"application/vnd.oasis.opendocument.text-master":["odm"],"application/vnd.oasis.opendocument.text-template":["ott"],"application/vnd.oasis.opendocument.text-web":["oth"],"application/vnd.olpc-sugar":["xo"],"application/vnd.oma.dd2+xml":["dd2"],"application/vnd.openblox.game+xml":["obgx"],"application/vnd.openofficeorg.extension":["oxt"],"application/vnd.openstreetmap.data+xml":["osm"],"application/vnd.openxmlformats-officedocument.presentationml.presentation":["pptx"],"application/vnd.openxmlformats-officedocument.presentationml.slide":["sldx"],"application/vnd.openxmlformats-officedocument.presentationml.slideshow":["ppsx"],"application/vnd.openxmlformats-officedocument.presentationml.template":["potx"],"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet":["xlsx"],"application/vnd.openxmlformats-officedocument.spreadsheetml.template":["xltx"],"application/vnd.openxmlformats-officedocument.wordprocessingml.document":["docx"],"application/vnd.openxmlformats-officedocument.wordprocessingml.template":["dotx"],"application/vnd.osgeo.mapguide.package":["mgp"],"application/vnd.osgi.dp":["dp"],"application/vnd.osgi.subsystem":["esa"],"application/vnd.palm":["pdb","pqa","oprc"],"application/vnd.pawaafile":["paw"],"application/vnd.pg.format":["str"],"application/vnd.pg.osasli":["ei6"],"application/vnd.picsel":["efif"],"application/vnd.pmi.widget":["wg"],"application/vnd.pocketlearn":["plf"],"application/vnd.powerbuilder6":["pbd"],"application/vnd.previewsystems.box":["box"],"application/vnd.procrate.brushset":["brushset"],"application/vnd.procreate.brush":["brush"],"application/vnd.procreate.dream":["drm"],"application/vnd.proteus.magazine":["mgz"],"application/vnd.publishare-delta-tree":["qps"],"application/vnd.pvi.ptid1":["ptid"],"application/vnd.pwg-xhtml-print+xml":["xhtm"],"application/vnd.quark.quarkxpress":["qxd","qxt","qwd","qwt","qxl","qxb"],"application/vnd.rar":["rar"],"application/vnd.realvnc.bed":["bed"],"application/vnd.recordare.musicxml":["mxl"],"application/vnd.recordare.musicxml+xml":["musicxml"],"application/vnd.rig.cryptonote":["cryptonote"],"application/vnd.rim.cod":["cod"],"application/vnd.rn-realmedia":["rm"],"application/vnd.rn-realmedia-vbr":["rmvb"],"application/vnd.route66.link66+xml":["link66"],"application/vnd.sailingtracker.track":["st"],"application/vnd.seemail":["see"],"application/vnd.sema":["sema"],"application/vnd.semd":["semd"],"application/vnd.semf":["semf"],"application/vnd.shana.informed.formdata":["ifm"],"application/vnd.shana.informed.formtemplate":["itp"],"application/vnd.shana.informed.interchange":["iif"],"application/vnd.shana.informed.package":["ipk"],"application/vnd.simtech-mindmapper":["twd","twds"],"application/vnd.smaf":["mmf"],"application/vnd.smart.teacher":["teacher"],"application/vnd.software602.filler.form+xml":["fo"],"application/vnd.solent.sdkm+xml":["sdkm","sdkd"],"application/vnd.spotfire.dxp":["dxp"],"application/vnd.spotfire.sfs":["sfs"],"application/vnd.stardivision.calc":["sdc"],"application/vnd.stardivision.draw":["sda"],"application/vnd.stardivision.impress":["sdd"],"application/vnd.stardivision.math":["smf"],"application/vnd.stardivision.writer":["sdw","vor"],"application/vnd.stardivision.writer-global":["sgl"],"application/vnd.stepmania.package":["smzip"],"application/vnd.stepmania.stepchart":["sm"],"application/vnd.sun.wadl+xml":["wadl"],"application/vnd.sun.xml.calc":["sxc"],"application/vnd.sun.xml.calc.template":["stc"],"application/vnd.sun.xml.draw":["sxd"],"application/vnd.sun.xml.draw.template":["std"],"application/vnd.sun.xml.impress":["sxi"],"application/vnd.sun.xml.impress.template":["sti"],"application/vnd.sun.xml.math":["sxm"],"application/vnd.sun.xml.writer":["sxw"],"application/vnd.sun.xml.writer.global":["sxg"],"application/vnd.sun.xml.writer.template":["stw"],"application/vnd.sus-calendar":["sus","susp"],"application/vnd.svd":["svd"],"application/vnd.symbian.install":["sis","sisx"],"application/vnd.syncml+xml":["xsm"],"application/vnd.syncml.dm+wbxml":["bdm"],"application/vnd.syncml.dm+xml":["xdm"],"application/vnd.syncml.dmddf+xml":["ddf"],"application/vnd.tao.intent-module-archive":["tao"],"application/vnd.tcpdump.pcap":["pcap","cap","dmp"],"application/vnd.tmobile-livetv":["tmo"],"application/vnd.trid.tpt":["tpt"],"application/vnd.triscape.mxs":["mxs"],"application/vnd.trueapp":["tra"],"application/vnd.ufdl":["ufd","ufdl"],"application/vnd.uiq.theme":["utz"],"application/vnd.umajin":["umj"],"application/vnd.unity":["unityweb"],"application/vnd.uoml+xml":["uoml","uo"],"application/vnd.vcx":["vcx"],"application/vnd.visio":["vsd","vst","vss","vsw","vsdx","vtx"],"application/vnd.visionary":["vis"],"application/vnd.vsf":["vsf"],"application/vnd.wap.wbxml":["wbxml"],"application/vnd.wap.wmlc":["wmlc"],"application/vnd.wap.wmlscriptc":["wmlsc"],"application/vnd.webturbo":["wtb"],"application/vnd.wolfram.player":["nbp"],"application/vnd.wordperfect":["wpd"],"application/vnd.wqd":["wqd"],"application/vnd.wt.stf":["stf"],"application/vnd.xara":["xar"],"application/vnd.xfdl":["xfdl"],"application/vnd.yamaha.hv-dic":["hvd"],"application/vnd.yamaha.hv-script":["hvs"],"application/vnd.yamaha.hv-voice":["hvp"],"application/vnd.yamaha.openscoreformat":["osf"],"application/vnd.yamaha.openscoreformat.osfpvg+xml":["osfpvg"],"application/vnd.yamaha.smaf-audio":["saf"],"application/vnd.yamaha.smaf-phrase":["spf"],"application/vnd.yellowriver-custom-menu":["cmp"],"application/vnd.zul":["zir","zirz"],"application/vnd.zzazz.deck+xml":["zaz"],"application/x-7z-compressed":["7z"],"application/x-abiword":["abw"],"application/x-ace-compressed":["ace"],"application/x-apple-diskimage":["*dmg"],"application/x-arj":["arj"],"application/x-authorware-bin":["aab","x32","u32","vox"],"application/x-authorware-map":["aam"],"application/x-authorware-seg":["aas"],"application/x-bcpio":["bcpio"],"application/x-bdoc":["*bdoc"],"application/x-bittorrent":["torrent"],"application/x-blender":["blend"],"application/x-blorb":["blb","blorb"],"application/x-bzip":["bz"],"application/x-bzip2":["bz2","boz"],"application/x-cbr":["cbr","cba","cbt","cbz","cb7"],"application/x-cdlink":["vcd"],"application/x-cfs-compressed":["cfs"],"application/x-chat":["chat"],"application/x-chess-pgn":["pgn"],"application/x-chrome-extension":["crx"],"application/x-cocoa":["cco"],"application/x-compressed":["*rar"],"application/x-conference":["nsc"],"application/x-cpio":["cpio"],"application/x-csh":["csh"],"application/x-debian-package":["*deb","udeb"],"application/x-dgc-compressed":["dgc"],"application/x-director":["dir","dcr","dxr","cst","cct","cxt","w3d","fgd","swa"],"application/x-doom":["wad"],"application/x-dtbncx+xml":["ncx"],"application/x-dtbook+xml":["dtb"],"application/x-dtbresource+xml":["res"],"application/x-dvi":["dvi"],"application/x-envoy":["evy"],"application/x-eva":["eva"],"application/x-font-bdf":["bdf"],"application/x-font-ghostscript":["gsf"],"application/x-font-linux-psf":["psf"],"application/x-font-pcf":["pcf"],"application/x-font-snf":["snf"],"application/x-font-type1":["pfa","pfb","pfm","afm"],"application/x-freearc":["arc"],"application/x-futuresplash":["spl"],"application/x-gca-compressed":["gca"],"application/x-glulx":["ulx"],"application/x-gnumeric":["gnumeric"],"application/x-gramps-xml":["gramps"],"application/x-gtar":["gtar"],"application/x-hdf":["hdf"],"application/x-httpd-php":["php"],"application/x-install-instructions":["install"],"application/x-ipynb+json":["ipynb"],"application/x-iso9660-image":["*iso"],"application/x-iwork-keynote-sffkey":["*key"],"application/x-iwork-numbers-sffnumbers":["*numbers"],"application/x-iwork-pages-sffpages":["*pages"],"application/x-java-archive-diff":["jardiff"],"application/x-java-jnlp-file":["jnlp"],"application/x-keepass2":["kdbx"],"application/x-latex":["latex"],"application/x-lua-bytecode":["luac"],"application/x-lzh-compressed":["lzh","lha"],"application/x-makeself":["run"],"application/x-mie":["mie"],"application/x-mobipocket-ebook":["*prc","mobi"],"application/x-ms-application":["application"],"application/x-ms-shortcut":["lnk"],"application/x-ms-wmd":["wmd"],"application/x-ms-wmz":["wmz"],"application/x-ms-xbap":["xbap"],"application/x-msaccess":["mdb"],"application/x-msbinder":["obd"],"application/x-mscardfile":["crd"],"application/x-msclip":["clp"],"application/x-msdos-program":["*exe"],"application/x-msdownload":["*exe","*dll","com","bat","*msi"],"application/x-msmediaview":["mvb","m13","m14"],"application/x-msmetafile":["*wmf","*wmz","*emf","emz"],"application/x-msmoney":["mny"],"application/x-mspublisher":["pub"],"application/x-msschedule":["scd"],"application/x-msterminal":["trm"],"application/x-mswrite":["wri"],"application/x-netcdf":["nc","cdf"],"application/x-ns-proxy-autoconfig":["pac"],"application/x-nzb":["nzb"],"application/x-perl":["pl","pm"],"application/x-pilot":["*prc","*pdb"],"application/x-pkcs12":["p12","pfx"],"application/x-pkcs7-certificates":["p7b","spc"],"application/x-pkcs7-certreqresp":["p7r"],"application/x-rar-compressed":["*rar"],"application/x-redhat-package-manager":["rpm"],"application/x-research-info-systems":["ris"],"application/x-sea":["sea"],"application/x-sh":["sh"],"application/x-shar":["shar"],"application/x-shockwave-flash":["swf"],"application/x-silverlight-app":["xap"],"application/x-sql":["*sql"],"application/x-stuffit":["sit"],"application/x-stuffitx":["sitx"],"application/x-subrip":["srt"],"application/x-sv4cpio":["sv4cpio"],"application/x-sv4crc":["sv4crc"],"application/x-t3vm-image":["t3"],"application/x-tads":["gam"],"application/x-tar":["tar"],"application/x-tcl":["tcl","tk"],"application/x-tex":["tex"],"application/x-tex-tfm":["tfm"],"application/x-texinfo":["texinfo","texi"],"application/x-tgif":["*obj"],"application/x-ustar":["ustar"],"application/x-virtualbox-hdd":["hdd"],"application/x-virtualbox-ova":["ova"],"application/x-virtualbox-ovf":["ovf"],"application/x-virtualbox-vbox":["vbox"],"application/x-virtualbox-vbox-extpack":["vbox-extpack"],"application/x-virtualbox-vdi":["vdi"],"application/x-virtualbox-vhd":["vhd"],"application/x-virtualbox-vmdk":["vmdk"],"application/x-wais-source":["src"],"application/x-web-app-manifest+json":["webapp"],"application/x-x509-ca-cert":["der","crt","pem"],"application/x-xfig":["fig"],"application/x-xliff+xml":["*xlf"],"application/x-xpinstall":["xpi"],"application/x-xz":["xz"],"application/x-zip-compressed":["*zip"],"application/x-zmachine":["z1","z2","z3","z4","z5","z6","z7","z8"],"audio/vnd.dece.audio":["uva","uvva"],"audio/vnd.digital-winds":["eol"],"audio/vnd.dra":["dra"],"audio/vnd.dts":["dts"],"audio/vnd.dts.hd":["dtshd"],"audio/vnd.lucent.voice":["lvp"],"audio/vnd.ms-playready.media.pya":["pya"],"audio/vnd.nuera.ecelp4800":["ecelp4800"],"audio/vnd.nuera.ecelp7470":["ecelp7470"],"audio/vnd.nuera.ecelp9600":["ecelp9600"],"audio/vnd.rip":["rip"],"audio/x-aac":["*aac"],"audio/x-aiff":["aif","aiff","aifc"],"audio/x-caf":["caf"],"audio/x-flac":["flac"],"audio/x-m4a":["*m4a"],"audio/x-matroska":["mka"],"audio/x-mpegurl":["m3u"],"audio/x-ms-wax":["wax"],"audio/x-ms-wma":["wma"],"audio/x-pn-realaudio":["ram","ra"],"audio/x-pn-realaudio-plugin":["rmp"],"audio/x-realaudio":["*ra"],"audio/x-wav":["*wav"],"chemical/x-cdx":["cdx"],"chemical/x-cif":["cif"],"chemical/x-cmdf":["cmdf"],"chemical/x-cml":["cml"],"chemical/x-csml":["csml"],"chemical/x-xyz":["xyz"],"image/prs.btif":["btif","btf"],"image/prs.pti":["pti"],"image/vnd.adobe.photoshop":["psd"],"image/vnd.airzip.accelerator.azv":["azv"],"image/vnd.blockfact.facti":["facti"],"image/vnd.dece.graphic":["uvi","uvvi","uvg","uvvg"],"image/vnd.djvu":["djvu","djv"],"image/vnd.dvb.subtitle":["*sub"],"image/vnd.dwg":["dwg"],"image/vnd.dxf":["dxf"],"image/vnd.fastbidsheet":["fbs"],"image/vnd.fpx":["fpx"],"image/vnd.fst":["fst"],"image/vnd.fujixerox.edmics-mmr":["mmr"],"image/vnd.fujixerox.edmics-rlc":["rlc"],"image/vnd.microsoft.icon":["ico"],"image/vnd.ms-dds":["dds"],"image/vnd.ms-modi":["mdi"],"image/vnd.ms-photo":["wdp"],"image/vnd.net-fpx":["npx"],"image/vnd.pco.b16":["b16"],"image/vnd.tencent.tap":["tap"],"image/vnd.valve.source.texture":["vtf"],"image/vnd.wap.wbmp":["wbmp"],"image/vnd.xiff":["xif"],"image/vnd.zbrush.pcx":["pcx"],"image/x-3ds":["3ds"],"image/x-adobe-dng":["dng"],"image/x-cmu-raster":["ras"],"image/x-cmx":["cmx"],"image/x-freehand":["fh","fhc","fh4","fh5","fh7"],"image/x-icon":["*ico"],"image/x-jng":["jng"],"image/x-mrsid-image":["sid"],"image/x-ms-bmp":["*bmp"],"image/x-pcx":["*pcx"],"image/x-pict":["pic","pct"],"image/x-portable-anymap":["pnm"],"image/x-portable-bitmap":["pbm"],"image/x-portable-graymap":["pgm"],"image/x-portable-pixmap":["ppm"],"image/x-rgb":["rgb"],"image/x-tga":["tga"],"image/x-xbitmap":["xbm"],"image/x-xpixmap":["xpm"],"image/x-xwindowdump":["xwd"],"message/vnd.wfa.wsc":["wsc"],"model/vnd.bary":["bary"],"model/vnd.cld":["cld"],"model/vnd.collada+xml":["dae"],"model/vnd.dwf":["dwf"],"model/vnd.gdl":["gdl"],"model/vnd.gtw":["gtw"],"model/vnd.mts":["*mts"],"model/vnd.opengex":["ogex"],"model/vnd.parasolid.transmit.binary":["x_b"],"model/vnd.parasolid.transmit.text":["x_t"],"model/vnd.pytha.pyox":["pyo","pyox"],"model/vnd.sap.vds":["vds"],"model/vnd.usda":["usda"],"model/vnd.usdz+zip":["usdz"],"model/vnd.valve.source.compiled-map":["bsp"],"model/vnd.vtu":["vtu"],"text/prs.lines.tag":["dsc"],"text/vnd.curl":["curl"],"text/vnd.curl.dcurl":["dcurl"],"text/vnd.curl.mcurl":["mcurl"],"text/vnd.curl.scurl":["scurl"],"text/vnd.dvb.subtitle":["sub"],"text/vnd.familysearch.gedcom":["ged"],"text/vnd.fly":["fly"],"text/vnd.fmi.flexstor":["flx"],"text/vnd.graphviz":["gv"],"text/vnd.in3d.3dml":["3dml"],"text/vnd.in3d.spot":["spot"],"text/vnd.sun.j2me.app-descriptor":["jad"],"text/vnd.wap.wml":["wml"],"text/vnd.wap.wmlscript":["wmls"],"text/x-asm":["s","asm"],"text/x-c":["c","cc","cxx","cpp","h","hh","dic"],"text/x-component":["htc"],"text/x-fortran":["f","for","f77","f90"],"text/x-handlebars-template":["hbs"],"text/x-java-source":["java"],"text/x-lua":["lua"],"text/x-markdown":["mkd"],"text/x-nfo":["nfo"],"text/x-opml":["opml"],"text/x-org":["*org"],"text/x-pascal":["p","pas"],"text/x-processing":["pde"],"text/x-sass":["sass"],"text/x-scss":["scss"],"text/x-setext":["etx"],"text/x-sfv":["sfv"],"text/x-suse-ymp":["ymp"],"text/x-uuencode":["uu"],"text/x-vcalendar":["vcs"],"text/x-vcard":["vcf"],"video/vnd.dece.hd":["uvh","uvvh"],"video/vnd.dece.mobile":["uvm","uvvm"],"video/vnd.dece.pd":["uvp","uvvp"],"video/vnd.dece.sd":["uvs","uvvs"],"video/vnd.dece.video":["uvv","uvvv"],"video/vnd.dvb.file":["dvb"],"video/vnd.fvt":["fvt"],"video/vnd.mpegurl":["mxu","m4u"],"video/vnd.ms-playready.media.pyv":["pyv"],"video/vnd.uvvu.mp4":["uvu","uvvu"],"video/vnd.vivo":["viv"],"video/x-f4v":["f4v"],"video/x-fli":["fli"],"video/x-flv":["flv"],"video/x-m4v":["m4v"],"video/x-matroska":["mkv","mk3d","mks"],"video/x-mng":["mng"],"video/x-ms-asf":["asf","asx"],"video/x-ms-vob":["vob"],"video/x-ms-wm":["wm"],"video/x-ms-wmv":["wmv"],"video/x-ms-wmx":["wmx"],"video/x-ms-wvx":["wvx"],"video/x-msvideo":["avi"],"video/x-sgi-movie":["movie"],"video/x-smv":["smv"],"x-conference/x-cooltalk":["ice"]};Object.freeze(Bl);var kl=Bl;var Nl={"application/andrew-inset":["ez"],"application/appinstaller":["appinstaller"],"application/applixware":["aw"],"application/appx":["appx"],"application/appxbundle":["appxbundle"],"application/atom+xml":["atom"],"application/atomcat+xml":["atomcat"],"application/atomdeleted+xml":["atomdeleted"],"application/atomsvc+xml":["atomsvc"],"application/atsc-dwd+xml":["dwd"],"application/atsc-held+xml":["held"],"application/atsc-rsat+xml":["rsat"],"application/automationml-aml+xml":["aml"],"application/automationml-amlx+zip":["amlx"],"application/bdoc":["bdoc"],"application/calendar+xml":["xcs"],"application/ccxml+xml":["ccxml"],"application/cdfx+xml":["cdfx"],"application/cdmi-capability":["cdmia"],"application/cdmi-container":["cdmic"],"application/cdmi-domain":["cdmid"],"application/cdmi-object":["cdmio"],"application/cdmi-queue":["cdmiq"],"application/cpl+xml":["cpl"],"application/cu-seeme":["cu"],"application/cwl":["cwl"],"application/dash+xml":["mpd"],"application/dash-patch+xml":["mpp"],"application/davmount+xml":["davmount"],"application/dicom":["dcm"],"application/docbook+xml":["dbk"],"application/dssc+der":["dssc"],"application/dssc+xml":["xdssc"],"application/ecmascript":["ecma"],"application/emma+xml":["emma"],"application/emotionml+xml":["emotionml"],"application/epub+zip":["epub"],"application/exi":["exi"],"application/express":["exp"],"application/fdf":["fdf"],"application/fdt+xml":["fdt"],"application/font-tdpfr":["pfr"],"application/geo+json":["geojson"],"application/gml+xml":["gml"],"application/gpx+xml":["gpx"],"application/gxf":["gxf"],"application/gzip":["gz"],"application/hjson":["hjson"],"application/hyperstudio":["stk"],"application/inkml+xml":["ink","inkml"],"application/ipfix":["ipfix"],"application/its+xml":["its"],"application/java-archive":["jar","war","ear"],"application/java-serialized-object":["ser"],"application/java-vm":["class"],"application/javascript":["*js"],"application/json":["json","map"],"application/json5":["json5"],"application/jsonml+json":["jsonml"],"application/ld+json":["jsonld"],"application/lgr+xml":["lgr"],"application/lost+xml":["lostxml"],"application/mac-binhex40":["hqx"],"application/mac-compactpro":["cpt"],"application/mads+xml":["mads"],"application/manifest+json":["webmanifest"],"application/marc":["mrc"],"application/marcxml+xml":["mrcx"],"application/mathematica":["ma","nb","mb"],"application/mathml+xml":["mathml"],"application/mbox":["mbox"],"application/media-policy-dataset+xml":["mpf"],"application/mediaservercontrol+xml":["mscml"],"application/metalink+xml":["metalink"],"application/metalink4+xml":["meta4"],"application/mets+xml":["mets"],"application/mmt-aei+xml":["maei"],"application/mmt-usd+xml":["musd"],"application/mods+xml":["mods"],"application/mp21":["m21","mp21"],"application/mp4":["*mp4","*mpg4","mp4s","m4p"],"application/msix":["msix"],"application/msixbundle":["msixbundle"],"application/msword":["doc","dot"],"application/mxf":["mxf"],"application/n-quads":["nq"],"application/n-triples":["nt"],"application/node":["cjs"],"application/octet-stream":["bin","dms","lrf","mar","so","dist","distz","pkg","bpk","dump","elc","deploy","exe","dll","deb","dmg","iso","img","msi","msp","msm","buffer"],"application/oda":["oda"],"application/oebps-package+xml":["opf"],"application/ogg":["ogx"],"application/omdoc+xml":["omdoc"],"application/onenote":["onetoc","onetoc2","onetmp","onepkg","one","onea"],"application/oxps":["oxps"],"application/p2p-overlay+xml":["relo"],"application/patch-ops-error+xml":["xer"],"application/pdf":["pdf"],"application/pgp-encrypted":["pgp"],"application/pgp-keys":["asc"],"application/pgp-signature":["sig","*asc"],"application/pics-rules":["prf"],"application/pkcs10":["p10"],"application/pkcs7-mime":["p7m","p7c"],"application/pkcs7-signature":["p7s"],"application/pkcs8":["p8"],"application/pkix-attr-cert":["ac"],"application/pkix-cert":["cer"],"application/pkix-crl":["crl"],"application/pkix-pkipath":["pkipath"],"application/pkixcmp":["pki"],"application/pls+xml":["pls"],"application/postscript":["ai","eps","ps"],"application/provenance+xml":["provx"],"application/pskc+xml":["pskcxml"],"application/raml+yaml":["raml"],"application/rdf+xml":["rdf","owl"],"application/reginfo+xml":["rif"],"application/relax-ng-compact-syntax":["rnc"],"application/resource-lists+xml":["rl"],"application/resource-lists-diff+xml":["rld"],"application/rls-services+xml":["rs"],"application/route-apd+xml":["rapd"],"application/route-s-tsid+xml":["sls"],"application/route-usd+xml":["rusd"],"application/rpki-ghostbusters":["gbr"],"application/rpki-manifest":["mft"],"application/rpki-roa":["roa"],"application/rsd+xml":["rsd"],"application/rss+xml":["rss"],"application/rtf":["rtf"],"application/sbml+xml":["sbml"],"application/scvp-cv-request":["scq"],"application/scvp-cv-response":["scs"],"application/scvp-vp-request":["spq"],"application/scvp-vp-response":["spp"],"application/sdp":["sdp"],"application/senml+xml":["senmlx"],"application/sensml+xml":["sensmlx"],"application/set-payment-initiation":["setpay"],"application/set-registration-initiation":["setreg"],"application/shf+xml":["shf"],"application/sieve":["siv","sieve"],"application/smil+xml":["smi","smil"],"application/sparql-query":["rq"],"application/sparql-results+xml":["srx"],"application/sql":["sql"],"application/srgs":["gram"],"application/srgs+xml":["grxml"],"application/sru+xml":["sru"],"application/ssdl+xml":["ssdl"],"application/ssml+xml":["ssml"],"application/swid+xml":["swidtag"],"application/tei+xml":["tei","teicorpus"],"application/thraud+xml":["tfi"],"application/timestamped-data":["tsd"],"application/toml":["toml"],"application/trig":["trig"],"application/ttml+xml":["ttml"],"application/ubjson":["ubj"],"application/urc-ressheet+xml":["rsheet"],"application/urc-targetdesc+xml":["td"],"application/voicexml+xml":["vxml"],"application/wasm":["wasm"],"application/watcherinfo+xml":["wif"],"application/widget":["wgt"],"application/winhlp":["hlp"],"application/wsdl+xml":["wsdl"],"application/wspolicy+xml":["wspolicy"],"application/xaml+xml":["xaml"],"application/xcap-att+xml":["xav"],"application/xcap-caps+xml":["xca"],"application/xcap-diff+xml":["xdf"],"application/xcap-el+xml":["xel"],"application/xcap-ns+xml":["xns"],"application/xenc+xml":["xenc"],"application/xfdf":["xfdf"],"application/xhtml+xml":["xhtml","xht"],"application/xliff+xml":["xlf"],"application/xml":["xml","xsl","xsd","rng"],"application/xml-dtd":["dtd"],"application/xop+xml":["xop"],"application/xproc+xml":["xpl"],"application/xslt+xml":["*xsl","xslt"],"application/xspf+xml":["xspf"],"application/xv+xml":["mxml","xhvml","xvml","xvm"],"application/yang":["yang"],"application/yin+xml":["yin"],"application/zip":["zip"],"application/zip+dotlottie":["lottie"],"audio/3gpp":["*3gpp"],"audio/aac":["adts","aac"],"audio/adpcm":["adp"],"audio/amr":["amr"],"audio/basic":["au","snd"],"audio/midi":["mid","midi","kar","rmi"],"audio/mobile-xmf":["mxmf"],"audio/mp3":["*mp3"],"audio/mp4":["m4a","mp4a","m4b"],"audio/mpeg":["mpga","mp2","mp2a","mp3","m2a","m3a"],"audio/ogg":["oga","ogg","spx","opus"],"audio/s3m":["s3m"],"audio/silk":["sil"],"audio/wav":["wav"],"audio/wave":["*wav"],"audio/webm":["weba"],"audio/xm":["xm"],"font/collection":["ttc"],"font/otf":["otf"],"font/ttf":["ttf"],"font/woff":["woff"],"font/woff2":["woff2"],"image/aces":["exr"],"image/apng":["apng"],"image/avci":["avci"],"image/avcs":["avcs"],"image/avif":["avif"],"image/bmp":["bmp","dib"],"image/cgm":["cgm"],"image/dicom-rle":["drle"],"image/dpx":["dpx"],"image/emf":["emf"],"image/fits":["fits"],"image/g3fax":["g3"],"image/gif":["gif"],"image/heic":["heic"],"image/heic-sequence":["heics"],"image/heif":["heif"],"image/heif-sequence":["heifs"],"image/hej2k":["hej2"],"image/ief":["ief"],"image/jaii":["jaii"],"image/jais":["jais"],"image/jls":["jls"],"image/jp2":["jp2","jpg2"],"image/jpeg":["jpg","jpeg","jpe"],"image/jph":["jph"],"image/jphc":["jhc"],"image/jpm":["jpm","jpgm"],"image/jpx":["jpx","jpf"],"image/jxl":["jxl"],"image/jxr":["jxr"],"image/jxra":["jxra"],"image/jxrs":["jxrs"],"image/jxs":["jxs"],"image/jxsc":["jxsc"],"image/jxsi":["jxsi"],"image/jxss":["jxss"],"image/ktx":["ktx"],"image/ktx2":["ktx2"],"image/pjpeg":["jfif"],"image/png":["png"],"image/sgi":["sgi"],"image/svg+xml":["svg","svgz"],"image/t38":["t38"],"image/tiff":["tif","tiff"],"image/tiff-fx":["tfx"],"image/webp":["webp"],"image/wmf":["wmf"],"message/disposition-notification":["disposition-notification"],"message/global":["u8msg"],"message/global-delivery-status":["u8dsn"],"message/global-disposition-notification":["u8mdn"],"message/global-headers":["u8hdr"],"message/rfc822":["eml","mime","mht","mhtml"],"model/3mf":["3mf"],"model/gltf+json":["gltf"],"model/gltf-binary":["glb"],"model/iges":["igs","iges"],"model/jt":["jt"],"model/mesh":["msh","mesh","silo"],"model/mtl":["mtl"],"model/obj":["obj"],"model/prc":["prc"],"model/step":["step","stp","stpnc","p21","210"],"model/step+xml":["stpx"],"model/step+zip":["stpz"],"model/step-xml+zip":["stpxz"],"model/stl":["stl"],"model/u3d":["u3d"],"model/vrml":["wrl","vrml"],"model/x3d+binary":["*x3db","x3dbz"],"model/x3d+fastinfoset":["x3db"],"model/x3d+vrml":["*x3dv","x3dvz"],"model/x3d+xml":["x3d","x3dz"],"model/x3d-vrml":["x3dv"],"text/cache-manifest":["appcache","manifest"],"text/calendar":["ics","ifb"],"text/coffeescript":["coffee","litcoffee"],"text/css":["css"],"text/csv":["csv"],"text/html":["html","htm","shtml"],"text/jade":["jade"],"text/javascript":["js","mjs"],"text/jsx":["jsx"],"text/less":["less"],"text/markdown":["md","markdown"],"text/mathml":["mml"],"text/mdx":["mdx"],"text/n3":["n3"],"text/plain":["txt","text","conf","def","list","log","in","ini"],"text/richtext":["rtx"],"text/rtf":["*rtf"],"text/sgml":["sgml","sgm"],"text/shex":["shex"],"text/slim":["slim","slm"],"text/spdx":["spdx"],"text/stylus":["stylus","styl"],"text/tab-separated-values":["tsv"],"text/troff":["t","tr","roff","man","me","ms"],"text/turtle":["ttl"],"text/uri-list":["uri","uris","urls"],"text/vcard":["vcard"],"text/vtt":["vtt"],"text/wgsl":["wgsl"],"text/xml":["*xml"],"text/yaml":["yaml","yml"],"video/3gpp":["3gp","3gpp"],"video/3gpp2":["3g2"],"video/h261":["h261"],"video/h263":["h263"],"video/h264":["h264"],"video/iso.segment":["m4s"],"video/jpeg":["jpgv"],"video/jpm":["*jpm","*jpgm"],"video/mj2":["mj2","mjp2"],"video/mp2t":["ts","m2t","m2ts","mts"],"video/mp4":["mp4","mp4v","mpg4"],"video/mpeg":["mpeg","mpg","mpe","m1v","m2v"],"video/ogg":["ogv"],"video/quicktime":["qt","mov"],"video/webm":["webm"]};Object.freeze(Nl);var Vl=Nl;var _e=function(e,t,i,a){if(i==="a"&&!a)throw new TypeError("Private accessor was defined without a getter");if(typeof t=="function"?e!==t||!a:!t.has(e))throw new TypeError("Cannot read private member from an object whose class did not declare it");return i==="m"?a:i==="a"?a.call(e):a?a.value:t.get(e)},St,jt,rt,ya=class{constructor(...t){St.set(this,new Map),jt.set(this,new Map),rt.set(this,new Map);for(let i of t)this.define(i)}define(t,i=!1){for(let[a,n]of Object.entries(t)){a=a.toLowerCase(),n=n.map(r=>r.toLowerCase()),_e(this,rt,"f").has(a)||_e(this,rt,"f").set(a,new Set);let l=_e(this,rt,"f").get(a),o=!0;for(let r of n){let s=r.startsWith("*");if(r=s?r.slice(1):r,l?.add(r),o&&_e(this,jt,"f").set(a,r),o=!1,s)continue;let p=_e(this,St,"f").get(r);if(p&&p!=a&&!i)throw new Error(`"${a} -> ${r}" conflicts with "${p} -> ${r}". Pass \`force=true\` to override this definition.`);_e(this,St,"f").set(r,a)}}return this}getType(t){if(typeof t!="string")return null;let i=t.replace(/^.*[/\\]/s,"").toLowerCase(),a=i.replace(/^.*\./s,"").toLowerCase(),n=i.length{throw new Error("define() not allowed for built-in Mime objects. See https://github.com/broofa/mime/blob/main/README.md#custom-mime-instances")},Object.freeze(this);for(let t of _e(this,rt,"f").values())Object.freeze(t);return this}_getTestState(){return{types:_e(this,St,"f"),extensions:_e(this,jt,"f")}}};St=new WeakMap,jt=new WeakMap,rt=new WeakMap;var Ra=ya;var Gl=new Ra(Vl,kl)._freeze();var Ul=({addFilter:e,utils:t})=>{let{Type:i,replaceInString:a,toNaturalFileSize:n}=t;return e("ALLOW_HOPPER_ITEM",(l,{query:o})=>{if(!o("GET_ALLOW_FILE_SIZE_VALIDATION"))return!0;let r=o("GET_MAX_FILE_SIZE");if(r!==null&&l.size>r)return!1;let s=o("GET_MIN_FILE_SIZE");return!(s!==null&&l.sizenew Promise((r,s)=>{if(!o("GET_ALLOW_FILE_SIZE_VALIDATION"))return r(l);let p=o("GET_FILE_VALIDATE_SIZE_FILTER");if(p&&!p(l))return r(l);let c=o("GET_MAX_FILE_SIZE");if(c!==null&&l.size>c){s({status:{main:o("GET_LABEL_MAX_FILE_SIZE_EXCEEDED"),sub:a(o("GET_LABEL_MAX_FILE_SIZE"),{filesize:n(c,".",o("GET_FILE_SIZE_BASE"),o("GET_FILE_SIZE_LABELS",o))})}});return}let d=o("GET_MIN_FILE_SIZE");if(d!==null&&l.sizeg+f.fileSize,0)>m){s({status:{main:o("GET_LABEL_MAX_TOTAL_FILE_SIZE_EXCEEDED"),sub:a(o("GET_LABEL_MAX_TOTAL_FILE_SIZE"),{filesize:n(m,".",o("GET_FILE_SIZE_BASE"),o("GET_FILE_SIZE_LABELS",o))})}});return}r(l)})),{options:{allowFileSizeValidation:[!0,i.BOOLEAN],maxFileSize:[null,i.INT],minFileSize:[null,i.INT],maxTotalFileSize:[null,i.INT],fileValidateSizeFilter:[null,i.FUNCTION],labelMinFileSizeExceeded:["File is too small",i.STRING],labelMinFileSize:["Minimum file size is {filesize}",i.STRING],labelMaxFileSizeExceeded:["File is too large",i.STRING],labelMaxFileSize:["Maximum file size is {filesize}",i.STRING],labelMaxTotalFileSizeExceeded:["Maximum total size exceeded",i.STRING],labelMaxTotalFileSize:["Maximum total file size is {filesize}",i.STRING]}}},bm=typeof window<"u"&&typeof window.document<"u";bm&&document.dispatchEvent(new CustomEvent("FilePond:pluginloaded",{detail:Ul}));var Hl=Ul;var Wl=({addFilter:e,utils:t})=>{let{Type:i,isString:a,replaceInString:n,guesstimateMimeType:l,getExtensionFromFilename:o,getFilenameFromURL:r}=t,s=(u,g)=>{let f=(/^[^/]+/.exec(u)||[]).pop(),h=g.slice(0,-2);return f===h},p=(u,g)=>u.some(f=>/\*$/.test(f)?s(g,f):f===g),c=u=>{let g="";if(a(u)){let f=r(u),h=o(f);h&&(g=l(h))}else g=u.type;return g},d=(u,g,f)=>{if(g.length===0)return!0;let h=c(u);return f?new Promise((I,b)=>{f(u,h).then(E=>{p(g,E)?I():b()}).catch(b)}):p(g,h)},m=u=>g=>u[g]===null?!1:u[g]||g;return e("SET_ATTRIBUTE_TO_OPTION_MAP",u=>Object.assign(u,{accept:"acceptedFileTypes"})),e("ALLOW_HOPPER_ITEM",(u,{query:g})=>g("GET_ALLOW_FILE_TYPE_VALIDATION")?d(u,g("GET_ACCEPTED_FILE_TYPES")):!0),e("LOAD_FILE",(u,{query:g})=>new Promise((f,h)=>{if(!g("GET_ALLOW_FILE_TYPE_VALIDATION")){f(u);return}let I=g("GET_ACCEPTED_FILE_TYPES"),b=g("GET_FILE_VALIDATE_TYPE_DETECT_TYPE"),E=d(u,I,b),v=()=>{let y=I.map(m(g("GET_FILE_VALIDATE_TYPE_LABEL_EXPECTED_TYPES_MAP"))).filter(_=>_!==!1),T=y.filter((_,x)=>y.indexOf(_)===x);h({status:{main:g("GET_LABEL_FILE_TYPE_NOT_ALLOWED"),sub:n(g("GET_FILE_VALIDATE_TYPE_LABEL_EXPECTED_TYPES"),{allTypes:T.join(", "),allButLastType:T.slice(0,-1).join(", "),lastType:T[T.length-1]})}})};if(typeof E=="boolean")return E?f(u):v();E.then(()=>{f(u)}).catch(v)})),{options:{allowFileTypeValidation:[!0,i.BOOLEAN],acceptedFileTypes:[[],i.ARRAY],labelFileTypeNotAllowed:["File is of invalid type",i.STRING],fileValidateTypeLabelExpectedTypes:["Expects {allButLastType} or {lastType}",i.STRING],fileValidateTypeLabelExpectedTypesMap:[{},i.OBJECT],fileValidateTypeDetectType:[null,i.FUNCTION]}}},Em=typeof window<"u"&&typeof window.document<"u";Em&&document.dispatchEvent(new CustomEvent("FilePond:pluginloaded",{detail:Wl}));var jl=Wl;var Yl=e=>/^image/.test(e.type),ql=({addFilter:e,utils:t})=>{let{Type:i,isFile:a,getNumericAspectRatioFromString:n}=t,l=(p,c)=>!(!Yl(p.file)||!c("GET_ALLOW_IMAGE_CROP")),o=p=>typeof p=="object",r=p=>typeof p=="number",s=(p,c)=>p.setMetadata("crop",Object.assign({},p.getMetadata("crop"),c));return e("DID_CREATE_ITEM",(p,{query:c})=>{p.extend("setImageCrop",d=>{if(!(!l(p,c)||!o(center)))return p.setMetadata("crop",d),d}),p.extend("setImageCropCenter",d=>{if(!(!l(p,c)||!o(d)))return s(p,{center:d})}),p.extend("setImageCropZoom",d=>{if(!(!l(p,c)||!r(d)))return s(p,{zoom:Math.max(1,d)})}),p.extend("setImageCropRotation",d=>{if(!(!l(p,c)||!r(d)))return s(p,{rotation:d})}),p.extend("setImageCropFlip",d=>{if(!(!l(p,c)||!o(d)))return s(p,{flip:d})}),p.extend("setImageCropAspectRatio",d=>{if(!l(p,c)||typeof d>"u")return;let m=p.getMetadata("crop"),u=n(d),g={center:{x:.5,y:.5},flip:m?Object.assign({},m.flip):{horizontal:!1,vertical:!1},rotation:0,zoom:1,aspectRatio:u};return p.setMetadata("crop",g),g})}),e("DID_LOAD_ITEM",(p,{query:c})=>new Promise((d,m)=>{let u=p.file;if(!a(u)||!Yl(u)||!c("GET_ALLOW_IMAGE_CROP")||p.getMetadata("crop"))return d(p);let f=c("GET_IMAGE_CROP_ASPECT_RATIO");p.setMetadata("crop",{center:{x:.5,y:.5},flip:{horizontal:!1,vertical:!1},rotation:0,zoom:1,aspectRatio:f?n(f):null}),d(p)})),{options:{allowImageCrop:[!0,i.BOOLEAN],imageCropAspectRatio:[null,i.STRING]}}},Tm=typeof window<"u"&&typeof window.document<"u";Tm&&document.dispatchEvent(new CustomEvent("FilePond:pluginloaded",{detail:ql}));var $l=ql;var Sa=e=>/^image/.test(e.type),Xl=e=>{let{addFilter:t,utils:i,views:a}=e,{Type:n,createRoute:l,createItemAPI:o=c=>c}=i,{fileActionButton:r}=a;t("SHOULD_REMOVE_ON_REVERT",(c,{item:d,query:m})=>new Promise(u=>{let{file:g}=d,f=m("GET_ALLOW_IMAGE_EDIT")&&m("GET_IMAGE_EDIT_ALLOW_EDIT")&&Sa(g);u(!f)})),t("DID_LOAD_ITEM",(c,{query:d,dispatch:m})=>new Promise((u,g)=>{if(c.origin>1){u(c);return}let{file:f}=c;if(!d("GET_ALLOW_IMAGE_EDIT")||!d("GET_IMAGE_EDIT_INSTANT_EDIT")){u(c);return}if(!Sa(f)){u(c);return}let h=(b,E,v)=>y=>{s.shift(),y?E(b):v(b),m("KICK"),I()},I=()=>{if(!s.length)return;let{item:b,resolve:E,reject:v}=s[0];m("EDIT_ITEM",{id:b.id,handleEditorResponse:h(b,E,v)})};p({item:c,resolve:u,reject:g}),s.length===1&&I()})),t("DID_CREATE_ITEM",(c,{query:d,dispatch:m})=>{c.extend("edit",()=>{m("EDIT_ITEM",{id:c.id})})});let s=[],p=c=>(s.push(c),c);return t("CREATE_VIEW",c=>{let{is:d,view:m,query:u}=c;if(!u("GET_ALLOW_IMAGE_EDIT"))return;let g=u("GET_ALLOW_IMAGE_PREVIEW");if(!(d("file-info")&&!g||d("file")&&g))return;let h=u("GET_IMAGE_EDIT_EDITOR");if(!h)return;h.filepondCallbackBridge||(h.outputData=!0,h.outputFile=!1,h.filepondCallbackBridge={onconfirm:h.onconfirm||(()=>{}),oncancel:h.oncancel||(()=>{})});let I=({root:v,props:y,action:T})=>{let{id:_}=y,{handleEditorResponse:x}=T;h.cropAspectRatio=v.query("GET_IMAGE_CROP_ASPECT_RATIO")||h.cropAspectRatio,h.outputCanvasBackgroundColor=v.query("GET_IMAGE_TRANSFORM_CANVAS_BACKGROUND_COLOR")||h.outputCanvasBackgroundColor;let R=v.query("GET_ITEM",_);if(!R)return;let P=R.file,z=R.getMetadata("crop"),A={center:{x:.5,y:.5},flip:{horizontal:!1,vertical:!1},zoom:1,rotation:0,aspectRatio:null},B=R.getMetadata("resize"),w=R.getMetadata("filter")||null,F=R.getMetadata("filters")||null,S=R.getMetadata("colors")||null,L=R.getMetadata("markup")||null,D={crop:z||A,size:B?{upscale:B.upscale,mode:B.mode,width:B.size.width,height:B.size.height}:null,filter:F?F.id||F.matrix:v.query("GET_ALLOW_IMAGE_FILTER")&&v.query("GET_IMAGE_FILTER_COLOR_MATRIX")&&!S?w:null,color:S,markup:L};h.onconfirm=({data:O})=>{let{crop:U,size:C,filter:X,color:K,colorMatrix:Z,markup:ce}=O,V={};if(U&&(V.crop=U),C){let W=(R.getMetadata("resize")||{}).size,$={width:C.width,height:C.height};!($.width&&$.height)&&W&&($.width=W.width,$.height=W.height),($.width||$.height)&&(V.resize={upscale:C.upscale,mode:C.mode,size:$})}ce&&(V.markup=ce),V.colors=K,V.filters=X,V.filter=Z,R.setMetadata(V),h.filepondCallbackBridge.onconfirm(O,o(R)),x&&(h.onclose=()=>{x(!0),h.onclose=null})},h.oncancel=()=>{h.filepondCallbackBridge.oncancel(o(R)),x&&(h.onclose=()=>{x(!1),h.onclose=null})},h.open(P,D)},b=({root:v,props:y})=>{if(!u("GET_IMAGE_EDIT_ALLOW_EDIT"))return;let{id:T}=y,_=u("GET_ITEM",T);if(!_)return;let x=_.file;if(Sa(x))if(v.ref.handleEdit=R=>{R.stopPropagation(),v.dispatch("EDIT_ITEM",{id:T})},g){let R=m.createChildView(r,{label:"edit",icon:u("GET_IMAGE_EDIT_ICON_EDIT"),opacity:0});R.element.classList.add("filepond--action-edit-item"),R.element.dataset.align=u("GET_STYLE_IMAGE_EDIT_BUTTON_EDIT_ITEM_POSITION"),R.on("click",v.ref.handleEdit),v.ref.buttonEditItem=m.appendChildView(R)}else{let R=m.element.querySelector(".filepond--file-info-main"),P=document.createElement("button");P.className="filepond--action-edit-item-alt",P.innerHTML=u("GET_IMAGE_EDIT_ICON_EDIT")+"edit",P.addEventListener("click",v.ref.handleEdit),R.appendChild(P),v.ref.editButton=P}};m.registerDestroyer(({root:v})=>{v.ref.buttonEditItem&&v.ref.buttonEditItem.off("click",v.ref.handleEdit),v.ref.editButton&&v.ref.editButton.removeEventListener("click",v.ref.handleEdit)});let E={EDIT_ITEM:I,DID_LOAD_ITEM:b};if(g){let v=({root:y})=>{y.ref.buttonEditItem&&(y.ref.buttonEditItem.opacity=1)};E.DID_IMAGE_PREVIEW_SHOW=v}m.registerWriter(l(E))}),{options:{allowImageEdit:[!0,n.BOOLEAN],styleImageEditButtonEditItemPosition:["bottom center",n.STRING],imageEditInstantEdit:[!1,n.BOOLEAN],imageEditAllowEdit:[!0,n.BOOLEAN],imageEditIconEdit:['',n.STRING],imageEditEditor:[null,n.OBJECT]}}},Im=typeof window<"u"&&typeof window.document<"u";Im&&document.dispatchEvent(new CustomEvent("FilePond:pluginloaded",{detail:Xl}));var Kl=Xl;var vm=e=>/^image\/jpeg/.test(e.type),st={JPEG:65496,APP1:65505,EXIF:1165519206,TIFF:18761,Orientation:274,Unknown:65280},ct=(e,t,i=!1)=>e.getUint16(t,i),Zl=(e,t,i=!1)=>e.getUint32(t,i),xm=e=>new Promise((t,i)=>{let a=new FileReader;a.onload=function(n){let l=new DataView(n.target.result);if(ct(l,0)!==st.JPEG){t(-1);return}let o=l.byteLength,r=2;for(;rym,Sm="data:image/jpg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/4QA6RXhpZgAATU0AKgAAAAgAAwESAAMAAAABAAYAAAEoAAMAAAABAAIAAAITAAMAAAABAAEAAAAAAAD/2wBDAP//////////////////////////////////////////////////////////////////////////////////////wAALCAABAAIBASIA/8QAJgABAAAAAAAAAAAAAAAAAAAAAxABAAAAAAAAAAAAAAAAAAAAAP/aAAgBAQAAPwBH/9k=",Ql,Ii=Rm()?new Image:{};Ii.onload=()=>Ql=Ii.naturalWidth>Ii.naturalHeight;Ii.src=Sm;var _m=()=>Ql,Jl=({addFilter:e,utils:t})=>{let{Type:i,isFile:a}=t;return e("DID_LOAD_ITEM",(n,{query:l})=>new Promise((o,r)=>{let s=n.file;if(!a(s)||!vm(s)||!l("GET_ALLOW_IMAGE_EXIF_ORIENTATION")||!_m())return o(n);xm(s).then(p=>{n.setMetadata("exif",{orientation:p}),o(n)})})),{options:{allowImageExifOrientation:[!0,i.BOOLEAN]}}},wm=typeof window<"u"&&typeof window.document<"u";wm&&document.dispatchEvent(new CustomEvent("FilePond:pluginloaded",{detail:Jl}));var eo=Jl;var Lm=e=>/^image/.test(e.type),to=(e,t)=>qt(e.x*t,e.y*t),io=(e,t)=>qt(e.x+t.x,e.y+t.y),Mm=e=>{let t=Math.sqrt(e.x*e.x+e.y*e.y);return t===0?{x:0,y:0}:qt(e.x/t,e.y/t)},vi=(e,t,i)=>{let a=Math.cos(t),n=Math.sin(t),l=qt(e.x-i.x,e.y-i.y);return qt(i.x+a*l.x-n*l.y,i.y+n*l.x+a*l.y)},qt=(e=0,t=0)=>({x:e,y:t}),Te=(e,t,i=1,a)=>{if(typeof e=="string")return parseFloat(e)*i;if(typeof e=="number")return e*(a?t[a]:Math.min(t.width,t.height))},Am=(e,t,i)=>{let a=e.borderStyle||e.lineStyle||"solid",n=e.backgroundColor||e.fontColor||"transparent",l=e.borderColor||e.lineColor||"transparent",o=Te(e.borderWidth||e.lineWidth,t,i),r=e.lineCap||"round",s=e.lineJoin||"round",p=typeof a=="string"?"":a.map(d=>Te(d,t,i)).join(","),c=e.opacity||1;return{"stroke-linecap":r,"stroke-linejoin":s,"stroke-width":o||0,"stroke-dasharray":p,stroke:l,fill:n,opacity:c}},we=e=>e!=null,zm=(e,t,i=1)=>{let a=Te(e.x,t,i,"width")||Te(e.left,t,i,"width"),n=Te(e.y,t,i,"height")||Te(e.top,t,i,"height"),l=Te(e.width,t,i,"width"),o=Te(e.height,t,i,"height"),r=Te(e.right,t,i,"width"),s=Te(e.bottom,t,i,"height");return we(n)||(we(o)&&we(s)?n=t.height-o-s:n=s),we(a)||(we(l)&&we(r)?a=t.width-l-r:a=r),we(l)||(we(a)&&we(r)?l=t.width-a-r:l=0),we(o)||(we(n)&&we(s)?o=t.height-n-s:o=0),{x:a||0,y:n||0,width:l||0,height:o||0}},Pm=e=>e.map((t,i)=>`${i===0?"M":"L"} ${t.x} ${t.y}`).join(" "),Be=(e,t)=>Object.keys(t).forEach(i=>e.setAttribute(i,t[i])),Fm="http://www.w3.org/2000/svg",_t=(e,t)=>{let i=document.createElementNS(Fm,e);return t&&Be(i,t),i},Om=e=>Be(e,{...e.rect,...e.styles}),Dm=e=>{let t=e.rect.x+e.rect.width*.5,i=e.rect.y+e.rect.height*.5,a=e.rect.width*.5,n=e.rect.height*.5;return Be(e,{cx:t,cy:i,rx:a,ry:n,...e.styles})},Cm={contain:"xMidYMid meet",cover:"xMidYMid slice"},Bm=(e,t)=>{Be(e,{...e.rect,...e.styles,preserveAspectRatio:Cm[t.fit]||"none"})},km={left:"start",center:"middle",right:"end"},Nm=(e,t,i,a)=>{let n=Te(t.fontSize,i,a),l=t.fontFamily||"sans-serif",o=t.fontWeight||"normal",r=km[t.textAlign]||"start";Be(e,{...e.rect,...e.styles,"stroke-width":0,"font-weight":o,"font-size":n,"font-family":l,"text-anchor":r}),e.text!==t.text&&(e.text=t.text,e.textContent=t.text.length?t.text:" ")},Vm=(e,t,i,a)=>{Be(e,{...e.rect,...e.styles,fill:"none"});let n=e.childNodes[0],l=e.childNodes[1],o=e.childNodes[2],r=e.rect,s={x:e.rect.x+e.rect.width,y:e.rect.y+e.rect.height};if(Be(n,{x1:r.x,y1:r.y,x2:s.x,y2:s.y}),!t.lineDecoration)return;l.style.display="none",o.style.display="none";let p=Mm({x:s.x-r.x,y:s.y-r.y}),c=Te(.05,i,a);if(t.lineDecoration.indexOf("arrow-begin")!==-1){let d=to(p,c),m=io(r,d),u=vi(r,2,m),g=vi(r,-2,m);Be(l,{style:"display:block;",d:`M${u.x},${u.y} L${r.x},${r.y} L${g.x},${g.y}`})}if(t.lineDecoration.indexOf("arrow-end")!==-1){let d=to(p,-c),m=io(s,d),u=vi(s,2,m),g=vi(s,-2,m);Be(o,{style:"display:block;",d:`M${u.x},${u.y} L${s.x},${s.y} L${g.x},${g.y}`})}},Gm=(e,t,i,a)=>{Be(e,{...e.styles,fill:"none",d:Pm(t.points.map(n=>({x:Te(n.x,i,a,"width"),y:Te(n.y,i,a,"height")})))})},xi=e=>t=>_t(e,{id:t.id}),Um=e=>{let t=_t("image",{id:e.id,"stroke-linecap":"round","stroke-linejoin":"round",opacity:"0"});return t.onload=()=>{t.setAttribute("opacity",e.opacity||1)},t.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",e.src),t},Hm=e=>{let t=_t("g",{id:e.id,"stroke-linecap":"round","stroke-linejoin":"round"}),i=_t("line");t.appendChild(i);let a=_t("path");t.appendChild(a);let n=_t("path");return t.appendChild(n),t},Wm={image:Um,rect:xi("rect"),ellipse:xi("ellipse"),text:xi("text"),path:xi("path"),line:Hm},jm={rect:Om,ellipse:Dm,image:Bm,text:Nm,path:Gm,line:Vm},Ym=(e,t)=>Wm[e](t),qm=(e,t,i,a,n)=>{t!=="path"&&(e.rect=zm(i,a,n)),e.styles=Am(i,a,n),jm[t](e,i,a,n)},$m=["x","y","left","top","right","bottom","width","height"],Xm=e=>typeof e=="string"&&/%/.test(e)?parseFloat(e)/100:e,Km=e=>{let[t,i]=e,a=i.points?{}:$m.reduce((n,l)=>(n[l]=Xm(i[l]),n),{});return[t,{zIndex:0,...i,...a}]},Zm=(e,t)=>e[1].zIndex>t[1].zIndex?1:e[1].zIndexe.utils.createView({name:"image-preview-markup",tag:"svg",ignoreRect:!0,mixins:{apis:["width","height","crop","markup","resize","dirty"]},write:({root:t,props:i})=>{if(!i.dirty)return;let{crop:a,resize:n,markup:l}=i,o=i.width,r=i.height,s=a.width,p=a.height;if(n){let{size:u}=n,g=u&&u.width,f=u&&u.height,h=n.mode,I=n.upscale;g&&!f&&(f=g),f&&!g&&(g=f);let b=s{let[g,f]=u,h=Ym(g,f);qm(h,g,f,c,d),t.element.appendChild(h)})}}),Yt=(e,t)=>({x:e,y:t}),Jm=(e,t)=>e.x*t.x+e.y*t.y,ao=(e,t)=>Yt(e.x-t.x,e.y-t.y),eu=(e,t)=>Jm(ao(e,t),ao(e,t)),no=(e,t)=>Math.sqrt(eu(e,t)),lo=(e,t)=>{let i=e,a=1.5707963267948966,n=t,l=1.5707963267948966-t,o=Math.sin(a),r=Math.sin(n),s=Math.sin(l),p=Math.cos(l),c=i/o,d=c*r,m=c*s;return Yt(p*d,p*m)},tu=(e,t)=>{let i=e.width,a=e.height,n=lo(i,t),l=lo(a,t),o=Yt(e.x+Math.abs(n.x),e.y-Math.abs(n.y)),r=Yt(e.x+e.width+Math.abs(l.y),e.y+Math.abs(l.x)),s=Yt(e.x-Math.abs(l.y),e.y+e.height-Math.abs(l.x));return{width:no(o,r),height:no(o,s)}},iu=(e,t,i=1)=>{let a=e.height/e.width,n=1,l=t,o=1,r=a;r>l&&(r=l,o=r/a);let s=Math.max(n/o,l/r),p=e.width/(i*s*o),c=p*t;return{width:p,height:c}},ro=(e,t,i,a)=>{let n=a.x>.5?1-a.x:a.x,l=a.y>.5?1-a.y:a.y,o=n*2*e.width,r=l*2*e.height,s=tu(t,i);return Math.max(s.width/o,s.height/r)},so=(e,t)=>{let i=e.width,a=i*t;a>e.height&&(a=e.height,i=a/t);let n=(e.width-i)*.5,l=(e.height-a)*.5;return{x:n,y:l,width:i,height:a}},au=(e,t={})=>{let{zoom:i,rotation:a,center:n,aspectRatio:l}=t;l||(l=e.height/e.width);let o=iu(e,l,i),r={x:o.width*.5,y:o.height*.5},s={x:0,y:0,width:o.width,height:o.height,center:r},p=typeof t.scaleToFit>"u"||t.scaleToFit,c=ro(e,so(s,l),a,p?n:{x:.5,y:.5}),d=i*c;return{widthFloat:o.width/d,heightFloat:o.height/d,width:Math.round(o.width/d),height:Math.round(o.height/d)}},Ce={type:"spring",stiffness:.5,damping:.45,mass:10},nu=e=>e.utils.createView({name:"image-bitmap",ignoreRect:!0,mixins:{styles:["scaleX","scaleY"]},create:({root:t,props:i})=>{t.appendChild(i.image)}}),lu=e=>e.utils.createView({name:"image-canvas-wrapper",tag:"div",ignoreRect:!0,mixins:{apis:["crop","width","height"],styles:["originX","originY","translateX","translateY","scaleX","scaleY","rotateZ"],animations:{originX:Ce,originY:Ce,scaleX:Ce,scaleY:Ce,translateX:Ce,translateY:Ce,rotateZ:Ce}},create:({root:t,props:i})=>{i.width=i.image.width,i.height=i.image.height,t.ref.bitmap=t.appendChildView(t.createChildView(nu(e),{image:i.image}))},write:({root:t,props:i})=>{let{flip:a}=i.crop,{bitmap:n}=t.ref;n.scaleX=a.horizontal?-1:1,n.scaleY=a.vertical?-1:1}}),ou=e=>e.utils.createView({name:"image-clip",tag:"div",ignoreRect:!0,mixins:{apis:["crop","markup","resize","width","height","dirty","background"],styles:["width","height","opacity"],animations:{opacity:{type:"tween",duration:250}}},didWriteView:function({root:t,props:i}){i.background&&(t.element.style.backgroundColor=i.background)},create:({root:t,props:i})=>{t.ref.image=t.appendChildView(t.createChildView(lu(e),Object.assign({},i))),t.ref.createMarkup=()=>{t.ref.markup||(t.ref.markup=t.appendChildView(t.createChildView(Qm(e),Object.assign({},i))))},t.ref.destroyMarkup=()=>{t.ref.markup&&(t.removeChildView(t.ref.markup),t.ref.markup=null)};let a=t.query("GET_IMAGE_PREVIEW_TRANSPARENCY_INDICATOR");a!==null&&(a==="grid"?t.element.dataset.transparencyIndicator=a:t.element.dataset.transparencyIndicator="color")},write:({root:t,props:i,shouldOptimize:a})=>{let{crop:n,markup:l,resize:o,dirty:r,width:s,height:p}=i;t.ref.image.crop=n;let c={x:0,y:0,width:s,height:p,center:{x:s*.5,y:p*.5}},d={width:t.ref.image.width,height:t.ref.image.height},m={x:n.center.x*d.width,y:n.center.y*d.height},u={x:c.center.x-d.width*n.center.x,y:c.center.y-d.height*n.center.y},g=Math.PI*2+n.rotation%(Math.PI*2),f=n.aspectRatio||d.height/d.width,h=typeof n.scaleToFit>"u"||n.scaleToFit,I=ro(d,so(c,f),g,h?n.center:{x:.5,y:.5}),b=n.zoom*I;l&&l.length?(t.ref.createMarkup(),t.ref.markup.width=s,t.ref.markup.height=p,t.ref.markup.resize=o,t.ref.markup.dirty=r,t.ref.markup.markup=l,t.ref.markup.crop=au(d,n)):t.ref.markup&&t.ref.destroyMarkup();let E=t.ref.image;if(a){E.originX=null,E.originY=null,E.translateX=null,E.translateY=null,E.rotateZ=null,E.scaleX=null,E.scaleY=null;return}E.originX=m.x,E.originY=m.y,E.translateX=u.x,E.translateY=u.y,E.rotateZ=g,E.scaleX=b,E.scaleY=b}}),ru=e=>e.utils.createView({name:"image-preview",tag:"div",ignoreRect:!0,mixins:{apis:["image","crop","markup","resize","dirty","background"],styles:["translateY","scaleX","scaleY","opacity"],animations:{scaleX:Ce,scaleY:Ce,translateY:Ce,opacity:{type:"tween",duration:400}}},create:({root:t,props:i})=>{t.ref.clip=t.appendChildView(t.createChildView(ou(e),{id:i.id,image:i.image,crop:i.crop,markup:i.markup,resize:i.resize,dirty:i.dirty,background:i.background}))},write:({root:t,props:i,shouldOptimize:a})=>{let{clip:n}=t.ref,{image:l,crop:o,markup:r,resize:s,dirty:p}=i;if(n.crop=o,n.markup=r,n.resize=s,n.dirty=p,n.opacity=a?0:1,a||t.rect.element.hidden)return;let c=l.height/l.width,d=o.aspectRatio||c,m=t.rect.inner.width,u=t.rect.inner.height,g=t.query("GET_IMAGE_PREVIEW_HEIGHT"),f=t.query("GET_IMAGE_PREVIEW_MIN_HEIGHT"),h=t.query("GET_IMAGE_PREVIEW_MAX_HEIGHT"),I=t.query("GET_PANEL_ASPECT_RATIO"),b=t.query("GET_ALLOW_MULTIPLE");I&&!b&&(g=m*I,d=I);let E=g!==null?g:Math.max(f,Math.min(m*d,h)),v=E/d;v>m&&(v=m,E=v*d),E>u&&(E=u,v=u/d),n.width=v,n.height=E}}),su=` + + + + + + + + + + + + + + + + + +`,oo=0,cu=e=>e.utils.createView({name:"image-preview-overlay",tag:"div",ignoreRect:!0,create:({root:t,props:i})=>{let a=su;if(document.querySelector("base")){let n=new URL(window.location.href.replace(window.location.hash,"")).href;a=a.replace(/url\(\#/g,"url("+n+"#")}oo++,t.element.classList.add(`filepond--image-preview-overlay-${i.status}`),t.element.innerHTML=a.replace(/__UID__/g,oo)},mixins:{styles:["opacity"],animations:{opacity:{type:"spring",mass:25}}}}),du=function(){self.onmessage=e=>{createImageBitmap(e.data.message.file).then(t=>{self.postMessage({id:e.data.id,message:t},[t])})}},pu=function(){self.onmessage=e=>{let t=e.data.message.imageData,i=e.data.message.colorMatrix,a=t.data,n=a.length,l=i[0],o=i[1],r=i[2],s=i[3],p=i[4],c=i[5],d=i[6],m=i[7],u=i[8],g=i[9],f=i[10],h=i[11],I=i[12],b=i[13],E=i[14],v=i[15],y=i[16],T=i[17],_=i[18],x=i[19],R=0,P=0,z=0,A=0,B=0;for(;R{let i=new Image;i.onload=()=>{let a=i.naturalWidth,n=i.naturalHeight;i=null,t(a,n)},i.src=e},uu={1:()=>[1,0,0,1,0,0],2:e=>[-1,0,0,1,e,0],3:(e,t)=>[-1,0,0,-1,e,t],4:(e,t)=>[1,0,0,-1,0,t],5:()=>[0,1,1,0,0,0],6:(e,t)=>[0,1,-1,0,t,0],7:(e,t)=>[0,-1,-1,0,t,e],8:e=>[0,-1,1,0,0,e]},gu=(e,t,i,a)=>{a!==-1&&e.transform.apply(e,uu[a](t,i))},fu=(e,t,i,a)=>{t=Math.round(t),i=Math.round(i);let n=document.createElement("canvas");n.width=t,n.height=i;let l=n.getContext("2d");return a>=5&&a<=8&&([t,i]=[i,t]),gu(l,t,i,a),l.drawImage(e,0,0,t,i),n},co=e=>/^image/.test(e.type)&&!/svg/.test(e.type),hu=10,bu=10,Eu=e=>{let t=Math.min(hu/e.width,bu/e.height),i=document.createElement("canvas"),a=i.getContext("2d"),n=i.width=Math.ceil(e.width*t),l=i.height=Math.ceil(e.height*t);a.drawImage(e,0,0,n,l);let o=null;try{o=a.getImageData(0,0,n,l).data}catch{return null}let r=o.length,s=0,p=0,c=0,d=0;for(;dMath.floor(Math.sqrt(e/(t/4))),Tu=(e,t)=>(t=t||document.createElement("canvas"),t.width=e.width,t.height=e.height,t.getContext("2d").drawImage(e,0,0),t),Iu=e=>{let t;try{t=new ImageData(e.width,e.height)}catch{t=document.createElement("canvas").getContext("2d").createImageData(e.width,e.height)}return t.data.set(new Uint8ClampedArray(e.data)),t},vu=e=>new Promise((t,i)=>{let a=new Image;a.crossOrigin="Anonymous",a.onload=()=>{t(a)},a.onerror=n=>{i(n)},a.src=e}),xu=e=>{let t=cu(e),i=ru(e),{createWorker:a}=e.utils,n=(b,E,v)=>new Promise(y=>{b.ref.imageData||(b.ref.imageData=v.getContext("2d").getImageData(0,0,v.width,v.height));let T=Iu(b.ref.imageData);if(!E||E.length!==20)return v.getContext("2d").putImageData(T,0,0),y();let _=a(pu);_.post({imageData:T,colorMatrix:E},x=>{v.getContext("2d").putImageData(x,0,0),_.terminate(),y()},[T.data.buffer])}),l=(b,E)=>{b.removeChildView(E),E.image.width=1,E.image.height=1,E._destroy()},o=({root:b})=>{let E=b.ref.images.shift();return E.opacity=0,E.translateY=-15,b.ref.imageViewBin.push(E),E},r=({root:b,props:E,image:v})=>{let y=E.id,T=b.query("GET_ITEM",{id:y});if(!T)return;let _=T.getMetadata("crop")||{center:{x:.5,y:.5},flip:{horizontal:!1,vertical:!1},zoom:1,rotation:0,aspectRatio:null},x=b.query("GET_IMAGE_TRANSFORM_CANVAS_BACKGROUND_COLOR"),R,P,z=!1;b.query("GET_IMAGE_PREVIEW_MARKUP_SHOW")&&(R=T.getMetadata("markup")||[],P=T.getMetadata("resize"),z=!0);let A=b.appendChildView(b.createChildView(i,{id:y,image:v,crop:_,resize:P,markup:R,dirty:z,background:x,opacity:0,scaleX:1.15,scaleY:1.15,translateY:15}),b.childViews.length);b.ref.images.push(A),A.opacity=1,A.scaleX=1,A.scaleY=1,A.translateY=0,setTimeout(()=>{b.dispatch("DID_IMAGE_PREVIEW_SHOW",{id:y})},250)},s=({root:b,props:E})=>{let v=b.query("GET_ITEM",{id:E.id});if(!v)return;let y=b.ref.images[b.ref.images.length-1];y.crop=v.getMetadata("crop"),y.background=b.query("GET_IMAGE_TRANSFORM_CANVAS_BACKGROUND_COLOR"),b.query("GET_IMAGE_PREVIEW_MARKUP_SHOW")&&(y.dirty=!0,y.resize=v.getMetadata("resize"),y.markup=v.getMetadata("markup"))},p=({root:b,props:E,action:v})=>{if(!/crop|filter|markup|resize/.test(v.change.key)||!b.ref.images.length)return;let y=b.query("GET_ITEM",{id:E.id});if(y){if(/filter/.test(v.change.key)){let T=b.ref.images[b.ref.images.length-1];n(b,v.change.value,T.image);return}if(/crop|markup|resize/.test(v.change.key)){let T=y.getMetadata("crop"),_=b.ref.images[b.ref.images.length-1];if(T&&T.aspectRatio&&_.crop&&_.crop.aspectRatio&&Math.abs(T.aspectRatio-_.crop.aspectRatio)>1e-5){let x=o({root:b});r({root:b,props:E,image:Tu(x.image)})}else s({root:b,props:E})}}},c=b=>{let v=window.navigator.userAgent.match(/Firefox\/([0-9]+)\./),y=v?parseInt(v[1]):null;return y!==null&&y<=58?!1:"createImageBitmap"in window&&co(b)},d=({root:b,props:E})=>{let{id:v}=E,y=b.query("GET_ITEM",v);if(!y)return;let T=URL.createObjectURL(y.file);mu(T,(_,x)=>{b.dispatch("DID_IMAGE_PREVIEW_CALCULATE_SIZE",{id:v,width:_,height:x})})},m=({root:b,props:E})=>{let{id:v}=E,y=b.query("GET_ITEM",v);if(!y)return;let T=URL.createObjectURL(y.file),_=()=>{vu(T).then(x)},x=R=>{URL.revokeObjectURL(T);let z=(y.getMetadata("exif")||{}).orientation||-1,{width:A,height:B}=R;if(!A||!B)return;z>=5&&z<=8&&([A,B]=[B,A]);let w=Math.max(1,window.devicePixelRatio*.75),S=b.query("GET_IMAGE_PREVIEW_ZOOM_FACTOR")*w,L=B/A,D=b.rect.element.width,O=b.rect.element.height,U=D,C=U*L;L>1?(U=Math.min(A,D*S),C=U*L):(C=Math.min(B,O*S),U=C/L);let X=fu(R,U,C,z),K=()=>{let ce=b.query("GET_IMAGE_PREVIEW_CALCULATE_AVERAGE_IMAGE_COLOR")?Eu(data):null;y.setMetadata("color",ce,!0),"close"in R&&R.close(),b.ref.overlayShadow.opacity=1,r({root:b,props:E,image:X})},Z=y.getMetadata("filter");Z?n(b,Z,X).then(K):K()};if(c(y.file)){let R=a(du);R.post({file:y.file},P=>{if(R.terminate(),!P){_();return}x(P)})}else _()},u=({root:b})=>{let E=b.ref.images[b.ref.images.length-1];E.translateY=0,E.scaleX=1,E.scaleY=1,E.opacity=1},g=({root:b})=>{b.ref.overlayShadow.opacity=1,b.ref.overlayError.opacity=0,b.ref.overlaySuccess.opacity=0},f=({root:b})=>{b.ref.overlayShadow.opacity=.25,b.ref.overlayError.opacity=1},h=({root:b})=>{b.ref.overlayShadow.opacity=.25,b.ref.overlaySuccess.opacity=1},I=({root:b})=>{b.ref.images=[],b.ref.imageData=null,b.ref.imageViewBin=[],b.ref.overlayShadow=b.appendChildView(b.createChildView(t,{opacity:0,status:"idle"})),b.ref.overlaySuccess=b.appendChildView(b.createChildView(t,{opacity:0,status:"success"})),b.ref.overlayError=b.appendChildView(b.createChildView(t,{opacity:0,status:"failure"}))};return e.utils.createView({name:"image-preview-wrapper",create:I,styles:["height"],apis:["height"],destroy:({root:b})=>{b.ref.images.forEach(E=>{E.image.width=1,E.image.height=1})},didWriteView:({root:b})=>{b.ref.images.forEach(E=>{E.dirty=!1})},write:e.utils.createRoute({DID_IMAGE_PREVIEW_DRAW:u,DID_IMAGE_PREVIEW_CONTAINER_CREATE:d,DID_FINISH_CALCULATE_PREVIEWSIZE:m,DID_UPDATE_ITEM_METADATA:p,DID_THROW_ITEM_LOAD_ERROR:f,DID_THROW_ITEM_PROCESSING_ERROR:f,DID_THROW_ITEM_INVALID:f,DID_COMPLETE_ITEM_PROCESSING:h,DID_START_ITEM_PROCESSING:g,DID_REVERT_ITEM_PROCESSING:g},({root:b})=>{let E=b.ref.imageViewBin.filter(v=>v.opacity===0);b.ref.imageViewBin=b.ref.imageViewBin.filter(v=>v.opacity>0),E.forEach(v=>l(b,v)),E.length=0})})},po=e=>{let{addFilter:t,utils:i}=e,{Type:a,createRoute:n,isFile:l}=i,o=xu(e);return t("CREATE_VIEW",r=>{let{is:s,view:p,query:c}=r;if(!s("file")||!c("GET_ALLOW_IMAGE_PREVIEW"))return;let d=({root:h,props:I})=>{let{id:b}=I,E=c("GET_ITEM",b);if(!E||!l(E.file)||E.archived)return;let v=E.file;if(!Lm(v)||!c("GET_IMAGE_PREVIEW_FILTER_ITEM")(E))return;let y="createImageBitmap"in(window||{}),T=c("GET_IMAGE_PREVIEW_MAX_FILE_SIZE");if(!y&&T&&v.size>T)return;h.ref.imagePreview=p.appendChildView(p.createChildView(o,{id:b}));let _=h.query("GET_IMAGE_PREVIEW_HEIGHT");_&&h.dispatch("DID_UPDATE_PANEL_HEIGHT",{id:E.id,height:_});let x=!y&&v.size>c("GET_IMAGE_PREVIEW_MAX_INSTANT_PREVIEW_FILE_SIZE");h.dispatch("DID_IMAGE_PREVIEW_CONTAINER_CREATE",{id:b},x)},m=(h,I)=>{if(!h.ref.imagePreview)return;let{id:b}=I,E=h.query("GET_ITEM",{id:b});if(!E)return;let v=h.query("GET_PANEL_ASPECT_RATIO"),y=h.query("GET_ITEM_PANEL_ASPECT_RATIO"),T=h.query("GET_IMAGE_PREVIEW_HEIGHT");if(v||y||T)return;let{imageWidth:_,imageHeight:x}=h.ref;if(!_||!x)return;let R=h.query("GET_IMAGE_PREVIEW_MIN_HEIGHT"),P=h.query("GET_IMAGE_PREVIEW_MAX_HEIGHT"),A=(E.getMetadata("exif")||{}).orientation||-1;if(A>=5&&A<=8&&([_,x]=[x,_]),!co(E.file)||h.query("GET_IMAGE_PREVIEW_UPSCALE")){let D=2048/_;_*=D,x*=D}let B=x/_,w=(E.getMetadata("crop")||{}).aspectRatio||B,F=Math.max(R,Math.min(x,P)),S=h.rect.element.width,L=Math.min(S*w,F);h.dispatch("DID_UPDATE_PANEL_HEIGHT",{id:E.id,height:L})},u=({root:h})=>{h.ref.shouldRescale=!0},g=({root:h,action:I})=>{I.change.key==="crop"&&(h.ref.shouldRescale=!0)},f=({root:h,action:I})=>{h.ref.imageWidth=I.width,h.ref.imageHeight=I.height,h.ref.shouldRescale=!0,h.ref.shouldDrawPreview=!0,h.dispatch("KICK")};p.registerWriter(n({DID_RESIZE_ROOT:u,DID_STOP_RESIZE:u,DID_LOAD_ITEM:d,DID_IMAGE_PREVIEW_CALCULATE_SIZE:f,DID_UPDATE_ITEM_METADATA:g},({root:h,props:I})=>{h.ref.imagePreview&&(h.rect.element.hidden||(h.ref.shouldRescale&&(m(h,I),h.ref.shouldRescale=!1),h.ref.shouldDrawPreview&&(requestAnimationFrame(()=>{requestAnimationFrame(()=>{h.dispatch("DID_FINISH_CALCULATE_PREVIEWSIZE",{id:I.id})})}),h.ref.shouldDrawPreview=!1)))}))}),{options:{allowImagePreview:[!0,a.BOOLEAN],imagePreviewFilterItem:[()=>!0,a.FUNCTION],imagePreviewHeight:[null,a.INT],imagePreviewMinHeight:[44,a.INT],imagePreviewMaxHeight:[256,a.INT],imagePreviewMaxFileSize:[null,a.INT],imagePreviewZoomFactor:[2,a.INT],imagePreviewUpscale:[!1,a.BOOLEAN],imagePreviewMaxInstantPreviewFileSize:[1e6,a.INT],imagePreviewTransparencyIndicator:[null,a.STRING],imagePreviewCalculateAverageImageColor:[!1,a.BOOLEAN],imagePreviewMarkupShow:[!0,a.BOOLEAN],imagePreviewMarkupFilter:[()=>!0,a.FUNCTION]}}},yu=typeof window<"u"&&typeof window.document<"u";yu&&document.dispatchEvent(new CustomEvent("FilePond:pluginloaded",{detail:po}));var mo=po;var Ru=e=>/^image/.test(e.type),Su=(e,t)=>{let i=new Image;i.onload=()=>{let a=i.naturalWidth,n=i.naturalHeight;i=null,t({width:a,height:n})},i.onerror=()=>t(null),i.src=e},uo=({addFilter:e,utils:t})=>{let{Type:i}=t;return e("DID_LOAD_ITEM",(a,{query:n})=>new Promise((l,o)=>{let r=a.file;if(!Ru(r)||!n("GET_ALLOW_IMAGE_RESIZE"))return l(a);let s=n("GET_IMAGE_RESIZE_MODE"),p=n("GET_IMAGE_RESIZE_TARGET_WIDTH"),c=n("GET_IMAGE_RESIZE_TARGET_HEIGHT"),d=n("GET_IMAGE_RESIZE_UPSCALE");if(p===null&&c===null)return l(a);let m=p===null?c:p,u=c===null?m:c,g=URL.createObjectURL(r);Su(g,f=>{if(URL.revokeObjectURL(g),!f)return l(a);let{width:h,height:I}=f,b=(a.getMetadata("exif")||{}).orientation||-1;if(b>=5&&b<=8&&([h,I]=[I,h]),h===m&&I===u)return l(a);if(!d){if(s==="cover"){if(h<=m||I<=u)return l(a)}else if(h<=m&&I<=m)return l(a)}a.setMetadata("resize",{mode:s,upscale:d,size:{width:m,height:u}}),l(a)})})),{options:{allowImageResize:[!0,i.BOOLEAN],imageResizeMode:["cover",i.STRING],imageResizeUpscale:[!0,i.BOOLEAN],imageResizeTargetWidth:[null,i.INT],imageResizeTargetHeight:[null,i.INT]}}},_u=typeof window<"u"&&typeof window.document<"u";_u&&document.dispatchEvent(new CustomEvent("FilePond:pluginloaded",{detail:uo}));var go=uo;var wu=e=>/^image/.test(e.type),Lu=e=>e.substr(0,e.lastIndexOf("."))||e,Mu={jpeg:"jpg","svg+xml":"svg"},Au=(e,t)=>{let i=Lu(e),a=t.split("/")[1],n=Mu[a]||a;return`${i}.${n}`},zu=e=>/jpeg|png|svg\+xml/.test(e)?e:"image/jpeg",Pu=e=>/^image/.test(e.type),Fu={1:()=>[1,0,0,1,0,0],2:e=>[-1,0,0,1,e,0],3:(e,t)=>[-1,0,0,-1,e,t],4:(e,t)=>[1,0,0,-1,0,t],5:()=>[0,1,1,0,0,0],6:(e,t)=>[0,1,-1,0,t,0],7:(e,t)=>[0,-1,-1,0,t,e],8:e=>[0,-1,1,0,0,e]},Ou=(e,t,i)=>(i===-1&&(i=1),Fu[i](e,t)),$t=(e,t)=>({x:e,y:t}),Du=(e,t)=>e.x*t.x+e.y*t.y,fo=(e,t)=>$t(e.x-t.x,e.y-t.y),Cu=(e,t)=>Du(fo(e,t),fo(e,t)),ho=(e,t)=>Math.sqrt(Cu(e,t)),bo=(e,t)=>{let i=e,a=1.5707963267948966,n=t,l=1.5707963267948966-t,o=Math.sin(a),r=Math.sin(n),s=Math.sin(l),p=Math.cos(l),c=i/o,d=c*r,m=c*s;return $t(p*d,p*m)},Bu=(e,t)=>{let i=e.width,a=e.height,n=bo(i,t),l=bo(a,t),o=$t(e.x+Math.abs(n.x),e.y-Math.abs(n.y)),r=$t(e.x+e.width+Math.abs(l.y),e.y+Math.abs(l.x)),s=$t(e.x-Math.abs(l.y),e.y+e.height-Math.abs(l.x));return{width:ho(o,r),height:ho(o,s)}},Io=(e,t,i=0,a={x:.5,y:.5})=>{let n=a.x>.5?1-a.x:a.x,l=a.y>.5?1-a.y:a.y,o=n*2*e.width,r=l*2*e.height,s=Bu(t,i);return Math.max(s.width/o,s.height/r)},vo=(e,t)=>{let i=e.width,a=i*t;a>e.height&&(a=e.height,i=a/t);let n=(e.width-i)*.5,l=(e.height-a)*.5;return{x:n,y:l,width:i,height:a}},Eo=(e,t,i=1)=>{let a=e.height/e.width,n=1,l=t,o=1,r=a;r>l&&(r=l,o=r/a);let s=Math.max(n/o,l/r),p=e.width/(i*s*o),c=p*t;return{width:p,height:c}},xo=e=>{e.width=1,e.height=1,e.getContext("2d").clearRect(0,0,1,1)},To=e=>e&&(e.horizontal||e.vertical),ku=(e,t,i)=>{if(t<=1&&!To(i))return e.width=e.naturalWidth,e.height=e.naturalHeight,e;let a=document.createElement("canvas"),n=e.naturalWidth,l=e.naturalHeight,o=t>=5&&t<=8;o?(a.width=l,a.height=n):(a.width=n,a.height=l);let r=a.getContext("2d");if(t&&r.transform.apply(r,Ou(n,l,t)),To(i)){let s=[1,0,0,1,0,0];(!o&&i.horizontal||o&i.vertical)&&(s[0]=-1,s[4]=n),(!o&&i.vertical||o&&i.horizontal)&&(s[3]=-1,s[5]=l),r.transform(...s)}return r.drawImage(e,0,0,n,l),a},Nu=(e,t,i={},a={})=>{let{canvasMemoryLimit:n,background:l=null}=a,o=i.zoom||1,r=ku(e,t,i.flip),s={width:r.width,height:r.height},p=i.aspectRatio||s.height/s.width,c=Eo(s,p,o);if(n){let E=c.width*c.height;if(E>n){let v=Math.sqrt(n)/Math.sqrt(E);s.width=Math.floor(s.width*v),s.height=Math.floor(s.height*v),c=Eo(s,p,o)}}let d=document.createElement("canvas"),m={x:c.width*.5,y:c.height*.5},u={x:0,y:0,width:c.width,height:c.height,center:m},g=typeof i.scaleToFit>"u"||i.scaleToFit,f=o*Io(s,vo(u,p),i.rotation,g?i.center:{x:.5,y:.5});d.width=Math.round(c.width/f),d.height=Math.round(c.height/f),m.x/=f,m.y/=f;let h={x:m.x-s.width*(i.center?i.center.x:.5),y:m.y-s.height*(i.center?i.center.y:.5)},I=d.getContext("2d");l&&(I.fillStyle=l,I.fillRect(0,0,d.width,d.height)),I.translate(m.x,m.y),I.rotate(i.rotation||0),I.drawImage(r,h.x-m.x,h.y-m.y,s.width,s.height);let b=I.getImageData(0,0,d.width,d.height);return xo(d),b},Vu=typeof window<"u"&&typeof window.document<"u";Vu&&(HTMLCanvasElement.prototype.toBlob||Object.defineProperty(HTMLCanvasElement.prototype,"toBlob",{value:function(e,t,i){var a=this.toDataURL(t,i).split(",")[1];setTimeout(function(){for(var n=atob(a),l=n.length,o=new Uint8Array(l),r=0;rnew Promise(a=>{let n=i?i(e):e;Promise.resolve(n).then(l=>{l.toBlob(a,t.type,t.quality)})}),Ri=(e,t)=>Xt(e.x*t,e.y*t),Si=(e,t)=>Xt(e.x+t.x,e.y+t.y),yo=e=>{let t=Math.sqrt(e.x*e.x+e.y*e.y);return t===0?{x:0,y:0}:Xt(e.x/t,e.y/t)},qe=(e,t,i)=>{let a=Math.cos(t),n=Math.sin(t),l=Xt(e.x-i.x,e.y-i.y);return Xt(i.x+a*l.x-n*l.y,i.y+n*l.x+a*l.y)},Xt=(e=0,t=0)=>({x:e,y:t}),me=(e,t,i=1,a)=>{if(typeof e=="string")return parseFloat(e)*i;if(typeof e=="number")return e*(a?t[a]:Math.min(t.width,t.height))},dt=(e,t,i)=>{let a=e.borderStyle||e.lineStyle||"solid",n=e.backgroundColor||e.fontColor||"transparent",l=e.borderColor||e.lineColor||"transparent",o=me(e.borderWidth||e.lineWidth,t,i),r=e.lineCap||"round",s=e.lineJoin||"round",p=typeof a=="string"?"":a.map(d=>me(d,t,i)).join(","),c=e.opacity||1;return{"stroke-linecap":r,"stroke-linejoin":s,"stroke-width":o||0,"stroke-dasharray":p,stroke:l,fill:n,opacity:c}},Le=e=>e!=null,Lt=(e,t,i=1)=>{let a=me(e.x,t,i,"width")||me(e.left,t,i,"width"),n=me(e.y,t,i,"height")||me(e.top,t,i,"height"),l=me(e.width,t,i,"width"),o=me(e.height,t,i,"height"),r=me(e.right,t,i,"width"),s=me(e.bottom,t,i,"height");return Le(n)||(Le(o)&&Le(s)?n=t.height-o-s:n=s),Le(a)||(Le(l)&&Le(r)?a=t.width-l-r:a=r),Le(l)||(Le(a)&&Le(r)?l=t.width-a-r:l=0),Le(o)||(Le(n)&&Le(s)?o=t.height-n-s:o=0),{x:a||0,y:n||0,width:l||0,height:o||0}},Uu=e=>e.map((t,i)=>`${i===0?"M":"L"} ${t.x} ${t.y}`).join(" "),ke=(e,t)=>Object.keys(t).forEach(i=>e.setAttribute(i,t[i])),Hu="http://www.w3.org/2000/svg",wt=(e,t)=>{let i=document.createElementNS(Hu,e);return t&&ke(i,t),i},Wu=e=>ke(e,{...e.rect,...e.styles}),ju=e=>{let t=e.rect.x+e.rect.width*.5,i=e.rect.y+e.rect.height*.5,a=e.rect.width*.5,n=e.rect.height*.5;return ke(e,{cx:t,cy:i,rx:a,ry:n,...e.styles})},Yu={contain:"xMidYMid meet",cover:"xMidYMid slice"},qu=(e,t)=>{ke(e,{...e.rect,...e.styles,preserveAspectRatio:Yu[t.fit]||"none"})},$u={left:"start",center:"middle",right:"end"},Xu=(e,t,i,a)=>{let n=me(t.fontSize,i,a),l=t.fontFamily||"sans-serif",o=t.fontWeight||"normal",r=$u[t.textAlign]||"start";ke(e,{...e.rect,...e.styles,"stroke-width":0,"font-weight":o,"font-size":n,"font-family":l,"text-anchor":r}),e.text!==t.text&&(e.text=t.text,e.textContent=t.text.length?t.text:" ")},Ku=(e,t,i,a)=>{ke(e,{...e.rect,...e.styles,fill:"none"});let n=e.childNodes[0],l=e.childNodes[1],o=e.childNodes[2],r=e.rect,s={x:e.rect.x+e.rect.width,y:e.rect.y+e.rect.height};if(ke(n,{x1:r.x,y1:r.y,x2:s.x,y2:s.y}),!t.lineDecoration)return;l.style.display="none",o.style.display="none";let p=yo({x:s.x-r.x,y:s.y-r.y}),c=me(.05,i,a);if(t.lineDecoration.indexOf("arrow-begin")!==-1){let d=Ri(p,c),m=Si(r,d),u=qe(r,2,m),g=qe(r,-2,m);ke(l,{style:"display:block;",d:`M${u.x},${u.y} L${r.x},${r.y} L${g.x},${g.y}`})}if(t.lineDecoration.indexOf("arrow-end")!==-1){let d=Ri(p,-c),m=Si(s,d),u=qe(s,2,m),g=qe(s,-2,m);ke(o,{style:"display:block;",d:`M${u.x},${u.y} L${s.x},${s.y} L${g.x},${g.y}`})}},Zu=(e,t,i,a)=>{ke(e,{...e.styles,fill:"none",d:Uu(t.points.map(n=>({x:me(n.x,i,a,"width"),y:me(n.y,i,a,"height")})))})},yi=e=>t=>wt(e,{id:t.id}),Qu=e=>{let t=wt("image",{id:e.id,"stroke-linecap":"round","stroke-linejoin":"round",opacity:"0"});return t.onload=()=>{t.setAttribute("opacity",e.opacity||1)},t.setAttributeNS("http://www.w3.org/1999/xlink","xlink:href",e.src),t},Ju=e=>{let t=wt("g",{id:e.id,"stroke-linecap":"round","stroke-linejoin":"round"}),i=wt("line");t.appendChild(i);let a=wt("path");t.appendChild(a);let n=wt("path");return t.appendChild(n),t},eg={image:Qu,rect:yi("rect"),ellipse:yi("ellipse"),text:yi("text"),path:yi("path"),line:Ju},tg={rect:Wu,ellipse:ju,image:qu,text:Xu,path:Zu,line:Ku},ig=(e,t)=>eg[e](t),ag=(e,t,i,a,n)=>{t!=="path"&&(e.rect=Lt(i,a,n)),e.styles=dt(i,a,n),tg[t](e,i,a,n)},Ro=(e,t)=>e[1].zIndex>t[1].zIndex?1:e[1].zIndexnew Promise(n=>{let{background:l=null}=a,o=new FileReader;o.onloadend=()=>{let r=o.result,s=document.createElement("div");s.style.cssText="position:absolute;pointer-events:none;width:0;height:0;visibility:hidden;",s.innerHTML=r;let p=s.querySelector("svg");document.body.appendChild(s);let c=p.getBBox();s.parentNode.removeChild(s);let d=s.querySelector("title"),m=p.getAttribute("viewBox")||"",u=p.getAttribute("width")||"",g=p.getAttribute("height")||"",f=parseFloat(u)||null,h=parseFloat(g)||null,I=(u.match(/[a-z]+/)||[])[0]||"",b=(g.match(/[a-z]+/)||[])[0]||"",E=m.split(" ").map(parseFloat),v=E.length?{x:E[0],y:E[1],width:E[2],height:E[3]}:c,y=f??v.width,T=h??v.height;p.style.overflow="visible",p.setAttribute("width",y),p.setAttribute("height",T);let _="";if(i&&i.length){let Z={width:y,height:T};_=i.sort(Ro).reduce((ce,V)=>{let W=ig(V[0],V[1]);return ag(W,V[0],V[1],Z),W.removeAttribute("id"),W.getAttribute("opacity")===1&&W.removeAttribute("opacity"),ce+` +`+W.outerHTML+` +`},""),_=` + +${_.replace(/ /g," ")} + +`}let x=t.aspectRatio||T/y,R=y,P=R*x,z=typeof t.scaleToFit>"u"||t.scaleToFit,A=t.center?t.center.x:.5,B=t.center?t.center.y:.5,w=Io({width:y,height:T},vo({width:R,height:P},x),t.rotation,z?{x:A,y:B}:{x:.5,y:.5}),F=t.zoom*w,S=t.rotation*(180/Math.PI),L={x:R*.5,y:P*.5},D={x:L.x-y*A,y:L.y-T*B},O=[`rotate(${S} ${L.x} ${L.y})`,`translate(${L.x} ${L.y})`,`scale(${F})`,`translate(${-L.x} ${-L.y})`,`translate(${D.x} ${D.y})`],U=t.flip&&t.flip.horizontal,C=t.flip&&t.flip.vertical,X=[`scale(${U?-1:1} ${C?-1:1})`,`translate(${U?-y:0} ${C?-T:0})`],K=` + + +${d?d.textContent:""} + + +${p.outerHTML}${_} + + +`;n(K)},o.readAsText(e)}),lg=e=>{let t;try{t=new ImageData(e.width,e.height)}catch{t=document.createElement("canvas").getContext("2d").createImageData(e.width,e.height)}return t.data.set(e.data),t},og=()=>{let e={resize:c,filter:p},t=(d,m)=>(d.forEach(u=>{m=e[u.type](m,u.data)}),m),i=(d,m)=>{let u=d.transforms,g=null;if(u.forEach(f=>{f.type==="filter"&&(g=f)}),g){let f=null;u.forEach(h=>{h.type==="resize"&&(f=h)}),f&&(f.data.matrix=g.data,u=u.filter(h=>h.type!=="filter"))}m(t(u,d.imageData))};self.onmessage=d=>{i(d.data.message,m=>{self.postMessage({id:d.data.id,message:m},[m.data.buffer])})};let a=1,n=1,l=1;function o(d,m,u){let g=m[d]/255,f=m[d+1]/255,h=m[d+2]/255,I=m[d+3]/255,b=g*u[0]+f*u[1]+h*u[2]+I*u[3]+u[4],E=g*u[5]+f*u[6]+h*u[7]+I*u[8]+u[9],v=g*u[10]+f*u[11]+h*u[12]+I*u[13]+u[14],y=g*u[15]+f*u[16]+h*u[17]+I*u[18]+u[19],T=Math.max(0,b*y)+a*(1-y),_=Math.max(0,E*y)+n*(1-y),x=Math.max(0,v*y)+l*(1-y);m[d]=Math.max(0,Math.min(1,T))*255,m[d+1]=Math.max(0,Math.min(1,_))*255,m[d+2]=Math.max(0,Math.min(1,x))*255}let r=self.JSON.stringify([1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0]);function s(d){return self.JSON.stringify(d||[])===r}function p(d,m){if(!m||s(m))return d;let u=d.data,g=u.length,f=m[0],h=m[1],I=m[2],b=m[3],E=m[4],v=m[5],y=m[6],T=m[7],_=m[8],x=m[9],R=m[10],P=m[11],z=m[12],A=m[13],B=m[14],w=m[15],F=m[16],S=m[17],L=m[18],D=m[19],O=0,U=0,C=0,X=0,K=0,Z=0,ce=0,V=0,W=0,$=0,ie=0,ee=0;for(;O1&&g===!1)return p(d,I);f=d.width*w,h=d.height*w}let b=d.width,E=d.height,v=Math.round(f),y=Math.round(h),T=d.data,_=new Uint8ClampedArray(v*y*4),x=b/v,R=E/y,P=Math.ceil(x*.5),z=Math.ceil(R*.5);for(let A=0;A=-1&&ie<=1&&(F=2*ie*ie*ie-3*ie*ie+1,F>0)){$=4*(W+K*b);let ee=T[$+3];C+=F*ee,L+=F,ee<255&&(F=F*ee/250),D+=F*T[$],O+=F*T[$+1],U+=F*T[$+2],S+=F}}}_[w]=D/S,_[w+1]=O/S,_[w+2]=U/S,_[w+3]=C/L,I&&o(w,_,I)}return{data:_,width:v,height:y}}},rg=(e,t)=>{if(e.getUint32(t+4,!1)!==1165519206)return;t+=4;let i=e.getUint16(t+=6,!1)===18761;t+=e.getUint32(t+4,i);let a=e.getUint16(t,i);t+=2;for(let n=0;n{let t=new DataView(e);if(t.getUint16(0)!==65496)return null;let i=2,a,n,l=!1;for(;i=65504&&a<=65519||a===65534)||(l||(l=rg(t,i,n)),i+n>t.byteLength)));)i+=n;return e.slice(0,i)},cg=e=>new Promise(t=>{let i=new FileReader;i.onload=()=>t(sg(i.result)||null),i.readAsArrayBuffer(e.slice(0,256*1024))}),dg=()=>window.BlobBuilder=window.BlobBuilder||window.WebKitBlobBuilder||window.MozBlobBuilder||window.MSBlobBuilder,pg=(e,t)=>{let i=dg();if(i){let a=new i;return a.append(e),a.getBlob(t)}return new Blob([e],{type:t})},mg=()=>Math.random().toString(36).substr(2,9),ug=e=>{let t=new Blob(["(",e.toString(),")()"],{type:"application/javascript"}),i=URL.createObjectURL(t),a=new Worker(i),n=[];return{transfer:()=>{},post:(l,o,r)=>{let s=mg();n[s]=o,a.onmessage=p=>{let c=n[p.data.id];c&&(c(p.data.message),delete n[p.data.id])},a.postMessage({id:s,message:l},r)},terminate:()=>{a.terminate(),URL.revokeObjectURL(i)}}},gg=e=>new Promise((t,i)=>{let a=new Image;a.onload=()=>{t(a)},a.onerror=n=>{i(n)},a.src=e}),fg=e=>e.reduce((t,i)=>t.then(a=>i().then(Array.prototype.concat.bind(a))),Promise.resolve([])),hg=(e,t)=>new Promise(i=>{let a={width:e.width,height:e.height},n=e.getContext("2d"),l=t.sort(Ro).map(o=>()=>new Promise(r=>{yg[o[0]](n,a,o[1],r)&&r()}));fg(l).then(()=>i(e))}),Mt=(e,t)=>{e.beginPath(),e.lineCap=t["stroke-linecap"],e.lineJoin=t["stroke-linejoin"],e.lineWidth=t["stroke-width"],t["stroke-dasharray"].length&&e.setLineDash(t["stroke-dasharray"].split(",")),e.fillStyle=t.fill,e.strokeStyle=t.stroke,e.globalAlpha=t.opacity||1},At=e=>{e.fill(),e.stroke(),e.globalAlpha=1},bg=(e,t,i)=>{let a=Lt(i,t),n=dt(i,t);return Mt(e,n),e.rect(a.x,a.y,a.width,a.height),At(e,n),!0},Eg=(e,t,i)=>{let a=Lt(i,t),n=dt(i,t);Mt(e,n);let l=a.x,o=a.y,r=a.width,s=a.height,p=.5522848,c=r/2*p,d=s/2*p,m=l+r,u=o+s,g=l+r/2,f=o+s/2;return e.moveTo(l,f),e.bezierCurveTo(l,f-d,g-c,o,g,o),e.bezierCurveTo(g+c,o,m,f-d,m,f),e.bezierCurveTo(m,f+d,g+c,u,g,u),e.bezierCurveTo(g-c,u,l,f+d,l,f),At(e,n),!0},Tg=(e,t,i,a)=>{let n=Lt(i,t),l=dt(i,t);Mt(e,l);let o=new Image;new URL(i.src,window.location.href).origin!==window.location.origin&&(o.crossOrigin=""),o.onload=()=>{if(i.fit==="cover"){let s=n.width/n.height,p=s>1?o.width:o.height*s,c=s>1?o.width/s:o.height,d=o.width*.5-p*.5,m=o.height*.5-c*.5;e.drawImage(o,d,m,p,c,n.x,n.y,n.width,n.height)}else if(i.fit==="contain"){let s=Math.min(n.width/o.width,n.height/o.height),p=s*o.width,c=s*o.height,d=n.x+n.width*.5-p*.5,m=n.y+n.height*.5-c*.5;e.drawImage(o,0,0,o.width,o.height,d,m,p,c)}else e.drawImage(o,0,0,o.width,o.height,n.x,n.y,n.width,n.height);At(e,l),a()},o.src=i.src},Ig=(e,t,i)=>{let a=Lt(i,t),n=dt(i,t);Mt(e,n);let l=me(i.fontSize,t),o=i.fontFamily||"sans-serif",r=i.fontWeight||"normal",s=i.textAlign||"left";return e.font=`${r} ${l}px ${o}`,e.textAlign=s,e.fillText(i.text,a.x,a.y),At(e,n),!0},vg=(e,t,i)=>{let a=dt(i,t);Mt(e,a),e.beginPath();let n=i.points.map(o=>({x:me(o.x,t,1,"width"),y:me(o.y,t,1,"height")}));e.moveTo(n[0].x,n[0].y);let l=n.length;for(let o=1;o{let a=Lt(i,t),n=dt(i,t);Mt(e,n),e.beginPath();let l={x:a.x,y:a.y},o={x:a.x+a.width,y:a.y+a.height};e.moveTo(l.x,l.y),e.lineTo(o.x,o.y);let r=yo({x:o.x-l.x,y:o.y-l.y}),s=.04*Math.min(t.width,t.height);if(i.lineDecoration.indexOf("arrow-begin")!==-1){let p=Ri(r,s),c=Si(l,p),d=qe(l,2,c),m=qe(l,-2,c);e.moveTo(d.x,d.y),e.lineTo(l.x,l.y),e.lineTo(m.x,m.y)}if(i.lineDecoration.indexOf("arrow-end")!==-1){let p=Ri(r,-s),c=Si(o,p),d=qe(o,2,c),m=qe(o,-2,c);e.moveTo(d.x,d.y),e.lineTo(o.x,o.y),e.lineTo(m.x,m.y)}return At(e,n),!0},yg={rect:bg,ellipse:Eg,image:Tg,text:Ig,line:xg,path:vg},Rg=e=>{let t=document.createElement("canvas");return t.width=e.width,t.height=e.height,t.getContext("2d").putImageData(e,0,0),t},Sg=(e,t,i={})=>new Promise((a,n)=>{if(!e||!Pu(e))return n({status:"not an image file",file:e});let{stripImageHead:l,beforeCreateBlob:o,afterCreateBlob:r,canvasMemoryLimit:s}=i,{crop:p,size:c,filter:d,markup:m,output:u}=t,g=t.image&&t.image.orientation?Math.max(1,Math.min(8,t.image.orientation)):null,f=u&&u.quality,h=f===null?null:f/100,I=u&&u.type||null,b=u&&u.background||null,E=[];c&&(typeof c.width=="number"||typeof c.height=="number")&&E.push({type:"resize",data:c}),d&&d.length===20&&E.push({type:"filter",data:d});let v=_=>{let x=r?r(_):_;Promise.resolve(x).then(a)},y=(_,x)=>{let R=Rg(_),P=m.length?hg(R,m):R;Promise.resolve(P).then(z=>{Gu(z,x,o).then(A=>{if(xo(z),l)return v(A);cg(e).then(B=>{B!==null&&(A=new Blob([B,A.slice(20)],{type:A.type})),v(A)})}).catch(n)})};if(/svg/.test(e.type)&&I===null)return ng(e,p,m,{background:b}).then(_=>{a(pg(_,"image/svg+xml"))});let T=URL.createObjectURL(e);gg(T).then(_=>{URL.revokeObjectURL(T);let x=Nu(_,g,p,{canvasMemoryLimit:s,background:b}),R={quality:h,type:I||e.type};if(!E.length)return y(x,R);let P=ug(og);P.post({transforms:E,imageData:x},z=>{y(lg(z),R),P.terminate()},[x.data.buffer])}).catch(n)}),_g=["x","y","left","top","right","bottom","width","height"],wg=e=>typeof e=="string"&&/%/.test(e)?parseFloat(e)/100:e,Lg=e=>{let[t,i]=e,a=i.points?{}:_g.reduce((n,l)=>(n[l]=wg(i[l]),n),{});return[t,{zIndex:0,...i,...a}]},Mg=e=>new Promise((t,i)=>{let a=new Image;a.src=URL.createObjectURL(e);let n=()=>{let o=a.naturalWidth,r=a.naturalHeight;o&&r&&(URL.revokeObjectURL(a.src),clearInterval(l),t({width:o,height:r}))};a.onerror=o=>{URL.revokeObjectURL(a.src),clearInterval(l),i(o)};let l=setInterval(n,1);n()});typeof window<"u"&&typeof window.document<"u"&&(HTMLCanvasElement.prototype.toBlob||Object.defineProperty(HTMLCanvasElement.prototype,"toBlob",{value:function(e,t,i){let a=this;setTimeout(()=>{let n=a.toDataURL(t,i).split(",")[1],l=atob(n),o=l.length,r=new Uint8Array(o);for(;o--;)r[o]=l.charCodeAt(o);e(new Blob([r],{type:t||"image/png"}))})}}));var wa=typeof window<"u"&&typeof window.document<"u",Ag=wa&&/iPad|iPhone|iPod/.test(navigator.userAgent)&&!window.MSStream,So=({addFilter:e,utils:t})=>{let{Type:i,forin:a,getFileFromBlob:n,isFile:l}=t,o=["crop","resize","filter","markup","output"],r=c=>(d,m,u)=>d(m,c?c(u):u),s=c=>c.aspectRatio===null&&c.rotation===0&&c.zoom===1&&c.center&&c.center.x===.5&&c.center.y===.5&&c.flip&&c.flip.horizontal===!1&&c.flip.vertical===!1;e("SHOULD_PREPARE_OUTPUT",(c,{query:d})=>new Promise(m=>{m(!d("IS_ASYNC"))}));let p=(c,d,m)=>new Promise(u=>{if(!c("GET_ALLOW_IMAGE_TRANSFORM")||m.archived||!l(d)||!wu(d))return u(!1);Mg(d).then(()=>{let g=c("GET_IMAGE_TRANSFORM_IMAGE_FILTER");if(g){let f=g(d);if(f==null)return handleRevert(!0);if(typeof f=="boolean")return u(f);if(typeof f.then=="function")return f.then(u)}u(!0)}).catch(g=>{u(!1)})});return e("DID_CREATE_ITEM",(c,{query:d,dispatch:m})=>{d("GET_ALLOW_IMAGE_TRANSFORM")&&c.extend("requestPrepare",()=>new Promise((u,g)=>{m("REQUEST_PREPARE_OUTPUT",{query:c.id,item:c,success:u,failure:g},!0)}))}),e("PREPARE_OUTPUT",(c,{query:d,item:m})=>new Promise(u=>{p(d,c,m).then(g=>{if(!g)return u(c);let f=[];d("GET_IMAGE_TRANSFORM_VARIANTS_INCLUDE_ORIGINAL")&&f.push(()=>new Promise(x=>{x({name:d("GET_IMAGE_TRANSFORM_VARIANTS_ORIGINAL_NAME"),file:c})})),d("GET_IMAGE_TRANSFORM_VARIANTS_INCLUDE_DEFAULT")&&f.push((x,R,P)=>new Promise(z=>{x(R,P).then(A=>z({name:d("GET_IMAGE_TRANSFORM_VARIANTS_DEFAULT_NAME"),file:A}))}));let h=d("GET_IMAGE_TRANSFORM_VARIANTS")||{};a(h,(x,R)=>{let P=r(R);f.push((z,A,B)=>new Promise(w=>{P(z,A,B).then(F=>w({name:x,file:F}))}))});let I=d("GET_IMAGE_TRANSFORM_OUTPUT_QUALITY"),b=d("GET_IMAGE_TRANSFORM_OUTPUT_QUALITY_MODE"),E=I===null?null:I/100,v=d("GET_IMAGE_TRANSFORM_OUTPUT_MIME_TYPE"),y=d("GET_IMAGE_TRANSFORM_CLIENT_TRANSFORMS")||o;m.setMetadata("output",{type:v,quality:E,client:y},!0);let T=(x,R)=>new Promise((P,z)=>{let A={...R};Object.keys(A).filter(C=>C!=="exif").forEach(C=>{y.indexOf(C)===-1&&delete A[C]});let{resize:B,exif:w,output:F,crop:S,filter:L,markup:D}=A,O={image:{orientation:w?w.orientation:null},output:F&&(F.type||typeof F.quality=="number"||F.background)?{type:F.type,quality:typeof F.quality=="number"?F.quality*100:null,background:F.background||d("GET_IMAGE_TRANSFORM_CANVAS_BACKGROUND_COLOR")||null}:void 0,size:B&&(B.size.width||B.size.height)?{mode:B.mode,upscale:B.upscale,...B.size}:void 0,crop:S&&!s(S)?{...S}:void 0,markup:D&&D.length?D.map(Lg):[],filter:L};if(O.output){let C=F.type?F.type!==x.type:!1,X=/\/jpe?g$/.test(x.type),K=F.quality!==null?X&&b==="always":!1;if(!!!(O.size||O.crop||O.filter||C||K))return P(x)}let U={beforeCreateBlob:d("GET_IMAGE_TRANSFORM_BEFORE_CREATE_BLOB"),afterCreateBlob:d("GET_IMAGE_TRANSFORM_AFTER_CREATE_BLOB"),canvasMemoryLimit:d("GET_IMAGE_TRANSFORM_CANVAS_MEMORY_LIMIT"),stripImageHead:d("GET_IMAGE_TRANSFORM_OUTPUT_STRIP_IMAGE_HEAD")};Sg(x,O,U).then(C=>{let X=n(C,Au(x.name,zu(C.type)));P(X)}).catch(z)}),_=f.map(x=>x(T,c,m.getMetadata()));Promise.all(_).then(x=>{u(x.length===1&&x[0].name===null?x[0].file:x)})})})),{options:{allowImageTransform:[!0,i.BOOLEAN],imageTransformImageFilter:[null,i.FUNCTION],imageTransformOutputMimeType:[null,i.STRING],imageTransformOutputQuality:[null,i.INT],imageTransformOutputStripImageHead:[!0,i.BOOLEAN],imageTransformClientTransforms:[null,i.ARRAY],imageTransformOutputQualityMode:["always",i.STRING],imageTransformVariants:[null,i.OBJECT],imageTransformVariantsIncludeDefault:[!0,i.BOOLEAN],imageTransformVariantsDefaultName:[null,i.STRING],imageTransformVariantsIncludeOriginal:[!1,i.BOOLEAN],imageTransformVariantsOriginalName:["original_",i.STRING],imageTransformBeforeCreateBlob:[null,i.FUNCTION],imageTransformAfterCreateBlob:[null,i.FUNCTION],imageTransformCanvasMemoryLimit:[wa&&Ag?4096*4096:null,i.INT],imageTransformCanvasBackgroundColor:[null,i.STRING]}}};wa&&document.dispatchEvent(new CustomEvent("FilePond:pluginloaded",{detail:So}));var _o=So;var La=e=>/^video/.test(e.type),Kt=e=>/^audio/.test(e.type),Ma=class{constructor(t,i){this.mediaEl=t,this.audioElems=i,this.onplayhead=!1,this.duration=0,this.timelineWidth=this.audioElems.timeline.offsetWidth-this.audioElems.playhead.offsetWidth,this.moveplayheadFn=this.moveplayhead.bind(this),this.registerListeners()}registerListeners(){this.mediaEl.addEventListener("timeupdate",this.timeUpdate.bind(this),!1),this.mediaEl.addEventListener("canplaythrough",()=>this.duration=this.mediaEl.duration,!1),this.audioElems.timeline.addEventListener("click",this.timelineClicked.bind(this),!1),this.audioElems.button.addEventListener("click",this.play.bind(this)),this.audioElems.playhead.addEventListener("mousedown",this.mouseDown.bind(this),!1),window.addEventListener("mouseup",this.mouseUp.bind(this),!1)}play(){this.mediaEl.paused?this.mediaEl.play():this.mediaEl.pause(),this.audioElems.button.classList.toggle("play"),this.audioElems.button.classList.toggle("pause")}timeUpdate(){let t=this.mediaEl.currentTime/this.duration*100;this.audioElems.playhead.style.marginLeft=t+"%",this.mediaEl.currentTime===this.duration&&(this.audioElems.button.classList.toggle("play"),this.audioElems.button.classList.toggle("pause"))}moveplayhead(t){let i=t.clientX-this.getPosition(this.audioElems.timeline);i>=0&&i<=this.timelineWidth&&(this.audioElems.playhead.style.marginLeft=i+"px"),i<0&&(this.audioElems.playhead.style.marginLeft="0px"),i>this.timelineWidth&&(this.audioElems.playhead.style.marginLeft=this.timelineWidth-4+"px")}timelineClicked(t){this.moveplayhead(t),this.mediaEl.currentTime=this.duration*this.clickPercent(t)}mouseDown(){this.onplayhead=!0,window.addEventListener("mousemove",this.moveplayheadFn,!0),this.mediaEl.removeEventListener("timeupdate",this.timeUpdate.bind(this),!1)}mouseUp(t){window.removeEventListener("mousemove",this.moveplayheadFn,!0),this.onplayhead==!0&&(this.moveplayhead(t),this.mediaEl.currentTime=this.duration*this.clickPercent(t),this.mediaEl.addEventListener("timeupdate",this.timeUpdate.bind(this),!1)),this.onplayhead=!1}clickPercent(t){return(t.clientX-this.getPosition(this.audioElems.timeline))/this.timelineWidth}getPosition(t){return t.getBoundingClientRect().left}},zg=e=>e.utils.createView({name:"media-preview",tag:"div",ignoreRect:!0,create:({root:t,props:i})=>{let{id:a}=i,n=t.query("GET_ITEM",{id:i.id}),l=Kt(n.file)?"audio":"video";if(t.ref.media=document.createElement(l),t.ref.media.setAttribute("controls",!0),t.element.appendChild(t.ref.media),Kt(n.file)){let o=document.createDocumentFragment();t.ref.audio=[],t.ref.audio.container=document.createElement("div"),t.ref.audio.button=document.createElement("span"),t.ref.audio.timeline=document.createElement("div"),t.ref.audio.playhead=document.createElement("div"),t.ref.audio.container.className="audioplayer",t.ref.audio.button.className="playpausebtn play",t.ref.audio.timeline.className="timeline",t.ref.audio.playhead.className="playhead",t.ref.audio.timeline.appendChild(t.ref.audio.playhead),t.ref.audio.container.appendChild(t.ref.audio.button),t.ref.audio.container.appendChild(t.ref.audio.timeline),o.appendChild(t.ref.audio.container),t.element.appendChild(o)}},write:e.utils.createRoute({DID_MEDIA_PREVIEW_LOAD:({root:t,props:i})=>{let{id:a}=i,n=t.query("GET_ITEM",{id:i.id});if(!n)return;let l=window.URL||window.webkitURL,o=new Blob([n.file],{type:n.file.type});t.ref.media.type=n.file.type,t.ref.media.src=n.file.mock&&n.file.url||l.createObjectURL(o),Kt(n.file)&&new Ma(t.ref.media,t.ref.audio),t.ref.media.addEventListener("loadeddata",()=>{let r=75;if(La(n.file)){let s=t.ref.media.offsetWidth,p=t.ref.media.videoWidth/s;r=t.ref.media.videoHeight/p}t.dispatch("DID_UPDATE_PANEL_HEIGHT",{id:i.id,height:r})},!1)}})}),Pg=e=>{let t=({root:a,props:n})=>{let{id:l}=n;a.query("GET_ITEM",l)&&a.dispatch("DID_MEDIA_PREVIEW_LOAD",{id:l})},i=({root:a,props:n})=>{let l=zg(e);a.ref.media=a.appendChildView(a.createChildView(l,{id:n.id}))};return e.utils.createView({name:"media-preview-wrapper",create:i,write:e.utils.createRoute({DID_MEDIA_PREVIEW_CONTAINER_CREATE:t})})},Aa=e=>{let{addFilter:t,utils:i}=e,{Type:a,createRoute:n}=i,l=Pg(e);return t("CREATE_VIEW",o=>{let{is:r,view:s,query:p}=o;if(!r("file"))return;let c=({root:d,props:m})=>{let{id:u}=m,g=p("GET_ITEM",u),f=p("GET_ALLOW_VIDEO_PREVIEW"),h=p("GET_ALLOW_AUDIO_PREVIEW");!g||g.archived||(!La(g.file)||!f)&&(!Kt(g.file)||!h)||(d.ref.mediaPreview=s.appendChildView(s.createChildView(l,{id:u})),d.dispatch("DID_MEDIA_PREVIEW_CONTAINER_CREATE",{id:u}))};s.registerWriter(n({DID_LOAD_ITEM:c},({root:d,props:m})=>{let{id:u}=m,g=p("GET_ITEM",u),f=d.query("GET_ALLOW_VIDEO_PREVIEW"),h=d.query("GET_ALLOW_AUDIO_PREVIEW");!g||(!La(g.file)||!f)&&(!Kt(g.file)||!h)||d.rect.element.hidden}))}),{options:{allowVideoPreview:[!0,a.BOOLEAN],allowAudioPreview:[!0,a.BOOLEAN]}}},Fg=typeof window<"u"&&typeof window.document<"u";Fg&&document.dispatchEvent(new CustomEvent("FilePond:pluginloaded",{detail:Aa}));var wo={labelIdle:'\u134B\u12ED\u120E\u127D \u1235\u1260\u12CD \u12A5\u12DA\u1205 \u130B\u122D \u12ED\u120D\u1240\u1241\u1275 \u12C8\u12ED\u121D \u134B\u12ED\u1209\u1295 \u12ED\u121D\u1228\u1321 ',labelInvalidField:"\u1218\u1235\u12A9 \u120D\u12AD \u12EB\u120D\u1206\u1291 \u134B\u12ED\u120E\u127D\u1295 \u12ED\u12DF\u120D",labelFileWaitingForSize:"\u12E8\u134B\u12ED\u1209\u1295 \u1218\u1320\u1295 \u1260\u1218\u1320\u1263\u1260\u1245 \u120B\u12ED",labelFileSizeNotAvailable:"\u12E8\u134B\u12ED\u1209\u1295 \u1218\u1320\u1295 \u120A\u1308\u129D \u12A0\u120D\u127B\u1208\u121D",labelFileLoading:"\u1260\u121B\u1295\u1260\u1265 \u120B\u12ED",labelFileLoadError:"\u1260\u121B\u1295\u1260\u1265 \u120B\u12ED \u127D\u130D\u122D \u1270\u1348\u1325\u122F\u120D",labelFileProcessing:"\u134B\u12ED\u1209\u1295 \u1260\u1218\u132B\u1295 \u120B\u12ED",labelFileProcessingComplete:"\u134B\u12ED\u1209\u1295 \u1218\u132B\u1295 \u1270\u1320\u1293\u1245\u124B\u120D",labelFileProcessingAborted:"\u134B\u12ED\u1209\u1295 \u1218\u132B\u1295 \u1270\u124B\u122D\u1327\u120D",labelFileProcessingError:"\u134B\u12ED\u1209\u1295 \u1260\u1218\u132B\u1295 \u120B\u12ED \u127D\u130D\u122D \u1270\u1348\u1325\u122F\u120D",labelFileProcessingRevertError:"\u1348\u12ED\u1209\u1295 \u1260\u1218\u1240\u120D\u1260\u1235 \u120B\u12ED \u127D\u130D\u122D \u1270\u1348\u1325\u122F\u120D",labelFileRemoveError:"\u1260\u121B\u1325\u134B\u1275 \u120B\u12ED \u127D\u130D\u122D \u1270\u1348\u1325\u122F\u120D",labelTapToCancel:"\u1208\u121B\u124B\u1228\u1325 \u1290\u12AB \u12EB\u12F5\u122D\u1309",labelTapToRetry:"\u12F0\u130D\u121E \u1208\u1218\u121E\u12A8\u122D \u1290\u12AB \u12EB\u12F5\u122D\u1309",labelTapToUndo:"\u12C8\u12F0\u1290\u1260\u1228\u1260\u1275 \u1208\u1218\u1218\u1208\u1235 \u1290\u12AB \u12EB\u12F5\u122D\u1309",labelButtonRemoveItem:"\u120B\u1325\u134B",labelButtonAbortItemLoad:"\u120B\u124B\u122D\u1325",labelButtonRetryItemLoad:"\u12F0\u130D\u121C \u120D\u121E\u12AD\u122D",labelButtonAbortItemProcessing:"\u12ED\u1245\u122D",labelButtonUndoItemProcessing:"\u12C8\u12F0\u1290\u1260\u1228\u1260\u1275 \u120D\u1218\u120D\u1235",labelButtonRetryItemProcessing:"\u12F0\u130D\u121C \u120D\u121E\u12AD\u122D",labelButtonProcessItem:"\u120D\u132B\u1295",labelMaxFileSizeExceeded:"\u134B\u12ED\u1209 \u1270\u120D\u124B\u120D",labelMaxFileSize:"\u12E8\u134B\u12ED\u120D \u1218\u1320\u1295 \u12A8 {filesize} \u1218\u1265\u1208\u1325 \u12A0\u12ED\u1348\u1240\u12F5\u121D",labelMaxTotalFileSizeExceeded:"\u12E8\u121A\u1348\u1240\u12F0\u12CD\u1295 \u1320\u1245\u120B\u120B \u12E8\u134B\u12ED\u120D \u1218\u1320\u1295 \u12A0\u120D\u1348\u12CB\u120D",labelMaxTotalFileSize:"\u1320\u1245\u120B\u120B \u12E8\u134B\u12ED\u120D \u1218\u1320\u1295 \u12A8 {filesize} \u1218\u1265\u1208\u1325 \u12A0\u12ED\u1348\u1240\u12F5\u121D",labelFileTypeNotAllowed:"\u12E8\u1270\u1233\u1233\u1270 \u12E8\u134B\u12ED\u120D \u12A0\u12ED\u1290\u1275 \u1290\u12CD",fileValidateTypeLabelExpectedTypes:"\u12E8\u134B\u12ED\u120D \u12A0\u12ED\u1290\u1271 \u1218\u1206\u1295 \u12E8\u121A\u1308\u1263\u12CD {allButLastType} \u12A5\u1293 {lastType} \u1290\u12CD",imageValidateSizeLabelFormatError:"\u12E8\u121D\u1235\u120D \u12A0\u12ED\u1290\u1271 \u1208\u1218\u132B\u1295 \u12A0\u12ED\u1206\u1295\u121D",imageValidateSizeLabelImageSizeTooSmall:"\u121D\u1235\u1209 \u1260\u1323\u121D \u12A0\u1295\u1237\u120D",imageValidateSizeLabelImageSizeTooBig:"\u121D\u1235\u1209 \u1260\u1323\u121D \u1270\u120D\u124B\u120D",imageValidateSizeLabelExpectedMinSize:"\u12DD\u1245\u1270\u129B\u12CD \u12E8\u121D\u1235\u120D \u120D\u12AC\u1275 {minWidth} \xD7 {minHeight} \u1290\u12CD",imageValidateSizeLabelExpectedMaxSize:"\u12A8\u134D\u1270\u129B\u12CD \u12E8\u121D\u1235\u120D \u120D\u12AC\u1275 {maxWidth} \xD7 {maxHeight} \u1290\u12CD",imageValidateSizeLabelImageResolutionTooLow:"\u12E8\u121D\u1235\u1209 \u1325\u122B\u1275 \u1260\u1323\u121D \u12DD\u1245\u1270\u129B \u1290\u12CD",imageValidateSizeLabelImageResolutionTooHigh:"\u12E8\u121D\u1235\u1209 \u1325\u122B\u1275 \u1260\u1323\u121D \u12A8\u134D\u1270\u129B \u1290\u12CD",imageValidateSizeLabelExpectedMinResolution:"\u12DD\u1245\u1270\u129B\u12CD \u12E8\u121D\u1235\u120D \u1325\u122B\u1275 {minResolution} \u1290\u12CD",imageValidateSizeLabelExpectedMaxResolution:"\u12A8\u134D\u1270\u129B\u12CD \u12E8\u121D\u1235\u120D \u1325\u122B\u1275 {maxResolution} \u1290\u12CD"};var Lo={labelIdle:'\u0627\u0633\u062D\u0628 \u0648 \u0627\u062F\u0631\u062C \u0645\u0644\u0641\u0627\u062A\u0643 \u0623\u0648 \u062A\u0635\u0641\u062D ',labelInvalidField:"\u0627\u0644\u062D\u0642\u0644 \u064A\u062D\u062A\u0648\u064A \u0639\u0644\u0649 \u0645\u0644\u0641\u0627\u062A \u063A\u064A\u0631 \u0635\u0627\u0644\u062D\u0629",labelFileWaitingForSize:"\u0628\u0627\u0646\u062A\u0638\u0627\u0631 \u0627\u0644\u062D\u062C\u0645",labelFileSizeNotAvailable:"\u0627\u0644\u062D\u062C\u0645 \u063A\u064A\u0631 \u0645\u062A\u0627\u062D",labelFileLoading:"\u0628\u0627\u0644\u0625\u0646\u062A\u0638\u0627\u0631",labelFileLoadError:"\u062D\u062F\u062B \u062E\u0637\u0623 \u0623\u062B\u0646\u0627\u0621 \u0627\u0644\u062A\u062D\u0645\u064A\u0644",labelFileProcessing:"\u064A\u062A\u0645 \u0627\u0644\u0631\u0641\u0639",labelFileProcessingComplete:"\u062A\u0645 \u0627\u0644\u0631\u0641\u0639",labelFileProcessingAborted:"\u062A\u0645 \u0625\u0644\u063A\u0627\u0621 \u0627\u0644\u0631\u0641\u0639",labelFileProcessingError:"\u062D\u062F\u062B \u062E\u0637\u0623 \u0623\u062B\u0646\u0627\u0621 \u0627\u0644\u0631\u0641\u0639",labelFileProcessingRevertError:"\u062D\u062F\u062B \u062E\u0637\u0623 \u0623\u062B\u0646\u0627\u0621 \u0627\u0644\u062A\u0631\u0627\u062C\u0639",labelFileRemoveError:"\u062D\u062F\u062B \u062E\u0637\u0623 \u0623\u062B\u0646\u0627\u0621 \u0627\u0644\u062D\u0630\u0641",labelTapToCancel:"\u0627\u0646\u0642\u0631 \u0644\u0644\u0625\u0644\u063A\u0627\u0621",labelTapToRetry:"\u0627\u0646\u0642\u0631 \u0644\u0625\u0639\u0627\u062F\u0629 \u0627\u0644\u0645\u062D\u0627\u0648\u0644\u0629",labelTapToUndo:"\u0627\u0646\u0642\u0631 \u0644\u0644\u062A\u0631\u0627\u062C\u0639",labelButtonRemoveItem:"\u0645\u0633\u062D",labelButtonAbortItemLoad:"\u0625\u0644\u063A\u0627\u0621",labelButtonRetryItemLoad:"\u0625\u0639\u0627\u062F\u0629",labelButtonAbortItemProcessing:"\u0625\u0644\u063A\u0627\u0621",labelButtonUndoItemProcessing:"\u062A\u0631\u0627\u062C\u0639",labelButtonRetryItemProcessing:"\u0625\u0639\u0627\u062F\u0629",labelButtonProcessItem:"\u0631\u0641\u0639",labelMaxFileSizeExceeded:"\u0627\u0644\u0645\u0644\u0641 \u0643\u0628\u064A\u0631 \u062C\u062F\u0627",labelMaxFileSize:"\u062D\u062C\u0645 \u0627\u0644\u0645\u0644\u0641 \u0627\u0644\u0623\u0642\u0635\u0649: {filesize}",labelMaxTotalFileSizeExceeded:"\u062A\u0645 \u062A\u062C\u0627\u0648\u0632 \u0627\u0644\u062D\u062F \u0627\u0644\u0623\u0642\u0635\u0649 \u0644\u0644\u062D\u062C\u0645 \u0627\u0644\u0625\u062C\u0645\u0627\u0644\u064A",labelMaxTotalFileSize:"\u0627\u0644\u062D\u062F \u0627\u0644\u0623\u0642\u0635\u0649 \u0644\u062D\u062C\u0645 \u0627\u0644\u0645\u0644\u0641: {filesize}",labelFileTypeNotAllowed:"\u0645\u0644\u0641 \u0645\u0646 \u0646\u0648\u0639 \u063A\u064A\u0631 \u0635\u0627\u0644\u062D",fileValidateTypeLabelExpectedTypes:"\u062A\u062A\u0648\u0642\u0639 {allButLastType} \u0645\u0646 {lastType}",imageValidateSizeLabelFormatError:"\u0646\u0648\u0639 \u0627\u0644\u0635\u0648\u0631\u0629 \u063A\u064A\u0631 \u0645\u062F\u0639\u0648\u0645",imageValidateSizeLabelImageSizeTooSmall:"\u0627\u0644\u0635\u0648\u0631\u0629 \u0635\u063A\u064A\u0631 \u062C\u062F\u0627",imageValidateSizeLabelImageSizeTooBig:"\u0627\u0644\u0635\u0648\u0631\u0629 \u0643\u0628\u064A\u0631\u0629 \u062C\u062F\u0627",imageValidateSizeLabelExpectedMinSize:"\u0627\u0644\u062D\u062F \u0627\u0644\u0623\u062F\u0646\u0649 \u0644\u0644\u0623\u0628\u0639\u0627\u062F \u0647\u0648: {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"\u0627\u0644\u062D\u062F \u0627\u0644\u0623\u0642\u0635\u0649 \u0644\u0644\u0623\u0628\u0639\u0627\u062F \u0647\u0648: {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"\u0627\u0644\u062F\u0642\u0629 \u0636\u0639\u064A\u0641\u0629 \u062C\u062F\u0627",imageValidateSizeLabelImageResolutionTooHigh:"\u0627\u0644\u062F\u0642\u0629 \u0645\u0631\u062A\u0641\u0639\u0629 \u062C\u062F\u0627",imageValidateSizeLabelExpectedMinResolution:"\u0623\u0642\u0644 \u062F\u0642\u0629: {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"\u0623\u0642\u0635\u0649 \u062F\u0642\u0629: {maxResolution}"};var Mo={labelIdle:'Fayl\u0131n\u0131z\u0131 S\xFCr\xFC\u015Fd\xFCr\xFCn & Burax\u0131n ya da Se\xE7in ',labelInvalidField:"Sah\u0259d\u0259 etibars\u0131z fayllar var",labelFileWaitingForSize:"\xD6l\xE7\xFC hesablan\u0131r",labelFileSizeNotAvailable:"\xD6l\xE7\xFC m\xF6vcud deyil",labelFileLoading:"Y\xFCkl\u0259nir",labelFileLoadError:"Y\xFCkl\u0259m\u0259 \u0259snas\u0131nda x\u0259ta ba\u015F verdi",labelFileProcessing:"Y\xFCkl\u0259nir",labelFileProcessingComplete:"Y\xFCkl\u0259m\u0259 tamamland\u0131",labelFileProcessingAborted:"Y\xFCkl\u0259m\u0259 l\u0259\u011Fv edildi",labelFileProcessingError:"Y\xFCk\u0259y\u0259rk\u0259n x\u0259ta ba\u015F verdi",labelFileProcessingRevertError:"Geri \xE7\u0259k\u0259rk\u0259n x\u0259ta ba\u015F verdi",labelFileRemoveError:"\xC7\u0131xarark\u0259n x\u0259ta ba\u015F verdi",labelTapToCancel:"\u0130mtina etm\u0259k \xFC\xE7\xFCn klikl\u0259yin",labelTapToRetry:"T\u0259krar yoxlamaq \xFC\xE7\xFCn klikl\u0259yin",labelTapToUndo:"Geri almaq \xFC\xE7\xFCn klikl\u0259yin",labelButtonRemoveItem:"\xC7\u0131xar",labelButtonAbortItemLoad:"\u0130mtina Et",labelButtonRetryItemLoad:"T\u0259krar yoxla",labelButtonAbortItemProcessing:"\u0130mtina et",labelButtonUndoItemProcessing:"Geri Al",labelButtonRetryItemProcessing:"T\u0259krar yoxla",labelButtonProcessItem:"Y\xFCkl\u0259",labelMaxFileSizeExceeded:"Fayl \xE7ox b\xF6y\xFCkd\xFCr",labelMaxFileSize:"\u018Fn b\xF6y\xFCk fayl \xF6l\xE7\xFCs\xFC: {filesize}",labelMaxTotalFileSizeExceeded:"Maksimum \xF6l\xE7\xFC ke\xE7ildi",labelMaxTotalFileSize:"Maksimum fayl \xF6l\xE7\xFCs\xFC :{filesize}",labelFileTypeNotAllowed:"Etibars\u0131z fayl tipi",fileValidateTypeLabelExpectedTypes:"Bu {allButLastType} ya da bu fayl olmas\u0131 laz\u0131md\u0131r: {lastType}",imageValidateSizeLabelFormatError:"\u015E\u0259kil tipi d\u0259st\u0259kl\u0259nmir",imageValidateSizeLabelImageSizeTooSmall:"\u015E\u0259kil \xE7ox ki\xE7ik",imageValidateSizeLabelImageSizeTooBig:"\u015E\u0259kil \xE7ox b\xF6y\xFCk",imageValidateSizeLabelExpectedMinSize:"Minimum \xF6l\xE7\xFC {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"Maksimum \xF6l\xE7\xFC {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"G\xF6r\xFCnt\xFC imkan\u0131 \xE7ox a\u015Fa\u011F\u0131",imageValidateSizeLabelImageResolutionTooHigh:"G\xF6r\xFCnt\xFC imkan\u0131 \xE7ox y\xFCks\u0259k",imageValidateSizeLabelExpectedMinResolution:"Minimum g\xF6r\xFCnt\xFC imkan\u0131 {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"Maximum g\xF6r\xFCnt\xFC imkan\u0131 {maxResolution}"};var Ao={labelIdle:'Arrossega i deixa anar els teus fitxers o Navega ',labelInvalidField:"El camp cont\xE9 fitxers inv\xE0lids",labelFileWaitingForSize:"Esperant mida",labelFileSizeNotAvailable:"Mida no disponible",labelFileLoading:"Carregant",labelFileLoadError:"Error durant la c\xE0rrega",labelFileProcessing:"Pujant",labelFileProcessingComplete:"Pujada completada",labelFileProcessingAborted:"Pujada cancel\xB7lada",labelFileProcessingError:"Error durant la pujada",labelFileProcessingRevertError:"Error durant la reversi\xF3",labelFileRemoveError:"Error durant l'eliminaci\xF3",labelTapToCancel:"toca per cancel\xB7lar",labelTapToRetry:"toca per reintentar",labelTapToUndo:"toca per desfer",labelButtonRemoveItem:"Eliminar",labelButtonAbortItemLoad:"Cancel\xB7lar",labelButtonRetryItemLoad:"Reintentar",labelButtonAbortItemProcessing:"Cancel\xB7lar",labelButtonUndoItemProcessing:"Desfer",labelButtonRetryItemProcessing:"Reintentar",labelButtonProcessItem:"Pujar",labelMaxFileSizeExceeded:"El fitxer \xE9s massa gran",labelMaxFileSize:"La mida m\xE0xima del fitxer \xE9s {filesize}",labelMaxTotalFileSizeExceeded:"Mida m\xE0xima total excedida",labelMaxTotalFileSize:"La mida m\xE0xima total del fitxer \xE9s {filesize}",labelFileTypeNotAllowed:"Fitxer de tipus inv\xE0lid",fileValidateTypeLabelExpectedTypes:"Espera {allButLastType} o {lastType}",imageValidateSizeLabelFormatError:"Tipus d'imatge no suportada",imageValidateSizeLabelImageSizeTooSmall:"La imatge \xE9s massa petita",imageValidateSizeLabelImageSizeTooBig:"La imatge \xE9s massa gran",imageValidateSizeLabelExpectedMinSize:"La mida m\xEDnima \xE9s {minWidth} x {minHeight}",imageValidateSizeLabelExpectedMaxSize:"La mida m\xE0xima \xE9s {maxWidth} x {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"La resoluci\xF3 \xE9s massa baixa",imageValidateSizeLabelImageResolutionTooHigh:"La resoluci\xF3 \xE9s massa alta",imageValidateSizeLabelExpectedMinResolution:"La resoluci\xF3 m\xEDnima \xE9s {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"La resoluci\xF3 m\xE0xima \xE9s {maxResolution}"};var zo={labelIdle:'\u067E\u06D5\u0695\u06AF\u06D5\u06A9\u0627\u0646 \u0641\u0695\u06CE \u0628\u062F\u06D5 \u0626\u06CE\u0631\u06D5 \u0628\u06C6 \u0628\u0627\u0631\u06A9\u0631\u062F\u0646 \u06CC\u0627\u0646 \u0647\u06D5\u06B5\u0628\u0698\u06CE\u0631\u06D5 ',labelInvalidField:"\u067E\u06D5\u0695\u06AF\u06D5\u06CC \u0646\u0627\u062F\u0631\u0648\u0633\u062A\u06CC \u062A\u06CE\u062F\u0627\u06CC\u06D5",labelFileWaitingForSize:"\u0686\u0627\u0648\u06D5\u0695\u0648\u0627\u0646\u06CC\u06CC \u0642\u06D5\u0628\u0627\u0631\u06D5",labelFileSizeNotAvailable:"\u0642\u06D5\u0628\u0627\u0631\u06D5 \u0628\u06D5\u0631\u062F\u06D5\u0633\u062A \u0646\u06CC\u06D5",labelFileLoading:"\u0628\u0627\u0631\u06A9\u0631\u062F\u0646",labelFileLoadError:"\u0647\u06D5\u06B5\u06D5 \u0644\u06D5\u0645\u0627\u0648\u06D5\u06CC \u0628\u0627\u0631\u06A9\u0631\u062F\u0646",labelFileProcessing:"\u0628\u0627\u0631\u06A9\u0631\u062F\u0646",labelFileProcessingComplete:"\u0628\u0627\u0631\u06A9\u0631\u062F\u0646 \u062A\u06D5\u0648\u0627\u0648 \u0628\u0648\u0648",labelFileProcessingAborted:"\u0628\u0627\u0631\u06A9\u0631\u062F\u0646 \u0647\u06D5\u06B5\u0648\u06D5\u0634\u0627\u06CC\u06D5\u0648\u06D5",labelFileProcessingError:"\u0647\u06D5\u06B5\u06D5 \u0644\u06D5\u06A9\u0627\u062A\u06CC \u0628\u0627\u0631\u06A9\u0631\u062F\u0646\u062F\u0627",labelFileProcessingRevertError:"\u0647\u06D5\u06B5\u06D5 \u0644\u06D5 \u06A9\u0627\u062A\u06CC \u06AF\u06D5\u0695\u0627\u0646\u06D5\u0648\u06D5",labelFileRemoveError:"\u0647\u06D5\u06B5\u06D5 \u0644\u06D5 \u06A9\u0627\u062A\u06CC \u0633\u0695\u06CC\u0646\u06D5\u0648\u06D5",labelTapToCancel:"\u0628\u06C6 \u0647\u06D5\u06B5\u0648\u06D5\u0634\u0627\u0646\u062F\u0646\u06D5\u0648\u06D5 Tab \u062F\u0627\u0628\u06AF\u0631\u06D5",labelTapToRetry:"tap \u062F\u0627\u0628\u06AF\u0631\u06D5 \u0628\u06C6 \u062F\u0648\u0648\u0628\u0627\u0631\u06D5\u06A9\u0631\u062F\u0646\u06D5\u0648\u06D5",labelTapToUndo:"tap \u062F\u0627\u0628\u06AF\u0631\u06D5 \u0628\u06C6 \u06AF\u06D5\u0695\u0627\u0646\u062F\u0646\u06D5\u0648\u06D5",labelButtonRemoveItem:"\u0633\u0695\u06CC\u0646\u06D5\u0648\u06D5",labelButtonAbortItemLoad:"\u0647\u06D5\u06B5\u0648\u06D5\u0634\u0627\u0646\u062F\u0646\u06D5\u0648\u06D5",labelButtonRetryItemLoad:"\u0647\u06D5\u0648\u06B5\u062F\u0627\u0646\u06D5\u0648\u06D5",labelButtonAbortItemProcessing:"\u067E\u06D5\u0634\u06CC\u0645\u0627\u0646\u0628\u0648\u0648\u0646\u06D5\u0648\u06D5",labelButtonUndoItemProcessing:"\u06AF\u06D5\u0695\u0627\u0646\u062F\u0646\u06D5\u0648\u06D5",labelButtonRetryItemProcessing:"\u0647\u06D5\u0648\u06B5\u062F\u0627\u0646\u06D5\u0648\u06D5",labelButtonProcessItem:"\u0628\u0627\u0631\u06A9\u0631\u062F\u0646",labelMaxFileSizeExceeded:"\u067E\u06D5\u0695\u06AF\u06D5 \u0632\u06C6\u0631 \u06AF\u06D5\u0648\u0631\u06D5\u06CC\u06D5",labelMaxFileSize:"\u0632\u06C6\u0631\u062A\u0631\u06CC\u0646 \u0642\u06D5\u0628\u0627\u0631\u06D5 {filesize}",labelMaxTotalFileSizeExceeded:"\u0632\u06C6\u0631\u062A\u0631\u06CC\u0646 \u0642\u06D5\u0628\u0627\u0631\u06D5\u06CC \u06A9\u06C6\u06CC \u06AF\u0634\u062A\u06CC \u062A\u06CE\u067E\u06D5\u0695\u06CE\u0646\u062F\u0631\u0627",labelMaxTotalFileSize:"\u0632\u06C6\u0631\u062A\u0631\u06CC\u0646 \u0642\u06D5\u0628\u0627\u0631\u06D5\u06CC \u06A9\u06C6\u06CC \u067E\u06D5\u0695\u06AF\u06D5 {filesize}",labelFileTypeNotAllowed:"\u062C\u06C6\u0631\u06CC \u067E\u06D5\u0695\u06AF\u06D5\u06A9\u06D5 \u0646\u0627\u062F\u0631\u0648\u0633\u062A\u06D5",fileValidateTypeLabelExpectedTypes:"\u062C\u06AF\u06D5 \u0644\u06D5 {allButLastType} \u06CC\u0627\u0646 {lastType}",imageValidateSizeLabelFormatError:"\u062C\u06C6\u0631\u06CC \u0648\u06CE\u0646\u06D5 \u067E\u0627\u06B5\u067E\u0634\u062A\u06CC\u06CC \u0646\u06D5\u06A9\u0631\u0627\u0648\u06D5",imageValidateSizeLabelImageSizeTooSmall:"\u0648\u06CE\u0646\u06D5\u06A9\u06D5 \u0632\u06C6\u0631 \u0628\u0686\u0648\u0648\u06A9\u06D5",imageValidateSizeLabelImageSizeTooBig:"\u0648\u06CE\u0646\u06D5\u06A9\u06D5 \u0632\u06C6\u0631 \u06AF\u06D5\u0648\u0631\u06D5\u06CC\u06D5",imageValidateSizeLabelExpectedMinSize:"\u06A9\u06D5\u0645\u062A\u0631\u06CC\u0646 \u0642\u06D5\u0628\u0627\u0631\u06D5 {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"\u0632\u06C6\u0631\u062A\u0631\u06CC\u0646 \u0642\u06D5\u0628\u0627\u0631\u06D5 {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"\u0648\u0631\u062F\u0628\u06CC\u0646\u06CC\u06CC\u06D5\u06A9\u06D5\u06CC \u0632\u06C6\u0631 \u06A9\u06D5\u0645\u06D5",imageValidateSizeLabelImageResolutionTooHigh:"\u0648\u0631\u062F\u0628\u06CC\u0646\u06CC\u06CC\u06D5\u06A9\u06D5\u06CC \u0632\u06C6\u0631 \u0628\u06D5\u0631\u0632\u06D5",imageValidateSizeLabelExpectedMinResolution:"\u06A9\u06D5\u0645\u062A\u0631\u06CC\u0646 \u0648\u0631\u062F\u0628\u06CC\u0646\u06CC\u06CC {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"\u0632\u06C6\u0631\u062A\u0631\u06CC\u0646 \u0648\u0631\u062F\u0628\u06CC\u0646\u06CC {maxResolution}"};var Po={labelIdle:'P\u0159et\xE1hn\u011Bte soubor sem (drag&drop) nebo Vyhledat ',labelInvalidField:"Pole obsahuje chybn\xE9 soubory",labelFileWaitingForSize:"Zji\u0161\u0165uje se velikost",labelFileSizeNotAvailable:"Velikost nen\xED zn\xE1m\xE1",labelFileLoading:"P\u0159en\xE1\u0161\xED se",labelFileLoadError:"Chyba p\u0159i p\u0159enosu",labelFileProcessing:"Prob\xEDh\xE1 upload",labelFileProcessingComplete:"Upload dokon\u010Den",labelFileProcessingAborted:"Upload stornov\xE1n",labelFileProcessingError:"Chyba p\u0159i uploadu",labelFileProcessingRevertError:"Chyba p\u0159i obnov\u011B",labelFileRemoveError:"Chyba p\u0159i odstran\u011Bn\xED",labelTapToCancel:"klepn\u011Bte pro storno",labelTapToRetry:"klepn\u011Bte pro opakov\xE1n\xED",labelTapToUndo:"klepn\u011Bte pro vr\xE1cen\xED",labelButtonRemoveItem:"Odstranit",labelButtonAbortItemLoad:"Storno",labelButtonRetryItemLoad:"Opakovat",labelButtonAbortItemProcessing:"Zp\u011Bt",labelButtonUndoItemProcessing:"Vr\xE1tit",labelButtonRetryItemProcessing:"Opakovat",labelButtonProcessItem:"Upload",labelMaxFileSizeExceeded:"Soubor je p\u0159\xEDli\u0161 velk\xFD",labelMaxFileSize:"Nejv\u011Bt\u0161\xED velikost souboru je {filesize}",labelMaxTotalFileSizeExceeded:"P\u0159ekro\u010Dena maxim\xE1ln\xED celkov\xE1 velikost souboru",labelMaxTotalFileSize:"Maxim\xE1ln\xED celkov\xE1 velikost souboru je {filesize}",labelFileTypeNotAllowed:"Soubor je nespr\xE1vn\xE9ho typu",fileValidateTypeLabelExpectedTypes:"O\u010Dek\xE1v\xE1 se {allButLastType} nebo {lastType}",imageValidateSizeLabelFormatError:"Obr\xE1zek tohoto typu nen\xED podporov\xE1n",imageValidateSizeLabelImageSizeTooSmall:"Obr\xE1zek je p\u0159\xEDli\u0161 mal\xFD",imageValidateSizeLabelImageSizeTooBig:"Obr\xE1zek je p\u0159\xEDli\u0161 velk\xFD",imageValidateSizeLabelExpectedMinSize:"Minim\xE1ln\xED rozm\u011Br je {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"Maxim\xE1ln\xED rozm\u011Br je {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"Rozli\u0161en\xED je p\u0159\xEDli\u0161 mal\xE9",imageValidateSizeLabelImageResolutionTooHigh:"Rozli\u0161en\xED je p\u0159\xEDli\u0161 velk\xE9",imageValidateSizeLabelExpectedMinResolution:"Minim\xE1ln\xED rozli\u0161en\xED je {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"Maxim\xE1ln\xED rozli\u0161en\xED je {maxResolution}"};var Fo={labelIdle:'Tr\xE6k & slip filer eller Gennemse ',labelInvalidField:"Felt indeholder ugyldige filer",labelFileWaitingForSize:"Venter p\xE5 st\xF8rrelse",labelFileSizeNotAvailable:"St\xF8rrelse ikke tilg\xE6ngelig",labelFileLoading:"Loader",labelFileLoadError:"Load fejlede",labelFileProcessing:"Uploader",labelFileProcessingComplete:"Upload f\xE6rdig",labelFileProcessingAborted:"Upload annulleret",labelFileProcessingError:"Upload fejlede",labelFileProcessingRevertError:"Fortryd fejlede",labelFileRemoveError:"Fjern fejlede",labelTapToCancel:"tryk for at annullere",labelTapToRetry:"tryk for at pr\xF8ve igen",labelTapToUndo:"tryk for at fortryde",labelButtonRemoveItem:"Fjern",labelButtonAbortItemLoad:"Annuller",labelButtonRetryItemLoad:"Fors\xF8g igen",labelButtonAbortItemProcessing:"Annuller",labelButtonUndoItemProcessing:"Fortryd",labelButtonRetryItemProcessing:"Pr\xF8v igen",labelButtonProcessItem:"Upload",labelMaxFileSizeExceeded:"Filen er for stor",labelMaxFileSize:"Maksimal filst\xF8rrelse er {filesize}",labelMaxTotalFileSizeExceeded:"Maksimal totalst\xF8rrelse overskredet",labelMaxTotalFileSize:"Maksimal total filst\xF8rrelse er {filesize}",labelFileTypeNotAllowed:"Ugyldig filtype",fileValidateTypeLabelExpectedTypes:"Forventer {allButLastType} eller {lastType}",imageValidateSizeLabelFormatError:"Ugyldigt format",imageValidateSizeLabelImageSizeTooSmall:"Billedet er for lille",imageValidateSizeLabelImageSizeTooBig:"Billedet er for stort",imageValidateSizeLabelExpectedMinSize:"Minimum st\xF8rrelse er {minBredde} \xD7 {minH\xF8jde}",imageValidateSizeLabelExpectedMaxSize:"Maksimal st\xF8rrelse er {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"For lav opl\xF8sning",imageValidateSizeLabelImageResolutionTooHigh:"For h\xF8j opl\xF8sning",imageValidateSizeLabelExpectedMinResolution:"Minimum opl\xF8sning er {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"Maksimal opl\xF8sning er {maxResolution}"};var Oo={labelIdle:'Dateien ablegen oder ausw\xE4hlen ',labelInvalidField:"Feld beinhaltet ung\xFCltige Dateien",labelFileWaitingForSize:"Dateigr\xF6\xDFe berechnen",labelFileSizeNotAvailable:"Dateigr\xF6\xDFe nicht verf\xFCgbar",labelFileLoading:"Laden",labelFileLoadError:"Fehler beim Laden",labelFileProcessing:"Upload l\xE4uft",labelFileProcessingComplete:"Upload abgeschlossen",labelFileProcessingAborted:"Upload abgebrochen",labelFileProcessingError:"Fehler beim Upload",labelFileProcessingRevertError:"Fehler beim Wiederherstellen",labelFileRemoveError:"Fehler beim L\xF6schen",labelTapToCancel:"abbrechen",labelTapToRetry:"erneut versuchen",labelTapToUndo:"r\xFCckg\xE4ngig",labelButtonRemoveItem:"Entfernen",labelButtonAbortItemLoad:"Verwerfen",labelButtonRetryItemLoad:"Erneut versuchen",labelButtonAbortItemProcessing:"Abbrechen",labelButtonUndoItemProcessing:"R\xFCckg\xE4ngig",labelButtonRetryItemProcessing:"Erneut versuchen",labelButtonProcessItem:"Upload",labelMaxFileSizeExceeded:"Datei ist zu gro\xDF",labelMaxFileSize:"Maximale Dateigr\xF6\xDFe: {filesize}",labelMaxTotalFileSizeExceeded:"Maximale gesamte Dateigr\xF6\xDFe \xFCberschritten",labelMaxTotalFileSize:"Maximale gesamte Dateigr\xF6\xDFe: {filesize}",labelFileTypeNotAllowed:"Dateityp ung\xFCltig",fileValidateTypeLabelExpectedTypes:"Erwartet {allButLastType} oder {lastType}",imageValidateSizeLabelFormatError:"Bildtyp nicht unterst\xFCtzt",imageValidateSizeLabelImageSizeTooSmall:"Bild ist zu klein",imageValidateSizeLabelImageSizeTooBig:"Bild ist zu gro\xDF",imageValidateSizeLabelExpectedMinSize:"Mindestgr\xF6\xDFe: {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"Maximale Gr\xF6\xDFe: {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"Aufl\xF6sung ist zu niedrig",imageValidateSizeLabelImageResolutionTooHigh:"Aufl\xF6sung ist zu hoch",imageValidateSizeLabelExpectedMinResolution:"Mindestaufl\xF6sung: {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"Maximale Aufl\xF6sung: {maxResolution}"};var Do={labelIdle:'\u03A3\u03CD\u03C1\u03B5\u03C4\u03B5 \u03C4\u03B1 \u03B1\u03C1\u03C7\u03B5\u03AF\u03B1 \u03C3\u03B1\u03C2 \u03C3\u03C4\u03BF \u03C0\u03BB\u03B1\u03AF\u03C3\u03B9\u03BF \u03AE \u0395\u03C0\u03B9\u03BB\u03AD\u03BE\u03C4\u03B5 ',labelInvalidField:"\u03A4\u03BF \u03C0\u03B5\u03B4\u03AF\u03BF \u03C0\u03B5\u03C1\u03B9\u03AD\u03C7\u03B5\u03B9 \u03BC\u03B7 \u03AD\u03B3\u03BA\u03C5\u03C1\u03B1 \u03B1\u03C1\u03C7\u03B5\u03AF\u03B1",labelFileWaitingForSize:"\u03A3\u03B5 \u03B1\u03BD\u03B1\u03BC\u03BF\u03BD\u03AE \u03B3\u03B9\u03B1 \u03C4\u03BF \u03BC\u03AD\u03B3\u03B5\u03B8\u03BF\u03C2",labelFileSizeNotAvailable:"\u039C\u03AD\u03B3\u03B5\u03B8\u03BF\u03C2 \u03BC\u03B7 \u03B4\u03B9\u03B1\u03B8\u03AD\u03C3\u03B9\u03BC\u03BF",labelFileLoading:"\u03A6\u03CC\u03C1\u03C4\u03C9\u03C3\u03B7 \u03C3\u03B5 \u03B5\u03BE\u03AD\u03BB\u03B9\u03BE\u03B7",labelFileLoadError:"\u03A3\u03C6\u03AC\u03BB\u03BC\u03B1 \u03BA\u03B1\u03C4\u03AC \u03C4\u03B7 \u03C6\u03CC\u03C1\u03C4\u03C9\u03C3\u03B7",labelFileProcessing:"\u0395\u03C0\u03B5\u03BE\u03B5\u03C1\u03B3\u03B1\u03C3\u03AF\u03B1",labelFileProcessingComplete:"\u0397 \u03B5\u03C0\u03B5\u03BE\u03B5\u03C1\u03B3\u03B1\u03C3\u03AF\u03B1 \u03BF\u03BB\u03BF\u03BA\u03BB\u03B7\u03C1\u03CE\u03B8\u03B7\u03BA\u03B5",labelFileProcessingAborted:"\u0397 \u03B5\u03C0\u03B5\u03BE\u03B5\u03C1\u03B3\u03B1\u03C3\u03AF\u03B1 \u03B1\u03BA\u03C5\u03C1\u03CE\u03B8\u03B7\u03BA\u03B5",labelFileProcessingError:"\u03A3\u03C6\u03AC\u03BB\u03BC\u03B1 \u03BA\u03B1\u03C4\u03AC \u03C4\u03B7\u03BD \u03B5\u03C0\u03B5\u03BE\u03B5\u03C1\u03B3\u03B1\u03C3\u03AF\u03B1",labelFileProcessingRevertError:"\u03A3\u03C6\u03AC\u03BB\u03BC\u03B1 \u03BA\u03B1\u03C4\u03AC \u03C4\u03B7\u03BD \u03B5\u03C0\u03B1\u03BD\u03B1\u03C6\u03BF\u03C1\u03AC",labelFileRemoveError:"\u03A3\u03C6\u03AC\u03BB\u03BC\u03B1 \u03BA\u03B1\u03C4\u03AC \u03C4\u03B7\u03BD \u03B4\u03B9\u03B1\u03B3\u03C1\u03B1\u03C6\u03AE",labelTapToCancel:"\u03C0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 \u03B3\u03B9\u03B1 \u03B1\u03BA\u03CD\u03C1\u03C9\u03C3\u03B7",labelTapToRetry:"\u03C0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 \u03B3\u03B9\u03B1 \u03B5\u03C0\u03B1\u03BD\u03AC\u03BB\u03B7\u03C8\u03B7",labelTapToUndo:"\u03C0\u03B1\u03C4\u03AE\u03C3\u03C4\u03B5 \u03B3\u03B9\u03B1 \u03B1\u03BD\u03B1\u03AF\u03C1\u03B5\u03C3\u03B7",labelButtonRemoveItem:"\u0391\u03C6\u03B1\u03AF\u03C1\u03B5\u03C3\u03B7",labelButtonAbortItemLoad:"\u0391\u03BA\u03CD\u03C1\u03C9\u03C3\u03B7",labelButtonRetryItemLoad:"\u0395\u03C0\u03B1\u03BD\u03AC\u03BB\u03B7\u03C8\u03B7",labelButtonAbortItemProcessing:"\u0391\u03BA\u03CD\u03C1\u03C9\u03C3\u03B7",labelButtonUndoItemProcessing:"\u0391\u03BD\u03B1\u03AF\u03C1\u03B5\u03C3\u03B7",labelButtonRetryItemProcessing:"\u0395\u03C0\u03B1\u03BD\u03AC\u03BB\u03B7\u03C8\u03B7",labelButtonProcessItem:"\u039C\u03B5\u03C4\u03B1\u03C6\u03CC\u03C1\u03C4\u03C9\u03C3\u03B7",labelMaxFileSizeExceeded:"\u03A4\u03BF \u03B1\u03C1\u03C7\u03B5\u03AF\u03BF \u03B5\u03AF\u03BD\u03B1\u03B9 \u03C0\u03BF\u03BB\u03CD \u03BC\u03B5\u03B3\u03AC\u03BB\u03BF",labelMaxFileSize:"\u03A4\u03BF \u03BC\u03AD\u03B3\u03B9\u03C3\u03C4\u03BF \u03BC\u03AD\u03B3\u03B5\u03B8\u03BF\u03C2 \u03B1\u03C1\u03C7\u03B5\u03AF\u03BF\u03C5 \u03B5\u03AF\u03BD\u03B1\u03B9 {filesize}",labelMaxTotalFileSizeExceeded:"\u03A5\u03C0\u03AD\u03C1\u03B2\u03B1\u03C3\u03B7 \u03C4\u03BF\u03C5 \u03BC\u03AD\u03B3\u03B9\u03C3\u03C4\u03BF\u03C5 \u03C3\u03C5\u03BD\u03BF\u03BB\u03B9\u03BA\u03BF\u03CD \u03BC\u03B5\u03B3\u03AD\u03B8\u03BF\u03C5\u03C2",labelMaxTotalFileSize:"\u03A4\u03BF \u03BC\u03AD\u03B3\u03B9\u03C3\u03C4\u03BF \u03C3\u03C5\u03BD\u03BF\u03BB\u03B9\u03BA\u03CC \u03BC\u03AD\u03B3\u03B5\u03B8\u03BF\u03C2 \u03B1\u03C1\u03C7\u03B5\u03AF\u03C9\u03BD \u03B5\u03AF\u03BD\u03B1\u03B9 {filesize}",labelFileTypeNotAllowed:"\u039C\u03B7 \u03AD\u03B3\u03BA\u03C5\u03C1\u03BF\u03C2 \u03C4\u03CD\u03C0\u03BF\u03C2 \u03B1\u03C1\u03C7\u03B5\u03AF\u03BF\u03C5",fileValidateTypeLabelExpectedTypes:"\u03A4\u03B1 \u03B1\u03C0\u03BF\u03B4\u03B5\u03BA\u03C4\u03AC \u03B1\u03C1\u03C7\u03B5\u03AF\u03B1 \u03B5\u03AF\u03BD\u03B1\u03B9 {allButLastType} \u03AE {lastType}",imageValidateSizeLabelFormatError:"\u039F \u03C4\u03CD\u03C0\u03BF\u03C2 \u03C4\u03B7\u03C2 \u03B5\u03B9\u03BA\u03CC\u03BD\u03B1\u03C2 \u03B4\u03B5\u03BD \u03C5\u03C0\u03BF\u03C3\u03C4\u03B7\u03C1\u03AF\u03B6\u03B5\u03C4\u03B1\u03B9",imageValidateSizeLabelImageSizeTooSmall:"\u0397 \u03B5\u03B9\u03BA\u03CC\u03BD\u03B1 \u03B5\u03AF\u03BD\u03B1\u03B9 \u03C0\u03BF\u03BB\u03CD \u03BC\u03B9\u03BA\u03C1\u03AE",imageValidateSizeLabelImageSizeTooBig:"\u0397 \u03B5\u03B9\u03BA\u03CC\u03BD\u03B1 \u03B5\u03AF\u03BD\u03B1\u03B9 \u03C0\u03BF\u03BB\u03CD \u03BC\u03B5\u03B3\u03AC\u03BB\u03B7",imageValidateSizeLabelExpectedMinSize:"\u03A4\u03BF \u03B5\u03BB\u03AC\u03C7\u03B9\u03C3\u03C4\u03BF \u03B1\u03C0\u03BF\u03B4\u03B5\u03BA\u03C4\u03CC \u03BC\u03AD\u03B3\u03B5\u03B8\u03BF\u03C2 \u03B5\u03AF\u03BD\u03B1\u03B9 {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"\u03A4\u03BF \u03BC\u03AD\u03B3\u03B9\u03C3\u03C4\u03BF \u03B1\u03C0\u03BF\u03B4\u03B5\u03BA\u03C4\u03CC \u03BC\u03AD\u03B3\u03B5\u03B8\u03BF\u03C2 \u03B5\u03AF\u03BD\u03B1\u03B9 {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"\u0397 \u03B1\u03BD\u03AC\u03BB\u03C5\u03C3\u03B7 \u03C4\u03B7\u03C2 \u03B5\u03B9\u03BA\u03CC\u03BD\u03B1\u03C2 \u03B5\u03AF\u03BD\u03B1\u03B9 \u03C0\u03BF\u03BB\u03CD \u03C7\u03B1\u03BC\u03B7\u03BB\u03AE",imageValidateSizeLabelImageResolutionTooHigh:"\u0397 \u03B1\u03BD\u03AC\u03BB\u03C5\u03C3\u03B7 \u03C4\u03B7\u03C2 \u03B5\u03B9\u03BA\u03CC\u03BD\u03B1\u03C2 \u03B5\u03AF\u03BD\u03B1\u03B9 \u03C0\u03BF\u03BB\u03CD \u03C5\u03C8\u03B7\u03BB\u03AE",imageValidateSizeLabelExpectedMinResolution:"\u0397 \u03B5\u03BB\u03AC\u03C7\u03B9\u03C3\u03C4\u03B7 \u03B1\u03C0\u03BF\u03B4\u03B5\u03BA\u03C4\u03AE \u03B1\u03BD\u03AC\u03BB\u03C5\u03C3\u03B7 \u03B5\u03AF\u03BD\u03B1\u03B9 {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"\u0397 \u03BC\u03AD\u03B3\u03B9\u03C3\u03C4\u03B7 \u03B1\u03C0\u03BF\u03B4\u03B5\u03BA\u03C4\u03AE \u03B1\u03BD\u03AC\u03BB\u03C5\u03C3\u03B7 \u03B5\u03AF\u03BD\u03B1\u03B9 {maxResolution}"};var Co={labelIdle:'Drag & Drop your files or Browse ',labelInvalidField:"Field contains invalid files",labelFileWaitingForSize:"Waiting for size",labelFileSizeNotAvailable:"Size not available",labelFileLoading:"Loading",labelFileLoadError:"Error during load",labelFileProcessing:"Uploading",labelFileProcessingComplete:"Upload complete",labelFileProcessingAborted:"Upload cancelled",labelFileProcessingError:"Error during upload",labelFileProcessingRevertError:"Error during revert",labelFileRemoveError:"Error during remove",labelTapToCancel:"tap to cancel",labelTapToRetry:"tap to retry",labelTapToUndo:"tap to undo",labelButtonRemoveItem:"Remove",labelButtonAbortItemLoad:"Abort",labelButtonRetryItemLoad:"Retry",labelButtonAbortItemProcessing:"Cancel",labelButtonUndoItemProcessing:"Undo",labelButtonRetryItemProcessing:"Retry",labelButtonProcessItem:"Upload",labelMaxFileSizeExceeded:"File is too large",labelMaxFileSize:"Maximum file size is {filesize}",labelMaxTotalFileSizeExceeded:"Maximum total size exceeded",labelMaxTotalFileSize:"Maximum total file size is {filesize}",labelFileTypeNotAllowed:"File of invalid type",fileValidateTypeLabelExpectedTypes:"Expects {allButLastType} or {lastType}",imageValidateSizeLabelFormatError:"Image type not supported",imageValidateSizeLabelImageSizeTooSmall:"Image is too small",imageValidateSizeLabelImageSizeTooBig:"Image is too big",imageValidateSizeLabelExpectedMinSize:"Minimum size is {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"Maximum size is {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"Resolution is too low",imageValidateSizeLabelImageResolutionTooHigh:"Resolution is too high",imageValidateSizeLabelExpectedMinResolution:"Minimum resolution is {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"Maximum resolution is {maxResolution}"};var Bo={labelIdle:'Arrastra y suelta tus archivos o Examina ',labelInvalidField:"El campo contiene archivos inv\xE1lidos",labelFileWaitingForSize:"Esperando tama\xF1o",labelFileSizeNotAvailable:"Tama\xF1o no disponible",labelFileLoading:"Cargando",labelFileLoadError:"Error durante la carga",labelFileProcessing:"Subiendo",labelFileProcessingComplete:"Subida completa",labelFileProcessingAborted:"Subida cancelada",labelFileProcessingError:"Error durante la subida",labelFileProcessingRevertError:"Error durante la reversi\xF3n",labelFileRemoveError:"Error durante la eliminaci\xF3n",labelTapToCancel:"toca para cancelar",labelTapToRetry:"tocar para reintentar",labelTapToUndo:"tocar para deshacer",labelButtonRemoveItem:"Eliminar",labelButtonAbortItemLoad:"Cancelar",labelButtonRetryItemLoad:"Reintentar",labelButtonAbortItemProcessing:"Cancelar",labelButtonUndoItemProcessing:"Deshacer",labelButtonRetryItemProcessing:"Reintentar",labelButtonProcessItem:"Subir",labelMaxFileSizeExceeded:"El archivo es demasiado grande",labelMaxFileSize:"El tama\xF1o m\xE1ximo del archivo es {filesize}",labelMaxTotalFileSizeExceeded:"Tama\xF1o total m\xE1ximo excedido",labelMaxTotalFileSize:"El tama\xF1o total m\xE1ximo del archivo es {filesize}",labelFileTypeNotAllowed:"Archivo de tipo inv\xE1lido",fileValidateTypeLabelExpectedTypes:"Espera {allButLastType} o {lastType}",imageValidateSizeLabelFormatError:"Tipo de imagen no soportada",imageValidateSizeLabelImageSizeTooSmall:"La imagen es demasiado peque\xF1a",imageValidateSizeLabelImageSizeTooBig:"La imagen es demasiado grande",imageValidateSizeLabelExpectedMinSize:"El tama\xF1o m\xEDnimo es {minWidth} x {minHeight}",imageValidateSizeLabelExpectedMaxSize:"El tama\xF1o m\xE1ximo es {maxWidth} x {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"La resoluci\xF3n es demasiado baja",imageValidateSizeLabelImageResolutionTooHigh:"La resoluci\xF3n es demasiado alta",imageValidateSizeLabelExpectedMinResolution:"La resoluci\xF3n m\xEDnima es {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"La resoluci\xF3n m\xE1xima es {maxResolution}"};var ko={labelIdle:'\u0641\u0627\u06CC\u0644 \u0631\u0627 \u0627\u06CC\u0646\u062C\u0627 \u0628\u06A9\u0634\u06CC\u062F \u0648 \u0631\u0647\u0627 \u06A9\u0646\u06CC\u062F\u060C \u06CC\u0627 \u062C\u0633\u062A\u062C\u0648 \u06A9\u0646\u06CC\u062F ',labelInvalidField:"\u0641\u06CC\u0644\u062F \u062F\u0627\u0631\u0627\u06CC \u0641\u0627\u06CC\u0644 \u0647\u0627\u06CC \u0646\u0627\u0645\u0639\u062A\u0628\u0631 \u0627\u0633\u062A",labelFileWaitingForSize:"Waiting for size",labelFileSizeNotAvailable:"\u062D\u062C\u0645 \u0641\u0627\u06CC\u0644 \u0645\u062C\u0627\u0632 \u0646\u06CC\u0633\u062A",labelFileLoading:"\u062F\u0631\u062D\u0627\u0644 \u0628\u0627\u0631\u06AF\u0630\u0627\u0631\u06CC",labelFileLoadError:"\u062E\u0637\u0627 \u062F\u0631 \u0632\u0645\u0627\u0646 \u0627\u062C\u0631\u0627",labelFileProcessing:"\u062F\u0631\u062D\u0627\u0644 \u0628\u0627\u0631\u06AF\u0630\u0627\u0631\u06CC",labelFileProcessingComplete:"\u0628\u0627\u0631\u06AF\u0630\u0627\u0631\u06CC \u06A9\u0627\u0645\u0644 \u0634\u062F",labelFileProcessingAborted:"\u0628\u0627\u0631\u06AF\u0630\u0627\u0631\u06CC \u0644\u063A\u0648 \u0634\u062F",labelFileProcessingError:"\u062E\u0637\u0627 \u062F\u0631 \u0632\u0645\u0627\u0646 \u0628\u0627\u0631\u06AF\u0630\u0627\u0631\u06CC",labelFileProcessingRevertError:"\u062E\u0637\u0627 \u062F\u0631 \u0632\u0645\u0627\u0646 \u062D\u0630\u0641",labelFileRemoveError:"\u062E\u0637\u0627 \u062F\u0631 \u0632\u0645\u0627\u0646 \u062D\u0630\u0641",labelTapToCancel:"\u0628\u0631\u0627\u06CC \u0644\u063A\u0648 \u0636\u0631\u0628\u0647 \u0628\u0632\u0646\u06CC\u062F",labelTapToRetry:"\u0628\u0631\u0627\u06CC \u062A\u06A9\u0631\u0627\u0631 \u06A9\u0644\u06CC\u06A9 \u06A9\u0646\u06CC\u062F",labelTapToUndo:"\u0628\u0631\u0627\u06CC \u0628\u0631\u06AF\u0634\u062A \u06A9\u0644\u06CC\u06A9 \u06A9\u0646\u06CC\u062F",labelButtonRemoveItem:"\u062D\u0630\u0641",labelButtonAbortItemLoad:"\u0644\u063A\u0648",labelButtonRetryItemLoad:"\u062A\u06A9\u0631\u0627\u0631",labelButtonAbortItemProcessing:"\u0644\u063A\u0648",labelButtonUndoItemProcessing:"\u0628\u0631\u06AF\u0634\u062A",labelButtonRetryItemProcessing:"\u062A\u06A9\u0631\u0627\u0631",labelButtonProcessItem:"\u0628\u0627\u0631\u06AF\u0630\u0627\u0631\u06CC",labelMaxFileSizeExceeded:"\u0641\u0627\u06CC\u0644 \u0628\u0633\u06CC\u0627\u0631 \u062D\u062C\u06CC\u0645 \u0627\u0633\u062A",labelMaxFileSize:"\u062D\u062F\u0627\u06A9\u062B\u0631 \u0645\u062C\u0627\u0632 \u0641\u0627\u06CC\u0644 {filesize} \u0627\u0633\u062A",labelMaxTotalFileSizeExceeded:"\u0627\u0632 \u062D\u062F\u0627\u06A9\u062B\u0631 \u062D\u062C\u0645 \u0641\u0627\u06CC\u0644 \u0628\u06CC\u0634\u062A\u0631 \u0634\u062F",labelMaxTotalFileSize:"\u062D\u062F\u0627\u06A9\u062B\u0631 \u062D\u062C\u0645 \u0641\u0627\u06CC\u0644 {filesize} \u0627\u0633\u062A",labelFileTypeNotAllowed:"\u0646\u0648\u0639 \u0641\u0627\u06CC\u0644 \u0646\u0627\u0645\u0639\u062A\u0628\u0631 \u0627\u0633\u062A",fileValidateTypeLabelExpectedTypes:"\u062F\u0631 \u0627\u0646\u062A\u0638\u0627\u0631 {allButLastType} \u06CC\u0627 {lastType}",imageValidateSizeLabelFormatError:"\u0641\u0631\u0645\u062A \u062A\u0635\u0648\u06CC\u0631 \u067E\u0634\u062A\u06CC\u0628\u0627\u0646\u06CC \u0646\u0645\u06CC \u0634\u0648\u062F",imageValidateSizeLabelImageSizeTooSmall:"\u062A\u0635\u0648\u06CC\u0631 \u0628\u0633\u06CC\u0627\u0631 \u06A9\u0648\u0686\u06A9 \u0627\u0633\u062A",imageValidateSizeLabelImageSizeTooBig:"\u062A\u0635\u0648\u06CC\u0631 \u0628\u0633\u06CC\u0627\u0631 \u0628\u0632\u0631\u06AF \u0627\u0633\u062A",imageValidateSizeLabelExpectedMinSize:"\u062D\u062F\u0627\u0642\u0644 \u0627\u0646\u062F\u0627\u0632\u0647 {minWidth} \xD7 {minHeight} \u0627\u0633\u062A",imageValidateSizeLabelExpectedMaxSize:"\u062D\u062F\u0627\u06A9\u062B\u0631 \u0627\u0646\u062F\u0627\u0632\u0647 {maxWidth} \xD7 {maxHeight} \u0627\u0633\u062A",imageValidateSizeLabelImageResolutionTooLow:"\u0648\u0636\u0648\u062D \u062A\u0635\u0648\u06CC\u0631 \u0628\u0633\u06CC\u0627\u0631 \u06A9\u0645 \u0627\u0633\u062A",imageValidateSizeLabelImageResolutionTooHigh:"\u0648\u0636\u0648\u0639 \u062A\u0635\u0648\u06CC\u0631 \u0628\u0633\u06CC\u0627\u0631 \u0632\u06CC\u0627\u062F \u0627\u0633\u062A",imageValidateSizeLabelExpectedMinResolution:"\u062D\u062F\u0627\u0642\u0644 \u0648\u0636\u0648\u062D \u062A\u0635\u0648\u06CC\u0631 {minResolution} \u0627\u0633\u062A",imageValidateSizeLabelExpectedMaxResolution:"\u062D\u062F\u0627\u06A9\u062B\u0631 \u0648\u0636\u0648\u062D \u062A\u0635\u0648\u06CC\u0631 {maxResolution} \u0627\u0633\u062A"};var No={labelIdle:'Ved\xE4 ja pudota tiedostoja tai Selaa ',labelInvalidField:"Kent\xE4ss\xE4 on virheellisi\xE4 tiedostoja",labelFileWaitingForSize:"Odotetaan kokoa",labelFileSizeNotAvailable:"Kokoa ei saatavilla",labelFileLoading:"Ladataan",labelFileLoadError:"Virhe latauksessa",labelFileProcessing:"L\xE4hetet\xE4\xE4n",labelFileProcessingComplete:"L\xE4hetys valmis",labelFileProcessingAborted:"L\xE4hetys peruttu",labelFileProcessingError:"Virhe l\xE4hetyksess\xE4",labelFileProcessingRevertError:"Virhe palautuksessa",labelFileRemoveError:"Virhe poistamisessa",labelTapToCancel:"peruuta napauttamalla",labelTapToRetry:"yrit\xE4 uudelleen napauttamalla",labelTapToUndo:"kumoa napauttamalla",labelButtonRemoveItem:"Poista",labelButtonAbortItemLoad:"Keskeyt\xE4",labelButtonRetryItemLoad:"Yrit\xE4 uudelleen",labelButtonAbortItemProcessing:"Peruuta",labelButtonUndoItemProcessing:"Kumoa",labelButtonRetryItemProcessing:"Yrit\xE4 uudelleen",labelButtonProcessItem:"L\xE4het\xE4",labelMaxFileSizeExceeded:"Tiedoston koko on liian suuri",labelMaxFileSize:"Tiedoston maksimikoko on {filesize}",labelMaxTotalFileSizeExceeded:"Tiedostojen yhdistetty maksimikoko ylitetty",labelMaxTotalFileSize:"Tiedostojen yhdistetty maksimikoko on {filesize}",labelFileTypeNotAllowed:"Tiedostotyyppi\xE4 ei sallita",fileValidateTypeLabelExpectedTypes:"Sallitaan {allButLastType} tai {lastType}",imageValidateSizeLabelFormatError:"Kuvatyyppi\xE4 ei tueta",imageValidateSizeLabelImageSizeTooSmall:"Kuva on liian pieni",imageValidateSizeLabelImageSizeTooBig:"Kuva on liian suuri",imageValidateSizeLabelExpectedMinSize:"Minimikoko on {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"Maksimikoko on {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"Resoluutio on liian pieni",imageValidateSizeLabelImageResolutionTooHigh:"Resoluutio on liian suuri",imageValidateSizeLabelExpectedMinResolution:"Minimiresoluutio on {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"Maksimiresoluutio on {maxResolution}"};var Vo={labelIdle:'Faites glisser vos fichiers ou Parcourir ',labelInvalidField:"Le champ contient des fichiers invalides",labelFileWaitingForSize:"En attente de taille",labelFileSizeNotAvailable:"Taille non disponible",labelFileLoading:"Chargement",labelFileLoadError:"Erreur durant le chargement",labelFileProcessing:"Traitement",labelFileProcessingComplete:"Traitement effectu\xE9",labelFileProcessingAborted:"Traitement interrompu",labelFileProcessingError:"Erreur durant le traitement",labelFileProcessingRevertError:"Erreur durant la restauration",labelFileRemoveError:"Erreur durant la suppression",labelTapToCancel:"appuyer pour annuler",labelTapToRetry:"appuyer pour r\xE9essayer",labelTapToUndo:"appuyer pour revenir en arri\xE8re",labelButtonRemoveItem:"Retirer",labelButtonAbortItemLoad:"Annuler",labelButtonRetryItemLoad:"Recommencer",labelButtonAbortItemProcessing:"Annuler",labelButtonUndoItemProcessing:"Revenir en arri\xE8re",labelButtonRetryItemProcessing:"Recommencer",labelButtonProcessItem:"Transf\xE9rer",labelMaxFileSizeExceeded:"Le fichier est trop volumineux",labelMaxFileSize:"La taille maximale de fichier est {filesize}",labelMaxTotalFileSizeExceeded:"Taille totale maximale d\xE9pass\xE9e",labelMaxTotalFileSize:"La taille totale maximale des fichiers est {filesize}",labelFileTypeNotAllowed:"Fichier non valide",fileValidateTypeLabelExpectedTypes:"Attendu {allButLastType} ou {lastType}",imageValidateSizeLabelFormatError:"Type d'image non pris en charge",imageValidateSizeLabelImageSizeTooSmall:"L'image est trop petite",imageValidateSizeLabelImageSizeTooBig:"L'image est trop grande",imageValidateSizeLabelExpectedMinSize:"La taille minimale est {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"La taille maximale est {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"La r\xE9solution est trop faible",imageValidateSizeLabelImageResolutionTooHigh:"La r\xE9solution est trop \xE9lev\xE9e",imageValidateSizeLabelExpectedMinResolution:"La r\xE9solution minimale est {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"La r\xE9solution maximale est {maxResolution}"};var Go={labelIdle:'\u05D2\u05E8\u05D5\u05E8 \u05D5\u05E9\u05D7\u05E8\u05E8 \u05D0\u05EA \u05D4\u05E7\u05D1\u05E6\u05D9\u05DD \u05DB\u05D0\u05DF \u05D0\u05D5 \u05DC\u05D7\u05E5 \u05DB\u05D0\u05DF \u05DC\u05D1\u05D7\u05D9\u05E8\u05D4 ',labelInvalidField:"\u05E7\u05D5\u05D1\u05E5 \u05DC\u05D0 \u05D7\u05D5\u05E7\u05D9",labelFileWaitingForSize:"\u05DE\u05D7\u05E9\u05D1 \u05D0\u05EA \u05D2\u05D5\u05D3\u05DC \u05D4\u05E7\u05D1\u05E6\u05D9\u05DD",labelFileSizeNotAvailable:"\u05DC\u05D0 \u05E0\u05D9\u05EA\u05DF \u05DC\u05E7\u05D1\u05D5\u05E2 \u05D0\u05EA \u05D2\u05D5\u05D3\u05DC \u05D4\u05E7\u05D1\u05E6\u05D9\u05DD",labelFileLoading:"\u05D8\u05D5\u05E2\u05DF...",labelFileLoadError:"\u05E9\u05D2\u05D9\u05D0\u05D4 \u05D0\u05E8\u05E2\u05D4 \u05D1\u05E2\u05EA \u05D8\u05E2\u05D9\u05E0\u05EA \u05D4\u05E7\u05D1\u05E6\u05D9\u05DD",labelFileProcessing:"\u05DE\u05E2\u05DC\u05D4 \u05D0\u05EA \u05D4\u05E7\u05D1\u05E6\u05D9\u05DD",labelFileProcessingComplete:"\u05D4\u05E2\u05DC\u05D0\u05EA \u05D4\u05E7\u05D1\u05E6\u05D9\u05DD \u05D4\u05E1\u05EA\u05D9\u05D9\u05DE\u05D4",labelFileProcessingAborted:"\u05D4\u05E2\u05DC\u05D0\u05EA \u05D4\u05E7\u05D1\u05E6\u05D9\u05DD \u05D1\u05D5\u05D8\u05DC\u05D4",labelFileProcessingError:"\u05E9\u05D2\u05D9\u05D0\u05D4 \u05D0\u05E8\u05E2\u05D4 \u05D1\u05E2\u05EA \u05D4\u05E2\u05DC\u05D0\u05EA \u05D4\u05E7\u05D1\u05E6\u05D9\u05DD",labelFileProcessingRevertError:"\u05E9\u05D2\u05D9\u05D0\u05D4 \u05D0\u05E8\u05E2\u05D4 \u05D1\u05E2\u05EA \u05E9\u05D7\u05D6\u05D5\u05E8 \u05D4\u05E7\u05D1\u05E6\u05D9\u05DD",labelFileRemoveError:"\u05E9\u05D2\u05D9\u05D0\u05D4 \u05D0\u05E8\u05E2\u05D4 \u05D1\u05E2\u05EA \u05D4\u05E1\u05E8\u05EA \u05D4\u05E7\u05D5\u05D1\u05E5",labelTapToCancel:"\u05D4\u05E7\u05DC\u05E7 \u05DC\u05D1\u05D9\u05D8\u05D5\u05DC",labelTapToRetry:"\u05D4\u05E7\u05DC\u05E7 \u05DC\u05E0\u05E1\u05D5\u05EA \u05E9\u05E0\u05D9\u05EA",labelTapToUndo:"\u05D4\u05E7\u05DC\u05E7 \u05DC\u05E9\u05D7\u05D6\u05E8",labelButtonRemoveItem:"\u05D4\u05E1\u05E8",labelButtonAbortItemLoad:"\u05D1\u05D8\u05DC",labelButtonRetryItemLoad:"\u05D8\u05E2\u05DF \u05E9\u05E0\u05D9\u05EA",labelButtonAbortItemProcessing:"\u05D1\u05D8\u05DC",labelButtonUndoItemProcessing:"\u05E9\u05D7\u05D6\u05E8",labelButtonRetryItemProcessing:"\u05E0\u05E1\u05D4 \u05E9\u05E0\u05D9\u05EA",labelButtonProcessItem:"\u05D4\u05E2\u05DC\u05D4 \u05E7\u05D5\u05D1\u05E5",labelMaxFileSizeExceeded:"\u05D4\u05E7\u05D5\u05D1\u05E5 \u05D2\u05D3\u05D5\u05DC \u05DE\u05D3\u05D9",labelMaxFileSize:"\u05D2\u05D5\u05D3\u05DC \u05D4\u05DE\u05D9\u05E8\u05D1\u05D9 \u05D4\u05DE\u05D5\u05EA\u05E8 \u05D4\u05D5\u05D0: {filesize}",labelMaxTotalFileSizeExceeded:"\u05D2\u05D5\u05D3\u05DC \u05D4\u05E7\u05D1\u05E6\u05D9\u05DD \u05D7\u05D5\u05E8\u05D2 \u05DE\u05D4\u05DB\u05DE\u05D5\u05EA \u05D4\u05DE\u05D5\u05EA\u05E8\u05EA",labelMaxTotalFileSize:"\u05D4\u05D2\u05D5\u05D3\u05DC \u05D4\u05DE\u05D9\u05E8\u05D1\u05D9 \u05E9\u05DC \u05E1\u05DA \u05D4\u05E7\u05D1\u05E6\u05D9\u05DD: {filesize}",labelFileTypeNotAllowed:"\u05E7\u05D5\u05D1\u05E5 \u05DE\u05E1\u05D5\u05D2 \u05D6\u05D4 \u05D0\u05D9\u05E0\u05D5 \u05DE\u05D5\u05EA\u05E8",fileValidateTypeLabelExpectedTypes:"\u05D4\u05E7\u05D1\u05E6\u05D9\u05DD \u05D4\u05DE\u05D5\u05EA\u05E8\u05D9\u05DD \u05D4\u05DD {allButLastType} \u05D0\u05D5 {lastType}",imageValidateSizeLabelFormatError:"\u05EA\u05DE\u05D5\u05E0\u05D4 \u05D1\u05E4\u05D5\u05E8\u05DE\u05D8 \u05D6\u05D4 \u05D0\u05D9\u05E0\u05D4 \u05E0\u05EA\u05DE\u05DB\u05EA",imageValidateSizeLabelImageSizeTooSmall:"\u05EA\u05DE\u05D5\u05E0\u05D4 \u05D6\u05D5 \u05E7\u05D8\u05E0\u05D4 \u05DE\u05D3\u05D9",imageValidateSizeLabelImageSizeTooBig:"\u05EA\u05DE\u05D5\u05E0\u05D4 \u05D6\u05D5 \u05D2\u05D3\u05D5\u05DC\u05D4 \u05DE\u05D3\u05D9",imageValidateSizeLabelExpectedMinSize:"\u05D4\u05D2\u05D5\u05D3\u05DC \u05E6\u05E8\u05D9\u05DA \u05DC\u05D4\u05D9\u05D5\u05EA \u05DC\u05E4\u05D7\u05D5\u05EA: {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"\u05D4\u05D2\u05D5\u05D3\u05DC \u05D4\u05DE\u05E8\u05D1\u05D9 \u05D4\u05DE\u05D5\u05EA\u05E8: {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"\u05D4\u05E8\u05D6\u05D5\u05DC\u05D5\u05E6\u05D9\u05D4 \u05E9\u05DC \u05EA\u05DE\u05D5\u05E0\u05D4 \u05D6\u05D5 \u05E0\u05DE\u05D5\u05DB\u05D4 \u05DE\u05D3\u05D9",imageValidateSizeLabelImageResolutionTooHigh:"\u05D4\u05E8\u05D6\u05D5\u05DC\u05D5\u05E6\u05D9\u05D4 \u05E9\u05DC \u05EA\u05DE\u05D5\u05E0\u05D4 \u05D6\u05D5 \u05D2\u05D1\u05D5\u05D4\u05D4 \u05DE\u05D3\u05D9",imageValidateSizeLabelExpectedMinResolution:"\u05D4\u05E8\u05D6\u05D5\u05DC\u05D5\u05E6\u05D9\u05D4 \u05E6\u05E8\u05D9\u05DB\u05D4 \u05DC\u05D4\u05D9\u05D5\u05EA \u05DC\u05E4\u05D7\u05D5\u05EA: {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"\u05D4\u05E8\u05D6\u05D5\u05DC\u05D5\u05E6\u05D9\u05D4 \u05D4\u05DE\u05D9\u05E8\u05D1\u05D9\u05EA \u05D4\u05DE\u05D5\u05EA\u05E8\u05EA \u05D4\u05D9\u05D0: {maxResolution}"};var Uo={labelIdle:'Ovdje "ispusti" datoteku ili Pretra\u017Ei ',labelInvalidField:"Polje sadr\u017Ei neispravne datoteke",labelFileWaitingForSize:"\u010Cekanje na veli\u010Dinu datoteke",labelFileSizeNotAvailable:"Veli\u010Dina datoteke nije dostupna",labelFileLoading:"U\u010Ditavanje",labelFileLoadError:"Gre\u0161ka tijekom u\u010Ditavanja",labelFileProcessing:"Prijenos",labelFileProcessingComplete:"Prijenos zavr\u0161en",labelFileProcessingAborted:"Prijenos otkazan",labelFileProcessingError:"Gre\u0161ka tijekom prijenosa",labelFileProcessingRevertError:"Gre\u0161ka tijekom vra\u0107anja",labelFileRemoveError:"Gre\u0161ka tijekom uklananja datoteke",labelTapToCancel:"Dodirni za prekid",labelTapToRetry:"Dodirni za ponovno",labelTapToUndo:"Dodirni za vra\u0107anje",labelButtonRemoveItem:"Ukloni",labelButtonAbortItemLoad:"Odbaci",labelButtonRetryItemLoad:"Ponovi",labelButtonAbortItemProcessing:"Prekini",labelButtonUndoItemProcessing:"Vrati",labelButtonRetryItemProcessing:"Ponovi",labelButtonProcessItem:"Prijenos",labelMaxFileSizeExceeded:"Datoteka je prevelika",labelMaxFileSize:"Maksimalna veli\u010Dina datoteke je {filesize}",labelMaxTotalFileSizeExceeded:"Maksimalna ukupna veli\u010Dina datoteke prekora\u010Dena",labelMaxTotalFileSize:"Maksimalna ukupna veli\u010Dina datoteke je {filesize}",labelFileTypeNotAllowed:"Tip datoteke nije podr\u017Ean",fileValidateTypeLabelExpectedTypes:"O\u010Dekivan {allButLastType} ili {lastType}",imageValidateSizeLabelFormatError:"Tip slike nije podr\u017Ean",imageValidateSizeLabelImageSizeTooSmall:"Slika je premala",imageValidateSizeLabelImageSizeTooBig:"Slika je prevelika",imageValidateSizeLabelExpectedMinSize:"Minimalna veli\u010Dina je {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"Maksimalna veli\u010Dina je {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"Rezolucija je preniska",imageValidateSizeLabelImageResolutionTooHigh:"Rezolucija je previsoka",imageValidateSizeLabelExpectedMinResolution:"Minimalna rezolucija je {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"Maksimalna rezolucija je {maxResolution}"};var Ho={labelIdle:'Mozgasd ide a f\xE1jlt a felt\xF6lt\xE9shez, vagy tall\xF3z\xE1s ',labelInvalidField:"A mez\u0151 \xE9rv\xE9nytelen f\xE1jlokat tartalmaz",labelFileWaitingForSize:"F\xE1ljm\xE9ret kisz\xE1mol\xE1sa",labelFileSizeNotAvailable:"A f\xE1jlm\xE9ret nem el\xE9rhet\u0151",labelFileLoading:"T\xF6lt\xE9s",labelFileLoadError:"Hiba a bet\xF6lt\xE9s sor\xE1n",labelFileProcessing:"Felt\xF6lt\xE9s",labelFileProcessingComplete:"Sikeres felt\xF6lt\xE9s",labelFileProcessingAborted:"A felt\xF6lt\xE9s megszak\xEDtva",labelFileProcessingError:"Hiba t\xF6rt\xE9nt a felt\xF6lt\xE9s sor\xE1n",labelFileProcessingRevertError:"Hiba a vissza\xE1ll\xEDt\xE1s sor\xE1n",labelFileRemoveError:"Hiba t\xF6rt\xE9nt az elt\xE1vol\xEDt\xE1s sor\xE1n",labelTapToCancel:"koppints a t\xF6rl\xE9shez",labelTapToRetry:"koppints az \xFAjrakezd\xE9shez",labelTapToUndo:"koppints a visszavon\xE1shoz",labelButtonRemoveItem:"Elt\xE1vol\xEDt\xE1s",labelButtonAbortItemLoad:"Megszak\xEDt\xE1s",labelButtonRetryItemLoad:"\xDAjrapr\xF3b\xE1lkoz\xE1s",labelButtonAbortItemProcessing:"Megszak\xEDt\xE1s",labelButtonUndoItemProcessing:"Visszavon\xE1s",labelButtonRetryItemProcessing:"\xDAjrapr\xF3b\xE1lkoz\xE1s",labelButtonProcessItem:"Felt\xF6lt\xE9s",labelMaxFileSizeExceeded:"A f\xE1jl t\xFAll\xE9pte a maxim\xE1lis m\xE9retet",labelMaxFileSize:"Maxim\xE1lis f\xE1jlm\xE9ret: {filesize}",labelMaxTotalFileSizeExceeded:"T\xFAll\xE9pte a maxim\xE1lis teljes m\xE9retet",labelMaxTotalFileSize:"A maxim\xE1is teljes f\xE1jlm\xE9ret: {filesize}",labelFileTypeNotAllowed:"\xC9rv\xE9nytelen t\xEDpus\xFA f\xE1jl",fileValidateTypeLabelExpectedTypes:"Enged\xE9lyezett t\xEDpusok {allButLastType} vagy {lastType}",imageValidateSizeLabelFormatError:"A k\xE9pt\xEDpus nem t\xE1mogatott",imageValidateSizeLabelImageSizeTooSmall:"A k\xE9p t\xFAl kicsi",imageValidateSizeLabelImageSizeTooBig:"A k\xE9p t\xFAl nagy",imageValidateSizeLabelExpectedMinSize:"Minimum m\xE9ret: {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"Maximum m\xE9ret: {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"A felbont\xE1s t\xFAl alacsony",imageValidateSizeLabelImageResolutionTooHigh:"A felbont\xE1s t\xFAl magas",imageValidateSizeLabelExpectedMinResolution:"Minim\xE1is felbont\xE1s: {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"Maxim\xE1lis felbont\xE1s: {maxResolution}"};var Wo={labelIdle:'Seret & Jatuhkan berkas Anda atau Jelajahi',labelInvalidField:"Isian berisi berkas yang tidak valid",labelFileWaitingForSize:"Menunggu ukuran berkas",labelFileSizeNotAvailable:"Ukuran berkas tidak tersedia",labelFileLoading:"Memuat",labelFileLoadError:"Kesalahan saat memuat",labelFileProcessing:"Mengunggah",labelFileProcessingComplete:"Pengunggahan selesai",labelFileProcessingAborted:"Pengunggahan dibatalkan",labelFileProcessingError:"Kesalahan saat pengunggahan",labelFileProcessingRevertError:"Kesalahan saat pemulihan",labelFileRemoveError:"Kesalahan saat penghapusan",labelTapToCancel:"ketuk untuk membatalkan",labelTapToRetry:"ketuk untuk mencoba lagi",labelTapToUndo:"ketuk untuk mengurungkan",labelButtonRemoveItem:"Hapus",labelButtonAbortItemLoad:"Batalkan",labelButtonRetryItemLoad:"Coba Kembali",labelButtonAbortItemProcessing:"Batalkan",labelButtonUndoItemProcessing:"Urungkan",labelButtonRetryItemProcessing:"Coba Kembali",labelButtonProcessItem:"Unggah",labelMaxFileSizeExceeded:"Berkas terlalu besar",labelMaxFileSize:"Ukuran berkas maksimum adalah {filesize}",labelMaxTotalFileSizeExceeded:"Jumlah berkas maksimum terlampaui",labelMaxTotalFileSize:"Jumlah berkas maksimum adalah {filesize}",labelFileTypeNotAllowed:"Jenis berkas tidak valid",fileValidateTypeLabelExpectedTypes:"Mengharapkan {allButLastType} atau {lastType}",imageValidateSizeLabelFormatError:"Jenis citra tidak didukung",imageValidateSizeLabelImageSizeTooSmall:"Citra terlalu kecil",imageValidateSizeLabelImageSizeTooBig:"Citra terlalu besar",imageValidateSizeLabelExpectedMinSize:"Ukuran minimum adalah {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"Ukuran maksimum adalah {minWidth} \xD7 {minHeight}",imageValidateSizeLabelImageResolutionTooLow:"Resolusi terlalu rendah",imageValidateSizeLabelImageResolutionTooHigh:"Resolusi terlalu tinggi",imageValidateSizeLabelExpectedMinResolution:"Resolusi minimum adalah {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"Resolusi maksimum adalah {maxResolution}"};var jo={labelIdle:'Trascina e rilascia i tuoi file oppure Sfoglia ',labelInvalidField:"Il campo contiene dei file non validi",labelFileWaitingForSize:"In attesa della dimensione",labelFileSizeNotAvailable:"Dimensione non disponibile",labelFileLoading:"Caricamento",labelFileLoadError:"Errore durante il caricamento",labelFileProcessing:"Caricamento",labelFileProcessingComplete:"Caricamento completato",labelFileProcessingAborted:"Caricamento cancellato",labelFileProcessingError:"Errore durante il caricamento",labelFileProcessingRevertError:"Errore durante il ripristino",labelFileRemoveError:"Errore durante l'eliminazione",labelTapToCancel:"tocca per cancellare",labelTapToRetry:"tocca per riprovare",labelTapToUndo:"tocca per ripristinare",labelButtonRemoveItem:"Elimina",labelButtonAbortItemLoad:"Cancella",labelButtonRetryItemLoad:"Ritenta",labelButtonAbortItemProcessing:"Cancella",labelButtonUndoItemProcessing:"Indietro",labelButtonRetryItemProcessing:"Ritenta",labelButtonProcessItem:"Carica",labelMaxFileSizeExceeded:"La dimensione del file \xE8 eccessiva",labelMaxFileSize:"La dimensione massima del file \xE8 {filesize}",labelMaxTotalFileSizeExceeded:"Dimensione totale massima superata",labelMaxTotalFileSize:"La dimensione massima totale dei file \xE8 {filesize}",labelFileTypeNotAllowed:"File non supportato",fileValidateTypeLabelExpectedTypes:"Aspetta {allButLastType} o {lastType}",imageValidateSizeLabelFormatError:"Tipo di immagine non supportata",imageValidateSizeLabelImageSizeTooSmall:"L'immagine \xE8 troppo piccola",imageValidateSizeLabelImageSizeTooBig:"L'immagine \xE8 troppo grande",imageValidateSizeLabelExpectedMinSize:"La dimensione minima \xE8 {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"La dimensione massima \xE8 {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"La risoluzione \xE8 troppo bassa",imageValidateSizeLabelImageResolutionTooHigh:"La risoluzione \xE8 troppo alta",imageValidateSizeLabelExpectedMinResolution:"La risoluzione minima \xE8 {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"La risoluzione massima \xE8 {maxResolution}"};var Yo={labelIdle:'\u30D5\u30A1\u30A4\u30EB\u3092\u30C9\u30E9\u30C3\u30B0&\u30C9\u30ED\u30C3\u30D7\u53C8\u306F\u30D5\u30A1\u30A4\u30EB\u9078\u629E',labelInvalidField:"\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u3067\u304D\u306A\u3044\u30D5\u30A1\u30A4\u30EB\u304C\u542B\u307E\u308C\u3066\u3044\u307E\u3059",labelFileWaitingForSize:"\u30D5\u30A1\u30A4\u30EB\u30B5\u30A4\u30BA\u3092\u5F85\u3063\u3066\u3044\u307E\u3059",labelFileSizeNotAvailable:"\u30D5\u30A1\u30A4\u30EB\u30B5\u30A4\u30BA\u304C\u307F\u3064\u304B\u308A\u307E\u305B\u3093",labelFileLoading:"\u8AAD\u8FBC\u4E2D...",labelFileLoadError:"\u8AAD\u8FBC\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F",labelFileProcessing:"\u8AAD\u8FBC\u4E2D...",labelFileProcessingComplete:"\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u5B8C\u4E86",labelFileProcessingAborted:"\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u304C\u30AD\u30E3\u30F3\u30BB\u30EB\u3055\u308C\u307E\u3057\u305F",labelFileProcessingError:"\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F",labelFileProcessingRevertError:"\u30ED\u30FC\u30EB\u30D0\u30C3\u30AF\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F",labelFileRemoveError:"\u524A\u9664\u4E2D\u306B\u30A8\u30E9\u30FC\u304C\u767A\u751F",labelTapToCancel:"\u30AF\u30EA\u30C3\u30AF\u3057\u3066\u30AD\u30E3\u30F3\u30BB\u30EB",labelTapToRetry:"\u30AF\u30EA\u30C3\u30AF\u3057\u3066\u3082\u3046\u4E00\u5EA6\u304A\u8A66\u3057\u4E0B\u3055\u3044",labelTapToUndo:"\u5143\u306B\u623B\u3059\u306B\u306F\u30BF\u30C3\u30D7\u3057\u307E\u3059",labelButtonRemoveItem:"\u524A\u9664",labelButtonAbortItemLoad:"\u4E2D\u65AD",labelButtonRetryItemLoad:"\u3082\u3046\u4E00\u5EA6\u5B9F\u884C",labelButtonAbortItemProcessing:"\u30AD\u30E3\u30F3\u30BB\u30EB",labelButtonUndoItemProcessing:"\u5143\u306B\u623B\u3059",labelButtonRetryItemProcessing:"\u3082\u3046\u4E00\u5EA6\u5B9F\u884C",labelButtonProcessItem:"\u30A2\u30C3\u30D7\u30ED\u30FC\u30C9",labelMaxFileSizeExceeded:"\u30D5\u30A1\u30A4\u30EB\u30B5\u30A4\u30BA\u304C\u5927\u304D\u3059\u304E\u307E\u3059",labelMaxFileSize:"\u6700\u5927\u30D5\u30A1\u30A4\u30EB\u30B5\u30A4\u30BA\u306F {filesize} \u3067\u3059",labelMaxTotalFileSizeExceeded:"\u6700\u5927\u5408\u8A08\u30B5\u30A4\u30BA\u3092\u8D85\u3048\u307E\u3057\u305F",labelMaxTotalFileSize:"\u6700\u5927\u5408\u8A08\u30D5\u30A1\u30A4\u30EB\u30B5\u30A4\u30BA\u306F {filesize} \u3067\u3059",labelFileTypeNotAllowed:"\u7121\u52B9\u306A\u30D5\u30A1\u30A4\u30EB\u3067\u3059",fileValidateTypeLabelExpectedTypes:"\u30B5\u30DD\u30FC\u30C8\u3057\u3066\u3044\u308B\u30D5\u30A1\u30A4\u30EB\u306F {allButLastType} \u53C8\u306F {lastType} \u3067\u3059",imageValidateSizeLabelFormatError:"\u30B5\u30DD\u30FC\u30C8\u3057\u3066\u3044\u306A\u3044\u753B\u50CF\u3067\u3059",imageValidateSizeLabelImageSizeTooSmall:"\u753B\u50CF\u304C\u5C0F\u3055\u3059\u304E\u307E\u3059",imageValidateSizeLabelImageSizeTooBig:"\u753B\u50CF\u304C\u5927\u304D\u3059\u304E\u307E\u3059",imageValidateSizeLabelExpectedMinSize:"\u753B\u50CF\u306E\u6700\u5C0F\u30B5\u30A4\u30BA\u306F{minWidth}\xD7{minHeight}\u3067\u3059",imageValidateSizeLabelExpectedMaxSize:"\u753B\u50CF\u306E\u6700\u5927\u30B5\u30A4\u30BA\u306F{maxWidth} \xD7 {maxHeight}\u3067\u3059",imageValidateSizeLabelImageResolutionTooLow:"\u753B\u50CF\u306E\u89E3\u50CF\u5EA6\u304C\u4F4E\u3059\u304E\u307E\u3059",imageValidateSizeLabelImageResolutionTooHigh:"\u753B\u50CF\u306E\u89E3\u50CF\u5EA6\u304C\u9AD8\u3059\u304E\u307E\u3059",imageValidateSizeLabelExpectedMinResolution:"\u753B\u50CF\u306E\u6700\u5C0F\u89E3\u50CF\u5EA6\u306F{minResolution}\u3067\u3059",imageValidateSizeLabelExpectedMaxResolution:"\u753B\u50CF\u306E\u6700\u5927\u89E3\u50CF\u5EA6\u306F{maxResolution}\u3067\u3059"};var qo={labelIdle:'\u1791\u17B6\u1789&\u178A\u17B6\u1780\u17CB\u17A0\u17D2\u179C\u17B6\u179B\u17CB\u17AF\u1780\u179F\u17B6\u179A\u179A\u1794\u179F\u17CB\u17A2\u17D2\u1793\u1780 \u17AC \u179F\u17D2\u179C\u17C2\u1784\u179A\u1780 ',labelInvalidField:"\u1785\u1793\u17D2\u179B\u17C4\u17C7\u1798\u17B6\u1793\u17AF\u1780\u179F\u17B6\u179A\u1798\u17B7\u1793\u178F\u17D2\u179A\u17B9\u1798\u178F\u17D2\u179A\u17BC\u179C",labelFileWaitingForSize:"\u1780\u17C6\u1796\u17BB\u1784\u179A\u1784\u17CB\u1785\u17B6\u17C6\u1791\u17C6\u17A0\u17C6",labelFileSizeNotAvailable:"\u1791\u17C6\u17A0\u17C6\u1798\u17B7\u1793\u17A2\u17B6\u1785\u1794\u17D2\u179A\u17BE\u1794\u17B6\u1793",labelFileLoading:"\u1780\u17C6\u1796\u17BB\u1784\u178A\u17C6\u178E\u17BE\u179A\u1780\u17B6\u179A",labelFileLoadError:"\u1798\u17B6\u1793\u1794\u1789\u17D2\u17A0\u17B6\u1780\u17C6\u17A1\u17BB\u1784\u1796\u17C1\u179B\u178A\u17C6\u178E\u17BE\u179A\u1780\u17B6\u179A",labelFileProcessing:"\u1780\u17C6\u1796\u17BB\u1784\u1795\u17D2\u1791\u17BB\u1780\u17A1\u17BE\u1784",labelFileProcessingComplete:"\u1780\u17B6\u179A\u1795\u17D2\u1791\u17BB\u1780\u17A1\u17BE\u1784\u1796\u17C1\u1789\u179B\u17C1\u1789",labelFileProcessingAborted:"\u1780\u17B6\u179A\u1794\u1784\u17D2\u17A0\u17C4\u17C7\u178F\u17D2\u179A\u17BC\u179C\u1794\u17B6\u1793\u1794\u17C4\u17C7\u1794\u1784\u17CB",labelFileProcessingError:"\u1798\u17B6\u1793\u1794\u1789\u17D2\u17A0\u17B6\u1780\u17C6\u17A1\u17BB\u1784\u1796\u17C1\u179B\u1780\u17C6\u1796\u17BB\u1784\u1795\u17D2\u1791\u17BB\u1780\u17A1\u17BE\u1784",labelFileProcessingRevertError:"\u1798\u17B6\u1793\u1794\u1789\u17D2\u17A0\u17B6\u1780\u17C6\u17A1\u17BB\u1784\u1796\u17C1\u179B\u178F\u17D2\u179A\u17A1\u1794\u17CB",labelFileRemoveError:"\u1798\u17B6\u1793\u1794\u1789\u17D2\u17A0\u17B6\u1780\u17C6\u17A1\u17BB\u1784\u1796\u17C1\u179B\u178A\u1780\u1785\u17C1\u1789",labelTapToCancel:"\u1785\u17BB\u1785\u178A\u17BE\u1798\u17D2\u1794\u17B8\u1794\u17C4\u17C7\u1794\u1784\u17CB",labelTapToRetry:"\u1785\u17BB\u1785\u178A\u17BE\u1798\u17D2\u1794\u17B8\u1796\u17D2\u1799\u17B6\u1799\u17B6\u1798\u1798\u17D2\u178F\u1784\u1791\u17C0\u178F",labelTapToUndo:"\u1785\u17BB\u1785\u178A\u17BE\u1798\u17D2\u1794\u17B8\u1798\u17B7\u1793\u1792\u17D2\u179C\u17BE\u179C\u17B7\u1789",labelButtonRemoveItem:"\u1799\u1780\u1785\u17C1\u1789",labelButtonAbortItemLoad:"\u1794\u17C4\u17C7\u1794\u1784\u17CB",labelButtonRetryItemLoad:"\u1796\u17D2\u1799\u17B6\u1799\u17B6\u1798\u1798\u17D2\u178F\u1784\u1791\u17C0\u178F",labelButtonAbortItemProcessing:"\u1794\u17C4\u17C7\u1794\u1784\u17CB",labelButtonUndoItemProcessing:"\u1798\u17B7\u1793\u1792\u17D2\u179C\u17BE\u179C\u17B7\u1789",labelButtonRetryItemProcessing:"\u1796\u17D2\u1799\u17B6\u1799\u17B6\u1798\u1798\u17D2\u178F\u1784\u1791\u17C0\u178F",labelButtonProcessItem:"\u1795\u17D2\u1791\u17BB\u1780\u17A1\u17BE\u1784",labelMaxFileSizeExceeded:"\u17AF\u1780\u179F\u17B6\u179A\u1792\u17C6\u1796\u17C1\u1780",labelMaxFileSize:"\u1791\u17C6\u17A0\u17C6\u17AF\u1780\u179F\u17B6\u179A\u17A2\u178F\u17B7\u1794\u179A\u1798\u17B6\u1782\u17BA {filesize}",labelMaxTotalFileSizeExceeded:"\u179B\u17BE\u179F\u1791\u17C6\u17A0\u17C6\u179F\u179A\u17BB\u1794\u17A2\u178F\u17B7\u1794\u179A\u1798\u17B6",labelMaxTotalFileSize:"\u1791\u17C6\u17A0\u17C6\u17AF\u1780\u179F\u17B6\u179A\u179F\u179A\u17BB\u1794\u17A2\u178F\u17B7\u1794\u179A\u1798\u17B6\u1782\u17BA {filesize}",labelFileTypeNotAllowed:"\u1794\u17D2\u179A\u1797\u17C1\u1791\u17AF\u1780\u179F\u17B6\u179A\u1798\u17B7\u1793\u178F\u17D2\u179A\u17B9\u1798\u178F\u17D2\u179A\u17BC\u179C",fileValidateTypeLabelExpectedTypes:"\u179A\u17C6\u1796\u17B9\u1784\u1790\u17B6 {allButLastType} \u17AC {lastType}",imageValidateSizeLabelFormatError:"\u1794\u17D2\u179A\u1797\u17C1\u1791\u179A\u17BC\u1794\u1797\u17B6\u1796\u1798\u17B7\u1793\u178F\u17D2\u179A\u17B9\u1798\u178F\u17D2\u179A\u17BC\u179C",imageValidateSizeLabelImageSizeTooSmall:"\u179A\u17BC\u1794\u1797\u17B6\u1796\u178F\u17BC\u1785\u1796\u17C1\u1780",imageValidateSizeLabelImageSizeTooBig:"\u179A\u17BC\u1794\u1797\u17B6\u1796\u1792\u17C6\u1796\u17C1\u1780",imageValidateSizeLabelExpectedMinSize:"\u1791\u17C6\u17A0\u17C6\u17A2\u1794\u17D2\u1794\u1794\u179A\u1798\u17B6\u1782\u17BA {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"\u1791\u17C6\u17A0\u17C6\u17A2\u178F\u17B7\u1794\u179A\u1798\u17B6\u1782\u17BA {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"\u1782\u17BB\u178E\u1797\u17B6\u1796\u1794\u1784\u17D2\u17A0\u17B6\u1789\u1791\u17B6\u1794\u1796\u17C1\u1780",imageValidateSizeLabelImageResolutionTooHigh:"\u1782\u17BB\u178E\u1797\u17B6\u1796\u1794\u1784\u17D2\u17A0\u17B6\u1789\u1781\u17D2\u1796\u179F\u17CB\u1796\u17C1\u1780",imageValidateSizeLabelExpectedMinResolution:"\u1782\u17BB\u178E\u1797\u17B6\u1796\u1794\u1784\u17D2\u17A0\u17B6\u1789\u17A2\u1794\u17D2\u1794\u1794\u179A\u1798\u17B6\u1782\u17BA {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"\u1782\u17BB\u178E\u1797\u17B6\u1796\u1794\u1784\u17D2\u17A0\u17B6\u1789\u17A2\u178F\u17B7\u1794\u179A\u1798\u17B6\u1782\u17BA {maxResolution}"};var $o={labelIdle:'\uD30C\uC77C\uC744 \uB4DC\uB798\uADF8 \uD558\uAC70\uB098 \uCC3E\uC544\uBCF4\uAE30 ',labelInvalidField:"\uD544\uB4DC\uC5D0 \uC720\uD6A8\uD558\uC9C0 \uC54A\uC740 \uD30C\uC77C\uC774 \uC788\uC2B5\uB2C8\uB2E4.",labelFileWaitingForSize:"\uC6A9\uB7C9 \uD655\uC778\uC911",labelFileSizeNotAvailable:"\uC0AC\uC6A9\uD560 \uC218 \uC5C6\uB294 \uC6A9\uB7C9",labelFileLoading:"\uBD88\uB7EC\uC624\uB294 \uC911",labelFileLoadError:"\uD30C\uC77C \uBD88\uB7EC\uC624\uAE30 \uC2E4\uD328",labelFileProcessing:"\uC5C5\uB85C\uB4DC \uC911",labelFileProcessingComplete:"\uC5C5\uB85C\uB4DC \uC131\uACF5",labelFileProcessingAborted:"\uC5C5\uB85C\uB4DC \uCDE8\uC18C\uB428",labelFileProcessingError:"\uD30C\uC77C \uC5C5\uB85C\uB4DC \uC2E4\uD328",labelFileProcessingRevertError:"\uB418\uB3CC\uB9AC\uAE30 \uC2E4\uD328",labelFileRemoveError:"\uC81C\uAC70 \uC2E4\uD328",labelTapToCancel:"\uD0ED\uD558\uC5EC \uCDE8\uC18C",labelTapToRetry:"\uD0ED\uD558\uC5EC \uC7AC\uC2DC\uC791",labelTapToUndo:"\uD0ED\uD558\uC5EC \uC2E4\uD589 \uCDE8\uC18C",labelButtonRemoveItem:"\uC81C\uAC70",labelButtonAbortItemLoad:"\uC911\uB2E8",labelButtonRetryItemLoad:"\uC7AC\uC2DC\uC791",labelButtonAbortItemProcessing:"\uCDE8\uC18C",labelButtonUndoItemProcessing:"\uC2E4\uD589 \uCDE8\uC18C",labelButtonRetryItemProcessing:"\uC7AC\uC2DC\uC791",labelButtonProcessItem:"\uC5C5\uB85C\uB4DC",labelMaxFileSizeExceeded:"\uD30C\uC77C\uC774 \uB108\uBB34 \uD07D\uB2C8\uB2E4.",labelMaxFileSize:"\uCD5C\uB300 \uD30C\uC77C \uC6A9\uB7C9\uC740 {filesize} \uC785\uB2C8\uB2E4.",labelMaxTotalFileSizeExceeded:"\uCD5C\uB300 \uC804\uCCB4 \uD30C\uC77C \uC6A9\uB7C9 \uCD08\uACFC\uD558\uC600\uC2B5\uB2C8\uB2E4.",labelMaxTotalFileSize:"\uCD5C\uB300 \uC804\uCCB4 \uD30C\uC77C \uC6A9\uB7C9\uC740 {filesize} \uC785\uB2C8\uB2E4.",labelFileTypeNotAllowed:"\uC798\uBABB\uB41C \uD615\uC2DD\uC758 \uD30C\uC77C",fileValidateTypeLabelExpectedTypes:"{allButLastType} \uB610\uB294 {lastType}",imageValidateSizeLabelFormatError:"\uC9C0\uC6D0\uB418\uC9C0 \uC54A\uB294 \uC774\uBBF8\uC9C0 \uC720\uD615",imageValidateSizeLabelImageSizeTooSmall:"\uC774\uBBF8\uC9C0\uAC00 \uB108\uBB34 \uC791\uC2B5\uB2C8\uB2E4.",imageValidateSizeLabelImageSizeTooBig:"\uC774\uBBF8\uC9C0\uAC00 \uB108\uBB34 \uD07D\uB2C8\uB2E4.",imageValidateSizeLabelExpectedMinSize:"\uC774\uBBF8\uC9C0 \uCD5C\uC18C \uD06C\uAE30\uB294 {minWidth} \xD7 {minHeight} \uC785\uB2C8\uB2E4",imageValidateSizeLabelExpectedMaxSize:"\uC774\uBBF8\uC9C0 \uCD5C\uB300 \uD06C\uAE30\uB294 {maxWidth} \xD7 {maxHeight} \uC785\uB2C8\uB2E4",imageValidateSizeLabelImageResolutionTooLow:"\uD574\uC0C1\uB3C4\uAC00 \uB108\uBB34 \uB0AE\uC2B5\uB2C8\uB2E4.",imageValidateSizeLabelImageResolutionTooHigh:"\uD574\uC0C1\uB3C4\uAC00 \uB108\uBB34 \uB192\uC2B5\uB2C8\uB2E4.",imageValidateSizeLabelExpectedMinResolution:"\uCD5C\uC18C \uD574\uC0C1\uB3C4\uB294 {minResolution} \uC785\uB2C8\uB2E4.",imageValidateSizeLabelExpectedMaxResolution:"\uCD5C\uB300 \uD574\uC0C1\uB3C4\uB294 {maxResolution} \uC785\uB2C8\uB2E4."};var Xo={labelIdle:'\u012Ed\u0117kite failus \u010Dia arba Ie\u0161kokite ',labelInvalidField:"Laukelis talpina netinkamus failus",labelFileWaitingForSize:"Laukiama dyd\u017Eio",labelFileSizeNotAvailable:"Dydis ne\u017Einomas",labelFileLoading:"Kraunama",labelFileLoadError:"Klaida \u012Fkeliant",labelFileProcessing:"\u012Ekeliama",labelFileProcessingComplete:"\u012Ek\u0117limas s\u0117kmingas",labelFileProcessingAborted:"\u012Ek\u0117limas at\u0161auktas",labelFileProcessingError:"\u012Ekeliant \u012Fvyko klaida",labelFileProcessingRevertError:"At\u0161aukiant \u012Fvyko klaida",labelFileRemoveError:"I\u0161trinant \u012Fvyko klaida",labelTapToCancel:"Palieskite nor\u0117dami at\u0161aukti",labelTapToRetry:"Palieskite nor\u0117dami pakartoti",labelTapToUndo:"Palieskite nor\u0117dami at\u0161aukti",labelButtonRemoveItem:"I\u0161trinti",labelButtonAbortItemLoad:"Sustabdyti",labelButtonRetryItemLoad:"Pakartoti",labelButtonAbortItemProcessing:"At\u0161aukti",labelButtonUndoItemProcessing:"At\u0161aukti",labelButtonRetryItemProcessing:"Pakartoti",labelButtonProcessItem:"\u012Ekelti",labelMaxFileSizeExceeded:"Failas per didelis",labelMaxFileSize:"Maksimalus failo dydis yra {filesize}",labelMaxTotalFileSizeExceeded:"Vir\u0161ijote maksimal\u0173 leistin\u0105 dyd\u012F",labelMaxTotalFileSize:"Maksimalus leistinas dydis yra {filesize}",labelFileTypeNotAllowed:"Netinkamas failas",fileValidateTypeLabelExpectedTypes:"Tikisi {allButLastType} arba {lastType}",imageValidateSizeLabelFormatError:"Nuotraukos formatas nepalaikomas",imageValidateSizeLabelImageSizeTooSmall:"Nuotrauka per ma\u017Ea",imageValidateSizeLabelImageSizeTooBig:"Nuotrauka per didel\u0117",imageValidateSizeLabelExpectedMinSize:"Minimalus dydis yra {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"Maksimalus dydis yra {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"Rezoliucija per ma\u017Ea",imageValidateSizeLabelImageResolutionTooHigh:"Rezoliucija per didel\u0117",imageValidateSizeLabelExpectedMinResolution:"Minimali rezoliucija yra {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"Maksimali rezoliucija yra {maxResolution}"};var Ko={labelIdle:'I file hn\xFBkl\xFBt rawh, emaw Zawnna ',labelInvalidField:"Hemi hian files diklo a kengtel",labelFileWaitingForSize:"A lenzawng a ngh\xE2k mek",labelFileSizeNotAvailable:"A lenzawng a awmlo",labelFileLoading:"Loading",labelFileLoadError:"Load laiin dik lo a awm",labelFileProcessing:"Uploading",labelFileProcessingComplete:"Upload a zo",labelFileProcessingAborted:"Upload s\xFBt a ni",labelFileProcessingError:"Upload laiin dik lo a awm",labelFileProcessingRevertError:"Dahk\xEEr laiin dik lo a awm",labelFileRemoveError:"Paih laiin dik lo a awm",labelTapToCancel:"S\xFBt turin hmet rawh",labelTapToRetry:"Tinawn turin hmet rawh",labelTapToUndo:"Tilet turin hmet rawh",labelButtonRemoveItem:"Paihna",labelButtonAbortItemLoad:"Tihtlawlhna",labelButtonRetryItemLoad:"Tihnawnna",labelButtonAbortItemProcessing:"S\xFBtna",labelButtonUndoItemProcessing:"Tihletna",labelButtonRetryItemProcessing:"Tihnawnna",labelButtonProcessItem:"Upload",labelMaxFileSizeExceeded:"File a lian lutuk",labelMaxFileSize:"File lenzawng tam ber chu {filesize} ani",labelMaxTotalFileSizeExceeded:"A lenzawng belh kh\xE2wm tam ber a p\xEAl",labelMaxTotalFileSize:"File lenzawng belh kh\xE2wm tam ber chu {filesize} a ni",labelFileTypeNotAllowed:"File type dik lo a ni",fileValidateTypeLabelExpectedTypes:"{allButLastType} emaw {lastType} emaw beisei a ni",imageValidateSizeLabelFormatError:"Thlal\xE2k type a thl\xE2wplo",imageValidateSizeLabelImageSizeTooSmall:"Thlal\xE2k hi a t\xEA lutuk",imageValidateSizeLabelImageSizeTooBig:"Thlal\xE2k hi a lian lutuk",imageValidateSizeLabelExpectedMinSize:"A lenzawng tl\xEAm ber chu {minWidth} x {minHeight} a ni",imageValidateSizeLabelExpectedMaxSize:"A lenzawng tam ber chu {maxWidth} x {maxHeight} a ni",imageValidateSizeLabelImageResolutionTooLow:"Resolution a hniam lutuk",imageValidateSizeLabelImageResolutionTooHigh:"Resolution a s\xE2ng lutuk",imageValidateSizeLabelExpectedMinResolution:"Resolution hniam ber chu {minResolution} a ni",imageValidateSizeLabelExpectedMaxResolution:"Resolution s\xE2ng ber chu {maxResolution} a ni"};var Zo={labelIdle:'Ievelciet savus failus vai p\u0101rl\u016Bkojiet \u0161eit ',labelInvalidField:"Lauks satur neder\u012Bgus failus",labelFileWaitingForSize:"Gaid\u0101m faila izm\u0113ru",labelFileSizeNotAvailable:"Izm\u0113rs nav pieejams",labelFileLoading:"Notiek iel\u0101de",labelFileLoadError:"Notika k\u013C\u016Bda iel\u0101des laik\u0101",labelFileProcessing:"Notiek aug\u0161upiel\u0101de",labelFileProcessingComplete:"Aug\u0161upiel\u0101de pabeigta",labelFileProcessingAborted:"Aug\u0161upiel\u0101de atcelta",labelFileProcessingError:"Notika k\u013C\u016Bda aug\u0161upiel\u0101des laik\u0101",labelFileProcessingRevertError:"Notika k\u013C\u016Bda atgrie\u0161anas laik\u0101",labelFileRemoveError:"Notika k\u013C\u016Bda dz\u0113\u0161anas laik\u0101",labelTapToCancel:"pieskarieties, lai atceltu",labelTapToRetry:"pieskarieties, lai m\u0113\u0123in\u0101tu v\u0113lreiz",labelTapToUndo:"pieskarieties, lai atsauktu",labelButtonRemoveItem:"Dz\u0113st",labelButtonAbortItemLoad:"P\u0101rtraukt",labelButtonRetryItemLoad:"M\u0113\u0123in\u0101t v\u0113lreiz",labelButtonAbortItemProcessing:"P\u0101rtraucam",labelButtonUndoItemProcessing:"Atsaucam",labelButtonRetryItemProcessing:"M\u0113\u0123in\u0101m v\u0113lreiz",labelButtonProcessItem:"Aug\u0161upiel\u0101d\u0113t",labelMaxFileSizeExceeded:"Fails ir p\u0101r\u0101k liels",labelMaxFileSize:"Maksim\u0101lais faila izm\u0113rs ir {filesize}",labelMaxTotalFileSizeExceeded:"P\u0101rsniegts maksim\u0101lais kop\u0113jais failu izm\u0113rs",labelMaxTotalFileSize:"Maksim\u0101lais kop\u0113jais failu izm\u0113rs ir {filesize}",labelFileTypeNotAllowed:"Neder\u012Bgs faila tips",fileValidateTypeLabelExpectedTypes:"Sagaid\u0101m {allButLastType} vai {lastType}",imageValidateSizeLabelFormatError:"Neatbilsto\u0161s att\u0113la tips",imageValidateSizeLabelImageSizeTooSmall:"Att\u0113ls ir p\u0101r\u0101k mazs",imageValidateSizeLabelImageSizeTooBig:"Att\u0113ls ir p\u0101r\u0101k liels",imageValidateSizeLabelExpectedMinSize:"Minim\u0101lais izm\u0113rs ir {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"Maksim\u0101lais izm\u0113rs ir {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"Iz\u0161\u0137irtsp\u0113ja ir p\u0101r\u0101k zema",imageValidateSizeLabelImageResolutionTooHigh:"Iz\u0161\u0137irtsp\u0113ja ir p\u0101r\u0101k augsta",imageValidateSizeLabelExpectedMinResolution:"Minim\u0101l\u0101 iz\u0161\u0137irtsp\u0113ja ir {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"Maksim\u0101l\u0101 iz\u0161\u0137irtsp\u0113ja ir {maxResolution}"};var Qo={labelIdle:'Dra og slipp filene dine, eller Bla gjennom... ',labelInvalidField:"Feltet inneholder ugyldige filer",labelFileWaitingForSize:"Venter p\xE5 st\xF8rrelse",labelFileSizeNotAvailable:"St\xF8rrelse ikke tilgjengelig",labelFileLoading:"Laster",labelFileLoadError:"Feil under lasting",labelFileProcessing:"Laster opp",labelFileProcessingComplete:"Opplasting ferdig",labelFileProcessingAborted:"Opplasting avbrutt",labelFileProcessingError:"Feil under opplasting",labelFileProcessingRevertError:"Feil under reversering",labelFileRemoveError:"Feil under flytting",labelTapToCancel:"klikk for \xE5 avbryte",labelTapToRetry:"klikk for \xE5 pr\xF8ve p\xE5 nytt",labelTapToUndo:"klikk for \xE5 angre",labelButtonRemoveItem:"Fjern",labelButtonAbortItemLoad:"Avbryt",labelButtonRetryItemLoad:"Pr\xF8v p\xE5 nytt",labelButtonAbortItemProcessing:"Avbryt",labelButtonUndoItemProcessing:"Angre",labelButtonRetryItemProcessing:"Pr\xF8v p\xE5 nytt",labelButtonProcessItem:"Last opp",labelMaxFileSizeExceeded:"Filen er for stor",labelMaxFileSize:"Maksimal filst\xF8rrelse er {filesize}",labelMaxTotalFileSizeExceeded:"Maksimal total st\xF8rrelse oversteget",labelMaxTotalFileSize:"Maksimal total st\xF8rrelse er {filesize}",labelFileTypeNotAllowed:"Ugyldig filtype",fileValidateTypeLabelExpectedTypes:"Forventer {allButLastType} eller {lastType}",imageValidateSizeLabelFormatError:"Bildeformat ikke st\xF8ttet",imageValidateSizeLabelImageSizeTooSmall:"Bildet er for lite",imageValidateSizeLabelImageSizeTooBig:"Bildet er for stort",imageValidateSizeLabelExpectedMinSize:"Minimumsst\xF8rrelse er {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"Maksimumsst\xF8rrelse er {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"Oppl\xF8sningen er for lav",imageValidateSizeLabelImageResolutionTooHigh:"Oppl\xF8sningen er for h\xF8y",imageValidateSizeLabelExpectedMinResolution:"Minimum oppl\xF8sning er {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"Maksimal oppl\xF8sning er {maxResolution}"};var Jo={labelIdle:'Drag & Drop je bestanden of Bladeren ',labelInvalidField:"Veld bevat ongeldige bestanden",labelFileWaitingForSize:"Wachten op grootte",labelFileSizeNotAvailable:"Grootte niet beschikbaar",labelFileLoading:"Laden",labelFileLoadError:"Fout tijdens laden",labelFileProcessing:"Uploaden",labelFileProcessingComplete:"Upload afgerond",labelFileProcessingAborted:"Upload geannuleerd",labelFileProcessingError:"Fout tijdens upload",labelFileProcessingRevertError:"Fout bij herstellen",labelFileRemoveError:"Fout bij verwijderen",labelTapToCancel:"tik om te annuleren",labelTapToRetry:"tik om opnieuw te proberen",labelTapToUndo:"tik om ongedaan te maken",labelButtonRemoveItem:"Verwijderen",labelButtonAbortItemLoad:"Afbreken",labelButtonRetryItemLoad:"Opnieuw proberen",labelButtonAbortItemProcessing:"Annuleren",labelButtonUndoItemProcessing:"Ongedaan maken",labelButtonRetryItemProcessing:"Opnieuw proberen",labelButtonProcessItem:"Upload",labelMaxFileSizeExceeded:"Bestand is te groot",labelMaxFileSize:"Maximale bestandsgrootte is {filesize}",labelMaxTotalFileSizeExceeded:"Maximale totale grootte overschreden",labelMaxTotalFileSize:"Maximale totale bestandsgrootte is {filesize}",labelFileTypeNotAllowed:"Ongeldig bestandstype",fileValidateTypeLabelExpectedTypes:"Verwacht {allButLastType} of {lastType}",imageValidateSizeLabelFormatError:"Afbeeldingstype niet ondersteund",imageValidateSizeLabelImageSizeTooSmall:"Afbeelding is te klein",imageValidateSizeLabelImageSizeTooBig:"Afbeelding is te groot",imageValidateSizeLabelExpectedMinSize:"Minimale afmeting is {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"Maximale afmeting is {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"Resolutie is te laag",imageValidateSizeLabelImageResolutionTooHigh:"Resolution is too high",imageValidateSizeLabelExpectedMinResolution:"Minimale resolutie is {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"Maximale resolutie is {maxResolution}"};var er={labelIdle:'Przeci\u0105gnij i upu\u015B\u0107 lub wybierz pliki',labelInvalidField:"Nieprawid\u0142owe pliki",labelFileWaitingForSize:"Pobieranie rozmiaru",labelFileSizeNotAvailable:"Nieznany rozmiar",labelFileLoading:"Wczytywanie",labelFileLoadError:"B\u0142\u0105d wczytywania",labelFileProcessing:"Przesy\u0142anie",labelFileProcessingComplete:"Przes\u0142ano",labelFileProcessingAborted:"Przerwano",labelFileProcessingError:"Przesy\u0142anie nie powiod\u0142o si\u0119",labelFileProcessingRevertError:"Co\u015B posz\u0142o nie tak",labelFileRemoveError:"Nieudane usuni\u0119cie",labelTapToCancel:"Anuluj",labelTapToRetry:"Pon\xF3w",labelTapToUndo:"Cofnij",labelButtonRemoveItem:"Usu\u0144",labelButtonAbortItemLoad:"Przerwij",labelButtonRetryItemLoad:"Pon\xF3w",labelButtonAbortItemProcessing:"Anuluj",labelButtonUndoItemProcessing:"Cofnij",labelButtonRetryItemProcessing:"Pon\xF3w",labelButtonProcessItem:"Prze\u015Blij",labelMaxFileSizeExceeded:"Plik jest zbyt du\u017Cy",labelMaxFileSize:"Dopuszczalna wielko\u015B\u0107 pliku to {filesize}",labelMaxTotalFileSizeExceeded:"Przekroczono \u0142\u0105czny rozmiar plik\xF3w",labelMaxTotalFileSize:"\u0141\u0105czny rozmiar plik\xF3w nie mo\u017Ce przekroczy\u0107 {filesize}",labelFileTypeNotAllowed:"Niedozwolony rodzaj pliku",fileValidateTypeLabelExpectedTypes:"Oczekiwano {allButLastType} lub {lastType}",imageValidateSizeLabelFormatError:"Nieobs\u0142ugiwany format obrazu",imageValidateSizeLabelImageSizeTooSmall:"Obraz jest zbyt ma\u0142y",imageValidateSizeLabelImageSizeTooBig:"Obraz jest zbyt du\u017Cy",imageValidateSizeLabelExpectedMinSize:"Minimalne wymiary obrazu to {minWidth}\xD7{minHeight}",imageValidateSizeLabelExpectedMaxSize:"Maksymalna wymiary obrazu to {maxWidth}\xD7{maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"Rozdzielczo\u015B\u0107 jest zbyt niska",imageValidateSizeLabelImageResolutionTooHigh:"Rozdzielczo\u015B\u0107 jest zbyt wysoka",imageValidateSizeLabelExpectedMinResolution:"Minimalna rozdzielczo\u015B\u0107 to {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"Maksymalna rozdzielczo\u015B\u0107 to {maxResolution}"};var tr={labelIdle:'Arraste & Largue os ficheiros ou Seleccione ',labelInvalidField:"O campo cont\xE9m ficheiros inv\xE1lidos",labelFileWaitingForSize:"A aguardar tamanho",labelFileSizeNotAvailable:"Tamanho n\xE3o dispon\xEDvel",labelFileLoading:"A carregar",labelFileLoadError:"Erro ao carregar",labelFileProcessing:"A carregar",labelFileProcessingComplete:"Carregamento completo",labelFileProcessingAborted:"Carregamento cancelado",labelFileProcessingError:"Erro ao carregar",labelFileProcessingRevertError:"Erro ao reverter",labelFileRemoveError:"Erro ao remover",labelTapToCancel:"carregue para cancelar",labelTapToRetry:"carregue para tentar novamente",labelTapToUndo:"carregue para desfazer",labelButtonRemoveItem:"Remover",labelButtonAbortItemLoad:"Abortar",labelButtonRetryItemLoad:"Tentar novamente",labelButtonAbortItemProcessing:"Cancelar",labelButtonUndoItemProcessing:"Desfazer",labelButtonRetryItemProcessing:"Tentar novamente",labelButtonProcessItem:"Carregar",labelMaxFileSizeExceeded:"Ficheiro demasiado grande",labelMaxFileSize:"O tamanho m\xE1ximo do ficheiro \xE9 de {filesize}",labelMaxTotalFileSizeExceeded:"Tamanho m\xE1ximo total excedido",labelMaxTotalFileSize:"O tamanho m\xE1ximo total do ficheiro \xE9 de {filesize}",labelFileTypeNotAllowed:"Tipo de ficheiro inv\xE1lido",fileValidateTypeLabelExpectedTypes:"\xC9 esperado {allButLastType} ou {lastType}",imageValidateSizeLabelFormatError:"Tipo de imagem n\xE3o suportada",imageValidateSizeLabelImageSizeTooSmall:"A imagem \xE9 demasiado pequena",imageValidateSizeLabelImageSizeTooBig:"A imagem \xE9 demasiado grande",imageValidateSizeLabelExpectedMinSize:"O tamanho m\xEDnimo \xE9 de {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"O tamanho m\xE1ximo \xE9 de {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"A resolu\xE7\xE3o \xE9 demasiado baixa",imageValidateSizeLabelImageResolutionTooHigh:"A resolu\xE7\xE3o \xE9 demasiado grande",imageValidateSizeLabelExpectedMinResolution:"A resolu\xE7\xE3o m\xEDnima \xE9 de {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"A resolu\xE7\xE3o m\xE1xima \xE9 de {maxResolution}"};var ir={labelIdle:'Arraste e solte os arquivos ou Clique aqui ',labelInvalidField:"Arquivos inv\xE1lidos",labelFileWaitingForSize:"Calculando o tamanho do arquivo",labelFileSizeNotAvailable:"Tamanho do arquivo indispon\xEDvel",labelFileLoading:"Carregando",labelFileLoadError:"Erro durante o carregamento",labelFileProcessing:"Enviando",labelFileProcessingComplete:"Envio finalizado",labelFileProcessingAborted:"Envio cancelado",labelFileProcessingError:"Erro durante o envio",labelFileProcessingRevertError:"Erro ao reverter o envio",labelFileRemoveError:"Erro ao remover o arquivo",labelTapToCancel:"clique para cancelar",labelTapToRetry:"clique para reenviar",labelTapToUndo:"clique para desfazer",labelButtonRemoveItem:"Remover",labelButtonAbortItemLoad:"Abortar",labelButtonRetryItemLoad:"Reenviar",labelButtonAbortItemProcessing:"Cancelar",labelButtonUndoItemProcessing:"Desfazer",labelButtonRetryItemProcessing:"Reenviar",labelButtonProcessItem:"Enviar",labelMaxFileSizeExceeded:"Arquivo \xE9 muito grande",labelMaxFileSize:"O tamanho m\xE1ximo permitido: {filesize}",labelMaxTotalFileSizeExceeded:"Tamanho total dos arquivos excedido",labelMaxTotalFileSize:"Tamanho total permitido: {filesize}",labelFileTypeNotAllowed:"Tipo de arquivo inv\xE1lido",fileValidateTypeLabelExpectedTypes:"Tipos de arquivo suportados s\xE3o {allButLastType} ou {lastType}",imageValidateSizeLabelFormatError:"Tipo de imagem inv\xE1lida",imageValidateSizeLabelImageSizeTooSmall:"Imagem muito pequena",imageValidateSizeLabelImageSizeTooBig:"Imagem muito grande",imageValidateSizeLabelExpectedMinSize:"Tamanho m\xEDnimo permitida: {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"Tamanho m\xE1ximo permitido: {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"Resolu\xE7\xE3o muito baixa",imageValidateSizeLabelImageResolutionTooHigh:"Resolu\xE7\xE3o muito alta",imageValidateSizeLabelExpectedMinResolution:"Resolu\xE7\xE3o m\xEDnima permitida: {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"Resolu\xE7\xE3o m\xE1xima permitida: {maxResolution}"};var ar={labelIdle:'Trage \u0219i plaseaz\u0103 fi\u0219iere sau Caut\u0103-le ',labelInvalidField:"C\xE2mpul con\u021Bine fi\u0219iere care nu sunt valide",labelFileWaitingForSize:"\xCEn a\u0219teptarea dimensiunii",labelFileSizeNotAvailable:"Dimensiunea nu este diponibil\u0103",labelFileLoading:"Se \xEEncarc\u0103",labelFileLoadError:"Eroare la \xEEnc\u0103rcare",labelFileProcessing:"Se \xEEncarc\u0103",labelFileProcessingComplete:"\xCEnc\u0103rcare finalizat\u0103",labelFileProcessingAborted:"\xCEnc\u0103rcare anulat\u0103",labelFileProcessingError:"Eroare la \xEEnc\u0103rcare",labelFileProcessingRevertError:"Eroare la anulare",labelFileRemoveError:"Eroare la \u015Ftergere",labelTapToCancel:"apas\u0103 pentru a anula",labelTapToRetry:"apas\u0103 pentru a re\xEEncerca",labelTapToUndo:"apas\u0103 pentru a anula",labelButtonRemoveItem:"\u015Eterge",labelButtonAbortItemLoad:"Anuleaz\u0103",labelButtonRetryItemLoad:"Re\xEEncearc\u0103",labelButtonAbortItemProcessing:"Anuleaz\u0103",labelButtonUndoItemProcessing:"Anuleaz\u0103",labelButtonRetryItemProcessing:"Re\xEEncearc\u0103",labelButtonProcessItem:"\xCEncarc\u0103",labelMaxFileSizeExceeded:"Fi\u0219ierul este prea mare",labelMaxFileSize:"Dimensiunea maxim\u0103 a unui fi\u0219ier este de {filesize}",labelMaxTotalFileSizeExceeded:"Dimensiunea total\u0103 maxim\u0103 a fost dep\u0103\u0219it\u0103",labelMaxTotalFileSize:"Dimensiunea total\u0103 maxim\u0103 a fi\u0219ierelor este de {filesize}",labelFileTypeNotAllowed:"Tipul fi\u0219ierului nu este valid",fileValidateTypeLabelExpectedTypes:"Se a\u0219teapt\u0103 {allButLastType} sau {lastType}",imageValidateSizeLabelFormatError:"Formatul imaginii nu este acceptat",imageValidateSizeLabelImageSizeTooSmall:"Imaginea este prea mic\u0103",imageValidateSizeLabelImageSizeTooBig:"Imaginea este prea mare",imageValidateSizeLabelExpectedMinSize:"M\u0103rimea minim\u0103 este de {maxWidth} x {maxHeight}",imageValidateSizeLabelExpectedMaxSize:"M\u0103rimea maxim\u0103 este de {maxWidth} x {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"Rezolu\u021Bia este prea mic\u0103",imageValidateSizeLabelImageResolutionTooHigh:"Rezolu\u021Bia este prea mare",imageValidateSizeLabelExpectedMinResolution:"Rezolu\u021Bia minim\u0103 este de {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"Rezolu\u021Bia maxim\u0103 este de {maxResolution}"};var nr={labelIdle:'\u041F\u0435\u0440\u0435\u0442\u0430\u0449\u0438\u0442\u0435 \u0444\u0430\u0439\u043B\u044B \u0438\u043B\u0438 \u0432\u044B\u0431\u0435\u0440\u0438\u0442\u0435 ',labelInvalidField:"\u041F\u043E\u043B\u0435 \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u0442 \u043D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u044B\u0435 \u0444\u0430\u0439\u043B\u044B",labelFileWaitingForSize:"\u0423\u043A\u0430\u0436\u0438\u0442\u0435 \u0440\u0430\u0437\u043C\u0435\u0440",labelFileSizeNotAvailable:"\u0420\u0430\u0437\u043C\u0435\u0440 \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F",labelFileLoading:"\u041E\u0436\u0438\u0434\u0430\u043D\u0438\u0435",labelFileLoadError:"\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438 \u043E\u0436\u0438\u0434\u0430\u043D\u0438\u0438",labelFileProcessing:"\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430",labelFileProcessingComplete:"\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u0430",labelFileProcessingAborted:"\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u043E\u0442\u043C\u0435\u043D\u0435\u043D\u0430",labelFileProcessingError:"\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438 \u0437\u0430\u0433\u0440\u0443\u0437\u043A\u0435",labelFileProcessingRevertError:"\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438 \u0432\u043E\u0437\u0432\u0440\u0430\u0442\u0435",labelFileRemoveError:"\u041E\u0448\u0438\u0431\u043A\u0430 \u043F\u0440\u0438 \u0443\u0434\u0430\u043B\u0435\u043D\u0438\u0438",labelTapToCancel:"\u043D\u0430\u0436\u043C\u0438\u0442\u0435 \u0434\u043B\u044F \u043E\u0442\u043C\u0435\u043D\u044B",labelTapToRetry:"\u043D\u0430\u0436\u043C\u0438\u0442\u0435, \u0447\u0442\u043E\u0431\u044B \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u044C \u043F\u043E\u043F\u044B\u0442\u043A\u0443",labelTapToUndo:"\u043D\u0430\u0436\u043C\u0438\u0442\u0435 \u0434\u043B\u044F \u043E\u0442\u043C\u0435\u043D\u044B \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u0433\u043E \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F",labelButtonRemoveItem:"\u0423\u0434\u0430\u043B\u0438\u0442\u044C",labelButtonAbortItemLoad:"\u041F\u0440\u0435\u043A\u0440\u0430\u0449\u0435\u043D\u043E",labelButtonRetryItemLoad:"\u041F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u0435 \u043F\u043E\u043F\u044B\u0442\u043A\u0443",labelButtonAbortItemProcessing:"\u041E\u0442\u043C\u0435\u043D\u0430",labelButtonUndoItemProcessing:"\u041E\u0442\u043C\u0435\u043D\u0430 \u043F\u043E\u0441\u043B\u0435\u0434\u043D\u0435\u0433\u043E \u0434\u0435\u0439\u0441\u0442\u0432\u0438\u044F",labelButtonRetryItemProcessing:"\u041F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u0435 \u043F\u043E\u043F\u044B\u0442\u043A\u0443",labelButtonProcessItem:"\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430",labelMaxFileSizeExceeded:"\u0424\u0430\u0439\u043B \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u0431\u043E\u043B\u044C\u0448\u043E\u0439",labelMaxFileSize:"\u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u0440\u0430\u0437\u043C\u0435\u0440 \u0444\u0430\u0439\u043B\u0430: {filesize}",labelMaxTotalFileSizeExceeded:"\u041F\u0440\u0435\u0432\u044B\u0448\u0435\u043D \u043C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u0440\u0430\u0437\u043C\u0435\u0440",labelMaxTotalFileSize:"\u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u0440\u0430\u0437\u043C\u0435\u0440 \u0444\u0430\u0439\u043B\u0430: {filesize}",labelFileTypeNotAllowed:"\u0424\u0430\u0439\u043B \u043D\u0435\u0432\u0435\u0440\u043D\u043E\u0433\u043E \u0442\u0438\u043F\u0430",fileValidateTypeLabelExpectedTypes:"\u041E\u0436\u0438\u0434\u0430\u0435\u0442\u0441\u044F {allButLastType} \u0438\u043B\u0438 {lastType}",imageValidateSizeLabelFormatError:"\u0422\u0438\u043F \u0438\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u044F \u043D\u0435 \u043F\u043E\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044F",imageValidateSizeLabelImageSizeTooSmall:"\u0418\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435 \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u043C\u0430\u043B\u0435\u043D\u044C\u043A\u043E\u0435",imageValidateSizeLabelImageSizeTooBig:"\u0418\u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u0438\u0435 \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u0431\u043E\u043B\u044C\u0448\u043E\u0435",imageValidateSizeLabelExpectedMinSize:"\u041C\u0438\u043D\u0438\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u0440\u0430\u0437\u043C\u0435\u0440: {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"\u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u044B\u0439 \u0440\u0430\u0437\u043C\u0435\u0440: {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u043D\u0438\u0437\u043A\u043E\u0435",imageValidateSizeLabelImageResolutionTooHigh:"\u0420\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u0435 \u0441\u043B\u0438\u0448\u043A\u043E\u043C \u0432\u044B\u0441\u043E\u043A\u043E\u0435",imageValidateSizeLabelExpectedMinResolution:"\u041C\u0438\u043D\u0438\u043C\u0430\u043B\u044C\u043D\u043E\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u0435: {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"\u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u043E\u0435 \u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u0438\u0435: {maxResolution}"};var lr={labelIdle:'Natiahn\xFA\u0165 s\xFAbor (drag&drop) alebo Vyh\u013Eada\u0165 ',labelInvalidField:"Pole obsahuje chybn\xE9 s\xFAbory",labelFileWaitingForSize:"Zis\u0165uje sa ve\u013Ekos\u0165",labelFileSizeNotAvailable:"Nezn\xE1ma ve\u013Ekos\u0165",labelFileLoading:"Pren\xE1\u0161a sa",labelFileLoadError:"Chyba pri prenose",labelFileProcessing:"Prebieha upload",labelFileProcessingComplete:"Upload dokon\u010Den\xFD",labelFileProcessingAborted:"Upload stornovan\xFD",labelFileProcessingError:"Chyba pri uploade",labelFileProcessingRevertError:"Chyba pri obnove",labelFileRemoveError:"Chyba pri odstr\xE1nen\xED",labelTapToCancel:"Kliknite pre storno",labelTapToRetry:"Kliknite pre opakovanie",labelTapToUndo:"Kliknite pre vr\xE1tenie",labelButtonRemoveItem:"Odstr\xE1ni\u0165",labelButtonAbortItemLoad:"Storno",labelButtonRetryItemLoad:"Opakova\u0165",labelButtonAbortItemProcessing:"Sp\xE4\u0165",labelButtonUndoItemProcessing:"Vr\xE1ti\u0165",labelButtonRetryItemProcessing:"Opakova\u0165",labelButtonProcessItem:"Upload",labelMaxFileSizeExceeded:"S\xFAbor je pr\xEDli\u0161 ve\u013Ek\xFD",labelMaxFileSize:"Najv\xE4\u010D\u0161ia ve\u013Ekos\u0165 s\xFAboru je {filesize}",labelMaxTotalFileSizeExceeded:"Prekro\u010Den\xE1 maxim\xE1lna celkov\xE1 ve\u013Ekos\u0165 s\xFAboru",labelMaxTotalFileSize:"Maxim\xE1lna celkov\xE1 ve\u013Ekos\u0165 s\xFAboru je {filesize}",labelFileTypeNotAllowed:"S\xFAbor je nespr\xE1vneho typu",fileValidateTypeLabelExpectedTypes:"O\u010Dak\xE1va sa {allButLastType} alebo {lastType}",imageValidateSizeLabelFormatError:"Obr\xE1zok tohto typu nie je podporovan\xFD",imageValidateSizeLabelImageSizeTooSmall:"Obr\xE1zok je pr\xEDli\u0161 mal\xFD",imageValidateSizeLabelImageSizeTooBig:"Obr\xE1zok je pr\xEDli\u0161 ve\u013Ek\xFD",imageValidateSizeLabelExpectedMinSize:"Minim\xE1lny rozmer je {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"Maxim\xE1lny rozmer je {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"Rozl\xED\u0161enie je pr\xEDli\u0161 mal\xE9",imageValidateSizeLabelImageResolutionTooHigh:"Rozli\u0161enie je pr\xEDli\u0161 ve\u013Ek\xE9",imageValidateSizeLabelExpectedMinResolution:"Minim\xE1lne rozl\xED\u0161enie je {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"Maxim\xE1lne rozl\xED\u0161enie je {maxResolution}"};var or={labelIdle:'Drag och sl\xE4pp dina filer eller Bl\xE4ddra ',labelInvalidField:"F\xE4ltet inneh\xE5ller felaktiga filer",labelFileWaitingForSize:"V\xE4ntar p\xE5 storlek",labelFileSizeNotAvailable:"Storleken finns inte tillg\xE4nglig",labelFileLoading:"Laddar",labelFileLoadError:"Fel under laddning",labelFileProcessing:"Laddar upp",labelFileProcessingComplete:"Uppladdning klar",labelFileProcessingAborted:"Uppladdning avbruten",labelFileProcessingError:"Fel under uppladdning",labelFileProcessingRevertError:"Fel under \xE5terst\xE4llning",labelFileRemoveError:"Fel under borttagning",labelTapToCancel:"tryck f\xF6r att avbryta",labelTapToRetry:"tryck f\xF6r att f\xF6rs\xF6ka igen",labelTapToUndo:"tryck f\xF6r att \xE5ngra",labelButtonRemoveItem:"Tabort",labelButtonAbortItemLoad:"Avbryt",labelButtonRetryItemLoad:"F\xF6rs\xF6k igen",labelButtonAbortItemProcessing:"Avbryt",labelButtonUndoItemProcessing:"\xC5ngra",labelButtonRetryItemProcessing:"F\xF6rs\xF6k igen",labelButtonProcessItem:"Ladda upp",labelMaxFileSizeExceeded:"Filen \xE4r f\xF6r stor",labelMaxFileSize:"St\xF6rsta till\xE5tna filstorlek \xE4r {filesize}",labelMaxTotalFileSizeExceeded:"Maximal uppladdningsstorlek uppn\xE5d",labelMaxTotalFileSize:"Maximal uppladdningsstorlek \xE4r {filesize}",labelFileTypeNotAllowed:"Felaktig filtyp",fileValidateTypeLabelExpectedTypes:"Godk\xE4nda filtyper {allButLastType} eller {lastType}",imageValidateSizeLabelFormatError:"Bildtypen saknar st\xF6d",imageValidateSizeLabelImageSizeTooSmall:"Bilden \xE4r f\xF6r liten",imageValidateSizeLabelImageSizeTooBig:"Bilden \xE4r f\xF6r stor",imageValidateSizeLabelExpectedMinSize:"Minimal storlek \xE4r {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"Maximal storlek \xE4r {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"Uppl\xF6sningen \xE4r f\xF6r l\xE5g",imageValidateSizeLabelImageResolutionTooHigh:"Uppl\xF6sningen \xE4r f\xF6r h\xF6g",imageValidateSizeLabelExpectedMinResolution:"Minsta till\xE5tna uppl\xF6sning \xE4r {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"H\xF6gsta till\xE5tna uppl\xF6sning \xE4r {maxResolution}"};var rr={labelIdle:'Dosyan\u0131z\u0131 S\xFCr\xFCkleyin & B\u0131rak\u0131n ya da Se\xE7in ',labelInvalidField:"Alan ge\xE7ersiz dosyalar i\xE7eriyor",labelFileWaitingForSize:"Boyut hesaplan\u0131yor",labelFileSizeNotAvailable:"Boyut mevcut de\u011Fil",labelFileLoading:"Y\xFCkleniyor",labelFileLoadError:"Y\xFCkleme s\u0131ras\u0131nda hata olu\u015Ftu",labelFileProcessing:"Y\xFCkleniyor",labelFileProcessingComplete:"Y\xFCkleme tamamland\u0131",labelFileProcessingAborted:"Y\xFCkleme iptal edildi",labelFileProcessingError:"Y\xFCklerken hata olu\u015Ftu",labelFileProcessingRevertError:"Geri \xE7ekerken hata olu\u015Ftu",labelFileRemoveError:"Kald\u0131r\u0131rken hata olu\u015Ftu",labelTapToCancel:"\u0130ptal etmek i\xE7in t\u0131klay\u0131n",labelTapToRetry:"Tekrar denemek i\xE7in t\u0131klay\u0131n",labelTapToUndo:"Geri almak i\xE7in t\u0131klay\u0131n",labelButtonRemoveItem:"Kald\u0131r",labelButtonAbortItemLoad:"\u0130ptal Et",labelButtonRetryItemLoad:"Tekrar dene",labelButtonAbortItemProcessing:"\u0130ptal et",labelButtonUndoItemProcessing:"Geri Al",labelButtonRetryItemProcessing:"Tekrar dene",labelButtonProcessItem:"Y\xFCkle",labelMaxFileSizeExceeded:"Dosya \xE7ok b\xFCy\xFCk",labelMaxFileSize:"En fazla dosya boyutu: {filesize}",labelMaxTotalFileSizeExceeded:"Maximum boyut a\u015F\u0131ld\u0131",labelMaxTotalFileSize:"Maximum dosya boyutu :{filesize}",labelFileTypeNotAllowed:"Ge\xE7ersiz dosya tipi",fileValidateTypeLabelExpectedTypes:"\u015Eu {allButLastType} ya da \u015Fu dosya olmas\u0131 gerekir: {lastType}",imageValidateSizeLabelFormatError:"Resim tipi desteklenmiyor",imageValidateSizeLabelImageSizeTooSmall:"Resim \xE7ok k\xFC\xE7\xFCk",imageValidateSizeLabelImageSizeTooBig:"Resim \xE7ok b\xFCy\xFCk",imageValidateSizeLabelExpectedMinSize:"Minimum boyut {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"Maximum boyut {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"\xC7\xF6z\xFCn\xFCrl\xFCk \xE7ok d\xFC\u015F\xFCk",imageValidateSizeLabelImageResolutionTooHigh:"\xC7\xF6z\xFCn\xFCrl\xFCk \xE7ok y\xFCksek",imageValidateSizeLabelExpectedMinResolution:"Minimum \xE7\xF6z\xFCn\xFCrl\xFCk {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"Maximum \xE7\xF6z\xFCn\xFCrl\xFCk {maxResolution}"};var sr={labelIdle:'\u041F\u0435\u0440\u0435\u0442\u044F\u0433\u043D\u0456\u0442\u044C \u0444\u0430\u0439\u043B\u0438 \u0430\u0431\u043E \u0432\u0438\u0431\u0435\u0440\u0456\u0442\u044C ',labelInvalidField:"\u041F\u043E\u043B\u0435 \u043C\u0456\u0441\u0442\u0438\u0442\u044C \u043D\u0435\u0434\u043E\u043F\u0443\u0441\u0442\u0438\u043C\u0456 \u0444\u0430\u0439\u043B\u0438",labelFileWaitingForSize:"\u0412\u043A\u0430\u0436\u0456\u0442\u044C \u0440\u043E\u0437\u043C\u0456\u0440",labelFileSizeNotAvailable:"\u0420\u043E\u0437\u043C\u0456\u0440 \u043D\u0435 \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u0438\u0439",labelFileLoading:"\u041E\u0447\u0456\u043A\u0443\u0432\u0430\u043D\u043D\u044F",labelFileLoadError:"\u041F\u043E\u043C\u0438\u043B\u043A\u0430 \u043F\u0440\u0438 \u043E\u0447\u0456\u043A\u0443\u0432\u0430\u043D\u043D\u0456",labelFileProcessing:"\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F",labelFileProcessingComplete:"\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F \u0437\u0430\u0432\u0435\u0440\u0448\u0435\u043D\u043E",labelFileProcessingAborted:"\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F \u0441\u043A\u0430\u0441\u043E\u0432\u0430\u043D\u043E",labelFileProcessingError:"\u041F\u043E\u043C\u0438\u043B\u043A\u0430 \u043F\u0440\u0438 \u0437\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u0456",labelFileProcessingRevertError:"\u041F\u043E\u043C\u0438\u043B\u043A\u0430 \u043F\u0440\u0438 \u0432\u0456\u0434\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u0456",labelFileRemoveError:"\u041F\u043E\u043C\u0438\u043B\u043A\u0430 \u043F\u0440\u0438 \u0432\u0438\u0434\u0430\u043B\u0435\u043D\u043D\u0456",labelTapToCancel:"\u0412\u0456\u0434\u043C\u0456\u043D\u0438\u0442\u0438",labelTapToRetry:"\u041D\u0430\u0442\u0438\u0441\u043D\u0456\u0442\u044C, \u0449\u043E\u0431 \u043F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u0438 \u0441\u043F\u0440\u043E\u0431\u0443",labelTapToUndo:"\u041D\u0430\u0442\u0438\u0441\u043D\u0456\u0442\u044C, \u0449\u043E\u0431 \u0432\u0456\u0434\u043C\u0456\u043D\u0438\u0442\u0438 \u043E\u0441\u0442\u0430\u043D\u043D\u044E \u0434\u0456\u044E",labelButtonRemoveItem:"\u0412\u0438\u0434\u0430\u043B\u0438\u0442\u0438",labelButtonAbortItemLoad:"\u0412\u0456\u0434\u043C\u0456\u043D\u0438\u0442\u0438",labelButtonRetryItemLoad:"\u041F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u0438 \u0441\u043F\u0440\u043E\u0431\u0443",labelButtonAbortItemProcessing:"\u0412\u0456\u0434\u043C\u0456\u043D\u0438\u0442\u0438",labelButtonUndoItemProcessing:"\u0412\u0456\u0434\u043C\u0456\u043D\u0438\u0442\u0438 \u043E\u0441\u0442\u0430\u043D\u043D\u044E \u0434\u0456\u044E",labelButtonRetryItemProcessing:"\u041F\u043E\u0432\u0442\u043E\u0440\u0438\u0442\u0438 \u0441\u043F\u0440\u043E\u0431\u0443",labelButtonProcessItem:"\u0417\u0430\u0432\u0430\u043D\u0442\u0430\u0436\u0435\u043D\u043D\u044F",labelMaxFileSizeExceeded:"\u0424\u0430\u0439\u043B \u0437\u0430\u043D\u0430\u0434\u0442\u043E \u0432\u0435\u043B\u0438\u043A\u0438\u0439",labelMaxFileSize:"\u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u0438\u0439 \u0440\u043E\u0437\u043C\u0456\u0440 \u0444\u0430\u0439\u043B\u0443: {filesize}",labelMaxTotalFileSizeExceeded:"\u041F\u0435\u0440\u0435\u0432\u0438\u0449\u0435\u043D\u043E \u043C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u0438\u0439 \u0437\u0430\u0433\u0430\u043B\u044C\u043D\u0438\u0439 \u0440\u043E\u0437\u043C\u0456\u0440",labelMaxTotalFileSize:"\u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u0438\u0439 \u0437\u0430\u0433\u0430\u043B\u044C\u043D\u0438\u0439 \u0440\u043E\u0437\u043C\u0456\u0440: {filesize}",labelFileTypeNotAllowed:"\u0424\u043E\u0440\u043C\u0430\u0442 \u0444\u0430\u0439\u043B\u0443 \u043D\u0435 \u043F\u0456\u0434\u0442\u0440\u0438\u043C\u0443\u0454\u0442\u044C\u0441\u044F",fileValidateTypeLabelExpectedTypes:"\u041E\u0447\u0456\u043A\u0443\u0454\u0442\u044C\u0441\u044F {allButLastType} \u0430\u0431\u043E {lastType}",imageValidateSizeLabelFormatError:"\u0424\u043E\u0440\u043C\u0430\u0442 \u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u043D\u044F \u043D\u0435 \u043F\u0456\u0434\u0442\u0440\u0438\u043C\u0443\u0454\u0442\u044C\u0441\u044F",imageValidateSizeLabelImageSizeTooSmall:"\u0417\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u043D\u044F \u0437\u0430\u043D\u0430\u0434\u0442\u043E \u043C\u0430\u043B\u0435\u043D\u044C\u043A\u0435",imageValidateSizeLabelImageSizeTooBig:"\u0417\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u043D\u044F \u0437\u0430\u043D\u0430\u0434\u0442\u043E \u0432\u0435\u043B\u0438\u043A\u0435",imageValidateSizeLabelExpectedMinSize:"\u041C\u0456\u043D\u0456\u043C\u0430\u043B\u044C\u043D\u0438\u0439 \u0440\u043E\u0437\u043C\u0456\u0440: {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"\u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u0438\u0439 \u0440\u043E\u0437\u043C\u0456\u0440: {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"\u0420\u043E\u0437\u043C\u0456\u0440\u0438 \u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u043D\u044F \u0437\u0430\u043D\u0430\u0434\u0442\u043E \u043C\u0430\u043B\u0435\u043D\u044C\u043A\u0456",imageValidateSizeLabelImageResolutionTooHigh:"\u0420\u043E\u0437\u043C\u0456\u0440\u0438 \u0437\u043E\u0431\u0440\u0430\u0436\u0435\u043D\u043D\u044F \u0437\u0430\u043D\u0430\u0434\u0442\u043E \u0432\u0435\u043B\u0438\u043A\u0456",imageValidateSizeLabelExpectedMinResolution:"\u041C\u0456\u043D\u0456\u043C\u0430\u043B\u044C\u043D\u0456 \u0440\u043E\u0437\u043C\u0456\u0440\u0438: {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"\u041C\u0430\u043A\u0441\u0438\u043C\u0430\u043B\u044C\u043D\u0456 \u0440\u043E\u0437\u043C\u0456\u0440\u0438: {maxResolution}"};var cr={labelIdle:'K\xE9o th\u1EA3 t\u1EC7p c\u1EE7a b\u1EA1n ho\u1EB7c T\xECm ki\u1EBFm ',labelInvalidField:"Tr\u01B0\u1EDDng ch\u1EE9a c\xE1c t\u1EC7p kh\xF4ng h\u1EE3p l\u1EC7",labelFileWaitingForSize:"\u0110ang ch\u1EDD k\xEDch th\u01B0\u1EDBc",labelFileSizeNotAvailable:"K\xEDch th\u01B0\u1EDBc kh\xF4ng c\xF3 s\u1EB5n",labelFileLoading:"\u0110ang t\u1EA3i",labelFileLoadError:"L\u1ED7i khi t\u1EA3i",labelFileProcessing:"\u0110ang t\u1EA3i l\xEAn",labelFileProcessingComplete:"T\u1EA3i l\xEAn th\xE0nh c\xF4ng",labelFileProcessingAborted:"\u0110\xE3 hu\u1EF7 t\u1EA3i l\xEAn",labelFileProcessingError:"L\u1ED7i khi t\u1EA3i l\xEAn",labelFileProcessingRevertError:"L\u1ED7i khi ho\xE0n nguy\xEAn",labelFileRemoveError:"L\u1ED7i khi x\xF3a",labelTapToCancel:"nh\u1EA5n \u0111\u1EC3 h\u1EE7y",labelTapToRetry:"nh\u1EA5n \u0111\u1EC3 th\u1EED l\u1EA1i",labelTapToUndo:"nh\u1EA5n \u0111\u1EC3 ho\xE0n t\xE1c",labelButtonRemoveItem:"Xo\xE1",labelButtonAbortItemLoad:"Hu\u1EF7 b\u1ECF",labelButtonRetryItemLoad:"Th\u1EED l\u1EA1i",labelButtonAbortItemProcessing:"H\u1EE7y b\u1ECF",labelButtonUndoItemProcessing:"Ho\xE0n t\xE1c",labelButtonRetryItemProcessing:"Th\u1EED l\u1EA1i",labelButtonProcessItem:"T\u1EA3i l\xEAn",labelMaxFileSizeExceeded:"T\u1EADp tin qu\xE1 l\u1EDBn",labelMaxFileSize:"K\xEDch th\u01B0\u1EDBc t\u1EC7p t\u1ED1i \u0111a l\xE0 {filesize}",labelMaxTotalFileSizeExceeded:"\u0110\xE3 v\u01B0\u1EE3t qu\xE1 t\u1ED5ng k\xEDch th\u01B0\u1EDBc t\u1ED1i \u0111a",labelMaxTotalFileSize:"T\u1ED5ng k\xEDch th\u01B0\u1EDBc t\u1EC7p t\u1ED1i \u0111a l\xE0 {filesize}",labelFileTypeNotAllowed:"T\u1EC7p thu\u1ED9c lo\u1EA1i kh\xF4ng h\u1EE3p l\u1EC7",fileValidateTypeLabelExpectedTypes:"Ki\u1EC3u t\u1EC7p h\u1EE3p l\u1EC7 l\xE0 {allButLastType} ho\u1EB7c {lastType}",imageValidateSizeLabelFormatError:"Lo\u1EA1i h\xECnh \u1EA3nh kh\xF4ng \u0111\u01B0\u1EE3c h\u1ED7 tr\u1EE3",imageValidateSizeLabelImageSizeTooSmall:"H\xECnh \u1EA3nh qu\xE1 nh\u1ECF",imageValidateSizeLabelImageSizeTooBig:"H\xECnh \u1EA3nh qu\xE1 l\u1EDBn",imageValidateSizeLabelExpectedMinSize:"K\xEDch th\u01B0\u1EDBc t\u1ED1i thi\u1EC3u l\xE0 {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"K\xEDch th\u01B0\u1EDBc t\u1ED1i \u0111a l\xE0 {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"\u0110\u1ED9 ph\xE2n gi\u1EA3i qu\xE1 th\u1EA5p",imageValidateSizeLabelImageResolutionTooHigh:"\u0110\u1ED9 ph\xE2n gi\u1EA3i qu\xE1 cao",imageValidateSizeLabelExpectedMinResolution:"\u0110\u1ED9 ph\xE2n gi\u1EA3i t\u1ED1i thi\u1EC3u l\xE0 {minResolution}",imageValidateSizeLabelExpectedMaxResolution:"\u0110\u1ED9 ph\xE2n gi\u1EA3i t\u1ED1i \u0111a l\xE0 {maxResolution}"};var dr={labelIdle:'\u62D6\u653E\u6587\u4EF6\uFF0C\u6216\u8005 \u6D4F\u89C8 ',labelInvalidField:"\u5B57\u6BB5\u5305\u542B\u65E0\u6548\u6587\u4EF6",labelFileWaitingForSize:"\u8BA1\u7B97\u6587\u4EF6\u5927\u5C0F",labelFileSizeNotAvailable:"\u6587\u4EF6\u5927\u5C0F\u4E0D\u53EF\u7528",labelFileLoading:"\u52A0\u8F7D",labelFileLoadError:"\u52A0\u8F7D\u9519\u8BEF",labelFileProcessing:"\u4E0A\u4F20",labelFileProcessingComplete:"\u5DF2\u4E0A\u4F20",labelFileProcessingAborted:"\u4E0A\u4F20\u5DF2\u53D6\u6D88",labelFileProcessingError:"\u4E0A\u4F20\u51FA\u9519",labelFileProcessingRevertError:"\u8FD8\u539F\u51FA\u9519",labelFileRemoveError:"\u5220\u9664\u51FA\u9519",labelTapToCancel:"\u70B9\u51FB\u53D6\u6D88",labelTapToRetry:"\u70B9\u51FB\u91CD\u8BD5",labelTapToUndo:"\u70B9\u51FB\u64A4\u6D88",labelButtonRemoveItem:"\u5220\u9664",labelButtonAbortItemLoad:"\u4E2D\u6B62",labelButtonRetryItemLoad:"\u91CD\u8BD5",labelButtonAbortItemProcessing:"\u53D6\u6D88",labelButtonUndoItemProcessing:"\u64A4\u6D88",labelButtonRetryItemProcessing:"\u91CD\u8BD5",labelButtonProcessItem:"\u4E0A\u4F20",labelMaxFileSizeExceeded:"\u6587\u4EF6\u592A\u5927",labelMaxFileSize:"\u6700\u5927\u503C: {filesize}",labelMaxTotalFileSizeExceeded:"\u8D85\u8FC7\u6700\u5927\u6587\u4EF6\u5927\u5C0F",labelMaxTotalFileSize:"\u6700\u5927\u6587\u4EF6\u5927\u5C0F\uFF1A{filesize}",labelFileTypeNotAllowed:"\u6587\u4EF6\u7C7B\u578B\u65E0\u6548",fileValidateTypeLabelExpectedTypes:"\u5E94\u4E3A {allButLastType} \u6216 {lastType}",imageValidateSizeLabelFormatError:"\u4E0D\u652F\u6301\u56FE\u50CF\u7C7B\u578B",imageValidateSizeLabelImageSizeTooSmall:"\u56FE\u50CF\u592A\u5C0F",imageValidateSizeLabelImageSizeTooBig:"\u56FE\u50CF\u592A\u5927",imageValidateSizeLabelExpectedMinSize:"\u6700\u5C0F\u503C: {minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"\u6700\u5927\u503C: {maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"\u5206\u8FA8\u7387\u592A\u4F4E",imageValidateSizeLabelImageResolutionTooHigh:"\u5206\u8FA8\u7387\u592A\u9AD8",imageValidateSizeLabelExpectedMinResolution:"\u6700\u5C0F\u5206\u8FA8\u7387\uFF1A{minResolution}",imageValidateSizeLabelExpectedMaxResolution:"\u6700\u5927\u5206\u8FA8\u7387\uFF1A{maxResolution}"};var pr={labelIdle:'\u62D6\u653E\u6A94\u6848\uFF0C\u6216\u8005 \u700F\u89BD ',labelInvalidField:"\u4E0D\u652F\u63F4\u6B64\u6A94\u6848",labelFileWaitingForSize:"\u6B63\u5728\u8A08\u7B97\u6A94\u6848\u5927\u5C0F",labelFileSizeNotAvailable:"\u6A94\u6848\u5927\u5C0F\u4E0D\u7B26",labelFileLoading:"\u8B80\u53D6\u4E2D",labelFileLoadError:"\u8B80\u53D6\u932F\u8AA4",labelFileProcessing:"\u4E0A\u50B3",labelFileProcessingComplete:"\u5DF2\u4E0A\u50B3",labelFileProcessingAborted:"\u4E0A\u50B3\u5DF2\u53D6\u6D88",labelFileProcessingError:"\u4E0A\u50B3\u767C\u751F\u932F\u8AA4",labelFileProcessingRevertError:"\u9084\u539F\u932F\u8AA4",labelFileRemoveError:"\u522A\u9664\u932F\u8AA4",labelTapToCancel:"\u9EDE\u64CA\u53D6\u6D88",labelTapToRetry:"\u9EDE\u64CA\u91CD\u8A66",labelTapToUndo:"\u9EDE\u64CA\u9084\u539F",labelButtonRemoveItem:"\u522A\u9664",labelButtonAbortItemLoad:"\u505C\u6B62",labelButtonRetryItemLoad:"\u91CD\u8A66",labelButtonAbortItemProcessing:"\u53D6\u6D88",labelButtonUndoItemProcessing:"\u53D6\u6D88",labelButtonRetryItemProcessing:"\u91CD\u8A66",labelButtonProcessItem:"\u4E0A\u50B3",labelMaxFileSizeExceeded:"\u6A94\u6848\u904E\u5927",labelMaxFileSize:"\u6700\u5927\u503C\uFF1A{filesize}",labelMaxTotalFileSizeExceeded:"\u8D85\u904E\u6700\u5927\u53EF\u4E0A\u50B3\u5927\u5C0F",labelMaxTotalFileSize:"\u6700\u5927\u53EF\u4E0A\u50B3\u5927\u5C0F\uFF1A{filesize}",labelFileTypeNotAllowed:"\u4E0D\u652F\u63F4\u6B64\u985E\u578B\u6A94\u6848",fileValidateTypeLabelExpectedTypes:"\u61C9\u70BA {allButLastType} \u6216 {lastType}",imageValidateSizeLabelFormatError:"\u4E0D\u652F\u6301\u6B64\u985E\u5716\u7247\u985E\u578B",imageValidateSizeLabelImageSizeTooSmall:"\u5716\u7247\u904E\u5C0F",imageValidateSizeLabelImageSizeTooBig:"\u5716\u7247\u904E\u5927",imageValidateSizeLabelExpectedMinSize:"\u6700\u5C0F\u5C3A\u5BF8\uFF1A{minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"\u6700\u5927\u5C3A\u5BF8\uFF1A{maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"\u89E3\u6790\u5EA6\u904E\u4F4E",imageValidateSizeLabelImageResolutionTooHigh:"\u89E3\u6790\u5EA6\u904E\u9AD8",imageValidateSizeLabelExpectedMinResolution:"\u6700\u4F4E\u89E3\u6790\u5EA6\uFF1A{minResolution}",imageValidateSizeLabelExpectedMaxResolution:"\u6700\u9AD8\u89E3\u6790\u5EA6\uFF1A{maxResolution}"};var mr={labelIdle:'\u62D6\u653E\u6A94\u6848\uFF0C\u6216\u8005 \u700F\u89BD ',labelInvalidField:"\u4E0D\u652F\u63F4\u6B64\u6A94\u6848",labelFileWaitingForSize:"\u6B63\u5728\u8A08\u7B97\u6A94\u6848\u5927\u5C0F",labelFileSizeNotAvailable:"\u6A94\u6848\u5927\u5C0F\u4E0D\u7B26",labelFileLoading:"\u8B80\u53D6\u4E2D",labelFileLoadError:"\u8B80\u53D6\u932F\u8AA4",labelFileProcessing:"\u4E0A\u50B3",labelFileProcessingComplete:"\u5DF2\u4E0A\u50B3",labelFileProcessingAborted:"\u4E0A\u50B3\u5DF2\u53D6\u6D88",labelFileProcessingError:"\u4E0A\u50B3\u767C\u751F\u932F\u8AA4",labelFileProcessingRevertError:"\u9084\u539F\u932F\u8AA4",labelFileRemoveError:"\u522A\u9664\u932F\u8AA4",labelTapToCancel:"\u9EDE\u64CA\u53D6\u6D88",labelTapToRetry:"\u9EDE\u64CA\u91CD\u8A66",labelTapToUndo:"\u9EDE\u64CA\u9084\u539F",labelButtonRemoveItem:"\u522A\u9664",labelButtonAbortItemLoad:"\u505C\u6B62",labelButtonRetryItemLoad:"\u91CD\u8A66",labelButtonAbortItemProcessing:"\u53D6\u6D88",labelButtonUndoItemProcessing:"\u53D6\u6D88",labelButtonRetryItemProcessing:"\u91CD\u8A66",labelButtonProcessItem:"\u4E0A\u50B3",labelMaxFileSizeExceeded:"\u6A94\u6848\u904E\u5927",labelMaxFileSize:"\u6700\u5927\u503C\uFF1A{filesize}",labelMaxTotalFileSizeExceeded:"\u8D85\u904E\u6700\u5927\u53EF\u4E0A\u50B3\u5927\u5C0F",labelMaxTotalFileSize:"\u6700\u5927\u53EF\u4E0A\u50B3\u5927\u5C0F\uFF1A{filesize}",labelFileTypeNotAllowed:"\u4E0D\u652F\u63F4\u6B64\u985E\u578B\u6A94\u6848",fileValidateTypeLabelExpectedTypes:"\u61C9\u70BA {allButLastType} \u6216 {lastType}",imageValidateSizeLabelFormatError:"\u4E0D\u652F\u6301\u6B64\u985E\u5716\u7247\u985E\u578B",imageValidateSizeLabelImageSizeTooSmall:"\u5716\u7247\u904E\u5C0F",imageValidateSizeLabelImageSizeTooBig:"\u5716\u7247\u904E\u5927",imageValidateSizeLabelExpectedMinSize:"\u6700\u5C0F\u5C3A\u5BF8\uFF1A{minWidth} \xD7 {minHeight}",imageValidateSizeLabelExpectedMaxSize:"\u6700\u5927\u5C3A\u5BF8\uFF1A{maxWidth} \xD7 {maxHeight}",imageValidateSizeLabelImageResolutionTooLow:"\u89E3\u6790\u5EA6\u904E\u4F4E",imageValidateSizeLabelImageResolutionTooHigh:"\u89E3\u6790\u5EA6\u904E\u9AD8",imageValidateSizeLabelExpectedMinResolution:"\u6700\u4F4E\u89E3\u6790\u5EA6\uFF1A{minResolution}",imageValidateSizeLabelExpectedMaxResolution:"\u6700\u9AD8\u89E3\u6790\u5EA6\uFF1A{maxResolution}"};ve(Hl);ve(jl);ve($l);ve(Kl);ve(eo);ve(mo);ve(go);ve(_o);ve(Aa);window.FilePond=na;function Og({acceptedFileTypes:e,automaticallyCropImagesAspectRatio:t,automaticallyOpenImageEditorForAspectRatio:i,automaticallyResizeImagesHeight:a,automaticallyResizeImagesMode:n,automaticallyResizeImagesWidth:l,cancelUploadUsing:o,canEditSvgs:r,confirmSvgEditingMessage:s,deleteUploadedFileUsing:p,disabledSvgEditingMessage:c,getUploadedFilesUsing:d,hasCircleCropper:m,hasImageEditor:u,imageEditorEmptyFillColor:g,imageEditorMode:f,imageEditorViewportHeight:h,imageEditorViewportWidth:I,imagePreviewHeight:b,isAvatar:E,isDeletable:v,isDisabled:y,isDownloadable:T,isImageEditorExplicitlyEnabled:_,isMultiple:x,isOpenable:R,isPasteable:P,isPreviewable:z,isReorderable:A,isSvgEditingConfirmed:B,itemPanelAspectRatio:w,loadingIndicatorPosition:F,locale:S,maxFiles:L,maxFilesValidationMessage:D,maxParallelUploads:O,maxSize:U,mimeTypeMap:C,minSize:X,panelAspectRatio:K,panelLayout:Z,placeholder:ce,removeUploadedFileButtonPosition:V,removeUploadedFileUsing:W,reorderUploadedFilesUsing:$,shouldAppendFiles:ie,shouldAutomaticallyUpscaleImagesWhenResizing:ee,shouldOrientImageFromExif:pt,shouldTransformImage:gr,state:fr,uploadButtonPosition:hr,uploadingMessage:br,uploadProgressIndicatorPosition:Er,uploadUsing:Tr}){return{fileKeyIndex:{},pond:null,shouldUpdateState:!0,state:fr,lastState:null,error:null,uploadedFileIndex:{},isEditorOpen:!1,isEditorOpenedForAspectRatio:!1,editingFile:{},currentRatio:"",editor:{},visibilityObserver:null,intersectionObserver:null,isInitializing:!1,async init(){if(this.pond||this.isInitializing)return;if(this.isInitializing=!0,!this.visibilityObserver){let k=()=>{this.$el.offsetParent===null||getComputedStyle(this.$el).visibility==="hidden"||(this.pond?document.dispatchEvent(new Event("visibilitychange")):this.init())};this.visibilityObserver=new ResizeObserver(()=>k()),this.visibilityObserver.observe(this.$el),this.intersectionObserver=new IntersectionObserver(j=>{j[0]?.isIntersecting&&k()},{threshold:0}),this.intersectionObserver.observe(this.$el)}if(this.$el.offsetParent===null||getComputedStyle(this.$el).visibility==="hidden"){this.isInitializing=!1;return}Dt(ur[S]??ur.en),this.pond=ft(this.$refs.input,{acceptedFileTypes:e,allowImageExifOrientation:pt,allowPaste:P,allowRemove:v,allowReorder:A,allowImagePreview:z,allowVideoPreview:z,allowAudioPreview:z,allowImageTransform:gr,credits:!1,files:await this.getFiles(),imageCropAspectRatio:t,imagePreviewHeight:b,imageResizeTargetHeight:a,imageResizeTargetWidth:l,imageResizeMode:n,imageResizeUpscale:ee,imageTransformOutputStripImageHead:!1,itemInsertLocation:ie?"after":"before",...ce&&{labelIdle:ce},maxFiles:L,maxFileSize:U,minFileSize:X,...O&&{maxParallelUploads:O},styleButtonProcessItemPosition:hr,styleButtonRemoveItemPosition:V,styleItemPanelAspectRatio:w,styleLoadIndicatorPosition:F,stylePanelAspectRatio:K,stylePanelLayout:Z,styleProgressIndicatorPosition:Er,server:{load:async(k,j)=>{let Ne=await(await fetch(k,{cache:"no-store"})).blob();j(Ne)},process:(k,j,q,Ne,Me,$e,Ir)=>{this.shouldUpdateState=!1;let za=("10000000-1000-4000-8000"+-1e11).replace(/[018]/g,Zt=>(Zt^crypto.getRandomValues(new Uint8Array(1))[0]&15>>Zt/4).toString(16));return Tr(za,j,Zt=>{this.shouldUpdateState=!0,Ne(Zt)},Me,$e),{abort:()=>{o(za),Ir()}}},remove:async(k,j)=>{let q=this.uploadedFileIndex[k]??null;q&&(await p(q),j())},revert:async(k,j)=>{await W(k),j()}},allowImageEdit:_,imageEditEditor:{open:k=>this.loadEditor(k),onconfirm:()=>{},oncancel:()=>this.closeEditor(),onclose:()=>this.closeEditor()},fileValidateTypeDetectType:(k,j)=>new Promise((q,Ne)=>{let Me=k.name.split(".").pop().toLowerCase(),$e=C[Me]||j||Gl.getType(Me);$e?q($e):Ne()})}),this.$watch("state",async()=>{if(this.pond&&this.shouldUpdateState&&this.state!==void 0){if(this.state!==null&&Object.values(this.state).filter(k=>k.startsWith("livewire-file:")).length){this.lastState=null;return}JSON.stringify(this.state)!==this.lastState&&(this.lastState=JSON.stringify(this.state),this.pond.files=await this.getFiles())}}),this.pond.on("reorderfiles",async k=>{let j=k.map(q=>q.source instanceof File?q.serverId:this.uploadedFileIndex[q.source]??null).filter(q=>q);await $(ie?j:j.reverse())}),this.pond.on("initfile",async k=>{T&&(E||this.insertDownloadLink(k))}),this.pond.on("initfile",async k=>{R&&(E||this.insertOpenLink(k))}),this.pond.on("addfilestart",async k=>{this.error=null,k.status===Tt.PROCESSING_QUEUED&&this.dispatchFormEvent("form-processing-started",{message:br})});let N=async()=>{this.pond.getFiles().filter(k=>k.status===Tt.PROCESSING||k.status===Tt.PROCESSING_QUEUED).length||this.dispatchFormEvent("form-processing-finished")};this.pond.on("processfile",N),this.pond.on("processfileabort",N),this.pond.on("processfilerevert",N),this.pond.on("removefile",N),this.pond.on("warning",k=>{k.body==="Max files"&&(this.error=D)}),Z==="compact circle"&&this.pond.on("error",k=>{this.error=`${k.main}: ${k.sub}`.replace("Expects or","Expects")}),this.pond.on("removefile",()=>this.error=null),i&&this.pond.on("addfile",(k,j)=>{k||j.file instanceof File&&j.file.type.startsWith("image/")&&this.checkImageAspectRatio(j.file)}),this.isInitializing=!1},destroy(){this.visibilityObserver?.disconnect(),this.intersectionObserver?.disconnect(),this.destroyEditor(),this.pond&&(ht(this.$refs.input),this.pond=null)},dispatchFormEvent(G,N={}){this.$el.closest("form")?.dispatchEvent(new CustomEvent(G,{composed:!0,cancelable:!0,detail:N}))},async getUploadedFiles(){let G=await d();this.fileKeyIndex=G??{},this.uploadedFileIndex=Object.entries(this.fileKeyIndex).filter(([N,k])=>k?.url).reduce((N,[k,j])=>(N[j.url]=k,N),{})},async getFiles(){await this.getUploadedFiles();let G=[];for(let N of Object.values(this.fileKeyIndex))N&&G.push({source:N.url,options:{type:"local",...!N.type||z&&(/^audio/.test(N.type)||/^image/.test(N.type)||/^video/.test(N.type))?{}:{file:{name:N.name,size:N.size,type:N.type}}}});return ie?G:G.reverse()},insertDownloadLink(G){if(G.origin!==Bt.LOCAL)return;let N=this.getDownloadLink(G);N&&document.getElementById(`filepond--item-${G.id}`).querySelector(".filepond--file-info-main").prepend(N)},insertOpenLink(G){if(G.origin!==Bt.LOCAL)return;let N=this.getOpenLink(G);N&&document.getElementById(`filepond--item-${G.id}`).querySelector(".filepond--file-info-main").prepend(N)},getDownloadLink(G){let N=G.source;if(!N)return;let k=document.createElement("a");return k.className="filepond--download-icon",k.href=N,k.download=G.file.name,k},getOpenLink(G){let N=G.source;if(!N)return;let k=document.createElement("a");return k.className="filepond--open-icon",k.href=N,k.target="_blank",k},initEditor(){if(y||!u)return;let G={aspectRatio:i??I/h,autoCropArea:1,center:!0,cropBoxResizable:!0,guides:!0,highlight:!0,responsive:!0,toggleDragModeOnDblclick:!0,viewMode:f,wheelZoomRatio:.02};_&&(G.crop=N=>{this.$refs.xPositionInput.value=Math.round(N.detail.x),this.$refs.yPositionInput.value=Math.round(N.detail.y),this.$refs.heightInput.value=Math.round(N.detail.height),this.$refs.widthInput.value=Math.round(N.detail.width),this.$refs.rotationInput.value=N.detail.rotate}),this.editor=new xa(this.$refs.editor,G)},closeEditor(){if(this.isEditorOpenedForAspectRatio){let G=this.pond.getFiles().find(N=>N.filename===this.editingFile.name);G&&this.pond.removeFile(G.id,{revert:!0}),this.isEditorOpenedForAspectRatio=!1}this.editingFile={},this.isEditorOpen=!1,this.destroyEditor()},fixImageDimensions(G,N){if(G.type!=="image/svg+xml")return N(G);let k=new FileReader;k.onload=j=>{let q=new DOMParser().parseFromString(j.target.result,"image/svg+xml")?.querySelector("svg");if(!q)return N(G);let Ne=["viewBox","ViewBox","viewbox"].find($e=>q.hasAttribute($e));if(!Ne)return N(G);let Me=q.getAttribute(Ne).split(" ");return!Me||Me.length!==4?N(G):(q.setAttribute("width",parseFloat(Me[2])+"pt"),q.setAttribute("height",parseFloat(Me[3])+"pt"),N(new File([new Blob([new XMLSerializer().serializeToString(q)],{type:"image/svg+xml"})],G.name,{type:"image/svg+xml",_relativePath:""})))},k.readAsText(G)},loadEditor(G){if(y||!u||!G)return;let N=G.type==="image/svg+xml";if(!r&&N){alert(c);return}B&&N&&!confirm(s)||this.fixImageDimensions(G,k=>{this.editingFile=k,this.initEditor();let j=new FileReader;j.onload=q=>{this.isEditorOpen=!0,setTimeout(()=>this.editor.replace(q.target.result),200)},j.readAsDataURL(G)})},getRoundedCanvas(G){let N=G.width,k=G.height,j=document.createElement("canvas");j.width=N,j.height=k;let q=j.getContext("2d");return q.imageSmoothingEnabled=!0,q.drawImage(G,0,0,N,k),q.globalCompositeOperation="destination-in",q.beginPath(),q.ellipse(N/2,k/2,N/2,k/2,0,0,2*Math.PI),q.fill(),j},saveEditor(){if(y||!u)return;this.isEditorOpenedForAspectRatio=!1;let G=this.editor.getCroppedCanvas({fillColor:g??"transparent",height:a,imageSmoothingEnabled:!0,imageSmoothingQuality:"high",width:l});m&&(G=this.getRoundedCanvas(G)),G.toBlob(N=>{this.pond.removeFile(this.pond.getFiles().find(k=>k.filename===this.editingFile.name)?.id,{revert:!0}),this.$nextTick(()=>{this.shouldUpdateState=!1;let k=this.editingFile.name.slice(0,this.editingFile.name.lastIndexOf(".")),j=this.editingFile.name.split(".").pop();j==="svg"&&(j="png");let q=/-v(\d+)/;q.test(k)?k=k.replace(q,(Ne,Me)=>`-v${Number(Me)+1}`):k+="-v1",this.pond.addFile(new File([N],`${k}.${j}`,{type:this.editingFile.type==="image/svg+xml"||m?"image/png":this.editingFile.type,lastModified:new Date().getTime()})).then(()=>{this.closeEditor()}).catch(()=>{this.closeEditor()})})},m?"image/png":this.editingFile.type)},destroyEditor(){this.editor&&typeof this.editor.destroy=="function"&&this.editor.destroy(),this.editor=null},checkImageAspectRatio(G){if(!i)return;let N=new Image,k=URL.createObjectURL(G);N.onload=()=>{URL.revokeObjectURL(k);let j=N.width/N.height;Math.abs(j-i)>.01&&(this.isEditorOpenedForAspectRatio=!0,this.loadEditor(G))},N.onerror=()=>{URL.revokeObjectURL(k)},N.src=k}}}var ur={am:wo,ar:Lo,az:Mo,ca:Ao,ckb:zo,cs:Po,da:Fo,de:Oo,el:Do,en:Co,es:Bo,fa:ko,fi:No,fr:Vo,he:Go,hr:Uo,hu:Ho,id:Wo,it:jo,ja:Yo,km:qo,ko:$o,lt:Xo,lus:Ko,lv:Zo,nb:Qo,nl:Jo,pl:er,pt:tr,pt_BR:ir,ro:ar,ru:nr,sk:lr,sv:or,tr:rr,uk:sr,vi:cr,zh_CN:dr,zh_HK:pr,zh_TW:mr};export{Og as default}; +/*! Bundled license information: + +filepond/dist/filepond.esm.js: + (*! + * FilePond 4.32.10 + * Licensed under MIT, https://opensource.org/licenses/MIT/ + * Please visit https://pqina.nl/filepond/ for details. + *) + +cropperjs/dist/cropper.esm.js: + (*! + * Cropper.js v1.6.2 + * https://fengyuanchen.github.io/cropperjs + * + * Copyright 2015-present Chen Fengyuan + * Released under the MIT license + * + * Date: 2024-04-21T07:43:05.335Z + *) + +filepond-plugin-file-validate-size/dist/filepond-plugin-file-validate-size.esm.js: + (*! + * FilePondPluginFileValidateSize 2.2.8 + * Licensed under MIT, https://opensource.org/licenses/MIT/ + * Please visit https://pqina.nl/filepond/ for details. + *) + +filepond-plugin-file-validate-type/dist/filepond-plugin-file-validate-type.esm.js: + (*! + * FilePondPluginFileValidateType 1.2.9 + * Licensed under MIT, https://opensource.org/licenses/MIT/ + * Please visit https://pqina.nl/filepond/ for details. + *) + +filepond-plugin-image-crop/dist/filepond-plugin-image-crop.esm.js: + (*! + * FilePondPluginImageCrop 2.0.6 + * Licensed under MIT, https://opensource.org/licenses/MIT/ + * Please visit https://pqina.nl/filepond/ for details. + *) + +filepond-plugin-image-edit/dist/filepond-plugin-image-edit.esm.js: + (*! + * FilePondPluginImageEdit 1.6.3 + * Licensed under MIT, https://opensource.org/licenses/MIT/ + * Please visit https://pqina.nl/filepond/ for details. + *) + +filepond-plugin-image-exif-orientation/dist/filepond-plugin-image-exif-orientation.esm.js: + (*! + * FilePondPluginImageExifOrientation 1.0.11 + * Licensed under MIT, https://opensource.org/licenses/MIT/ + * Please visit https://pqina.nl/filepond/ for details. + *) + +filepond-plugin-image-preview/dist/filepond-plugin-image-preview.esm.js: + (*! + * FilePondPluginImagePreview 4.6.12 + * Licensed under MIT, https://opensource.org/licenses/MIT/ + * Please visit https://pqina.nl/filepond/ for details. + *) + +filepond-plugin-image-resize/dist/filepond-plugin-image-resize.esm.js: + (*! + * FilePondPluginImageResize 2.0.10 + * Licensed under MIT, https://opensource.org/licenses/MIT/ + * Please visit https://pqina.nl/filepond/ for details. + *) + +filepond-plugin-image-transform/dist/filepond-plugin-image-transform.esm.js: + (*! + * FilePondPluginImageTransform 3.8.7 + * Licensed under MIT, https://opensource.org/licenses/MIT/ + * Please visit https://pqina.nl/filepond/ for details. + *) + +filepond-plugin-media-preview/dist/filepond-plugin-media-preview.esm.js: + (*! + * FilePondPluginMediaPreview 1.0.11 + * Licensed under MIT, https://opensource.org/licenses/MIT/ + * Please visit undefined for details. + *) +*/ diff --git a/public/js/filament/forms/components/key-value.js b/public/js/filament/forms/components/key-value.js new file mode 100644 index 00000000..293864be --- /dev/null +++ b/public/js/filament/forms/components/key-value.js @@ -0,0 +1 @@ +function a({state:r}){return{state:r,rows:[],init(){this.updateRows(),this.rows.length<=0?this.rows.push({key:"",value:""}):this.updateState(),this.$watch("state",(e,t)=>{if(!Array.isArray(e))return;let s=i=>i===null?0:Array.isArray(i)?i.length:typeof i!="object"?0:Object.keys(i).length;s(e)===0&&s(t)===0||this.updateRows()})},addRow(){this.rows.push({key:"",value:""}),this.updateState()},deleteRow(e){this.rows.splice(e,1),this.rows.length<=0&&this.addRow(),this.updateState()},reorderRows(e){let t=Alpine.raw(this.rows);this.rows=[];let s=t.splice(e.oldIndex,1)[0];t.splice(e.newIndex,0,s),this.$nextTick(()=>{this.rows=t,this.updateState()})},updateRows(){let t=Alpine.raw(this.state).map(({key:s,value:i})=>({key:s,value:i}));this.rows.forEach(s=>{(s.key===""||s.key===null)&&t.push({key:"",value:s.value})}),this.rows=t},updateState(){let e=[];this.rows.forEach(t=>{t.key===""||t.key===null||e.push({key:t.key,value:t.value})}),JSON.stringify(this.state)!==JSON.stringify(e)&&(this.state=e)}}}export{a as default}; diff --git a/public/js/filament/forms/components/markdown-editor.js b/public/js/filament/forms/components/markdown-editor.js new file mode 100644 index 00000000..0ae0a759 --- /dev/null +++ b/public/js/filament/forms/components/markdown-editor.js @@ -0,0 +1,51 @@ +var ss=Object.defineProperty;var Sd=Object.getOwnPropertyDescriptor;var Td=Object.getOwnPropertyNames;var Ld=Object.prototype.hasOwnProperty;var Cd=(o,p)=>()=>(o&&(p=o(o=0)),p);var Ke=(o,p)=>()=>(p||o((p={exports:{}}).exports,p),p.exports);var Ed=(o,p,v,C)=>{if(p&&typeof p=="object"||typeof p=="function")for(let b of Td(p))!Ld.call(o,b)&&b!==v&&ss(o,b,{get:()=>p[b],enumerable:!(C=Sd(p,b))||C.enumerable});return o};var zd=o=>Ed(ss({},"__esModule",{value:!0}),o);var We=Ke((Yo,Qo)=>{(function(o,p){typeof Yo=="object"&&typeof Qo<"u"?Qo.exports=p():typeof define=="function"&&define.amd?define(p):(o=o||self,o.CodeMirror=p())})(Yo,(function(){"use strict";var o=navigator.userAgent,p=navigator.platform,v=/gecko\/\d/i.test(o),C=/MSIE \d/.test(o),b=/Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(o),S=/Edge\/(\d+)/.exec(o),s=C||b||S,h=s&&(C?document.documentMode||6:+(S||b)[1]),g=!S&&/WebKit\//.test(o),T=g&&/Qt\/\d+\.\d+/.test(o),w=!S&&/Chrome\/(\d+)/.exec(o),c=w&&+w[1],d=/Opera\//.test(o),k=/Apple Computer/.test(navigator.vendor),z=/Mac OS X 1\d\D([8-9]|\d\d)\D/.test(o),M=/PhantomJS/.test(o),_=k&&(/Mobile\/\w+/.test(o)||navigator.maxTouchPoints>2),W=/Android/.test(o),E=_||W||/webOS|BlackBerry|Opera Mini|Opera Mobi|IEMobile/i.test(o),O=_||/Mac/.test(p),G=/\bCrOS\b/.test(o),J=/win/i.test(p),re=d&&o.match(/Version\/(\d*\.\d*)/);re&&(re=Number(re[1])),re&&re>=15&&(d=!1,g=!0);var q=O&&(T||d&&(re==null||re<12.11)),I=v||s&&h>=9;function D(e){return new RegExp("(^|\\s)"+e+"(?:$|\\s)\\s*")}var Q=function(e,t){var n=e.className,r=D(t).exec(n);if(r){var i=n.slice(r.index+r[0].length);e.className=n.slice(0,r.index)+(i?r[1]+i:"")}};function j(e){for(var t=e.childNodes.length;t>0;--t)e.removeChild(e.firstChild);return e}function V(e,t){return j(e).appendChild(t)}function y(e,t,n,r){var i=document.createElement(e);if(n&&(i.className=n),r&&(i.style.cssText=r),typeof t=="string")i.appendChild(document.createTextNode(t));else if(t)for(var a=0;a=t)return l+(t-a);l+=u-a,l+=n-l%n,a=u+1}}var qe=function(){this.id=null,this.f=null,this.time=0,this.handler=Ee(this.onTimeout,this)};qe.prototype.onTimeout=function(e){e.id=0,e.time<=+new Date?e.f():setTimeout(e.handler,e.time-+new Date)},qe.prototype.set=function(e,t){this.f=t;var n=+new Date+e;(!this.id||n=t)return r+Math.min(l,t-i);if(i+=a-r,i+=n-i%n,r=a+1,i>=t)return r}}var U=[""];function Z(e){for(;U.length<=e;)U.push(ce(U)+" ");return U[e]}function ce(e){return e[e.length-1]}function He(e,t){for(var n=[],r=0;r"\x80"&&(e.toUpperCase()!=e.toLowerCase()||Ue.test(e))}function Me(e,t){return t?t.source.indexOf("\\w")>-1&&we(e)?!0:t.test(e):we(e)}function Le(e){for(var t in e)if(e.hasOwnProperty(t)&&e[t])return!1;return!0}var $=/[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/;function H(e){return e.charCodeAt(0)>=768&&$.test(e)}function se(e,t,n){for(;(n<0?t>0:tn?-1:1;;){if(t==n)return t;var i=(t+n)/2,a=r<0?Math.ceil(i):Math.floor(i);if(a==t)return e(a)?t:n;e(a)?n=a:t=a+r}}function nt(e,t,n,r){if(!e)return r(t,n,"ltr",0);for(var i=!1,a=0;at||t==n&&l.to==t)&&(r(Math.max(l.from,t),Math.min(l.to,n),l.level==1?"rtl":"ltr",a),i=!0)}i||r(t,n,"ltr")}var dt=null;function Pt(e,t,n){var r;dt=null;for(var i=0;it)return i;a.to==t&&(a.from!=a.to&&n=="before"?r=i:dt=i),a.from==t&&(a.from!=a.to&&n!="before"?r=i:dt=i)}return r??dt}var Ft=(function(){var e="bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN",t="nnnnnnNNr%%r,rNNmmmmmmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmmmnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmnNmmmmmmrrmmNmmmmrr1111111111";function n(m){return m<=247?e.charAt(m):1424<=m&&m<=1524?"R":1536<=m&&m<=1785?t.charAt(m-1536):1774<=m&&m<=2220?"r":8192<=m&&m<=8203?"w":m==8204?"b":"L"}var r=/[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/,i=/[stwN]/,a=/[LRr]/,l=/[Lb1n]/,u=/[1n]/;function f(m,A,B){this.level=m,this.from=A,this.to=B}return function(m,A){var B=A=="ltr"?"L":"R";if(m.length==0||A=="ltr"&&!r.test(m))return!1;for(var ee=m.length,Y=[],ie=0;ie-1&&(r[t]=i.slice(0,a).concat(i.slice(a+1)))}}}function it(e,t){var n=nr(e,t);if(n.length)for(var r=Array.prototype.slice.call(arguments,2),i=0;i0}function Wt(e){e.prototype.on=function(t,n){Ie(this,t,n)},e.prototype.off=function(t,n){_t(this,t,n)}}function kt(e){e.preventDefault?e.preventDefault():e.returnValue=!1}function Rr(e){e.stopPropagation?e.stopPropagation():e.cancelBubble=!0}function Ct(e){return e.defaultPrevented!=null?e.defaultPrevented:e.returnValue==!1}function dr(e){kt(e),Rr(e)}function yn(e){return e.target||e.srcElement}function Ut(e){var t=e.which;return t==null&&(e.button&1?t=1:e.button&2?t=3:e.button&4&&(t=2)),O&&e.ctrlKey&&t==1&&(t=3),t}var eo=(function(){if(s&&h<9)return!1;var e=y("div");return"draggable"in e||"dragDrop"in e})(),Hr;function ei(e){if(Hr==null){var t=y("span","\u200B");V(e,y("span",[t,document.createTextNode("x")])),e.firstChild.offsetHeight!=0&&(Hr=t.offsetWidth<=1&&t.offsetHeight>2&&!(s&&h<8))}var n=Hr?y("span","\u200B"):y("span","\xA0",null,"display: inline-block; width: 1px; margin-right: -1px");return n.setAttribute("cm-text",""),n}var xn;function pr(e){if(xn!=null)return xn;var t=V(e,document.createTextNode("A\u062EA")),n=X(t,0,1).getBoundingClientRect(),r=X(t,1,2).getBoundingClientRect();return j(e),!n||n.left==n.right?!1:xn=r.right-n.right<3}var Ht=` + +b`.split(/\n/).length!=3?function(e){for(var t=0,n=[],r=e.length;t<=r;){var i=e.indexOf(` +`,t);i==-1&&(i=e.length);var a=e.slice(t,e.charAt(i-1)=="\r"?i-1:i),l=a.indexOf("\r");l!=-1?(n.push(a.slice(0,l)),t+=l+1):(n.push(a),t=i+1)}return n}:function(e){return e.split(/\r\n?|\n/)},hr=window.getSelection?function(e){try{return e.selectionStart!=e.selectionEnd}catch{return!1}}:function(e){var t;try{t=e.ownerDocument.selection.createRange()}catch{}return!t||t.parentElement()!=e?!1:t.compareEndPoints("StartToEnd",t)!=0},ti=(function(){var e=y("div");return"oncopy"in e?!0:(e.setAttribute("oncopy","return;"),typeof e.oncopy=="function")})(),$t=null;function to(e){if($t!=null)return $t;var t=V(e,y("span","x")),n=t.getBoundingClientRect(),r=X(t,0,1).getBoundingClientRect();return $t=Math.abs(n.left-r.left)>1}var Wr={},Kt={};function Gt(e,t){arguments.length>2&&(t.dependencies=Array.prototype.slice.call(arguments,2)),Wr[e]=t}function Cr(e,t){Kt[e]=t}function Ur(e){if(typeof e=="string"&&Kt.hasOwnProperty(e))e=Kt[e];else if(e&&typeof e.name=="string"&&Kt.hasOwnProperty(e.name)){var t=Kt[e.name];typeof t=="string"&&(t={name:t}),e=oe(t,e),e.name=t.name}else{if(typeof e=="string"&&/^[\w\-]+\/[\w\-]+\+xml$/.test(e))return Ur("application/xml");if(typeof e=="string"&&/^[\w\-]+\/[\w\-]+\+json$/.test(e))return Ur("application/json")}return typeof e=="string"?{name:e}:e||{name:"null"}}function $r(e,t){t=Ur(t);var n=Wr[t.name];if(!n)return $r(e,"text/plain");var r=n(e,t);if(gr.hasOwnProperty(t.name)){var i=gr[t.name];for(var a in i)i.hasOwnProperty(a)&&(r.hasOwnProperty(a)&&(r["_"+a]=r[a]),r[a]=i[a])}if(r.name=t.name,t.helperType&&(r.helperType=t.helperType),t.modeProps)for(var l in t.modeProps)r[l]=t.modeProps[l];return r}var gr={};function Kr(e,t){var n=gr.hasOwnProperty(e)?gr[e]:gr[e]={};ge(t,n)}function Vt(e,t){if(t===!0)return t;if(e.copyState)return e.copyState(t);var n={};for(var r in t){var i=t[r];i instanceof Array&&(i=i.concat([])),n[r]=i}return n}function _n(e,t){for(var n;e.innerMode&&(n=e.innerMode(t),!(!n||n.mode==e));)t=n.state,e=n.mode;return n||{mode:e,state:t}}function Gr(e,t,n){return e.startState?e.startState(t,n):!0}var at=function(e,t,n){this.pos=this.start=0,this.string=e,this.tabSize=t||8,this.lastColumnPos=this.lastColumnValue=0,this.lineStart=0,this.lineOracle=n};at.prototype.eol=function(){return this.pos>=this.string.length},at.prototype.sol=function(){return this.pos==this.lineStart},at.prototype.peek=function(){return this.string.charAt(this.pos)||void 0},at.prototype.next=function(){if(this.post},at.prototype.eatSpace=function(){for(var e=this.pos;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>e},at.prototype.skipToEnd=function(){this.pos=this.string.length},at.prototype.skipTo=function(e){var t=this.string.indexOf(e,this.pos);if(t>-1)return this.pos=t,!0},at.prototype.backUp=function(e){this.pos-=e},at.prototype.column=function(){return this.lastColumnPos0?null:(a&&t!==!1&&(this.pos+=a[0].length),a)}},at.prototype.current=function(){return this.string.slice(this.start,this.pos)},at.prototype.hideFirstChars=function(e,t){this.lineStart+=e;try{return t()}finally{this.lineStart-=e}},at.prototype.lookAhead=function(e){var t=this.lineOracle;return t&&t.lookAhead(e)},at.prototype.baseToken=function(){var e=this.lineOracle;return e&&e.baseToken(this.pos)};function Ae(e,t){if(t-=e.first,t<0||t>=e.size)throw new Error("There is no line "+(t+e.first)+" in the document.");for(var n=e;!n.lines;)for(var r=0;;++r){var i=n.children[r],a=i.chunkSize();if(t=e.first&&tn?ne(n,Ae(e,n).text.length):Sc(t,Ae(e,t.line).text.length)}function Sc(e,t){var n=e.ch;return n==null||n>t?ne(e.line,t):n<0?ne(e.line,0):e}function ca(e,t){for(var n=[],r=0;rthis.maxLookAhead&&(this.maxLookAhead=e),t},Jt.prototype.baseToken=function(e){if(!this.baseTokens)return null;for(;this.baseTokens[this.baseTokenPos]<=e;)this.baseTokenPos+=2;var t=this.baseTokens[this.baseTokenPos+1];return{type:t&&t.replace(/( |^)overlay .*/,""),size:this.baseTokens[this.baseTokenPos]-e}},Jt.prototype.nextLine=function(){this.line++,this.maxLookAhead>0&&this.maxLookAhead--},Jt.fromSaved=function(e,t,n){return t instanceof ri?new Jt(e,Vt(e.mode,t.state),n,t.lookAhead):new Jt(e,Vt(e.mode,t),n)},Jt.prototype.save=function(e){var t=e!==!1?Vt(this.doc.mode,this.state):this.state;return this.maxLookAhead>0?new ri(t,this.maxLookAhead):t};function fa(e,t,n,r){var i=[e.state.modeGen],a={};va(e,t.text,e.doc.mode,n,function(m,A){return i.push(m,A)},a,r);for(var l=n.state,u=function(m){n.baseTokens=i;var A=e.state.overlays[m],B=1,ee=0;n.state=!0,va(e,t.text,A.mode,n,function(Y,ie){for(var ue=B;eeY&&i.splice(B,1,Y,i[B+1],me),B+=2,ee=Math.min(Y,me)}if(ie)if(A.opaque)i.splice(ue,B-ue,Y,"overlay "+ie),B=ue+2;else for(;uee.options.maxHighlightLength&&Vt(e.doc.mode,r.state),a=fa(e,t,r);i&&(r.state=i),t.stateAfter=r.save(!i),t.styles=a.styles,a.classes?t.styleClasses=a.classes:t.styleClasses&&(t.styleClasses=null),n===e.doc.highlightFrontier&&(e.doc.modeFrontier=Math.max(e.doc.modeFrontier,++e.doc.highlightFrontier))}return t.styles}function wn(e,t,n){var r=e.doc,i=e.display;if(!r.mode.startState)return new Jt(r,!0,t);var a=Tc(e,t,n),l=a>r.first&&Ae(r,a-1).stateAfter,u=l?Jt.fromSaved(r,l,a):new Jt(r,Gr(r.mode),a);return r.iter(a,t,function(f){ro(e,f.text,u);var m=u.line;f.stateAfter=m==t-1||m%5==0||m>=i.viewFrom&&mt.start)return a}throw new Error("Mode "+e.name+" failed to advance stream.")}var ha=function(e,t,n){this.start=e.start,this.end=e.pos,this.string=e.current(),this.type=t||null,this.state=n};function ga(e,t,n,r){var i=e.doc,a=i.mode,l;t=je(i,t);var u=Ae(i,t.line),f=wn(e,t.line,n),m=new at(u.text,e.options.tabSize,f),A;for(r&&(A=[]);(r||m.pose.options.maxHighlightLength?(u=!1,l&&ro(e,t,r,A.pos),A.pos=t.length,B=null):B=ma(no(n,A,r.state,ee),a),ee){var Y=ee[0].name;Y&&(B="m-"+(B?Y+" "+B:Y))}if(!u||m!=B){for(;fl;--u){if(u<=a.first)return a.first;var f=Ae(a,u-1),m=f.stateAfter;if(m&&(!n||u+(m instanceof ri?m.lookAhead:0)<=a.modeFrontier))return u;var A=Oe(f.text,null,e.options.tabSize);(i==null||r>A)&&(i=u-1,r=A)}return i}function Lc(e,t){if(e.modeFrontier=Math.min(e.modeFrontier,t),!(e.highlightFrontiern;r--){var i=Ae(e,r).stateAfter;if(i&&(!(i instanceof ri)||r+i.lookAhead=t:a.to>t);(r||(r=[])).push(new ni(l,a.from,f?null:a.to))}}return r}function Dc(e,t,n){var r;if(e)for(var i=0;i=t:a.to>t);if(u||a.from==t&&l.type=="bookmark"&&(!n||a.marker.insertLeft)){var f=a.from==null||(l.inclusiveLeft?a.from<=t:a.from0&&u)for(var Ce=0;Ce0)){var A=[f,1],B=ye(m.from,u.from),ee=ye(m.to,u.to);(B<0||!l.inclusiveLeft&&!B)&&A.push({from:m.from,to:u.from}),(ee>0||!l.inclusiveRight&&!ee)&&A.push({from:u.to,to:m.to}),i.splice.apply(i,A),f+=A.length-3}}return i}function xa(e){var t=e.markedSpans;if(t){for(var n=0;nt)&&(!r||oo(r,a.marker)<0)&&(r=a.marker)}return r}function Sa(e,t,n,r,i){var a=Ae(e,t),l=or&&a.markedSpans;if(l)for(var u=0;u=0&&B<=0||A<=0&&B>=0)&&(A<=0&&(f.marker.inclusiveRight&&i.inclusiveLeft?ye(m.to,n)>=0:ye(m.to,n)>0)||A>=0&&(f.marker.inclusiveRight&&i.inclusiveLeft?ye(m.from,r)<=0:ye(m.from,r)<0)))return!0}}}function Zt(e){for(var t;t=wa(e);)e=t.find(-1,!0).line;return e}function Ic(e){for(var t;t=ai(e);)e=t.find(1,!0).line;return e}function Nc(e){for(var t,n;t=ai(e);)e=t.find(1,!0).line,(n||(n=[])).push(e);return n}function ao(e,t){var n=Ae(e,t),r=Zt(n);return n==r?t:x(r)}function Ta(e,t){if(t>e.lastLine())return t;var n=Ae(e,t),r;if(!mr(e,n))return t;for(;r=ai(n);)n=r.find(1,!0).line;return x(n)+1}function mr(e,t){var n=or&&t.markedSpans;if(n){for(var r=void 0,i=0;it.maxLineLength&&(t.maxLineLength=i,t.maxLine=r)})}var Xr=function(e,t,n){this.text=e,_a(this,t),this.height=n?n(this):1};Xr.prototype.lineNo=function(){return x(this)},Wt(Xr);function Oc(e,t,n,r){e.text=t,e.stateAfter&&(e.stateAfter=null),e.styles&&(e.styles=null),e.order!=null&&(e.order=null),xa(e),_a(e,n);var i=r?r(e):1;i!=e.height&&Bt(e,i)}function Pc(e){e.parent=null,xa(e)}var Bc={},jc={};function La(e,t){if(!e||/^\s*$/.test(e))return null;var n=t.addModeClass?jc:Bc;return n[e]||(n[e]=e.replace(/\S+/g,"cm-$&"))}function Ca(e,t){var n=K("span",null,null,g?"padding-right: .1px":null),r={pre:K("pre",[n],"CodeMirror-line"),content:n,col:0,pos:0,cm:e,trailingSpace:!1,splitSpaces:e.getOption("lineWrapping")};t.measure={};for(var i=0;i<=(t.rest?t.rest.length:0);i++){var a=i?t.rest[i-1]:t.line,l=void 0;r.pos=0,r.addToken=Hc,pr(e.display.measure)&&(l=Pe(a,e.doc.direction))&&(r.addToken=Uc(r.addToken,l)),r.map=[];var u=t!=e.display.externalMeasured&&x(a);$c(a,r,da(e,a,u)),a.styleClasses&&(a.styleClasses.bgClass&&(r.bgClass=xe(a.styleClasses.bgClass,r.bgClass||"")),a.styleClasses.textClass&&(r.textClass=xe(a.styleClasses.textClass,r.textClass||""))),r.map.length==0&&r.map.push(0,0,r.content.appendChild(ei(e.display.measure))),i==0?(t.measure.map=r.map,t.measure.cache={}):((t.measure.maps||(t.measure.maps=[])).push(r.map),(t.measure.caches||(t.measure.caches=[])).push({}))}if(g){var f=r.content.lastChild;(/\bcm-tab\b/.test(f.className)||f.querySelector&&f.querySelector(".cm-tab"))&&(r.content.className="cm-tab-wrap-hack")}return it(e,"renderLine",e,t.line,r.pre),r.pre.className&&(r.textClass=xe(r.pre.className,r.textClass||"")),r}function Rc(e){var t=y("span","\u2022","cm-invalidchar");return t.title="\\u"+e.charCodeAt(0).toString(16),t.setAttribute("aria-label",t.title),t}function Hc(e,t,n,r,i,a,l){if(t){var u=e.splitSpaces?Wc(t,e.trailingSpace):t,f=e.cm.state.specialChars,m=!1,A;if(!f.test(t))e.col+=t.length,A=document.createTextNode(u),e.map.push(e.pos,e.pos+t.length,A),s&&h<9&&(m=!0),e.pos+=t.length;else{A=document.createDocumentFragment();for(var B=0;;){f.lastIndex=B;var ee=f.exec(t),Y=ee?ee.index-B:t.length-B;if(Y){var ie=document.createTextNode(u.slice(B,B+Y));s&&h<9?A.appendChild(y("span",[ie])):A.appendChild(ie),e.map.push(e.pos,e.pos+Y,ie),e.col+=Y,e.pos+=Y}if(!ee)break;B+=Y+1;var ue=void 0;if(ee[0]==" "){var me=e.cm.options.tabSize,ve=me-e.col%me;ue=A.appendChild(y("span",Z(ve),"cm-tab")),ue.setAttribute("role","presentation"),ue.setAttribute("cm-text"," "),e.col+=ve}else ee[0]=="\r"||ee[0]==` +`?(ue=A.appendChild(y("span",ee[0]=="\r"?"\u240D":"\u2424","cm-invalidchar")),ue.setAttribute("cm-text",ee[0]),e.col+=1):(ue=e.cm.options.specialCharPlaceholder(ee[0]),ue.setAttribute("cm-text",ee[0]),s&&h<9?A.appendChild(y("span",[ue])):A.appendChild(ue),e.col+=1);e.map.push(e.pos,e.pos+1,ue),e.pos++}}if(e.trailingSpace=u.charCodeAt(t.length-1)==32,n||r||i||m||a||l){var _e=n||"";r&&(_e+=r),i&&(_e+=i);var be=y("span",[A],_e,a);if(l)for(var Ce in l)l.hasOwnProperty(Ce)&&Ce!="style"&&Ce!="class"&&be.setAttribute(Ce,l[Ce]);return e.content.appendChild(be)}e.content.appendChild(A)}}function Wc(e,t){if(e.length>1&&!/ /.test(e))return e;for(var n=t,r="",i=0;im&&B.from<=m));ee++);if(B.to>=A)return e(n,r,i,a,l,u,f);e(n,r.slice(0,B.to-m),i,a,null,u,f),a=null,r=r.slice(B.to-m),m=B.to}}}function Ea(e,t,n,r){var i=!r&&n.widgetNode;i&&e.map.push(e.pos,e.pos+t,i),!r&&e.cm.display.input.needsContentAttribute&&(i||(i=e.content.appendChild(document.createElement("span"))),i.setAttribute("cm-marker",n.id)),i&&(e.cm.display.input.setUneditable(i),e.content.appendChild(i)),e.pos+=t,e.trailingSpace=!1}function $c(e,t,n){var r=e.markedSpans,i=e.text,a=0;if(!r){for(var l=1;lf||$e.collapsed&&Fe.to==f&&Fe.from==f)){if(Fe.to!=null&&Fe.to!=f&&Y>Fe.to&&(Y=Fe.to,ue=""),$e.className&&(ie+=" "+$e.className),$e.css&&(ee=(ee?ee+";":"")+$e.css),$e.startStyle&&Fe.from==f&&(me+=" "+$e.startStyle),$e.endStyle&&Fe.to==Y&&(Ce||(Ce=[])).push($e.endStyle,Fe.to),$e.title&&((_e||(_e={})).title=$e.title),$e.attributes)for(var Ve in $e.attributes)(_e||(_e={}))[Ve]=$e.attributes[Ve];$e.collapsed&&(!ve||oo(ve.marker,$e)<0)&&(ve=Fe)}else Fe.from>f&&Y>Fe.from&&(Y=Fe.from)}if(Ce)for(var vt=0;vt=u)break;for(var Ot=Math.min(u,Y);;){if(A){var At=f+A.length;if(!ve){var ut=At>Ot?A.slice(0,Ot-f):A;t.addToken(t,ut,B?B+ie:ie,me,f+ut.length==Y?ue:"",ee,_e)}if(At>=Ot){A=A.slice(Ot-f),f=Ot;break}f=At,me=""}A=i.slice(a,a=n[m++]),B=La(n[m++],t.cm.options)}}}function za(e,t,n){this.line=t,this.rest=Nc(t),this.size=this.rest?x(ce(this.rest))-n+1:1,this.node=this.text=null,this.hidden=mr(e,t)}function si(e,t,n){for(var r=[],i,a=t;a2&&a.push((f.bottom+m.top)/2-n.top)}}a.push(n.bottom-n.top)}}function Na(e,t,n){if(e.line==t)return{map:e.measure.map,cache:e.measure.cache};if(e.rest){for(var r=0;rn)return{map:e.measure.maps[i],cache:e.measure.caches[i],before:!0}}}function rf(e,t){t=Zt(t);var n=x(t),r=e.display.externalMeasured=new za(e.doc,t,n);r.lineN=n;var i=r.built=Ca(e,r);return r.text=i.pre,V(e.display.lineMeasure,i.pre),r}function Oa(e,t,n,r){return tr(e,Qr(e,t),n,r)}function po(e,t){if(t>=e.display.viewFrom&&t=n.lineN&&tt)&&(a=f-u,i=a-1,t>=f&&(l="right")),i!=null){if(r=e[m+2],u==f&&n==(r.insertLeft?"left":"right")&&(l=n),n=="left"&&i==0)for(;m&&e[m-2]==e[m-3]&&e[m-1].insertLeft;)r=e[(m-=3)+2],l="left";if(n=="right"&&i==f-u)for(;m=0&&(n=e[i]).left==n.right;i--);return n}function of(e,t,n,r){var i=Ba(t.map,n,r),a=i.node,l=i.start,u=i.end,f=i.collapse,m;if(a.nodeType==3){for(var A=0;A<4;A++){for(;l&&H(t.line.text.charAt(i.coverStart+l));)--l;for(;i.coverStart+u0&&(f=r="right");var B;e.options.lineWrapping&&(B=a.getClientRects()).length>1?m=B[r=="right"?B.length-1:0]:m=a.getBoundingClientRect()}if(s&&h<9&&!l&&(!m||!m.left&&!m.right)){var ee=a.parentNode.getClientRects()[0];ee?m={left:ee.left,right:ee.left+Jr(e.display),top:ee.top,bottom:ee.bottom}:m=Pa}for(var Y=m.top-t.rect.top,ie=m.bottom-t.rect.top,ue=(Y+ie)/2,me=t.view.measure.heights,ve=0;ve=r.text.length?(f=r.text.length,m="before"):f<=0&&(f=0,m="after"),!u)return l(m=="before"?f-1:f,m=="before");function A(ie,ue,me){var ve=u[ue],_e=ve.level==1;return l(me?ie-1:ie,_e!=me)}var B=Pt(u,f,m),ee=dt,Y=A(f,B,m=="before");return ee!=null&&(Y.other=A(f,ee,m!="before")),Y}function $a(e,t){var n=0;t=je(e.doc,t),e.options.lineWrapping||(n=Jr(e.display)*t.ch);var r=Ae(e.doc,t.line),i=ar(r)+ui(e.display);return{left:n,right:n,top:i,bottom:i+r.height}}function go(e,t,n,r,i){var a=ne(e,t,n);return a.xRel=i,r&&(a.outside=r),a}function mo(e,t,n){var r=e.doc;if(n+=e.display.viewOffset,n<0)return go(r.first,0,null,-1,-1);var i=P(r,n),a=r.first+r.size-1;if(i>a)return go(r.first+r.size-1,Ae(r,a).text.length,null,1,1);t<0&&(t=0);for(var l=Ae(r,i);;){var u=lf(e,l,i,t,n),f=Fc(l,u.ch+(u.xRel>0||u.outside>0?1:0));if(!f)return u;var m=f.find(1);if(m.line==i)return m;l=Ae(r,i=m.line)}}function Ka(e,t,n,r){r-=ho(t);var i=t.text.length,a=De(function(l){return tr(e,n,l-1).bottom<=r},i,0);return i=De(function(l){return tr(e,n,l).top>r},a,i),{begin:a,end:i}}function Ga(e,t,n,r){n||(n=Qr(e,t));var i=ci(e,t,tr(e,n,r),"line").top;return Ka(e,t,n,i)}function vo(e,t,n,r){return e.bottom<=n?!1:e.top>n?!0:(r?e.left:e.right)>t}function lf(e,t,n,r,i){i-=ar(t);var a=Qr(e,t),l=ho(t),u=0,f=t.text.length,m=!0,A=Pe(t,e.doc.direction);if(A){var B=(e.options.lineWrapping?uf:sf)(e,t,n,a,A,r,i);m=B.level!=1,u=m?B.from:B.to-1,f=m?B.to:B.from-1}var ee=null,Y=null,ie=De(function(Ne){var Fe=tr(e,a,Ne);return Fe.top+=l,Fe.bottom+=l,vo(Fe,r,i,!1)?(Fe.top<=i&&Fe.left<=r&&(ee=Ne,Y=Fe),!0):!1},u,f),ue,me,ve=!1;if(Y){var _e=r-Y.left=Ce.bottom?1:0}return ie=se(t.text,ie,1),go(n,ie,me,ve,r-ue)}function sf(e,t,n,r,i,a,l){var u=De(function(B){var ee=i[B],Y=ee.level!=1;return vo(Xt(e,ne(n,Y?ee.to:ee.from,Y?"before":"after"),"line",t,r),a,l,!0)},0,i.length-1),f=i[u];if(u>0){var m=f.level!=1,A=Xt(e,ne(n,m?f.from:f.to,m?"after":"before"),"line",t,r);vo(A,a,l,!0)&&A.top>l&&(f=i[u-1])}return f}function uf(e,t,n,r,i,a,l){var u=Ka(e,t,r,l),f=u.begin,m=u.end;/\s/.test(t.text.charAt(m-1))&&m--;for(var A=null,B=null,ee=0;ee=m||Y.to<=f)){var ie=Y.level!=1,ue=tr(e,r,ie?Math.min(m,Y.to)-1:Math.max(f,Y.from)).right,me=ueme)&&(A=Y,B=me)}}return A||(A=i[i.length-1]),A.fromm&&(A={from:A.from,to:m,level:A.level}),A}var zr;function Vr(e){if(e.cachedTextHeight!=null)return e.cachedTextHeight;if(zr==null){zr=y("pre",null,"CodeMirror-line-like");for(var t=0;t<49;++t)zr.appendChild(document.createTextNode("x")),zr.appendChild(y("br"));zr.appendChild(document.createTextNode("x"))}V(e.measure,zr);var n=zr.offsetHeight/50;return n>3&&(e.cachedTextHeight=n),j(e.measure),n||1}function Jr(e){if(e.cachedCharWidth!=null)return e.cachedCharWidth;var t=y("span","xxxxxxxxxx"),n=y("pre",[t],"CodeMirror-line-like");V(e.measure,n);var r=t.getBoundingClientRect(),i=(r.right-r.left)/10;return i>2&&(e.cachedCharWidth=i),i||10}function bo(e){for(var t=e.display,n={},r={},i=t.gutters.clientLeft,a=t.gutters.firstChild,l=0;a;a=a.nextSibling,++l){var u=e.display.gutterSpecs[l].className;n[u]=a.offsetLeft+a.clientLeft+i,r[u]=a.clientWidth}return{fixedPos:yo(t),gutterTotalWidth:t.gutters.offsetWidth,gutterLeft:n,gutterWidth:r,wrapperWidth:t.wrapper.clientWidth}}function yo(e){return e.scroller.getBoundingClientRect().left-e.sizer.getBoundingClientRect().left}function Za(e){var t=Vr(e.display),n=e.options.lineWrapping,r=n&&Math.max(5,e.display.scroller.clientWidth/Jr(e.display)-3);return function(i){if(mr(e.doc,i))return 0;var a=0;if(i.widgets)for(var l=0;l0&&(m=Ae(e.doc,f.line).text).length==f.ch){var A=Oe(m,m.length,e.options.tabSize)-m.length;f=ne(f.line,Math.max(0,Math.round((a-Ia(e.display).left)/Jr(e.display))-A))}return f}function Ar(e,t){if(t>=e.display.viewTo||(t-=e.display.viewFrom,t<0))return null;for(var n=e.display.view,r=0;rt)&&(i.updateLineNumbers=t),e.curOp.viewChanged=!0,t>=i.viewTo)or&&ao(e.doc,t)i.viewFrom?br(e):(i.viewFrom+=r,i.viewTo+=r);else if(t<=i.viewFrom&&n>=i.viewTo)br(e);else if(t<=i.viewFrom){var a=di(e,n,n+r,1);a?(i.view=i.view.slice(a.index),i.viewFrom=a.lineN,i.viewTo+=r):br(e)}else if(n>=i.viewTo){var l=di(e,t,t,-1);l?(i.view=i.view.slice(0,l.index),i.viewTo=l.lineN):br(e)}else{var u=di(e,t,t,-1),f=di(e,n,n+r,1);u&&f?(i.view=i.view.slice(0,u.index).concat(si(e,u.lineN,f.lineN)).concat(i.view.slice(f.index)),i.viewTo+=r):br(e)}var m=i.externalMeasured;m&&(n=i.lineN&&t=r.viewTo)){var a=r.view[Ar(e,t)];if(a.node!=null){var l=a.changes||(a.changes=[]);Se(l,n)==-1&&l.push(n)}}}function br(e){e.display.viewFrom=e.display.viewTo=e.doc.first,e.display.view=[],e.display.viewOffset=0}function di(e,t,n,r){var i=Ar(e,t),a,l=e.display.view;if(!or||n==e.doc.first+e.doc.size)return{index:i,lineN:n};for(var u=e.display.viewFrom,f=0;f0){if(i==l.length-1)return null;a=u+l[i].size-t,i++}else a=u-t;t+=a,n+=a}for(;ao(e.doc,n)!=n;){if(i==(r<0?0:l.length-1))return null;n+=r*l[i-(r<0?1:0)].size,i+=r}return{index:i,lineN:n}}function cf(e,t,n){var r=e.display,i=r.view;i.length==0||t>=r.viewTo||n<=r.viewFrom?(r.view=si(e,t,n),r.viewFrom=t):(r.viewFrom>t?r.view=si(e,t,r.viewFrom).concat(r.view):r.viewFromn&&(r.view=r.view.slice(0,Ar(e,n)))),r.viewTo=n}function Xa(e){for(var t=e.display.view,n=0,r=0;r=e.display.viewTo||f.to().line0?l:e.defaultCharWidth())+"px"}if(r.other){var u=n.appendChild(y("div","\xA0","CodeMirror-cursor CodeMirror-secondarycursor"));u.style.display="",u.style.left=r.other.left+"px",u.style.top=r.other.top+"px",u.style.height=(r.other.bottom-r.other.top)*.85+"px"}}function pi(e,t){return e.top-t.top||e.left-t.left}function ff(e,t,n){var r=e.display,i=e.doc,a=document.createDocumentFragment(),l=Ia(e.display),u=l.left,f=Math.max(r.sizerWidth,Er(e)-r.sizer.offsetLeft)-l.right,m=i.direction=="ltr";function A(be,Ce,Ne,Fe){Ce<0&&(Ce=0),Ce=Math.round(Ce),Fe=Math.round(Fe),a.appendChild(y("div",null,"CodeMirror-selected","position: absolute; left: "+be+`px; + top: `+Ce+"px; width: "+(Ne??f-be)+`px; + height: `+(Fe-Ce)+"px"))}function B(be,Ce,Ne){var Fe=Ae(i,be),$e=Fe.text.length,Ve,vt;function rt(ut,Dt){return fi(e,ne(be,ut),"div",Fe,Dt)}function Ot(ut,Dt,yt){var ft=Ga(e,Fe,null,ut),ct=Dt=="ltr"==(yt=="after")?"left":"right",lt=yt=="after"?ft.begin:ft.end-(/\s/.test(Fe.text.charAt(ft.end-1))?2:1);return rt(lt,ct)[ct]}var At=Pe(Fe,i.direction);return nt(At,Ce||0,Ne??$e,function(ut,Dt,yt,ft){var ct=yt=="ltr",lt=rt(ut,ct?"left":"right"),qt=rt(Dt-1,ct?"right":"left"),pn=Ce==null&&ut==0,Sr=Ne==null&&Dt==$e,St=ft==0,rr=!At||ft==At.length-1;if(qt.top-lt.top<=3){var bt=(m?pn:Sr)&&St,Zo=(m?Sr:pn)&&rr,cr=bt?u:(ct?lt:qt).left,Nr=Zo?f:(ct?qt:lt).right;A(cr,lt.top,Nr-cr,lt.bottom)}else{var Or,Lt,hn,Xo;ct?(Or=m&&pn&&St?u:lt.left,Lt=m?f:Ot(ut,yt,"before"),hn=m?u:Ot(Dt,yt,"after"),Xo=m&&Sr&&rr?f:qt.right):(Or=m?Ot(ut,yt,"before"):u,Lt=!m&&pn&&St?f:lt.right,hn=!m&&Sr&&rr?u:qt.left,Xo=m?Ot(Dt,yt,"after"):f),A(Or,lt.top,Lt-Or,lt.bottom),lt.bottom0?t.blinker=setInterval(function(){e.hasFocus()||en(e),t.cursorDiv.style.visibility=(n=!n)?"":"hidden"},e.options.cursorBlinkRate):e.options.cursorBlinkRate<0&&(t.cursorDiv.style.visibility="hidden")}}function Qa(e){e.hasFocus()||(e.display.input.focus(),e.state.focused||So(e))}function wo(e){e.state.delayingBlurEvent=!0,setTimeout(function(){e.state.delayingBlurEvent&&(e.state.delayingBlurEvent=!1,e.state.focused&&en(e))},100)}function So(e,t){e.state.delayingBlurEvent&&!e.state.draggingText&&(e.state.delayingBlurEvent=!1),e.options.readOnly!="nocursor"&&(e.state.focused||(it(e,"focus",e,t),e.state.focused=!0,le(e.display.wrapper,"CodeMirror-focused"),!e.curOp&&e.display.selForContextMenu!=e.doc.sel&&(e.display.input.reset(),g&&setTimeout(function(){return e.display.input.reset(!0)},20)),e.display.input.receivedFocus()),ko(e))}function en(e,t){e.state.delayingBlurEvent||(e.state.focused&&(it(e,"blur",e,t),e.state.focused=!1,Q(e.display.wrapper,"CodeMirror-focused")),clearInterval(e.display.blinker),setTimeout(function(){e.state.focused||(e.display.shift=!1)},150))}function hi(e){for(var t=e.display,n=t.lineDiv.offsetTop,r=Math.max(0,t.scroller.getBoundingClientRect().top),i=t.lineDiv.getBoundingClientRect().top,a=0,l=0;l.005||Y<-.005)&&(ie.display.sizerWidth){var ue=Math.ceil(A/Jr(e.display));ue>e.display.maxLineLength&&(e.display.maxLineLength=ue,e.display.maxLine=u.line,e.display.maxLineChanged=!0)}}}Math.abs(a)>2&&(t.scroller.scrollTop+=a)}function Va(e){if(e.widgets)for(var t=0;t=l&&(a=P(t,ar(Ae(t,f))-e.wrapper.clientHeight),l=f)}return{from:a,to:Math.max(l,a+1)}}function df(e,t){if(!ot(e,"scrollCursorIntoView")){var n=e.display,r=n.sizer.getBoundingClientRect(),i=null,a=n.wrapper.ownerDocument;if(t.top+r.top<0?i=!0:t.bottom+r.top>(a.defaultView.innerHeight||a.documentElement.clientHeight)&&(i=!1),i!=null&&!M){var l=y("div","\u200B",null,`position: absolute; + top: `+(t.top-n.viewOffset-ui(e.display))+`px; + height: `+(t.bottom-t.top+er(e)+n.barHeight)+`px; + left: `+t.left+"px; width: "+Math.max(2,t.right-t.left)+"px;");e.display.lineSpace.appendChild(l),l.scrollIntoView(i),e.display.lineSpace.removeChild(l)}}}function pf(e,t,n,r){r==null&&(r=0);var i;!e.options.lineWrapping&&t==n&&(n=t.sticky=="before"?ne(t.line,t.ch+1,"before"):t,t=t.ch?ne(t.line,t.sticky=="before"?t.ch-1:t.ch,"after"):t);for(var a=0;a<5;a++){var l=!1,u=Xt(e,t),f=!n||n==t?u:Xt(e,n);i={left:Math.min(u.left,f.left),top:Math.min(u.top,f.top)-r,right:Math.max(u.left,f.left),bottom:Math.max(u.bottom,f.bottom)+r};var m=To(e,i),A=e.doc.scrollTop,B=e.doc.scrollLeft;if(m.scrollTop!=null&&(An(e,m.scrollTop),Math.abs(e.doc.scrollTop-A)>1&&(l=!0)),m.scrollLeft!=null&&(Dr(e,m.scrollLeft),Math.abs(e.doc.scrollLeft-B)>1&&(l=!0)),!l)break}return i}function hf(e,t){var n=To(e,t);n.scrollTop!=null&&An(e,n.scrollTop),n.scrollLeft!=null&&Dr(e,n.scrollLeft)}function To(e,t){var n=e.display,r=Vr(e.display);t.top<0&&(t.top=0);var i=e.curOp&&e.curOp.scrollTop!=null?e.curOp.scrollTop:n.scroller.scrollTop,a=fo(e),l={};t.bottom-t.top>a&&(t.bottom=t.top+a);var u=e.doc.height+co(n),f=t.topu-r;if(t.topi+a){var A=Math.min(t.top,(m?u:t.bottom)-a);A!=i&&(l.scrollTop=A)}var B=e.options.fixedGutter?0:n.gutters.offsetWidth,ee=e.curOp&&e.curOp.scrollLeft!=null?e.curOp.scrollLeft:n.scroller.scrollLeft-B,Y=Er(e)-n.gutters.offsetWidth,ie=t.right-t.left>Y;return ie&&(t.right=t.left+Y),t.left<10?l.scrollLeft=0:t.leftY+ee-3&&(l.scrollLeft=t.right+(ie?0:10)-Y),l}function Lo(e,t){t!=null&&(mi(e),e.curOp.scrollTop=(e.curOp.scrollTop==null?e.doc.scrollTop:e.curOp.scrollTop)+t)}function tn(e){mi(e);var t=e.getCursor();e.curOp.scrollToPos={from:t,to:t,margin:e.options.cursorScrollMargin}}function Mn(e,t,n){(t!=null||n!=null)&&mi(e),t!=null&&(e.curOp.scrollLeft=t),n!=null&&(e.curOp.scrollTop=n)}function gf(e,t){mi(e),e.curOp.scrollToPos=t}function mi(e){var t=e.curOp.scrollToPos;if(t){e.curOp.scrollToPos=null;var n=$a(e,t.from),r=$a(e,t.to);Ja(e,n,r,t.margin)}}function Ja(e,t,n,r){var i=To(e,{left:Math.min(t.left,n.left),top:Math.min(t.top,n.top)-r,right:Math.max(t.right,n.right),bottom:Math.max(t.bottom,n.bottom)+r});Mn(e,i.scrollLeft,i.scrollTop)}function An(e,t){Math.abs(e.doc.scrollTop-t)<2||(v||Eo(e,{top:t}),el(e,t,!0),v&&Eo(e),Fn(e,100))}function el(e,t,n){t=Math.max(0,Math.min(e.display.scroller.scrollHeight-e.display.scroller.clientHeight,t)),!(e.display.scroller.scrollTop==t&&!n)&&(e.doc.scrollTop=t,e.display.scrollbars.setScrollTop(t),e.display.scroller.scrollTop!=t&&(e.display.scroller.scrollTop=t))}function Dr(e,t,n,r){t=Math.max(0,Math.min(t,e.display.scroller.scrollWidth-e.display.scroller.clientWidth)),!((n?t==e.doc.scrollLeft:Math.abs(e.doc.scrollLeft-t)<2)&&!r)&&(e.doc.scrollLeft=t,ol(e),e.display.scroller.scrollLeft!=t&&(e.display.scroller.scrollLeft=t),e.display.scrollbars.setScrollLeft(t))}function Dn(e){var t=e.display,n=t.gutters.offsetWidth,r=Math.round(e.doc.height+co(e.display));return{clientHeight:t.scroller.clientHeight,viewHeight:t.wrapper.clientHeight,scrollWidth:t.scroller.scrollWidth,clientWidth:t.scroller.clientWidth,viewWidth:t.wrapper.clientWidth,barLeft:e.options.fixedGutter?n:0,docHeight:r,scrollHeight:r+er(e)+t.barHeight,nativeBarWidth:t.nativeBarWidth,gutterWidth:n}}var qr=function(e,t,n){this.cm=n;var r=this.vert=y("div",[y("div",null,null,"min-width: 1px")],"CodeMirror-vscrollbar"),i=this.horiz=y("div",[y("div",null,null,"height: 100%; min-height: 1px")],"CodeMirror-hscrollbar");r.tabIndex=i.tabIndex=-1,e(r),e(i),Ie(r,"scroll",function(){r.clientHeight&&t(r.scrollTop,"vertical")}),Ie(i,"scroll",function(){i.clientWidth&&t(i.scrollLeft,"horizontal")}),this.checkedZeroWidth=!1,s&&h<8&&(this.horiz.style.minHeight=this.vert.style.minWidth="18px")};qr.prototype.update=function(e){var t=e.scrollWidth>e.clientWidth+1,n=e.scrollHeight>e.clientHeight+1,r=e.nativeBarWidth;if(n){this.vert.style.display="block",this.vert.style.bottom=t?r+"px":"0";var i=e.viewHeight-(t?r:0);this.vert.firstChild.style.height=Math.max(0,e.scrollHeight-e.clientHeight+i)+"px"}else this.vert.scrollTop=0,this.vert.style.display="",this.vert.firstChild.style.height="0";if(t){this.horiz.style.display="block",this.horiz.style.right=n?r+"px":"0",this.horiz.style.left=e.barLeft+"px";var a=e.viewWidth-e.barLeft-(n?r:0);this.horiz.firstChild.style.width=Math.max(0,e.scrollWidth-e.clientWidth+a)+"px"}else this.horiz.style.display="",this.horiz.firstChild.style.width="0";return!this.checkedZeroWidth&&e.clientHeight>0&&(r==0&&this.zeroWidthHack(),this.checkedZeroWidth=!0),{right:n?r:0,bottom:t?r:0}},qr.prototype.setScrollLeft=function(e){this.horiz.scrollLeft!=e&&(this.horiz.scrollLeft=e),this.disableHoriz&&this.enableZeroWidthBar(this.horiz,this.disableHoriz,"horiz")},qr.prototype.setScrollTop=function(e){this.vert.scrollTop!=e&&(this.vert.scrollTop=e),this.disableVert&&this.enableZeroWidthBar(this.vert,this.disableVert,"vert")},qr.prototype.zeroWidthHack=function(){var e=O&&!z?"12px":"18px";this.horiz.style.height=this.vert.style.width=e,this.horiz.style.visibility=this.vert.style.visibility="hidden",this.disableHoriz=new qe,this.disableVert=new qe},qr.prototype.enableZeroWidthBar=function(e,t,n){e.style.visibility="";function r(){var i=e.getBoundingClientRect(),a=n=="vert"?document.elementFromPoint(i.right-1,(i.top+i.bottom)/2):document.elementFromPoint((i.right+i.left)/2,i.bottom-1);a!=e?e.style.visibility="hidden":t.set(1e3,r)}t.set(1e3,r)},qr.prototype.clear=function(){var e=this.horiz.parentNode;e.removeChild(this.horiz),e.removeChild(this.vert)};var qn=function(){};qn.prototype.update=function(){return{bottom:0,right:0}},qn.prototype.setScrollLeft=function(){},qn.prototype.setScrollTop=function(){},qn.prototype.clear=function(){};function rn(e,t){t||(t=Dn(e));var n=e.display.barWidth,r=e.display.barHeight;tl(e,t);for(var i=0;i<4&&n!=e.display.barWidth||r!=e.display.barHeight;i++)n!=e.display.barWidth&&e.options.lineWrapping&&hi(e),tl(e,Dn(e)),n=e.display.barWidth,r=e.display.barHeight}function tl(e,t){var n=e.display,r=n.scrollbars.update(t);n.sizer.style.paddingRight=(n.barWidth=r.right)+"px",n.sizer.style.paddingBottom=(n.barHeight=r.bottom)+"px",n.heightForcer.style.borderBottom=r.bottom+"px solid transparent",r.right&&r.bottom?(n.scrollbarFiller.style.display="block",n.scrollbarFiller.style.height=r.bottom+"px",n.scrollbarFiller.style.width=r.right+"px"):n.scrollbarFiller.style.display="",r.bottom&&e.options.coverGutterNextToScrollbar&&e.options.fixedGutter?(n.gutterFiller.style.display="block",n.gutterFiller.style.height=r.bottom+"px",n.gutterFiller.style.width=t.gutterWidth+"px"):n.gutterFiller.style.display=""}var rl={native:qr,null:qn};function nl(e){e.display.scrollbars&&(e.display.scrollbars.clear(),e.display.scrollbars.addClass&&Q(e.display.wrapper,e.display.scrollbars.addClass)),e.display.scrollbars=new rl[e.options.scrollbarStyle](function(t){e.display.wrapper.insertBefore(t,e.display.scrollbarFiller),Ie(t,"mousedown",function(){e.state.focused&&setTimeout(function(){return e.display.input.focus()},0)}),t.setAttribute("cm-not-content","true")},function(t,n){n=="horizontal"?Dr(e,t):An(e,t)},e),e.display.scrollbars.addClass&&le(e.display.wrapper,e.display.scrollbars.addClass)}var mf=0;function Fr(e){e.curOp={cm:e,viewChanged:!1,startHeight:e.doc.height,forceUpdate:!1,updateInput:0,typing:!1,changeObjs:null,cursorActivityHandlers:null,cursorActivityCalled:0,selectionChanged:!1,updateMaxLine:!1,scrollLeft:null,scrollTop:null,scrollToPos:null,focus:!1,id:++mf,markArrays:null},Kc(e.curOp)}function Ir(e){var t=e.curOp;t&&Zc(t,function(n){for(var r=0;r=n.viewTo)||n.maxLineChanged&&t.options.lineWrapping,e.update=e.mustUpdate&&new vi(t,e.mustUpdate&&{top:e.scrollTop,ensure:e.scrollToPos},e.forceUpdate)}function yf(e){e.updatedDisplay=e.mustUpdate&&Co(e.cm,e.update)}function xf(e){var t=e.cm,n=t.display;e.updatedDisplay&&hi(t),e.barMeasure=Dn(t),n.maxLineChanged&&!t.options.lineWrapping&&(e.adjustWidthTo=Oa(t,n.maxLine,n.maxLine.text.length).left+3,t.display.sizerWidth=e.adjustWidthTo,e.barMeasure.scrollWidth=Math.max(n.scroller.clientWidth,n.sizer.offsetLeft+e.adjustWidthTo+er(t)+t.display.barWidth),e.maxScrollLeft=Math.max(0,n.sizer.offsetLeft+e.adjustWidthTo-Er(t))),(e.updatedDisplay||e.selectionChanged)&&(e.preparedSelection=n.input.prepareSelection())}function _f(e){var t=e.cm;e.adjustWidthTo!=null&&(t.display.sizer.style.minWidth=e.adjustWidthTo+"px",e.maxScrollLeft=e.display.viewTo)){var n=+new Date+e.options.workTime,r=wn(e,t.highlightFrontier),i=[];t.iter(r.line,Math.min(t.first+t.size,e.display.viewTo+500),function(a){if(r.line>=e.display.viewFrom){var l=a.styles,u=a.text.length>e.options.maxHighlightLength?Vt(t.mode,r.state):null,f=fa(e,a,r,!0);u&&(r.state=u),a.styles=f.styles;var m=a.styleClasses,A=f.classes;A?a.styleClasses=A:m&&(a.styleClasses=null);for(var B=!l||l.length!=a.styles.length||m!=A&&(!m||!A||m.bgClass!=A.bgClass||m.textClass!=A.textClass),ee=0;!B&&een)return Fn(e,e.options.workDelay),!0}),t.highlightFrontier=r.line,t.modeFrontier=Math.max(t.modeFrontier,r.line),i.length&&Nt(e,function(){for(var a=0;a=n.viewFrom&&t.visible.to<=n.viewTo&&(n.updateLineNumbers==null||n.updateLineNumbers>=n.viewTo)&&n.renderedView==n.view&&Xa(e)==0)return!1;al(e)&&(br(e),t.dims=bo(e));var i=r.first+r.size,a=Math.max(t.visible.from-e.options.viewportMargin,r.first),l=Math.min(i,t.visible.to+e.options.viewportMargin);n.viewFroml&&n.viewTo-l<20&&(l=Math.min(i,n.viewTo)),or&&(a=ao(e.doc,a),l=Ta(e.doc,l));var u=a!=n.viewFrom||l!=n.viewTo||n.lastWrapHeight!=t.wrapperHeight||n.lastWrapWidth!=t.wrapperWidth;cf(e,a,l),n.viewOffset=ar(Ae(e.doc,n.viewFrom)),e.display.mover.style.top=n.viewOffset+"px";var f=Xa(e);if(!u&&f==0&&!t.force&&n.renderedView==n.view&&(n.updateLineNumbers==null||n.updateLineNumbers>=n.viewTo))return!1;var m=Tf(e);return f>4&&(n.lineDiv.style.display="none"),Cf(e,n.updateLineNumbers,t.dims),f>4&&(n.lineDiv.style.display=""),n.renderedView=n.view,Lf(m),j(n.cursorDiv),j(n.selectionDiv),n.gutters.style.height=n.sizer.style.minHeight=0,u&&(n.lastWrapHeight=t.wrapperHeight,n.lastWrapWidth=t.wrapperWidth,Fn(e,400)),n.updateLineNumbers=null,!0}function il(e,t){for(var n=t.viewport,r=!0;;r=!1){if(!r||!e.options.lineWrapping||t.oldDisplayWidth==Er(e)){if(n&&n.top!=null&&(n={top:Math.min(e.doc.height+co(e.display)-fo(e),n.top)}),t.visible=gi(e.display,e.doc,n),t.visible.from>=e.display.viewFrom&&t.visible.to<=e.display.viewTo)break}else r&&(t.visible=gi(e.display,e.doc,n));if(!Co(e,t))break;hi(e);var i=Dn(e);zn(e),rn(e,i),Mo(e,i),t.force=!1}t.signal(e,"update",e),(e.display.viewFrom!=e.display.reportedViewFrom||e.display.viewTo!=e.display.reportedViewTo)&&(t.signal(e,"viewportChange",e,e.display.viewFrom,e.display.viewTo),e.display.reportedViewFrom=e.display.viewFrom,e.display.reportedViewTo=e.display.viewTo)}function Eo(e,t){var n=new vi(e,t);if(Co(e,n)){hi(e),il(e,n);var r=Dn(e);zn(e),rn(e,r),Mo(e,r),n.finish()}}function Cf(e,t,n){var r=e.display,i=e.options.lineNumbers,a=r.lineDiv,l=a.firstChild;function u(ie){var ue=ie.nextSibling;return g&&O&&e.display.currentWheelTarget==ie?ie.style.display="none":ie.parentNode.removeChild(ie),ue}for(var f=r.view,m=r.viewFrom,A=0;A-1&&(Y=!1),Ma(e,B,m,n)),Y&&(j(B.lineNumber),B.lineNumber.appendChild(document.createTextNode(he(e.options,m)))),l=B.node.nextSibling}m+=B.size}for(;l;)l=u(l)}function zo(e){var t=e.gutters.offsetWidth;e.sizer.style.marginLeft=t+"px",ht(e,"gutterChanged",e)}function Mo(e,t){e.display.sizer.style.minHeight=t.docHeight+"px",e.display.heightForcer.style.top=t.docHeight+"px",e.display.gutters.style.height=t.docHeight+e.display.barHeight+er(e)+"px"}function ol(e){var t=e.display,n=t.view;if(!(!t.alignWidgets&&(!t.gutters.firstChild||!e.options.fixedGutter))){for(var r=yo(t)-t.scroller.scrollLeft+e.doc.scrollLeft,i=t.gutters.offsetWidth,a=r+"px",l=0;lu.clientWidth,m=u.scrollHeight>u.clientHeight;if(r&&f||i&&m){if(i&&O&&g){e:for(var A=t.target,B=l.view;A!=u;A=A.parentNode)for(var ee=0;ee=0&&ye(e,r.to())<=0)return n}return-1};var Ye=function(e,t){this.anchor=e,this.head=t};Ye.prototype.from=function(){return Zr(this.anchor,this.head)},Ye.prototype.to=function(){return Et(this.anchor,this.head)},Ye.prototype.empty=function(){return this.head.line==this.anchor.line&&this.head.ch==this.anchor.ch};function Yt(e,t,n){var r=e&&e.options.selectionsMayTouch,i=t[n];t.sort(function(ee,Y){return ye(ee.from(),Y.from())}),n=Se(t,i);for(var a=1;a0:f>=0){var m=Zr(u.from(),l.from()),A=Et(u.to(),l.to()),B=u.empty()?l.from()==l.head:u.from()==u.head;a<=n&&--n,t.splice(--a,2,new Ye(B?A:m,B?m:A))}}return new jt(t,n)}function yr(e,t){return new jt([new Ye(e,t||e)],0)}function xr(e){return e.text?ne(e.from.line+e.text.length-1,ce(e.text).length+(e.text.length==1?e.from.ch:0)):e.to}function cl(e,t){if(ye(e,t.from)<0)return e;if(ye(e,t.to)<=0)return xr(t);var n=e.line+t.text.length-(t.to.line-t.from.line)-1,r=e.ch;return e.line==t.to.line&&(r+=xr(t).ch-t.to.ch),ne(n,r)}function Do(e,t){for(var n=[],r=0;r1&&e.remove(u.line+1,ie-1),e.insert(u.line+1,ve)}ht(e,"change",e,t)}function _r(e,t,n){function r(i,a,l){if(i.linked)for(var u=0;u1&&!e.done[e.done.length-2].ranges)return e.done.pop(),ce(e.done)}function ml(e,t,n,r){var i=e.history;i.undone.length=0;var a=+new Date,l,u;if((i.lastOp==r||i.lastOrigin==t.origin&&t.origin&&(t.origin.charAt(0)=="+"&&i.lastModTime>a-(e.cm?e.cm.options.historyEventDelay:500)||t.origin.charAt(0)=="*"))&&(l=Df(i,i.lastOp==r)))u=ce(l.changes),ye(t.from,t.to)==0&&ye(t.from,u.to)==0?u.to=xr(t):l.changes.push(Io(e,t));else{var f=ce(i.done);for((!f||!f.ranges)&&xi(e.sel,i.done),l={changes:[Io(e,t)],generation:i.generation},i.done.push(l);i.done.length>i.undoDepth;)i.done.shift(),i.done[0].ranges||i.done.shift()}i.done.push(n),i.generation=++i.maxGeneration,i.lastModTime=i.lastSelTime=a,i.lastOp=i.lastSelOp=r,i.lastOrigin=i.lastSelOrigin=t.origin,u||it(e,"historyAdded")}function qf(e,t,n,r){var i=t.charAt(0);return i=="*"||i=="+"&&n.ranges.length==r.ranges.length&&n.somethingSelected()==r.somethingSelected()&&new Date-e.history.lastSelTime<=(e.cm?e.cm.options.historyEventDelay:500)}function Ff(e,t,n,r){var i=e.history,a=r&&r.origin;n==i.lastSelOp||a&&i.lastSelOrigin==a&&(i.lastModTime==i.lastSelTime&&i.lastOrigin==a||qf(e,a,ce(i.done),t))?i.done[i.done.length-1]=t:xi(t,i.done),i.lastSelTime=+new Date,i.lastSelOrigin=a,i.lastSelOp=n,r&&r.clearRedo!==!1&&gl(i.undone)}function xi(e,t){var n=ce(t);n&&n.ranges&&n.equals(e)||t.push(e)}function vl(e,t,n,r){var i=t["spans_"+e.id],a=0;e.iter(Math.max(e.first,n),Math.min(e.first+e.size,r),function(l){l.markedSpans&&((i||(i=t["spans_"+e.id]={}))[a]=l.markedSpans),++a})}function If(e){if(!e)return null;for(var t,n=0;n-1&&(ce(u)[B]=m[B],delete m[B])}}return r}function No(e,t,n,r){if(r){var i=e.anchor;if(n){var a=ye(t,i)<0;a!=ye(n,i)<0?(i=t,t=n):a!=ye(t,n)<0&&(t=n)}return new Ye(i,t)}else return new Ye(n||t,t)}function _i(e,t,n,r,i){i==null&&(i=e.cm&&(e.cm.display.shift||e.extend)),wt(e,new jt([No(e.sel.primary(),t,n,i)],0),r)}function yl(e,t,n){for(var r=[],i=e.cm&&(e.cm.display.shift||e.extend),a=0;a=t.ch:u.to>t.ch))){if(i&&(it(f,"beforeCursorEnter"),f.explicitlyCleared))if(a.markedSpans){--l;continue}else break;if(!f.atomic)continue;if(n){var B=f.find(r<0?1:-1),ee=void 0;if((r<0?A:m)&&(B=Tl(e,B,-r,B&&B.line==t.line?a:null)),B&&B.line==t.line&&(ee=ye(B,n))&&(r<0?ee<0:ee>0))return on(e,B,t,r,i)}var Y=f.find(r<0?-1:1);return(r<0?m:A)&&(Y=Tl(e,Y,r,Y.line==t.line?a:null)),Y?on(e,Y,t,r,i):null}}return t}function wi(e,t,n,r,i){var a=r||1,l=on(e,t,n,a,i)||!i&&on(e,t,n,a,!0)||on(e,t,n,-a,i)||!i&&on(e,t,n,-a,!0);return l||(e.cantEdit=!0,ne(e.first,0))}function Tl(e,t,n,r){return n<0&&t.ch==0?t.line>e.first?je(e,ne(t.line-1)):null:n>0&&t.ch==(r||Ae(e,t.line)).text.length?t.line=0;--i)El(e,{from:r[i].from,to:r[i].to,text:i?[""]:t.text,origin:t.origin});else El(e,t)}}function El(e,t){if(!(t.text.length==1&&t.text[0]==""&&ye(t.from,t.to)==0)){var n=Do(e,t);ml(e,t,n,e.cm?e.cm.curOp.id:NaN),On(e,t,n,io(e,t));var r=[];_r(e,function(i,a){!a&&Se(r,i.history)==-1&&(Dl(i.history,t),r.push(i.history)),On(i,t,null,io(i,t))})}}function Si(e,t,n){var r=e.cm&&e.cm.state.suppressEdits;if(!(r&&!n)){for(var i=e.history,a,l=e.sel,u=t=="undo"?i.done:i.undone,f=t=="undo"?i.undone:i.done,m=0;m=0;--Y){var ie=ee(Y);if(ie)return ie.v}}}}function zl(e,t){if(t!=0&&(e.first+=t,e.sel=new jt(He(e.sel.ranges,function(i){return new Ye(ne(i.anchor.line+t,i.anchor.ch),ne(i.head.line+t,i.head.ch))}),e.sel.primIndex),e.cm)){zt(e.cm,e.first,e.first-t,t);for(var n=e.cm.display,r=n.viewFrom;re.lastLine())){if(t.from.linea&&(t={from:t.from,to:ne(a,Ae(e,a).text.length),text:[t.text[0]],origin:t.origin}),t.removed=ir(e,t.from,t.to),n||(n=Do(e,t)),e.cm?Pf(e.cm,t,r):Fo(e,t,r),ki(e,n,ke),e.cantEdit&&wi(e,ne(e.firstLine(),0))&&(e.cantEdit=!1)}}function Pf(e,t,n){var r=e.doc,i=e.display,a=t.from,l=t.to,u=!1,f=a.line;e.options.lineWrapping||(f=x(Zt(Ae(r,a.line))),r.iter(f,l.line+1,function(Y){if(Y==i.maxLine)return u=!0,!0})),r.sel.contains(t.from,t.to)>-1&&Rt(e),Fo(r,t,n,Za(e)),e.options.lineWrapping||(r.iter(f,a.line+t.text.length,function(Y){var ie=li(Y);ie>i.maxLineLength&&(i.maxLine=Y,i.maxLineLength=ie,i.maxLineChanged=!0,u=!1)}),u&&(e.curOp.updateMaxLine=!0)),Lc(r,a.line),Fn(e,400);var m=t.text.length-(l.line-a.line)-1;t.full?zt(e):a.line==l.line&&t.text.length==1&&!dl(e.doc,t)?vr(e,a.line,"text"):zt(e,a.line,l.line+1,m);var A=It(e,"changes"),B=It(e,"change");if(B||A){var ee={from:a,to:l,text:t.text,removed:t.removed,origin:t.origin};B&&ht(e,"change",e,ee),A&&(e.curOp.changeObjs||(e.curOp.changeObjs=[])).push(ee)}e.display.selForContextMenu=null}function ln(e,t,n,r,i){var a;r||(r=n),ye(r,n)<0&&(a=[r,n],n=a[0],r=a[1]),typeof t=="string"&&(t=e.splitLines(t)),an(e,{from:n,to:r,text:t,origin:i})}function Ml(e,t,n,r){n1||!(this.children[0]instanceof Bn))){var u=[];this.collapse(u),this.children=[new Bn(u)],this.children[0].parent=this}},collapse:function(e){for(var t=0;t50){for(var l=i.lines.length%25+25,u=l;u10);e.parent.maybeSpill()}},iterN:function(e,t,n){for(var r=0;re.display.maxLineLength&&(e.display.maxLine=m,e.display.maxLineLength=A,e.display.maxLineChanged=!0)}r!=null&&e&&this.collapsed&&zt(e,r,i+1),this.lines.length=0,this.explicitlyCleared=!0,this.atomic&&this.doc.cantEdit&&(this.doc.cantEdit=!1,e&&wl(e.doc)),e&&ht(e,"markerCleared",e,this,r,i),t&&Ir(e),this.parent&&this.parent.clear()}},kr.prototype.find=function(e,t){e==null&&this.type=="bookmark"&&(e=1);for(var n,r,i=0;i0||l==0&&a.clearWhenEmpty!==!1)return a;if(a.replacedWith&&(a.collapsed=!0,a.widgetNode=K("span",[a.replacedWith],"CodeMirror-widget"),r.handleMouseEvents||a.widgetNode.setAttribute("cm-ignore-events","true"),r.insertLeft&&(a.widgetNode.insertLeft=!0)),a.collapsed){if(Sa(e,t.line,t,n,a)||t.line!=n.line&&Sa(e,n.line,t,n,a))throw new Error("Inserting collapsed marker partially overlapping an existing one");Ec()}a.addToHistory&&ml(e,{from:t,to:n,origin:"markText"},e.sel,NaN);var u=t.line,f=e.cm,m;if(e.iter(u,n.line+1,function(B){f&&a.collapsed&&!f.options.lineWrapping&&Zt(B)==f.display.maxLine&&(m=!0),a.collapsed&&u!=t.line&&Bt(B,0),Mc(B,new ni(a,u==t.line?t.ch:null,u==n.line?n.ch:null),e.cm&&e.cm.curOp),++u}),a.collapsed&&e.iter(t.line,n.line+1,function(B){mr(e,B)&&Bt(B,0)}),a.clearOnEnter&&Ie(a,"beforeCursorEnter",function(){return a.clear()}),a.readOnly&&(Cc(),(e.history.done.length||e.history.undone.length)&&e.clearHistory()),a.collapsed&&(a.id=++Fl,a.atomic=!0),f){if(m&&(f.curOp.updateMaxLine=!0),a.collapsed)zt(f,t.line,n.line+1);else if(a.className||a.startStyle||a.endStyle||a.css||a.attributes||a.title)for(var A=t.line;A<=n.line;A++)vr(f,A,"text");a.atomic&&wl(f.doc),ht(f,"markerAdded",f,a)}return a}var Hn=function(e,t){this.markers=e,this.primary=t;for(var n=0;n=0;f--)an(this,r[f]);u?_l(this,u):this.cm&&tn(this.cm)}),undo:mt(function(){Si(this,"undo")}),redo:mt(function(){Si(this,"redo")}),undoSelection:mt(function(){Si(this,"undo",!0)}),redoSelection:mt(function(){Si(this,"redo",!0)}),setExtending:function(e){this.extend=e},getExtending:function(){return this.extend},historySize:function(){for(var e=this.history,t=0,n=0,r=0;r=e.ch)&&t.push(i.marker.parent||i.marker)}return t},findMarks:function(e,t,n){e=je(this,e),t=je(this,t);var r=[],i=e.line;return this.iter(e.line,t.line+1,function(a){var l=a.markedSpans;if(l)for(var u=0;u=f.to||f.from==null&&i!=e.line||f.from!=null&&i==t.line&&f.from>=t.ch)&&(!n||n(f.marker))&&r.push(f.marker.parent||f.marker)}++i}),r},getAllMarks:function(){var e=[];return this.iter(function(t){var n=t.markedSpans;if(n)for(var r=0;re)return t=e,!0;e-=a,++n}),je(this,ne(n,t))},indexFromPos:function(e){e=je(this,e);var t=e.ch;if(e.linet&&(t=e.from),e.to!=null&&e.to-1){t.state.draggingText(e),setTimeout(function(){return t.display.input.focus()},20);return}try{var A=e.dataTransfer.getData("Text");if(A){var B;if(t.state.draggingText&&!t.state.draggingText.copy&&(B=t.listSelections()),ki(t.doc,yr(n,n)),B)for(var ee=0;ee=0;u--)ln(e.doc,"",r[u].from,r[u].to,"+delete");tn(e)})}function Po(e,t,n){var r=se(e.text,t+n,n);return r<0||r>e.text.length?null:r}function Bo(e,t,n){var r=Po(e,t.ch,n);return r==null?null:new ne(t.line,r,n<0?"after":"before")}function jo(e,t,n,r,i){if(e){t.doc.direction=="rtl"&&(i=-i);var a=Pe(n,t.doc.direction);if(a){var l=i<0?ce(a):a[0],u=i<0==(l.level==1),f=u?"after":"before",m;if(l.level>0||t.doc.direction=="rtl"){var A=Qr(t,n);m=i<0?n.text.length-1:0;var B=tr(t,A,m).top;m=De(function(ee){return tr(t,A,ee).top==B},i<0==(l.level==1)?l.from:l.to-1,m),f=="before"&&(m=Po(n,m,1))}else m=i<0?l.to:l.from;return new ne(r,m,f)}}return new ne(r,i<0?n.text.length:0,i<0?"before":"after")}function Vf(e,t,n,r){var i=Pe(t,e.doc.direction);if(!i)return Bo(t,n,r);n.ch>=t.text.length?(n.ch=t.text.length,n.sticky="before"):n.ch<=0&&(n.ch=0,n.sticky="after");var a=Pt(i,n.ch,n.sticky),l=i[a];if(e.doc.direction=="ltr"&&l.level%2==0&&(r>0?l.to>n.ch:l.from=l.from&&ee>=A.begin)){var Y=B?"before":"after";return new ne(n.line,ee,Y)}}var ie=function(ve,_e,be){for(var Ce=function(Ve,vt){return vt?new ne(n.line,u(Ve,1),"before"):new ne(n.line,Ve,"after")};ve>=0&&ve0==(Ne.level!=1),$e=Fe?be.begin:u(be.end,-1);if(Ne.from<=$e&&$e0?A.end:u(A.begin,-1);return me!=null&&!(r>0&&me==t.text.length)&&(ue=ie(r>0?0:i.length-1,r,m(me)),ue)?ue:null}var $n={selectAll:Ll,singleSelection:function(e){return e.setSelection(e.getCursor("anchor"),e.getCursor("head"),ke)},killLine:function(e){return cn(e,function(t){if(t.empty()){var n=Ae(e.doc,t.head.line).text.length;return t.head.ch==n&&t.head.line0)i=new ne(i.line,i.ch+1),e.replaceRange(a.charAt(i.ch-1)+a.charAt(i.ch-2),ne(i.line,i.ch-2),i,"+transpose");else if(i.line>e.doc.first){var l=Ae(e.doc,i.line-1).text;l&&(i=new ne(i.line,1),e.replaceRange(a.charAt(0)+e.doc.lineSeparator()+l.charAt(l.length-1),ne(i.line-1,l.length-1),i,"+transpose"))}}n.push(new Ye(i,i))}e.setSelections(n)})},newlineAndIndent:function(e){return Nt(e,function(){for(var t=e.listSelections(),n=t.length-1;n>=0;n--)e.replaceRange(e.doc.lineSeparator(),t[n].anchor,t[n].head,"+input");t=e.listSelections();for(var r=0;re&&ye(t,this.pos)==0&&n==this.button};var Gn,Zn;function od(e,t){var n=+new Date;return Zn&&Zn.compare(n,e,t)?(Gn=Zn=null,"triple"):Gn&&Gn.compare(n,e,t)?(Zn=new Ho(n,e,t),Gn=null,"double"):(Gn=new Ho(n,e,t),Zn=null,"single")}function Yl(e){var t=this,n=t.display;if(!(ot(t,e)||n.activeTouch&&n.input.supportsTouch())){if(n.input.ensurePolled(),n.shift=e.shiftKey,lr(n,e)){g||(n.scroller.draggable=!1,setTimeout(function(){return n.scroller.draggable=!0},100));return}if(!Wo(t,e)){var r=Mr(t,e),i=Ut(e),a=r?od(r,i):"single";pe(t).focus(),i==1&&t.state.selectingText&&t.state.selectingText(e),!(r&&ad(t,i,r,a,e))&&(i==1?r?sd(t,r,a,e):yn(e)==n.scroller&&kt(e):i==2?(r&&_i(t.doc,r),setTimeout(function(){return n.input.focus()},20)):i==3&&(I?t.display.input.onContextMenu(e):wo(t)))}}}function ad(e,t,n,r,i){var a="Click";return r=="double"?a="Double"+a:r=="triple"&&(a="Triple"+a),a=(t==1?"Left":t==2?"Middle":"Right")+a,Kn(e,Rl(a,i),i,function(l){if(typeof l=="string"&&(l=$n[l]),!l)return!1;var u=!1;try{e.isReadOnly()&&(e.state.suppressEdits=!0),u=l(e,n)!=Ze}finally{e.state.suppressEdits=!1}return u})}function ld(e,t,n){var r=e.getOption("configureMouse"),i=r?r(e,t,n):{};if(i.unit==null){var a=G?n.shiftKey&&n.metaKey:n.altKey;i.unit=a?"rectangle":t=="single"?"char":t=="double"?"word":"line"}return(i.extend==null||e.doc.extend)&&(i.extend=e.doc.extend||n.shiftKey),i.addNew==null&&(i.addNew=O?n.metaKey:n.ctrlKey),i.moveOnDrag==null&&(i.moveOnDrag=!(O?n.altKey:n.ctrlKey)),i}function sd(e,t,n,r){s?setTimeout(Ee(Qa,e),0):e.curOp.focus=R(de(e));var i=ld(e,n,r),a=e.doc.sel,l;e.options.dragDrop&&eo&&!e.isReadOnly()&&n=="single"&&(l=a.contains(t))>-1&&(ye((l=a.ranges[l]).from(),t)<0||t.xRel>0)&&(ye(l.to(),t)>0||t.xRel<0)?ud(e,r,t,i):cd(e,r,t,i)}function ud(e,t,n,r){var i=e.display,a=!1,l=gt(e,function(m){g&&(i.scroller.draggable=!1),e.state.draggingText=!1,e.state.delayingBlurEvent&&(e.hasFocus()?e.state.delayingBlurEvent=!1:wo(e)),_t(i.wrapper.ownerDocument,"mouseup",l),_t(i.wrapper.ownerDocument,"mousemove",u),_t(i.scroller,"dragstart",f),_t(i.scroller,"drop",l),a||(kt(m),r.addNew||_i(e.doc,n,null,null,r.extend),g&&!k||s&&h==9?setTimeout(function(){i.wrapper.ownerDocument.body.focus({preventScroll:!0}),i.input.focus()},20):i.input.focus())}),u=function(m){a=a||Math.abs(t.clientX-m.clientX)+Math.abs(t.clientY-m.clientY)>=10},f=function(){return a=!0};g&&(i.scroller.draggable=!0),e.state.draggingText=l,l.copy=!r.moveOnDrag,Ie(i.wrapper.ownerDocument,"mouseup",l),Ie(i.wrapper.ownerDocument,"mousemove",u),Ie(i.scroller,"dragstart",f),Ie(i.scroller,"drop",l),e.state.delayingBlurEvent=!0,setTimeout(function(){return i.input.focus()},20),i.scroller.dragDrop&&i.scroller.dragDrop()}function Ql(e,t,n){if(n=="char")return new Ye(t,t);if(n=="word")return e.findWordAt(t);if(n=="line")return new Ye(ne(t.line,0),je(e.doc,ne(t.line+1,0)));var r=n(e,t);return new Ye(r.from,r.to)}function cd(e,t,n,r){s&&wo(e);var i=e.display,a=e.doc;kt(t);var l,u,f=a.sel,m=f.ranges;if(r.addNew&&!r.extend?(u=a.sel.contains(n),u>-1?l=m[u]:l=new Ye(n,n)):(l=a.sel.primary(),u=a.sel.primIndex),r.unit=="rectangle")r.addNew||(l=new Ye(n,n)),n=Mr(e,t,!0,!0),u=-1;else{var A=Ql(e,n,r.unit);r.extend?l=No(l,A.anchor,A.head,r.extend):l=A}r.addNew?u==-1?(u=m.length,wt(a,Yt(e,m.concat([l]),u),{scroll:!1,origin:"*mouse"})):m.length>1&&m[u].empty()&&r.unit=="char"&&!r.extend?(wt(a,Yt(e,m.slice(0,u).concat(m.slice(u+1)),0),{scroll:!1,origin:"*mouse"}),f=a.sel):Oo(a,u,l,Je):(u=0,wt(a,new jt([l],0),Je),f=a.sel);var B=n;function ee(be){if(ye(B,be)!=0)if(B=be,r.unit=="rectangle"){for(var Ce=[],Ne=e.options.tabSize,Fe=Oe(Ae(a,n.line).text,n.ch,Ne),$e=Oe(Ae(a,be.line).text,be.ch,Ne),Ve=Math.min(Fe,$e),vt=Math.max(Fe,$e),rt=Math.min(n.line,be.line),Ot=Math.min(e.lastLine(),Math.max(n.line,be.line));rt<=Ot;rt++){var At=Ae(a,rt).text,ut=Ge(At,Ve,Ne);Ve==vt?Ce.push(new Ye(ne(rt,ut),ne(rt,ut))):At.length>ut&&Ce.push(new Ye(ne(rt,ut),ne(rt,Ge(At,vt,Ne))))}Ce.length||Ce.push(new Ye(n,n)),wt(a,Yt(e,f.ranges.slice(0,u).concat(Ce),u),{origin:"*mouse",scroll:!1}),e.scrollIntoView(be)}else{var Dt=l,yt=Ql(e,be,r.unit),ft=Dt.anchor,ct;ye(yt.anchor,ft)>0?(ct=yt.head,ft=Zr(Dt.from(),yt.anchor)):(ct=yt.anchor,ft=Et(Dt.to(),yt.head));var lt=f.ranges.slice(0);lt[u]=fd(e,new Ye(je(a,ft),ct)),wt(a,Yt(e,lt,u),Je)}}var Y=i.wrapper.getBoundingClientRect(),ie=0;function ue(be){var Ce=++ie,Ne=Mr(e,be,!0,r.unit=="rectangle");if(Ne)if(ye(Ne,B)!=0){e.curOp.focus=R(de(e)),ee(Ne);var Fe=gi(i,a);(Ne.line>=Fe.to||Ne.lineY.bottom?20:0;$e&&setTimeout(gt(e,function(){ie==Ce&&(i.scroller.scrollTop+=$e,ue(be))}),50)}}function me(be){e.state.selectingText=!1,ie=1/0,be&&(kt(be),i.input.focus()),_t(i.wrapper.ownerDocument,"mousemove",ve),_t(i.wrapper.ownerDocument,"mouseup",_e),a.history.lastSelOrigin=null}var ve=gt(e,function(be){be.buttons===0||!Ut(be)?me(be):ue(be)}),_e=gt(e,me);e.state.selectingText=_e,Ie(i.wrapper.ownerDocument,"mousemove",ve),Ie(i.wrapper.ownerDocument,"mouseup",_e)}function fd(e,t){var n=t.anchor,r=t.head,i=Ae(e.doc,n.line);if(ye(n,r)==0&&n.sticky==r.sticky)return t;var a=Pe(i);if(!a)return t;var l=Pt(a,n.ch,n.sticky),u=a[l];if(u.from!=n.ch&&u.to!=n.ch)return t;var f=l+(u.from==n.ch==(u.level!=1)?0:1);if(f==0||f==a.length)return t;var m;if(r.line!=n.line)m=(r.line-n.line)*(e.doc.direction=="ltr"?1:-1)>0;else{var A=Pt(a,r.ch,r.sticky),B=A-l||(r.ch-n.ch)*(u.level==1?-1:1);A==f-1||A==f?m=B<0:m=B>0}var ee=a[f+(m?-1:0)],Y=m==(ee.level==1),ie=Y?ee.from:ee.to,ue=Y?"after":"before";return n.ch==ie&&n.sticky==ue?t:new Ye(new ne(n.line,ie,ue),r)}function Vl(e,t,n,r){var i,a;if(t.touches)i=t.touches[0].clientX,a=t.touches[0].clientY;else try{i=t.clientX,a=t.clientY}catch{return!1}if(i>=Math.floor(e.display.gutters.getBoundingClientRect().right))return!1;r&&kt(t);var l=e.display,u=l.lineDiv.getBoundingClientRect();if(a>u.bottom||!It(e,n))return Ct(t);a-=u.top-l.viewOffset;for(var f=0;f=i){var A=P(e.doc,a),B=e.display.gutterSpecs[f];return it(e,n,e,A,B.className,t),Ct(t)}}}function Wo(e,t){return Vl(e,t,"gutterClick",!0)}function Jl(e,t){lr(e.display,t)||dd(e,t)||ot(e,t,"contextmenu")||I||e.display.input.onContextMenu(t)}function dd(e,t){return It(e,"gutterContextMenu")?Vl(e,t,"gutterContextMenu",!1):!1}function es(e){e.display.wrapper.className=e.display.wrapper.className.replace(/\s*cm-s-\S+/g,"")+e.options.theme.replace(/(^|\s)\s*/g," cm-s-"),En(e)}var fn={toString:function(){return"CodeMirror.Init"}},ts={},Ei={};function pd(e){var t=e.optionHandlers;function n(r,i,a,l){e.defaults[r]=i,a&&(t[r]=l?function(u,f,m){m!=fn&&a(u,f,m)}:a)}e.defineOption=n,e.Init=fn,n("value","",function(r,i){return r.setValue(i)},!0),n("mode",null,function(r,i){r.doc.modeOption=i,qo(r)},!0),n("indentUnit",2,qo,!0),n("indentWithTabs",!1),n("smartIndent",!0),n("tabSize",4,function(r){Nn(r),En(r),zt(r)},!0),n("lineSeparator",null,function(r,i){if(r.doc.lineSep=i,!!i){var a=[],l=r.doc.first;r.doc.iter(function(f){for(var m=0;;){var A=f.text.indexOf(i,m);if(A==-1)break;m=A+i.length,a.push(ne(l,A))}l++});for(var u=a.length-1;u>=0;u--)ln(r.doc,i,a[u],ne(a[u].line,a[u].ch+i.length))}}),n("specialChars",/[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b\u200e\u200f\u2028\u2029\u202d\u202e\u2066\u2067\u2069\ufeff\ufff9-\ufffc]/g,function(r,i,a){r.state.specialChars=new RegExp(i.source+(i.test(" ")?"":"| "),"g"),a!=fn&&r.refresh()}),n("specialCharPlaceholder",Rc,function(r){return r.refresh()},!0),n("electricChars",!0),n("inputStyle",E?"contenteditable":"textarea",function(){throw new Error("inputStyle can not (yet) be changed in a running editor")},!0),n("spellcheck",!1,function(r,i){return r.getInputField().spellcheck=i},!0),n("autocorrect",!1,function(r,i){return r.getInputField().autocorrect=i},!0),n("autocapitalize",!1,function(r,i){return r.getInputField().autocapitalize=i},!0),n("rtlMoveVisually",!J),n("wholeLineUpdateBefore",!0),n("theme","default",function(r){es(r),In(r)},!0),n("keyMap","default",function(r,i,a){var l=Li(i),u=a!=fn&&Li(a);u&&u.detach&&u.detach(r,l),l.attach&&l.attach(r,u||null)}),n("extraKeys",null),n("configureMouse",null),n("lineWrapping",!1,gd,!0),n("gutters",[],function(r,i){r.display.gutterSpecs=Ao(i,r.options.lineNumbers),In(r)},!0),n("fixedGutter",!0,function(r,i){r.display.gutters.style.left=i?yo(r.display)+"px":"0",r.refresh()},!0),n("coverGutterNextToScrollbar",!1,function(r){return rn(r)},!0),n("scrollbarStyle","native",function(r){nl(r),rn(r),r.display.scrollbars.setScrollTop(r.doc.scrollTop),r.display.scrollbars.setScrollLeft(r.doc.scrollLeft)},!0),n("lineNumbers",!1,function(r,i){r.display.gutterSpecs=Ao(r.options.gutters,i),In(r)},!0),n("firstLineNumber",1,In,!0),n("lineNumberFormatter",function(r){return r},In,!0),n("showCursorWhenSelecting",!1,zn,!0),n("resetSelectionOnContextMenu",!0),n("lineWiseCopyCut",!0),n("pasteLinesPerSelection",!0),n("selectionsMayTouch",!1),n("readOnly",!1,function(r,i){i=="nocursor"&&(en(r),r.display.input.blur()),r.display.input.readOnlyChanged(i)}),n("screenReaderLabel",null,function(r,i){i=i===""?null:i,r.display.input.screenReaderLabelChanged(i)}),n("disableInput",!1,function(r,i){i||r.display.input.reset()},!0),n("dragDrop",!0,hd),n("allowDropFileTypes",null),n("cursorBlinkRate",530),n("cursorScrollMargin",0),n("cursorHeight",1,zn,!0),n("singleCursorHeightPerLine",!0,zn,!0),n("workTime",100),n("workDelay",100),n("flattenSpans",!0,Nn,!0),n("addModeClass",!1,Nn,!0),n("pollInterval",100),n("undoDepth",200,function(r,i){return r.doc.history.undoDepth=i}),n("historyEventDelay",1250),n("viewportMargin",10,function(r){return r.refresh()},!0),n("maxHighlightLength",1e4,Nn,!0),n("moveInputWithCursor",!0,function(r,i){i||r.display.input.resetPosition()}),n("tabindex",null,function(r,i){return r.display.input.getField().tabIndex=i||""}),n("autofocus",null),n("direction","ltr",function(r,i){return r.doc.setDirection(i)},!0),n("phrases",null)}function hd(e,t,n){var r=n&&n!=fn;if(!t!=!r){var i=e.display.dragFunctions,a=t?Ie:_t;a(e.display.scroller,"dragstart",i.start),a(e.display.scroller,"dragenter",i.enter),a(e.display.scroller,"dragover",i.over),a(e.display.scroller,"dragleave",i.leave),a(e.display.scroller,"drop",i.drop)}}function gd(e){e.options.lineWrapping?(le(e.display.wrapper,"CodeMirror-wrap"),e.display.sizer.style.minWidth="",e.display.sizerWidth=null):(Q(e.display.wrapper,"CodeMirror-wrap"),so(e)),xo(e),zt(e),En(e),setTimeout(function(){return rn(e)},100)}function tt(e,t){var n=this;if(!(this instanceof tt))return new tt(e,t);this.options=t=t?ge(t):{},ge(ts,t,!1);var r=t.value;typeof r=="string"?r=new Mt(r,t.mode,null,t.lineSeparator,t.direction):t.mode&&(r.modeOption=t.mode),this.doc=r;var i=new tt.inputStyles[t.inputStyle](this),a=this.display=new Ef(e,r,i,t);a.wrapper.CodeMirror=this,es(this),t.lineWrapping&&(this.display.wrapper.className+=" CodeMirror-wrap"),nl(this),this.state={keyMaps:[],overlays:[],modeGen:0,overwrite:!1,delayingBlurEvent:!1,focused:!1,suppressEdits:!1,pasteIncoming:-1,cutIncoming:-1,selectingText:!1,draggingText:!1,highlight:new qe,keySeq:null,specialChars:null},t.autofocus&&!E&&a.input.focus(),s&&h<11&&setTimeout(function(){return n.display.input.reset(!0)},20),md(this),Gf(),Fr(this),this.curOp.forceUpdate=!0,pl(this,r),t.autofocus&&!E||this.hasFocus()?setTimeout(function(){n.hasFocus()&&!n.state.focused&&So(n)},20):en(this);for(var l in Ei)Ei.hasOwnProperty(l)&&Ei[l](this,t[l],fn);al(this),t.finishInit&&t.finishInit(this);for(var u=0;u400}Ie(t.scroller,"touchstart",function(f){if(!ot(e,f)&&!a(f)&&!Wo(e,f)){t.input.ensurePolled(),clearTimeout(n);var m=+new Date;t.activeTouch={start:m,moved:!1,prev:m-r.end<=300?r:null},f.touches.length==1&&(t.activeTouch.left=f.touches[0].pageX,t.activeTouch.top=f.touches[0].pageY)}}),Ie(t.scroller,"touchmove",function(){t.activeTouch&&(t.activeTouch.moved=!0)}),Ie(t.scroller,"touchend",function(f){var m=t.activeTouch;if(m&&!lr(t,f)&&m.left!=null&&!m.moved&&new Date-m.start<300){var A=e.coordsChar(t.activeTouch,"page"),B;!m.prev||l(m,m.prev)?B=new Ye(A,A):!m.prev.prev||l(m,m.prev.prev)?B=e.findWordAt(A):B=new Ye(ne(A.line,0),je(e.doc,ne(A.line+1,0))),e.setSelection(B.anchor,B.head),e.focus(),kt(f)}i()}),Ie(t.scroller,"touchcancel",i),Ie(t.scroller,"scroll",function(){t.scroller.clientHeight&&(An(e,t.scroller.scrollTop),Dr(e,t.scroller.scrollLeft,!0),it(e,"scroll",e))}),Ie(t.scroller,"mousewheel",function(f){return ul(e,f)}),Ie(t.scroller,"DOMMouseScroll",function(f){return ul(e,f)}),Ie(t.wrapper,"scroll",function(){return t.wrapper.scrollTop=t.wrapper.scrollLeft=0}),t.dragFunctions={enter:function(f){ot(e,f)||dr(f)},over:function(f){ot(e,f)||(Kf(e,f),dr(f))},start:function(f){return $f(e,f)},drop:gt(e,Uf),leave:function(f){ot(e,f)||Ol(e)}};var u=t.input.getField();Ie(u,"keyup",function(f){return Zl.call(e,f)}),Ie(u,"keydown",gt(e,Gl)),Ie(u,"keypress",gt(e,Xl)),Ie(u,"focus",function(f){return So(e,f)}),Ie(u,"blur",function(f){return en(e,f)})}var Uo=[];tt.defineInitHook=function(e){return Uo.push(e)};function Xn(e,t,n,r){var i=e.doc,a;n==null&&(n="add"),n=="smart"&&(i.mode.indent?a=wn(e,t).state:n="prev");var l=e.options.tabSize,u=Ae(i,t),f=Oe(u.text,null,l);u.stateAfter&&(u.stateAfter=null);var m=u.text.match(/^\s*/)[0],A;if(!r&&!/\S/.test(u.text))A=0,n="not";else if(n=="smart"&&(A=i.mode.indent(a,u.text.slice(m.length),u.text),A==Ze||A>150)){if(!r)return;n="prev"}n=="prev"?t>i.first?A=Oe(Ae(i,t-1).text,null,l):A=0:n=="add"?A=f+e.options.indentUnit:n=="subtract"?A=f-e.options.indentUnit:typeof n=="number"&&(A=f+n),A=Math.max(0,A);var B="",ee=0;if(e.options.indentWithTabs)for(var Y=Math.floor(A/l);Y;--Y)ee+=l,B+=" ";if(eel,f=Ht(t),m=null;if(u&&r.ranges.length>1)if(Qt&&Qt.text.join(` +`)==t){if(r.ranges.length%Qt.text.length==0){m=[];for(var A=0;A=0;ee--){var Y=r.ranges[ee],ie=Y.from(),ue=Y.to();Y.empty()&&(n&&n>0?ie=ne(ie.line,ie.ch-n):e.state.overwrite&&!u?ue=ne(ue.line,Math.min(Ae(a,ue.line).text.length,ue.ch+ce(f).length)):u&&Qt&&Qt.lineWise&&Qt.text.join(` +`)==f.join(` +`)&&(ie=ue=ne(ie.line,0)));var me={from:ie,to:ue,text:m?m[ee%m.length]:f,origin:i||(u?"paste":e.state.cutIncoming>l?"cut":"+input")};an(e.doc,me),ht(e,"inputRead",e,me)}t&&!u&&ns(e,t),tn(e),e.curOp.updateInput<2&&(e.curOp.updateInput=B),e.curOp.typing=!0,e.state.pasteIncoming=e.state.cutIncoming=-1}function rs(e,t){var n=e.clipboardData&&e.clipboardData.getData("Text");if(n)return e.preventDefault(),!t.isReadOnly()&&!t.options.disableInput&&t.hasFocus()&&Nt(t,function(){return $o(t,n,0,null,"paste")}),!0}function ns(e,t){if(!(!e.options.electricChars||!e.options.smartIndent))for(var n=e.doc.sel,r=n.ranges.length-1;r>=0;r--){var i=n.ranges[r];if(!(i.head.ch>100||r&&n.ranges[r-1].head.line==i.head.line)){var a=e.getModeAt(i.head),l=!1;if(a.electricChars){for(var u=0;u-1){l=Xn(e,i.head.line,"smart");break}}else a.electricInput&&a.electricInput.test(Ae(e.doc,i.head.line).text.slice(0,i.head.ch))&&(l=Xn(e,i.head.line,"smart"));l&&ht(e,"electricInput",e,i.head.line)}}}function is(e){for(var t=[],n=[],r=0;ra&&(Xn(this,u.head.line,r,!0),a=u.head.line,l==this.doc.sel.primIndex&&tn(this));else{var f=u.from(),m=u.to(),A=Math.max(a,f.line);a=Math.min(this.lastLine(),m.line-(m.ch?0:1))+1;for(var B=A;B0&&Oo(this.doc,l,new Ye(f,ee[l].to()),ke)}}}),getTokenAt:function(r,i){return ga(this,r,i)},getLineTokens:function(r,i){return ga(this,ne(r),i,!0)},getTokenTypeAt:function(r){r=je(this.doc,r);var i=da(this,Ae(this.doc,r.line)),a=0,l=(i.length-1)/2,u=r.ch,f;if(u==0)f=i[2];else for(;;){var m=a+l>>1;if((m?i[m*2-1]:0)>=u)l=m;else if(i[m*2+1]f&&(r=f,l=!0),u=Ae(this.doc,r)}else u=r;return ci(this,u,{top:0,left:0},i||"page",a||l).top+(l?this.doc.height-ar(u):0)},defaultTextHeight:function(){return Vr(this.display)},defaultCharWidth:function(){return Jr(this.display)},getViewport:function(){return{from:this.display.viewFrom,to:this.display.viewTo}},addWidget:function(r,i,a,l,u){var f=this.display;r=Xt(this,je(this.doc,r));var m=r.bottom,A=r.left;if(i.style.position="absolute",i.setAttribute("cm-ignore-events","true"),this.display.input.setUneditable(i),f.sizer.appendChild(i),l=="over")m=r.top;else if(l=="above"||l=="near"){var B=Math.max(f.wrapper.clientHeight,this.doc.height),ee=Math.max(f.sizer.clientWidth,f.lineSpace.clientWidth);(l=="above"||r.bottom+i.offsetHeight>B)&&r.top>i.offsetHeight?m=r.top-i.offsetHeight:r.bottom+i.offsetHeight<=B&&(m=r.bottom),A+i.offsetWidth>ee&&(A=ee-i.offsetWidth)}i.style.top=m+"px",i.style.left=i.style.right="",u=="right"?(A=f.sizer.clientWidth-i.offsetWidth,i.style.right="0px"):(u=="left"?A=0:u=="middle"&&(A=(f.sizer.clientWidth-i.offsetWidth)/2),i.style.left=A+"px"),a&&hf(this,{left:A,top:m,right:A+i.offsetWidth,bottom:m+i.offsetHeight})},triggerOnKeyDown:Tt(Gl),triggerOnKeyPress:Tt(Xl),triggerOnKeyUp:Zl,triggerOnMouseDown:Tt(Yl),execCommand:function(r){if($n.hasOwnProperty(r))return $n[r].call(null,this)},triggerElectric:Tt(function(r){ns(this,r)}),findPosH:function(r,i,a,l){var u=1;i<0&&(u=-1,i=-i);for(var f=je(this.doc,r),m=0;m0&&A(a.charAt(l-1));)--l;for(;u.5||this.options.lineWrapping)&&xo(this),it(this,"refresh",this)}),swapDoc:Tt(function(r){var i=this.doc;return i.cm=null,this.state.selectingText&&this.state.selectingText(),pl(this,r),En(this),this.display.input.reset(),Mn(this,r.scrollLeft,r.scrollTop),this.curOp.forceScroll=!0,ht(this,"swapDoc",this,i),i}),phrase:function(r){var i=this.options.phrases;return i&&Object.prototype.hasOwnProperty.call(i,r)?i[r]:r},getInputField:function(){return this.display.input.getField()},getWrapperElement:function(){return this.display.wrapper},getScrollerElement:function(){return this.display.scroller},getGutterElement:function(){return this.display.gutters}},Wt(e),e.registerHelper=function(r,i,a){n.hasOwnProperty(r)||(n[r]=e[r]={_global:[]}),n[r][i]=a},e.registerGlobalHelper=function(r,i,a,l){e.registerHelper(r,i,l),n[r]._global.push({pred:a,val:l})}}function Go(e,t,n,r,i){var a=t,l=n,u=Ae(e,t.line),f=i&&e.direction=="rtl"?-n:n;function m(){var _e=t.line+f;return _e=e.first+e.size?!1:(t=new ne(_e,t.ch,t.sticky),u=Ae(e,_e))}function A(_e){var be;if(r=="codepoint"){var Ce=u.text.charCodeAt(t.ch+(n>0?0:-1));if(isNaN(Ce))be=null;else{var Ne=n>0?Ce>=55296&&Ce<56320:Ce>=56320&&Ce<57343;be=new ne(t.line,Math.max(0,Math.min(u.text.length,t.ch+n*(Ne?2:1))),-n)}}else i?be=Vf(e.cm,u,t,n):be=Bo(u,t,n);if(be==null)if(!_e&&m())t=jo(i,e.cm,u,t.line,f);else return!1;else t=be;return!0}if(r=="char"||r=="codepoint")A();else if(r=="column")A(!0);else if(r=="word"||r=="group")for(var B=null,ee=r=="group",Y=e.cm&&e.cm.getHelper(t,"wordChars"),ie=!0;!(n<0&&!A(!ie));ie=!1){var ue=u.text.charAt(t.ch)||` +`,me=Me(ue,Y)?"w":ee&&ue==` +`?"n":!ee||/\s/.test(ue)?null:"p";if(ee&&!ie&&!me&&(me="s"),B&&B!=me){n<0&&(n=1,A(),t.sticky="after");break}if(me&&(B=me),n>0&&!A(!ie))break}var ve=wi(e,t,a,l,!0);return Xe(a,ve)&&(ve.hitSide=!0),ve}function as(e,t,n,r){var i=e.doc,a=t.left,l;if(r=="page"){var u=Math.min(e.display.wrapper.clientHeight,pe(e).innerHeight||i(e).documentElement.clientHeight),f=Math.max(u-.5*Vr(e.display),3);l=(n>0?t.bottom:t.top)+n*f}else r=="line"&&(l=n>0?t.bottom+3:t.top-3);for(var m;m=mo(e,a,l),!!m.outside;){if(n<0?l<=0:l>=i.height){m.hitSide=!0;break}l+=n*5}return m}var Qe=function(e){this.cm=e,this.lastAnchorNode=this.lastAnchorOffset=this.lastFocusNode=this.lastFocusOffset=null,this.polling=new qe,this.composing=null,this.gracePeriod=!1,this.readDOMTimeout=null};Qe.prototype.init=function(e){var t=this,n=this,r=n.cm,i=n.div=e.lineDiv;i.contentEditable=!0,Ko(i,r.options.spellcheck,r.options.autocorrect,r.options.autocapitalize);function a(u){for(var f=u.target;f;f=f.parentNode){if(f==i)return!0;if(/\bCodeMirror-(?:line)?widget\b/.test(f.className))break}return!1}Ie(i,"paste",function(u){!a(u)||ot(r,u)||rs(u,r)||h<=11&&setTimeout(gt(r,function(){return t.updateFromDOM()}),20)}),Ie(i,"compositionstart",function(u){t.composing={data:u.data,done:!1}}),Ie(i,"compositionupdate",function(u){t.composing||(t.composing={data:u.data,done:!1})}),Ie(i,"compositionend",function(u){t.composing&&(u.data!=t.composing.data&&t.readFromDOMSoon(),t.composing.done=!0)}),Ie(i,"touchstart",function(){return n.forceCompositionEnd()}),Ie(i,"input",function(){t.composing||t.readFromDOMSoon()});function l(u){if(!(!a(u)||ot(r,u))){if(r.somethingSelected())zi({lineWise:!1,text:r.getSelections()}),u.type=="cut"&&r.replaceSelection("",null,"cut");else if(r.options.lineWiseCopyCut){var f=is(r);zi({lineWise:!0,text:f.text}),u.type=="cut"&&r.operation(function(){r.setSelections(f.ranges,0,ke),r.replaceSelection("",null,"cut")})}else return;if(u.clipboardData){u.clipboardData.clearData();var m=Qt.text.join(` +`);if(u.clipboardData.setData("Text",m),u.clipboardData.getData("Text")==m){u.preventDefault();return}}var A=os(),B=A.firstChild;Ko(B),r.display.lineSpace.insertBefore(A,r.display.lineSpace.firstChild),B.value=Qt.text.join(` +`);var ee=R(ze(i));F(B),setTimeout(function(){r.display.lineSpace.removeChild(A),ee.focus(),ee==i&&n.showPrimarySelection()},50)}}Ie(i,"copy",l),Ie(i,"cut",l)},Qe.prototype.screenReaderLabelChanged=function(e){e?this.div.setAttribute("aria-label",e):this.div.removeAttribute("aria-label")},Qe.prototype.prepareSelection=function(){var e=Ya(this.cm,!1);return e.focus=R(ze(this.div))==this.div,e},Qe.prototype.showSelection=function(e,t){!e||!this.cm.display.view.length||((e.focus||t)&&this.showPrimarySelection(),this.showMultipleSelections(e))},Qe.prototype.getSelection=function(){return this.cm.display.wrapper.ownerDocument.getSelection()},Qe.prototype.showPrimarySelection=function(){var e=this.getSelection(),t=this.cm,n=t.doc.sel.primary(),r=n.from(),i=n.to();if(t.display.viewTo==t.display.viewFrom||r.line>=t.display.viewTo||i.line=t.display.viewFrom&&ls(t,r)||{node:u[0].measure.map[2],offset:0},m=i.linee.firstLine()&&(r=ne(r.line-1,Ae(e.doc,r.line-1).length)),i.ch==Ae(e.doc,i.line).text.length&&i.linet.viewTo-1)return!1;var a,l,u;r.line==t.viewFrom||(a=Ar(e,r.line))==0?(l=x(t.view[0].line),u=t.view[0].node):(l=x(t.view[a].line),u=t.view[a-1].node.nextSibling);var f=Ar(e,i.line),m,A;if(f==t.view.length-1?(m=t.viewTo-1,A=t.lineDiv.lastChild):(m=x(t.view[f+1].line)-1,A=t.view[f+1].node.previousSibling),!u)return!1;for(var B=e.doc.splitLines(yd(e,u,A,l,m)),ee=ir(e.doc,ne(l,0),ne(m,Ae(e.doc,m).text.length));B.length>1&&ee.length>1;)if(ce(B)==ce(ee))B.pop(),ee.pop(),m--;else if(B[0]==ee[0])B.shift(),ee.shift(),l++;else break;for(var Y=0,ie=0,ue=B[0],me=ee[0],ve=Math.min(ue.length,me.length);Yr.ch&&_e.charCodeAt(_e.length-ie-1)==be.charCodeAt(be.length-ie-1);)Y--,ie++;B[B.length-1]=_e.slice(0,_e.length-ie).replace(/^\u200b+/,""),B[0]=B[0].slice(Y).replace(/\u200b+$/,"");var Ne=ne(l,Y),Fe=ne(m,ee.length?ce(ee).length-ie:0);if(B.length>1||B[0]||ye(Ne,Fe))return ln(e.doc,B,Ne,Fe,"+input"),!0},Qe.prototype.ensurePolled=function(){this.forceCompositionEnd()},Qe.prototype.reset=function(){this.forceCompositionEnd()},Qe.prototype.forceCompositionEnd=function(){this.composing&&(clearTimeout(this.readDOMTimeout),this.composing=null,this.updateFromDOM(),this.div.blur(),this.div.focus())},Qe.prototype.readFromDOMSoon=function(){var e=this;this.readDOMTimeout==null&&(this.readDOMTimeout=setTimeout(function(){if(e.readDOMTimeout=null,e.composing)if(e.composing.done)e.composing=null;else return;e.updateFromDOM()},80))},Qe.prototype.updateFromDOM=function(){var e=this;(this.cm.isReadOnly()||!this.pollContent())&&Nt(this.cm,function(){return zt(e.cm)})},Qe.prototype.setUneditable=function(e){e.contentEditable="false"},Qe.prototype.onKeyPress=function(e){e.charCode==0||this.composing||(e.preventDefault(),this.cm.isReadOnly()||gt(this.cm,$o)(this.cm,String.fromCharCode(e.charCode==null?e.keyCode:e.charCode),0))},Qe.prototype.readOnlyChanged=function(e){this.div.contentEditable=String(e!="nocursor")},Qe.prototype.onContextMenu=function(){},Qe.prototype.resetPosition=function(){},Qe.prototype.needsContentAttribute=!0;function ls(e,t){var n=po(e,t.line);if(!n||n.hidden)return null;var r=Ae(e.doc,t.line),i=Na(n,r,t.line),a=Pe(r,e.doc.direction),l="left";if(a){var u=Pt(a,t.ch);l=u%2?"right":"left"}var f=Ba(i.map,t.ch,l);return f.offset=f.collapse=="right"?f.end:f.start,f}function bd(e){for(var t=e;t;t=t.parentNode)if(/CodeMirror-gutter-wrapper/.test(t.className))return!0;return!1}function dn(e,t){return t&&(e.bad=!0),e}function yd(e,t,n,r,i){var a="",l=!1,u=e.doc.lineSeparator(),f=!1;function m(Y){return function(ie){return ie.id==Y}}function A(){l&&(a+=u,f&&(a+=u),l=f=!1)}function B(Y){Y&&(A(),a+=Y)}function ee(Y){if(Y.nodeType==1){var ie=Y.getAttribute("cm-text");if(ie){B(ie);return}var ue=Y.getAttribute("cm-marker"),me;if(ue){var ve=e.findMarks(ne(r,0),ne(i+1,0),m(+ue));ve.length&&(me=ve[0].find(0))&&B(ir(e.doc,me.from,me.to).join(u));return}if(Y.getAttribute("contenteditable")=="false")return;var _e=/^(pre|div|p|li|table|br)$/i.test(Y.nodeName);if(!/^br$/i.test(Y.nodeName)&&Y.textContent.length==0)return;_e&&A();for(var be=0;be=9&&t.hasSelection&&(t.hasSelection=null),n.poll()}),Ie(i,"paste",function(l){ot(r,l)||rs(l,r)||(r.state.pasteIncoming=+new Date,n.fastPoll())});function a(l){if(!ot(r,l)){if(r.somethingSelected())zi({lineWise:!1,text:r.getSelections()});else if(r.options.lineWiseCopyCut){var u=is(r);zi({lineWise:!0,text:u.text}),l.type=="cut"?r.setSelections(u.ranges,null,ke):(n.prevInput="",i.value=u.text.join(` +`),F(i))}else return;l.type=="cut"&&(r.state.cutIncoming=+new Date)}}Ie(i,"cut",a),Ie(i,"copy",a),Ie(e.scroller,"paste",function(l){if(!(lr(e,l)||ot(r,l))){if(!i.dispatchEvent){r.state.pasteIncoming=+new Date,n.focus();return}var u=new Event("paste");u.clipboardData=l.clipboardData,i.dispatchEvent(u)}}),Ie(e.lineSpace,"selectstart",function(l){lr(e,l)||kt(l)}),Ie(i,"compositionstart",function(){var l=r.getCursor("from");n.composing&&n.composing.range.clear(),n.composing={start:l,range:r.markText(l,r.getCursor("to"),{className:"CodeMirror-composing"})}}),Ie(i,"compositionend",function(){n.composing&&(n.poll(),n.composing.range.clear(),n.composing=null)})},st.prototype.createField=function(e){this.wrapper=os(),this.textarea=this.wrapper.firstChild;var t=this.cm.options;Ko(this.textarea,t.spellcheck,t.autocorrect,t.autocapitalize)},st.prototype.screenReaderLabelChanged=function(e){e?this.textarea.setAttribute("aria-label",e):this.textarea.removeAttribute("aria-label")},st.prototype.prepareSelection=function(){var e=this.cm,t=e.display,n=e.doc,r=Ya(e);if(e.options.moveInputWithCursor){var i=Xt(e,n.sel.primary().head,"div"),a=t.wrapper.getBoundingClientRect(),l=t.lineDiv.getBoundingClientRect();r.teTop=Math.max(0,Math.min(t.wrapper.clientHeight-10,i.top+l.top-a.top)),r.teLeft=Math.max(0,Math.min(t.wrapper.clientWidth-10,i.left+l.left-a.left))}return r},st.prototype.showSelection=function(e){var t=this.cm,n=t.display;V(n.cursorDiv,e.cursors),V(n.selectionDiv,e.selection),e.teTop!=null&&(this.wrapper.style.top=e.teTop+"px",this.wrapper.style.left=e.teLeft+"px")},st.prototype.reset=function(e){if(!(this.contextMenuPending||this.composing&&e)){var t=this.cm;if(this.resetting=!0,t.somethingSelected()){this.prevInput="";var n=t.getSelection();this.textarea.value=n,t.state.focused&&F(this.textarea),s&&h>=9&&(this.hasSelection=n)}else e||(this.prevInput=this.textarea.value="",s&&h>=9&&(this.hasSelection=null));this.resetting=!1}},st.prototype.getField=function(){return this.textarea},st.prototype.supportsTouch=function(){return!1},st.prototype.focus=function(){if(this.cm.options.readOnly!="nocursor"&&(!E||R(ze(this.textarea))!=this.textarea))try{this.textarea.focus()}catch{}},st.prototype.blur=function(){this.textarea.blur()},st.prototype.resetPosition=function(){this.wrapper.style.top=this.wrapper.style.left=0},st.prototype.receivedFocus=function(){this.slowPoll()},st.prototype.slowPoll=function(){var e=this;this.pollingFast||this.polling.set(this.cm.options.pollInterval,function(){e.poll(),e.cm.state.focused&&e.slowPoll()})},st.prototype.fastPoll=function(){var e=!1,t=this;t.pollingFast=!0;function n(){var r=t.poll();!r&&!e?(e=!0,t.polling.set(60,n)):(t.pollingFast=!1,t.slowPoll())}t.polling.set(20,n)},st.prototype.poll=function(){var e=this,t=this.cm,n=this.textarea,r=this.prevInput;if(this.contextMenuPending||this.resetting||!t.state.focused||hr(n)&&!r&&!this.composing||t.isReadOnly()||t.options.disableInput||t.state.keySeq)return!1;var i=n.value;if(i==r&&!t.somethingSelected())return!1;if(s&&h>=9&&this.hasSelection===i||O&&/[\uf700-\uf7ff]/.test(i))return t.display.input.reset(),!1;if(t.doc.sel==t.display.selForContextMenu){var a=i.charCodeAt(0);if(a==8203&&!r&&(r="\u200B"),a==8666)return this.reset(),this.cm.execCommand("undo")}for(var l=0,u=Math.min(r.length,i.length);l1e3||i.indexOf(` +`)>-1?n.value=e.prevInput="":e.prevInput=i,e.composing&&(e.composing.range.clear(),e.composing.range=t.markText(e.composing.start,t.getCursor("to"),{className:"CodeMirror-composing"}))}),!0},st.prototype.ensurePolled=function(){this.pollingFast&&this.poll()&&(this.pollingFast=!1)},st.prototype.onKeyPress=function(){s&&h>=9&&(this.hasSelection=null),this.fastPoll()},st.prototype.onContextMenu=function(e){var t=this,n=t.cm,r=n.display,i=t.textarea;t.contextMenuPending&&t.contextMenuPending();var a=Mr(n,e),l=r.scroller.scrollTop;if(!a||d)return;var u=n.options.resetSelectionOnContextMenu;u&&n.doc.sel.contains(a)==-1&>(n,wt)(n.doc,yr(a),ke);var f=i.style.cssText,m=t.wrapper.style.cssText,A=t.wrapper.offsetParent.getBoundingClientRect();t.wrapper.style.cssText="position: static",i.style.cssText=`position: absolute; width: 30px; height: 30px; + top: `+(e.clientY-A.top-5)+"px; left: "+(e.clientX-A.left-5)+`px; + z-index: 1000; background: `+(s?"rgba(255, 255, 255, .05)":"transparent")+`; + outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);`;var B;g&&(B=i.ownerDocument.defaultView.scrollY),r.input.focus(),g&&i.ownerDocument.defaultView.scrollTo(null,B),r.input.reset(),n.somethingSelected()||(i.value=t.prevInput=" "),t.contextMenuPending=Y,r.selForContextMenu=n.doc.sel,clearTimeout(r.detectingSelectAll);function ee(){if(i.selectionStart!=null){var ue=n.somethingSelected(),me="\u200B"+(ue?i.value:"");i.value="\u21DA",i.value=me,t.prevInput=ue?"":"\u200B",i.selectionStart=1,i.selectionEnd=me.length,r.selForContextMenu=n.doc.sel}}function Y(){if(t.contextMenuPending==Y&&(t.contextMenuPending=!1,t.wrapper.style.cssText=m,i.style.cssText=f,s&&h<9&&r.scrollbars.setScrollTop(r.scroller.scrollTop=l),i.selectionStart!=null)){(!s||s&&h<9)&&ee();var ue=0,me=function(){r.selForContextMenu==n.doc.sel&&i.selectionStart==0&&i.selectionEnd>0&&t.prevInput=="\u200B"?gt(n,Ll)(n):ue++<10?r.detectingSelectAll=setTimeout(me,500):(r.selForContextMenu=null,r.input.reset())};r.detectingSelectAll=setTimeout(me,200)}}if(s&&h>=9&&ee(),I){dr(e);var ie=function(){_t(window,"mouseup",ie),setTimeout(Y,20)};Ie(window,"mouseup",ie)}else setTimeout(Y,50)},st.prototype.readOnlyChanged=function(e){e||this.reset(),this.textarea.disabled=e=="nocursor",this.textarea.readOnly=!!e},st.prototype.setUneditable=function(){},st.prototype.needsContentAttribute=!1;function _d(e,t){if(t=t?ge(t):{},t.value=e.value,!t.tabindex&&e.tabIndex&&(t.tabindex=e.tabIndex),!t.placeholder&&e.placeholder&&(t.placeholder=e.placeholder),t.autofocus==null){var n=R(ze(e));t.autofocus=n==e||e.getAttribute("autofocus")!=null&&n==document.body}function r(){e.value=u.getValue()}var i;if(e.form&&(Ie(e.form,"submit",r),!t.leaveSubmitMethodAlone)){var a=e.form;i=a.submit;try{var l=a.submit=function(){r(),a.submit=i,a.submit(),a.submit=l}}catch{}}t.finishInit=function(f){f.save=r,f.getTextArea=function(){return e},f.toTextArea=function(){f.toTextArea=isNaN,r(),e.parentNode.removeChild(f.getWrapperElement()),e.style.display="",e.form&&(_t(e.form,"submit",r),!t.leaveSubmitMethodAlone&&typeof e.form.submit=="function"&&(e.form.submit=i))}},e.style.display="none";var u=tt(function(f){return e.parentNode.insertBefore(f,e.nextSibling)},t);return u}function kd(e){e.off=_t,e.on=Ie,e.wheelEventPixels=zf,e.Doc=Mt,e.splitLines=Ht,e.countColumn=Oe,e.findColumn=Ge,e.isWordChar=we,e.Pass=Ze,e.signal=it,e.Line=Xr,e.changeEnd=xr,e.scrollbarModel=rl,e.Pos=ne,e.cmpPos=ye,e.modes=Wr,e.mimeModes=Kt,e.resolveMode=Ur,e.getMode=$r,e.modeExtensions=gr,e.extendMode=Kr,e.copyState=Vt,e.startState=Gr,e.innerMode=_n,e.commands=$n,e.keyMap=ur,e.keyName=Hl,e.isModifierKey=jl,e.lookupKey=un,e.normalizeKeyMap=Qf,e.StringStream=at,e.SharedTextMarker=Hn,e.TextMarker=kr,e.LineWidget=Rn,e.e_preventDefault=kt,e.e_stopPropagation=Rr,e.e_stop=dr,e.addClass=le,e.contains=N,e.rmClass=Q,e.keyNames=wr}pd(tt),vd(tt);var wd="iter insert remove copy getEditor constructor".split(" ");for(var Ai in Mt.prototype)Mt.prototype.hasOwnProperty(Ai)&&Se(wd,Ai)<0&&(tt.prototype[Ai]=(function(e){return function(){return e.apply(this.doc,arguments)}})(Mt.prototype[Ai]));return Wt(Mt),tt.inputStyles={textarea:st,contenteditable:Qe},tt.defineMode=function(e){!tt.defaults.mode&&e!="null"&&(tt.defaults.mode=e),Gt.apply(this,arguments)},tt.defineMIME=Cr,tt.defineMode("null",function(){return{token:function(e){return e.skipToEnd()}}}),tt.defineMIME("text/plain","null"),tt.defineExtension=function(e,t){tt.prototype[e]=t},tt.defineDocExtension=function(e,t){Mt.prototype[e]=t},tt.fromTextArea=_d,kd(tt),tt.version="5.65.20",tt}))});var Yn=Ke((us,cs)=>{(function(o){typeof us=="object"&&typeof cs=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";o.overlayMode=function(p,v,C){return{startState:function(){return{base:o.startState(p),overlay:o.startState(v),basePos:0,baseCur:null,overlayPos:0,overlayCur:null,streamSeen:null}},copyState:function(b){return{base:o.copyState(p,b.base),overlay:o.copyState(v,b.overlay),basePos:b.basePos,baseCur:null,overlayPos:b.overlayPos,overlayCur:null}},token:function(b,S){return(b!=S.streamSeen||Math.min(S.basePos,S.overlayPos){(function(o){typeof fs=="object"&&typeof ds=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";var p=/^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/,v=/^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/,C=/[*+-]\s/;o.commands.newlineAndIndentContinueMarkdownList=function(S){if(S.getOption("disableInput"))return o.Pass;for(var s=S.listSelections(),h=[],g=0;g\s*$/.test(z),E=!/>\s*$/.test(z);(W||E)&&S.replaceRange("",{line:T.line,ch:0},{line:T.line,ch:T.ch+1}),h[g]=` +`}else{var O=M[1],G=M[5],J=!(C.test(M[2])||M[2].indexOf(">")>=0),re=J?parseInt(M[3],10)+1+M[4]:M[2].replace("x"," ");h[g]=` +`+O+re+G,J&&b(S,T)}}S.replaceSelections(h)};function b(S,s){var h=s.line,g=0,T=0,w=p.exec(S.getLine(h)),c=w[1];do{g+=1;var d=h+g,k=S.getLine(d),z=p.exec(k);if(z){var M=z[1],_=parseInt(w[3],10)+g-T,W=parseInt(z[3],10),E=W;if(c===M&&!isNaN(W))_===W&&(E=W+1),_>W&&(E=_+1),S.replaceRange(k.replace(p,M+E+z[4]+z[5]),{line:d,ch:0},{line:d,ch:k.length});else{if(c.length>M.length||c.length{(function(o){typeof hs=="object"&&typeof gs=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){o.defineOption("placeholder","",function(h,g,T){var w=T&&T!=o.Init;if(g&&!w)h.on("blur",b),h.on("change",S),h.on("swapDoc",S),o.on(h.getInputField(),"compositionupdate",h.state.placeholderCompose=function(){C(h)}),S(h);else if(!g&&w){h.off("blur",b),h.off("change",S),h.off("swapDoc",S),o.off(h.getInputField(),"compositionupdate",h.state.placeholderCompose),p(h);var c=h.getWrapperElement();c.className=c.className.replace(" CodeMirror-empty","")}g&&!h.hasFocus()&&b(h)});function p(h){h.state.placeholder&&(h.state.placeholder.parentNode.removeChild(h.state.placeholder),h.state.placeholder=null)}function v(h){p(h);var g=h.state.placeholder=document.createElement("pre");g.style.cssText="height: 0; overflow: visible",g.style.direction=h.getOption("direction"),g.className="CodeMirror-placeholder CodeMirror-line-like";var T=h.getOption("placeholder");typeof T=="string"&&(T=document.createTextNode(T)),g.appendChild(T),h.display.lineSpace.insertBefore(g,h.display.lineSpace.firstChild)}function C(h){setTimeout(function(){var g=!1;if(h.lineCount()==1){var T=h.getInputField();g=T.nodeName=="TEXTAREA"?!h.getLine(0).length:!/[^\u200b]/.test(T.querySelector(".CodeMirror-line").textContent)}g?v(h):p(h)},20)}function b(h){s(h)&&v(h)}function S(h){var g=h.getWrapperElement(),T=s(h);g.className=g.className.replace(" CodeMirror-empty","")+(T?" CodeMirror-empty":""),T?v(h):p(h)}function s(h){return h.lineCount()===1&&h.getLine(0)===""}})});var ys=Ke((vs,bs)=>{(function(o){typeof vs=="object"&&typeof bs=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";o.defineOption("styleSelectedText",!1,function(w,c,d){var k=d&&d!=o.Init;c&&!k?(w.state.markedSelection=[],w.state.markedSelectionStyle=typeof c=="string"?c:"CodeMirror-selectedtext",g(w),w.on("cursorActivity",p),w.on("change",v)):!c&&k&&(w.off("cursorActivity",p),w.off("change",v),h(w),w.state.markedSelection=w.state.markedSelectionStyle=null)});function p(w){w.state.markedSelection&&w.operation(function(){T(w)})}function v(w){w.state.markedSelection&&w.state.markedSelection.length&&w.operation(function(){h(w)})}var C=8,b=o.Pos,S=o.cmpPos;function s(w,c,d,k){if(S(c,d)!=0)for(var z=w.state.markedSelection,M=w.state.markedSelectionStyle,_=c.line;;){var W=_==c.line?c:b(_,0),E=_+C,O=E>=d.line,G=O?d:b(E,0),J=w.markText(W,G,{className:M});if(k==null?z.push(J):z.splice(k++,0,J),O)break;_=E}}function h(w){for(var c=w.state.markedSelection,d=0;d1)return g(w);var c=w.getCursor("start"),d=w.getCursor("end"),k=w.state.markedSelection;if(!k.length)return s(w,c,d);var z=k[0].find(),M=k[k.length-1].find();if(!z||!M||d.line-c.line<=C||S(c,M.to)>=0||S(d,z.from)<=0)return g(w);for(;S(c,z.from)>0;)k.shift().clear(),z=k[0].find();for(S(c,z.from)<0&&(z.to.line-c.line0&&(d.line-M.from.line{(function(o){typeof xs=="object"&&typeof _s=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";var p=o.Pos;function v(_){var W=_.flags;return W??(_.ignoreCase?"i":"")+(_.global?"g":"")+(_.multiline?"m":"")}function C(_,W){for(var E=v(_),O=E,G=0;Gre);q++){var I=_.getLine(J++);O=O==null?I:O+` +`+I}G=G*2,W.lastIndex=E.ch;var D=W.exec(O);if(D){var Q=O.slice(0,D.index).split(` +`),j=D[0].split(` +`),V=E.line+Q.length-1,y=Q[Q.length-1].length;return{from:p(V,y),to:p(V+j.length-1,j.length==1?y+j[0].length:j[j.length-1].length),match:D}}}}function h(_,W,E){for(var O,G=0;G<=_.length;){W.lastIndex=G;var J=W.exec(_);if(!J)break;var re=J.index+J[0].length;if(re>_.length-E)break;(!O||re>O.index+O[0].length)&&(O=J),G=J.index+1}return O}function g(_,W,E){W=C(W,"g");for(var O=E.line,G=E.ch,J=_.firstLine();O>=J;O--,G=-1){var re=_.getLine(O),q=h(re,W,G<0?0:re.length-G);if(q)return{from:p(O,q.index),to:p(O,q.index+q[0].length),match:q}}}function T(_,W,E){if(!b(W))return g(_,W,E);W=C(W,"gm");for(var O,G=1,J=_.getLine(E.line).length-E.ch,re=E.line,q=_.firstLine();re>=q;){for(var I=0;I=q;I++){var D=_.getLine(re--);O=O==null?D:D+` +`+O}G*=2;var Q=h(O,W,J);if(Q){var j=O.slice(0,Q.index).split(` +`),V=Q[0].split(` +`),y=re+j.length,K=j[j.length-1].length;return{from:p(y,K),to:p(y+V.length-1,V.length==1?K+V[0].length:V[V.length-1].length),match:Q}}}}var w,c;String.prototype.normalize?(w=function(_){return _.normalize("NFD").toLowerCase()},c=function(_){return _.normalize("NFD")}):(w=function(_){return _.toLowerCase()},c=function(_){return _});function d(_,W,E,O){if(_.length==W.length)return E;for(var G=0,J=E+Math.max(0,_.length-W.length);;){if(G==J)return G;var re=G+J>>1,q=O(_.slice(0,re)).length;if(q==E)return re;q>E?J=re:G=re+1}}function k(_,W,E,O){if(!W.length)return null;var G=O?w:c,J=G(W).split(/\r|\n\r?/);e:for(var re=E.line,q=E.ch,I=_.lastLine()+1-J.length;re<=I;re++,q=0){var D=_.getLine(re).slice(q),Q=G(D);if(J.length==1){var j=Q.indexOf(J[0]);if(j==-1)continue e;var E=d(D,Q,j,G)+q;return{from:p(re,d(D,Q,j,G)+q),to:p(re,d(D,Q,j+J[0].length,G)+q)}}else{var V=Q.length-J[0].length;if(Q.slice(V)!=J[0])continue e;for(var y=1;y=I;re--,q=-1){var D=_.getLine(re);q>-1&&(D=D.slice(0,q));var Q=G(D);if(J.length==1){var j=Q.lastIndexOf(J[0]);if(j==-1)continue e;return{from:p(re,d(D,Q,j,G)),to:p(re,d(D,Q,j+J[0].length,G))}}else{var V=J[J.length-1];if(Q.slice(0,V.length)!=V)continue e;for(var y=1,E=re-J.length+1;y(this.doc.getLine(W.line)||"").length&&(W.ch=0,W.line++)),o.cmpPos(W,this.doc.clipPos(W))!=0))return this.atOccurrence=!1;var E=this.matches(_,W);if(this.afterEmptyMatch=E&&o.cmpPos(E.from,E.to)==0,E)return this.pos=E,this.atOccurrence=!0,this.pos.match||!0;var O=p(_?this.doc.firstLine():this.doc.lastLine()+1,0);return this.pos={from:O,to:O},this.atOccurrence=!1},from:function(){if(this.atOccurrence)return this.pos.from},to:function(){if(this.atOccurrence)return this.pos.to},replace:function(_,W){if(this.atOccurrence){var E=o.splitLines(_);this.doc.replaceRange(E,this.pos.from,this.pos.to,W),this.pos.to=p(this.pos.from.line+E.length-1,E[E.length-1].length+(E.length==1?this.pos.from.ch:0))}}},o.defineExtension("getSearchCursor",function(_,W,E){return new M(this.doc,_,W,E)}),o.defineDocExtension("getSearchCursor",function(_,W,E){return new M(this,_,W,E)}),o.defineExtension("selectMatches",function(_,W){for(var E=[],O=this.getSearchCursor(_,this.getCursor("from"),W);O.findNext()&&!(o.cmpPos(O.to(),this.getCursor("to"))>0);)E.push({anchor:O.from(),head:O.to()});E.length&&this.setSelections(E,0)})})});var Vo=Ke((ws,Ss)=>{(function(o){typeof ws=="object"&&typeof Ss=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";function p(N,R,le,xe,F,L){this.indented=N,this.column=R,this.type=le,this.info=xe,this.align=F,this.prev=L}function v(N,R,le,xe){var F=N.indented;return N.context&&N.context.type=="statement"&&le!="statement"&&(F=N.context.indented),N.context=new p(F,R,le,xe,null,N.context)}function C(N){var R=N.context.type;return(R==")"||R=="]"||R=="}")&&(N.indented=N.context.indented),N.context=N.context.prev}function b(N,R,le){if(R.prevToken=="variable"||R.prevToken=="type"||/\S(?:[^- ]>|[*\]])\s*$|\*$/.test(N.string.slice(0,le))||R.typeAtEndOfLine&&N.column()==N.indentation())return!0}function S(N){for(;;){if(!N||N.type=="top")return!0;if(N.type=="}"&&N.prev.info!="namespace")return!1;N=N.prev}}o.defineMode("clike",function(N,R){var le=N.indentUnit,xe=R.statementIndentUnit||le,F=R.dontAlignCalls,L=R.keywords||{},de=R.types||{},ze=R.builtin||{},pe=R.blockKeywords||{},Ee=R.defKeywords||{},ge=R.atoms||{},Oe=R.hooks||{},qe=R.multiLineStrings,Se=R.indentStatements!==!1,Be=R.indentSwitch!==!1,Ze=R.namespaceSeparator,ke=R.isPunctuationChar||/[\[\]{}\(\),;\:\.]/,Je=R.numberStart||/[\d\.]/,Re=R.number||/^(?:0x[a-f\d]+|0b[01]+|(?:\d+\.?\d*|\.\d+)(?:e[-+]?\d+)?)(u|ll?|l|f)?/i,Ge=R.isOperatorChar||/[+\-*&%=<>!?|\/]/,U=R.isIdentifierChar||/[\w\$_\xa1-\uffff]/,Z=R.isReservedIdentifier||!1,ce,He;function te(we,Me){var Le=we.next();if(Oe[Le]){var $=Oe[Le](we,Me);if($!==!1)return $}if(Le=='"'||Le=="'")return Me.tokenize=fe(Le),Me.tokenize(we,Me);if(Je.test(Le)){if(we.backUp(1),we.match(Re))return"number";we.next()}if(ke.test(Le))return ce=Le,null;if(Le=="/"){if(we.eat("*"))return Me.tokenize=oe,oe(we,Me);if(we.eat("/"))return we.skipToEnd(),"comment"}if(Ge.test(Le)){for(;!we.match(/^\/[\/*]/,!1)&&we.eat(Ge););return"operator"}if(we.eatWhile(U),Ze)for(;we.match(Ze);)we.eatWhile(U);var H=we.current();return h(L,H)?(h(pe,H)&&(ce="newstatement"),h(Ee,H)&&(He=!0),"keyword"):h(de,H)?"type":h(ze,H)||Z&&Z(H)?(h(pe,H)&&(ce="newstatement"),"builtin"):h(ge,H)?"atom":"variable"}function fe(we){return function(Me,Le){for(var $=!1,H,se=!1;(H=Me.next())!=null;){if(H==we&&!$){se=!0;break}$=!$&&H=="\\"}return(se||!($||qe))&&(Le.tokenize=null),"string"}}function oe(we,Me){for(var Le=!1,$;$=we.next();){if($=="/"&&Le){Me.tokenize=null;break}Le=$=="*"}return"comment"}function Ue(we,Me){R.typeFirstDefinitions&&we.eol()&&S(Me.context)&&(Me.typeAtEndOfLine=b(we,Me,we.pos))}return{startState:function(we){return{tokenize:null,context:new p((we||0)-le,0,"top",null,!1),indented:0,startOfLine:!0,prevToken:null}},token:function(we,Me){var Le=Me.context;if(we.sol()&&(Le.align==null&&(Le.align=!1),Me.indented=we.indentation(),Me.startOfLine=!0),we.eatSpace())return Ue(we,Me),null;ce=He=null;var $=(Me.tokenize||te)(we,Me);if($=="comment"||$=="meta")return $;if(Le.align==null&&(Le.align=!0),ce==";"||ce==":"||ce==","&&we.match(/^\s*(?:\/\/.*)?$/,!1))for(;Me.context.type=="statement";)C(Me);else if(ce=="{")v(Me,we.column(),"}");else if(ce=="[")v(Me,we.column(),"]");else if(ce=="(")v(Me,we.column(),")");else if(ce=="}"){for(;Le.type=="statement";)Le=C(Me);for(Le.type=="}"&&(Le=C(Me));Le.type=="statement";)Le=C(Me)}else ce==Le.type?C(Me):Se&&((Le.type=="}"||Le.type=="top")&&ce!=";"||Le.type=="statement"&&ce=="newstatement")&&v(Me,we.column(),"statement",we.current());if($=="variable"&&(Me.prevToken=="def"||R.typeFirstDefinitions&&b(we,Me,we.start)&&S(Me.context)&&we.match(/^\s*\(/,!1))&&($="def"),Oe.token){var H=Oe.token(we,Me,$);H!==void 0&&($=H)}return $=="def"&&R.styleDefs===!1&&($="variable"),Me.startOfLine=!1,Me.prevToken=He?"def":$||ce,Ue(we,Me),$},indent:function(we,Me){if(we.tokenize!=te&&we.tokenize!=null||we.typeAtEndOfLine&&S(we.context))return o.Pass;var Le=we.context,$=Me&&Me.charAt(0),H=$==Le.type;if(Le.type=="statement"&&$=="}"&&(Le=Le.prev),R.dontIndentStatements)for(;Le.type=="statement"&&R.dontIndentStatements.test(Le.info);)Le=Le.prev;if(Oe.indent){var se=Oe.indent(we,Le,Me,le);if(typeof se=="number")return se}var De=Le.prev&&Le.prev.info=="switch";if(R.allmanIndentation&&/[{(]/.test($)){for(;Le.type!="top"&&Le.type!="}";)Le=Le.prev;return Le.indented}return Le.type=="statement"?Le.indented+($=="{"?0:xe):Le.align&&(!F||Le.type!=")")?Le.column+(H?0:1):Le.type==")"&&!H?Le.indented+xe:Le.indented+(H?0:le)+(!H&&De&&!/^(?:case|default)\b/.test(Me)?le:0)},electricInput:Be?/^\s*(?:case .*?:|default:|\{\}?|\})$/:/^\s*[{}]$/,blockCommentStart:"/*",blockCommentEnd:"*/",blockCommentContinue:" * ",lineComment:"//",fold:"brace"}});function s(N){for(var R={},le=N.split(" "),xe=0;xe!?|\/#:@]/,hooks:{"@":function(N){return N.eatWhile(/[\w\$_]/),"meta"},'"':function(N,R){return N.match('""')?(R.tokenize=j,R.tokenize(N,R)):!1},"'":function(N){return N.match(/^(\\[^'\s]+|[^\\'])'/)?"string-2":(N.eatWhile(/[\w\$_\xa1-\uffff]/),"atom")},"=":function(N,R){var le=R.context;return le.type=="}"&&le.align&&N.eat(">")?(R.context=new p(le.indented,le.column,le.type,le.info,null,le.prev),"operator"):!1},"/":function(N,R){return N.eat("*")?(R.tokenize=V(1),R.tokenize(N,R)):!1}},modeProps:{closeBrackets:{pairs:'()[]{}""',triples:'"'}}});function y(N){return function(R,le){for(var xe=!1,F,L=!1;!R.eol();){if(!N&&!xe&&R.match('"')){L=!0;break}if(N&&R.match('"""')){L=!0;break}F=R.next(),!xe&&F=="$"&&R.match("{")&&R.skipTo("}"),xe=!xe&&F=="\\"&&!N}return(L||!N)&&(le.tokenize=null),"string"}}Q("text/x-kotlin",{name:"clike",keywords:s("package as typealias class interface this super val operator var fun for is in This throw return annotation break continue object if else while do try when !in !is as? file import where by get set abstract enum open inner override private public internal protected catch finally out final vararg reified dynamic companion constructor init sealed field property receiver param sparam lateinit data inline noinline tailrec external annotation crossinline const operator infix suspend actual expect setparam value"),types:s("Boolean Byte Character CharSequence Class ClassLoader Cloneable Comparable Compiler Double Exception Float Integer Long Math Number Object Package Pair Process Runtime Runnable SecurityManager Short StackTraceElement StrictMath String StringBuffer System Thread ThreadGroup ThreadLocal Throwable Triple Void Annotation Any BooleanArray ByteArray Char CharArray DeprecationLevel DoubleArray Enum FloatArray Function Int IntArray Lazy LazyThreadSafetyMode LongArray Nothing ShortArray Unit"),intendSwitch:!1,indentStatements:!1,multiLineStrings:!0,number:/^(?:0x[a-f\d_]+|0b[01_]+|(?:[\d_]+(\.\d+)?|\.\d+)(?:e[-+]?[\d_]+)?)(u|ll?|l|f)?/i,blockKeywords:s("catch class do else finally for if where try while enum"),defKeywords:s("class val var object interface fun"),atoms:s("true false null this"),hooks:{"@":function(N){return N.eatWhile(/[\w\$_]/),"meta"},"*":function(N,R){return R.prevToken=="."?"variable":"operator"},'"':function(N,R){return R.tokenize=y(N.match('""')),R.tokenize(N,R)},"/":function(N,R){return N.eat("*")?(R.tokenize=V(1),R.tokenize(N,R)):!1},indent:function(N,R,le,xe){var F=le&&le.charAt(0);if((N.prevToken=="}"||N.prevToken==")")&&le=="")return N.indented;if(N.prevToken=="operator"&&le!="}"&&N.context.type!="}"||N.prevToken=="variable"&&F=="."||(N.prevToken=="}"||N.prevToken==")")&&F==".")return xe*2+R.indented;if(R.align&&R.type=="}")return R.indented+(N.context.type==(le||"").charAt(0)?0:xe)}},modeProps:{closeBrackets:{triples:'"'}}}),Q(["x-shader/x-vertex","x-shader/x-fragment"],{name:"clike",keywords:s("sampler1D sampler2D sampler3D samplerCube sampler1DShadow sampler2DShadow const attribute uniform varying break continue discard return for while do if else struct in out inout"),types:s("float int bool void vec2 vec3 vec4 ivec2 ivec3 ivec4 bvec2 bvec3 bvec4 mat2 mat3 mat4"),blockKeywords:s("for while do if else struct"),builtin:s("radians degrees sin cos tan asin acos atan pow exp log exp2 sqrt inversesqrt abs sign floor ceil fract mod min max clamp mix step smoothstep length distance dot cross normalize ftransform faceforward reflect refract matrixCompMult lessThan lessThanEqual greaterThan greaterThanEqual equal notEqual any all not texture1D texture1DProj texture1DLod texture1DProjLod texture2D texture2DProj texture2DLod texture2DProjLod texture3D texture3DProj texture3DLod texture3DProjLod textureCube textureCubeLod shadow1D shadow2D shadow1DProj shadow2DProj shadow1DLod shadow2DLod shadow1DProjLod shadow2DProjLod dFdx dFdy fwidth noise1 noise2 noise3 noise4"),atoms:s("true false gl_FragColor gl_SecondaryColor gl_Normal gl_Vertex gl_MultiTexCoord0 gl_MultiTexCoord1 gl_MultiTexCoord2 gl_MultiTexCoord3 gl_MultiTexCoord4 gl_MultiTexCoord5 gl_MultiTexCoord6 gl_MultiTexCoord7 gl_FogCoord gl_PointCoord gl_Position gl_PointSize gl_ClipVertex gl_FrontColor gl_BackColor gl_FrontSecondaryColor gl_BackSecondaryColor gl_TexCoord gl_FogFragCoord gl_FragCoord gl_FrontFacing gl_FragData gl_FragDepth gl_ModelViewMatrix gl_ProjectionMatrix gl_ModelViewProjectionMatrix gl_TextureMatrix gl_NormalMatrix gl_ModelViewMatrixInverse gl_ProjectionMatrixInverse gl_ModelViewProjectionMatrixInverse gl_TextureMatrixTranspose gl_ModelViewMatrixInverseTranspose gl_ProjectionMatrixInverseTranspose gl_ModelViewProjectionMatrixInverseTranspose gl_TextureMatrixInverseTranspose gl_NormalScale gl_DepthRange gl_ClipPlane gl_Point gl_FrontMaterial gl_BackMaterial gl_LightSource gl_LightModel gl_FrontLightModelProduct gl_BackLightModelProduct gl_TextureColor gl_EyePlaneS gl_EyePlaneT gl_EyePlaneR gl_EyePlaneQ gl_FogParameters gl_MaxLights gl_MaxClipPlanes gl_MaxTextureUnits gl_MaxTextureCoords gl_MaxVertexAttribs gl_MaxVertexUniformComponents gl_MaxVaryingFloats gl_MaxVertexTextureImageUnits gl_MaxTextureImageUnits gl_MaxFragmentUniformComponents gl_MaxCombineTextureImageUnits gl_MaxDrawBuffers"),indentSwitch:!1,hooks:{"#":E},modeProps:{fold:["brace","include"]}}),Q("text/x-nesc",{name:"clike",keywords:s(g+" as atomic async call command component components configuration event generic implementation includes interface module new norace nx_struct nx_union post provides signal task uses abstract extends"),types:z,blockKeywords:s(_),atoms:s("null true false"),hooks:{"#":E},modeProps:{fold:["brace","include"]}}),Q("text/x-objectivec",{name:"clike",keywords:s(g+" "+w),types:M,builtin:s(c),blockKeywords:s(_+" @synthesize @try @catch @finally @autoreleasepool @synchronized"),defKeywords:s(W+" @interface @implementation @protocol @class"),dontIndentStatements:/^@.*$/,typeFirstDefinitions:!0,atoms:s("YES NO NULL Nil nil true false nullptr"),isReservedIdentifier:G,hooks:{"#":E,"*":O},modeProps:{fold:["brace","include"]}}),Q("text/x-objectivec++",{name:"clike",keywords:s(g+" "+w+" "+T),types:M,builtin:s(c),blockKeywords:s(_+" @synthesize @try @catch @finally @autoreleasepool @synchronized class try catch"),defKeywords:s(W+" @interface @implementation @protocol @class class namespace"),dontIndentStatements:/^@.*$|^template$/,typeFirstDefinitions:!0,atoms:s("YES NO NULL Nil nil true false nullptr"),isReservedIdentifier:G,hooks:{"#":E,"*":O,u:re,U:re,L:re,R:re,0:J,1:J,2:J,3:J,4:J,5:J,6:J,7:J,8:J,9:J,token:function(N,R,le){if(le=="variable"&&N.peek()=="("&&(R.prevToken==";"||R.prevToken==null||R.prevToken=="}")&&q(N.current()))return"def"}},namespaceSeparator:"::",modeProps:{fold:["brace","include"]}}),Q("text/x-squirrel",{name:"clike",keywords:s("base break clone continue const default delete enum extends function in class foreach local resume return this throw typeof yield constructor instanceof static"),types:z,blockKeywords:s("case catch class else for foreach if switch try while"),defKeywords:s("function local class"),typeFirstDefinitions:!0,atoms:s("true false null"),hooks:{"#":E},modeProps:{fold:["brace","include"]}});var K=null;function X(N){return function(R,le){for(var xe=!1,F,L=!1;!R.eol();){if(!xe&&R.match('"')&&(N=="single"||R.match('""'))){L=!0;break}if(!xe&&R.match("``")){K=X(N),L=!0;break}F=R.next(),xe=N=="single"&&!xe&&F=="\\"}return L&&(le.tokenize=null),"string"}}Q("text/x-ceylon",{name:"clike",keywords:s("abstracts alias assembly assert assign break case catch class continue dynamic else exists extends finally for function given if import in interface is let module new nonempty object of out outer package return satisfies super switch then this throw try value void while"),types:function(N){var R=N.charAt(0);return R===R.toUpperCase()&&R!==R.toLowerCase()},blockKeywords:s("case catch class dynamic else finally for function if interface module new object switch try while"),defKeywords:s("class dynamic function interface module object package value"),builtin:s("abstract actual aliased annotation by default deprecated doc final formal late license native optional sealed see serializable shared suppressWarnings tagged throws variable"),isPunctuationChar:/[\[\]{}\(\),;\:\.`]/,isOperatorChar:/[+\-*&%=<>!?|^~:\/]/,numberStart:/[\d#$]/,number:/^(?:#[\da-fA-F_]+|\$[01_]+|[\d_]+[kMGTPmunpf]?|[\d_]+\.[\d_]+(?:[eE][-+]?\d+|[kMGTPmunpf]|)|)/i,multiLineStrings:!0,typeFirstDefinitions:!0,atoms:s("true false null larger smaller equal empty finished"),indentSwitch:!1,styleDefs:!1,hooks:{"@":function(N){return N.eatWhile(/[\w\$_]/),"meta"},'"':function(N,R){return R.tokenize=X(N.match('""')?"triple":"single"),R.tokenize(N,R)},"`":function(N,R){return!K||!N.match("`")?!1:(R.tokenize=K,K=null,R.tokenize(N,R))},"'":function(N){return N.eatWhile(/[\w\$_\xa1-\uffff]/),"atom"},token:function(N,R,le){if((le=="variable"||le=="type")&&R.prevToken==".")return"variable-2"}},modeProps:{fold:["brace","import"],closeBrackets:{triples:'"'}}})})});var Cs=Ke((Ts,Ls)=>{(function(o){typeof Ts=="object"&&typeof Ls=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";o.defineMode("cmake",function(){var p=/({)?[a-zA-Z0-9_]+(})?/;function v(b,S){for(var s,h,g=!1;!b.eol()&&(s=b.next())!=S.pending;){if(s==="$"&&h!="\\"&&S.pending=='"'){g=!0;break}h=s}return g&&b.backUp(1),s==S.pending?S.continueString=!1:S.continueString=!0,"string"}function C(b,S){var s=b.next();return s==="$"?b.match(p)?"variable-2":"variable":S.continueString?(b.backUp(1),v(b,S)):b.match(/(\s+)?\w+\(/)||b.match(/(\s+)?\w+\ \(/)?(b.backUp(1),"def"):s=="#"?(b.skipToEnd(),"comment"):s=="'"||s=='"'?(S.pending=s,v(b,S)):s=="("||s==")"?"bracket":s.match(/[0-9]/)?"number":(b.eatWhile(/[\w-]/),null)}return{startState:function(){var b={};return b.inDefinition=!1,b.inInclude=!1,b.continueString=!1,b.pending=!1,b},token:function(b,S){return b.eatSpace()?null:C(b,S)}}}),o.defineMIME("text/x-cmake","cmake")})});var gn=Ke((Es,zs)=>{(function(o){typeof Es=="object"&&typeof zs=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";o.defineMode("css",function(I,D){var Q=D.inline;D.propertyKeywords||(D=o.resolveMode("text/css"));var j=I.indentUnit,V=D.tokenHooks,y=D.documentTypes||{},K=D.mediaTypes||{},X=D.mediaFeatures||{},N=D.mediaValueKeywords||{},R=D.propertyKeywords||{},le=D.nonStandardPropertyKeywords||{},xe=D.fontProperties||{},F=D.counterDescriptors||{},L=D.colorKeywords||{},de=D.valueKeywords||{},ze=D.allowNested,pe=D.lineComment,Ee=D.supportsAtComponent===!0,ge=I.highlightNonStandardPropertyKeywords!==!1,Oe,qe;function Se(te,fe){return Oe=fe,te}function Be(te,fe){var oe=te.next();if(V[oe]){var Ue=V[oe](te,fe);if(Ue!==!1)return Ue}if(oe=="@")return te.eatWhile(/[\w\\\-]/),Se("def",te.current());if(oe=="="||(oe=="~"||oe=="|")&&te.eat("="))return Se(null,"compare");if(oe=='"'||oe=="'")return fe.tokenize=Ze(oe),fe.tokenize(te,fe);if(oe=="#")return te.eatWhile(/[\w\\\-]/),Se("atom","hash");if(oe=="!")return te.match(/^\s*\w*/),Se("keyword","important");if(/\d/.test(oe)||oe=="."&&te.eat(/\d/))return te.eatWhile(/[\w.%]/),Se("number","unit");if(oe==="-"){if(/[\d.]/.test(te.peek()))return te.eatWhile(/[\w.%]/),Se("number","unit");if(te.match(/^-[\w\\\-]*/))return te.eatWhile(/[\w\\\-]/),te.match(/^\s*:/,!1)?Se("variable-2","variable-definition"):Se("variable-2","variable");if(te.match(/^\w+-/))return Se("meta","meta")}else return/[,+>*\/]/.test(oe)?Se(null,"select-op"):oe=="."&&te.match(/^-?[_a-z][_a-z0-9-]*/i)?Se("qualifier","qualifier"):/[:;{}\[\]\(\)]/.test(oe)?Se(null,oe):te.match(/^[\w-.]+(?=\()/)?(/^(url(-prefix)?|domain|regexp)$/i.test(te.current())&&(fe.tokenize=ke),Se("variable callee","variable")):/[\w\\\-]/.test(oe)?(te.eatWhile(/[\w\\\-]/),Se("property","word")):Se(null,null)}function Ze(te){return function(fe,oe){for(var Ue=!1,we;(we=fe.next())!=null;){if(we==te&&!Ue){te==")"&&fe.backUp(1);break}Ue=!Ue&&we=="\\"}return(we==te||!Ue&&te!=")")&&(oe.tokenize=null),Se("string","string")}}function ke(te,fe){return te.next(),te.match(/^\s*[\"\')]/,!1)?fe.tokenize=null:fe.tokenize=Ze(")"),Se(null,"(")}function Je(te,fe,oe){this.type=te,this.indent=fe,this.prev=oe}function Re(te,fe,oe,Ue){return te.context=new Je(oe,fe.indentation()+(Ue===!1?0:j),te.context),oe}function Ge(te){return te.context.prev&&(te.context=te.context.prev),te.context.type}function U(te,fe,oe){return He[oe.context.type](te,fe,oe)}function Z(te,fe,oe,Ue){for(var we=Ue||1;we>0;we--)oe.context=oe.context.prev;return U(te,fe,oe)}function ce(te){var fe=te.current().toLowerCase();de.hasOwnProperty(fe)?qe="atom":L.hasOwnProperty(fe)?qe="keyword":qe="variable"}var He={};return He.top=function(te,fe,oe){if(te=="{")return Re(oe,fe,"block");if(te=="}"&&oe.context.prev)return Ge(oe);if(Ee&&/@component/i.test(te))return Re(oe,fe,"atComponentBlock");if(/^@(-moz-)?document$/i.test(te))return Re(oe,fe,"documentTypes");if(/^@(media|supports|(-moz-)?document|import)$/i.test(te))return Re(oe,fe,"atBlock");if(/^@(font-face|counter-style)/i.test(te))return oe.stateArg=te,"restricted_atBlock_before";if(/^@(-(moz|ms|o|webkit)-)?keyframes$/i.test(te))return"keyframes";if(te&&te.charAt(0)=="@")return Re(oe,fe,"at");if(te=="hash")qe="builtin";else if(te=="word")qe="tag";else{if(te=="variable-definition")return"maybeprop";if(te=="interpolation")return Re(oe,fe,"interpolation");if(te==":")return"pseudo";if(ze&&te=="(")return Re(oe,fe,"parens")}return oe.context.type},He.block=function(te,fe,oe){if(te=="word"){var Ue=fe.current().toLowerCase();return R.hasOwnProperty(Ue)?(qe="property","maybeprop"):le.hasOwnProperty(Ue)?(qe=ge?"string-2":"property","maybeprop"):ze?(qe=fe.match(/^\s*:(?:\s|$)/,!1)?"property":"tag","block"):(qe+=" error","maybeprop")}else return te=="meta"?"block":!ze&&(te=="hash"||te=="qualifier")?(qe="error","block"):He.top(te,fe,oe)},He.maybeprop=function(te,fe,oe){return te==":"?Re(oe,fe,"prop"):U(te,fe,oe)},He.prop=function(te,fe,oe){if(te==";")return Ge(oe);if(te=="{"&&ze)return Re(oe,fe,"propBlock");if(te=="}"||te=="{")return Z(te,fe,oe);if(te=="(")return Re(oe,fe,"parens");if(te=="hash"&&!/^#([0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/.test(fe.current()))qe+=" error";else if(te=="word")ce(fe);else if(te=="interpolation")return Re(oe,fe,"interpolation");return"prop"},He.propBlock=function(te,fe,oe){return te=="}"?Ge(oe):te=="word"?(qe="property","maybeprop"):oe.context.type},He.parens=function(te,fe,oe){return te=="{"||te=="}"?Z(te,fe,oe):te==")"?Ge(oe):te=="("?Re(oe,fe,"parens"):te=="interpolation"?Re(oe,fe,"interpolation"):(te=="word"&&ce(fe),"parens")},He.pseudo=function(te,fe,oe){return te=="meta"?"pseudo":te=="word"?(qe="variable-3",oe.context.type):U(te,fe,oe)},He.documentTypes=function(te,fe,oe){return te=="word"&&y.hasOwnProperty(fe.current())?(qe="tag",oe.context.type):He.atBlock(te,fe,oe)},He.atBlock=function(te,fe,oe){if(te=="(")return Re(oe,fe,"atBlock_parens");if(te=="}"||te==";")return Z(te,fe,oe);if(te=="{")return Ge(oe)&&Re(oe,fe,ze?"block":"top");if(te=="interpolation")return Re(oe,fe,"interpolation");if(te=="word"){var Ue=fe.current().toLowerCase();Ue=="only"||Ue=="not"||Ue=="and"||Ue=="or"?qe="keyword":K.hasOwnProperty(Ue)?qe="attribute":X.hasOwnProperty(Ue)?qe="property":N.hasOwnProperty(Ue)?qe="keyword":R.hasOwnProperty(Ue)?qe="property":le.hasOwnProperty(Ue)?qe=ge?"string-2":"property":de.hasOwnProperty(Ue)?qe="atom":L.hasOwnProperty(Ue)?qe="keyword":qe="error"}return oe.context.type},He.atComponentBlock=function(te,fe,oe){return te=="}"?Z(te,fe,oe):te=="{"?Ge(oe)&&Re(oe,fe,ze?"block":"top",!1):(te=="word"&&(qe="error"),oe.context.type)},He.atBlock_parens=function(te,fe,oe){return te==")"?Ge(oe):te=="{"||te=="}"?Z(te,fe,oe,2):He.atBlock(te,fe,oe)},He.restricted_atBlock_before=function(te,fe,oe){return te=="{"?Re(oe,fe,"restricted_atBlock"):te=="word"&&oe.stateArg=="@counter-style"?(qe="variable","restricted_atBlock_before"):U(te,fe,oe)},He.restricted_atBlock=function(te,fe,oe){return te=="}"?(oe.stateArg=null,Ge(oe)):te=="word"?(oe.stateArg=="@font-face"&&!xe.hasOwnProperty(fe.current().toLowerCase())||oe.stateArg=="@counter-style"&&!F.hasOwnProperty(fe.current().toLowerCase())?qe="error":qe="property","maybeprop"):"restricted_atBlock"},He.keyframes=function(te,fe,oe){return te=="word"?(qe="variable","keyframes"):te=="{"?Re(oe,fe,"top"):U(te,fe,oe)},He.at=function(te,fe,oe){return te==";"?Ge(oe):te=="{"||te=="}"?Z(te,fe,oe):(te=="word"?qe="tag":te=="hash"&&(qe="builtin"),"at")},He.interpolation=function(te,fe,oe){return te=="}"?Ge(oe):te=="{"||te==";"?Z(te,fe,oe):(te=="word"?qe="variable":te!="variable"&&te!="("&&te!=")"&&(qe="error"),"interpolation")},{startState:function(te){return{tokenize:null,state:Q?"block":"top",stateArg:null,context:new Je(Q?"block":"top",te||0,null)}},token:function(te,fe){if(!fe.tokenize&&te.eatSpace())return null;var oe=(fe.tokenize||Be)(te,fe);return oe&&typeof oe=="object"&&(Oe=oe[1],oe=oe[0]),qe=oe,Oe!="comment"&&(fe.state=He[fe.state](Oe,te,fe)),qe},indent:function(te,fe){var oe=te.context,Ue=fe&&fe.charAt(0),we=oe.indent;return oe.type=="prop"&&(Ue=="}"||Ue==")")&&(oe=oe.prev),oe.prev&&(Ue=="}"&&(oe.type=="block"||oe.type=="top"||oe.type=="interpolation"||oe.type=="restricted_atBlock")?(oe=oe.prev,we=oe.indent):(Ue==")"&&(oe.type=="parens"||oe.type=="atBlock_parens")||Ue=="{"&&(oe.type=="at"||oe.type=="atBlock"))&&(we=Math.max(0,oe.indent-j))),we},electricChars:"}",blockCommentStart:"/*",blockCommentEnd:"*/",blockCommentContinue:" * ",lineComment:pe,fold:"brace"}});function p(I){for(var D={},Q=0;Q{(function(o){typeof Ms=="object"&&typeof As=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";o.defineMode("diff",function(){var p={"+":"positive","-":"negative","@":"meta"};return{token:function(v){var C=v.string.search(/[\t ]+?$/);if(!v.sol()||C===0)return v.skipToEnd(),("error "+(p[v.string.charAt(0)]||"")).replace(/ $/,"");var b=p[v.peek()]||v.skipToEnd();return C===-1?v.skipToEnd():v.pos=C,b}}}),o.defineMIME("text/x-diff","diff")})});var mn=Ke((qs,Fs)=>{(function(o){typeof qs=="object"&&typeof Fs=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";var p={autoSelfClosers:{area:!0,base:!0,br:!0,col:!0,command:!0,embed:!0,frame:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0,menuitem:!0},implicitlyClosed:{dd:!0,li:!0,optgroup:!0,option:!0,p:!0,rp:!0,rt:!0,tbody:!0,td:!0,tfoot:!0,th:!0,tr:!0},contextGrabbers:{dd:{dd:!0,dt:!0},dt:{dd:!0,dt:!0},li:{li:!0},option:{option:!0,optgroup:!0},optgroup:{optgroup:!0},p:{address:!0,article:!0,aside:!0,blockquote:!0,dir:!0,div:!0,dl:!0,fieldset:!0,footer:!0,form:!0,h1:!0,h2:!0,h3:!0,h4:!0,h5:!0,h6:!0,header:!0,hgroup:!0,hr:!0,menu:!0,nav:!0,ol:!0,p:!0,pre:!0,section:!0,table:!0,ul:!0},rp:{rp:!0,rt:!0},rt:{rp:!0,rt:!0},tbody:{tbody:!0,tfoot:!0},td:{td:!0,th:!0},tfoot:{tbody:!0},th:{td:!0,th:!0},thead:{tbody:!0,tfoot:!0},tr:{tr:!0}},doNotIndent:{pre:!0},allowUnquoted:!0,allowMissing:!0,caseFold:!0},v={autoSelfClosers:{},implicitlyClosed:{},contextGrabbers:{},doNotIndent:{},allowUnquoted:!1,allowMissing:!1,allowMissingTagName:!1,caseFold:!1};o.defineMode("xml",function(C,b){var S=C.indentUnit,s={},h=b.htmlMode?p:v;for(var g in h)s[g]=h[g];for(var g in b)s[g]=b[g];var T,w;function c(y,K){function X(le){return K.tokenize=le,le(y,K)}var N=y.next();if(N=="<")return y.eat("!")?y.eat("[")?y.match("CDATA[")?X(z("atom","]]>")):null:y.match("--")?X(z("comment","-->")):y.match("DOCTYPE",!0,!0)?(y.eatWhile(/[\w\._\-]/),X(M(1))):null:y.eat("?")?(y.eatWhile(/[\w\._\-]/),K.tokenize=z("meta","?>"),"meta"):(T=y.eat("/")?"closeTag":"openTag",K.tokenize=d,"tag bracket");if(N=="&"){var R;return y.eat("#")?y.eat("x")?R=y.eatWhile(/[a-fA-F\d]/)&&y.eat(";"):R=y.eatWhile(/[\d]/)&&y.eat(";"):R=y.eatWhile(/[\w\.\-:]/)&&y.eat(";"),R?"atom":"error"}else return y.eatWhile(/[^&<]/),null}c.isInText=!0;function d(y,K){var X=y.next();if(X==">"||X=="/"&&y.eat(">"))return K.tokenize=c,T=X==">"?"endTag":"selfcloseTag","tag bracket";if(X=="=")return T="equals",null;if(X=="<"){K.tokenize=c,K.state=G,K.tagName=K.tagStart=null;var N=K.tokenize(y,K);return N?N+" tag error":"tag error"}else return/[\'\"]/.test(X)?(K.tokenize=k(X),K.stringStartCol=y.column(),K.tokenize(y,K)):(y.match(/^[^\s\u00a0=<>\"\']*[^\s\u00a0=<>\"\'\/]/),"word")}function k(y){var K=function(X,N){for(;!X.eol();)if(X.next()==y){N.tokenize=d;break}return"string"};return K.isInAttribute=!0,K}function z(y,K){return function(X,N){for(;!X.eol();){if(X.match(K)){N.tokenize=c;break}X.next()}return y}}function M(y){return function(K,X){for(var N;(N=K.next())!=null;){if(N=="<")return X.tokenize=M(y+1),X.tokenize(K,X);if(N==">")if(y==1){X.tokenize=c;break}else return X.tokenize=M(y-1),X.tokenize(K,X)}return"meta"}}function _(y){return y&&y.toLowerCase()}function W(y,K,X){this.prev=y.context,this.tagName=K||"",this.indent=y.indented,this.startOfLine=X,(s.doNotIndent.hasOwnProperty(K)||y.context&&y.context.noIndent)&&(this.noIndent=!0)}function E(y){y.context&&(y.context=y.context.prev)}function O(y,K){for(var X;;){if(!y.context||(X=y.context.tagName,!s.contextGrabbers.hasOwnProperty(_(X))||!s.contextGrabbers[_(X)].hasOwnProperty(_(K))))return;E(y)}}function G(y,K,X){return y=="openTag"?(X.tagStart=K.column(),J):y=="closeTag"?re:G}function J(y,K,X){return y=="word"?(X.tagName=K.current(),w="tag",D):s.allowMissingTagName&&y=="endTag"?(w="tag bracket",D(y,K,X)):(w="error",J)}function re(y,K,X){if(y=="word"){var N=K.current();return X.context&&X.context.tagName!=N&&s.implicitlyClosed.hasOwnProperty(_(X.context.tagName))&&E(X),X.context&&X.context.tagName==N||s.matchClosing===!1?(w="tag",q):(w="tag error",I)}else return s.allowMissingTagName&&y=="endTag"?(w="tag bracket",q(y,K,X)):(w="error",I)}function q(y,K,X){return y!="endTag"?(w="error",q):(E(X),G)}function I(y,K,X){return w="error",q(y,K,X)}function D(y,K,X){if(y=="word")return w="attribute",Q;if(y=="endTag"||y=="selfcloseTag"){var N=X.tagName,R=X.tagStart;return X.tagName=X.tagStart=null,y=="selfcloseTag"||s.autoSelfClosers.hasOwnProperty(_(N))?O(X,N):(O(X,N),X.context=new W(X,N,R==X.indented)),G}return w="error",D}function Q(y,K,X){return y=="equals"?j:(s.allowMissing||(w="error"),D(y,K,X))}function j(y,K,X){return y=="string"?V:y=="word"&&s.allowUnquoted?(w="string",D):(w="error",D(y,K,X))}function V(y,K,X){return y=="string"?V:D(y,K,X)}return{startState:function(y){var K={tokenize:c,state:G,indented:y||0,tagName:null,tagStart:null,context:null};return y!=null&&(K.baseIndent=y),K},token:function(y,K){if(!K.tagName&&y.sol()&&(K.indented=y.indentation()),y.eatSpace())return null;T=null;var X=K.tokenize(y,K);return(X||T)&&X!="comment"&&(w=null,K.state=K.state(T||X,y,K),w&&(X=w=="error"?X+" error":w)),X},indent:function(y,K,X){var N=y.context;if(y.tokenize.isInAttribute)return y.tagStart==y.indented?y.stringStartCol+1:y.indented+S;if(N&&N.noIndent)return o.Pass;if(y.tokenize!=d&&y.tokenize!=c)return X?X.match(/^(\s*)/)[0].length:0;if(y.tagName)return s.multilineTagIndentPastTag!==!1?y.tagStart+y.tagName.length+2:y.tagStart+S*(s.multilineTagIndentFactor||1);if(s.alignCDATA&&/$/,blockCommentStart:"",configuration:s.htmlMode?"html":"xml",helperType:s.htmlMode?"html":"xml",skipAttribute:function(y){y.state==j&&(y.state=D)},xmlCurrentTag:function(y){return y.tagName?{name:y.tagName,close:y.type=="closeTag"}:null},xmlCurrentContext:function(y){for(var K=[],X=y.context;X;X=X.prev)K.push(X.tagName);return K.reverse()}}}),o.defineMIME("text/xml","xml"),o.defineMIME("application/xml","xml"),o.mimeModes.hasOwnProperty("text/html")||o.defineMIME("text/html",{name:"xml",htmlMode:!0})})});var vn=Ke((Is,Ns)=>{(function(o){typeof Is=="object"&&typeof Ns=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";o.defineMode("javascript",function(p,v){var C=p.indentUnit,b=v.statementIndent,S=v.jsonld,s=v.json||S,h=v.trackScope!==!1,g=v.typescript,T=v.wordCharacters||/[\w$\xa1-\uffff]/,w=(function(){function x(pt){return{type:pt,style:"keyword"}}var P=x("keyword a"),ae=x("keyword b"),he=x("keyword c"),ne=x("keyword d"),ye=x("operator"),Xe={type:"atom",style:"atom"};return{if:x("if"),while:P,with:P,else:ae,do:ae,try:ae,finally:ae,return:ne,break:ne,continue:ne,new:x("new"),delete:he,void:he,throw:he,debugger:x("debugger"),var:x("var"),const:x("var"),let:x("var"),function:x("function"),catch:x("catch"),for:x("for"),switch:x("switch"),case:x("case"),default:x("default"),in:ye,typeof:ye,instanceof:ye,true:Xe,false:Xe,null:Xe,undefined:Xe,NaN:Xe,Infinity:Xe,this:x("this"),class:x("class"),super:x("atom"),yield:he,export:x("export"),import:x("import"),extends:he,await:he}})(),c=/[+\-*&%=<>!?|~^@]/,d=/^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/;function k(x){for(var P=!1,ae,he=!1;(ae=x.next())!=null;){if(!P){if(ae=="/"&&!he)return;ae=="["?he=!0:he&&ae=="]"&&(he=!1)}P=!P&&ae=="\\"}}var z,M;function _(x,P,ae){return z=x,M=ae,P}function W(x,P){var ae=x.next();if(ae=='"'||ae=="'")return P.tokenize=E(ae),P.tokenize(x,P);if(ae=="."&&x.match(/^\d[\d_]*(?:[eE][+\-]?[\d_]+)?/))return _("number","number");if(ae=="."&&x.match(".."))return _("spread","meta");if(/[\[\]{}\(\),;\:\.]/.test(ae))return _(ae);if(ae=="="&&x.eat(">"))return _("=>","operator");if(ae=="0"&&x.match(/^(?:x[\dA-Fa-f_]+|o[0-7_]+|b[01_]+)n?/))return _("number","number");if(/\d/.test(ae))return x.match(/^[\d_]*(?:n|(?:\.[\d_]*)?(?:[eE][+\-]?[\d_]+)?)?/),_("number","number");if(ae=="/")return x.eat("*")?(P.tokenize=O,O(x,P)):x.eat("/")?(x.skipToEnd(),_("comment","comment")):Bt(x,P,1)?(k(x),x.match(/^\b(([gimyus])(?![gimyus]*\2))+\b/),_("regexp","string-2")):(x.eat("="),_("operator","operator",x.current()));if(ae=="`")return P.tokenize=G,G(x,P);if(ae=="#"&&x.peek()=="!")return x.skipToEnd(),_("meta","meta");if(ae=="#"&&x.eatWhile(T))return _("variable","property");if(ae=="<"&&x.match("!--")||ae=="-"&&x.match("->")&&!/\S/.test(x.string.slice(0,x.start)))return x.skipToEnd(),_("comment","comment");if(c.test(ae))return(ae!=">"||!P.lexical||P.lexical.type!=">")&&(x.eat("=")?(ae=="!"||ae=="=")&&x.eat("="):/[<>*+\-|&?]/.test(ae)&&(x.eat(ae),ae==">"&&x.eat(ae))),ae=="?"&&x.eat(".")?_("."):_("operator","operator",x.current());if(T.test(ae)){x.eatWhile(T);var he=x.current();if(P.lastType!="."){if(w.propertyIsEnumerable(he)){var ne=w[he];return _(ne.type,ne.style,he)}if(he=="async"&&x.match(/^(\s|\/\*([^*]|\*(?!\/))*?\*\/)*[\[\(\w]/,!1))return _("async","keyword",he)}return _("variable","variable",he)}}function E(x){return function(P,ae){var he=!1,ne;if(S&&P.peek()=="@"&&P.match(d))return ae.tokenize=W,_("jsonld-keyword","meta");for(;(ne=P.next())!=null&&!(ne==x&&!he);)he=!he&&ne=="\\";return he||(ae.tokenize=W),_("string","string")}}function O(x,P){for(var ae=!1,he;he=x.next();){if(he=="/"&&ae){P.tokenize=W;break}ae=he=="*"}return _("comment","comment")}function G(x,P){for(var ae=!1,he;(he=x.next())!=null;){if(!ae&&(he=="`"||he=="$"&&x.eat("{"))){P.tokenize=W;break}ae=!ae&&he=="\\"}return _("quasi","string-2",x.current())}var J="([{}])";function re(x,P){P.fatArrowAt&&(P.fatArrowAt=null);var ae=x.string.indexOf("=>",x.start);if(!(ae<0)){if(g){var he=/:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(x.string.slice(x.start,ae));he&&(ae=he.index)}for(var ne=0,ye=!1,Xe=ae-1;Xe>=0;--Xe){var pt=x.string.charAt(Xe),Et=J.indexOf(pt);if(Et>=0&&Et<3){if(!ne){++Xe;break}if(--ne==0){pt=="("&&(ye=!0);break}}else if(Et>=3&&Et<6)++ne;else if(T.test(pt))ye=!0;else if(/["'\/`]/.test(pt))for(;;--Xe){if(Xe==0)return;var Zr=x.string.charAt(Xe-1);if(Zr==pt&&x.string.charAt(Xe-2)!="\\"){Xe--;break}}else if(ye&&!ne){++Xe;break}}ye&&!ne&&(P.fatArrowAt=Xe)}}var q={atom:!0,number:!0,variable:!0,string:!0,regexp:!0,this:!0,import:!0,"jsonld-keyword":!0};function I(x,P,ae,he,ne,ye){this.indented=x,this.column=P,this.type=ae,this.prev=ne,this.info=ye,he!=null&&(this.align=he)}function D(x,P){if(!h)return!1;for(var ae=x.localVars;ae;ae=ae.next)if(ae.name==P)return!0;for(var he=x.context;he;he=he.prev)for(var ae=he.vars;ae;ae=ae.next)if(ae.name==P)return!0}function Q(x,P,ae,he,ne){var ye=x.cc;for(j.state=x,j.stream=ne,j.marked=null,j.cc=ye,j.style=P,x.lexical.hasOwnProperty("align")||(x.lexical.align=!0);;){var Xe=ye.length?ye.pop():s?Se:Oe;if(Xe(ae,he)){for(;ye.length&&ye[ye.length-1].lex;)ye.pop()();return j.marked?j.marked:ae=="variable"&&D(x,he)?"variable-2":P}}}var j={state:null,column:null,marked:null,cc:null};function V(){for(var x=arguments.length-1;x>=0;x--)j.cc.push(arguments[x])}function y(){return V.apply(null,arguments),!0}function K(x,P){for(var ae=P;ae;ae=ae.next)if(ae.name==x)return!0;return!1}function X(x){var P=j.state;if(j.marked="def",!!h){if(P.context){if(P.lexical.info=="var"&&P.context&&P.context.block){var ae=N(x,P.context);if(ae!=null){P.context=ae;return}}else if(!K(x,P.localVars)){P.localVars=new xe(x,P.localVars);return}}v.globalVars&&!K(x,P.globalVars)&&(P.globalVars=new xe(x,P.globalVars))}}function N(x,P){if(P)if(P.block){var ae=N(x,P.prev);return ae?ae==P.prev?P:new le(ae,P.vars,!0):null}else return K(x,P.vars)?P:new le(P.prev,new xe(x,P.vars),!1);else return null}function R(x){return x=="public"||x=="private"||x=="protected"||x=="abstract"||x=="readonly"}function le(x,P,ae){this.prev=x,this.vars=P,this.block=ae}function xe(x,P){this.name=x,this.next=P}var F=new xe("this",new xe("arguments",null));function L(){j.state.context=new le(j.state.context,j.state.localVars,!1),j.state.localVars=F}function de(){j.state.context=new le(j.state.context,j.state.localVars,!0),j.state.localVars=null}L.lex=de.lex=!0;function ze(){j.state.localVars=j.state.context.vars,j.state.context=j.state.context.prev}ze.lex=!0;function pe(x,P){var ae=function(){var he=j.state,ne=he.indented;if(he.lexical.type=="stat")ne=he.lexical.indented;else for(var ye=he.lexical;ye&&ye.type==")"&&ye.align;ye=ye.prev)ne=ye.indented;he.lexical=new I(ne,j.stream.column(),x,null,he.lexical,P)};return ae.lex=!0,ae}function Ee(){var x=j.state;x.lexical.prev&&(x.lexical.type==")"&&(x.indented=x.lexical.indented),x.lexical=x.lexical.prev)}Ee.lex=!0;function ge(x){function P(ae){return ae==x?y():x==";"||ae=="}"||ae==")"||ae=="]"?V():y(P)}return P}function Oe(x,P){return x=="var"?y(pe("vardef",P),Rr,ge(";"),Ee):x=="keyword a"?y(pe("form"),Ze,Oe,Ee):x=="keyword b"?y(pe("form"),Oe,Ee):x=="keyword d"?j.stream.match(/^\s*$/,!1)?y():y(pe("stat"),Je,ge(";"),Ee):x=="debugger"?y(ge(";")):x=="{"?y(pe("}"),de,De,Ee,ze):x==";"?y():x=="if"?(j.state.lexical.info=="else"&&j.state.cc[j.state.cc.length-1]==Ee&&j.state.cc.pop()(),y(pe("form"),Ze,Oe,Ee,Hr)):x=="function"?y(Ht):x=="for"?y(pe("form"),de,ei,Oe,ze,Ee):x=="class"||g&&P=="interface"?(j.marked="keyword",y(pe("form",x=="class"?x:P),Wr,Ee)):x=="variable"?g&&P=="declare"?(j.marked="keyword",y(Oe)):g&&(P=="module"||P=="enum"||P=="type")&&j.stream.match(/^\s*\w/,!1)?(j.marked="keyword",P=="enum"?y(Ae):P=="type"?y(ti,ge("operator"),Pe,ge(";")):y(pe("form"),Ct,ge("{"),pe("}"),De,Ee,Ee)):g&&P=="namespace"?(j.marked="keyword",y(pe("form"),Se,Oe,Ee)):g&&P=="abstract"?(j.marked="keyword",y(Oe)):y(pe("stat"),Ue):x=="switch"?y(pe("form"),Ze,ge("{"),pe("}","switch"),de,De,Ee,Ee,ze):x=="case"?y(Se,ge(":")):x=="default"?y(ge(":")):x=="catch"?y(pe("form"),L,qe,Oe,Ee,ze):x=="export"?y(pe("stat"),Ur,Ee):x=="import"?y(pe("stat"),gr,Ee):x=="async"?y(Oe):P=="@"?y(Se,Oe):V(pe("stat"),Se,ge(";"),Ee)}function qe(x){if(x=="(")return y($t,ge(")"))}function Se(x,P){return ke(x,P,!1)}function Be(x,P){return ke(x,P,!0)}function Ze(x){return x!="("?V():y(pe(")"),Je,ge(")"),Ee)}function ke(x,P,ae){if(j.state.fatArrowAt==j.stream.start){var he=ae?He:ce;if(x=="(")return y(L,pe(")"),H($t,")"),Ee,ge("=>"),he,ze);if(x=="variable")return V(L,Ct,ge("=>"),he,ze)}var ne=ae?Ge:Re;return q.hasOwnProperty(x)?y(ne):x=="function"?y(Ht,ne):x=="class"||g&&P=="interface"?(j.marked="keyword",y(pe("form"),to,Ee)):x=="keyword c"||x=="async"?y(ae?Be:Se):x=="("?y(pe(")"),Je,ge(")"),Ee,ne):x=="operator"||x=="spread"?y(ae?Be:Se):x=="["?y(pe("]"),at,Ee,ne):x=="{"?se(Me,"}",null,ne):x=="quasi"?V(U,ne):x=="new"?y(te(ae)):y()}function Je(x){return x.match(/[;\}\)\],]/)?V():V(Se)}function Re(x,P){return x==","?y(Je):Ge(x,P,!1)}function Ge(x,P,ae){var he=ae==!1?Re:Ge,ne=ae==!1?Se:Be;if(x=="=>")return y(L,ae?He:ce,ze);if(x=="operator")return/\+\+|--/.test(P)||g&&P=="!"?y(he):g&&P=="<"&&j.stream.match(/^([^<>]|<[^<>]*>)*>\s*\(/,!1)?y(pe(">"),H(Pe,">"),Ee,he):P=="?"?y(Se,ge(":"),ne):y(ne);if(x=="quasi")return V(U,he);if(x!=";"){if(x=="(")return se(Be,")","call",he);if(x==".")return y(we,he);if(x=="[")return y(pe("]"),Je,ge("]"),Ee,he);if(g&&P=="as")return j.marked="keyword",y(Pe,he);if(x=="regexp")return j.state.lastType=j.marked="operator",j.stream.backUp(j.stream.pos-j.stream.start-1),y(ne)}}function U(x,P){return x!="quasi"?V():P.slice(P.length-2)!="${"?y(U):y(Je,Z)}function Z(x){if(x=="}")return j.marked="string-2",j.state.tokenize=G,y(U)}function ce(x){return re(j.stream,j.state),V(x=="{"?Oe:Se)}function He(x){return re(j.stream,j.state),V(x=="{"?Oe:Be)}function te(x){return function(P){return P=="."?y(x?oe:fe):P=="variable"&&g?y(It,x?Ge:Re):V(x?Be:Se)}}function fe(x,P){if(P=="target")return j.marked="keyword",y(Re)}function oe(x,P){if(P=="target")return j.marked="keyword",y(Ge)}function Ue(x){return x==":"?y(Ee,Oe):V(Re,ge(";"),Ee)}function we(x){if(x=="variable")return j.marked="property",y()}function Me(x,P){if(x=="async")return j.marked="property",y(Me);if(x=="variable"||j.style=="keyword"){if(j.marked="property",P=="get"||P=="set")return y(Le);var ae;return g&&j.state.fatArrowAt==j.stream.start&&(ae=j.stream.match(/^\s*:\s*/,!1))&&(j.state.fatArrowAt=j.stream.pos+ae[0].length),y($)}else{if(x=="number"||x=="string")return j.marked=S?"property":j.style+" property",y($);if(x=="jsonld-keyword")return y($);if(g&&R(P))return j.marked="keyword",y(Me);if(x=="[")return y(Se,nt,ge("]"),$);if(x=="spread")return y(Be,$);if(P=="*")return j.marked="keyword",y(Me);if(x==":")return V($)}}function Le(x){return x!="variable"?V($):(j.marked="property",y(Ht))}function $(x){if(x==":")return y(Be);if(x=="(")return V(Ht)}function H(x,P,ae){function he(ne,ye){if(ae?ae.indexOf(ne)>-1:ne==","){var Xe=j.state.lexical;return Xe.info=="call"&&(Xe.pos=(Xe.pos||0)+1),y(function(pt,Et){return pt==P||Et==P?V():V(x)},he)}return ne==P||ye==P?y():ae&&ae.indexOf(";")>-1?V(x):y(ge(P))}return function(ne,ye){return ne==P||ye==P?y():V(x,he)}}function se(x,P,ae){for(var he=3;he"),Pe);if(x=="quasi")return V(_t,Rt)}function xt(x){if(x=="=>")return y(Pe)}function Ie(x){return x.match(/[\}\)\]]/)?y():x==","||x==";"?y(Ie):V(nr,Ie)}function nr(x,P){if(x=="variable"||j.style=="keyword")return j.marked="property",y(nr);if(P=="?"||x=="number"||x=="string")return y(nr);if(x==":")return y(Pe);if(x=="[")return y(ge("variable"),dt,ge("]"),nr);if(x=="(")return V(hr,nr);if(!x.match(/[;\}\)\],]/))return y()}function _t(x,P){return x!="quasi"?V():P.slice(P.length-2)!="${"?y(_t):y(Pe,it)}function it(x){if(x=="}")return j.marked="string-2",j.state.tokenize=G,y(_t)}function ot(x,P){return x=="variable"&&j.stream.match(/^\s*[?:]/,!1)||P=="?"?y(ot):x==":"?y(Pe):x=="spread"?y(ot):V(Pe)}function Rt(x,P){if(P=="<")return y(pe(">"),H(Pe,">"),Ee,Rt);if(P=="|"||x=="."||P=="&")return y(Pe);if(x=="[")return y(Pe,ge("]"),Rt);if(P=="extends"||P=="implements")return j.marked="keyword",y(Pe);if(P=="?")return y(Pe,ge(":"),Pe)}function It(x,P){if(P=="<")return y(pe(">"),H(Pe,">"),Ee,Rt)}function Wt(){return V(Pe,kt)}function kt(x,P){if(P=="=")return y(Pe)}function Rr(x,P){return P=="enum"?(j.marked="keyword",y(Ae)):V(Ct,nt,Ut,eo)}function Ct(x,P){if(g&&R(P))return j.marked="keyword",y(Ct);if(x=="variable")return X(P),y();if(x=="spread")return y(Ct);if(x=="[")return se(yn,"]");if(x=="{")return se(dr,"}")}function dr(x,P){return x=="variable"&&!j.stream.match(/^\s*:/,!1)?(X(P),y(Ut)):(x=="variable"&&(j.marked="property"),x=="spread"?y(Ct):x=="}"?V():x=="["?y(Se,ge("]"),ge(":"),dr):y(ge(":"),Ct,Ut))}function yn(){return V(Ct,Ut)}function Ut(x,P){if(P=="=")return y(Be)}function eo(x){if(x==",")return y(Rr)}function Hr(x,P){if(x=="keyword b"&&P=="else")return y(pe("form","else"),Oe,Ee)}function ei(x,P){if(P=="await")return y(ei);if(x=="(")return y(pe(")"),xn,Ee)}function xn(x){return x=="var"?y(Rr,pr):x=="variable"?y(pr):V(pr)}function pr(x,P){return x==")"?y():x==";"?y(pr):P=="in"||P=="of"?(j.marked="keyword",y(Se,pr)):V(Se,pr)}function Ht(x,P){if(P=="*")return j.marked="keyword",y(Ht);if(x=="variable")return X(P),y(Ht);if(x=="(")return y(L,pe(")"),H($t,")"),Ee,Pt,Oe,ze);if(g&&P=="<")return y(pe(">"),H(Wt,">"),Ee,Ht)}function hr(x,P){if(P=="*")return j.marked="keyword",y(hr);if(x=="variable")return X(P),y(hr);if(x=="(")return y(L,pe(")"),H($t,")"),Ee,Pt,ze);if(g&&P=="<")return y(pe(">"),H(Wt,">"),Ee,hr)}function ti(x,P){if(x=="keyword"||x=="variable")return j.marked="type",y(ti);if(P=="<")return y(pe(">"),H(Wt,">"),Ee)}function $t(x,P){return P=="@"&&y(Se,$t),x=="spread"?y($t):g&&R(P)?(j.marked="keyword",y($t)):g&&x=="this"?y(nt,Ut):V(Ct,nt,Ut)}function to(x,P){return x=="variable"?Wr(x,P):Kt(x,P)}function Wr(x,P){if(x=="variable")return X(P),y(Kt)}function Kt(x,P){if(P=="<")return y(pe(">"),H(Wt,">"),Ee,Kt);if(P=="extends"||P=="implements"||g&&x==",")return P=="implements"&&(j.marked="keyword"),y(g?Pe:Se,Kt);if(x=="{")return y(pe("}"),Gt,Ee)}function Gt(x,P){if(x=="async"||x=="variable"&&(P=="static"||P=="get"||P=="set"||g&&R(P))&&j.stream.match(/^\s+#?[\w$\xa1-\uffff]/,!1))return j.marked="keyword",y(Gt);if(x=="variable"||j.style=="keyword")return j.marked="property",y(Cr,Gt);if(x=="number"||x=="string")return y(Cr,Gt);if(x=="[")return y(Se,nt,ge("]"),Cr,Gt);if(P=="*")return j.marked="keyword",y(Gt);if(g&&x=="(")return V(hr,Gt);if(x==";"||x==",")return y(Gt);if(x=="}")return y();if(P=="@")return y(Se,Gt)}function Cr(x,P){if(P=="!"||P=="?")return y(Cr);if(x==":")return y(Pe,Ut);if(P=="=")return y(Be);var ae=j.state.lexical.prev,he=ae&&ae.info=="interface";return V(he?hr:Ht)}function Ur(x,P){return P=="*"?(j.marked="keyword",y(Gr,ge(";"))):P=="default"?(j.marked="keyword",y(Se,ge(";"))):x=="{"?y(H($r,"}"),Gr,ge(";")):V(Oe)}function $r(x,P){if(P=="as")return j.marked="keyword",y(ge("variable"));if(x=="variable")return V(Be,$r)}function gr(x){return x=="string"?y():x=="("?V(Se):x=="."?V(Re):V(Kr,Vt,Gr)}function Kr(x,P){return x=="{"?se(Kr,"}"):(x=="variable"&&X(P),P=="*"&&(j.marked="keyword"),y(_n))}function Vt(x){if(x==",")return y(Kr,Vt)}function _n(x,P){if(P=="as")return j.marked="keyword",y(Kr)}function Gr(x,P){if(P=="from")return j.marked="keyword",y(Se)}function at(x){return x=="]"?y():V(H(Be,"]"))}function Ae(){return V(pe("form"),Ct,ge("{"),pe("}"),H(ir,"}"),Ee,Ee)}function ir(){return V(Ct,Ut)}function kn(x,P){return x.lastType=="operator"||x.lastType==","||c.test(P.charAt(0))||/[,.]/.test(P.charAt(0))}function Bt(x,P,ae){return P.tokenize==W&&/^(?:operator|sof|keyword [bcd]|case|new|export|default|spread|[\[{}\(,;:]|=>)$/.test(P.lastType)||P.lastType=="quasi"&&/\{\s*$/.test(x.string.slice(0,x.pos-(ae||0)))}return{startState:function(x){var P={tokenize:W,lastType:"sof",cc:[],lexical:new I((x||0)-C,0,"block",!1),localVars:v.localVars,context:v.localVars&&new le(null,null,!1),indented:x||0};return v.globalVars&&typeof v.globalVars=="object"&&(P.globalVars=v.globalVars),P},token:function(x,P){if(x.sol()&&(P.lexical.hasOwnProperty("align")||(P.lexical.align=!1),P.indented=x.indentation(),re(x,P)),P.tokenize!=O&&x.eatSpace())return null;var ae=P.tokenize(x,P);return z=="comment"?ae:(P.lastType=z=="operator"&&(M=="++"||M=="--")?"incdec":z,Q(P,ae,z,M,x))},indent:function(x,P){if(x.tokenize==O||x.tokenize==G)return o.Pass;if(x.tokenize!=W)return 0;var ae=P&&P.charAt(0),he=x.lexical,ne;if(!/^\s*else\b/.test(P))for(var ye=x.cc.length-1;ye>=0;--ye){var Xe=x.cc[ye];if(Xe==Ee)he=he.prev;else if(Xe!=Hr&&Xe!=ze)break}for(;(he.type=="stat"||he.type=="form")&&(ae=="}"||(ne=x.cc[x.cc.length-1])&&(ne==Re||ne==Ge)&&!/^[,\.=+\-*:?[\(]/.test(P));)he=he.prev;b&&he.type==")"&&he.prev.type=="stat"&&(he=he.prev);var pt=he.type,Et=ae==pt;return pt=="vardef"?he.indented+(x.lastType=="operator"||x.lastType==","?he.info.length+1:0):pt=="form"&&ae=="{"?he.indented:pt=="form"?he.indented+C:pt=="stat"?he.indented+(kn(x,P)?b||C:0):he.info=="switch"&&!Et&&v.doubleIndentSwitch!=!1?he.indented+(/^(?:case|default)\b/.test(P)?C:2*C):he.align?he.column+(Et?0:1):he.indented+(Et?0:C)},electricInput:/^\s*(?:case .*?:|default:|\{|\})$/,blockCommentStart:s?null:"/*",blockCommentEnd:s?null:"*/",blockCommentContinue:s?null:" * ",lineComment:s?null:"//",fold:"brace",closeBrackets:"()[]{}''\"\"``",helperType:s?"json":"javascript",jsonldMode:S,jsonMode:s,expressionAllowed:Bt,skipExpression:function(x){Q(x,"atom","atom","true",new o.StringStream("",2,null))}}}),o.registerHelper("wordChars","javascript",/[\w$]/),o.defineMIME("text/javascript","javascript"),o.defineMIME("text/ecmascript","javascript"),o.defineMIME("application/javascript","javascript"),o.defineMIME("application/x-javascript","javascript"),o.defineMIME("application/ecmascript","javascript"),o.defineMIME("application/json",{name:"javascript",json:!0}),o.defineMIME("application/x-json",{name:"javascript",json:!0}),o.defineMIME("application/manifest+json",{name:"javascript",json:!0}),o.defineMIME("application/ld+json",{name:"javascript",jsonld:!0}),o.defineMIME("text/typescript",{name:"javascript",typescript:!0}),o.defineMIME("application/typescript",{name:"javascript",typescript:!0})})});var Qn=Ke((Os,Ps)=>{(function(o){typeof Os=="object"&&typeof Ps=="object"?o(We(),mn(),vn(),gn()):typeof define=="function"&&define.amd?define(["../../lib/codemirror","../xml/xml","../javascript/javascript","../css/css"],o):o(CodeMirror)})(function(o){"use strict";var p={script:[["lang",/(javascript|babel)/i,"javascript"],["type",/^(?:text|application)\/(?:x-)?(?:java|ecma)script$|^module$|^$/i,"javascript"],["type",/./,"text/plain"],[null,null,"javascript"]],style:[["lang",/^css$/i,"css"],["type",/^(text\/)?(x-)?(stylesheet|css)$/i,"css"],["type",/./,"text/plain"],[null,null,"css"]]};function v(T,w,c){var d=T.current(),k=d.search(w);return k>-1?T.backUp(d.length-k):d.match(/<\/?$/)&&(T.backUp(d.length),T.match(w,!1)||T.match(d)),c}var C={};function b(T){var w=C[T];return w||(C[T]=new RegExp("\\s+"+T+`\\s*=\\s*('|")?([^'"]+)('|")?\\s*`))}function S(T,w){var c=T.match(b(w));return c?/^\s*(.*?)\s*$/.exec(c[2])[1]:""}function s(T,w){return new RegExp((w?"^":"")+"","i")}function h(T,w){for(var c in T)for(var d=w[c]||(w[c]=[]),k=T[c],z=k.length-1;z>=0;z--)d.unshift(k[z])}function g(T,w){for(var c=0;c=0;M--)d.script.unshift(["type",z[M].matches,z[M].mode]);function _(W,E){var O=c.token(W,E.htmlState),G=/\btag\b/.test(O),J;if(G&&!/[<>\s\/]/.test(W.current())&&(J=E.htmlState.tagName&&E.htmlState.tagName.toLowerCase())&&d.hasOwnProperty(J))E.inTag=J+" ";else if(E.inTag&&G&&/>$/.test(W.current())){var re=/^([\S]+) (.*)/.exec(E.inTag);E.inTag=null;var q=W.current()==">"&&g(d[re[1]],re[2]),I=o.getMode(T,q),D=s(re[1],!0),Q=s(re[1],!1);E.token=function(j,V){return j.match(D,!1)?(V.token=_,V.localState=V.localMode=null,null):v(j,Q,V.localMode.token(j,V.localState))},E.localMode=I,E.localState=o.startState(I,c.indent(E.htmlState,"",""))}else E.inTag&&(E.inTag+=W.current(),W.eol()&&(E.inTag+=" "));return O}return{startState:function(){var W=o.startState(c);return{token:_,inTag:null,localMode:null,localState:null,htmlState:W}},copyState:function(W){var E;return W.localState&&(E=o.copyState(W.localMode,W.localState)),{token:W.token,inTag:W.inTag,localMode:W.localMode,localState:E,htmlState:o.copyState(c,W.htmlState)}},token:function(W,E){return E.token(W,E)},indent:function(W,E,O){return!W.localMode||/^\s*<\//.test(E)?c.indent(W.htmlState,E,O):W.localMode.indent?W.localMode.indent(W.localState,E,O):o.Pass},innerMode:function(W){return{state:W.localState||W.htmlState,mode:W.localMode||c}}}},"xml","javascript","css"),o.defineMIME("text/html","htmlmixed")})});var Rs=Ke((Bs,js)=>{(function(o){typeof Bs=="object"&&typeof js=="object"?o(We(),Qn(),Yn()):typeof define=="function"&&define.amd?define(["../../lib/codemirror","../htmlmixed/htmlmixed","../../addon/mode/overlay"],o):o(CodeMirror)})(function(o){"use strict";o.defineMode("django:inner",function(){var p=["block","endblock","for","endfor","true","false","filter","endfilter","loop","none","self","super","if","elif","endif","as","else","import","with","endwith","without","context","ifequal","endifequal","ifnotequal","endifnotequal","extends","include","load","comment","endcomment","empty","url","static","trans","blocktrans","endblocktrans","now","regroup","lorem","ifchanged","endifchanged","firstof","debug","cycle","csrf_token","autoescape","endautoescape","spaceless","endspaceless","ssi","templatetag","verbatim","endverbatim","widthratio"],v=["add","addslashes","capfirst","center","cut","date","default","default_if_none","dictsort","dictsortreversed","divisibleby","escape","escapejs","filesizeformat","first","floatformat","force_escape","get_digit","iriencode","join","last","length","length_is","linebreaks","linebreaksbr","linenumbers","ljust","lower","make_list","phone2numeric","pluralize","pprint","random","removetags","rjust","safe","safeseq","slice","slugify","stringformat","striptags","time","timesince","timeuntil","title","truncatechars","truncatechars_html","truncatewords","truncatewords_html","unordered_list","upper","urlencode","urlize","urlizetrunc","wordcount","wordwrap","yesno"],C=["==","!=","<",">","<=",">="],b=["in","not","or","and"];p=new RegExp("^\\b("+p.join("|")+")\\b"),v=new RegExp("^\\b("+v.join("|")+")\\b"),C=new RegExp("^\\b("+C.join("|")+")\\b"),b=new RegExp("^\\b("+b.join("|")+")\\b");function S(c,d){if(c.match("{{"))return d.tokenize=h,"tag";if(c.match("{%"))return d.tokenize=g,"tag";if(c.match("{#"))return d.tokenize=T,"comment";for(;c.next()!=null&&!c.match(/\{[{%#]/,!1););return null}function s(c,d){return function(k,z){if(!z.escapeNext&&k.eat(c))z.tokenize=d;else{z.escapeNext&&(z.escapeNext=!1);var M=k.next();M=="\\"&&(z.escapeNext=!0)}return"string"}}function h(c,d){if(d.waitDot){if(d.waitDot=!1,c.peek()!=".")return"null";if(c.match(/\.\W+/))return"error";if(c.eat("."))return d.waitProperty=!0,"null";throw Error("Unexpected error while waiting for property.")}if(d.waitPipe){if(d.waitPipe=!1,c.peek()!="|")return"null";if(c.match(/\.\W+/))return"error";if(c.eat("|"))return d.waitFilter=!0,"null";throw Error("Unexpected error while waiting for filter.")}return d.waitProperty&&(d.waitProperty=!1,c.match(/\b(\w+)\b/))?(d.waitDot=!0,d.waitPipe=!0,"property"):d.waitFilter&&(d.waitFilter=!1,c.match(v))?"variable-2":c.eatSpace()?(d.waitProperty=!1,"null"):c.match(/\b\d+(\.\d+)?\b/)?"number":c.match("'")?(d.tokenize=s("'",d.tokenize),"string"):c.match('"')?(d.tokenize=s('"',d.tokenize),"string"):c.match(/\b(\w+)\b/)&&!d.foundVariable?(d.waitDot=!0,d.waitPipe=!0,"variable"):c.match("}}")?(d.waitProperty=null,d.waitFilter=null,d.waitDot=null,d.waitPipe=null,d.tokenize=S,"tag"):(c.next(),"null")}function g(c,d){if(d.waitDot){if(d.waitDot=!1,c.peek()!=".")return"null";if(c.match(/\.\W+/))return"error";if(c.eat("."))return d.waitProperty=!0,"null";throw Error("Unexpected error while waiting for property.")}if(d.waitPipe){if(d.waitPipe=!1,c.peek()!="|")return"null";if(c.match(/\.\W+/))return"error";if(c.eat("|"))return d.waitFilter=!0,"null";throw Error("Unexpected error while waiting for filter.")}if(d.waitProperty&&(d.waitProperty=!1,c.match(/\b(\w+)\b/)))return d.waitDot=!0,d.waitPipe=!0,"property";if(d.waitFilter&&(d.waitFilter=!1,c.match(v)))return"variable-2";if(c.eatSpace())return d.waitProperty=!1,"null";if(c.match(/\b\d+(\.\d+)?\b/))return"number";if(c.match("'"))return d.tokenize=s("'",d.tokenize),"string";if(c.match('"'))return d.tokenize=s('"',d.tokenize),"string";if(c.match(C))return"operator";if(c.match(b))return"keyword";var k=c.match(p);return k?(k[0]=="comment"&&(d.blockCommentTag=!0),"keyword"):c.match(/\b(\w+)\b/)?(d.waitDot=!0,d.waitPipe=!0,"variable"):c.match("%}")?(d.waitProperty=null,d.waitFilter=null,d.waitDot=null,d.waitPipe=null,d.blockCommentTag?(d.blockCommentTag=!1,d.tokenize=w):d.tokenize=S,"tag"):(c.next(),"null")}function T(c,d){return c.match(/^.*?#\}/)?d.tokenize=S:c.skipToEnd(),"comment"}function w(c,d){return c.match(/\{%\s*endcomment\s*%\}/,!1)?(d.tokenize=g,c.match("{%"),"tag"):(c.next(),"comment")}return{startState:function(){return{tokenize:S}},token:function(c,d){return d.tokenize(c,d)},blockCommentStart:"{% comment %}",blockCommentEnd:"{% endcomment %}"}}),o.defineMode("django",function(p){var v=o.getMode(p,"text/html"),C=o.getMode(p,"django:inner");return o.overlayMode(v,C)}),o.defineMIME("text/x-django","django")})});var Di=Ke((Hs,Ws)=>{(function(o){typeof Hs=="object"&&typeof Ws=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";o.defineSimpleMode=function(w,c){o.defineMode(w,function(d){return o.simpleMode(d,c)})},o.simpleMode=function(w,c){p(c,"start");var d={},k=c.meta||{},z=!1;for(var M in c)if(M!=k&&c.hasOwnProperty(M))for(var _=d[M]=[],W=c[M],E=0;E2&&O.token&&typeof O.token!="string"){for(var re=2;re-1)return o.Pass;var M=d.indent.length-1,_=w[d.state];e:for(;;){for(var W=0;W<_.length;W++){var E=_[W];if(E.data.dedent&&E.data.dedentIfLineStart!==!1){var O=E.regex.exec(k);if(O&&O[0]){M--,(E.next||E.push)&&(_=w[E.next||E.push]),k=k.slice(O[0].length);continue e}}}break}return M<0?0:d.indent[M]}}})});var Ks=Ke((Us,$s)=>{(function(o){typeof Us=="object"&&typeof $s=="object"?o(We(),Di()):typeof define=="function"&&define.amd?define(["../../lib/codemirror","../../addon/mode/simple"],o):o(CodeMirror)})(function(o){"use strict";var p="from",v=new RegExp("^(\\s*)\\b("+p+")\\b","i"),C=["run","cmd","entrypoint","shell"],b=new RegExp("^(\\s*)("+C.join("|")+")(\\s+\\[)","i"),S="expose",s=new RegExp("^(\\s*)("+S+")(\\s+)","i"),h=["arg","from","maintainer","label","env","add","copy","volume","user","workdir","onbuild","stopsignal","healthcheck","shell"],g=[p,S].concat(C).concat(h),T="("+g.join("|")+")",w=new RegExp("^(\\s*)"+T+"(\\s*)(#.*)?$","i"),c=new RegExp("^(\\s*)"+T+"(\\s+)","i");o.defineSimpleMode("dockerfile",{start:[{regex:/^\s*#.*$/,sol:!0,token:"comment"},{regex:v,token:[null,"keyword"],sol:!0,next:"from"},{regex:w,token:[null,"keyword",null,"error"],sol:!0},{regex:b,token:[null,"keyword",null],sol:!0,next:"array"},{regex:s,token:[null,"keyword",null],sol:!0,next:"expose"},{regex:c,token:[null,"keyword",null],sol:!0,next:"arguments"},{regex:/./,token:null}],from:[{regex:/\s*$/,token:null,next:"start"},{regex:/(\s*)(#.*)$/,token:[null,"error"],next:"start"},{regex:/(\s*\S+\s+)(as)/i,token:[null,"keyword"],next:"start"},{token:null,next:"start"}],single:[{regex:/(?:[^\\']|\\.)/,token:"string"},{regex:/'/,token:"string",pop:!0}],double:[{regex:/(?:[^\\"]|\\.)/,token:"string"},{regex:/"/,token:"string",pop:!0}],array:[{regex:/\]/,token:null,next:"start"},{regex:/"(?:[^\\"]|\\.)*"?/,token:"string"}],expose:[{regex:/\d+$/,token:"number",next:"start"},{regex:/[^\d]+$/,token:null,next:"start"},{regex:/\d+/,token:"number"},{regex:/[^\d]+/,token:null},{token:null,next:"start"}],arguments:[{regex:/^\s*#.*$/,sol:!0,token:"comment"},{regex:/"(?:[^\\"]|\\.)*"?$/,token:"string",next:"start"},{regex:/"/,token:"string",push:"double"},{regex:/'(?:[^\\']|\\.)*'?$/,token:"string",next:"start"},{regex:/'/,token:"string",push:"single"},{regex:/[^#"']+[\\`]$/,token:null},{regex:/[^#"']+$/,token:null,next:"start"},{regex:/[^#"']+/,token:null},{token:null,next:"start"}],meta:{lineComment:"#"}}),o.defineMIME("text/x-dockerfile","dockerfile")})});var Xs=Ke((Gs,Zs)=>{(function(o){typeof Gs=="object"&&typeof Zs=="object"?o(We()):typeof define=="function"&&define.amd?define(["../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";o.modeInfo=[{name:"APL",mime:"text/apl",mode:"apl",ext:["dyalog","apl"]},{name:"PGP",mimes:["application/pgp","application/pgp-encrypted","application/pgp-keys","application/pgp-signature"],mode:"asciiarmor",ext:["asc","pgp","sig"]},{name:"ASN.1",mime:"text/x-ttcn-asn",mode:"asn.1",ext:["asn","asn1"]},{name:"Asterisk",mime:"text/x-asterisk",mode:"asterisk",file:/^extensions\.conf$/i},{name:"Brainfuck",mime:"text/x-brainfuck",mode:"brainfuck",ext:["b","bf"]},{name:"C",mime:"text/x-csrc",mode:"clike",ext:["c","h","ino"]},{name:"C++",mime:"text/x-c++src",mode:"clike",ext:["cpp","c++","cc","cxx","hpp","h++","hh","hxx"],alias:["cpp"]},{name:"Cobol",mime:"text/x-cobol",mode:"cobol",ext:["cob","cpy","cbl"]},{name:"C#",mime:"text/x-csharp",mode:"clike",ext:["cs"],alias:["csharp","cs"]},{name:"Clojure",mime:"text/x-clojure",mode:"clojure",ext:["clj","cljc","cljx"]},{name:"ClojureScript",mime:"text/x-clojurescript",mode:"clojure",ext:["cljs"]},{name:"Closure Stylesheets (GSS)",mime:"text/x-gss",mode:"css",ext:["gss"]},{name:"CMake",mime:"text/x-cmake",mode:"cmake",ext:["cmake","cmake.in"],file:/^CMakeLists\.txt$/},{name:"CoffeeScript",mimes:["application/vnd.coffeescript","text/coffeescript","text/x-coffeescript"],mode:"coffeescript",ext:["coffee"],alias:["coffee","coffee-script"]},{name:"Common Lisp",mime:"text/x-common-lisp",mode:"commonlisp",ext:["cl","lisp","el"],alias:["lisp"]},{name:"Cypher",mime:"application/x-cypher-query",mode:"cypher",ext:["cyp","cypher"]},{name:"Cython",mime:"text/x-cython",mode:"python",ext:["pyx","pxd","pxi"]},{name:"Crystal",mime:"text/x-crystal",mode:"crystal",ext:["cr"]},{name:"CSS",mime:"text/css",mode:"css",ext:["css"]},{name:"CQL",mime:"text/x-cassandra",mode:"sql",ext:["cql"]},{name:"D",mime:"text/x-d",mode:"d",ext:["d"]},{name:"Dart",mimes:["application/dart","text/x-dart"],mode:"dart",ext:["dart"]},{name:"diff",mime:"text/x-diff",mode:"diff",ext:["diff","patch"]},{name:"Django",mime:"text/x-django",mode:"django"},{name:"Dockerfile",mime:"text/x-dockerfile",mode:"dockerfile",file:/^Dockerfile$/},{name:"DTD",mime:"application/xml-dtd",mode:"dtd",ext:["dtd"]},{name:"Dylan",mime:"text/x-dylan",mode:"dylan",ext:["dylan","dyl","intr"]},{name:"EBNF",mime:"text/x-ebnf",mode:"ebnf"},{name:"ECL",mime:"text/x-ecl",mode:"ecl",ext:["ecl"]},{name:"edn",mime:"application/edn",mode:"clojure",ext:["edn"]},{name:"Eiffel",mime:"text/x-eiffel",mode:"eiffel",ext:["e"]},{name:"Elm",mime:"text/x-elm",mode:"elm",ext:["elm"]},{name:"Embedded JavaScript",mime:"application/x-ejs",mode:"htmlembedded",ext:["ejs"]},{name:"Embedded Ruby",mime:"application/x-erb",mode:"htmlembedded",ext:["erb"]},{name:"Erlang",mime:"text/x-erlang",mode:"erlang",ext:["erl"]},{name:"Esper",mime:"text/x-esper",mode:"sql"},{name:"Factor",mime:"text/x-factor",mode:"factor",ext:["factor"]},{name:"FCL",mime:"text/x-fcl",mode:"fcl"},{name:"Forth",mime:"text/x-forth",mode:"forth",ext:["forth","fth","4th"]},{name:"Fortran",mime:"text/x-fortran",mode:"fortran",ext:["f","for","f77","f90","f95"]},{name:"F#",mime:"text/x-fsharp",mode:"mllike",ext:["fs"],alias:["fsharp"]},{name:"Gas",mime:"text/x-gas",mode:"gas",ext:["s"]},{name:"Gherkin",mime:"text/x-feature",mode:"gherkin",ext:["feature"]},{name:"GitHub Flavored Markdown",mime:"text/x-gfm",mode:"gfm",file:/^(readme|contributing|history)\.md$/i},{name:"Go",mime:"text/x-go",mode:"go",ext:["go"]},{name:"Groovy",mime:"text/x-groovy",mode:"groovy",ext:["groovy","gradle"],file:/^Jenkinsfile$/},{name:"HAML",mime:"text/x-haml",mode:"haml",ext:["haml"]},{name:"Haskell",mime:"text/x-haskell",mode:"haskell",ext:["hs"]},{name:"Haskell (Literate)",mime:"text/x-literate-haskell",mode:"haskell-literate",ext:["lhs"]},{name:"Haxe",mime:"text/x-haxe",mode:"haxe",ext:["hx"]},{name:"HXML",mime:"text/x-hxml",mode:"haxe",ext:["hxml"]},{name:"ASP.NET",mime:"application/x-aspx",mode:"htmlembedded",ext:["aspx"],alias:["asp","aspx"]},{name:"HTML",mime:"text/html",mode:"htmlmixed",ext:["html","htm","handlebars","hbs"],alias:["xhtml"]},{name:"HTTP",mime:"message/http",mode:"http"},{name:"IDL",mime:"text/x-idl",mode:"idl",ext:["pro"]},{name:"Pug",mime:"text/x-pug",mode:"pug",ext:["jade","pug"],alias:["jade"]},{name:"Java",mime:"text/x-java",mode:"clike",ext:["java"]},{name:"Java Server Pages",mime:"application/x-jsp",mode:"htmlembedded",ext:["jsp"],alias:["jsp"]},{name:"JavaScript",mimes:["text/javascript","text/ecmascript","application/javascript","application/x-javascript","application/ecmascript"],mode:"javascript",ext:["js"],alias:["ecmascript","js","node"]},{name:"JSON",mimes:["application/json","application/x-json"],mode:"javascript",ext:["json","map"],alias:["json5"]},{name:"JSON-LD",mime:"application/ld+json",mode:"javascript",ext:["jsonld"],alias:["jsonld"]},{name:"JSX",mime:"text/jsx",mode:"jsx",ext:["jsx"]},{name:"Jinja2",mime:"text/jinja2",mode:"jinja2",ext:["j2","jinja","jinja2"]},{name:"Julia",mime:"text/x-julia",mode:"julia",ext:["jl"],alias:["jl"]},{name:"Kotlin",mime:"text/x-kotlin",mode:"clike",ext:["kt"]},{name:"LESS",mime:"text/x-less",mode:"css",ext:["less"]},{name:"LiveScript",mime:"text/x-livescript",mode:"livescript",ext:["ls"],alias:["ls"]},{name:"Lua",mime:"text/x-lua",mode:"lua",ext:["lua"]},{name:"Markdown",mime:"text/x-markdown",mode:"markdown",ext:["markdown","md","mkd"]},{name:"mIRC",mime:"text/mirc",mode:"mirc"},{name:"MariaDB SQL",mime:"text/x-mariadb",mode:"sql"},{name:"Mathematica",mime:"text/x-mathematica",mode:"mathematica",ext:["m","nb","wl","wls"]},{name:"Modelica",mime:"text/x-modelica",mode:"modelica",ext:["mo"]},{name:"MUMPS",mime:"text/x-mumps",mode:"mumps",ext:["mps"]},{name:"MS SQL",mime:"text/x-mssql",mode:"sql"},{name:"mbox",mime:"application/mbox",mode:"mbox",ext:["mbox"]},{name:"MySQL",mime:"text/x-mysql",mode:"sql"},{name:"Nginx",mime:"text/x-nginx-conf",mode:"nginx",file:/nginx.*\.conf$/i},{name:"NSIS",mime:"text/x-nsis",mode:"nsis",ext:["nsh","nsi"]},{name:"NTriples",mimes:["application/n-triples","application/n-quads","text/n-triples"],mode:"ntriples",ext:["nt","nq"]},{name:"Objective-C",mime:"text/x-objectivec",mode:"clike",ext:["m"],alias:["objective-c","objc"]},{name:"Objective-C++",mime:"text/x-objectivec++",mode:"clike",ext:["mm"],alias:["objective-c++","objc++"]},{name:"OCaml",mime:"text/x-ocaml",mode:"mllike",ext:["ml","mli","mll","mly"]},{name:"Octave",mime:"text/x-octave",mode:"octave",ext:["m"]},{name:"Oz",mime:"text/x-oz",mode:"oz",ext:["oz"]},{name:"Pascal",mime:"text/x-pascal",mode:"pascal",ext:["p","pas"]},{name:"PEG.js",mime:"null",mode:"pegjs",ext:["jsonld"]},{name:"Perl",mime:"text/x-perl",mode:"perl",ext:["pl","pm"]},{name:"PHP",mimes:["text/x-php","application/x-httpd-php","application/x-httpd-php-open"],mode:"php",ext:["php","php3","php4","php5","php7","phtml"]},{name:"Pig",mime:"text/x-pig",mode:"pig",ext:["pig"]},{name:"Plain Text",mime:"text/plain",mode:"null",ext:["txt","text","conf","def","list","log"]},{name:"PLSQL",mime:"text/x-plsql",mode:"sql",ext:["pls"]},{name:"PostgreSQL",mime:"text/x-pgsql",mode:"sql"},{name:"PowerShell",mime:"application/x-powershell",mode:"powershell",ext:["ps1","psd1","psm1"]},{name:"Properties files",mime:"text/x-properties",mode:"properties",ext:["properties","ini","in"],alias:["ini","properties"]},{name:"ProtoBuf",mime:"text/x-protobuf",mode:"protobuf",ext:["proto"]},{name:"Python",mime:"text/x-python",mode:"python",ext:["BUILD","bzl","py","pyw"],file:/^(BUCK|BUILD)$/},{name:"Puppet",mime:"text/x-puppet",mode:"puppet",ext:["pp"]},{name:"Q",mime:"text/x-q",mode:"q",ext:["q"]},{name:"R",mime:"text/x-rsrc",mode:"r",ext:["r","R"],alias:["rscript"]},{name:"reStructuredText",mime:"text/x-rst",mode:"rst",ext:["rst"],alias:["rst"]},{name:"RPM Changes",mime:"text/x-rpm-changes",mode:"rpm"},{name:"RPM Spec",mime:"text/x-rpm-spec",mode:"rpm",ext:["spec"]},{name:"Ruby",mime:"text/x-ruby",mode:"ruby",ext:["rb"],alias:["jruby","macruby","rake","rb","rbx"]},{name:"Rust",mime:"text/x-rustsrc",mode:"rust",ext:["rs"]},{name:"SAS",mime:"text/x-sas",mode:"sas",ext:["sas"]},{name:"Sass",mime:"text/x-sass",mode:"sass",ext:["sass"]},{name:"Scala",mime:"text/x-scala",mode:"clike",ext:["scala"]},{name:"Scheme",mime:"text/x-scheme",mode:"scheme",ext:["scm","ss"]},{name:"SCSS",mime:"text/x-scss",mode:"css",ext:["scss"]},{name:"Shell",mimes:["text/x-sh","application/x-sh"],mode:"shell",ext:["sh","ksh","bash"],alias:["bash","sh","zsh"],file:/^PKGBUILD$/},{name:"Sieve",mime:"application/sieve",mode:"sieve",ext:["siv","sieve"]},{name:"Slim",mimes:["text/x-slim","application/x-slim"],mode:"slim",ext:["slim"]},{name:"Smalltalk",mime:"text/x-stsrc",mode:"smalltalk",ext:["st"]},{name:"Smarty",mime:"text/x-smarty",mode:"smarty",ext:["tpl"]},{name:"Solr",mime:"text/x-solr",mode:"solr"},{name:"SML",mime:"text/x-sml",mode:"mllike",ext:["sml","sig","fun","smackspec"]},{name:"Soy",mime:"text/x-soy",mode:"soy",ext:["soy"],alias:["closure template"]},{name:"SPARQL",mime:"application/sparql-query",mode:"sparql",ext:["rq","sparql"],alias:["sparul"]},{name:"Spreadsheet",mime:"text/x-spreadsheet",mode:"spreadsheet",alias:["excel","formula"]},{name:"SQL",mime:"text/x-sql",mode:"sql",ext:["sql"]},{name:"SQLite",mime:"text/x-sqlite",mode:"sql"},{name:"Squirrel",mime:"text/x-squirrel",mode:"clike",ext:["nut"]},{name:"Stylus",mime:"text/x-styl",mode:"stylus",ext:["styl"]},{name:"Swift",mime:"text/x-swift",mode:"swift",ext:["swift"]},{name:"sTeX",mime:"text/x-stex",mode:"stex"},{name:"LaTeX",mime:"text/x-latex",mode:"stex",ext:["text","ltx","tex"],alias:["tex"]},{name:"SystemVerilog",mime:"text/x-systemverilog",mode:"verilog",ext:["v","sv","svh"]},{name:"Tcl",mime:"text/x-tcl",mode:"tcl",ext:["tcl"]},{name:"Textile",mime:"text/x-textile",mode:"textile",ext:["textile"]},{name:"TiddlyWiki",mime:"text/x-tiddlywiki",mode:"tiddlywiki"},{name:"Tiki wiki",mime:"text/tiki",mode:"tiki"},{name:"TOML",mime:"text/x-toml",mode:"toml",ext:["toml"]},{name:"Tornado",mime:"text/x-tornado",mode:"tornado"},{name:"troff",mime:"text/troff",mode:"troff",ext:["1","2","3","4","5","6","7","8","9"]},{name:"TTCN",mime:"text/x-ttcn",mode:"ttcn",ext:["ttcn","ttcn3","ttcnpp"]},{name:"TTCN_CFG",mime:"text/x-ttcn-cfg",mode:"ttcn-cfg",ext:["cfg"]},{name:"Turtle",mime:"text/turtle",mode:"turtle",ext:["ttl"]},{name:"TypeScript",mime:"application/typescript",mode:"javascript",ext:["ts"],alias:["ts"]},{name:"TypeScript-JSX",mime:"text/typescript-jsx",mode:"jsx",ext:["tsx"],alias:["tsx"]},{name:"Twig",mime:"text/x-twig",mode:"twig"},{name:"Web IDL",mime:"text/x-webidl",mode:"webidl",ext:["webidl"]},{name:"VB.NET",mime:"text/x-vb",mode:"vb",ext:["vb"]},{name:"VBScript",mime:"text/vbscript",mode:"vbscript",ext:["vbs"]},{name:"Velocity",mime:"text/velocity",mode:"velocity",ext:["vtl"]},{name:"Verilog",mime:"text/x-verilog",mode:"verilog",ext:["v"]},{name:"VHDL",mime:"text/x-vhdl",mode:"vhdl",ext:["vhd","vhdl"]},{name:"Vue.js Component",mimes:["script/x-vue","text/x-vue"],mode:"vue",ext:["vue"]},{name:"XML",mimes:["application/xml","text/xml"],mode:"xml",ext:["xml","xsl","xsd","svg"],alias:["rss","wsdl","xsd"]},{name:"XQuery",mime:"application/xquery",mode:"xquery",ext:["xy","xquery"]},{name:"Yacas",mime:"text/x-yacas",mode:"yacas",ext:["ys"]},{name:"YAML",mimes:["text/x-yaml","text/yaml"],mode:"yaml",ext:["yaml","yml"],alias:["yml"]},{name:"Z80",mime:"text/x-z80",mode:"z80",ext:["z80"]},{name:"mscgen",mime:"text/x-mscgen",mode:"mscgen",ext:["mscgen","mscin","msc"]},{name:"xu",mime:"text/x-xu",mode:"mscgen",ext:["xu"]},{name:"msgenny",mime:"text/x-msgenny",mode:"mscgen",ext:["msgenny"]},{name:"WebAssembly",mime:"text/webassembly",mode:"wast",ext:["wat","wast"]}];for(var p=0;p-1&&C.substring(s+1,C.length);if(h)return o.findModeByExtension(h)},o.findModeByName=function(C){C=C.toLowerCase();for(var b=0;b{(function(o){typeof Ys=="object"&&typeof Qs=="object"?o(We(),mn(),Xs()):typeof define=="function"&&define.amd?define(["../../lib/codemirror","../xml/xml","../meta"],o):o(CodeMirror)})(function(o){"use strict";o.defineMode("markdown",function(p,v){var C=o.getMode(p,"text/html"),b=C.name=="null";function S(F){if(o.findModeByName){var L=o.findModeByName(F);L&&(F=L.mime||L.mimes[0])}var de=o.getMode(p,F);return de.name=="null"?null:de}v.highlightFormatting===void 0&&(v.highlightFormatting=!1),v.maxBlockquoteDepth===void 0&&(v.maxBlockquoteDepth=0),v.taskLists===void 0&&(v.taskLists=!1),v.strikethrough===void 0&&(v.strikethrough=!1),v.emoji===void 0&&(v.emoji=!1),v.fencedCodeBlockHighlighting===void 0&&(v.fencedCodeBlockHighlighting=!0),v.fencedCodeBlockDefaultMode===void 0&&(v.fencedCodeBlockDefaultMode="text/plain"),v.xml===void 0&&(v.xml=!0),v.tokenTypeOverrides===void 0&&(v.tokenTypeOverrides={});var s={header:"header",code:"comment",quote:"quote",list1:"variable-2",list2:"variable-3",list3:"keyword",hr:"hr",image:"image",imageAltText:"image-alt-text",imageMarker:"image-marker",formatting:"formatting",linkInline:"link",linkEmail:"link",linkText:"link",linkHref:"string",em:"em",strong:"strong",strikethrough:"strikethrough",emoji:"builtin"};for(var h in s)s.hasOwnProperty(h)&&v.tokenTypeOverrides[h]&&(s[h]=v.tokenTypeOverrides[h]);var g=/^([*\-_])(?:\s*\1){2,}\s*$/,T=/^(?:[*\-+]|^[0-9]+([.)]))\s+/,w=/^\[(x| )\](?=\s)/i,c=v.allowAtxHeaderWithoutSpace?/^(#+)/:/^(#+)(?: |$)/,d=/^ {0,3}(?:\={1,}|-{2,})\s*$/,k=/^[^#!\[\]*_\\<>` "'(~:]+/,z=/^(~~~+|```+)[ \t]*([\w\/+#-]*)[^\n`]*$/,M=/^\s*\[[^\]]+?\]:.*$/,_=/[!"#$%&'()*+,\-.\/:;<=>?@\[\\\]^_`{|}~\xA1\xA7\xAB\xB6\xB7\xBB\xBF\u037E\u0387\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u0AF0\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F14\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1360-\u1368\u1400\u166D\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CC0-\u1CC7\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u2308-\u230B\u2329\u232A\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30-\u2E42\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA8FC\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uAAF0\uAAF1\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uFF3F\uFF5B\uFF5D\uFF5F-\uFF65]|\uD800[\uDD00-\uDD02\uDF9F\uDFD0]|\uD801\uDD6F|\uD802[\uDC57\uDD1F\uDD3F\uDE50-\uDE58\uDE7F\uDEF0-\uDEF6\uDF39-\uDF3F\uDF99-\uDF9C]|\uD804[\uDC47-\uDC4D\uDCBB\uDCBC\uDCBE-\uDCC1\uDD40-\uDD43\uDD74\uDD75\uDDC5-\uDDC9\uDDCD\uDDDB\uDDDD-\uDDDF\uDE38-\uDE3D\uDEA9]|\uD805[\uDCC6\uDDC1-\uDDD7\uDE41-\uDE43\uDF3C-\uDF3E]|\uD809[\uDC70-\uDC74]|\uD81A[\uDE6E\uDE6F\uDEF5\uDF37-\uDF3B\uDF44]|\uD82F\uDC9F|\uD836[\uDE87-\uDE8B]/,W=" ";function E(F,L,de){return L.f=L.inline=de,de(F,L)}function O(F,L,de){return L.f=L.block=de,de(F,L)}function G(F){return!F||!/\S/.test(F.string)}function J(F){if(F.linkTitle=!1,F.linkHref=!1,F.linkText=!1,F.em=!1,F.strong=!1,F.strikethrough=!1,F.quote=0,F.indentedCode=!1,F.f==q){var L=b;if(!L){var de=o.innerMode(C,F.htmlState);L=de.mode.name=="xml"&&de.state.tagStart===null&&!de.state.context&&de.state.tokenize.isInText}L&&(F.f=j,F.block=re,F.htmlState=null)}return F.trailingSpace=0,F.trailingSpaceNewLine=!1,F.prevLine=F.thisLine,F.thisLine={stream:null},null}function re(F,L){var de=F.column()===L.indentation,ze=G(L.prevLine.stream),pe=L.indentedCode,Ee=L.prevLine.hr,ge=L.list!==!1,Oe=(L.listStack[L.listStack.length-1]||0)+3;L.indentedCode=!1;var qe=L.indentation;if(L.indentationDiff===null&&(L.indentationDiff=L.indentation,ge)){for(L.list=null;qe=4&&(pe||L.prevLine.fencedCodeEnd||L.prevLine.header||ze))return F.skipToEnd(),L.indentedCode=!0,s.code;if(F.eatSpace())return null;if(de&&L.indentation<=Oe&&(Ze=F.match(c))&&Ze[1].length<=6)return L.quote=0,L.header=Ze[1].length,L.thisLine.header=!0,v.highlightFormatting&&(L.formatting="header"),L.f=L.inline,D(L);if(L.indentation<=Oe&&F.eat(">"))return L.quote=de?1:L.quote+1,v.highlightFormatting&&(L.formatting="quote"),F.eatSpace(),D(L);if(!Be&&!L.setext&&de&&L.indentation<=Oe&&(Ze=F.match(T))){var ke=Ze[1]?"ol":"ul";return L.indentation=qe+F.current().length,L.list=!0,L.quote=0,L.listStack.push(L.indentation),L.em=!1,L.strong=!1,L.code=!1,L.strikethrough=!1,v.taskLists&&F.match(w,!1)&&(L.taskList=!0),L.f=L.inline,v.highlightFormatting&&(L.formatting=["list","list-"+ke]),D(L)}else{if(de&&L.indentation<=Oe&&(Ze=F.match(z,!0)))return L.quote=0,L.fencedEndRE=new RegExp(Ze[1]+"+ *$"),L.localMode=v.fencedCodeBlockHighlighting&&S(Ze[2]||v.fencedCodeBlockDefaultMode),L.localMode&&(L.localState=o.startState(L.localMode)),L.f=L.block=I,v.highlightFormatting&&(L.formatting="code-block"),L.code=-1,D(L);if(L.setext||(!Se||!ge)&&!L.quote&&L.list===!1&&!L.code&&!Be&&!M.test(F.string)&&(Ze=F.lookAhead(1))&&(Ze=Ze.match(d)))return L.setext?(L.header=L.setext,L.setext=0,F.skipToEnd(),v.highlightFormatting&&(L.formatting="header")):(L.header=Ze[0].charAt(0)=="="?1:2,L.setext=L.header),L.thisLine.header=!0,L.f=L.inline,D(L);if(Be)return F.skipToEnd(),L.hr=!0,L.thisLine.hr=!0,s.hr;if(F.peek()==="[")return E(F,L,N)}return E(F,L,L.inline)}function q(F,L){var de=C.token(F,L.htmlState);if(!b){var ze=o.innerMode(C,L.htmlState);(ze.mode.name=="xml"&&ze.state.tagStart===null&&!ze.state.context&&ze.state.tokenize.isInText||L.md_inside&&F.current().indexOf(">")>-1)&&(L.f=j,L.block=re,L.htmlState=null)}return de}function I(F,L){var de=L.listStack[L.listStack.length-1]||0,ze=L.indentation=F.quote?L.push(s.formatting+"-"+F.formatting[de]+"-"+F.quote):L.push("error"))}if(F.taskOpen)return L.push("meta"),L.length?L.join(" "):null;if(F.taskClosed)return L.push("property"),L.length?L.join(" "):null;if(F.linkHref?L.push(s.linkHref,"url"):(F.strong&&L.push(s.strong),F.em&&L.push(s.em),F.strikethrough&&L.push(s.strikethrough),F.emoji&&L.push(s.emoji),F.linkText&&L.push(s.linkText),F.code&&L.push(s.code),F.image&&L.push(s.image),F.imageAltText&&L.push(s.imageAltText,"link"),F.imageMarker&&L.push(s.imageMarker)),F.header&&L.push(s.header,s.header+"-"+F.header),F.quote&&(L.push(s.quote),!v.maxBlockquoteDepth||v.maxBlockquoteDepth>=F.quote?L.push(s.quote+"-"+F.quote):L.push(s.quote+"-"+v.maxBlockquoteDepth)),F.list!==!1){var ze=(F.listStack.length-1)%3;ze?ze===1?L.push(s.list2):L.push(s.list3):L.push(s.list1)}return F.trailingSpaceNewLine?L.push("trailing-space-new-line"):F.trailingSpace&&L.push("trailing-space-"+(F.trailingSpace%2?"a":"b")),L.length?L.join(" "):null}function Q(F,L){if(F.match(k,!0))return D(L)}function j(F,L){var de=L.text(F,L);if(typeof de<"u")return de;if(L.list)return L.list=null,D(L);if(L.taskList){var ze=F.match(w,!0)[1]===" ";return ze?L.taskOpen=!0:L.taskClosed=!0,v.highlightFormatting&&(L.formatting="task"),L.taskList=!1,D(L)}if(L.taskOpen=!1,L.taskClosed=!1,L.header&&F.match(/^#+$/,!0))return v.highlightFormatting&&(L.formatting="header"),D(L);var pe=F.next();if(L.linkTitle){L.linkTitle=!1;var Ee=pe;pe==="("&&(Ee=")"),Ee=(Ee+"").replace(/([.?*+^\[\]\\(){}|-])/g,"\\$1");var ge="^\\s*(?:[^"+Ee+"\\\\]+|\\\\\\\\|\\\\.)"+Ee;if(F.match(new RegExp(ge),!0))return s.linkHref}if(pe==="`"){var Oe=L.formatting;v.highlightFormatting&&(L.formatting="code"),F.eatWhile("`");var qe=F.current().length;if(L.code==0&&(!L.quote||qe==1))return L.code=qe,D(L);if(qe==L.code){var Se=D(L);return L.code=0,Se}else return L.formatting=Oe,D(L)}else if(L.code)return D(L);if(pe==="\\"&&(F.next(),v.highlightFormatting)){var Be=D(L),Ze=s.formatting+"-escape";return Be?Be+" "+Ze:Ze}if(pe==="!"&&F.match(/\[[^\]]*\] ?(?:\(|\[)/,!1))return L.imageMarker=!0,L.image=!0,v.highlightFormatting&&(L.formatting="image"),D(L);if(pe==="["&&L.imageMarker&&F.match(/[^\]]*\](\(.*?\)| ?\[.*?\])/,!1))return L.imageMarker=!1,L.imageAltText=!0,v.highlightFormatting&&(L.formatting="image"),D(L);if(pe==="]"&&L.imageAltText){v.highlightFormatting&&(L.formatting="image");var Be=D(L);return L.imageAltText=!1,L.image=!1,L.inline=L.f=y,Be}if(pe==="["&&!L.image)return L.linkText&&F.match(/^.*?\]/)||(L.linkText=!0,v.highlightFormatting&&(L.formatting="link")),D(L);if(pe==="]"&&L.linkText){v.highlightFormatting&&(L.formatting="link");var Be=D(L);return L.linkText=!1,L.inline=L.f=F.match(/\(.*?\)| ?\[.*?\]/,!1)?y:j,Be}if(pe==="<"&&F.match(/^(https?|ftps?):\/\/(?:[^\\>]|\\.)+>/,!1)){L.f=L.inline=V,v.highlightFormatting&&(L.formatting="link");var Be=D(L);return Be?Be+=" ":Be="",Be+s.linkInline}if(pe==="<"&&F.match(/^[^> \\]+@(?:[^\\>]|\\.)+>/,!1)){L.f=L.inline=V,v.highlightFormatting&&(L.formatting="link");var Be=D(L);return Be?Be+=" ":Be="",Be+s.linkEmail}if(v.xml&&pe==="<"&&F.match(/^(!--|\?|!\[CDATA\[|[a-z][a-z0-9-]*(?:\s+[a-z_:.\-]+(?:\s*=\s*[^>]+)?)*\s*(?:>|$))/i,!1)){var ke=F.string.indexOf(">",F.pos);if(ke!=-1){var Je=F.string.substring(F.start,ke);/markdown\s*=\s*('|"){0,1}1('|"){0,1}/.test(Je)&&(L.md_inside=!0)}return F.backUp(1),L.htmlState=o.startState(C),O(F,L,q)}if(v.xml&&pe==="<"&&F.match(/^\/\w*?>/))return L.md_inside=!1,"tag";if(pe==="*"||pe==="_"){for(var Re=1,Ge=F.pos==1?" ":F.string.charAt(F.pos-2);Re<3&&F.eat(pe);)Re++;var U=F.peek()||" ",Z=!/\s/.test(U)&&(!_.test(U)||/\s/.test(Ge)||_.test(Ge)),ce=!/\s/.test(Ge)&&(!_.test(Ge)||/\s/.test(U)||_.test(U)),He=null,te=null;if(Re%2&&(!L.em&&Z&&(pe==="*"||!ce||_.test(Ge))?He=!0:L.em==pe&&ce&&(pe==="*"||!Z||_.test(U))&&(He=!1)),Re>1&&(!L.strong&&Z&&(pe==="*"||!ce||_.test(Ge))?te=!0:L.strong==pe&&ce&&(pe==="*"||!Z||_.test(U))&&(te=!1)),te!=null||He!=null){v.highlightFormatting&&(L.formatting=He==null?"strong":te==null?"em":"strong em"),He===!0&&(L.em=pe),te===!0&&(L.strong=pe);var Se=D(L);return He===!1&&(L.em=!1),te===!1&&(L.strong=!1),Se}}else if(pe===" "&&(F.eat("*")||F.eat("_"))){if(F.peek()===" ")return D(L);F.backUp(1)}if(v.strikethrough){if(pe==="~"&&F.eatWhile(pe)){if(L.strikethrough){v.highlightFormatting&&(L.formatting="strikethrough");var Se=D(L);return L.strikethrough=!1,Se}else if(F.match(/^[^\s]/,!1))return L.strikethrough=!0,v.highlightFormatting&&(L.formatting="strikethrough"),D(L)}else if(pe===" "&&F.match("~~",!0)){if(F.peek()===" ")return D(L);F.backUp(2)}}if(v.emoji&&pe===":"&&F.match(/^(?:[a-z_\d+][a-z_\d+-]*|\-[a-z_\d+][a-z_\d+-]*):/)){L.emoji=!0,v.highlightFormatting&&(L.formatting="emoji");var fe=D(L);return L.emoji=!1,fe}return pe===" "&&(F.match(/^ +$/,!1)?L.trailingSpace++:L.trailingSpace&&(L.trailingSpaceNewLine=!0)),D(L)}function V(F,L){var de=F.next();if(de===">"){L.f=L.inline=j,v.highlightFormatting&&(L.formatting="link");var ze=D(L);return ze?ze+=" ":ze="",ze+s.linkInline}return F.match(/^[^>]+/,!0),s.linkInline}function y(F,L){if(F.eatSpace())return null;var de=F.next();return de==="("||de==="["?(L.f=L.inline=X(de==="("?")":"]"),v.highlightFormatting&&(L.formatting="link-string"),L.linkHref=!0,D(L)):"error"}var K={")":/^(?:[^\\\(\)]|\\.|\((?:[^\\\(\)]|\\.)*\))*?(?=\))/,"]":/^(?:[^\\\[\]]|\\.|\[(?:[^\\\[\]]|\\.)*\])*?(?=\])/};function X(F){return function(L,de){var ze=L.next();if(ze===F){de.f=de.inline=j,v.highlightFormatting&&(de.formatting="link-string");var pe=D(de);return de.linkHref=!1,pe}return L.match(K[F]),de.linkHref=!0,D(de)}}function N(F,L){return F.match(/^([^\]\\]|\\.)*\]:/,!1)?(L.f=R,F.next(),v.highlightFormatting&&(L.formatting="link"),L.linkText=!0,D(L)):E(F,L,j)}function R(F,L){if(F.match("]:",!0)){L.f=L.inline=le,v.highlightFormatting&&(L.formatting="link");var de=D(L);return L.linkText=!1,de}return F.match(/^([^\]\\]|\\.)+/,!0),s.linkText}function le(F,L){return F.eatSpace()?null:(F.match(/^[^\s]+/,!0),F.peek()===void 0?L.linkTitle=!0:F.match(/^(?:\s+(?:"(?:[^"\\]|\\.)+"|'(?:[^'\\]|\\.)+'|\((?:[^)\\]|\\.)+\)))?/,!0),L.f=L.inline=j,s.linkHref+" url")}var xe={startState:function(){return{f:re,prevLine:{stream:null},thisLine:{stream:null},block:re,htmlState:null,indentation:0,inline:j,text:Q,formatting:!1,linkText:!1,linkHref:!1,linkTitle:!1,code:0,em:!1,strong:!1,header:0,setext:0,hr:!1,taskList:!1,list:!1,listStack:[],quote:0,trailingSpace:0,trailingSpaceNewLine:!1,strikethrough:!1,emoji:!1,fencedEndRE:null}},copyState:function(F){return{f:F.f,prevLine:F.prevLine,thisLine:F.thisLine,block:F.block,htmlState:F.htmlState&&o.copyState(C,F.htmlState),indentation:F.indentation,localMode:F.localMode,localState:F.localMode?o.copyState(F.localMode,F.localState):null,inline:F.inline,text:F.text,formatting:!1,linkText:F.linkText,linkTitle:F.linkTitle,linkHref:F.linkHref,code:F.code,em:F.em,strong:F.strong,strikethrough:F.strikethrough,emoji:F.emoji,header:F.header,setext:F.setext,hr:F.hr,taskList:F.taskList,list:F.list,listStack:F.listStack.slice(0),quote:F.quote,indentedCode:F.indentedCode,trailingSpace:F.trailingSpace,trailingSpaceNewLine:F.trailingSpaceNewLine,md_inside:F.md_inside,fencedEndRE:F.fencedEndRE}},token:function(F,L){if(L.formatting=!1,F!=L.thisLine.stream){if(L.header=0,L.hr=!1,F.match(/^\s*$/,!0))return J(L),null;if(L.prevLine=L.thisLine,L.thisLine={stream:F},L.taskList=!1,L.trailingSpace=0,L.trailingSpaceNewLine=!1,!L.localState&&(L.f=L.block,L.f!=q)){var de=F.match(/^\s*/,!0)[0].replace(/\t/g,W).length;if(L.indentation=de,L.indentationDiff=null,de>0)return null}}return L.f(F,L)},innerMode:function(F){return F.block==q?{state:F.htmlState,mode:C}:F.localState?{state:F.localState,mode:F.localMode}:{state:F,mode:xe}},indent:function(F,L,de){return F.block==q&&C.indent?C.indent(F.htmlState,L,de):F.localState&&F.localMode.indent?F.localMode.indent(F.localState,L,de):o.Pass},blankLine:J,getType:D,blockCommentStart:"",closeBrackets:"()[]{}''\"\"``",fold:"markdown"};return xe},"xml"),o.defineMIME("text/markdown","markdown"),o.defineMIME("text/x-markdown","markdown")})});var eu=Ke((Vs,Js)=>{(function(o){typeof Vs=="object"&&typeof Js=="object"?o(We(),Jo(),Yn()):typeof define=="function"&&define.amd?define(["../../lib/codemirror","../markdown/markdown","../../addon/mode/overlay"],o):o(CodeMirror)})(function(o){"use strict";var p=/^((?:(?:aaas?|about|acap|adiumxtra|af[ps]|aim|apt|attachment|aw|beshare|bitcoin|bolo|callto|cap|chrome(?:-extension)?|cid|coap|com-eventbrite-attendee|content|crid|cvs|data|dav|dict|dlna-(?:playcontainer|playsingle)|dns|doi|dtn|dvb|ed2k|facetime|feed|file|finger|fish|ftp|geo|gg|git|gizmoproject|go|gopher|gtalk|h323|hcp|https?|iax|icap|icon|im|imap|info|ipn|ipp|irc[6s]?|iris(?:\.beep|\.lwz|\.xpc|\.xpcs)?|itms|jar|javascript|jms|keyparc|lastfm|ldaps?|magnet|mailto|maps|market|message|mid|mms|ms-help|msnim|msrps?|mtqp|mumble|mupdate|mvn|news|nfs|nih?|nntp|notes|oid|opaquelocktoken|palm|paparazzi|platform|pop|pres|proxy|psyc|query|res(?:ource)?|rmi|rsync|rtmp|rtsp|secondlife|service|session|sftp|sgn|shttp|sieve|sips?|skype|sm[bs]|snmp|soap\.beeps?|soldat|spotify|ssh|steam|svn|tag|teamspeak|tel(?:net)?|tftp|things|thismessage|tip|tn3270|tv|udp|unreal|urn|ut2004|vemmi|ventrilo|view-source|webcal|wss?|wtai|wyciwyg|xcon(?:-userid)?|xfire|xmlrpc\.beeps?|xmpp|xri|ymsgr|z39\.50[rs]?):(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`*!()\[\]{};:'".,<>?«»“”‘’]))/i;o.defineMode("gfm",function(v,C){var b=0;function S(T){return T.code=!1,null}var s={startState:function(){return{code:!1,codeBlock:!1,ateSpace:!1}},copyState:function(T){return{code:T.code,codeBlock:T.codeBlock,ateSpace:T.ateSpace}},token:function(T,w){if(w.combineTokens=null,w.codeBlock)return T.match(/^```+/)?(w.codeBlock=!1,null):(T.skipToEnd(),null);if(T.sol()&&(w.code=!1),T.sol()&&T.match(/^```+/))return T.skipToEnd(),w.codeBlock=!0,null;if(T.peek()==="`"){T.next();var c=T.pos;T.eatWhile("`");var d=1+T.pos-c;return w.code?d===b&&(w.code=!1):(b=d,w.code=!0),null}else if(w.code)return T.next(),null;if(T.eatSpace())return w.ateSpace=!0,null;if((T.sol()||w.ateSpace)&&(w.ateSpace=!1,C.gitHubSpice!==!1)){if(T.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+@)?(?=.{0,6}\d)(?:[a-f0-9]{7,40}\b)/))return w.combineTokens=!0,"link";if(T.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+)?#[0-9]+\b/))return w.combineTokens=!0,"link"}return T.match(p)&&T.string.slice(T.start-2,T.start)!="]("&&(T.start==0||/\W/.test(T.string.charAt(T.start-1)))?(w.combineTokens=!0,"link"):(T.next(),null)},blankLine:S},h={taskLists:!0,strikethrough:!0,emoji:!0};for(var g in C)h[g]=C[g];return h.name="markdown",o.overlayMode(o.getMode(v,h),s)},"markdown"),o.defineMIME("text/x-gfm","gfm")})});var nu=Ke((tu,ru)=>{(function(o){typeof tu=="object"&&typeof ru=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";o.defineMode("go",function(p){var v=p.indentUnit,C={break:!0,case:!0,chan:!0,const:!0,continue:!0,default:!0,defer:!0,else:!0,fallthrough:!0,for:!0,func:!0,go:!0,goto:!0,if:!0,import:!0,interface:!0,map:!0,package:!0,range:!0,return:!0,select:!0,struct:!0,switch:!0,type:!0,var:!0,bool:!0,byte:!0,complex64:!0,complex128:!0,float32:!0,float64:!0,int8:!0,int16:!0,int32:!0,int64:!0,string:!0,uint8:!0,uint16:!0,uint32:!0,uint64:!0,int:!0,uint:!0,uintptr:!0,error:!0,rune:!0,any:!0,comparable:!0},b={true:!0,false:!0,iota:!0,nil:!0,append:!0,cap:!0,close:!0,complex:!0,copy:!0,delete:!0,imag:!0,len:!0,make:!0,new:!0,panic:!0,print:!0,println:!0,real:!0,recover:!0},S=/[+\-*&^%:=<>!|\/]/,s;function h(k,z){var M=k.next();if(M=='"'||M=="'"||M=="`")return z.tokenize=g(M),z.tokenize(k,z);if(/[\d\.]/.test(M))return M=="."?k.match(/^[0-9_]+([eE][\-+]?[0-9_]+)?/):M=="0"?k.match(/^[xX][0-9a-fA-F_]+/)||k.match(/^[0-7_]+/):k.match(/^[0-9_]*\.?[0-9_]*([eE][\-+]?[0-9_]+)?/),"number";if(/[\[\]{}\(\),;\:\.]/.test(M))return s=M,null;if(M=="/"){if(k.eat("*"))return z.tokenize=T,T(k,z);if(k.eat("/"))return k.skipToEnd(),"comment"}if(S.test(M))return k.eatWhile(S),"operator";k.eatWhile(/[\w\$_\xa1-\uffff]/);var _=k.current();return C.propertyIsEnumerable(_)?((_=="case"||_=="default")&&(s="case"),"keyword"):b.propertyIsEnumerable(_)?"atom":"variable"}function g(k){return function(z,M){for(var _=!1,W,E=!1;(W=z.next())!=null;){if(W==k&&!_){E=!0;break}_=!_&&k!="`"&&W=="\\"}return(E||!(_||k=="`"))&&(M.tokenize=h),"string"}}function T(k,z){for(var M=!1,_;_=k.next();){if(_=="/"&&M){z.tokenize=h;break}M=_=="*"}return"comment"}function w(k,z,M,_,W){this.indented=k,this.column=z,this.type=M,this.align=_,this.prev=W}function c(k,z,M){return k.context=new w(k.indented,z,M,null,k.context)}function d(k){if(k.context.prev){var z=k.context.type;return(z==")"||z=="]"||z=="}")&&(k.indented=k.context.indented),k.context=k.context.prev}}return{startState:function(k){return{tokenize:null,context:new w((k||0)-v,0,"top",!1),indented:0,startOfLine:!0}},token:function(k,z){var M=z.context;if(k.sol()&&(M.align==null&&(M.align=!1),z.indented=k.indentation(),z.startOfLine=!0,M.type=="case"&&(M.type="}")),k.eatSpace())return null;s=null;var _=(z.tokenize||h)(k,z);return _=="comment"||(M.align==null&&(M.align=!0),s=="{"?c(z,k.column(),"}"):s=="["?c(z,k.column(),"]"):s=="("?c(z,k.column(),")"):s=="case"?M.type="case":(s=="}"&&M.type=="}"||s==M.type)&&d(z),z.startOfLine=!1),_},indent:function(k,z){if(k.tokenize!=h&&k.tokenize!=null)return o.Pass;var M=k.context,_=z&&z.charAt(0);if(M.type=="case"&&/^(?:case|default)\b/.test(z))return k.context.type="}",M.indented;var W=_==M.type;return M.align?M.column+(W?0:1):M.indented+(W?0:v)},electricChars:"{}):",closeBrackets:"()[]{}''\"\"``",fold:"brace",blockCommentStart:"/*",blockCommentEnd:"*/",lineComment:"//"}}),o.defineMIME("text/x-go","go")})});var au=Ke((iu,ou)=>{(function(o){typeof iu=="object"&&typeof ou=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";o.defineMode("http",function(){function p(T,w){return T.skipToEnd(),w.cur=h,"error"}function v(T,w){return T.match(/^HTTP\/\d\.\d/)?(w.cur=C,"keyword"):T.match(/^[A-Z]+/)&&/[ \t]/.test(T.peek())?(w.cur=S,"keyword"):p(T,w)}function C(T,w){var c=T.match(/^\d+/);if(!c)return p(T,w);w.cur=b;var d=Number(c[0]);return d>=100&&d<200?"positive informational":d>=200&&d<300?"positive success":d>=300&&d<400?"positive redirect":d>=400&&d<500?"negative client-error":d>=500&&d<600?"negative server-error":"error"}function b(T,w){return T.skipToEnd(),w.cur=h,null}function S(T,w){return T.eatWhile(/\S/),w.cur=s,"string-2"}function s(T,w){return T.match(/^HTTP\/\d\.\d$/)?(w.cur=h,"keyword"):p(T,w)}function h(T){return T.sol()&&!T.eat(/[ \t]/)?T.match(/^.*?:/)?"atom":(T.skipToEnd(),"error"):(T.skipToEnd(),"string")}function g(T){return T.skipToEnd(),null}return{token:function(T,w){var c=w.cur;return c!=h&&c!=g&&T.eatSpace()?null:c(T,w)},blankLine:function(T){T.cur=g},startState:function(){return{cur:v}}}}),o.defineMIME("message/http","http")})});var uu=Ke((lu,su)=>{(function(o){typeof lu=="object"&&typeof su=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";o.defineMode("jinja2",function(){var p=["and","as","block","endblock","by","cycle","debug","else","elif","extends","filter","endfilter","firstof","do","for","endfor","if","endif","ifchanged","endifchanged","ifequal","endifequal","ifnotequal","set","raw","endraw","endifnotequal","in","include","load","not","now","or","parsed","regroup","reversed","spaceless","call","endcall","macro","endmacro","endspaceless","ssi","templatetag","openblock","closeblock","openvariable","closevariable","without","context","openbrace","closebrace","opencomment","closecomment","widthratio","url","with","endwith","get_current_language","trans","endtrans","noop","blocktrans","endblocktrans","get_available_languages","get_current_language_bidi","pluralize","autoescape","endautoescape"],v=/^[+\-*&%=<>!?|~^]/,C=/^[:\[\(\{]/,b=["true","false"],S=/^(\d[+\-\*\/])?\d+(\.\d+)?/;p=new RegExp("(("+p.join(")|(")+"))\\b"),b=new RegExp("(("+b.join(")|(")+"))\\b");function s(h,g){var T=h.peek();if(g.incomment)return h.skipTo("#}")?(h.eatWhile(/\#|}/),g.incomment=!1):h.skipToEnd(),"comment";if(g.intag){if(g.operator){if(g.operator=!1,h.match(b))return"atom";if(h.match(S))return"number"}if(g.sign){if(g.sign=!1,h.match(b))return"atom";if(h.match(S))return"number"}if(g.instring)return T==g.instring&&(g.instring=!1),h.next(),"string";if(T=="'"||T=='"')return g.instring=T,h.next(),"string";if(g.inbraces>0&&T==")")h.next(),g.inbraces--;else if(T=="(")h.next(),g.inbraces++;else if(g.inbrackets>0&&T=="]")h.next(),g.inbrackets--;else if(T=="[")h.next(),g.inbrackets++;else{if(!g.lineTag&&(h.match(g.intag+"}")||h.eat("-")&&h.match(g.intag+"}")))return g.intag=!1,"tag";if(h.match(v))return g.operator=!0,"operator";if(h.match(C))g.sign=!0;else{if(h.column()==1&&g.lineTag&&h.match(p))return"keyword";if(h.eat(" ")||h.sol()){if(h.match(p))return"keyword";if(h.match(b))return"atom";if(h.match(S))return"number";h.sol()&&h.next()}else h.next()}}return"variable"}else if(h.eat("{")){if(h.eat("#"))return g.incomment=!0,h.skipTo("#}")?(h.eatWhile(/\#|}/),g.incomment=!1):h.skipToEnd(),"comment";if(T=h.eat(/\{|%/))return g.intag=T,g.inbraces=0,g.inbrackets=0,T=="{"&&(g.intag="}"),h.eat("-"),"tag"}else if(h.eat("#")){if(h.peek()=="#")return h.skipToEnd(),"comment";if(!h.eol())return g.intag=!0,g.lineTag=!0,g.inbraces=0,g.inbrackets=0,"tag"}h.next()}return{startState:function(){return{tokenize:s,inbrackets:0,inbraces:0}},token:function(h,g){var T=g.tokenize(h,g);return h.eol()&&g.lineTag&&!g.instring&&g.inbraces==0&&g.inbrackets==0&&(g.intag=!1,g.lineTag=!1),T},blockCommentStart:"{#",blockCommentEnd:"#}",lineComment:"##"}}),o.defineMIME("text/jinja2","jinja2")})});var du=Ke((cu,fu)=>{(function(o){typeof cu=="object"&&typeof fu=="object"?o(We(),mn(),vn()):typeof define=="function"&&define.amd?define(["../../lib/codemirror","../xml/xml","../javascript/javascript"],o):o(CodeMirror)})(function(o){"use strict";function p(C,b,S,s){this.state=C,this.mode=b,this.depth=S,this.prev=s}function v(C){return new p(o.copyState(C.mode,C.state),C.mode,C.depth,C.prev&&v(C.prev))}o.defineMode("jsx",function(C,b){var S=o.getMode(C,{name:"xml",allowMissing:!0,multilineTagIndentPastTag:!1,allowMissingTagName:!0}),s=o.getMode(C,b&&b.base||"javascript");function h(c){var d=c.tagName;c.tagName=null;var k=S.indent(c,"","");return c.tagName=d,k}function g(c,d){return d.context.mode==S?T(c,d,d.context):w(c,d,d.context)}function T(c,d,k){if(k.depth==2)return c.match(/^.*?\*\//)?k.depth=1:c.skipToEnd(),"comment";if(c.peek()=="{"){S.skipAttribute(k.state);var z=h(k.state),M=k.state.context;if(M&&c.match(/^[^>]*>\s*$/,!1)){for(;M.prev&&!M.startOfLine;)M=M.prev;M.startOfLine?z-=C.indentUnit:k.prev.state.lexical&&(z=k.prev.state.lexical.indented)}else k.depth==1&&(z+=C.indentUnit);return d.context=new p(o.startState(s,z),s,0,d.context),null}if(k.depth==1){if(c.peek()=="<")return S.skipAttribute(k.state),d.context=new p(o.startState(S,h(k.state)),S,0,d.context),null;if(c.match("//"))return c.skipToEnd(),"comment";if(c.match("/*"))return k.depth=2,g(c,d)}var _=S.token(c,k.state),W=c.current(),E;return/\btag\b/.test(_)?/>$/.test(W)?k.state.context?k.depth=0:d.context=d.context.prev:/^-1&&c.backUp(W.length-E),_}function w(c,d,k){if(c.peek()=="<"&&!c.match(/^<([^<>]|<[^>]*>)+,\s*>/,!1)&&s.expressionAllowed(c,k.state))return d.context=new p(o.startState(S,s.indent(k.state,"","")),S,0,d.context),s.skipExpression(k.state),null;var z=s.token(c,k.state);if(!z&&k.depth!=null){var M=c.current();M=="{"?k.depth++:M=="}"&&--k.depth==0&&(d.context=d.context.prev)}return z}return{startState:function(){return{context:new p(o.startState(s),s)}},copyState:function(c){return{context:v(c.context)}},token:g,indent:function(c,d,k){return c.context.mode.indent(c.context.state,d,k)},innerMode:function(c){return c.context}}},"xml","javascript"),o.defineMIME("text/jsx","jsx"),o.defineMIME("text/typescript-jsx",{name:"jsx",base:{name:"javascript",typescript:!0}})})});var gu=Ke((pu,hu)=>{(function(o){typeof pu=="object"&&typeof hu=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";o.defineMode("nginx",function(p){function v(k){for(var z={},M=k.split(" "),_=0;_*\/]/.test(_)?g(null,"select-op"):/[;{}:\[\]]/.test(_)?g(null,_):(k.eatWhile(/[\w\\\-]/),g("variable","variable"))}function w(k,z){for(var M=!1,_;(_=k.next())!=null;){if(M&&_=="/"){z.tokenize=T;break}M=_=="*"}return g("comment","comment")}function c(k,z){for(var M=0,_;(_=k.next())!=null;){if(M>=2&&_==">"){z.tokenize=T;break}M=_=="-"?M+1:0}return g("comment","comment")}function d(k){return function(z,M){for(var _=!1,W;(W=z.next())!=null&&!(W==k&&!_);)_=!_&&W=="\\";return _||(M.tokenize=T),g("string","string")}}return{startState:function(k){return{tokenize:T,baseIndent:k||0,stack:[]}},token:function(k,z){if(k.eatSpace())return null;h=null;var M=z.tokenize(k,z),_=z.stack[z.stack.length-1];return h=="hash"&&_=="rule"?M="atom":M=="variable"&&(_=="rule"?M="number":(!_||_=="@media{")&&(M="tag")),_=="rule"&&/^[\{\};]$/.test(h)&&z.stack.pop(),h=="{"?_=="@media"?z.stack[z.stack.length-1]="@media{":z.stack.push("{"):h=="}"?z.stack.pop():h=="@media"?z.stack.push("@media"):_=="{"&&h!="comment"&&z.stack.push("rule"),M},indent:function(k,z){var M=k.stack.length;return/^\}/.test(z)&&(M-=k.stack[k.stack.length-1]=="rule"?2:1),k.baseIndent+M*s},electricChars:"}"}}),o.defineMIME("text/x-nginx-conf","nginx")})});var bu=Ke((mu,vu)=>{(function(o){typeof mu=="object"&&typeof vu=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";o.defineMode("pascal",function(){function p(T){for(var w={},c=T.split(" "),d=0;d!?|\/]/;function S(T,w){var c=T.next();if(c=="#"&&w.startOfLine)return T.skipToEnd(),"meta";if(c=='"'||c=="'")return w.tokenize=s(c),w.tokenize(T,w);if(c=="("&&T.eat("*"))return w.tokenize=h,h(T,w);if(c=="{")return w.tokenize=g,g(T,w);if(/[\[\]\(\),;\:\.]/.test(c))return null;if(/\d/.test(c))return T.eatWhile(/[\w\.]/),"number";if(c=="/"&&T.eat("/"))return T.skipToEnd(),"comment";if(b.test(c))return T.eatWhile(b),"operator";T.eatWhile(/[\w\$_]/);var d=T.current().toLowerCase();return v.propertyIsEnumerable(d)?"keyword":C.propertyIsEnumerable(d)?"atom":"variable"}function s(T){return function(w,c){for(var d=!1,k,z=!1;(k=w.next())!=null;){if(k==T&&!d){z=!0;break}d=!d&&k=="\\"}return(z||!d)&&(c.tokenize=null),"string"}}function h(T,w){for(var c=!1,d;d=T.next();){if(d==")"&&c){w.tokenize=null;break}c=d=="*"}return"comment"}function g(T,w){for(var c;c=T.next();)if(c=="}"){w.tokenize=null;break}return"comment"}return{startState:function(){return{tokenize:null}},token:function(T,w){if(T.eatSpace())return null;var c=(w.tokenize||S)(T,w);return c=="comment"||c=="meta",c},electricChars:"{}"}}),o.defineMIME("text/x-pascal","pascal")})});var _u=Ke((yu,xu)=>{(function(o){typeof yu=="object"&&typeof xu=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";o.defineMode("perl",function(){var S={"->":4,"++":4,"--":4,"**":4,"=~":4,"!~":4,"*":4,"/":4,"%":4,x:4,"+":4,"-":4,".":4,"<<":4,">>":4,"<":4,">":4,"<=":4,">=":4,lt:4,gt:4,le:4,ge:4,"==":4,"!=":4,"<=>":4,eq:4,ne:4,cmp:4,"~~":4,"&":4,"|":4,"^":4,"&&":4,"||":4,"//":4,"..":4,"...":4,"?":4,":":4,"=":4,"+=":4,"-=":4,"*=":4,",":4,"=>":4,"::":4,not:4,and:4,or:4,xor:4,BEGIN:[5,1],END:[5,1],PRINT:[5,1],PRINTF:[5,1],GETC:[5,1],READ:[5,1],READLINE:[5,1],DESTROY:[5,1],TIE:[5,1],TIEHANDLE:[5,1],UNTIE:[5,1],STDIN:5,STDIN_TOP:5,STDOUT:5,STDOUT_TOP:5,STDERR:5,STDERR_TOP:5,$ARG:5,$_:5,"@ARG":5,"@_":5,$LIST_SEPARATOR:5,'$"':5,$PROCESS_ID:5,$PID:5,$$:5,$REAL_GROUP_ID:5,$GID:5,"$(":5,$EFFECTIVE_GROUP_ID:5,$EGID:5,"$)":5,$PROGRAM_NAME:5,$0:5,$SUBSCRIPT_SEPARATOR:5,$SUBSEP:5,"$;":5,$REAL_USER_ID:5,$UID:5,"$<":5,$EFFECTIVE_USER_ID:5,$EUID:5,"$>":5,$a:5,$b:5,$COMPILING:5,"$^C":5,$DEBUGGING:5,"$^D":5,"${^ENCODING}":5,$ENV:5,"%ENV":5,$SYSTEM_FD_MAX:5,"$^F":5,"@F":5,"${^GLOBAL_PHASE}":5,"$^H":5,"%^H":5,"@INC":5,"%INC":5,$INPLACE_EDIT:5,"$^I":5,"$^M":5,$OSNAME:5,"$^O":5,"${^OPEN}":5,$PERLDB:5,"$^P":5,$SIG:5,"%SIG":5,$BASETIME:5,"$^T":5,"${^TAINT}":5,"${^UNICODE}":5,"${^UTF8CACHE}":5,"${^UTF8LOCALE}":5,$PERL_VERSION:5,"$^V":5,"${^WIN32_SLOPPY_STAT}":5,$EXECUTABLE_NAME:5,"$^X":5,$1:5,$MATCH:5,"$&":5,"${^MATCH}":5,$PREMATCH:5,"$`":5,"${^PREMATCH}":5,$POSTMATCH:5,"$'":5,"${^POSTMATCH}":5,$LAST_PAREN_MATCH:5,"$+":5,$LAST_SUBMATCH_RESULT:5,"$^N":5,"@LAST_MATCH_END":5,"@+":5,"%LAST_PAREN_MATCH":5,"%+":5,"@LAST_MATCH_START":5,"@-":5,"%LAST_MATCH_START":5,"%-":5,$LAST_REGEXP_CODE_RESULT:5,"$^R":5,"${^RE_DEBUG_FLAGS}":5,"${^RE_TRIE_MAXBUF}":5,$ARGV:5,"@ARGV":5,ARGV:5,ARGVOUT:5,$OUTPUT_FIELD_SEPARATOR:5,$OFS:5,"$,":5,$INPUT_LINE_NUMBER:5,$NR:5,"$.":5,$INPUT_RECORD_SEPARATOR:5,$RS:5,"$/":5,$OUTPUT_RECORD_SEPARATOR:5,$ORS:5,"$\\":5,$OUTPUT_AUTOFLUSH:5,"$|":5,$ACCUMULATOR:5,"$^A":5,$FORMAT_FORMFEED:5,"$^L":5,$FORMAT_PAGE_NUMBER:5,"$%":5,$FORMAT_LINES_LEFT:5,"$-":5,$FORMAT_LINE_BREAK_CHARACTERS:5,"$:":5,$FORMAT_LINES_PER_PAGE:5,"$=":5,$FORMAT_TOP_NAME:5,"$^":5,$FORMAT_NAME:5,"$~":5,"${^CHILD_ERROR_NATIVE}":5,$EXTENDED_OS_ERROR:5,"$^E":5,$EXCEPTIONS_BEING_CAUGHT:5,"$^S":5,$WARNING:5,"$^W":5,"${^WARNING_BITS}":5,$OS_ERROR:5,$ERRNO:5,"$!":5,"%OS_ERROR":5,"%ERRNO":5,"%!":5,$CHILD_ERROR:5,"$?":5,$EVAL_ERROR:5,"$@":5,$OFMT:5,"$#":5,"$*":5,$ARRAY_BASE:5,"$[":5,$OLD_PERL_VERSION:5,"$]":5,if:[1,1],elsif:[1,1],else:[1,1],while:[1,1],unless:[1,1],for:[1,1],foreach:[1,1],abs:1,accept:1,alarm:1,atan2:1,bind:1,binmode:1,bless:1,bootstrap:1,break:1,caller:1,chdir:1,chmod:1,chomp:1,chop:1,chown:1,chr:1,chroot:1,close:1,closedir:1,connect:1,continue:[1,1],cos:1,crypt:1,dbmclose:1,dbmopen:1,default:1,defined:1,delete:1,die:1,do:1,dump:1,each:1,endgrent:1,endhostent:1,endnetent:1,endprotoent:1,endpwent:1,endservent:1,eof:1,eval:1,exec:1,exists:1,exit:1,exp:1,fcntl:1,fileno:1,flock:1,fork:1,format:1,formline:1,getc:1,getgrent:1,getgrgid:1,getgrnam:1,gethostbyaddr:1,gethostbyname:1,gethostent:1,getlogin:1,getnetbyaddr:1,getnetbyname:1,getnetent:1,getpeername:1,getpgrp:1,getppid:1,getpriority:1,getprotobyname:1,getprotobynumber:1,getprotoent:1,getpwent:1,getpwnam:1,getpwuid:1,getservbyname:1,getservbyport:1,getservent:1,getsockname:1,getsockopt:1,given:1,glob:1,gmtime:1,goto:1,grep:1,hex:1,import:1,index:1,int:1,ioctl:1,join:1,keys:1,kill:1,last:1,lc:1,lcfirst:1,length:1,link:1,listen:1,local:2,localtime:1,lock:1,log:1,lstat:1,m:null,map:1,mkdir:1,msgctl:1,msgget:1,msgrcv:1,msgsnd:1,my:2,new:1,next:1,no:1,oct:1,open:1,opendir:1,ord:1,our:2,pack:1,package:1,pipe:1,pop:1,pos:1,print:1,printf:1,prototype:1,push:1,q:null,qq:null,qr:null,quotemeta:null,qw:null,qx:null,rand:1,read:1,readdir:1,readline:1,readlink:1,readpipe:1,recv:1,redo:1,ref:1,rename:1,require:1,reset:1,return:1,reverse:1,rewinddir:1,rindex:1,rmdir:1,s:null,say:1,scalar:1,seek:1,seekdir:1,select:1,semctl:1,semget:1,semop:1,send:1,setgrent:1,sethostent:1,setnetent:1,setpgrp:1,setpriority:1,setprotoent:1,setpwent:1,setservent:1,setsockopt:1,shift:1,shmctl:1,shmget:1,shmread:1,shmwrite:1,shutdown:1,sin:1,sleep:1,socket:1,socketpair:1,sort:1,splice:1,split:1,sprintf:1,sqrt:1,srand:1,stat:1,state:1,study:1,sub:1,substr:1,symlink:1,syscall:1,sysopen:1,sysread:1,sysseek:1,system:1,syswrite:1,tell:1,telldir:1,tie:1,tied:1,time:1,times:1,tr:null,truncate:1,uc:1,ucfirst:1,umask:1,undef:1,unlink:1,unpack:1,unshift:1,untie:1,use:1,utime:1,values:1,vec:1,wait:1,waitpid:1,wantarray:1,warn:1,when:1,write:1,y:null},s="string-2",h=/[goseximacplud]/;function g(c,d,k,z,M){return d.chain=null,d.style=null,d.tail=null,d.tokenize=function(_,W){for(var E=!1,O,G=0;O=_.next();){if(O===k[G]&&!E)return k[++G]!==void 0?(W.chain=k[G],W.style=z,W.tail=M):M&&_.eatWhile(M),W.tokenize=w,z;E=!E&&O=="\\"}return z},d.tokenize(c,d)}function T(c,d,k){return d.tokenize=function(z,M){return z.string==k&&(M.tokenize=w),z.skipToEnd(),"string"},d.tokenize(c,d)}function w(c,d){if(c.eatSpace())return null;if(d.chain)return g(c,d,d.chain,d.style,d.tail);if(c.match(/^(\-?((\d[\d_]*)?\.\d+(e[+-]?\d+)?|\d+\.\d*)|0x[\da-fA-F_]+|0b[01_]+|\d[\d_]*(e[+-]?\d+)?)/))return"number";if(c.match(/^<<(?=[_a-zA-Z])/))return c.eatWhile(/\w/),T(c,d,c.current().substr(2));if(c.sol()&&c.match(/^\=item(?!\w)/))return T(c,d,"=cut");var k=c.next();if(k=='"'||k=="'"){if(v(c,3)=="<<"+k){var z=c.pos;c.eatWhile(/\w/);var M=c.current().substr(1);if(M&&c.eat(k))return T(c,d,M);c.pos=z}return g(c,d,[k],"string")}if(k=="q"){var _=p(c,-2);if(!(_&&/\w/.test(_))){if(_=p(c,0),_=="x"){if(_=p(c,1),_=="(")return b(c,2),g(c,d,[")"],s,h);if(_=="[")return b(c,2),g(c,d,["]"],s,h);if(_=="{")return b(c,2),g(c,d,["}"],s,h);if(_=="<")return b(c,2),g(c,d,[">"],s,h);if(/[\^'"!~\/]/.test(_))return b(c,1),g(c,d,[c.eat(_)],s,h)}else if(_=="q"){if(_=p(c,1),_=="(")return b(c,2),g(c,d,[")"],"string");if(_=="[")return b(c,2),g(c,d,["]"],"string");if(_=="{")return b(c,2),g(c,d,["}"],"string");if(_=="<")return b(c,2),g(c,d,[">"],"string");if(/[\^'"!~\/]/.test(_))return b(c,1),g(c,d,[c.eat(_)],"string")}else if(_=="w"){if(_=p(c,1),_=="(")return b(c,2),g(c,d,[")"],"bracket");if(_=="[")return b(c,2),g(c,d,["]"],"bracket");if(_=="{")return b(c,2),g(c,d,["}"],"bracket");if(_=="<")return b(c,2),g(c,d,[">"],"bracket");if(/[\^'"!~\/]/.test(_))return b(c,1),g(c,d,[c.eat(_)],"bracket")}else if(_=="r"){if(_=p(c,1),_=="(")return b(c,2),g(c,d,[")"],s,h);if(_=="[")return b(c,2),g(c,d,["]"],s,h);if(_=="{")return b(c,2),g(c,d,["}"],s,h);if(_=="<")return b(c,2),g(c,d,[">"],s,h);if(/[\^'"!~\/]/.test(_))return b(c,1),g(c,d,[c.eat(_)],s,h)}else if(/[\^'"!~\/(\[{<]/.test(_)){if(_=="(")return b(c,1),g(c,d,[")"],"string");if(_=="[")return b(c,1),g(c,d,["]"],"string");if(_=="{")return b(c,1),g(c,d,["}"],"string");if(_=="<")return b(c,1),g(c,d,[">"],"string");if(/[\^'"!~\/]/.test(_))return g(c,d,[c.eat(_)],"string")}}}if(k=="m"){var _=p(c,-2);if(!(_&&/\w/.test(_))&&(_=c.eat(/[(\[{<\^'"!~\/]/),_)){if(/[\^'"!~\/]/.test(_))return g(c,d,[_],s,h);if(_=="(")return g(c,d,[")"],s,h);if(_=="[")return g(c,d,["]"],s,h);if(_=="{")return g(c,d,["}"],s,h);if(_=="<")return g(c,d,[">"],s,h)}}if(k=="s"){var _=/[\/>\]})\w]/.test(p(c,-2));if(!_&&(_=c.eat(/[(\[{<\^'"!~\/]/),_))return _=="["?g(c,d,["]","]"],s,h):_=="{"?g(c,d,["}","}"],s,h):_=="<"?g(c,d,[">",">"],s,h):_=="("?g(c,d,[")",")"],s,h):g(c,d,[_,_],s,h)}if(k=="y"){var _=/[\/>\]})\w]/.test(p(c,-2));if(!_&&(_=c.eat(/[(\[{<\^'"!~\/]/),_))return _=="["?g(c,d,["]","]"],s,h):_=="{"?g(c,d,["}","}"],s,h):_=="<"?g(c,d,[">",">"],s,h):_=="("?g(c,d,[")",")"],s,h):g(c,d,[_,_],s,h)}if(k=="t"){var _=/[\/>\]})\w]/.test(p(c,-2));if(!_&&(_=c.eat("r"),_&&(_=c.eat(/[(\[{<\^'"!~\/]/),_)))return _=="["?g(c,d,["]","]"],s,h):_=="{"?g(c,d,["}","}"],s,h):_=="<"?g(c,d,[">",">"],s,h):_=="("?g(c,d,[")",")"],s,h):g(c,d,[_,_],s,h)}if(k=="`")return g(c,d,[k],"variable-2");if(k=="/")return/~\s*$/.test(v(c))?g(c,d,[k],s,h):"operator";if(k=="$"){var z=c.pos;if(c.eatWhile(/\d/)||c.eat("{")&&c.eatWhile(/\d/)&&c.eat("}"))return"variable-2";c.pos=z}if(/[$@%]/.test(k)){var z=c.pos;if(c.eat("^")&&c.eat(/[A-Z]/)||!/[@$%&]/.test(p(c,-2))&&c.eat(/[=|\\\-#?@;:&`~\^!\[\]*'"$+.,\/<>()]/)){var _=c.current();if(S[_])return"variable-2"}c.pos=z}if(/[$@%&]/.test(k)&&(c.eatWhile(/[\w$]/)||c.eat("{")&&c.eatWhile(/[\w$]/)&&c.eat("}"))){var _=c.current();return S[_]?"variable-2":"variable"}if(k=="#"&&p(c,-2)!="$")return c.skipToEnd(),"comment";if(/[:+\-\^*$&%@=<>!?|\/~\.]/.test(k)){var z=c.pos;if(c.eatWhile(/[:+\-\^*$&%@=<>!?|\/~\.]/),S[c.current()])return"operator";c.pos=z}if(k=="_"&&c.pos==1){if(C(c,6)=="_END__")return g(c,d,["\0"],"comment");if(C(c,7)=="_DATA__")return g(c,d,["\0"],"variable-2");if(C(c,7)=="_C__")return g(c,d,["\0"],"string")}if(/\w/.test(k)){var z=c.pos;if(p(c,-2)=="{"&&(p(c,0)=="}"||c.eatWhile(/\w/)&&p(c,0)=="}"))return"string";c.pos=z}if(/[A-Z]/.test(k)){var W=p(c,-2),z=c.pos;if(c.eatWhile(/[A-Z_]/),/[\da-z]/.test(p(c,0)))c.pos=z;else{var _=S[c.current()];return _?(_[1]&&(_=_[0]),W!=":"?_==1?"keyword":_==2?"def":_==3?"atom":_==4?"operator":_==5?"variable-2":"meta":"meta"):"meta"}}if(/[a-zA-Z_]/.test(k)){var W=p(c,-2);c.eatWhile(/\w/);var _=S[c.current()];return _?(_[1]&&(_=_[0]),W!=":"?_==1?"keyword":_==2?"def":_==3?"atom":_==4?"operator":_==5?"variable-2":"meta":"meta"):"meta"}return null}return{startState:function(){return{tokenize:w,chain:null,style:null,tail:null}},token:function(c,d){return(d.tokenize||w)(c,d)},lineComment:"#"}}),o.registerHelper("wordChars","perl",/[\w$]/),o.defineMIME("text/x-perl","perl");function p(S,s){return S.string.charAt(S.pos+(s||0))}function v(S,s){if(s){var h=S.pos-s;return S.string.substr(h>=0?h:0,s)}else return S.string.substr(0,S.pos-1)}function C(S,s){var h=S.string.length,g=h-S.pos+1;return S.string.substr(S.pos,s&&s=(g=S.string.length-1)?S.pos=g:S.pos=h}})});var Su=Ke((ku,wu)=>{(function(o){typeof ku=="object"&&typeof wu=="object"?o(We(),Qn(),Vo()):typeof define=="function"&&define.amd?define(["../../lib/codemirror","../htmlmixed/htmlmixed","../clike/clike"],o):o(CodeMirror)})(function(o){"use strict";function p(T){for(var w={},c=T.split(" "),d=0;d\w/,!1)&&(w.tokenize=v([[["->",null]],[[/[\w]+/,"variable"]]],c,d)),"variable-2";for(var k=!1;!T.eol()&&(k||d===!1||!T.match("{$",!1)&&!T.match(/^(\$[a-zA-Z_][a-zA-Z0-9_]*|\$\{)/,!1));){if(!k&&T.match(c)){w.tokenize=null,w.tokStack.pop(),w.tokStack.pop();break}k=T.next()=="\\"&&!k}return"string"}var S="abstract and array as break case catch class clone const continue declare default do else elseif enddeclare endfor endforeach endif endswitch endwhile enum extends final for foreach function global goto if implements interface instanceof namespace new or private protected public static switch throw trait try use var while xor die echo empty exit eval include include_once isset list require require_once return print unset __halt_compiler self static parent yield insteadof finally readonly match",s="true false null TRUE FALSE NULL __CLASS__ __DIR__ __FILE__ __LINE__ __METHOD__ __FUNCTION__ __NAMESPACE__ __TRAIT__",h="func_num_args func_get_arg func_get_args strlen strcmp strncmp strcasecmp strncasecmp each error_reporting define defined trigger_error user_error set_error_handler restore_error_handler get_declared_classes get_loaded_extensions extension_loaded get_extension_funcs debug_backtrace constant bin2hex hex2bin sleep usleep time mktime gmmktime strftime gmstrftime strtotime date gmdate getdate localtime checkdate flush wordwrap htmlspecialchars htmlentities html_entity_decode md5 md5_file crc32 getimagesize image_type_to_mime_type phpinfo phpversion phpcredits strnatcmp strnatcasecmp substr_count strspn strcspn strtok strtoupper strtolower strpos strrpos strrev hebrev hebrevc nl2br basename dirname pathinfo stripslashes stripcslashes strstr stristr strrchr str_shuffle str_word_count strcoll substr substr_replace quotemeta ucfirst ucwords strtr addslashes addcslashes rtrim str_replace str_repeat count_chars chunk_split trim ltrim strip_tags similar_text explode implode setlocale localeconv parse_str str_pad chop strchr sprintf printf vprintf vsprintf sscanf fscanf parse_url urlencode urldecode rawurlencode rawurldecode readlink linkinfo link unlink exec system escapeshellcmd escapeshellarg passthru shell_exec proc_open proc_close rand srand getrandmax mt_rand mt_srand mt_getrandmax base64_decode base64_encode abs ceil floor round is_finite is_nan is_infinite bindec hexdec octdec decbin decoct dechex base_convert number_format fmod ip2long long2ip getenv putenv getopt microtime gettimeofday getrusage uniqid quoted_printable_decode set_time_limit get_cfg_var magic_quotes_runtime set_magic_quotes_runtime get_magic_quotes_gpc get_magic_quotes_runtime import_request_variables error_log serialize unserialize memory_get_usage memory_get_peak_usage var_dump var_export debug_zval_dump print_r highlight_file show_source highlight_string ini_get ini_get_all ini_set ini_alter ini_restore get_include_path set_include_path restore_include_path setcookie header headers_sent connection_aborted connection_status ignore_user_abort parse_ini_file is_uploaded_file move_uploaded_file intval floatval doubleval strval gettype settype is_null is_resource is_bool is_long is_float is_int is_integer is_double is_real is_numeric is_string is_array is_object is_scalar ereg ereg_replace eregi eregi_replace split spliti join sql_regcase dl pclose popen readfile rewind rmdir umask fclose feof fgetc fgets fgetss fread fopen fpassthru ftruncate fstat fseek ftell fflush fwrite fputs mkdir rename copy tempnam tmpfile file file_get_contents file_put_contents stream_select stream_context_create stream_context_set_params stream_context_set_option stream_context_get_options stream_filter_prepend stream_filter_append fgetcsv flock get_meta_tags stream_set_write_buffer set_file_buffer set_socket_blocking stream_set_blocking socket_set_blocking stream_get_meta_data stream_register_wrapper stream_wrapper_register stream_set_timeout socket_set_timeout socket_get_status realpath fnmatch fsockopen pfsockopen pack unpack get_browser crypt opendir closedir chdir getcwd rewinddir readdir dir glob fileatime filectime filegroup fileinode filemtime fileowner fileperms filesize filetype file_exists is_writable is_writeable is_readable is_executable is_file is_dir is_link stat lstat chown touch clearstatcache mail ob_start ob_flush ob_clean ob_end_flush ob_end_clean ob_get_flush ob_get_clean ob_get_length ob_get_level ob_get_status ob_get_contents ob_implicit_flush ob_list_handlers ksort krsort natsort natcasesort asort arsort sort rsort usort uasort uksort shuffle array_walk count end prev next reset current key min max in_array array_search extract compact array_fill range array_multisort array_push array_pop array_shift array_unshift array_splice array_slice array_merge array_merge_recursive array_keys array_values array_count_values array_reverse array_reduce array_pad array_flip array_change_key_case array_rand array_unique array_intersect array_intersect_assoc array_diff array_diff_assoc array_sum array_filter array_map array_chunk array_key_exists array_intersect_key array_combine array_column pos sizeof key_exists assert assert_options version_compare ftok str_rot13 aggregate session_name session_module_name session_save_path session_id session_regenerate_id session_decode session_register session_unregister session_is_registered session_encode session_start session_destroy session_unset session_set_save_handler session_cache_limiter session_cache_expire session_set_cookie_params session_get_cookie_params session_write_close preg_match preg_match_all preg_replace preg_replace_callback preg_split preg_quote preg_grep overload ctype_alnum ctype_alpha ctype_cntrl ctype_digit ctype_lower ctype_graph ctype_print ctype_punct ctype_space ctype_upper ctype_xdigit virtual apache_request_headers apache_note apache_lookup_uri apache_child_terminate apache_setenv apache_response_headers apache_get_version getallheaders mysql_connect mysql_pconnect mysql_close mysql_select_db mysql_create_db mysql_drop_db mysql_query mysql_unbuffered_query mysql_db_query mysql_list_dbs mysql_list_tables mysql_list_fields mysql_list_processes mysql_error mysql_errno mysql_affected_rows mysql_insert_id mysql_result mysql_num_rows mysql_num_fields mysql_fetch_row mysql_fetch_array mysql_fetch_assoc mysql_fetch_object mysql_data_seek mysql_fetch_lengths mysql_fetch_field mysql_field_seek mysql_free_result mysql_field_name mysql_field_table mysql_field_len mysql_field_type mysql_field_flags mysql_escape_string mysql_real_escape_string mysql_stat mysql_thread_id mysql_client_encoding mysql_get_client_info mysql_get_host_info mysql_get_proto_info mysql_get_server_info mysql_info mysql mysql_fieldname mysql_fieldtable mysql_fieldlen mysql_fieldtype mysql_fieldflags mysql_selectdb mysql_createdb mysql_dropdb mysql_freeresult mysql_numfields mysql_numrows mysql_listdbs mysql_listtables mysql_listfields mysql_db_name mysql_dbname mysql_tablename mysql_table_name pg_connect pg_pconnect pg_close pg_connection_status pg_connection_busy pg_connection_reset pg_host pg_dbname pg_port pg_tty pg_options pg_ping pg_query pg_send_query pg_cancel_query pg_fetch_result pg_fetch_row pg_fetch_assoc pg_fetch_array pg_fetch_object pg_fetch_all pg_affected_rows pg_get_result pg_result_seek pg_result_status pg_free_result pg_last_oid pg_num_rows pg_num_fields pg_field_name pg_field_num pg_field_size pg_field_type pg_field_prtlen pg_field_is_null pg_get_notify pg_get_pid pg_result_error pg_last_error pg_last_notice pg_put_line pg_end_copy pg_copy_to pg_copy_from pg_trace pg_untrace pg_lo_create pg_lo_unlink pg_lo_open pg_lo_close pg_lo_read pg_lo_write pg_lo_read_all pg_lo_import pg_lo_export pg_lo_seek pg_lo_tell pg_escape_string pg_escape_bytea pg_unescape_bytea pg_client_encoding pg_set_client_encoding pg_meta_data pg_convert pg_insert pg_update pg_delete pg_select pg_exec pg_getlastoid pg_cmdtuples pg_errormessage pg_numrows pg_numfields pg_fieldname pg_fieldsize pg_fieldtype pg_fieldnum pg_fieldprtlen pg_fieldisnull pg_freeresult pg_result pg_loreadall pg_locreate pg_lounlink pg_loopen pg_loclose pg_loread pg_lowrite pg_loimport pg_loexport http_response_code get_declared_traits getimagesizefromstring socket_import_stream stream_set_chunk_size trait_exists header_register_callback class_uses session_status session_register_shutdown echo print global static exit array empty eval isset unset die include require include_once require_once json_decode json_encode json_last_error json_last_error_msg curl_close curl_copy_handle curl_errno curl_error curl_escape curl_exec curl_file_create curl_getinfo curl_init curl_multi_add_handle curl_multi_close curl_multi_exec curl_multi_getcontent curl_multi_info_read curl_multi_init curl_multi_remove_handle curl_multi_select curl_multi_setopt curl_multi_strerror curl_pause curl_reset curl_setopt_array curl_setopt curl_share_close curl_share_init curl_share_setopt curl_strerror curl_unescape curl_version mysqli_affected_rows mysqli_autocommit mysqli_change_user mysqli_character_set_name mysqli_close mysqli_commit mysqli_connect_errno mysqli_connect_error mysqli_connect mysqli_data_seek mysqli_debug mysqli_dump_debug_info mysqli_errno mysqli_error_list mysqli_error mysqli_fetch_all mysqli_fetch_array mysqli_fetch_assoc mysqli_fetch_field_direct mysqli_fetch_field mysqli_fetch_fields mysqli_fetch_lengths mysqli_fetch_object mysqli_fetch_row mysqli_field_count mysqli_field_seek mysqli_field_tell mysqli_free_result mysqli_get_charset mysqli_get_client_info mysqli_get_client_stats mysqli_get_client_version mysqli_get_connection_stats mysqli_get_host_info mysqli_get_proto_info mysqli_get_server_info mysqli_get_server_version mysqli_info mysqli_init mysqli_insert_id mysqli_kill mysqli_more_results mysqli_multi_query mysqli_next_result mysqli_num_fields mysqli_num_rows mysqli_options mysqli_ping mysqli_prepare mysqli_query mysqli_real_connect mysqli_real_escape_string mysqli_real_query mysqli_reap_async_query mysqli_refresh mysqli_rollback mysqli_select_db mysqli_set_charset mysqli_set_local_infile_default mysqli_set_local_infile_handler mysqli_sqlstate mysqli_ssl_set mysqli_stat mysqli_stmt_init mysqli_store_result mysqli_thread_id mysqli_thread_safe mysqli_use_result mysqli_warning_count";o.registerHelper("hintWords","php",[S,s,h].join(" ").split(" ")),o.registerHelper("wordChars","php",/[\w$]/);var g={name:"clike",helperType:"php",keywords:p(S),blockKeywords:p("catch do else elseif for foreach if switch try while finally"),defKeywords:p("class enum function interface namespace trait"),atoms:p(s),builtin:p(h),multiLineStrings:!0,hooks:{$:function(T){return T.eatWhile(/[\w\$_]/),"variable-2"},"<":function(T,w){var c;if(c=T.match(/^<<\s*/)){var d=T.eat(/['"]/);T.eatWhile(/[\w\.]/);var k=T.current().slice(c[0].length+(d?2:1));if(d&&T.eat(d),k)return(w.tokStack||(w.tokStack=[])).push(k,0),w.tokenize=C(k,d!="'"),"string"}return!1},"#":function(T){for(;!T.eol()&&!T.match("?>",!1);)T.next();return"comment"},"/":function(T){if(T.eat("/")){for(;!T.eol()&&!T.match("?>",!1);)T.next();return"comment"}return!1},'"':function(T,w){return(w.tokStack||(w.tokStack=[])).push('"',0),w.tokenize=C('"'),"string"},"{":function(T,w){return w.tokStack&&w.tokStack.length&&w.tokStack[w.tokStack.length-1]++,!1},"}":function(T,w){return w.tokStack&&w.tokStack.length>0&&!--w.tokStack[w.tokStack.length-1]&&(w.tokenize=C(w.tokStack[w.tokStack.length-2])),!1}}};o.defineMode("php",function(T,w){var c=o.getMode(T,w&&w.htmlMode||"text/html"),d=o.getMode(T,g);function k(z,M){var _=M.curMode==d;if(z.sol()&&M.pending&&M.pending!='"'&&M.pending!="'"&&(M.pending=null),_)return _&&M.php.tokenize==null&&z.match("?>")?(M.curMode=c,M.curState=M.html,M.php.context.prev||(M.php=null),"meta"):d.token(z,M.curState);if(z.match(/^<\?\w*/))return M.curMode=d,M.php||(M.php=o.startState(d,c.indent(M.html,"",""))),M.curState=M.php,"meta";if(M.pending=='"'||M.pending=="'"){for(;!z.eol()&&z.next()!=M.pending;);var W="string"}else if(M.pending&&z.pos/.test(E)?M.pending=G[0]:M.pending={end:z.pos,style:W},z.backUp(E.length-O)),W}return{startState:function(){var z=o.startState(c),M=w.startOpen?o.startState(d):null;return{html:z,php:M,curMode:w.startOpen?d:c,curState:w.startOpen?M:z,pending:null}},copyState:function(z){var M=z.html,_=o.copyState(c,M),W=z.php,E=W&&o.copyState(d,W),O;return z.curMode==c?O=_:O=E,{html:_,php:E,curMode:z.curMode,curState:O,pending:z.pending}},token:k,indent:function(z,M,_){return z.curMode!=d&&/^\s*<\//.test(M)||z.curMode==d&&/^\?>/.test(M)?c.indent(z.html,M,_):z.curMode.indent(z.curState,M,_)},blockCommentStart:"/*",blockCommentEnd:"*/",lineComment:"//",innerMode:function(z){return{state:z.curState,mode:z.curMode}}}},"htmlmixed","clike"),o.defineMIME("application/x-httpd-php","php"),o.defineMIME("application/x-httpd-php-open",{name:"php",startOpen:!0}),o.defineMIME("text/x-php",g)})});var Cu=Ke((Tu,Lu)=>{(function(o){typeof Tu=="object"&&typeof Lu=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";function p(s){return new RegExp("^(("+s.join(")|(")+"))\\b","i")}var v=["package","message","import","syntax","required","optional","repeated","reserved","default","extensions","packed","bool","bytes","double","enum","float","string","int32","int64","uint32","uint64","sint32","sint64","fixed32","fixed64","sfixed32","sfixed64","option","service","rpc","returns"],C=p(v);o.registerHelper("hintWords","protobuf",v);var b=new RegExp("^[_A-Za-z\xA1-\uFFFF][_A-Za-z0-9\xA1-\uFFFF]*");function S(s){return s.eatSpace()?null:s.match("//")?(s.skipToEnd(),"comment"):s.match(/^[0-9\.+-]/,!1)&&(s.match(/^[+-]?0x[0-9a-fA-F]+/)||s.match(/^[+-]?\d*\.\d+([EeDd][+-]?\d+)?/)||s.match(/^[+-]?\d+([EeDd][+-]?\d+)?/))?"number":s.match(/^"([^"]|(""))*"/)||s.match(/^'([^']|(''))*'/)?"string":s.match(C)?"keyword":s.match(b)?"variable":(s.next(),null)}o.defineMode("protobuf",function(){return{token:S,fold:"brace"}}),o.defineMIME("text/x-protobuf","protobuf")})});var Mu=Ke((Eu,zu)=>{(function(o){typeof Eu=="object"&&typeof zu=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";function p(h){return new RegExp("^(("+h.join(")|(")+"))\\b")}var v=p(["and","or","not","is"]),C=["as","assert","break","class","continue","def","del","elif","else","except","finally","for","from","global","if","import","lambda","pass","raise","return","try","while","with","yield","in","False","True"],b=["abs","all","any","bin","bool","bytearray","callable","chr","classmethod","compile","complex","delattr","dict","dir","divmod","enumerate","eval","filter","float","format","frozenset","getattr","globals","hasattr","hash","help","hex","id","input","int","isinstance","issubclass","iter","len","list","locals","map","max","memoryview","min","next","object","oct","open","ord","pow","property","range","repr","reversed","round","set","setattr","slice","sorted","staticmethod","str","sum","super","tuple","type","vars","zip","__import__","NotImplemented","Ellipsis","__debug__"];o.registerHelper("hintWords","python",C.concat(b).concat(["exec","print"]));function S(h){return h.scopes[h.scopes.length-1]}o.defineMode("python",function(h,g){for(var T="error",w=g.delimiters||g.singleDelimiters||/^[\(\)\[\]\{\}@,:`=;\.\\]/,c=[g.singleOperators,g.doubleOperators,g.doubleDelimiters,g.tripleDelimiters,g.operators||/^([-+*/%\/&|^]=?|[<>=]+|\/\/=?|\*\*=?|!=|[~!@]|\.\.\.)/],d=0;dR?D(X):le0&&j(K,X)&&(xe+=" "+T),xe}}return re(K,X)}function re(K,X,N){if(K.eatSpace())return null;if(!N&&K.match(/^#.*/))return"comment";if(K.match(/^[0-9\.]/,!1)){var R=!1;if(K.match(/^[\d_]*\.\d+(e[\+\-]?\d+)?/i)&&(R=!0),K.match(/^[\d_]+\.\d*/)&&(R=!0),K.match(/^\.\d+/)&&(R=!0),R)return K.eat(/J/i),"number";var le=!1;if(K.match(/^0x[0-9a-f_]+/i)&&(le=!0),K.match(/^0b[01_]+/i)&&(le=!0),K.match(/^0o[0-7_]+/i)&&(le=!0),K.match(/^[1-9][\d_]*(e[\+\-]?[\d_]+)?/)&&(K.eat(/J/i),le=!0),K.match(/^0(?![\dx])/i)&&(le=!0),le)return K.eat(/L/i),"number"}if(K.match(E)){var xe=K.current().toLowerCase().indexOf("f")!==-1;return xe?(X.tokenize=q(K.current(),X.tokenize),X.tokenize(K,X)):(X.tokenize=I(K.current(),X.tokenize),X.tokenize(K,X))}for(var F=0;F=0;)K=K.substr(1);var N=K.length==1,R="string";function le(F){return function(L,de){var ze=re(L,de,!0);return ze=="punctuation"&&(L.current()=="{"?de.tokenize=le(F+1):L.current()=="}"&&(F>1?de.tokenize=le(F-1):de.tokenize=xe)),ze}}function xe(F,L){for(;!F.eol();)if(F.eatWhile(/[^'"\{\}\\]/),F.eat("\\")){if(F.next(),N&&F.eol())return R}else{if(F.match(K))return L.tokenize=X,R;if(F.match("{{"))return R;if(F.match("{",!1))return L.tokenize=le(0),F.current()?R:L.tokenize(F,L);if(F.match("}}"))return R;if(F.match("}"))return T;F.eat(/['"]/)}if(N){if(g.singleLineStringErrors)return T;L.tokenize=X}return R}return xe.isString=!0,xe}function I(K,X){for(;"rubf".indexOf(K.charAt(0).toLowerCase())>=0;)K=K.substr(1);var N=K.length==1,R="string";function le(xe,F){for(;!xe.eol();)if(xe.eatWhile(/[^'"\\]/),xe.eat("\\")){if(xe.next(),N&&xe.eol())return R}else{if(xe.match(K))return F.tokenize=X,R;xe.eat(/['"]/)}if(N){if(g.singleLineStringErrors)return T;F.tokenize=X}return R}return le.isString=!0,le}function D(K){for(;S(K).type!="py";)K.scopes.pop();K.scopes.push({offset:S(K).offset+h.indentUnit,type:"py",align:null})}function Q(K,X,N){var R=K.match(/^[\s\[\{\(]*(?:#|$)/,!1)?null:K.column()+1;X.scopes.push({offset:X.indent+k,type:N,align:R})}function j(K,X){for(var N=K.indentation();X.scopes.length>1&&S(X).offset>N;){if(S(X).type!="py")return!0;X.scopes.pop()}return S(X).offset!=N}function V(K,X){K.sol()&&(X.beginningOfLine=!0,X.dedent=!1);var N=X.tokenize(K,X),R=K.current();if(X.beginningOfLine&&R=="@")return K.match(W,!1)?"meta":_?"operator":T;if(/\S/.test(R)&&(X.beginningOfLine=!1),(N=="variable"||N=="builtin")&&X.lastToken=="meta"&&(N="meta"),(R=="pass"||R=="return")&&(X.dedent=!0),R=="lambda"&&(X.lambda=!0),R==":"&&!X.lambda&&S(X).type=="py"&&K.match(/^\s*(?:#|$)/,!1)&&D(X),R.length==1&&!/string|comment/.test(N)){var le="[({".indexOf(R);if(le!=-1&&Q(K,X,"])}".slice(le,le+1)),le="])}".indexOf(R),le!=-1)if(S(X).type==R)X.indent=X.scopes.pop().offset-k;else return T}return X.dedent&&K.eol()&&S(X).type=="py"&&X.scopes.length>1&&X.scopes.pop(),N}var y={startState:function(K){return{tokenize:J,scopes:[{offset:K||0,type:"py",align:null}],indent:K||0,lastToken:null,lambda:!1,dedent:0}},token:function(K,X){var N=X.errorToken;N&&(X.errorToken=!1);var R=V(K,X);return R&&R!="comment"&&(X.lastToken=R=="keyword"||R=="punctuation"?K.current():R),R=="punctuation"&&(R=null),K.eol()&&X.lambda&&(X.lambda=!1),N?R+" "+T:R},indent:function(K,X){if(K.tokenize!=J)return K.tokenize.isString?o.Pass:0;var N=S(K),R=N.type==X.charAt(0)||N.type=="py"&&!K.dedent&&/^(else:|elif |except |finally:)/.test(X);return N.align!=null?N.align-(R?1:0):N.offset-(R?k:0)},electricInput:/^\s*([\}\]\)]|else:|elif |except |finally:)$/,closeBrackets:{triples:`'"`},lineComment:"#",fold:"indent"};return y}),o.defineMIME("text/x-python","python");var s=function(h){return h.split(" ")};o.defineMIME("text/x-cython",{name:"python",extra_keywords:s("by cdef cimport cpdef ctypedef enum except extern gil include nogil property public readonly struct union DEF IF ELIF ELSE")})})});var qu=Ke((Au,Du)=>{(function(o){typeof Au=="object"&&typeof Du=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";function p(g){for(var T={},w=0,c=g.length;w]/)?(E.eat(/[\<\>]/),"atom"):E.eat(/[\+\-\*\/\&\|\:\!]/)?"atom":E.eat(/[a-zA-Z$@_\xa1-\uffff]/)?(E.eatWhile(/[\w$\xa1-\uffff]/),E.eat(/[\?\!\=]/),"atom"):"operator";if(G=="@"&&E.match(/^@?[a-zA-Z_\xa1-\uffff]/))return E.eat("@"),E.eatWhile(/[\w\xa1-\uffff]/),"variable-2";if(G=="$")return E.eat(/[a-zA-Z_]/)?E.eatWhile(/[\w]/):E.eat(/\d/)?E.eat(/\d/):E.next(),"variable-3";if(/[a-zA-Z_\xa1-\uffff]/.test(G))return E.eatWhile(/[\w\xa1-\uffff]/),E.eat(/[\?\!]/),E.eat(":")?"atom":"ident";if(G=="|"&&(O.varList||O.lastTok=="{"||O.lastTok=="do"))return T="|",null;if(/[\(\)\[\]{}\\;]/.test(G))return T=G,null;if(G=="-"&&E.eat(">"))return"arrow";if(/[=+\-\/*:\.^%<>~|]/.test(G)){var D=E.eatWhile(/[=+\-\/*:\.^%<>~|]/);return G=="."&&!D&&(T="."),"operator"}else return null}}}function d(E){for(var O=E.pos,G=0,J,re=!1,q=!1;(J=E.next())!=null;)if(q)q=!1;else{if("[{(".indexOf(J)>-1)G++;else if("]})".indexOf(J)>-1){if(G--,G<0)break}else if(J=="/"&&G==0){re=!0;break}q=J=="\\"}return E.backUp(E.pos-O),re}function k(E){return E||(E=1),function(O,G){if(O.peek()=="}"){if(E==1)return G.tokenize.pop(),G.tokenize[G.tokenize.length-1](O,G);G.tokenize[G.tokenize.length-1]=k(E-1)}else O.peek()=="{"&&(G.tokenize[G.tokenize.length-1]=k(E+1));return c(O,G)}}function z(){var E=!1;return function(O,G){return E?(G.tokenize.pop(),G.tokenize[G.tokenize.length-1](O,G)):(E=!0,c(O,G))}}function M(E,O,G,J){return function(re,q){var I=!1,D;for(q.context.type==="read-quoted-paused"&&(q.context=q.context.prev,re.eat("}"));(D=re.next())!=null;){if(D==E&&(J||!I)){q.tokenize.pop();break}if(G&&D=="#"&&!I){if(re.eat("{")){E=="}"&&(q.context={prev:q.context,type:"read-quoted-paused"}),q.tokenize.push(k());break}else if(/[@\$]/.test(re.peek())){q.tokenize.push(z());break}}I=!I&&D=="\\"}return O}}function _(E,O){return function(G,J){return O&&G.eatSpace(),G.match(E)?J.tokenize.pop():G.skipToEnd(),"string"}}function W(E,O){return E.sol()&&E.match("=end")&&E.eol()&&O.tokenize.pop(),E.skipToEnd(),"comment"}return{startState:function(){return{tokenize:[c],indented:0,context:{type:"top",indented:-g.indentUnit},continuedLine:!1,lastTok:null,varList:!1}},token:function(E,O){T=null,E.sol()&&(O.indented=E.indentation());var G=O.tokenize[O.tokenize.length-1](E,O),J,re=T;if(G=="ident"){var q=E.current();G=O.lastTok=="."?"property":C.propertyIsEnumerable(E.current())?"keyword":/^[A-Z]/.test(q)?"tag":O.lastTok=="def"||O.lastTok=="class"||O.varList?"def":"variable",G=="keyword"&&(re=q,b.propertyIsEnumerable(q)?J="indent":S.propertyIsEnumerable(q)?J="dedent":((q=="if"||q=="unless")&&E.column()==E.indentation()||q=="do"&&O.context.indented{(function(o){typeof Fu=="object"&&typeof Iu=="object"?o(We(),Di()):typeof define=="function"&&define.amd?define(["../../lib/codemirror","../../addon/mode/simple"],o):o(CodeMirror)})(function(o){"use strict";o.defineSimpleMode("rust",{start:[{regex:/b?"/,token:"string",next:"string"},{regex:/b?r"/,token:"string",next:"string_raw"},{regex:/b?r#+"/,token:"string",next:"string_raw_hash"},{regex:/'(?:[^'\\]|\\(?:[nrt0'"]|x[\da-fA-F]{2}|u\{[\da-fA-F]{6}\}))'/,token:"string-2"},{regex:/b'(?:[^']|\\(?:['\\nrt0]|x[\da-fA-F]{2}))'/,token:"string-2"},{regex:/(?:(?:[0-9][0-9_]*)(?:(?:[Ee][+-]?[0-9_]+)|\.[0-9_]+(?:[Ee][+-]?[0-9_]+)?)(?:f32|f64)?)|(?:0(?:b[01_]+|(?:o[0-7_]+)|(?:x[0-9a-fA-F_]+))|(?:[0-9][0-9_]*))(?:u8|u16|u32|u64|i8|i16|i32|i64|isize|usize)?/,token:"number"},{regex:/(let(?:\s+mut)?|fn|enum|mod|struct|type|union)(\s+)([a-zA-Z_][a-zA-Z0-9_]*)/,token:["keyword",null,"def"]},{regex:/(?:abstract|alignof|as|async|await|box|break|continue|const|crate|do|dyn|else|enum|extern|fn|for|final|if|impl|in|loop|macro|match|mod|move|offsetof|override|priv|proc|pub|pure|ref|return|self|sizeof|static|struct|super|trait|type|typeof|union|unsafe|unsized|use|virtual|where|while|yield)\b/,token:"keyword"},{regex:/\b(?:Self|isize|usize|char|bool|u8|u16|u32|u64|f16|f32|f64|i8|i16|i32|i64|str|Option)\b/,token:"atom"},{regex:/\b(?:true|false|Some|None|Ok|Err)\b/,token:"builtin"},{regex:/\b(fn)(\s+)([a-zA-Z_][a-zA-Z0-9_]*)/,token:["keyword",null,"def"]},{regex:/#!?\[.*\]/,token:"meta"},{regex:/\/\/.*/,token:"comment"},{regex:/\/\*/,token:"comment",next:"comment"},{regex:/[-+\/*=<>!]+/,token:"operator"},{regex:/[a-zA-Z_]\w*!/,token:"variable-3"},{regex:/[a-zA-Z_]\w*/,token:"variable"},{regex:/[\{\[\(]/,indent:!0},{regex:/[\}\]\)]/,dedent:!0}],string:[{regex:/"/,token:"string",next:"start"},{regex:/(?:[^\\"]|\\(?:.|$))*/,token:"string"}],string_raw:[{regex:/"/,token:"string",next:"start"},{regex:/[^"]*/,token:"string"}],string_raw_hash:[{regex:/"#+/,token:"string",next:"start"},{regex:/(?:[^"]|"(?!#))*/,token:"string"}],comment:[{regex:/.*?\*\//,token:"comment",next:"start"},{regex:/.*/,token:"comment"}],meta:{dontIndentStates:["comment"],electricInput:/^\s*\}$/,blockCommentStart:"/*",blockCommentEnd:"*/",lineComment:"//",fold:"brace"}}),o.defineMIME("text/x-rustsrc","rust"),o.defineMIME("text/rust","rust")})});var ea=Ke((Ou,Pu)=>{(function(o){typeof Ou=="object"&&typeof Pu=="object"?o(We(),gn()):typeof define=="function"&&define.amd?define(["../../lib/codemirror","../css/css"],o):o(CodeMirror)})(function(o){"use strict";o.defineMode("sass",function(p){var v=o.mimeModes["text/css"],C=v.propertyKeywords||{},b=v.colorKeywords||{},S=v.valueKeywords||{},s=v.fontProperties||{};function h(q){return new RegExp("^"+q.join("|"))}var g=["true","false","null","auto"],T=new RegExp("^"+g.join("|")),w=["\\(","\\)","=",">","<","==",">=","<=","\\+","-","\\!=","/","\\*","%","and","or","not",";","\\{","\\}",":"],c=h(w),d=/^::?[a-zA-Z_][\w\-]*/,k;function z(q){return!q.peek()||q.match(/\s+$/,!1)}function M(q,I){var D=q.peek();return D===")"?(q.next(),I.tokenizer=J,"operator"):D==="("?(q.next(),q.eatSpace(),"operator"):D==="'"||D==='"'?(I.tokenizer=W(q.next()),"string"):(I.tokenizer=W(")",!1),"string")}function _(q,I){return function(D,Q){return D.sol()&&D.indentation()<=q?(Q.tokenizer=J,J(D,Q)):(I&&D.skipTo("*/")?(D.next(),D.next(),Q.tokenizer=J):D.skipToEnd(),"comment")}}function W(q,I){I==null&&(I=!0);function D(Q,j){var V=Q.next(),y=Q.peek(),K=Q.string.charAt(Q.pos-2),X=V!=="\\"&&y===q||V===q&&K!=="\\";return X?(V!==q&&I&&Q.next(),z(Q)&&(j.cursorHalf=0),j.tokenizer=J,"string"):V==="#"&&y==="{"?(j.tokenizer=E(D),Q.next(),"operator"):"string"}return D}function E(q){return function(I,D){return I.peek()==="}"?(I.next(),D.tokenizer=q,"operator"):J(I,D)}}function O(q){if(q.indentCount==0){q.indentCount++;var I=q.scopes[0].offset,D=I+p.indentUnit;q.scopes.unshift({offset:D})}}function G(q){q.scopes.length!=1&&q.scopes.shift()}function J(q,I){var D=q.peek();if(q.match("/*"))return I.tokenizer=_(q.indentation(),!0),I.tokenizer(q,I);if(q.match("//"))return I.tokenizer=_(q.indentation(),!1),I.tokenizer(q,I);if(q.match("#{"))return I.tokenizer=E(J),"operator";if(D==='"'||D==="'")return q.next(),I.tokenizer=W(D),"string";if(I.cursorHalf){if(D==="#"&&(q.next(),q.match(/[0-9a-fA-F]{6}|[0-9a-fA-F]{3}/))||q.match(/^-?[0-9\.]+/))return z(q)&&(I.cursorHalf=0),"number";if(q.match(/^(px|em|in)\b/))return z(q)&&(I.cursorHalf=0),"unit";if(q.match(T))return z(q)&&(I.cursorHalf=0),"keyword";if(q.match(/^url/)&&q.peek()==="(")return I.tokenizer=M,z(q)&&(I.cursorHalf=0),"atom";if(D==="$")return q.next(),q.eatWhile(/[\w-]/),z(q)&&(I.cursorHalf=0),"variable-2";if(D==="!")return q.next(),I.cursorHalf=0,q.match(/^[\w]+/)?"keyword":"operator";if(q.match(c))return z(q)&&(I.cursorHalf=0),"operator";if(q.eatWhile(/[\w-]/))return z(q)&&(I.cursorHalf=0),k=q.current().toLowerCase(),S.hasOwnProperty(k)?"atom":b.hasOwnProperty(k)?"keyword":C.hasOwnProperty(k)?(I.prevProp=q.current().toLowerCase(),"property"):"tag";if(z(q))return I.cursorHalf=0,null}else{if(D==="-"&&q.match(/^-\w+-/))return"meta";if(D==="."){if(q.next(),q.match(/^[\w-]+/))return O(I),"qualifier";if(q.peek()==="#")return O(I),"tag"}if(D==="#"){if(q.next(),q.match(/^[\w-]+/))return O(I),"builtin";if(q.peek()==="#")return O(I),"tag"}if(D==="$")return q.next(),q.eatWhile(/[\w-]/),"variable-2";if(q.match(/^-?[0-9\.]+/))return"number";if(q.match(/^(px|em|in)\b/))return"unit";if(q.match(T))return"keyword";if(q.match(/^url/)&&q.peek()==="(")return I.tokenizer=M,"atom";if(D==="="&&q.match(/^=[\w-]+/))return O(I),"meta";if(D==="+"&&q.match(/^\+[\w-]+/))return"variable-3";if(D==="@"&&q.match("@extend")&&(q.match(/\s*[\w]/)||G(I)),q.match(/^@(else if|if|media|else|for|each|while|mixin|function)/))return O(I),"def";if(D==="@")return q.next(),q.eatWhile(/[\w-]/),"def";if(q.eatWhile(/[\w-]/))if(q.match(/ *: *[\w-\+\$#!\("']/,!1)){k=q.current().toLowerCase();var Q=I.prevProp+"-"+k;return C.hasOwnProperty(Q)?"property":C.hasOwnProperty(k)?(I.prevProp=k,"property"):s.hasOwnProperty(k)?"property":"tag"}else return q.match(/ *:/,!1)?(O(I),I.cursorHalf=1,I.prevProp=q.current().toLowerCase(),"property"):(q.match(/ *,/,!1)||O(I),"tag");if(D===":")return q.match(d)?"variable-3":(q.next(),I.cursorHalf=1,"operator")}return q.match(c)?"operator":(q.next(),null)}function re(q,I){q.sol()&&(I.indentCount=0);var D=I.tokenizer(q,I),Q=q.current();if((Q==="@return"||Q==="}")&&G(I),D!==null){for(var j=q.pos-Q.length,V=j+p.indentUnit*I.indentCount,y=[],K=0;K{(function(o){typeof Bu=="object"&&typeof ju=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";o.defineMode("shell",function(){var p={};function v(d,k){for(var z=0;z1&&d.eat("$");var z=d.next();return/['"({]/.test(z)?(k.tokens[0]=h(z,z=="("?"quote":z=="{"?"def":"string"),c(d,k)):(/\d/.test(z)||d.eatWhile(/\w/),k.tokens.shift(),"def")};function w(d){return function(k,z){return k.sol()&&k.string==d&&z.tokens.shift(),k.skipToEnd(),"string-2"}}function c(d,k){return(k.tokens[0]||s)(d,k)}return{startState:function(){return{tokens:[]}},token:function(d,k){return c(d,k)},closeBrackets:"()[]{}''\"\"``",lineComment:"#",fold:"brace"}}),o.defineMIME("text/x-sh","shell"),o.defineMIME("application/x-sh","shell")})});var Uu=Ke((Hu,Wu)=>{(function(o){typeof Hu=="object"&&typeof Wu=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";o.defineMode("sql",function(g,T){var w=T.client||{},c=T.atoms||{false:!0,true:!0,null:!0},d=T.builtin||s(h),k=T.keywords||s(S),z=T.operatorChars||/^[*+\-%<>!=&|~^\/]/,M=T.support||{},_=T.hooks||{},W=T.dateSQL||{date:!0,time:!0,timestamp:!0},E=T.backslashStringEscapes!==!1,O=T.brackets||/^[\{}\(\)\[\]]/,G=T.punctuation||/^[;.,:]/;function J(Q,j){var V=Q.next();if(_[V]){var y=_[V](Q,j);if(y!==!1)return y}if(M.hexNumber&&(V=="0"&&Q.match(/^[xX][0-9a-fA-F]+/)||(V=="x"||V=="X")&&Q.match(/^'[0-9a-fA-F]*'/)))return"number";if(M.binaryNumber&&((V=="b"||V=="B")&&Q.match(/^'[01]*'/)||V=="0"&&Q.match(/^b[01]+/)))return"number";if(V.charCodeAt(0)>47&&V.charCodeAt(0)<58)return Q.match(/^[0-9]*(\.[0-9]+)?([eE][-+]?[0-9]+)?/),M.decimallessFloat&&Q.match(/^\.(?!\.)/),"number";if(V=="?"&&(Q.eatSpace()||Q.eol()||Q.eat(";")))return"variable-3";if(V=="'"||V=='"'&&M.doubleQuote)return j.tokenize=re(V),j.tokenize(Q,j);if((M.nCharCast&&(V=="n"||V=="N")||M.charsetCast&&V=="_"&&Q.match(/[a-z][a-z0-9]*/i))&&(Q.peek()=="'"||Q.peek()=='"'))return"keyword";if(M.escapeConstant&&(V=="e"||V=="E")&&(Q.peek()=="'"||Q.peek()=='"'&&M.doubleQuote))return j.tokenize=function(X,N){return(N.tokenize=re(X.next(),!0))(X,N)},"keyword";if(M.commentSlashSlash&&V=="/"&&Q.eat("/"))return Q.skipToEnd(),"comment";if(M.commentHash&&V=="#"||V=="-"&&Q.eat("-")&&(!M.commentSpaceRequired||Q.eat(" ")))return Q.skipToEnd(),"comment";if(V=="/"&&Q.eat("*"))return j.tokenize=q(1),j.tokenize(Q,j);if(V=="."){if(M.zerolessFloat&&Q.match(/^(?:\d+(?:e[+-]?\d+)?)/i))return"number";if(Q.match(/^\.+/))return null;if(Q.match(/^[\w\d_$#]+/))return"variable-2"}else{if(z.test(V))return Q.eatWhile(z),"operator";if(O.test(V))return"bracket";if(G.test(V))return Q.eatWhile(G),"punctuation";if(V=="{"&&(Q.match(/^( )*(d|D|t|T|ts|TS)( )*'[^']*'( )*}/)||Q.match(/^( )*(d|D|t|T|ts|TS)( )*"[^"]*"( )*}/)))return"number";Q.eatWhile(/^[_\w\d]/);var K=Q.current().toLowerCase();return W.hasOwnProperty(K)&&(Q.match(/^( )+'[^']*'/)||Q.match(/^( )+"[^"]*"/))?"number":c.hasOwnProperty(K)?"atom":d.hasOwnProperty(K)?"type":k.hasOwnProperty(K)?"keyword":w.hasOwnProperty(K)?"builtin":null}}function re(Q,j){return function(V,y){for(var K=!1,X;(X=V.next())!=null;){if(X==Q&&!K){y.tokenize=J;break}K=(E||j)&&!K&&X=="\\"}return"string"}}function q(Q){return function(j,V){var y=j.match(/^.*?(\/\*|\*\/)/);return y?y[1]=="/*"?V.tokenize=q(Q+1):Q>1?V.tokenize=q(Q-1):V.tokenize=J:j.skipToEnd(),"comment"}}function I(Q,j,V){j.context={prev:j.context,indent:Q.indentation(),col:Q.column(),type:V}}function D(Q){Q.indent=Q.context.indent,Q.context=Q.context.prev}return{startState:function(){return{tokenize:J,context:null}},token:function(Q,j){if(Q.sol()&&j.context&&j.context.align==null&&(j.context.align=!1),j.tokenize==J&&Q.eatSpace())return null;var V=j.tokenize(Q,j);if(V=="comment")return V;j.context&&j.context.align==null&&(j.context.align=!0);var y=Q.current();return y=="("?I(Q,j,")"):y=="["?I(Q,j,"]"):j.context&&j.context.type==y&&D(j),V},indent:function(Q,j){var V=Q.context;if(!V)return o.Pass;var y=j.charAt(0)==V.type;return V.align?V.col+(y?0:1):V.indent+(y?0:g.indentUnit)},blockCommentStart:"/*",blockCommentEnd:"*/",lineComment:M.commentSlashSlash?"//":M.commentHash?"#":"--",closeBrackets:"()[]{}''\"\"``",config:T}});function p(g){for(var T;(T=g.next())!=null;)if(T=="`"&&!g.eat("`"))return"variable-2";return g.backUp(g.current().length-1),g.eatWhile(/\w/)?"variable-2":null}function v(g){for(var T;(T=g.next())!=null;)if(T=='"'&&!g.eat('"'))return"variable-2";return g.backUp(g.current().length-1),g.eatWhile(/\w/)?"variable-2":null}function C(g){return g.eat("@")&&(g.match("session."),g.match("local."),g.match("global.")),g.eat("'")?(g.match(/^.*'/),"variable-2"):g.eat('"')?(g.match(/^.*"/),"variable-2"):g.eat("`")?(g.match(/^.*`/),"variable-2"):g.match(/^[0-9a-zA-Z$\.\_]+/)?"variable-2":null}function b(g){return g.eat("N")?"atom":g.match(/^[a-zA-Z.#!?]/)?"variable-2":null}var S="alter and as asc between by count create delete desc distinct drop from group having in insert into is join like not on or order select set table union update values where limit ";function s(g){for(var T={},w=g.split(" "),c=0;c!=^\&|\/]/,brackets:/^[\{}\(\)]/,punctuation:/^[;.,:/]/,backslashStringEscapes:!1,dateSQL:s("date datetimeoffset datetime2 smalldatetime datetime time"),hooks:{"@":C}}),o.defineMIME("text/x-mysql",{name:"sql",client:s("charset clear connect edit ego exit go help nopager notee nowarning pager print prompt quit rehash source status system tee"),keywords:s(S+"accessible action add after algorithm all analyze asensitive at authors auto_increment autocommit avg avg_row_length before binary binlog both btree cache call cascade cascaded case catalog_name chain change changed character check checkpoint checksum class_origin client_statistics close coalesce code collate collation collations column columns comment commit committed completion concurrent condition connection consistent constraint contains continue contributors convert cross current current_date current_time current_timestamp current_user cursor data database databases day_hour day_microsecond day_minute day_second deallocate dec declare default delay_key_write delayed delimiter des_key_file describe deterministic dev_pop dev_samp deviance diagnostics directory disable discard distinctrow div dual dumpfile each elseif enable enclosed end ends engine engines enum errors escape escaped even event events every execute exists exit explain extended fast fetch field fields first flush for force foreign found_rows full fulltext function general get global grant grants group group_concat handler hash help high_priority hosts hour_microsecond hour_minute hour_second if ignore ignore_server_ids import index index_statistics infile inner innodb inout insensitive insert_method install interval invoker isolation iterate key keys kill language last leading leave left level limit linear lines list load local localtime localtimestamp lock logs low_priority master master_heartbeat_period master_ssl_verify_server_cert masters match max max_rows maxvalue message_text middleint migrate min min_rows minute_microsecond minute_second mod mode modifies modify mutex mysql_errno natural next no no_write_to_binlog offline offset one online open optimize option optionally out outer outfile pack_keys parser partition partitions password phase plugin plugins prepare preserve prev primary privileges procedure processlist profile profiles purge query quick range read read_write reads real rebuild recover references regexp relaylog release remove rename reorganize repair repeatable replace require resignal restrict resume return returns revoke right rlike rollback rollup row row_format rtree savepoint schedule schema schema_name schemas second_microsecond security sensitive separator serializable server session share show signal slave slow smallint snapshot soname spatial specific sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_no_cache sql_small_result sqlexception sqlstate sqlwarning ssl start starting starts status std stddev stddev_pop stddev_samp storage straight_join subclass_origin sum suspend table_name table_statistics tables tablespace temporary terminated to trailing transaction trigger triggers truncate uncommitted undo uninstall unique unlock upgrade usage use use_frm user user_resources user_statistics using utc_date utc_time utc_timestamp value variables varying view views warnings when while with work write xa xor year_month zerofill begin do then else loop repeat"),builtin:s("bool boolean bit blob decimal double float long longblob longtext medium mediumblob mediumint mediumtext time timestamp tinyblob tinyint tinytext text bigint int int1 int2 int3 int4 int8 integer float float4 float8 double char varbinary varchar varcharacter precision date datetime year unsigned signed numeric"),atoms:s("false true null unknown"),operatorChars:/^[*+\-%<>!=&|^]/,dateSQL:s("date time timestamp"),support:s("decimallessFloat zerolessFloat binaryNumber hexNumber doubleQuote nCharCast charsetCast commentHash commentSpaceRequired"),hooks:{"@":C,"`":p,"\\":b}}),o.defineMIME("text/x-mariadb",{name:"sql",client:s("charset clear connect edit ego exit go help nopager notee nowarning pager print prompt quit rehash source status system tee"),keywords:s(S+"accessible action add after algorithm all always analyze asensitive at authors auto_increment autocommit avg avg_row_length before binary binlog both btree cache call cascade cascaded case catalog_name chain change changed character check checkpoint checksum class_origin client_statistics close coalesce code collate collation collations column columns comment commit committed completion concurrent condition connection consistent constraint contains continue contributors convert cross current current_date current_time current_timestamp current_user cursor data database databases day_hour day_microsecond day_minute day_second deallocate dec declare default delay_key_write delayed delimiter des_key_file describe deterministic dev_pop dev_samp deviance diagnostics directory disable discard distinctrow div dual dumpfile each elseif enable enclosed end ends engine engines enum errors escape escaped even event events every execute exists exit explain extended fast fetch field fields first flush for force foreign found_rows full fulltext function general generated get global grant grants group group_concat handler hard hash help high_priority hosts hour_microsecond hour_minute hour_second if ignore ignore_server_ids import index index_statistics infile inner innodb inout insensitive insert_method install interval invoker isolation iterate key keys kill language last leading leave left level limit linear lines list load local localtime localtimestamp lock logs low_priority master master_heartbeat_period master_ssl_verify_server_cert masters match max max_rows maxvalue message_text middleint migrate min min_rows minute_microsecond minute_second mod mode modifies modify mutex mysql_errno natural next no no_write_to_binlog offline offset one online open optimize option optionally out outer outfile pack_keys parser partition partitions password persistent phase plugin plugins prepare preserve prev primary privileges procedure processlist profile profiles purge query quick range read read_write reads real rebuild recover references regexp relaylog release remove rename reorganize repair repeatable replace require resignal restrict resume return returns revoke right rlike rollback rollup row row_format rtree savepoint schedule schema schema_name schemas second_microsecond security sensitive separator serializable server session share show shutdown signal slave slow smallint snapshot soft soname spatial specific sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_no_cache sql_small_result sqlexception sqlstate sqlwarning ssl start starting starts status std stddev stddev_pop stddev_samp storage straight_join subclass_origin sum suspend table_name table_statistics tables tablespace temporary terminated to trailing transaction trigger triggers truncate uncommitted undo uninstall unique unlock upgrade usage use use_frm user user_resources user_statistics using utc_date utc_time utc_timestamp value variables varying view views virtual warnings when while with work write xa xor year_month zerofill begin do then else loop repeat"),builtin:s("bool boolean bit blob decimal double float long longblob longtext medium mediumblob mediumint mediumtext time timestamp tinyblob tinyint tinytext text bigint int int1 int2 int3 int4 int8 integer float float4 float8 double char varbinary varchar varcharacter precision date datetime year unsigned signed numeric"),atoms:s("false true null unknown"),operatorChars:/^[*+\-%<>!=&|^]/,dateSQL:s("date time timestamp"),support:s("decimallessFloat zerolessFloat binaryNumber hexNumber doubleQuote nCharCast charsetCast commentHash commentSpaceRequired"),hooks:{"@":C,"`":p,"\\":b}}),o.defineMIME("text/x-sqlite",{name:"sql",client:s("auth backup bail binary changes check clone databases dbinfo dump echo eqp exit explain fullschema headers help import imposter indexes iotrace limit lint load log mode nullvalue once open output print prompt quit read restore save scanstats schema separator session shell show stats system tables testcase timeout timer trace vfsinfo vfslist vfsname width"),keywords:s(S+"abort action add after all analyze attach autoincrement before begin cascade case cast check collate column commit conflict constraint cross current_date current_time current_timestamp database default deferrable deferred detach each else end escape except exclusive exists explain fail for foreign full glob if ignore immediate index indexed initially inner instead intersect isnull key left limit match natural no notnull null of offset outer plan pragma primary query raise recursive references regexp reindex release rename replace restrict right rollback row savepoint temp temporary then to transaction trigger unique using vacuum view virtual when with without"),builtin:s("bool boolean bit blob decimal double float long longblob longtext medium mediumblob mediumint mediumtext time timestamp tinyblob tinyint tinytext text clob bigint int int2 int8 integer float double char varchar date datetime year unsigned signed numeric real"),atoms:s("null current_date current_time current_timestamp"),operatorChars:/^[*+\-%<>!=&|/~]/,dateSQL:s("date time timestamp datetime"),support:s("decimallessFloat zerolessFloat"),identifierQuote:'"',hooks:{"@":C,":":C,"?":C,$:C,'"':v,"`":p}}),o.defineMIME("text/x-cassandra",{name:"sql",client:{},keywords:s("add all allow alter and any apply as asc authorize batch begin by clustering columnfamily compact consistency count create custom delete desc distinct drop each_quorum exists filtering from grant if in index insert into key keyspace keyspaces level limit local_one local_quorum modify nan norecursive nosuperuser not of on one order password permission permissions primary quorum rename revoke schema select set storage superuser table three to token truncate ttl two type unlogged update use user users using values where with writetime"),builtin:s("ascii bigint blob boolean counter decimal double float frozen inet int list map static text timestamp timeuuid tuple uuid varchar varint"),atoms:s("false true infinity NaN"),operatorChars:/^[<>=]/,dateSQL:{},support:s("commentSlashSlash decimallessFloat"),hooks:{}}),o.defineMIME("text/x-plsql",{name:"sql",client:s("appinfo arraysize autocommit autoprint autorecovery autotrace blockterminator break btitle cmdsep colsep compatibility compute concat copycommit copytypecheck define describe echo editfile embedded escape exec execute feedback flagger flush heading headsep instance linesize lno loboffset logsource long longchunksize markup native newpage numformat numwidth pagesize pause pno recsep recsepchar release repfooter repheader serveroutput shiftinout show showmode size spool sqlblanklines sqlcase sqlcode sqlcontinue sqlnumber sqlpluscompatibility sqlprefix sqlprompt sqlterminator suffix tab term termout time timing trimout trimspool ttitle underline verify version wrap"),keywords:s("abort accept access add all alter and any array arraylen as asc assert assign at attributes audit authorization avg base_table begin between binary_integer body boolean by case cast char char_base check close cluster clusters colauth column comment commit compress connect connected constant constraint crash create current currval cursor data_base database date dba deallocate debugoff debugon decimal declare default definition delay delete desc digits dispose distinct do drop else elseif elsif enable end entry escape exception exception_init exchange exclusive exists exit external fast fetch file for force form from function generic goto grant group having identified if immediate in increment index indexes indicator initial initrans insert interface intersect into is key level library like limited local lock log logging long loop master maxextents maxtrans member minextents minus mislabel mode modify multiset new next no noaudit nocompress nologging noparallel not nowait number_base object of off offline on online only open option or order out package parallel partition pctfree pctincrease pctused pls_integer positive positiven pragma primary prior private privileges procedure public raise range raw read rebuild record ref references refresh release rename replace resource restrict return returning returns reverse revoke rollback row rowid rowlabel rownum rows run savepoint schema segment select separate session set share snapshot some space split sql start statement storage subtype successful synonym tabauth table tables tablespace task terminate then to trigger truncate type union unique unlimited unrecoverable unusable update use using validate value values variable view views when whenever where while with work"),builtin:s("abs acos add_months ascii asin atan atan2 average bfile bfilename bigserial bit blob ceil character chartorowid chr clob concat convert cos cosh count dec decode deref dual dump dup_val_on_index empty error exp false float floor found glb greatest hextoraw initcap instr instrb int integer isopen last_day least length lengthb ln lower lpad ltrim lub make_ref max min mlslabel mod months_between natural naturaln nchar nclob new_time next_day nextval nls_charset_decl_len nls_charset_id nls_charset_name nls_initcap nls_lower nls_sort nls_upper nlssort no_data_found notfound null number numeric nvarchar2 nvl others power rawtohex real reftohex round rowcount rowidtochar rowtype rpad rtrim serial sign signtype sin sinh smallint soundex sqlcode sqlerrm sqrt stddev string substr substrb sum sysdate tan tanh to_char text to_date to_label to_multi_byte to_number to_single_byte translate true trunc uid unlogged upper user userenv varchar varchar2 variance varying vsize xml"),operatorChars:/^[*\/+\-%<>!=~]/,dateSQL:s("date time timestamp"),support:s("doubleQuote nCharCast zerolessFloat binaryNumber hexNumber")}),o.defineMIME("text/x-hive",{name:"sql",keywords:s("select alter $elem$ $key$ $value$ add after all analyze and archive as asc before between binary both bucket buckets by cascade case cast change cluster clustered clusterstatus collection column columns comment compute concatenate continue create cross cursor data database databases dbproperties deferred delete delimited desc describe directory disable distinct distribute drop else enable end escaped exclusive exists explain export extended external fetch fields fileformat first format formatted from full function functions grant group having hold_ddltime idxproperties if import in index indexes inpath inputdriver inputformat insert intersect into is items join keys lateral left like limit lines load local location lock locks mapjoin materialized minus msck no_drop nocompress not of offline on option or order out outer outputdriver outputformat overwrite partition partitioned partitions percent plus preserve procedure purge range rcfile read readonly reads rebuild recordreader recordwriter recover reduce regexp rename repair replace restrict revoke right rlike row schema schemas semi sequencefile serde serdeproperties set shared show show_database sort sorted ssl statistics stored streamtable table tables tablesample tblproperties temporary terminated textfile then tmp to touch transform trigger unarchive undo union uniquejoin unlock update use using utc utc_tmestamp view when where while with admin authorization char compact compactions conf cube current current_date current_timestamp day decimal defined dependency directories elem_type exchange file following for grouping hour ignore inner interval jar less logical macro minute month more none noscan over owner partialscan preceding pretty principals protection reload rewrite role roles rollup rows second server sets skewed transactions truncate unbounded unset uri user values window year"),builtin:s("bool boolean long timestamp tinyint smallint bigint int float double date datetime unsigned string array struct map uniontype key_type utctimestamp value_type varchar"),atoms:s("false true null unknown"),operatorChars:/^[*+\-%<>!=]/,dateSQL:s("date timestamp"),support:s("doubleQuote binaryNumber hexNumber")}),o.defineMIME("text/x-pgsql",{name:"sql",client:s("source"),keywords:s(S+"a abort abs absent absolute access according action ada add admin after aggregate alias all allocate also alter always analyse analyze and any are array array_agg array_max_cardinality as asc asensitive assert assertion assignment asymmetric at atomic attach attribute attributes authorization avg backward base64 before begin begin_frame begin_partition bernoulli between bigint binary bit bit_length blob blocked bom boolean both breadth by c cache call called cardinality cascade cascaded case cast catalog catalog_name ceil ceiling chain char char_length character character_length character_set_catalog character_set_name character_set_schema characteristics characters check checkpoint class class_origin clob close cluster coalesce cobol collate collation collation_catalog collation_name collation_schema collect column column_name columns command_function command_function_code comment comments commit committed concurrently condition condition_number configuration conflict connect connection connection_name constant constraint constraint_catalog constraint_name constraint_schema constraints constructor contains content continue control conversion convert copy corr corresponding cost count covar_pop covar_samp create cross csv cube cume_dist current current_catalog current_date current_default_transform_group current_path current_role current_row current_schema current_time current_timestamp current_transform_group_for_type current_user cursor cursor_name cycle data database datalink datatype date datetime_interval_code datetime_interval_precision day db deallocate debug dec decimal declare default defaults deferrable deferred defined definer degree delete delimiter delimiters dense_rank depends depth deref derived desc describe descriptor detach detail deterministic diagnostics dictionary disable discard disconnect dispatch distinct dlnewcopy dlpreviouscopy dlurlcomplete dlurlcompleteonly dlurlcompletewrite dlurlpath dlurlpathonly dlurlpathwrite dlurlscheme dlurlserver dlvalue do document domain double drop dump dynamic dynamic_function dynamic_function_code each element else elseif elsif empty enable encoding encrypted end end_frame end_partition endexec enforced enum equals errcode error escape event every except exception exclude excluding exclusive exec execute exists exit exp explain expression extension external extract false family fetch file filter final first first_value flag float floor following for force foreach foreign fortran forward found frame_row free freeze from fs full function functions fusion g general generated get global go goto grant granted greatest group grouping groups handler having header hex hierarchy hint hold hour id identity if ignore ilike immediate immediately immutable implementation implicit import in include including increment indent index indexes indicator info inherit inherits initially inline inner inout input insensitive insert instance instantiable instead int integer integrity intersect intersection interval into invoker is isnull isolation join k key key_member key_type label lag language large last last_value lateral lead leading leakproof least left length level library like like_regex limit link listen ln load local localtime localtimestamp location locator lock locked log logged loop lower m map mapping match matched materialized max max_cardinality maxvalue member merge message message_length message_octet_length message_text method min minute minvalue mod mode modifies module month more move multiset mumps name names namespace national natural nchar nclob nesting new next nfc nfd nfkc nfkd nil no none normalize normalized not nothing notice notify notnull nowait nth_value ntile null nullable nullif nulls number numeric object occurrences_regex octet_length octets of off offset oids old on only open operator option options or order ordering ordinality others out outer output over overlaps overlay overriding owned owner p pad parallel parameter parameter_mode parameter_name parameter_ordinal_position parameter_specific_catalog parameter_specific_name parameter_specific_schema parser partial partition pascal passing passthrough password path percent percent_rank percentile_cont percentile_disc perform period permission pg_context pg_datatype_name pg_exception_context pg_exception_detail pg_exception_hint placing plans pli policy portion position position_regex power precedes preceding precision prepare prepared preserve primary print_strict_params prior privileges procedural procedure procedures program public publication query quote raise range rank read reads real reassign recheck recovery recursive ref references referencing refresh regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy regr_syy reindex relative release rename repeatable replace replica requiring reset respect restart restore restrict result result_oid return returned_cardinality returned_length returned_octet_length returned_sqlstate returning returns reverse revoke right role rollback rollup routine routine_catalog routine_name routine_schema routines row row_count row_number rows rowtype rule savepoint scale schema schema_name schemas scope scope_catalog scope_name scope_schema scroll search second section security select selective self sensitive sequence sequences serializable server server_name session session_user set setof sets share show similar simple size skip slice smallint snapshot some source space specific specific_name specifictype sql sqlcode sqlerror sqlexception sqlstate sqlwarning sqrt stable stacked standalone start state statement static statistics stddev_pop stddev_samp stdin stdout storage strict strip structure style subclass_origin submultiset subscription substring substring_regex succeeds sum symmetric sysid system system_time system_user t table table_name tables tablesample tablespace temp template temporary text then ties time timestamp timezone_hour timezone_minute to token top_level_count trailing transaction transaction_active transactions_committed transactions_rolled_back transform transforms translate translate_regex translation treat trigger trigger_catalog trigger_name trigger_schema trim trim_array true truncate trusted type types uescape unbounded uncommitted under unencrypted union unique unknown unlink unlisten unlogged unnamed unnest until untyped update upper uri usage use_column use_variable user user_defined_type_catalog user_defined_type_code user_defined_type_name user_defined_type_schema using vacuum valid validate validator value value_of values var_pop var_samp varbinary varchar variable_conflict variadic varying verbose version versioning view views volatile warning when whenever where while whitespace width_bucket window with within without work wrapper write xml xmlagg xmlattributes xmlbinary xmlcast xmlcomment xmlconcat xmldeclaration xmldocument xmlelement xmlexists xmlforest xmliterate xmlnamespaces xmlparse xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltext xmlvalidate year yes zone"),builtin:s("bigint int8 bigserial serial8 bit varying varbit boolean bool box bytea character char varchar cidr circle date double precision float8 inet integer int int4 interval json jsonb line lseg macaddr macaddr8 money numeric decimal path pg_lsn point polygon real float4 smallint int2 smallserial serial2 serial serial4 text time zone timetz timestamp timestamptz tsquery tsvector txid_snapshot uuid xml"),atoms:s("false true null unknown"),operatorChars:/^[*\/+\-%<>!=&|^\/#@?~]/,backslashStringEscapes:!1,identifierQuote:'"',hooks:{'"':v},dateSQL:s("date time timestamp"),support:s("decimallessFloat zerolessFloat binaryNumber hexNumber nCharCast charsetCast escapeConstant")}),o.defineMIME("text/x-gql",{name:"sql",keywords:s("ancestor and asc by contains desc descendant distinct from group has in is limit offset on order select superset where"),atoms:s("false true"),builtin:s("blob datetime first key __key__ string integer double boolean null"),operatorChars:/^[*+\-%<>!=]/}),o.defineMIME("text/x-gpsql",{name:"sql",client:s("source"),keywords:s("abort absolute access action active add admin after aggregate all also alter always analyse analyze and any array as asc assertion assignment asymmetric at authorization backward before begin between bigint binary bit boolean both by cache called cascade cascaded case cast chain char character characteristics check checkpoint class close cluster coalesce codegen collate column comment commit committed concurrency concurrently configuration connection constraint constraints contains content continue conversion copy cost cpu_rate_limit create createdb createexttable createrole createuser cross csv cube current current_catalog current_date current_role current_schema current_time current_timestamp current_user cursor cycle data database day deallocate dec decimal declare decode default defaults deferrable deferred definer delete delimiter delimiters deny desc dictionary disable discard distinct distributed do document domain double drop dxl each else enable encoding encrypted end enum errors escape every except exchange exclude excluding exclusive execute exists explain extension external extract false family fetch fields filespace fill filter first float following for force foreign format forward freeze from full function global grant granted greatest group group_id grouping handler hash having header hold host hour identity if ignore ilike immediate immutable implicit in including inclusive increment index indexes inherit inherits initially inline inner inout input insensitive insert instead int integer intersect interval into invoker is isnull isolation join key language large last leading least left level like limit list listen load local localtime localtimestamp location lock log login mapping master match maxvalue median merge minute minvalue missing mode modifies modify month move name names national natural nchar new newline next no nocreatedb nocreateexttable nocreaterole nocreateuser noinherit nologin none noovercommit nosuperuser not nothing notify notnull nowait null nullif nulls numeric object of off offset oids old on only operator option options or order ordered others out outer over overcommit overlaps overlay owned owner parser partial partition partitions passing password percent percentile_cont percentile_disc placing plans position preceding precision prepare prepared preserve primary prior privileges procedural procedure protocol queue quote randomly range read readable reads real reassign recheck recursive ref references reindex reject relative release rename repeatable replace replica reset resource restart restrict returning returns revoke right role rollback rollup rootpartition row rows rule savepoint scatter schema scroll search second security segment select sequence serializable session session_user set setof sets share show similar simple smallint some split sql stable standalone start statement statistics stdin stdout storage strict strip subpartition subpartitions substring superuser symmetric sysid system table tablespace temp template temporary text then threshold ties time timestamp to trailing transaction treat trigger trim true truncate trusted type unbounded uncommitted unencrypted union unique unknown unlisten until update user using vacuum valid validation validator value values varchar variadic varying verbose version view volatile web when where whitespace window with within without work writable write xml xmlattributes xmlconcat xmlelement xmlexists xmlforest xmlparse xmlpi xmlroot xmlserialize year yes zone"),builtin:s("bigint int8 bigserial serial8 bit varying varbit boolean bool box bytea character char varchar cidr circle date double precision float float8 inet integer int int4 interval json jsonb line lseg macaddr macaddr8 money numeric decimal path pg_lsn point polygon real float4 smallint int2 smallserial serial2 serial serial4 text time without zone with timetz timestamp timestamptz tsquery tsvector txid_snapshot uuid xml"),atoms:s("false true null unknown"),operatorChars:/^[*+\-%<>!=&|^\/#@?~]/,dateSQL:s("date time timestamp"),support:s("decimallessFloat zerolessFloat binaryNumber hexNumber nCharCast charsetCast")}),o.defineMIME("text/x-sparksql",{name:"sql",keywords:s("add after all alter analyze and anti archive array as asc at between bucket buckets by cache cascade case cast change clear cluster clustered codegen collection column columns comment commit compact compactions compute concatenate cost create cross cube current current_date current_timestamp database databases data dbproperties defined delete delimited deny desc describe dfs directories distinct distribute drop else end escaped except exchange exists explain export extended external false fields fileformat first following for format formatted from full function functions global grant group grouping having if ignore import in index indexes inner inpath inputformat insert intersect interval into is items join keys last lateral lazy left like limit lines list load local location lock locks logical macro map minus msck natural no not null nulls of on optimize option options or order out outer outputformat over overwrite partition partitioned partitions percent preceding principals purge range recordreader recordwriter recover reduce refresh regexp rename repair replace reset restrict revoke right rlike role roles rollback rollup row rows schema schemas select semi separated serde serdeproperties set sets show skewed sort sorted start statistics stored stratify struct table tables tablesample tblproperties temp temporary terminated then to touch transaction transactions transform true truncate unarchive unbounded uncache union unlock unset use using values view when where window with"),builtin:s("abs acos acosh add_months aggregate and any approx_count_distinct approx_percentile array array_contains array_distinct array_except array_intersect array_join array_max array_min array_position array_remove array_repeat array_sort array_union arrays_overlap arrays_zip ascii asin asinh assert_true atan atan2 atanh avg base64 between bigint bin binary bit_and bit_count bit_get bit_length bit_or bit_xor bool_and bool_or boolean bround btrim cardinality case cast cbrt ceil ceiling char char_length character_length chr coalesce collect_list collect_set concat concat_ws conv corr cos cosh cot count count_if count_min_sketch covar_pop covar_samp crc32 cume_dist current_catalog current_database current_date current_timestamp current_timezone current_user date date_add date_format date_from_unix_date date_part date_sub date_trunc datediff day dayofmonth dayofweek dayofyear decimal decode degrees delimited dense_rank div double e element_at elt encode every exists exp explode explode_outer expm1 extract factorial filter find_in_set first first_value flatten float floor forall format_number format_string from_csv from_json from_unixtime from_utc_timestamp get_json_object getbit greatest grouping grouping_id hash hex hour hypot if ifnull in initcap inline inline_outer input_file_block_length input_file_block_start input_file_name inputformat instr int isnan isnotnull isnull java_method json_array_length json_object_keys json_tuple kurtosis lag last last_day last_value lcase lead least left length levenshtein like ln locate log log10 log1p log2 lower lpad ltrim make_date make_dt_interval make_interval make_timestamp make_ym_interval map map_concat map_entries map_filter map_from_arrays map_from_entries map_keys map_values map_zip_with max max_by md5 mean min min_by minute mod monotonically_increasing_id month months_between named_struct nanvl negative next_day not now nth_value ntile nullif nvl nvl2 octet_length or outputformat overlay parse_url percent_rank percentile percentile_approx pi pmod posexplode posexplode_outer position positive pow power printf quarter radians raise_error rand randn random rank rcfile reflect regexp regexp_extract regexp_extract_all regexp_like regexp_replace repeat replace reverse right rint rlike round row_number rpad rtrim schema_of_csv schema_of_json second sentences sequence sequencefile serde session_window sha sha1 sha2 shiftleft shiftright shiftrightunsigned shuffle sign signum sin sinh size skewness slice smallint some sort_array soundex space spark_partition_id split sqrt stack std stddev stddev_pop stddev_samp str_to_map string struct substr substring substring_index sum tan tanh textfile timestamp timestamp_micros timestamp_millis timestamp_seconds tinyint to_csv to_date to_json to_timestamp to_unix_timestamp to_utc_timestamp transform transform_keys transform_values translate trim trunc try_add try_divide typeof ucase unbase64 unhex uniontype unix_date unix_micros unix_millis unix_seconds unix_timestamp upper uuid var_pop var_samp variance version weekday weekofyear when width_bucket window xpath xpath_boolean xpath_double xpath_float xpath_int xpath_long xpath_number xpath_short xpath_string xxhash64 year zip_with"),atoms:s("false true null"),operatorChars:/^[*\/+\-%<>!=~&|^]/,dateSQL:s("date time timestamp"),support:s("doubleQuote zerolessFloat")}),o.defineMIME("text/x-esper",{name:"sql",client:s("source"),keywords:s("alter and as asc between by count create delete desc distinct drop from group having in insert into is join like not on or order select set table union update values where limit after all and as at asc avedev avg between by case cast coalesce count create current_timestamp day days delete define desc distinct else end escape events every exists false first from full group having hour hours in inner insert instanceof into irstream is istream join last lastweekday left limit like max match_recognize matches median measures metadatasql min minute minutes msec millisecond milliseconds not null offset on or order outer output partition pattern prev prior regexp retain-union retain-intersection right rstream sec second seconds select set some snapshot sql stddev sum then true unidirectional until update variable weekday when where window"),builtin:{},atoms:s("false true null"),operatorChars:/^[*+\-%<>!=&|^\/#@?~]/,dateSQL:s("time"),support:s("decimallessFloat zerolessFloat binaryNumber hexNumber")}),o.defineMIME("text/x-trino",{name:"sql",keywords:s("abs absent acos add admin after all all_match alter analyze and any any_match approx_distinct approx_most_frequent approx_percentile approx_set arbitrary array_agg array_distinct array_except array_intersect array_join array_max array_min array_position array_remove array_sort array_union arrays_overlap as asc asin at at_timezone atan atan2 authorization avg bar bernoulli beta_cdf between bing_tile bing_tile_at bing_tile_coordinates bing_tile_polygon bing_tile_quadkey bing_tile_zoom_level bing_tiles_around bit_count bitwise_and bitwise_and_agg bitwise_left_shift bitwise_not bitwise_or bitwise_or_agg bitwise_right_shift bitwise_right_shift_arithmetic bitwise_xor bool_and bool_or both by call cardinality cascade case cast catalogs cbrt ceil ceiling char2hexint checksum chr classify coalesce codepoint column columns combinations comment commit committed concat concat_ws conditional constraint contains contains_sequence convex_hull_agg copartition corr cos cosh cosine_similarity count count_if covar_pop covar_samp crc32 create cross cube cume_dist current current_catalog current_date current_groups current_path current_role current_schema current_time current_timestamp current_timezone current_user data date_add date_diff date_format date_parse date_trunc day day_of_month day_of_week day_of_year deallocate default define definer degrees delete dense_rank deny desc describe descriptor distinct distributed dow doy drop e element_at else empty empty_approx_set encoding end error escape evaluate_classifier_predictions every except excluding execute exists exp explain extract false features fetch filter final first first_value flatten floor following for format format_datetime format_number from from_base from_base32 from_base64 from_base64url from_big_endian_32 from_big_endian_64 from_encoded_polyline from_geojson_geometry from_hex from_ieee754_32 from_ieee754_64 from_iso8601_date from_iso8601_timestamp from_iso8601_timestamp_nanos from_unixtime from_unixtime_nanos from_utf8 full functions geometric_mean geometry_from_hadoop_shape geometry_invalid_reason geometry_nearest_points geometry_to_bing_tiles geometry_union geometry_union_agg grant granted grants graphviz great_circle_distance greatest group grouping groups hamming_distance hash_counts having histogram hmac_md5 hmac_sha1 hmac_sha256 hmac_sha512 hour human_readable_seconds if ignore in including index infinity initial inner input insert intersect intersection_cardinality into inverse_beta_cdf inverse_normal_cdf invoker io is is_finite is_infinite is_json_scalar is_nan isolation jaccard_index join json_array json_array_contains json_array_get json_array_length json_exists json_extract json_extract_scalar json_format json_object json_parse json_query json_size json_value keep key keys kurtosis lag last last_day_of_month last_value lateral lead leading learn_classifier learn_libsvm_classifier learn_libsvm_regressor learn_regressor least left length level levenshtein_distance like limit line_interpolate_point line_interpolate_points line_locate_point listagg ln local localtime localtimestamp log log10 log2 logical lower lpad ltrim luhn_check make_set_digest map_agg map_concat map_entries map_filter map_from_entries map_keys map_union map_values map_zip_with match match_recognize matched matches materialized max max_by md5 measures merge merge_set_digest millisecond min min_by minute mod month multimap_agg multimap_from_entries murmur3 nan natural next nfc nfd nfkc nfkd ngrams no none none_match normal_cdf normalize not now nth_value ntile null nullif nulls numeric_histogram object objectid_timestamp of offset omit on one only option or order ordinality outer output over overflow parse_data_size parse_datetime parse_duration partition partitions passing past path pattern per percent_rank permute pi position pow power preceding prepare privileges properties prune qdigest_agg quarter quotes radians rand random range rank read recursive reduce reduce_agg refresh regexp_count regexp_extract regexp_extract_all regexp_like regexp_position regexp_replace regexp_split regr_intercept regr_slope regress rename render repeat repeatable replace reset respect restrict returning reverse revoke rgb right role roles rollback rollup round row_number rows rpad rtrim running scalar schema schemas second security seek select sequence serializable session set sets sha1 sha256 sha512 show shuffle sign simplify_geometry sin skewness skip slice some soundex spatial_partitioning spatial_partitions split split_part split_to_map split_to_multimap spooky_hash_v2_32 spooky_hash_v2_64 sqrt st_area st_asbinary st_astext st_boundary st_buffer st_centroid st_contains st_convexhull st_coorddim st_crosses st_difference st_dimension st_disjoint st_distance st_endpoint st_envelope st_envelopeaspts st_equals st_exteriorring st_geometries st_geometryfromtext st_geometryn st_geometrytype st_geomfrombinary st_interiorringn st_interiorrings st_intersection st_intersects st_isclosed st_isempty st_isring st_issimple st_isvalid st_length st_linefromtext st_linestring st_multipoint st_numgeometries st_numinteriorring st_numpoints st_overlaps st_point st_pointn st_points st_polygon st_relate st_startpoint st_symdifference st_touches st_union st_within st_x st_xmax st_xmin st_y st_ymax st_ymin start starts_with stats stddev stddev_pop stddev_samp string strpos subset substr substring sum system table tables tablesample tan tanh tdigest_agg text then ties timestamp_objectid timezone_hour timezone_minute to to_base to_base32 to_base64 to_base64url to_big_endian_32 to_big_endian_64 to_char to_date to_encoded_polyline to_geojson_geometry to_geometry to_hex to_ieee754_32 to_ieee754_64 to_iso8601 to_milliseconds to_spherical_geography to_timestamp to_unixtime to_utf8 trailing transaction transform transform_keys transform_values translate trim trim_array true truncate try try_cast type typeof uescape unbounded uncommitted unconditional union unique unknown unmatched unnest update upper url_decode url_encode url_extract_fragment url_extract_host url_extract_parameter url_extract_path url_extract_port url_extract_protocol url_extract_query use user using utf16 utf32 utf8 validate value value_at_quantile values values_at_quantiles var_pop var_samp variance verbose version view week week_of_year when where width_bucket wilson_interval_lower wilson_interval_upper window with with_timezone within without word_stem work wrapper write xxhash64 year year_of_week yow zip zip_with"),builtin:s("array bigint bingtile boolean char codepoints color date decimal double function geometry hyperloglog int integer interval ipaddress joniregexp json json2016 jsonpath kdbtree likepattern map model objectid p4hyperloglog precision qdigest re2jregexp real regressor row setdigest smallint sphericalgeography tdigest time timestamp tinyint uuid varbinary varchar zone"),atoms:s("false true null unknown"),operatorChars:/^[[\]|<>=!\-+*/%]/,dateSQL:s("date time timestamp zone"),support:s("decimallessFloat zerolessFloat hexNumber")})})});var ta=Ke(($u,Ku)=>{(function(o){typeof $u=="object"&&typeof Ku=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";o.defineMode("stylus",function(E){for(var O=E.indentUnit,G="",J=_(p),re=/^(a|b|i|s|col|em)$/i,q=_(S),I=_(s),D=_(T),Q=_(g),j=_(v),V=M(v),y=_(b),K=_(C),X=_(h),N=/^\s*([.]{2,3}|&&|\|\||\*\*|[?!=:]?=|[-+*\/%<>]=?|\?:|\~)/,R=M(w),le=_(c),xe=new RegExp(/^\-(moz|ms|o|webkit)-/i),F=_(d),L="",de={},ze,pe,Ee,ge;G.length|~|\/)?\s*[\w-]*([a-z0-9-]|\*|\/\*)(\(|,)?)/),H.context.line.firstWord=L?L[0].replace(/^\s*/,""):"",H.context.line.indent=$.indentation(),ze=$.peek(),$.match("//"))return $.skipToEnd(),["comment","comment"];if($.match("/*"))return H.tokenize=qe,qe($,H);if(ze=='"'||ze=="'")return $.next(),H.tokenize=Se(ze),H.tokenize($,H);if(ze=="@")return $.next(),$.eatWhile(/[\w\\-]/),["def",$.current()];if(ze=="#"){if($.next(),$.match(/^[0-9a-f]{3}([0-9a-f]([0-9a-f]{2}){0,2})?\b(?!-)/i))return["atom","atom"];if($.match(/^[a-z][\w-]*/i))return["builtin","hash"]}return $.match(xe)?["meta","vendor-prefixes"]:$.match(/^-?[0-9]?\.?[0-9]/)?($.eatWhile(/[a-z%]/i),["number","unit"]):ze=="!"?($.next(),[$.match(/^(important|optional)/i)?"keyword":"operator","important"]):ze=="."&&$.match(/^\.[a-z][\w-]*/i)?["qualifier","qualifier"]:$.match(V)?($.peek()=="("&&(H.tokenize=Be),["property","word"]):$.match(/^[a-z][\w-]*\(/i)?($.backUp(1),["keyword","mixin"]):$.match(/^(\+|-)[a-z][\w-]*\(/i)?($.backUp(1),["keyword","block-mixin"]):$.string.match(/^\s*&/)&&$.match(/^[-_]+[a-z][\w-]*/)?["qualifier","qualifier"]:$.match(/^(\/|&)(-|_|:|\.|#|[a-z])/)?($.backUp(1),["variable-3","reference"]):$.match(/^&{1}\s*$/)?["variable-3","reference"]:$.match(R)?["operator","operator"]:$.match(/^\$?[-_]*[a-z0-9]+[\w-]*/i)?$.match(/^(\.|\[)[\w-\'\"\]]+/i,!1)&&!U($.current())?($.match("."),["variable-2","variable-name"]):["variable-2","word"]:$.match(N)?["operator",$.current()]:/[:;,{}\[\]\(\)]/.test(ze)?($.next(),[null,ze]):($.next(),[null,null])}function qe($,H){for(var se=!1,De;(De=$.next())!=null;){if(se&&De=="/"){H.tokenize=null;break}se=De=="*"}return["comment","comment"]}function Se($){return function(H,se){for(var De=!1,nt;(nt=H.next())!=null;){if(nt==$&&!De){$==")"&&H.backUp(1);break}De=!De&&nt=="\\"}return(nt==$||!De&&$!=")")&&(se.tokenize=null),["string","string"]}}function Be($,H){return $.next(),$.match(/\s*[\"\')]/,!1)?H.tokenize=null:H.tokenize=Se(")"),[null,"("]}function Ze($,H,se,De){this.type=$,this.indent=H,this.prev=se,this.line=De||{firstWord:"",indent:0}}function ke($,H,se,De){return De=De>=0?De:O,$.context=new Ze(se,H.indentation()+De,$.context),se}function Je($,H){var se=$.context.indent-O;return H=H||!1,$.context=$.context.prev,H&&($.context.indent=se),$.context.type}function Re($,H,se){return de[se.context.type]($,H,se)}function Ge($,H,se,De){for(var nt=De||1;nt>0;nt--)se.context=se.context.prev;return Re($,H,se)}function U($){return $.toLowerCase()in J}function Z($){return $=$.toLowerCase(),$ in q||$ in X}function ce($){return $.toLowerCase()in le}function He($){return $.toLowerCase().match(xe)}function te($){var H=$.toLowerCase(),se="variable-2";return U($)?se="tag":ce($)?se="block-keyword":Z($)?se="property":H in D||H in F?se="atom":H=="return"||H in Q?se="keyword":$.match(/^[A-Z]/)&&(se="string"),se}function fe($,H){return Me(H)&&($=="{"||$=="]"||$=="hash"||$=="qualifier")||$=="block-mixin"}function oe($,H){return $=="{"&&H.match(/^\s*\$?[\w-]+/i,!1)}function Ue($,H){return $==":"&&H.match(/^[a-z-]+/,!1)}function we($){return $.sol()||$.string.match(new RegExp("^\\s*"+W($.current())))}function Me($){return $.eol()||$.match(/^\s*$/,!1)}function Le($){var H=/^\s*[-_]*[a-z0-9]+[\w-]*/i,se=typeof $=="string"?$.match(H):$.string.match(H);return se?se[0].replace(/^\s*/,""):""}return de.block=function($,H,se){if($=="comment"&&we(H)||$==","&&Me(H)||$=="mixin")return ke(se,H,"block",0);if(oe($,H))return ke(se,H,"interpolation");if(Me(H)&&$=="]"&&!/^\s*(\.|#|:|\[|\*|&)/.test(H.string)&&!U(Le(H)))return ke(se,H,"block",0);if(fe($,H))return ke(se,H,"block");if($=="}"&&Me(H))return ke(se,H,"block",0);if($=="variable-name")return H.string.match(/^\s?\$[\w-\.\[\]\'\"]+$/)||ce(Le(H))?ke(se,H,"variableName"):ke(se,H,"variableName",0);if($=="=")return!Me(H)&&!ce(Le(H))?ke(se,H,"block",0):ke(se,H,"block");if($=="*"&&(Me(H)||H.match(/\s*(,|\.|#|\[|:|{)/,!1)))return ge="tag",ke(se,H,"block");if(Ue($,H))return ke(se,H,"pseudo");if(/@(font-face|media|supports|(-moz-)?document)/.test($))return ke(se,H,Me(H)?"block":"atBlock");if(/@(-(moz|ms|o|webkit)-)?keyframes$/.test($))return ke(se,H,"keyframes");if(/@extends?/.test($))return ke(se,H,"extend",0);if($&&$.charAt(0)=="@")return H.indentation()>0&&Z(H.current().slice(1))?(ge="variable-2","block"):/(@import|@require|@charset)/.test($)?ke(se,H,"block",0):ke(se,H,"block");if($=="reference"&&Me(H))return ke(se,H,"block");if($=="(")return ke(se,H,"parens");if($=="vendor-prefixes")return ke(se,H,"vendorPrefixes");if($=="word"){var De=H.current();if(ge=te(De),ge=="property")return we(H)?ke(se,H,"block",0):(ge="atom","block");if(ge=="tag"){if(/embed|menu|pre|progress|sub|table/.test(De)&&Z(Le(H))||H.string.match(new RegExp("\\[\\s*"+De+"|"+De+"\\s*\\]")))return ge="atom","block";if(re.test(De)&&(we(H)&&H.string.match(/=/)||!we(H)&&!H.string.match(/^(\s*\.|#|\&|\[|\/|>|\*)/)&&!U(Le(H))))return ge="variable-2",ce(Le(H))?"block":ke(se,H,"block",0);if(Me(H))return ke(se,H,"block")}if(ge=="block-keyword")return ge="keyword",H.current(/(if|unless)/)&&!we(H)?"block":ke(se,H,"block");if(De=="return")return ke(se,H,"block",0);if(ge=="variable-2"&&H.string.match(/^\s?\$[\w-\.\[\]\'\"]+$/))return ke(se,H,"block")}return se.context.type},de.parens=function($,H,se){if($=="(")return ke(se,H,"parens");if($==")")return se.context.prev.type=="parens"?Je(se):H.string.match(/^[a-z][\w-]*\(/i)&&Me(H)||ce(Le(H))||/(\.|#|:|\[|\*|&|>|~|\+|\/)/.test(Le(H))||!H.string.match(/^-?[a-z][\w-\.\[\]\'\"]*\s*=/)&&U(Le(H))?ke(se,H,"block"):H.string.match(/^[\$-]?[a-z][\w-\.\[\]\'\"]*\s*=/)||H.string.match(/^\s*(\(|\)|[0-9])/)||H.string.match(/^\s+[a-z][\w-]*\(/i)||H.string.match(/^\s+[\$-]?[a-z]/i)?ke(se,H,"block",0):Me(H)?ke(se,H,"block"):ke(se,H,"block",0);if($&&$.charAt(0)=="@"&&Z(H.current().slice(1))&&(ge="variable-2"),$=="word"){var De=H.current();ge=te(De),ge=="tag"&&re.test(De)&&(ge="variable-2"),(ge=="property"||De=="to")&&(ge="atom")}return $=="variable-name"?ke(se,H,"variableName"):Ue($,H)?ke(se,H,"pseudo"):se.context.type},de.vendorPrefixes=function($,H,se){return $=="word"?(ge="property",ke(se,H,"block",0)):Je(se)},de.pseudo=function($,H,se){return Z(Le(H.string))?Ge($,H,se):(H.match(/^[a-z-]+/),ge="variable-3",Me(H)?ke(se,H,"block"):Je(se))},de.atBlock=function($,H,se){if($=="(")return ke(se,H,"atBlock_parens");if(fe($,H))return ke(se,H,"block");if(oe($,H))return ke(se,H,"interpolation");if($=="word"){var De=H.current().toLowerCase();if(/^(only|not|and|or)$/.test(De)?ge="keyword":j.hasOwnProperty(De)?ge="tag":K.hasOwnProperty(De)?ge="attribute":y.hasOwnProperty(De)?ge="property":I.hasOwnProperty(De)?ge="string-2":ge=te(H.current()),ge=="tag"&&Me(H))return ke(se,H,"block")}return $=="operator"&&/^(not|and|or)$/.test(H.current())&&(ge="keyword"),se.context.type},de.atBlock_parens=function($,H,se){if($=="{"||$=="}")return se.context.type;if($==")")return Me(H)?ke(se,H,"block"):ke(se,H,"atBlock");if($=="word"){var De=H.current().toLowerCase();return ge=te(De),/^(max|min)/.test(De)&&(ge="property"),ge=="tag"&&(re.test(De)?ge="variable-2":ge="atom"),se.context.type}return de.atBlock($,H,se)},de.keyframes=function($,H,se){return H.indentation()=="0"&&($=="}"&&we(H)||$=="]"||$=="hash"||$=="qualifier"||U(H.current()))?Ge($,H,se):$=="{"?ke(se,H,"keyframes"):$=="}"?we(H)?Je(se,!0):ke(se,H,"keyframes"):$=="unit"&&/^[0-9]+\%$/.test(H.current())?ke(se,H,"keyframes"):$=="word"&&(ge=te(H.current()),ge=="block-keyword")?(ge="keyword",ke(se,H,"keyframes")):/@(font-face|media|supports|(-moz-)?document)/.test($)?ke(se,H,Me(H)?"block":"atBlock"):$=="mixin"?ke(se,H,"block",0):se.context.type},de.interpolation=function($,H,se){return $=="{"&&Je(se)&&ke(se,H,"block"),$=="}"?H.string.match(/^\s*(\.|#|:|\[|\*|&|>|~|\+|\/)/i)||H.string.match(/^\s*[a-z]/i)&&U(Le(H))?ke(se,H,"block"):!H.string.match(/^(\{|\s*\&)/)||H.match(/\s*[\w-]/,!1)?ke(se,H,"block",0):ke(se,H,"block"):$=="variable-name"?ke(se,H,"variableName",0):($=="word"&&(ge=te(H.current()),ge=="tag"&&(ge="atom")),se.context.type)},de.extend=function($,H,se){return $=="["||$=="="?"extend":$=="]"?Je(se):$=="word"?(ge=te(H.current()),"extend"):Je(se)},de.variableName=function($,H,se){return $=="string"||$=="["||$=="]"||H.current().match(/^(\.|\$)/)?(H.current().match(/^\.[\w-]+/i)&&(ge="variable-2"),"variableName"):Ge($,H,se)},{startState:function($){return{tokenize:null,state:"block",context:new Ze("block",$||0,null)}},token:function($,H){return!H.tokenize&&$.eatSpace()?null:(pe=(H.tokenize||Oe)($,H),pe&&typeof pe=="object"&&(Ee=pe[1],pe=pe[0]),ge=pe,H.state=de[H.state](Ee,$,H),ge)},indent:function($,H,se){var De=$.context,nt=H&&H.charAt(0),dt=De.indent,Pt=Le(H),Ft=se.match(/^\s*/)[0].replace(/\t/g,G).length,Pe=$.context.prev?$.context.prev.line.firstWord:"",xt=$.context.prev?$.context.prev.line.indent:Ft;return De.prev&&(nt=="}"&&(De.type=="block"||De.type=="atBlock"||De.type=="keyframes")||nt==")"&&(De.type=="parens"||De.type=="atBlock_parens")||nt=="{"&&De.type=="at")?dt=De.indent-O:/(\})/.test(nt)||(/@|\$|\d/.test(nt)||/^\{/.test(H)||/^\s*\/(\/|\*)/.test(H)||/^\s*\/\*/.test(Pe)||/^\s*[\w-\.\[\]\'\"]+\s*(\?|:|\+)?=/i.test(H)||/^(\+|-)?[a-z][\w-]*\(/i.test(H)||/^return/.test(H)||ce(Pt)?dt=Ft:/(\.|#|:|\[|\*|&|>|~|\+|\/)/.test(nt)||U(Pt)?/\,\s*$/.test(Pe)?dt=xt:/^\s+/.test(se)&&(/(\.|#|:|\[|\*|&|>|~|\+|\/)/.test(Pe)||U(Pe))?dt=Ft<=xt?xt:xt+O:dt=Ft:!/,\s*$/.test(se)&&(He(Pt)||Z(Pt))&&(ce(Pe)?dt=Ft<=xt?xt:xt+O:/^\{/.test(Pe)?dt=Ft<=xt?Ft:xt+O:He(Pe)||Z(Pe)?dt=Ft>=xt?xt:Ft:/^(\.|#|:|\[|\*|&|@|\+|\-|>|~|\/)/.test(Pe)||/=\s*$/.test(Pe)||U(Pe)||/^\$[\w-\.\[\]\'\"]/.test(Pe)?dt=xt+O:dt=Ft)),dt},electricChars:"}",blockCommentStart:"/*",blockCommentEnd:"*/",blockCommentContinue:" * ",lineComment:"//",fold:"indent"}});var p=["a","abbr","address","area","article","aside","audio","b","base","bdi","bdo","bgsound","blockquote","body","br","button","canvas","caption","cite","code","col","colgroup","data","datalist","dd","del","details","dfn","div","dl","dt","em","embed","fieldset","figcaption","figure","footer","form","h1","h2","h3","h4","h5","h6","head","header","hgroup","hr","html","i","iframe","img","input","ins","kbd","keygen","label","legend","li","link","main","map","mark","marquee","menu","menuitem","meta","meter","nav","nobr","noframes","noscript","object","ol","optgroup","option","output","p","param","pre","progress","q","rp","rt","ruby","s","samp","script","section","select","small","source","span","strong","style","sub","summary","sup","table","tbody","td","textarea","tfoot","th","thead","time","tr","track","u","ul","var","video"],v=["domain","regexp","url-prefix","url"],C=["all","aural","braille","handheld","print","projection","screen","tty","tv","embossed"],b=["width","min-width","max-width","height","min-height","max-height","device-width","min-device-width","max-device-width","device-height","min-device-height","max-device-height","aspect-ratio","min-aspect-ratio","max-aspect-ratio","device-aspect-ratio","min-device-aspect-ratio","max-device-aspect-ratio","color","min-color","max-color","color-index","min-color-index","max-color-index","monochrome","min-monochrome","max-monochrome","resolution","min-resolution","max-resolution","scan","grid","dynamic-range","video-dynamic-range"],S=["align-content","align-items","align-self","alignment-adjust","alignment-baseline","anchor-point","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","appearance","azimuth","backface-visibility","background","background-attachment","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","baseline-shift","binding","bleed","bookmark-label","bookmark-level","bookmark-state","bookmark-target","border","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","clear","clip","color","color-profile","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","content","counter-increment","counter-reset","crop","cue","cue-after","cue-before","cursor","direction","display","dominant-baseline","drop-initial-after-adjust","drop-initial-after-align","drop-initial-before-adjust","drop-initial-before-align","drop-initial-size","drop-initial-value","elevation","empty-cells","fit","fit-position","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","float-offset","flow-from","flow-into","font","font-feature-settings","font-family","font-kerning","font-language-override","font-size","font-size-adjust","font-stretch","font-style","font-synthesis","font-variant","font-variant-alternates","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-weight","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-position","grid-auto-rows","grid-column","grid-column-end","grid-column-start","grid-row","grid-row-end","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","inline-box-align","justify-content","left","letter-spacing","line-break","line-height","line-stacking","line-stacking-ruby","line-stacking-shift","line-stacking-strategy","list-style","list-style-image","list-style-position","list-style-type","margin","margin-bottom","margin-left","margin-right","margin-top","marker-offset","marks","marquee-direction","marquee-loop","marquee-play-count","marquee-speed","marquee-style","max-height","max-width","min-height","min-width","move-to","nav-down","nav-index","nav-left","nav-right","nav-up","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-style","overflow-wrap","overflow-x","overflow-y","padding","padding-bottom","padding-left","padding-right","padding-top","page","page-break-after","page-break-before","page-break-inside","page-policy","pause","pause-after","pause-before","perspective","perspective-origin","pitch","pitch-range","play-during","position","presentation-level","punctuation-trim","quotes","region-break-after","region-break-before","region-break-inside","region-fragment","rendering-intent","resize","rest","rest-after","rest-before","richness","right","rotation","rotation-point","ruby-align","ruby-overhang","ruby-position","ruby-span","shape-image-threshold","shape-inside","shape-margin","shape-outside","size","speak","speak-as","speak-header","speak-numeral","speak-punctuation","speech-rate","stress","string-set","tab-size","table-layout","target","target-name","target-new","target-position","text-align","text-align-last","text-decoration","text-decoration-color","text-decoration-line","text-decoration-skip","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-height","text-indent","text-justify","text-outline","text-overflow","text-shadow","text-size-adjust","text-space-collapse","text-transform","text-underline-position","text-wrap","top","transform","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","volume","white-space","widows","width","will-change","word-break","word-spacing","word-wrap","z-index","clip-path","clip-rule","mask","enable-background","filter","flood-color","flood-opacity","lighting-color","stop-color","stop-opacity","pointer-events","color-interpolation","color-interpolation-filters","color-rendering","fill","fill-opacity","fill-rule","image-rendering","marker","marker-end","marker-mid","marker-start","shape-rendering","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","text-rendering","baseline-shift","dominant-baseline","glyph-orientation-horizontal","glyph-orientation-vertical","text-anchor","writing-mode","font-smoothing","osx-font-smoothing"],s=["scrollbar-arrow-color","scrollbar-base-color","scrollbar-dark-shadow-color","scrollbar-face-color","scrollbar-highlight-color","scrollbar-shadow-color","scrollbar-3d-light-color","scrollbar-track-color","shape-inside","searchfield-cancel-button","searchfield-decoration","searchfield-results-button","searchfield-results-decoration","zoom"],h=["font-family","src","unicode-range","font-variant","font-feature-settings","font-stretch","font-weight","font-style"],g=["aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","indianred","indigo","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","snow","springgreen","steelblue","tan","teal","thistle","tomato","turquoise","violet","wheat","white","whitesmoke","yellow","yellowgreen"],T=["above","absolute","activeborder","additive","activecaption","afar","after-white-space","ahead","alias","all","all-scroll","alphabetic","alternate","always","amharic","amharic-abegede","antialiased","appworkspace","arabic-indic","armenian","asterisks","attr","auto","avoid","avoid-column","avoid-page","avoid-region","background","backwards","baseline","below","bidi-override","binary","bengali","blink","block","block-axis","bold","bolder","border","border-box","both","bottom","break","break-all","break-word","bullets","button","buttonface","buttonhighlight","buttonshadow","buttontext","calc","cambodian","capitalize","caps-lock-indicator","caption","captiontext","caret","cell","center","checkbox","circle","cjk-decimal","cjk-earthly-branch","cjk-heavenly-stem","cjk-ideographic","clear","clip","close-quote","col-resize","collapse","column","compact","condensed","conic-gradient","contain","content","contents","content-box","context-menu","continuous","copy","counter","counters","cover","crop","cross","crosshair","currentcolor","cursive","cyclic","dashed","decimal","decimal-leading-zero","default","default-button","destination-atop","destination-in","destination-out","destination-over","devanagari","disc","discard","disclosure-closed","disclosure-open","document","dot-dash","dot-dot-dash","dotted","double","down","e-resize","ease","ease-in","ease-in-out","ease-out","element","ellipse","ellipsis","embed","end","ethiopic","ethiopic-abegede","ethiopic-abegede-am-et","ethiopic-abegede-gez","ethiopic-abegede-ti-er","ethiopic-abegede-ti-et","ethiopic-halehame-aa-er","ethiopic-halehame-aa-et","ethiopic-halehame-am-et","ethiopic-halehame-gez","ethiopic-halehame-om-et","ethiopic-halehame-sid-et","ethiopic-halehame-so-et","ethiopic-halehame-ti-er","ethiopic-halehame-ti-et","ethiopic-halehame-tig","ethiopic-numeric","ew-resize","expanded","extends","extra-condensed","extra-expanded","fantasy","fast","fill","fixed","flat","flex","footnotes","forwards","from","geometricPrecision","georgian","graytext","groove","gujarati","gurmukhi","hand","hangul","hangul-consonant","hebrew","help","hidden","hide","high","higher","highlight","highlighttext","hiragana","hiragana-iroha","horizontal","hsl","hsla","icon","ignore","inactiveborder","inactivecaption","inactivecaptiontext","infinite","infobackground","infotext","inherit","initial","inline","inline-axis","inline-block","inline-flex","inline-table","inset","inside","intrinsic","invert","italic","japanese-formal","japanese-informal","justify","kannada","katakana","katakana-iroha","keep-all","khmer","korean-hangul-formal","korean-hanja-formal","korean-hanja-informal","landscape","lao","large","larger","left","level","lighter","line-through","linear","linear-gradient","lines","list-item","listbox","listitem","local","logical","loud","lower","lower-alpha","lower-armenian","lower-greek","lower-hexadecimal","lower-latin","lower-norwegian","lower-roman","lowercase","ltr","malayalam","match","matrix","matrix3d","media-play-button","media-slider","media-sliderthumb","media-volume-slider","media-volume-sliderthumb","medium","menu","menulist","menulist-button","menutext","message-box","middle","min-intrinsic","mix","mongolian","monospace","move","multiple","myanmar","n-resize","narrower","ne-resize","nesw-resize","no-close-quote","no-drop","no-open-quote","no-repeat","none","normal","not-allowed","nowrap","ns-resize","numbers","numeric","nw-resize","nwse-resize","oblique","octal","open-quote","optimizeLegibility","optimizeSpeed","oriya","oromo","outset","outside","outside-shape","overlay","overline","padding","padding-box","painted","page","paused","persian","perspective","plus-darker","plus-lighter","pointer","polygon","portrait","pre","pre-line","pre-wrap","preserve-3d","progress","push-button","radial-gradient","radio","read-only","read-write","read-write-plaintext-only","rectangle","region","relative","repeat","repeating-linear-gradient","repeating-radial-gradient","repeating-conic-gradient","repeat-x","repeat-y","reset","reverse","rgb","rgba","ridge","right","rotate","rotate3d","rotateX","rotateY","rotateZ","round","row-resize","rtl","run-in","running","s-resize","sans-serif","scale","scale3d","scaleX","scaleY","scaleZ","scroll","scrollbar","scroll-position","se-resize","searchfield","searchfield-cancel-button","searchfield-decoration","searchfield-results-button","searchfield-results-decoration","semi-condensed","semi-expanded","separate","serif","show","sidama","simp-chinese-formal","simp-chinese-informal","single","skew","skewX","skewY","skip-white-space","slide","slider-horizontal","slider-vertical","sliderthumb-horizontal","sliderthumb-vertical","slow","small","small-caps","small-caption","smaller","solid","somali","source-atop","source-in","source-out","source-over","space","spell-out","square","square-button","standard","start","static","status-bar","stretch","stroke","sub","subpixel-antialiased","super","sw-resize","symbolic","symbols","table","table-caption","table-cell","table-column","table-column-group","table-footer-group","table-header-group","table-row","table-row-group","tamil","telugu","text","text-bottom","text-top","textarea","textfield","thai","thick","thin","threeddarkshadow","threedface","threedhighlight","threedlightshadow","threedshadow","tibetan","tigre","tigrinya-er","tigrinya-er-abegede","tigrinya-et","tigrinya-et-abegede","to","top","trad-chinese-formal","trad-chinese-informal","translate","translate3d","translateX","translateY","translateZ","transparent","ultra-condensed","ultra-expanded","underline","up","upper-alpha","upper-armenian","upper-greek","upper-hexadecimal","upper-latin","upper-norwegian","upper-roman","uppercase","urdu","url","var","vertical","vertical-text","visible","visibleFill","visiblePainted","visibleStroke","visual","w-resize","wait","wave","wider","window","windowframe","windowtext","words","x-large","x-small","xor","xx-large","xx-small","bicubic","optimizespeed","grayscale","row","row-reverse","wrap","wrap-reverse","column-reverse","flex-start","flex-end","space-between","space-around","unset"],w=["in","and","or","not","is not","is a","is","isnt","defined","if unless"],c=["for","if","else","unless","from","to"],d=["null","true","false","href","title","type","not-allowed","readonly","disabled"],k=["@font-face","@keyframes","@media","@viewport","@page","@host","@supports","@block","@css"],z=p.concat(v,C,b,S,s,g,T,h,w,c,d,k);function M(E){return E=E.sort(function(O,G){return G>O}),new RegExp("^(("+E.join(")|(")+"))\\b")}function _(E){for(var O={},G=0;G{(function(o){typeof Gu=="object"&&typeof Zu=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";function p(q){for(var I={},D=0;D~^?!",h=":;,.(){}[]",g=/^\-?0b[01][01_]*/,T=/^\-?0o[0-7][0-7_]*/,w=/^\-?0x[\dA-Fa-f][\dA-Fa-f_]*(?:(?:\.[\dA-Fa-f][\dA-Fa-f_]*)?[Pp]\-?\d[\d_]*)?/,c=/^\-?\d[\d_]*(?:\.\d[\d_]*)?(?:[Ee]\-?\d[\d_]*)?/,d=/^\$\d+|(`?)[_A-Za-z][_A-Za-z$0-9]*\1/,k=/^\.(?:\$\d+|(`?)[_A-Za-z][_A-Za-z$0-9]*\1)/,z=/^\#[A-Za-z]+/,M=/^@(?:\$\d+|(`?)[_A-Za-z][_A-Za-z$0-9]*\1)/;function _(q,I,D){if(q.sol()&&(I.indented=q.indentation()),q.eatSpace())return null;var Q=q.peek();if(Q=="/"){if(q.match("//"))return q.skipToEnd(),"comment";if(q.match("/*"))return I.tokenize.push(O),O(q,I)}if(q.match(z))return"builtin";if(q.match(M))return"attribute";if(q.match(g)||q.match(T)||q.match(w)||q.match(c))return"number";if(q.match(k))return"property";if(s.indexOf(Q)>-1)return q.next(),"operator";if(h.indexOf(Q)>-1)return q.next(),q.match(".."),"punctuation";var j;if(j=q.match(/("""|"|')/)){var V=E.bind(null,j[0]);return I.tokenize.push(V),V(q,I)}if(q.match(d)){var y=q.current();return S.hasOwnProperty(y)?"variable-2":b.hasOwnProperty(y)?"atom":v.hasOwnProperty(y)?(C.hasOwnProperty(y)&&(I.prev="define"),"keyword"):D=="define"?"def":"variable"}return q.next(),null}function W(){var q=0;return function(I,D,Q){var j=_(I,D,Q);if(j=="punctuation"){if(I.current()=="(")++q;else if(I.current()==")"){if(q==0)return I.backUp(1),D.tokenize.pop(),D.tokenize[D.tokenize.length-1](I,D);--q}}return j}}function E(q,I,D){for(var Q=q.length==1,j,V=!1;j=I.peek();)if(V){if(I.next(),j=="(")return D.tokenize.push(W()),"string";V=!1}else{if(I.match(q))return D.tokenize.pop(),"string";I.next(),V=j=="\\"}return Q&&D.tokenize.pop(),"string"}function O(q,I){for(var D;D=q.next();)if(D==="/"&&q.eat("*"))I.tokenize.push(O);else if(D==="*"&&q.eat("/")){I.tokenize.pop();break}return"comment"}function G(q,I,D){this.prev=q,this.align=I,this.indented=D}function J(q,I){var D=I.match(/^\s*($|\/[\/\*])/,!1)?null:I.column()+1;q.context=new G(q.context,D,q.indented)}function re(q){q.context&&(q.indented=q.context.indented,q.context=q.context.prev)}o.defineMode("swift",function(q){return{startState:function(){return{prev:null,context:null,indented:0,tokenize:[]}},token:function(I,D){var Q=D.prev;D.prev=null;var j=D.tokenize[D.tokenize.length-1]||_,V=j(I,D,Q);if(!V||V=="comment"?D.prev=Q:D.prev||(D.prev=V),V=="punctuation"){var y=/[\(\[\{]|([\]\)\}])/.exec(I.current());y&&(y[1]?re:J)(D,I)}return V},indent:function(I,D){var Q=I.context;if(!Q)return 0;var j=/^[\]\}\)]/.test(D);return Q.align!=null?Q.align-(j?1:0):Q.indented+(j?0:q.indentUnit)},electricInput:/^\s*[\)\}\]]$/,lineComment:"//",blockCommentStart:"/*",blockCommentEnd:"*/",fold:"brace",closeBrackets:"()[]{}''\"\"``"}}),o.defineMIME("text/x-swift","swift")})});var Vu=Ke((Yu,Qu)=>{(function(o){typeof Yu=="object"&&typeof Qu=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";o.defineMode("coffeescript",function(p,v){var C="error";function b(I){return new RegExp("^(("+I.join(")|(")+"))\\b")}var S=/^(?:->|=>|\+[+=]?|-[\-=]?|\*[\*=]?|\/[\/=]?|[=!]=|<[><]?=?|>>?=?|%=?|&=?|\|=?|\^=?|\~|!|\?|(or|and|\|\||&&|\?)=)/,s=/^(?:[()\[\]{},:`=;]|\.\.?\.?)/,h=/^[_A-Za-z$][_A-Za-z$0-9]*/,g=/^@[_A-Za-z$][_A-Za-z$0-9]*/,T=b(["and","or","not","is","isnt","in","instanceof","typeof"]),w=["for","while","loop","if","unless","else","switch","try","catch","finally","class"],c=["break","by","continue","debugger","delete","do","in","of","new","return","then","this","@","throw","when","until","extends"],d=b(w.concat(c));w=b(w);var k=/^('{3}|\"{3}|['\"])/,z=/^(\/{3}|\/)/,M=["Infinity","NaN","undefined","null","true","false","on","off","yes","no"],_=b(M);function W(I,D){if(I.sol()){D.scope.align===null&&(D.scope.align=!1);var Q=D.scope.offset;if(I.eatSpace()){var j=I.indentation();return j>Q&&D.scope.type=="coffee"?"indent":j0&&J(I,D)}if(I.eatSpace())return null;var V=I.peek();if(I.match("####"))return I.skipToEnd(),"comment";if(I.match("###"))return D.tokenize=O,D.tokenize(I,D);if(V==="#")return I.skipToEnd(),"comment";if(I.match(/^-?[0-9\.]/,!1)){var y=!1;if(I.match(/^-?\d*\.\d+(e[\+\-]?\d+)?/i)&&(y=!0),I.match(/^-?\d+\.\d*/)&&(y=!0),I.match(/^-?\.\d+/)&&(y=!0),y)return I.peek()=="."&&I.backUp(1),"number";var K=!1;if(I.match(/^-?0x[0-9a-f]+/i)&&(K=!0),I.match(/^-?[1-9]\d*(e[\+\-]?\d+)?/)&&(K=!0),I.match(/^-?0(?![\dx])/i)&&(K=!0),K)return"number"}if(I.match(k))return D.tokenize=E(I.current(),!1,"string"),D.tokenize(I,D);if(I.match(z)){if(I.current()!="/"||I.match(/^.*\//,!1))return D.tokenize=E(I.current(),!0,"string-2"),D.tokenize(I,D);I.backUp(1)}return I.match(S)||I.match(T)?"operator":I.match(s)?"punctuation":I.match(_)?"atom":I.match(g)||D.prop&&I.match(h)?"property":I.match(d)?"keyword":I.match(h)?"variable":(I.next(),C)}function E(I,D,Q){return function(j,V){for(;!j.eol();)if(j.eatWhile(/[^'"\/\\]/),j.eat("\\")){if(j.next(),D&&j.eol())return Q}else{if(j.match(I))return V.tokenize=W,Q;j.eat(/['"\/]/)}return D&&(v.singleLineStringErrors?Q=C:V.tokenize=W),Q}}function O(I,D){for(;!I.eol();){if(I.eatWhile(/[^#]/),I.match("###")){D.tokenize=W;break}I.eatWhile("#")}return"comment"}function G(I,D,Q){Q=Q||"coffee";for(var j=0,V=!1,y=null,K=D.scope;K;K=K.prev)if(K.type==="coffee"||K.type=="}"){j=K.offset+p.indentUnit;break}Q!=="coffee"?(V=null,y=I.column()+I.current().length):D.scope.align&&(D.scope.align=!1),D.scope={offset:j,type:Q,prev:D.scope,align:V,alignOffset:y}}function J(I,D){if(D.scope.prev)if(D.scope.type==="coffee"){for(var Q=I.indentation(),j=!1,V=D.scope;V;V=V.prev)if(Q===V.offset){j=!0;break}if(!j)return!0;for(;D.scope.prev&&D.scope.offset!==Q;)D.scope=D.scope.prev;return!1}else return D.scope=D.scope.prev,!1}function re(I,D){var Q=D.tokenize(I,D),j=I.current();j==="return"&&(D.dedent=!0),((j==="->"||j==="=>")&&I.eol()||Q==="indent")&&G(I,D);var V="[({".indexOf(j);if(V!==-1&&G(I,D,"])}".slice(V,V+1)),w.exec(j)&&G(I,D),j=="then"&&J(I,D),Q==="dedent"&&J(I,D))return C;if(V="])}".indexOf(j),V!==-1){for(;D.scope.type=="coffee"&&D.scope.prev;)D.scope=D.scope.prev;D.scope.type==j&&(D.scope=D.scope.prev)}return D.dedent&&I.eol()&&(D.scope.type=="coffee"&&D.scope.prev&&(D.scope=D.scope.prev),D.dedent=!1),Q}var q={startState:function(I){return{tokenize:W,scope:{offset:I||0,type:"coffee",prev:null,align:!1},prop:!1,dedent:0}},token:function(I,D){var Q=D.scope.align===null&&D.scope;Q&&I.sol()&&(Q.align=!1);var j=re(I,D);return j&&j!="comment"&&(Q&&(Q.align=!0),D.prop=j=="punctuation"&&I.current()=="."),j},indent:function(I,D){if(I.tokenize!=W)return 0;var Q=I.scope,j=D&&"])}".indexOf(D.charAt(0))>-1;if(j)for(;Q.type=="coffee"&&Q.prev;)Q=Q.prev;var V=j&&Q.type===D.charAt(0);return Q.align?Q.alignOffset-(V?1:0):(V?Q.prev:Q).offset},lineComment:"#",fold:"indent"};return q}),o.defineMIME("application/vnd.coffeescript","coffeescript"),o.defineMIME("text/x-coffeescript","coffeescript"),o.defineMIME("text/coffeescript","coffeescript")})});var tc=Ke((Ju,ec)=>{(function(o){typeof Ju=="object"&&typeof ec=="object"?o(We(),vn(),gn(),Qn()):typeof define=="function"&&define.amd?define(["../../lib/codemirror","../javascript/javascript","../css/css","../htmlmixed/htmlmixed"],o):o(CodeMirror)})(function(o){"use strict";o.defineMode("pug",function(p){var v="keyword",C="meta",b="builtin",S="qualifier",s={"{":"}","(":")","[":"]"},h=o.getMode(p,"javascript");function g(){this.javaScriptLine=!1,this.javaScriptLineExcludesColon=!1,this.javaScriptArguments=!1,this.javaScriptArgumentsDepth=0,this.isInterpolating=!1,this.interpolationNesting=0,this.jsState=o.startState(h),this.restOfLine="",this.isIncludeFiltered=!1,this.isEach=!1,this.lastTag="",this.scriptType="",this.isAttrs=!1,this.attrsNest=[],this.inAttributeName=!0,this.attributeIsType=!1,this.attrValue="",this.indentOf=1/0,this.indentToken="",this.innerMode=null,this.innerState=null,this.innerModeForLine=!1}g.prototype.copy=function(){var U=new g;return U.javaScriptLine=this.javaScriptLine,U.javaScriptLineExcludesColon=this.javaScriptLineExcludesColon,U.javaScriptArguments=this.javaScriptArguments,U.javaScriptArgumentsDepth=this.javaScriptArgumentsDepth,U.isInterpolating=this.isInterpolating,U.interpolationNesting=this.interpolationNesting,U.jsState=o.copyState(h,this.jsState),U.innerMode=this.innerMode,this.innerMode&&this.innerState&&(U.innerState=o.copyState(this.innerMode,this.innerState)),U.restOfLine=this.restOfLine,U.isIncludeFiltered=this.isIncludeFiltered,U.isEach=this.isEach,U.lastTag=this.lastTag,U.scriptType=this.scriptType,U.isAttrs=this.isAttrs,U.attrsNest=this.attrsNest.slice(),U.inAttributeName=this.inAttributeName,U.attributeIsType=this.attributeIsType,U.attrValue=this.attrValue,U.indentOf=this.indentOf,U.indentToken=this.indentToken,U.innerModeForLine=this.innerModeForLine,U};function T(U,Z){if(U.sol()&&(Z.javaScriptLine=!1,Z.javaScriptLineExcludesColon=!1),Z.javaScriptLine){if(Z.javaScriptLineExcludesColon&&U.peek()===":"){Z.javaScriptLine=!1,Z.javaScriptLineExcludesColon=!1;return}var ce=h.token(U,Z.jsState);return U.eol()&&(Z.javaScriptLine=!1),ce||!0}}function w(U,Z){if(Z.javaScriptArguments){if(Z.javaScriptArgumentsDepth===0&&U.peek()!=="("){Z.javaScriptArguments=!1;return}if(U.peek()==="("?Z.javaScriptArgumentsDepth++:U.peek()===")"&&Z.javaScriptArgumentsDepth--,Z.javaScriptArgumentsDepth===0){Z.javaScriptArguments=!1;return}var ce=h.token(U,Z.jsState);return ce||!0}}function c(U){if(U.match(/^yield\b/))return"keyword"}function d(U){if(U.match(/^(?:doctype) *([^\n]+)?/))return C}function k(U,Z){if(U.match("#{"))return Z.isInterpolating=!0,Z.interpolationNesting=0,"punctuation"}function z(U,Z){if(Z.isInterpolating){if(U.peek()==="}"){if(Z.interpolationNesting--,Z.interpolationNesting<0)return U.next(),Z.isInterpolating=!1,"punctuation"}else U.peek()==="{"&&Z.interpolationNesting++;return h.token(U,Z.jsState)||!0}}function M(U,Z){if(U.match(/^case\b/))return Z.javaScriptLine=!0,v}function _(U,Z){if(U.match(/^when\b/))return Z.javaScriptLine=!0,Z.javaScriptLineExcludesColon=!0,v}function W(U){if(U.match(/^default\b/))return v}function E(U,Z){if(U.match(/^extends?\b/))return Z.restOfLine="string",v}function O(U,Z){if(U.match(/^append\b/))return Z.restOfLine="variable",v}function G(U,Z){if(U.match(/^prepend\b/))return Z.restOfLine="variable",v}function J(U,Z){if(U.match(/^block\b *(?:(prepend|append)\b)?/))return Z.restOfLine="variable",v}function re(U,Z){if(U.match(/^include\b/))return Z.restOfLine="string",v}function q(U,Z){if(U.match(/^include:([a-zA-Z0-9\-]+)/,!1)&&U.match("include"))return Z.isIncludeFiltered=!0,v}function I(U,Z){if(Z.isIncludeFiltered){var ce=R(U,Z);return Z.isIncludeFiltered=!1,Z.restOfLine="string",ce}}function D(U,Z){if(U.match(/^mixin\b/))return Z.javaScriptLine=!0,v}function Q(U,Z){if(U.match(/^\+([-\w]+)/))return U.match(/^\( *[-\w]+ *=/,!1)||(Z.javaScriptArguments=!0,Z.javaScriptArgumentsDepth=0),"variable";if(U.match("+#{",!1))return U.next(),Z.mixinCallAfter=!0,k(U,Z)}function j(U,Z){if(Z.mixinCallAfter)return Z.mixinCallAfter=!1,U.match(/^\( *[-\w]+ *=/,!1)||(Z.javaScriptArguments=!0,Z.javaScriptArgumentsDepth=0),!0}function V(U,Z){if(U.match(/^(if|unless|else if|else)\b/))return Z.javaScriptLine=!0,v}function y(U,Z){if(U.match(/^(- *)?(each|for)\b/))return Z.isEach=!0,v}function K(U,Z){if(Z.isEach){if(U.match(/^ in\b/))return Z.javaScriptLine=!0,Z.isEach=!1,v;if(U.sol()||U.eol())Z.isEach=!1;else if(U.next()){for(;!U.match(/^ in\b/,!1)&&U.next(););return"variable"}}}function X(U,Z){if(U.match(/^while\b/))return Z.javaScriptLine=!0,v}function N(U,Z){var ce;if(ce=U.match(/^(\w(?:[-:\w]*\w)?)\/?/))return Z.lastTag=ce[1].toLowerCase(),Z.lastTag==="script"&&(Z.scriptType="application/javascript"),"tag"}function R(U,Z){if(U.match(/^:([\w\-]+)/)){var ce;return p&&p.innerModes&&(ce=p.innerModes(U.current().substring(1))),ce||(ce=U.current().substring(1)),typeof ce=="string"&&(ce=o.getMode(p,ce)),Be(U,Z,ce),"atom"}}function le(U,Z){if(U.match(/^(!?=|-)/))return Z.javaScriptLine=!0,"punctuation"}function xe(U){if(U.match(/^#([\w-]+)/))return b}function F(U){if(U.match(/^\.([\w-]+)/))return S}function L(U,Z){if(U.peek()=="(")return U.next(),Z.isAttrs=!0,Z.attrsNest=[],Z.inAttributeName=!0,Z.attrValue="",Z.attributeIsType=!1,"punctuation"}function de(U,Z){if(Z.isAttrs){if(s[U.peek()]&&Z.attrsNest.push(s[U.peek()]),Z.attrsNest[Z.attrsNest.length-1]===U.peek())Z.attrsNest.pop();else if(U.eat(")"))return Z.isAttrs=!1,"punctuation";if(Z.inAttributeName&&U.match(/^[^=,\)!]+/))return(U.peek()==="="||U.peek()==="!")&&(Z.inAttributeName=!1,Z.jsState=o.startState(h),Z.lastTag==="script"&&U.current().trim().toLowerCase()==="type"?Z.attributeIsType=!0:Z.attributeIsType=!1),"attribute";var ce=h.token(U,Z.jsState);if(Z.attributeIsType&&ce==="string"&&(Z.scriptType=U.current().toString()),Z.attrsNest.length===0&&(ce==="string"||ce==="variable"||ce==="keyword"))try{return Function("","var x "+Z.attrValue.replace(/,\s*$/,"").replace(/^!/,"")),Z.inAttributeName=!0,Z.attrValue="",U.backUp(U.current().length),de(U,Z)}catch{}return Z.attrValue+=U.current(),ce||!0}}function ze(U,Z){if(U.match(/^&attributes\b/))return Z.javaScriptArguments=!0,Z.javaScriptArgumentsDepth=0,"keyword"}function pe(U){if(U.sol()&&U.eatSpace())return"indent"}function Ee(U,Z){if(U.match(/^ *\/\/(-)?([^\n]*)/))return Z.indentOf=U.indentation(),Z.indentToken="comment","comment"}function ge(U){if(U.match(/^: */))return"colon"}function Oe(U,Z){if(U.match(/^(?:\| ?| )([^\n]+)/))return"string";if(U.match(/^(<[^\n]*)/,!1))return Be(U,Z,"htmlmixed"),Z.innerModeForLine=!0,Ze(U,Z,!0)}function qe(U,Z){if(U.eat(".")){var ce=null;return Z.lastTag==="script"&&Z.scriptType.toLowerCase().indexOf("javascript")!=-1?ce=Z.scriptType.toLowerCase().replace(/"|'/g,""):Z.lastTag==="style"&&(ce="css"),Be(U,Z,ce),"dot"}}function Se(U){return U.next(),null}function Be(U,Z,ce){ce=o.mimeModes[ce]||ce,ce=p.innerModes&&p.innerModes(ce)||ce,ce=o.mimeModes[ce]||ce,ce=o.getMode(p,ce),Z.indentOf=U.indentation(),ce&&ce.name!=="null"?Z.innerMode=ce:Z.indentToken="string"}function Ze(U,Z,ce){if(U.indentation()>Z.indentOf||Z.innerModeForLine&&!U.sol()||ce)return Z.innerMode?(Z.innerState||(Z.innerState=Z.innerMode.startState?o.startState(Z.innerMode,U.indentation()):{}),U.hideFirstChars(Z.indentOf+2,function(){return Z.innerMode.token(U,Z.innerState)||!0})):(U.skipToEnd(),Z.indentToken);U.sol()&&(Z.indentOf=1/0,Z.indentToken=null,Z.innerMode=null,Z.innerState=null)}function ke(U,Z){if(U.sol()&&(Z.restOfLine=""),Z.restOfLine){U.skipToEnd();var ce=Z.restOfLine;return Z.restOfLine="",ce}}function Je(){return new g}function Re(U){return U.copy()}function Ge(U,Z){var ce=Ze(U,Z)||ke(U,Z)||z(U,Z)||I(U,Z)||K(U,Z)||de(U,Z)||T(U,Z)||w(U,Z)||j(U,Z)||c(U)||d(U)||k(U,Z)||M(U,Z)||_(U,Z)||W(U)||E(U,Z)||O(U,Z)||G(U,Z)||J(U,Z)||re(U,Z)||q(U,Z)||D(U,Z)||Q(U,Z)||V(U,Z)||y(U,Z)||X(U,Z)||N(U,Z)||R(U,Z)||le(U,Z)||xe(U)||F(U)||L(U,Z)||ze(U,Z)||pe(U)||Oe(U,Z)||Ee(U,Z)||ge(U)||qe(U,Z)||Se(U);return ce===!0?null:ce}return{startState:Je,copyState:Re,token:Ge}},"javascript","css","htmlmixed"),o.defineMIME("text/x-pug","pug"),o.defineMIME("text/x-jade","pug")})});var ic=Ke((rc,nc)=>{(function(o){typeof rc=="object"&&typeof nc=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";o.multiplexingMode=function(p){var v=Array.prototype.slice.call(arguments,1);function C(b,S,s,h){if(typeof S=="string"){var g=b.indexOf(S,s);return h&&g>-1?g+S.length:g}var T=S.exec(s?b.slice(s):b);return T?T.index+s+(h?T[0].length:0):-1}return{startState:function(){return{outer:o.startState(p),innerActive:null,inner:null,startingInner:!1}},copyState:function(b){return{outer:o.copyState(p,b.outer),innerActive:b.innerActive,inner:b.innerActive&&o.copyState(b.innerActive.mode,b.inner),startingInner:b.startingInner}},token:function(b,S){if(S.innerActive){var z=S.innerActive,h=b.string;if(!z.close&&b.sol())return S.innerActive=S.inner=null,this.token(b,S);var w=z.close&&!S.startingInner?C(h,z.close,b.pos,z.parseDelimiters):-1;if(w==b.pos&&!z.parseDelimiters)return b.match(z.close),S.innerActive=S.inner=null,z.delimStyle&&z.delimStyle+" "+z.delimStyle+"-close";w>-1&&(b.string=h.slice(0,w));var M=z.mode.token(b,S.inner);return w>-1?b.string=h:b.pos>b.start&&(S.startingInner=!1),w==b.pos&&z.parseDelimiters&&(S.innerActive=S.inner=null),z.innerStyle&&(M?M=M+" "+z.innerStyle:M=z.innerStyle),M}else{for(var s=1/0,h=b.string,g=0;g{(function(o){typeof oc=="object"&&typeof ac=="object"?o(We(),Di(),ic()):typeof define=="function"&&define.amd?define(["../../lib/codemirror","../../addon/mode/simple","../../addon/mode/multiplex"],o):o(CodeMirror)})(function(o){"use strict";o.defineSimpleMode("handlebars-tags",{start:[{regex:/\{\{\{/,push:"handlebars_raw",token:"tag"},{regex:/\{\{!--/,push:"dash_comment",token:"comment"},{regex:/\{\{!/,push:"comment",token:"comment"},{regex:/\{\{/,push:"handlebars",token:"tag"}],handlebars_raw:[{regex:/\}\}\}/,pop:!0,token:"tag"}],handlebars:[{regex:/\}\}/,pop:!0,token:"tag"},{regex:/"(?:[^\\"]|\\.)*"?/,token:"string"},{regex:/'(?:[^\\']|\\.)*'?/,token:"string"},{regex:/>|[#\/]([A-Za-z_]\w*)/,token:"keyword"},{regex:/(?:else|this)\b/,token:"keyword"},{regex:/\d+/i,token:"number"},{regex:/=|~|@|true|false/,token:"atom"},{regex:/(?:\.\.\/)*(?:[A-Za-z_][\w\.]*)+/,token:"variable-2"}],dash_comment:[{regex:/--\}\}/,pop:!0,token:"comment"},{regex:/./,token:"comment"}],comment:[{regex:/\}\}/,pop:!0,token:"comment"},{regex:/./,token:"comment"}],meta:{blockCommentStart:"{{--",blockCommentEnd:"--}}"}}),o.defineMode("handlebars",function(p,v){var C=o.getMode(p,"handlebars-tags");return!v||!v.base?C:o.multiplexingMode(o.getMode(p,v.base),{open:"{{",close:/\}\}\}?/,mode:C,parseDelimiters:!0})}),o.defineMIME("text/x-handlebars-template","handlebars")})});var cc=Ke((sc,uc)=>{(function(o){"use strict";typeof sc=="object"&&typeof uc=="object"?o(We(),Yn(),mn(),vn(),Vu(),gn(),ea(),ta(),tc(),lc()):typeof define=="function"&&define.amd?define(["../../lib/codemirror","../../addon/mode/overlay","../xml/xml","../javascript/javascript","../coffeescript/coffeescript","../css/css","../sass/sass","../stylus/stylus","../pug/pug","../handlebars/handlebars"],o):o(CodeMirror)})(function(o){var p={script:[["lang",/coffee(script)?/,"coffeescript"],["type",/^(?:text|application)\/(?:x-)?coffee(?:script)?$/,"coffeescript"],["lang",/^babel$/,"javascript"],["type",/^text\/babel$/,"javascript"],["type",/^text\/ecmascript-\d+$/,"javascript"]],style:[["lang",/^stylus$/i,"stylus"],["lang",/^sass$/i,"sass"],["lang",/^less$/i,"text/x-less"],["lang",/^scss$/i,"text/x-scss"],["type",/^(text\/)?(x-)?styl(us)?$/i,"stylus"],["type",/^text\/sass/i,"sass"],["type",/^(text\/)?(x-)?scss$/i,"text/x-scss"],["type",/^(text\/)?(x-)?less$/i,"text/x-less"]],template:[["lang",/^vue-template$/i,"vue"],["lang",/^pug$/i,"pug"],["lang",/^handlebars$/i,"handlebars"],["type",/^(text\/)?(x-)?pug$/i,"pug"],["type",/^text\/x-handlebars-template$/i,"handlebars"],[null,null,"vue-template"]]};o.defineMode("vue-template",function(v,C){var b={token:function(S){if(S.match(/^\{\{.*?\}\}/))return"meta mustache";for(;S.next()&&!S.match("{{",!1););return null}};return o.overlayMode(o.getMode(v,C.backdrop||"text/html"),b)}),o.defineMode("vue",function(v){return o.getMode(v,{name:"htmlmixed",tags:p})},"htmlmixed","xml","javascript","coffeescript","css","sass","stylus","pug","handlebars"),o.defineMIME("script/x-vue","vue"),o.defineMIME("text/x-vue","vue")})});var pc=Ke((fc,dc)=>{(function(o){typeof fc=="object"&&typeof dc=="object"?o(We()):typeof define=="function"&&define.amd?define(["../../lib/codemirror"],o):o(CodeMirror)})(function(o){"use strict";o.defineMode("yaml",function(){var p=["true","false","on","off","yes","no"],v=new RegExp("\\b(("+p.join(")|(")+"))$","i");return{token:function(C,b){var S=C.peek(),s=b.escaped;if(b.escaped=!1,S=="#"&&(C.pos==0||/\s/.test(C.string.charAt(C.pos-1))))return C.skipToEnd(),"comment";if(C.match(/^('([^']|\\.)*'?|"([^"]|\\.)*"?)/))return"string";if(b.literal&&C.indentation()>b.keyCol)return C.skipToEnd(),"string";if(b.literal&&(b.literal=!1),C.sol()){if(b.keyCol=0,b.pair=!1,b.pairStart=!1,C.match("---")||C.match("..."))return"def";if(C.match(/\s*-\s+/))return"meta"}if(C.match(/^(\{|\}|\[|\])/))return S=="{"?b.inlinePairs++:S=="}"?b.inlinePairs--:S=="["?b.inlineList++:b.inlineList--,"meta";if(b.inlineList>0&&!s&&S==",")return C.next(),"meta";if(b.inlinePairs>0&&!s&&S==",")return b.keyCol=0,b.pair=!1,b.pairStart=!1,C.next(),"meta";if(b.pairStart){if(C.match(/^\s*(\||\>)\s*/))return b.literal=!0,"meta";if(C.match(/^\s*(\&|\*)[a-z0-9\._-]+\b/i))return"variable-2";if(b.inlinePairs==0&&C.match(/^\s*-?[0-9\.\,]+\s?$/)||b.inlinePairs>0&&C.match(/^\s*-?[0-9\.\,]+\s?(?=(,|}))/))return"number";if(C.match(v))return"keyword"}return!b.pair&&C.match(/^\s*(?:[,\[\]{}&*!|>'"%@`][^\s'":]|[^\s,\[\]{}#&*!|>'"%@`])[^#:]*(?=:($|\s))/)?(b.pair=!0,b.keyCol=C.indentation(),"atom"):b.pair&&C.match(/^:\s*/)?(b.pairStart=!0,"meta"):(b.pairStart=!1,b.escaped=S=="\\",C.next(),null)},startState:function(){return{pair:!1,pairStart:!1,keyCol:0,inlinePairs:0,inlineList:0,literal:!1,escaped:!1}},lineComment:"#",fold:"indent"}}),o.defineMIME("text/x-yaml","yaml"),o.defineMIME("text/yaml","yaml")})});var $d={};function qd(o){for(var p;(p=Md.exec(o))!==null;){var v=p[0];if(v.indexOf("target=")===-1){var C=v.replace(/>$/,' target="_blank">');o=o.replace(v,C)}}return o}function Fd(o){for(var p=new DOMParser,v=p.parseFromString(o,"text/html"),C=v.getElementsByTagName("li"),b=0;b0){for(var d=document.createElement("i"),k=0;k=0&&(w=s.getLineHandle(d),!v(w));d--);var W=s.getTokenAt({line:d,ch:1}),E=C(W).fencedChars,O,G,J,re;v(s.getLineHandle(h.line))?(O="",G=h.line):v(s.getLineHandle(h.line-1))?(O="",G=h.line-1):(O=E+` +`,G=h.line),v(s.getLineHandle(g.line))?(J="",re=g.line,g.ch===0&&(re+=1)):g.ch!==0&&v(s.getLineHandle(g.line+1))?(J="",re=g.line+1):(J=E+` +`,re=g.line+1),g.ch===0&&(re-=1),s.operation(function(){s.replaceRange(J,{line:re,ch:0},{line:re+(J?0:1),ch:0}),s.replaceRange(O,{line:G,ch:0},{line:G+(O?0:1),ch:0})}),s.setSelection({line:G+(O?1:0),ch:0},{line:re+(O?1:-1),ch:0}),s.focus()}else{var q=h.line;if(v(s.getLineHandle(h.line))&&(b(s,h.line+1)==="fenced"?(d=h.line,q=h.line+1):(k=h.line,q=h.line-1)),d===void 0)for(d=q;d>=0&&(w=s.getLineHandle(d),!v(w));d--);if(k===void 0)for(z=s.lineCount(),k=q;k=0;d--)if(w=s.getLineHandle(d),!w.text.match(/^\s*$/)&&b(s,d,w)!=="indented"){d+=1;break}for(z=s.lineCount(),k=h.line;k\s+/,"unordered-list":C,"ordered-list":C},T=function(z,M){var _={quote:">","unordered-list":v,"ordered-list":"%%i."};return _[z].replace("%%i",M)},w=function(z,M){var _={quote:">","unordered-list":"\\"+v,"ordered-list":"\\d+."},W=new RegExp(_[z]);return M&&W.test(M)},c=function(z,M,_){var W=C.exec(M),E=T(z,d);return W!==null?(w(z,W[2])&&(E=""),M=W[1]+E+W[3]+M.replace(b,"").replace(g[z],"$1")):_==!1&&(M=E+" "+M),M},d=1,k=s.line;k<=h.line;k++)(function(z){var M=o.getLine(z);S[p]?M=M.replace(g[p],"$1"):(p=="unordered-list"&&(M=c("ordered-list",M,!0)),M=c(p,M,!1),d+=1),o.replaceRange(M,{line:z,ch:0},{line:z,ch:99999999999999})})(k);o.focus()}}function xc(o,p,v,C){if(!(!o.codemirror||o.isPreviewActive())){var b=o.codemirror,S=Tr(b),s=S[p];if(!s){jr(b,s,v,C);return}var h=b.getCursor("start"),g=b.getCursor("end"),T=b.getLine(h.line),w=T.slice(0,h.ch),c=T.slice(h.ch);p=="link"?w=w.replace(/(.*)[^!]\[/,"$1"):p=="image"&&(w=w.replace(/(.*)!\[$/,"$1")),c=c.replace(/]\(.*?\)/,""),b.replaceRange(w+c,{line:h.line,ch:0},{line:h.line,ch:99999999999999}),h.ch-=v[0].length,h!==g&&(g.ch-=v[0].length),b.setSelection(h,g),b.focus()}}function sa(o,p,v,C){if(!(!o.codemirror||o.isPreviewActive())){C=typeof C>"u"?v:C;var b=o.codemirror,S=Tr(b),s,h=v,g=C,T=b.getCursor("start"),w=b.getCursor("end");S[p]?(s=b.getLine(T.line),h=s.slice(0,T.ch),g=s.slice(T.ch),p=="bold"?(h=h.replace(/(\*\*|__)(?![\s\S]*(\*\*|__))/,""),g=g.replace(/(\*\*|__)/,"")):p=="italic"?(h=h.replace(/(\*|_)(?![\s\S]*(\*|_))/,""),g=g.replace(/(\*|_)/,"")):p=="strikethrough"&&(h=h.replace(/(\*\*|~~)(?![\s\S]*(\*\*|~~))/,""),g=g.replace(/(\*\*|~~)/,"")),b.replaceRange(h+g,{line:T.line,ch:0},{line:T.line,ch:99999999999999}),p=="bold"||p=="strikethrough"?(T.ch-=2,T!==w&&(w.ch-=2)):p=="italic"&&(T.ch-=1,T!==w&&(w.ch-=1))):(s=b.getSelection(),p=="bold"?(s=s.split("**").join(""),s=s.split("__").join("")):p=="italic"?(s=s.split("*").join(""),s=s.split("_").join("")):p=="strikethrough"&&(s=s.split("~~").join("")),b.replaceSelection(h+s+g),T.ch+=v.length,w.ch=T.ch+s.length),b.setSelection(T,w),b.focus()}}function Pd(o){if(!o.getWrapperElement().lastChild.classList.contains("editor-preview-active"))for(var p=o.getCursor("start"),v=o.getCursor("end"),C,b=p.line;b<=v.line;b++)C=o.getLine(b),C=C.replace(/^[ ]*([# ]+|\*|-|[> ]+|[0-9]+(.|\)))[ ]*/,""),o.replaceRange(C,{line:b,ch:0},{line:b,ch:99999999999999})}function Fi(o,p){if(Math.abs(o)<1024)return""+o+p[0];var v=0;do o/=1024,++v;while(Math.abs(o)>=1024&&v=19968?C+=v[b].length:C+=1;return C}function Te(o){o=o||{},o.parent=this;var p=!0;if(o.autoDownloadFontAwesome===!1&&(p=!1),o.autoDownloadFontAwesome!==!0)for(var v=document.styleSheets,C=0;C-1&&(p=!1);if(p){var b=document.createElement("link");b.rel="stylesheet",b.href="https://maxcdn.bootstrapcdn.com/font-awesome/latest/css/font-awesome.min.css",document.getElementsByTagName("head")[0].appendChild(b)}if(o.element)this.element=o.element;else if(o.element===null){console.log("EasyMDE: Error. No element was found.");return}if(o.toolbar===void 0){o.toolbar=[];for(var S in Pr)Object.prototype.hasOwnProperty.call(Pr,S)&&(S.indexOf("separator-")!=-1&&o.toolbar.push("|"),(Pr[S].default===!0||o.showIcons&&o.showIcons.constructor===Array&&o.showIcons.indexOf(S)!=-1)&&o.toolbar.push(S))}if(Object.prototype.hasOwnProperty.call(o,"previewClass")||(o.previewClass="editor-preview"),Object.prototype.hasOwnProperty.call(o,"status")||(o.status=["autosave","lines","words","cursor"],o.uploadImage&&o.status.unshift("upload-image")),o.previewRender||(o.previewRender=function(h){return this.parent.markdown(h)}),o.parsingConfig=fr({highlightFormatting:!0},o.parsingConfig||{}),o.insertTexts=fr({},Bd,o.insertTexts||{}),o.promptTexts=fr({},jd,o.promptTexts||{}),o.blockStyles=fr({},Hd,o.blockStyles||{}),o.autosave!=null&&(o.autosave.timeFormat=fr({},Rd,o.autosave.timeFormat||{})),o.iconClassMap=fr({},et,o.iconClassMap||{}),o.shortcuts=fr({},Ad,o.shortcuts||{}),o.maxHeight=o.maxHeight||void 0,o.direction=o.direction||"ltr",typeof o.maxHeight<"u"?o.minHeight=o.maxHeight:o.minHeight=o.minHeight||"300px",o.errorCallback=o.errorCallback||function(h){alert(h)},o.uploadImage=o.uploadImage||!1,o.imageMaxSize=o.imageMaxSize||2097152,o.imageAccept=o.imageAccept||"image/png, image/jpeg, image/gif, image/avif",o.imageTexts=fr({},Wd,o.imageTexts||{}),o.errorMessages=fr({},Ud,o.errorMessages||{}),o.imagePathAbsolute=o.imagePathAbsolute||!1,o.imageCSRFName=o.imageCSRFName||"csrfmiddlewaretoken",o.imageCSRFHeader=o.imageCSRFHeader||!1,o.autosave!=null&&o.autosave.unique_id!=null&&o.autosave.unique_id!=""&&(o.autosave.uniqueId=o.autosave.unique_id),o.overlayMode&&o.overlayMode.combine===void 0&&(o.overlayMode.combine=!0),this.options=o,this.render(),o.initialValue&&(!this.options.autosave||this.options.autosave.foundSavedValue!==!0)&&this.value(o.initialValue),o.uploadImage){var s=this;this.codemirror.on("dragenter",function(h,g){s.updateStatusBar("upload-image",s.options.imageTexts.sbOnDragEnter),g.stopPropagation(),g.preventDefault()}),this.codemirror.on("dragend",function(h,g){s.updateStatusBar("upload-image",s.options.imageTexts.sbInit),g.stopPropagation(),g.preventDefault()}),this.codemirror.on("dragleave",function(h,g){s.updateStatusBar("upload-image",s.options.imageTexts.sbInit),g.stopPropagation(),g.preventDefault()}),this.codemirror.on("dragover",function(h,g){s.updateStatusBar("upload-image",s.options.imageTexts.sbOnDragEnter),g.stopPropagation(),g.preventDefault()}),this.codemirror.on("drop",function(h,g){g.stopPropagation(),g.preventDefault(),o.imageUploadFunction?s.uploadImagesUsingCustomFunction(o.imageUploadFunction,g.dataTransfer.files):s.uploadImages(g.dataTransfer.files)}),this.codemirror.on("paste",function(h,g){o.imageUploadFunction?s.uploadImagesUsingCustomFunction(o.imageUploadFunction,g.clipboardData.files):s.uploadImages(g.clipboardData.files)})}}function kc(){if(typeof localStorage=="object")try{localStorage.setItem("smde_localStorage",1),localStorage.removeItem("smde_localStorage")}catch{return!1}else return!1;return!0}var mc,Md,Vn,Ad,Dd,ra,hc,et,Pr,Bd,jd,Rd,Hd,Wd,Ud,wc=Cd(()=>{mc=/Mac/.test(navigator.platform),Md=new RegExp(/()+?/g),Vn={toggleBold:Ii,toggleItalic:Ni,drawLink:Gi,toggleHeadingSmaller:Jn,toggleHeadingBigger:ji,drawImage:Zi,toggleBlockquote:Bi,toggleOrderedList:$i,toggleUnorderedList:Ui,toggleCodeBlock:Pi,togglePreview:Ji,toggleStrikethrough:Oi,toggleHeading1:Ri,toggleHeading2:Hi,toggleHeading3:Wi,toggleHeading4:na,toggleHeading5:ia,toggleHeading6:oa,cleanBlock:Ki,drawTable:Xi,drawHorizontalRule:Yi,undo:Qi,redo:Vi,toggleSideBySide:bn,toggleFullScreen:Br},Ad={toggleBold:"Cmd-B",toggleItalic:"Cmd-I",drawLink:"Cmd-K",toggleHeadingSmaller:"Cmd-H",toggleHeadingBigger:"Shift-Cmd-H",toggleHeading1:"Ctrl+Alt+1",toggleHeading2:"Ctrl+Alt+2",toggleHeading3:"Ctrl+Alt+3",toggleHeading4:"Ctrl+Alt+4",toggleHeading5:"Ctrl+Alt+5",toggleHeading6:"Ctrl+Alt+6",cleanBlock:"Cmd-E",drawImage:"Cmd-Alt-I",toggleBlockquote:"Cmd-'",toggleOrderedList:"Cmd-Alt-L",toggleUnorderedList:"Cmd-L",toggleCodeBlock:"Cmd-Alt-C",togglePreview:"Cmd-P",toggleSideBySide:"F9",toggleFullScreen:"F11"},Dd=function(o){for(var p in Vn)if(Vn[p]===o)return p;return null},ra=function(){var o=!1;return(function(p){(/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino|android|ipad|playbook|silk/i.test(p)||/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test(p.substr(0,4)))&&(o=!0)})(navigator.userAgent||navigator.vendor||window.opera),o};hc="";et={bold:"fa fa-bold",italic:"fa fa-italic",strikethrough:"fa fa-strikethrough",heading:"fa fa-header fa-heading","heading-smaller":"fa fa-header fa-heading header-smaller","heading-bigger":"fa fa-header fa-heading header-bigger","heading-1":"fa fa-header fa-heading header-1","heading-2":"fa fa-header fa-heading header-2","heading-3":"fa fa-header fa-heading header-3",code:"fa fa-code",quote:"fa fa-quote-left","ordered-list":"fa fa-list-ol","unordered-list":"fa fa-list-ul","clean-block":"fa fa-eraser",link:"fa fa-link",image:"fa fa-image","upload-image":"fa fa-image",table:"fa fa-table","horizontal-rule":"fa fa-minus",preview:"fa fa-eye","side-by-side":"fa fa-columns",fullscreen:"fa fa-arrows-alt",guide:"fa fa-question-circle",undo:"fa fa-undo",redo:"fa fa-repeat fa-redo"},Pr={bold:{name:"bold",action:Ii,className:et.bold,title:"Bold",default:!0},italic:{name:"italic",action:Ni,className:et.italic,title:"Italic",default:!0},strikethrough:{name:"strikethrough",action:Oi,className:et.strikethrough,title:"Strikethrough"},heading:{name:"heading",action:Jn,className:et.heading,title:"Heading",default:!0},"heading-smaller":{name:"heading-smaller",action:Jn,className:et["heading-smaller"],title:"Smaller Heading"},"heading-bigger":{name:"heading-bigger",action:ji,className:et["heading-bigger"],title:"Bigger Heading"},"heading-1":{name:"heading-1",action:Ri,className:et["heading-1"],title:"Big Heading"},"heading-2":{name:"heading-2",action:Hi,className:et["heading-2"],title:"Medium Heading"},"heading-3":{name:"heading-3",action:Wi,className:et["heading-3"],title:"Small Heading"},"separator-1":{name:"separator-1"},code:{name:"code",action:Pi,className:et.code,title:"Code"},quote:{name:"quote",action:Bi,className:et.quote,title:"Quote",default:!0},"unordered-list":{name:"unordered-list",action:Ui,className:et["unordered-list"],title:"Generic List",default:!0},"ordered-list":{name:"ordered-list",action:$i,className:et["ordered-list"],title:"Numbered List",default:!0},"clean-block":{name:"clean-block",action:Ki,className:et["clean-block"],title:"Clean block"},"separator-2":{name:"separator-2"},link:{name:"link",action:Gi,className:et.link,title:"Create Link",default:!0},image:{name:"image",action:Zi,className:et.image,title:"Insert Image",default:!0},"upload-image":{name:"upload-image",action:aa,className:et["upload-image"],title:"Import an image"},table:{name:"table",action:Xi,className:et.table,title:"Insert Table"},"horizontal-rule":{name:"horizontal-rule",action:Yi,className:et["horizontal-rule"],title:"Insert Horizontal Line"},"separator-3":{name:"separator-3"},preview:{name:"preview",action:Ji,className:et.preview,noDisable:!0,title:"Toggle Preview",default:!0},"side-by-side":{name:"side-by-side",action:bn,className:et["side-by-side"],noDisable:!0,noMobile:!0,title:"Toggle Side by Side",default:!0},fullscreen:{name:"fullscreen",action:Br,className:et.fullscreen,noDisable:!0,noMobile:!0,title:"Toggle Fullscreen",default:!0},"separator-4":{name:"separator-4"},guide:{name:"guide",action:"https://www.markdownguide.org/basic-syntax/",className:et.guide,noDisable:!0,title:"Markdown Guide",default:!0},"separator-5":{name:"separator-5"},undo:{name:"undo",action:Qi,className:et.undo,noDisable:!0,title:"Undo"},redo:{name:"redo",action:Vi,className:et.redo,noDisable:!0,title:"Redo"}},Bd={link:["[","](#url#)"],image:["![","](#url#)"],uploadedImage:["![](#url#)",""],table:["",` + +| Column 1 | Column 2 | Column 3 | +| -------- | -------- | -------- | +| Text | Text | Text | + +`],horizontalRule:["",` + +----- + +`]},jd={link:"URL for the link:",image:"URL of the image:"},Rd={locale:"en-US",format:{hour:"2-digit",minute:"2-digit"}},Hd={bold:"**",code:"```",italic:"*"},Wd={sbInit:"Attach files by drag and dropping or pasting from clipboard.",sbOnDragEnter:"Drop image to upload it.",sbOnDrop:"Uploading image #images_names#...",sbProgress:"Uploading #file_name#: #progress#%",sbOnUploaded:"Uploaded #image_name#",sizeUnits:" B, KB, MB"},Ud={noFileGiven:"You must select a file.",typeNotAllowed:"This image type is not allowed.",fileTooLarge:`Image #image_name# is too big (#image_size#). +Maximum file size is #image_max_size#.`,importError:"Something went wrong when uploading the image #image_name#."};Te.prototype.uploadImages=function(o,p,v){if(o.length!==0){for(var C=[],b=0;b!(q.closest&&q.closest(".editor-toolbar")||q.offsetParent===null)),re=J.indexOf(O);re!==-1&&re+1!(q.closest&&q.closest(".editor-toolbar")||q.offsetParent===null)),re=J.indexOf(O);if(re!==-1)for(let q=re-1;q>=0;q--){let I=J[q];if(I){I.focus();break}}}}for(var s in p.shortcuts)p.shortcuts[s]!==null&&Vn[s]!==null&&(function(E){C[vc(p.shortcuts[E])]=function(){var O=Vn[E];typeof O=="function"?O(v):typeof O=="string"&&window.open(O,"_blank")}})(s);C.Enter="newlineAndIndentContinueMarkdownList",C.Tab=E=>{let O=E.getSelection();O&&O.length>0?E.execCommand("indentMore"):b(E)},C["Shift-Tab"]=E=>{let O=E.getSelection();O&&O.length>0?E.execCommand("indentLess"):S(E)},C.Esc=function(E){E.getOption("fullScreen")&&Br(v)},this.documentOnKeyDown=function(E){E=E||window.event,E.keyCode==27&&v.codemirror.getOption("fullScreen")&&Br(v)},document.addEventListener("keydown",this.documentOnKeyDown,!1);var h,g;p.overlayMode?(CodeMirror.defineMode("overlay-mode",function(E){return CodeMirror.overlayMode(CodeMirror.getMode(E,p.spellChecker!==!1?"spell-checker":"gfm"),p.overlayMode.mode,p.overlayMode.combine)}),h="overlay-mode",g=p.parsingConfig,g.gitHubSpice=!1):(h=p.parsingConfig,h.name="gfm",h.gitHubSpice=!1),p.spellChecker!==!1&&(h="spell-checker",g=p.parsingConfig,g.name="gfm",g.gitHubSpice=!1,typeof p.spellChecker=="function"?p.spellChecker({codeMirrorInstance:CodeMirror}):CodeMirrorSpellChecker({codeMirrorInstance:CodeMirror}));function T(E,O,G){return{addNew:!1}}if(CodeMirror.getMode("php").mime="text/x-php",this.codemirror=CodeMirror.fromTextArea(o,{mode:h,backdrop:g,theme:p.theme!=null?p.theme:"easymde",tabSize:p.tabSize!=null?p.tabSize:2,indentUnit:p.tabSize!=null?p.tabSize:2,indentWithTabs:p.indentWithTabs!==!1,lineNumbers:p.lineNumbers===!0,autofocus:p.autofocus===!0,extraKeys:C,direction:p.direction,lineWrapping:p.lineWrapping!==!1,allowDropFileTypes:["text/plain"],placeholder:p.placeholder||o.getAttribute("placeholder")||"",styleSelectedText:p.styleSelectedText!=null?p.styleSelectedText:!ra(),scrollbarStyle:p.scrollbarStyle!=null?p.scrollbarStyle:"native",configureMouse:T,inputStyle:p.inputStyle!=null?p.inputStyle:ra()?"contenteditable":"textarea",spellcheck:p.nativeSpellcheck!=null?p.nativeSpellcheck:!0,autoRefresh:p.autoRefresh!=null?p.autoRefresh:!1}),this.codemirror.getScrollerElement().style.minHeight=p.minHeight,typeof p.maxHeight<"u"&&(this.codemirror.getScrollerElement().style.height=p.maxHeight),p.forceSync===!0){var w=this.codemirror;w.on("change",function(){w.save()})}this.gui={};var c=document.createElement("div");c.classList.add("EasyMDEContainer"),c.setAttribute("role","application");var d=this.codemirror.getWrapperElement();d.parentNode.insertBefore(c,d),c.appendChild(d),p.toolbar!==!1&&(this.gui.toolbar=this.createToolbar()),p.status!==!1&&(this.gui.statusbar=this.createStatusbar()),p.autosave!=null&&p.autosave.enabled===!0&&(this.autosave(),this.codemirror.on("change",function(){clearTimeout(v._autosave_timeout),v._autosave_timeout=setTimeout(function(){v.autosave()},v.options.autosave.submit_delay||v.options.autosave.delay||1e3)}));function k(E,O){var G,J=window.getComputedStyle(document.querySelector(".CodeMirror-sizer")).width.replace("px","");return E=2){var J=G[1];if(p.imagesPreviewHandler){var re=p.imagesPreviewHandler(G[1]);typeof re=="string"&&(J=re)}if(window.EMDEimagesCache[J])M(O,window.EMDEimagesCache[J]);else{var q=document.createElement("img");q.onload=function(){window.EMDEimagesCache[J]={naturalWidth:q.naturalWidth,naturalHeight:q.naturalHeight,url:J},M(O,window.EMDEimagesCache[J])},q.src=J}}}})}this.codemirror.on("update",function(){_()}),this.gui.sideBySide=this.createSideBySide(),this._rendered=this.element,(p.autofocus===!0||o.autofocus)&&this.codemirror.focus();var W=this.codemirror;setTimeout(function(){W.refresh()}.bind(W),0)};Te.prototype.cleanup=function(){document.removeEventListener("keydown",this.documentOnKeyDown)};Te.prototype.autosave=function(){if(kc()){var o=this;if(this.options.autosave.uniqueId==null||this.options.autosave.uniqueId==""){console.log("EasyMDE: You must set a uniqueId to use the autosave feature");return}this.options.autosave.binded!==!0&&(o.element.form!=null&&o.element.form!=null&&o.element.form.addEventListener("submit",function(){clearTimeout(o.autosaveTimeoutId),o.autosaveTimeoutId=void 0,localStorage.removeItem("smde_"+o.options.autosave.uniqueId)}),this.options.autosave.binded=!0),this.options.autosave.loaded!==!0&&(typeof localStorage.getItem("smde_"+this.options.autosave.uniqueId)=="string"&&localStorage.getItem("smde_"+this.options.autosave.uniqueId)!=""&&(this.codemirror.setValue(localStorage.getItem("smde_"+this.options.autosave.uniqueId)),this.options.autosave.foundSavedValue=!0),this.options.autosave.loaded=!0);var p=o.value();p!==""?localStorage.setItem("smde_"+this.options.autosave.uniqueId,p):localStorage.removeItem("smde_"+this.options.autosave.uniqueId);var v=document.getElementById("autosaved");if(v!=null&&v!=null&&v!=""){var C=new Date,b=new Intl.DateTimeFormat([this.options.autosave.timeFormat.locale,"en-US"],this.options.autosave.timeFormat.format).format(C),S=this.options.autosave.text==null?"Autosaved: ":this.options.autosave.text;v.innerHTML=S+b}}else console.log("EasyMDE: localStorage not available, cannot autosave")};Te.prototype.clearAutosavedValue=function(){if(kc()){if(this.options.autosave==null||this.options.autosave.uniqueId==null||this.options.autosave.uniqueId==""){console.log("EasyMDE: You must set a uniqueId to clear the autosave value");return}localStorage.removeItem("smde_"+this.options.autosave.uniqueId)}else console.log("EasyMDE: localStorage not available, cannot autosave")};Te.prototype.openBrowseFileWindow=function(o,p){var v=this,C=this.gui.toolbar.getElementsByClassName("imageInput")[0];C.click();function b(S){v.options.imageUploadFunction?v.uploadImagesUsingCustomFunction(v.options.imageUploadFunction,S.target.files):v.uploadImages(S.target.files,o,p),C.removeEventListener("change",b)}C.addEventListener("change",b)};Te.prototype.uploadImage=function(o,p,v){var C=this;p=p||function(T){yc(C,T)};function b(g){C.updateStatusBar("upload-image",g),setTimeout(function(){C.updateStatusBar("upload-image",C.options.imageTexts.sbInit)},1e4),v&&typeof v=="function"&&v(g),C.options.errorCallback(g)}function S(g){var T=C.options.imageTexts.sizeUnits.split(",");return g.replace("#image_name#",o.name).replace("#image_size#",Fi(o.size,T)).replace("#image_max_size#",Fi(C.options.imageMaxSize,T))}if(o.size>this.options.imageMaxSize){b(S(this.options.errorMessages.fileTooLarge));return}var s=new FormData;s.append("image",o),C.options.imageCSRFToken&&!C.options.imageCSRFHeader&&s.append(C.options.imageCSRFName,C.options.imageCSRFToken);var h=new XMLHttpRequest;h.upload.onprogress=function(g){if(g.lengthComputable){var T=""+Math.round(g.loaded*100/g.total);C.updateStatusBar("upload-image",C.options.imageTexts.sbProgress.replace("#file_name#",o.name).replace("#progress#",T))}},h.open("POST",this.options.imageUploadEndpoint),C.options.imageCSRFToken&&C.options.imageCSRFHeader&&h.setRequestHeader(C.options.imageCSRFName,C.options.imageCSRFToken),h.onload=function(){try{var g=JSON.parse(this.responseText)}catch{console.error("EasyMDE: The server did not return a valid json."),b(S(C.options.errorMessages.importError));return}this.status===200&&g&&!g.error&&g.data&&g.data.filePath?p((C.options.imagePathAbsolute?"":window.location.origin+"/")+g.data.filePath):g.error&&g.error in C.options.errorMessages?b(S(C.options.errorMessages[g.error])):g.error?b(S(g.error)):(console.error("EasyMDE: Received an unexpected response after uploading the image."+this.status+" ("+this.statusText+")"),b(S(C.options.errorMessages.importError)))},h.onerror=function(g){console.error("EasyMDE: An unexpected error occurred when trying to upload the image."+g.target.status+" ("+g.target.statusText+")"),b(C.options.errorMessages.importError)},h.send(s)};Te.prototype.uploadImageUsingCustomFunction=function(o,p){var v=this;function C(s){yc(v,s)}function b(s){var h=S(s);v.updateStatusBar("upload-image",h),setTimeout(function(){v.updateStatusBar("upload-image",v.options.imageTexts.sbInit)},1e4),v.options.errorCallback(h)}function S(s){var h=v.options.imageTexts.sizeUnits.split(",");return s.replace("#image_name#",p.name).replace("#image_size#",Fi(p.size,h)).replace("#image_max_size#",Fi(v.options.imageMaxSize,h))}o.apply(this,[p,C,b])};Te.prototype.setPreviewMaxHeight=function(){var o=this.codemirror,p=o.getWrapperElement(),v=p.nextSibling,C=parseInt(window.getComputedStyle(p).paddingTop),b=parseInt(window.getComputedStyle(p).borderTopWidth),S=parseInt(this.options.maxHeight),s=S+C*2+b*2,h=s.toString()+"px";v.style.height=h};Te.prototype.createSideBySide=function(){var o=this.codemirror,p=o.getWrapperElement(),v=p.nextSibling;if(!v||!v.classList.contains("editor-preview-side")){if(v=document.createElement("div"),v.className="editor-preview-side",this.options.previewClass)if(Array.isArray(this.options.previewClass))for(var C=0;CsetTimeout(d,300)),this.$root._editor&&(this.$root._editor.toTextArea(),this.$root._editor=null),this.$root._editor=this.editor=new EasyMDE({autoDownloadFontAwesome:!1,autoRefresh:!0,autoSave:!1,element:this.$refs.editor,imageAccept:"image/png, image/jpeg, image/gif, image/avif, image/webp",imageUploadFunction:c,initialValue:this.state??"",maxHeight:b,minHeight:S,placeholder:s,previewImagesInEditor:!0,spellChecker:!1,status:[{className:"upload-image",defaultValue:""}],toolbar:this.getToolbar(),uploadImage:o}),this.editor.codemirror.setOption("direction",document.documentElement?.dir??"ltr"),this.editor.codemirror.on("changes",(d,k)=>{try{let z=k[k.length-1];if(z.origin==="+input"){let M="(https://)",_=z.text[z.text.length-1];if(_.endsWith(M)&&_!=="[]"+M){let W=z.from,E=z.to,G=z.text.length>1?0:W.ch;setTimeout(()=>{d.setSelection({line:E.line,ch:G+_.lastIndexOf("(")+1},{line:E.line,ch:G+_.lastIndexOf(")")})},25)}}}catch{}}),this.editor.codemirror.on("change",Alpine.debounce(()=>{this.editor&&(this.state=this.editor.value(),p&&this.$wire.commit())},C??300)),v&&this.editor.codemirror.on("blur",()=>this.$wire.commit()),this.$watch("state",()=>{this.editor&&(this.editor.codemirror.hasFocus()||Alpine.raw(this.editor).value(this.state??""))}),h&&h(this)},destroy(){this.editor.cleanup(),this.editor=null},getToolbar(){let d=[];return w.forEach(k=>{k.forEach(z=>d.push(this.getToolbarButton(z))),k.length>0&&d.push("|")}),d[d.length-1]==="|"&&d.pop(),d},getToolbarButton(d){if(d==="bold")return this.getBoldToolbarButton();if(d==="italic")return this.getItalicToolbarButton();if(d==="strike")return this.getStrikeToolbarButton();if(d==="link")return this.getLinkToolbarButton();if(d==="heading")return this.getHeadingToolbarButton();if(d==="blockquote")return this.getBlockquoteToolbarButton();if(d==="codeBlock")return this.getCodeBlockToolbarButton();if(d==="bulletList")return this.getBulletListToolbarButton();if(d==="orderedList")return this.getOrderedListToolbarButton();if(d==="table")return this.getTableToolbarButton();if(d==="attachFiles")return this.getAttachFilesToolbarButton();if(d==="undo")return this.getUndoToolbarButton();if(d==="redo")return this.getRedoToolbarButton();console.error(`Markdown editor toolbar button "${d}" not found.`)},getBoldToolbarButton(){return{name:"bold",action:EasyMDE.toggleBold,title:T.tools?.bold}},getItalicToolbarButton(){return{name:"italic",action:EasyMDE.toggleItalic,title:T.tools?.italic}},getStrikeToolbarButton(){return{name:"strikethrough",action:EasyMDE.toggleStrikethrough,title:T.tools?.strike}},getLinkToolbarButton(){return{name:"link",action:EasyMDE.drawLink,title:T.tools?.link}},getHeadingToolbarButton(){return{name:"heading",action:EasyMDE.toggleHeadingSmaller,title:T.tools?.heading}},getBlockquoteToolbarButton(){return{name:"quote",action:EasyMDE.toggleBlockquote,title:T.tools?.blockquote}},getCodeBlockToolbarButton(){return{name:"code",action:EasyMDE.toggleCodeBlock,title:T.tools?.code_block}},getBulletListToolbarButton(){return{name:"unordered-list",action:EasyMDE.toggleUnorderedList,title:T.tools?.bullet_list}},getOrderedListToolbarButton(){return{name:"ordered-list",action:EasyMDE.toggleOrderedList,title:T.tools?.ordered_list}},getTableToolbarButton(){return{name:"table",action:EasyMDE.drawTable,title:T.tools?.table}},getAttachFilesToolbarButton(){return{name:"upload-image",action:EasyMDE.drawUploadedImage,title:T.tools?.attach_files}},getUndoToolbarButton(){return{name:"undo",action:EasyMDE.undo,title:T.tools?.undo}},getRedoToolbarButton(){return{name:"redo",action:EasyMDE.redo,title:T.tools?.redo}}}}export{Kd as default}; diff --git a/public/js/filament/forms/components/rich-editor.js b/public/js/filament/forms/components/rich-editor.js new file mode 100644 index 00000000..b882f8fc --- /dev/null +++ b/public/js/filament/forms/components/rich-editor.js @@ -0,0 +1,144 @@ +function ge(t){this.content=t}ge.prototype={constructor:ge,find:function(t){for(var e=0;e>1}};ge.from=function(t){if(t instanceof ge)return t;var e=[];if(t)for(var n in t)e.push(n,t[n]);return new ge(e)};var vi=ge;function ba(t,e,n){for(let r=0;;r++){if(r==t.childCount||r==e.childCount)return t.childCount==e.childCount?null:n;let o=t.child(r),i=e.child(r);if(o==i){n+=o.nodeSize;continue}if(!o.sameMarkup(i))return n;if(o.isText&&o.text!=i.text){for(let s=0;o.text[s]==i.text[s];s++)n++;return n}if(o.content.size||i.content.size){let s=ba(o.content,i.content,n+1);if(s!=null)return s}n+=o.nodeSize}}function wa(t,e,n,r){for(let o=t.childCount,i=e.childCount;;){if(o==0||i==0)return o==i?null:{a:n,b:r};let s=t.child(--o),l=e.child(--i),a=s.nodeSize;if(s==l){n-=a,r-=a;continue}if(!s.sameMarkup(l))return{a:n,b:r};if(s.isText&&s.text!=l.text){let c=0,d=Math.min(s.text.length,l.text.length);for(;ce&&r(a,o+l,i||null,s)!==!1&&a.content.size){let d=l+1;a.nodesBetween(Math.max(0,e-d),Math.min(a.content.size,n-d),r,o+d)}l=c}}descendants(e){this.nodesBetween(0,this.size,e)}textBetween(e,n,r,o){let i="",s=!0;return this.nodesBetween(e,n,(l,a)=>{let c=l.isText?l.text.slice(Math.max(e,a)-a,n-a):l.isLeaf?o?typeof o=="function"?o(l):o:l.type.spec.leafText?l.type.spec.leafText(l):"":"";l.isBlock&&(l.isLeaf&&c||l.isTextblock)&&r&&(s?s=!1:i+=r),i+=c},0),i}append(e){if(!e.size)return this;if(!this.size)return e;let n=this.lastChild,r=e.firstChild,o=this.content.slice(),i=0;for(n.isText&&n.sameMarkup(r)&&(o[o.length-1]=n.withText(n.text+r.text),i=1);ie)for(let i=0,s=0;se&&((sn)&&(l.isText?l=l.cut(Math.max(0,e-s),Math.min(l.text.length,n-s)):l=l.cut(Math.max(0,e-s-1),Math.min(l.content.size,n-s-1))),r.push(l),o+=l.nodeSize),s=a}return new t(r,o)}cutByIndex(e,n){return e==n?t.empty:e==0&&n==this.content.length?this:new t(this.content.slice(e,n))}replaceChild(e,n){let r=this.content[e];if(r==n)return this;let o=this.content.slice(),i=this.size+n.nodeSize-r.nodeSize;return o[e]=n,new t(o,i)}addToStart(e){return new t([e].concat(this.content),this.size+e.nodeSize)}addToEnd(e){return new t(this.content.concat(e),this.size+e.nodeSize)}eq(e){if(this.content.length!=e.content.length)return!1;for(let n=0;nthis.size||e<0)throw new RangeError(`Position ${e} outside of fragment (${this})`);for(let n=0,r=0;;n++){let o=this.child(n),i=r+o.nodeSize;if(i>=e)return i==e?Ar(n+1,i):Ar(n,r);r=i}}toString(){return"<"+this.toStringInner()+">"}toStringInner(){return this.content.join(", ")}toJSON(){return this.content.length?this.content.map(e=>e.toJSON()):null}static fromJSON(e,n){if(!n)return t.empty;if(!Array.isArray(n))throw new RangeError("Invalid input for Fragment.fromJSON");return new t(n.map(e.nodeFromJSON))}static fromArray(e){if(!e.length)return t.empty;let n,r=0;for(let o=0;othis.type.rank&&(n||(n=e.slice(0,o)),n.push(this),r=!0),n&&n.push(i)}}return n||(n=e.slice()),r||n.push(this),n}removeFromSet(e){for(let n=0;nr.type.rank-o.type.rank),n}};J.none=[];var Ft=class extends Error{},E=class t{constructor(e,n,r){this.content=e,this.openStart=n,this.openEnd=r}get size(){return this.content.size-this.openStart-this.openEnd}insertAt(e,n){let r=ka(this.content,e+this.openStart,n);return r&&new t(r,this.openStart,this.openEnd)}removeBetween(e,n){return new t(xa(this.content,e+this.openStart,n+this.openStart),this.openStart,this.openEnd)}eq(e){return this.content.eq(e.content)&&this.openStart==e.openStart&&this.openEnd==e.openEnd}toString(){return this.content+"("+this.openStart+","+this.openEnd+")"}toJSON(){if(!this.content.size)return null;let e={content:this.content.toJSON()};return this.openStart>0&&(e.openStart=this.openStart),this.openEnd>0&&(e.openEnd=this.openEnd),e}static fromJSON(e,n){if(!n)return t.empty;let r=n.openStart||0,o=n.openEnd||0;if(typeof r!="number"||typeof o!="number")throw new RangeError("Invalid input for Slice.fromJSON");return new t(v.fromJSON(e,n.content),r,o)}static maxOpen(e,n=!0){let r=0,o=0;for(let i=e.firstChild;i&&!i.isLeaf&&(n||!i.type.spec.isolating);i=i.firstChild)r++;for(let i=e.lastChild;i&&!i.isLeaf&&(n||!i.type.spec.isolating);i=i.lastChild)o++;return new t(e,r,o)}};E.empty=new E(v.empty,0,0);function xa(t,e,n){let{index:r,offset:o}=t.findIndex(e),i=t.maybeChild(r),{index:s,offset:l}=t.findIndex(n);if(o==e||i.isText){if(l!=n&&!t.child(s).isText)throw new RangeError("Removing non-flat range");return t.cut(0,e).append(t.cut(n))}if(r!=s)throw new RangeError("Removing non-flat range");return t.replaceChild(r,i.copy(xa(i.content,e-o-1,n-o-1)))}function ka(t,e,n,r){let{index:o,offset:i}=t.findIndex(e),s=t.maybeChild(o);if(i==e||s.isText)return r&&!r.canReplace(o,o,n)?null:t.cut(0,e).append(n).append(t.cut(e));let l=ka(s.content,e-i-1,n,s);return l&&t.replaceChild(o,s.copy(l))}function sp(t,e,n){if(n.openStart>t.depth)throw new Ft("Inserted content deeper than insertion position");if(t.depth-n.openStart!=e.depth-n.openEnd)throw new Ft("Inconsistent open depths");return Sa(t,e,n,0)}function Sa(t,e,n,r){let o=t.index(r),i=t.node(r);if(o==e.index(r)&&r=0&&t.isText&&t.sameMarkup(e[n])?e[n]=t.withText(e[n].text+t.text):e.push(t)}function zn(t,e,n,r){let o=(e||t).node(n),i=0,s=e?e.index(n):o.childCount;t&&(i=t.index(n),t.depth>n?i++:t.textOffset&&(Ht(t.nodeAfter,r),i++));for(let l=i;lo&&Ai(t,e,o+1),s=r.depth>o&&Ai(n,r,o+1),l=[];return zn(null,t,o,l),i&&s&&e.index(o)==n.index(o)?(Ca(i,s),Ht($t(i,va(t,e,n,r,o+1)),l)):(i&&Ht($t(i,Or(t,e,o+1)),l),zn(e,n,o,l),s&&Ht($t(s,Or(n,r,o+1)),l)),zn(r,null,o,l),new v(l)}function Or(t,e,n){let r=[];if(zn(null,t,n,r),t.depth>n){let o=Ai(t,e,n+1);Ht($t(o,Or(t,e,n+1)),r)}return zn(e,null,n,r),new v(r)}function lp(t,e){let n=e.depth-t.openStart,o=e.node(n).copy(t.content);for(let i=n-1;i>=0;i--)o=e.node(i).copy(v.from(o));return{start:o.resolveNoCache(t.openStart+n),end:o.resolveNoCache(o.content.size-t.openEnd-n)}}var Rr=class t{constructor(e,n,r){this.pos=e,this.path=n,this.parentOffset=r,this.depth=n.length/3-1}resolveDepth(e){return e==null?this.depth:e<0?this.depth+e:e}get parent(){return this.node(this.depth)}get doc(){return this.node(0)}node(e){return this.path[this.resolveDepth(e)*3]}index(e){return this.path[this.resolveDepth(e)*3+1]}indexAfter(e){return e=this.resolveDepth(e),this.index(e)+(e==this.depth&&!this.textOffset?0:1)}start(e){return e=this.resolveDepth(e),e==0?0:this.path[e*3-1]+1}end(e){return e=this.resolveDepth(e),this.start(e)+this.node(e).content.size}before(e){if(e=this.resolveDepth(e),!e)throw new RangeError("There is no position before the top-level node");return e==this.depth+1?this.pos:this.path[e*3-1]}after(e){if(e=this.resolveDepth(e),!e)throw new RangeError("There is no position after the top-level node");return e==this.depth+1?this.pos:this.path[e*3-1]+this.path[e*3].nodeSize}get textOffset(){return this.pos-this.path[this.path.length-1]}get nodeAfter(){let e=this.parent,n=this.index(this.depth);if(n==e.childCount)return null;let r=this.pos-this.path[this.path.length-1],o=e.child(n);return r?e.child(n).cut(r):o}get nodeBefore(){let e=this.index(this.depth),n=this.pos-this.path[this.path.length-1];return n?this.parent.child(e).cut(0,n):e==0?null:this.parent.child(e-1)}posAtIndex(e,n){n=this.resolveDepth(n);let r=this.path[n*3],o=n==0?0:this.path[n*3-1]+1;for(let i=0;i0;n--)if(this.start(n)<=e&&this.end(n)>=e)return n;return 0}blockRange(e=this,n){if(e.pos=0;r--)if(e.pos<=this.end(r)&&(!n||n(this.node(r))))return new Vt(this,e,r);return null}sameParent(e){return this.pos-this.parentOffset==e.pos-e.parentOffset}max(e){return e.pos>this.pos?e:this}min(e){return e.pos=0&&n<=e.content.size))throw new RangeError("Position "+n+" out of range");let r=[],o=0,i=n;for(let s=e;;){let{index:l,offset:a}=s.content.findIndex(i),c=i-a;if(r.push(s,l,o+a),!c||(s=s.child(l),s.isText))break;i=c-1,o+=a+1}return new t(n,r,i)}static resolveCached(e,n){let r=ca.get(e);if(r)for(let i=0;ie&&this.nodesBetween(e,n,i=>(r.isInSet(i.marks)&&(o=!0),!o)),o}get isBlock(){return this.type.isBlock}get isTextblock(){return this.type.isTextblock}get inlineContent(){return this.type.inlineContent}get isInline(){return this.type.isInline}get isText(){return this.type.isText}get isLeaf(){return this.type.isLeaf}get isAtom(){return this.type.isAtom}toString(){if(this.type.spec.toDebugString)return this.type.spec.toDebugString(this);let e=this.type.name;return this.content.size&&(e+="("+this.content.toStringInner()+")"),Ma(this.marks,e)}contentMatchAt(e){let n=this.type.contentMatch.matchFragment(this.content,0,e);if(!n)throw new Error("Called contentMatchAt on a node with invalid content");return n}canReplace(e,n,r=v.empty,o=0,i=r.childCount){let s=this.contentMatchAt(e).matchFragment(r,o,i),l=s&&s.matchFragment(this.content,n);if(!l||!l.validEnd)return!1;for(let a=o;an.type.name)}`);this.content.forEach(n=>n.check())}toJSON(){let e={type:this.type.name};for(let n in this.attrs){e.attrs=this.attrs;break}return this.content.size&&(e.content=this.content.toJSON()),this.marks.length&&(e.marks=this.marks.map(n=>n.toJSON())),e}static fromJSON(e,n){if(!n)throw new RangeError("Invalid input for Node.fromJSON");let r;if(n.marks){if(!Array.isArray(n.marks))throw new RangeError("Invalid mark data for Node.fromJSON");r=n.marks.map(e.markFromJSON)}if(n.type=="text"){if(typeof n.text!="string")throw new RangeError("Invalid text node in JSON");return e.text(n.text,r)}let o=v.fromJSON(e,n.content),i=e.nodeType(n.type).create(n.attrs,o,r);return i.type.checkAttrs(i.attrs),i}};ie.prototype.text=void 0;var Ni=class t extends ie{constructor(e,n,r,o){if(super(e,n,null,o),!r)throw new RangeError("Empty text nodes are not allowed");this.text=r}toString(){return this.type.spec.toDebugString?this.type.spec.toDebugString(this):Ma(this.marks,JSON.stringify(this.text))}get textContent(){return this.text}textBetween(e,n){return this.text.slice(e,n)}get nodeSize(){return this.text.length}mark(e){return e==this.marks?this:new t(this.type,this.attrs,this.text,e)}withText(e){return e==this.text?this:new t(this.type,this.attrs,e,this.marks)}cut(e=0,n=this.text.length){return e==0&&n==this.text.length?this:this.withText(this.text.slice(e,n))}eq(e){return this.sameMarkup(e)&&this.text==e.text}toJSON(){let e=super.toJSON();return e.text=this.text,e}};function Ma(t,e){for(let n=t.length-1;n>=0;n--)e=t[n].type.name+"("+e+")";return e}var _t=class t{constructor(e){this.validEnd=e,this.next=[],this.wrapCache=[]}static parse(e,n){let r=new Oi(e,n);if(r.next==null)return t.empty;let o=Ta(r);r.next&&r.err("Unexpected trailing text");let i=gp(mp(o));return yp(i,r),i}matchType(e){for(let n=0;nc.createAndFill()));for(let c=0;c=this.next.length)throw new RangeError(`There's no ${e}th edge in this content match`);return this.next[e]}toString(){let e=[];function n(r){e.push(r);for(let o=0;o{let i=o+(r.validEnd?"*":" ")+" ";for(let s=0;s"+e.indexOf(r.next[s].next);return i}).join(` +`)}};_t.empty=new _t(!0);var Oi=class{constructor(e,n){this.string=e,this.nodeTypes=n,this.inline=null,this.pos=0,this.tokens=e.split(/\s*(?=\b|\W|$)/),this.tokens[this.tokens.length-1]==""&&this.tokens.pop(),this.tokens[0]==""&&this.tokens.shift()}get next(){return this.tokens[this.pos]}eat(e){return this.next==e&&(this.pos++||!0)}err(e){throw new SyntaxError(e+" (in content expression '"+this.string+"')")}};function Ta(t){let e=[];do e.push(dp(t));while(t.eat("|"));return e.length==1?e[0]:{type:"choice",exprs:e}}function dp(t){let e=[];do e.push(up(t));while(t.next&&t.next!=")"&&t.next!="|");return e.length==1?e[0]:{type:"seq",exprs:e}}function up(t){let e=pp(t);for(;;)if(t.eat("+"))e={type:"plus",expr:e};else if(t.eat("*"))e={type:"star",expr:e};else if(t.eat("?"))e={type:"opt",expr:e};else if(t.eat("{"))e=fp(t,e);else break;return e}function da(t){/\D/.test(t.next)&&t.err("Expected number, got '"+t.next+"'");let e=Number(t.next);return t.pos++,e}function fp(t,e){let n=da(t),r=n;return t.eat(",")&&(t.next!="}"?r=da(t):r=-1),t.eat("}")||t.err("Unclosed braced range"),{type:"range",min:n,max:r,expr:e}}function hp(t,e){let n=t.nodeTypes,r=n[e];if(r)return[r];let o=[];for(let i in n){let s=n[i];s.isInGroup(e)&&o.push(s)}return o.length==0&&t.err("No node type or group '"+e+"' found"),o}function pp(t){if(t.eat("(")){let e=Ta(t);return t.eat(")")||t.err("Missing closing paren"),e}else if(/\W/.test(t.next))t.err("Unexpected token '"+t.next+"'");else{let e=hp(t,t.next).map(n=>(t.inline==null?t.inline=n.isInline:t.inline!=n.isInline&&t.err("Mixing inline and block content"),{type:"name",value:n}));return t.pos++,e.length==1?e[0]:{type:"choice",exprs:e}}}function mp(t){let e=[[]];return o(i(t,0),n()),e;function n(){return e.push([])-1}function r(s,l,a){let c={term:a,to:l};return e[s].push(c),c}function o(s,l){s.forEach(a=>a.to=l)}function i(s,l){if(s.type=="choice")return s.exprs.reduce((a,c)=>a.concat(i(c,l)),[]);if(s.type=="seq")for(let a=0;;a++){let c=i(s.exprs[a],l);if(a==s.exprs.length-1)return c;o(c,l=n())}else if(s.type=="star"){let a=n();return r(l,a),o(i(s.expr,a),a),[r(a)]}else if(s.type=="plus"){let a=n();return o(i(s.expr,l),a),o(i(s.expr,a),a),[r(a)]}else{if(s.type=="opt")return[r(l)].concat(i(s.expr,l));if(s.type=="range"){let a=l;for(let c=0;c{t[s].forEach(({term:l,to:a})=>{if(!l)return;let c;for(let d=0;d{c||o.push([l,c=[]]),c.indexOf(d)==-1&&c.push(d)})})});let i=e[r.join(",")]=new _t(r.indexOf(t.length-1)>-1);for(let s=0;s-1}get whitespace(){return this.spec.whitespace||(this.spec.code?"pre":"normal")}hasRequiredAttrs(){for(let e in this.attrs)if(this.attrs[e].isRequired)return!0;return!1}compatibleContent(e){return this==e||this.contentMatch.compatible(e.contentMatch)}computeAttrs(e){return!e&&this.defaultAttrs?this.defaultAttrs:Na(this.attrs,e)}create(e=null,n,r){if(this.isText)throw new Error("NodeType.create can't construct text nodes");return new ie(this,this.computeAttrs(e),v.from(n),J.setFrom(r))}createChecked(e=null,n,r){return n=v.from(n),this.checkContent(n),new ie(this,this.computeAttrs(e),n,J.setFrom(r))}createAndFill(e=null,n,r){if(e=this.computeAttrs(e),n=v.from(n),n.size){let s=this.contentMatch.fillBefore(n);if(!s)return null;n=s.append(n)}let o=this.contentMatch.matchFragment(n),i=o&&o.fillBefore(v.empty,!0);return i?new ie(this,e,n.append(i),J.setFrom(r)):null}validContent(e){let n=this.contentMatch.matchFragment(e);if(!n||!n.validEnd)return!1;for(let r=0;r-1}allowsMarks(e){if(this.markSet==null)return!0;for(let n=0;nr[i]=new t(i,n,s));let o=n.spec.topNode||"doc";if(!r[o])throw new RangeError("Schema is missing its top node type ('"+o+"')");if(!r.text)throw new RangeError("Every schema needs a 'text' type");for(let i in r.text.attrs)throw new RangeError("The text node type should not have attributes");return r}};function bp(t,e,n){let r=n.split("|");return o=>{let i=o===null?"null":typeof o;if(r.indexOf(i)<0)throw new RangeError(`Expected value of type ${r} for attribute ${e} on type ${t}, got ${i}`)}}var Ri=class{constructor(e,n,r){this.hasDefault=Object.prototype.hasOwnProperty.call(r,"default"),this.default=r.default,this.validate=typeof r.validate=="string"?bp(e,n,r.validate):r.validate}get isRequired(){return!this.hasDefault}},$n=class t{constructor(e,n,r,o){this.name=e,this.rank=n,this.schema=r,this.spec=o,this.attrs=Ra(e,o.attrs),this.excluded=null;let i=Ea(this.attrs);this.instance=i?new J(this,i):null}create(e=null){return!e&&this.instance?this.instance:new J(this,Na(this.attrs,e))}static compile(e,n){let r=Object.create(null),o=0;return e.forEach((i,s)=>r[i]=new t(i,o++,n,s)),r}removeFromSet(e){for(var n=0;n-1}},fn=class{constructor(e){this.linebreakReplacement=null,this.cached=Object.create(null);let n=this.spec={};for(let o in e)n[o]=e[o];n.nodes=vi.from(e.nodes),n.marks=vi.from(e.marks||{}),this.nodes=Dr.compile(this.spec.nodes,this),this.marks=$n.compile(this.spec.marks,this);let r=Object.create(null);for(let o in this.nodes){if(o in this.marks)throw new RangeError(o+" can not be both a node and a mark");let i=this.nodes[o],s=i.spec.content||"",l=i.spec.marks;if(i.contentMatch=r[s]||(r[s]=_t.parse(s,this.nodes)),i.inlineContent=i.contentMatch.inlineContent,i.spec.linebreakReplacement){if(this.linebreakReplacement)throw new RangeError("Multiple linebreak nodes defined");if(!i.isInline||!i.isLeaf)throw new RangeError("Linebreak replacement nodes must be inline leaf nodes");this.linebreakReplacement=i}i.markSet=l=="_"?null:l?fa(this,l.split(" ")):l==""||!i.inlineContent?[]:null}for(let o in this.marks){let i=this.marks[o],s=i.spec.excludes;i.excluded=s==null?[i]:s==""?[]:fa(this,s.split(" "))}this.nodeFromJSON=o=>ie.fromJSON(this,o),this.markFromJSON=o=>J.fromJSON(this,o),this.topNodeType=this.nodes[this.spec.topNode||"doc"],this.cached.wrappings=Object.create(null)}node(e,n=null,r,o){if(typeof e=="string")e=this.nodeType(e);else if(e instanceof Dr){if(e.schema!=this)throw new RangeError("Node type from different schema used ("+e.name+")")}else throw new RangeError("Invalid node type: "+e);return e.createChecked(n,r,o)}text(e,n){let r=this.nodes.text;return new Ni(r,r.defaultAttrs,e,J.setFrom(n))}mark(e,n){return typeof e=="string"&&(e=this.marks[e]),e.create(n)}nodeType(e){let n=this.nodes[e];if(!n)throw new RangeError("Unknown node type: "+e);return n}};function fa(t,e){let n=[];for(let r=0;r-1)&&n.push(s=a)}if(!s)throw new SyntaxError("Unknown mark type: '"+e[r]+"'")}return n}function wp(t){return t.tag!=null}function xp(t){return t.style!=null}var Xe=class t{constructor(e,n){this.schema=e,this.rules=n,this.tags=[],this.styles=[];let r=this.matchedStyles=[];n.forEach(o=>{if(wp(o))this.tags.push(o);else if(xp(o)){let i=/[^=]*/.exec(o.style)[0];r.indexOf(i)<0&&r.push(i),this.styles.push(o)}}),this.normalizeLists=!this.tags.some(o=>{if(!/^(ul|ol)\b/.test(o.tag)||!o.node)return!1;let i=e.nodes[o.node];return i.contentMatch.matchType(i)})}parse(e,n={}){let r=new Ir(this,n,!1);return r.addAll(e,J.none,n.from,n.to),r.finish()}parseSlice(e,n={}){let r=new Ir(this,n,!0);return r.addAll(e,J.none,n.from,n.to),E.maxOpen(r.finish())}matchTag(e,n,r){for(let o=r?this.tags.indexOf(r)+1:0;oe.length&&(l.charCodeAt(e.length)!=61||l.slice(e.length+1)!=n))){if(s.getAttrs){let a=s.getAttrs(n);if(a===!1)continue;s.attrs=a||void 0}return s}}}static schemaRules(e){let n=[];function r(o){let i=o.priority==null?50:o.priority,s=0;for(;s{r(s=pa(s)),s.mark||s.ignore||s.clearMark||(s.mark=o)})}for(let o in e.nodes){let i=e.nodes[o].spec.parseDOM;i&&i.forEach(s=>{r(s=pa(s)),s.node||s.ignore||s.mark||(s.node=o)})}return n}static fromSchema(e){return e.cached.domParser||(e.cached.domParser=new t(e,t.schemaRules(e)))}},Da={address:!0,article:!0,aside:!0,blockquote:!0,canvas:!0,dd:!0,div:!0,dl:!0,fieldset:!0,figcaption:!0,figure:!0,footer:!0,form:!0,h1:!0,h2:!0,h3:!0,h4:!0,h5:!0,h6:!0,header:!0,hgroup:!0,hr:!0,li:!0,noscript:!0,ol:!0,output:!0,p:!0,pre:!0,section:!0,table:!0,tfoot:!0,ul:!0},kp={head:!0,noscript:!0,object:!0,script:!0,style:!0,title:!0},Ia={ol:!0,ul:!0},Fn=1,Di=2,Hn=4;function ha(t,e,n){return e!=null?(e?Fn:0)|(e==="full"?Di:0):t&&t.whitespace=="pre"?Fn|Di:n&~Hn}var un=class{constructor(e,n,r,o,i,s){this.type=e,this.attrs=n,this.marks=r,this.solid=o,this.options=s,this.content=[],this.activeMarks=J.none,this.match=i||(s&Hn?null:e.contentMatch)}findWrapping(e){if(!this.match){if(!this.type)return[];let n=this.type.contentMatch.fillBefore(v.from(e));if(n)this.match=this.type.contentMatch.matchFragment(n);else{let r=this.type.contentMatch,o;return(o=r.findWrapping(e.type))?(this.match=r,o):null}}return this.match.findWrapping(e.type)}finish(e){if(!(this.options&Fn)){let r=this.content[this.content.length-1],o;if(r&&r.isText&&(o=/[ \t\r\n\u000c]+$/.exec(r.text))){let i=r;r.text.length==o[0].length?this.content.pop():this.content[this.content.length-1]=i.withText(i.text.slice(0,i.text.length-o[0].length))}}let n=v.from(this.content);return!e&&this.match&&(n=n.append(this.match.fillBefore(v.empty,!0))),this.type?this.type.create(this.attrs,n,this.marks):n}inlineContext(e){return this.type?this.type.inlineContent:this.content.length?this.content[0].isInline:e.parentNode&&!Da.hasOwnProperty(e.parentNode.nodeName.toLowerCase())}},Ir=class{constructor(e,n,r){this.parser=e,this.options=n,this.isOpen=r,this.open=0,this.localPreserveWS=!1;let o=n.topNode,i,s=ha(null,n.preserveWhitespace,0)|(r?Hn:0);o?i=new un(o.type,o.attrs,J.none,!0,n.topMatch||o.type.contentMatch,s):r?i=new un(null,null,J.none,!0,null,s):i=new un(e.schema.topNodeType,null,J.none,!0,null,s),this.nodes=[i],this.find=n.findPositions,this.needsBlock=!1}get top(){return this.nodes[this.open]}addDOM(e,n){e.nodeType==3?this.addTextNode(e,n):e.nodeType==1&&this.addElement(e,n)}addTextNode(e,n){let r=e.nodeValue,o=this.top,i=o.options&Di?"full":this.localPreserveWS||(o.options&Fn)>0,{schema:s}=this.parser;if(i==="full"||o.inlineContext(e)||/[^ \t\r\n\u000c]/.test(r)){if(i)if(i==="full")r=r.replace(/\r\n?/g,` +`);else if(s.linebreakReplacement&&/[\r\n]/.test(r)&&this.top.findWrapping(s.linebreakReplacement.create())){let l=r.split(/\r?\n|\r/);for(let a=0;a!a.clearMark(c)):n=n.concat(this.parser.schema.marks[a.mark].create(a.attrs)),a.consuming===!1)l=a;else break}}return n}addElementByRule(e,n,r,o){let i,s;if(n.node)if(s=this.parser.schema.nodes[n.node],s.isLeaf)this.insertNode(s.create(n.attrs),r,e.nodeName=="BR")||this.leafFallback(e,r);else{let a=this.enter(s,n.attrs||null,r,n.preserveWhitespace);a&&(i=!0,r=a)}else{let a=this.parser.schema.marks[n.mark];r=r.concat(a.create(n.attrs))}let l=this.top;if(s&&s.isLeaf)this.findInside(e);else if(o)this.addElement(e,r,o);else if(n.getContent)this.findInside(e),n.getContent(e,this.parser.schema).forEach(a=>this.insertNode(a,r,!1));else{let a=e;typeof n.contentElement=="string"?a=e.querySelector(n.contentElement):typeof n.contentElement=="function"?a=n.contentElement(e):n.contentElement&&(a=n.contentElement),this.findAround(e,a,!0),this.addAll(a,r),this.findAround(e,a,!1)}i&&this.sync(l)&&this.open--}addAll(e,n,r,o){let i=r||0;for(let s=r?e.childNodes[r]:e.firstChild,l=o==null?null:e.childNodes[o];s!=l;s=s.nextSibling,++i)this.findAtPoint(e,i),this.addDOM(s,n);this.findAtPoint(e,i)}findPlace(e,n,r){let o,i;for(let s=this.open,l=0;s>=0;s--){let a=this.nodes[s],c=a.findWrapping(e);if(c&&(!o||o.length>c.length+l)&&(o=c,i=a,!c.length))break;if(a.solid){if(r)break;l+=2}}if(!o)return null;this.sync(i);for(let s=0;s(s.type?s.type.allowsMarkType(c.type):ma(c.type,e))?(a=c.addToSet(a),!1):!0),this.nodes.push(new un(e,n,a,o,null,l)),this.open++,r}closeExtra(e=!1){let n=this.nodes.length-1;if(n>this.open){for(;n>this.open;n--)this.nodes[n-1].content.push(this.nodes[n].finish(e));this.nodes.length=this.open+1}}finish(){return this.open=0,this.closeExtra(this.isOpen),this.nodes[0].finish(!!(this.isOpen||this.options.topOpen))}sync(e){for(let n=this.open;n>=0;n--){if(this.nodes[n]==e)return this.open=n,!0;this.localPreserveWS&&(this.nodes[n].options|=Fn)}return!1}get currentPos(){this.closeExtra();let e=0;for(let n=this.open;n>=0;n--){let r=this.nodes[n].content;for(let o=r.length-1;o>=0;o--)e+=r[o].nodeSize;n&&e++}return e}findAtPoint(e,n){if(this.find)for(let r=0;r-1)return e.split(/\s*\|\s*/).some(this.matchesContext,this);let n=e.split("/"),r=this.options.context,o=!this.isOpen&&(!r||r.parent.type==this.nodes[0].type),i=-(r?r.depth+1:0)+(o?0:1),s=(l,a)=>{for(;l>=0;l--){let c=n[l];if(c==""){if(l==n.length-1||l==0)continue;for(;a>=i;a--)if(s(l-1,a))return!0;return!1}else{let d=a>0||a==0&&o?this.nodes[a].type:r&&a>=i?r.node(a-i).type:null;if(!d||d.name!=c&&!d.isInGroup(c))return!1;a--}}return!0};return s(n.length-1,this.open)}textblockFromContext(){let e=this.options.context;if(e)for(let n=e.depth;n>=0;n--){let r=e.node(n).contentMatchAt(e.indexAfter(n)).defaultType;if(r&&r.isTextblock&&r.defaultAttrs)return r}for(let n in this.parser.schema.nodes){let r=this.parser.schema.nodes[n];if(r.isTextblock&&r.defaultAttrs)return r}}};function Sp(t){for(let e=t.firstChild,n=null;e;e=e.nextSibling){let r=e.nodeType==1?e.nodeName.toLowerCase():null;r&&Ia.hasOwnProperty(r)&&n?(n.appendChild(e),e=n):r=="li"?n=e:r&&(n=null)}}function Cp(t,e){return(t.matches||t.msMatchesSelector||t.webkitMatchesSelector||t.mozMatchesSelector).call(t,e)}function pa(t){let e={};for(let n in t)e[n]=t[n];return e}function ma(t,e){let n=e.schema.nodes;for(let r in n){let o=n[r];if(!o.allowsMarkType(t))continue;let i=[],s=l=>{i.push(l);for(let a=0;a{if(i.length||s.marks.length){let l=0,a=0;for(;l=0;o--){let i=this.serializeMark(e.marks[o],e.isInline,n);i&&((i.contentDOM||i.dom).appendChild(r),r=i.dom)}return r}serializeMark(e,n,r={}){let o=this.marks[e.type.name];return o&&Er(Ti(r),o(e,n),null,e.attrs)}static renderSpec(e,n,r=null,o){return Er(e,n,r,o)}static fromSchema(e){return e.cached.domSerializer||(e.cached.domSerializer=new t(this.nodesFromSchema(e),this.marksFromSchema(e)))}static nodesFromSchema(e){let n=ga(e.nodes);return n.text||(n.text=r=>r.text),n}static marksFromSchema(e){return ga(e.marks)}};function ga(t){let e={};for(let n in t){let r=t[n].spec.toDOM;r&&(e[n]=r)}return e}function Ti(t){return t.document||window.document}var ya=new WeakMap;function vp(t){let e=ya.get(t);return e===void 0&&ya.set(t,e=Mp(t)),e}function Mp(t){let e=null;function n(r){if(r&&typeof r=="object")if(Array.isArray(r))if(typeof r[0]=="string")e||(e=[]),e.push(r);else for(let o=0;o-1)throw new RangeError("Using an array from an attribute object as a DOM spec. This may be an attempted cross site scripting attack.");let s=o.indexOf(" ");s>0&&(n=o.slice(0,s),o=o.slice(s+1));let l,a=n?t.createElementNS(n,o):t.createElement(o),c=e[1],d=1;if(c&&typeof c=="object"&&c.nodeType==null&&!Array.isArray(c)){d=2;for(let u in c)if(c[u]!=null){let f=u.indexOf(" ");f>0?a.setAttributeNS(u.slice(0,f),u.slice(f+1),c[u]):u=="style"&&a.style?a.style.cssText=c[u]:a.setAttribute(u,c[u])}}for(let u=d;ud)throw new RangeError("Content hole must be the only child of its parent node");return{dom:a,contentDOM:a}}else{let{dom:h,contentDOM:p}=Er(t,f,n,r);if(a.appendChild(h),p){if(l)throw new RangeError("Multiple content holes");l=p}}}return{dom:a,contentDOM:l}}var Ba=65535,za=Math.pow(2,16);function Tp(t,e){return t+e*za}function Pa(t){return t&Ba}function Ap(t){return(t-(t&Ba))/za}var Ha=1,$a=2,Pr=4,Fa=8,Wn=class{constructor(e,n,r){this.pos=e,this.delInfo=n,this.recover=r}get deleted(){return(this.delInfo&Fa)>0}get deletedBefore(){return(this.delInfo&(Ha|Pr))>0}get deletedAfter(){return(this.delInfo&($a|Pr))>0}get deletedAcross(){return(this.delInfo&Pr)>0}},dt=class t{constructor(e,n=!1){if(this.ranges=e,this.inverted=n,!e.length&&t.empty)return t.empty}recover(e){let n=0,r=Pa(e);if(!this.inverted)for(let o=0;oe)break;let c=this.ranges[l+i],d=this.ranges[l+s],u=a+c;if(e<=u){let f=c?e==a?-1:e==u?1:n:n,h=a+o+(f<0?0:d);if(r)return h;let p=e==(n<0?a:u)?null:Tp(l/3,e-a),m=e==a?$a:e==u?Ha:Pr;return(n<0?e!=a:e!=u)&&(m|=Fa),new Wn(h,m,p)}o+=d-c}return r?e+o:new Wn(e+o,0,null)}touches(e,n){let r=0,o=Pa(n),i=this.inverted?2:1,s=this.inverted?1:2;for(let l=0;le)break;let c=this.ranges[l+i],d=a+c;if(e<=d&&l==o*3)return!0;r+=this.ranges[l+s]-c}return!1}forEach(e){let n=this.inverted?2:1,r=this.inverted?1:2;for(let o=0,i=0;o=0;n--){let o=e.getMirror(n);this.appendMap(e._maps[n].invert(),o!=null&&o>n?r-o-1:void 0)}}invert(){let e=new t;return e.appendMappingInverted(this),e}map(e,n=1){if(this.mirror)return this._map(e,n,!0);for(let r=this.from;ri&&a!s.isAtom||!l.type.allowsMarkType(this.mark.type)?s:s.mark(this.mark.addToSet(s.marks)),o),n.openStart,n.openEnd);return ue.fromReplace(e,this.from,this.to,i)}invert(){return new ut(this.from,this.to,this.mark)}map(e){let n=e.mapResult(this.from,1),r=e.mapResult(this.to,-1);return n.deleted&&r.deleted||n.pos>=r.pos?null:new t(n.pos,r.pos,this.mark)}merge(e){return e instanceof t&&e.mark.eq(this.mark)&&this.from<=e.to&&this.to>=e.from?new t(Math.min(this.from,e.from),Math.max(this.to,e.to),this.mark):null}toJSON(){return{stepType:"addMark",mark:this.mark.toJSON(),from:this.from,to:this.to}}static fromJSON(e,n){if(typeof n.from!="number"||typeof n.to!="number")throw new RangeError("Invalid input for AddMarkStep.fromJSON");return new t(n.from,n.to,e.markFromJSON(n.mark))}};ce.jsonID("addMark",Un);var ut=class t extends ce{constructor(e,n,r){super(),this.from=e,this.to=n,this.mark=r}apply(e){let n=e.slice(this.from,this.to),r=new E(Hi(n.content,o=>o.mark(this.mark.removeFromSet(o.marks)),e),n.openStart,n.openEnd);return ue.fromReplace(e,this.from,this.to,r)}invert(){return new Un(this.from,this.to,this.mark)}map(e){let n=e.mapResult(this.from,1),r=e.mapResult(this.to,-1);return n.deleted&&r.deleted||n.pos>=r.pos?null:new t(n.pos,r.pos,this.mark)}merge(e){return e instanceof t&&e.mark.eq(this.mark)&&this.from<=e.to&&this.to>=e.from?new t(Math.min(this.from,e.from),Math.max(this.to,e.to),this.mark):null}toJSON(){return{stepType:"removeMark",mark:this.mark.toJSON(),from:this.from,to:this.to}}static fromJSON(e,n){if(typeof n.from!="number"||typeof n.to!="number")throw new RangeError("Invalid input for RemoveMarkStep.fromJSON");return new t(n.from,n.to,e.markFromJSON(n.mark))}};ce.jsonID("removeMark",ut);var Kn=class t extends ce{constructor(e,n){super(),this.pos=e,this.mark=n}apply(e){let n=e.nodeAt(this.pos);if(!n)return ue.fail("No node at mark step's position");let r=n.type.create(n.attrs,null,this.mark.addToSet(n.marks));return ue.fromReplace(e,this.pos,this.pos+1,new E(v.from(r),0,n.isLeaf?0:1))}invert(e){let n=e.nodeAt(this.pos);if(n){let r=this.mark.addToSet(n.marks);if(r.length==n.marks.length){for(let o=0;or.pos?null:new t(n.pos,r.pos,o,i,this.slice,this.insert,this.structure)}toJSON(){let e={stepType:"replaceAround",from:this.from,to:this.to,gapFrom:this.gapFrom,gapTo:this.gapTo,insert:this.insert};return this.slice.size&&(e.slice=this.slice.toJSON()),this.structure&&(e.structure=!0),e}static fromJSON(e,n){if(typeof n.from!="number"||typeof n.to!="number"||typeof n.gapFrom!="number"||typeof n.gapTo!="number"||typeof n.insert!="number")throw new RangeError("Invalid input for ReplaceAroundStep.fromJSON");return new t(n.from,n.to,n.gapFrom,n.gapTo,E.fromJSON(e,n.slice),n.insert,!!n.structure)}};ce.jsonID("replaceAround",se);function Bi(t,e,n){let r=t.resolve(e),o=n-e,i=r.depth;for(;o>0&&i>0&&r.indexAfter(i)==r.node(i).childCount;)i--,o--;if(o>0){let s=r.node(i).maybeChild(r.indexAfter(i));for(;o>0;){if(!s||s.isLeaf)return!0;s=s.firstChild,o--}}return!1}function Ep(t,e,n,r){let o=[],i=[],s,l;t.doc.nodesBetween(e,n,(a,c,d)=>{if(!a.isInline)return;let u=a.marks;if(!r.isInSet(u)&&d.type.allowsMarkType(r.type)){let f=Math.max(c,e),h=Math.min(c+a.nodeSize,n),p=r.addToSet(u);for(let m=0;mt.step(a)),i.forEach(a=>t.step(a))}function Np(t,e,n,r){let o=[],i=0;t.doc.nodesBetween(e,n,(s,l)=>{if(!s.isInline)return;i++;let a=null;if(r instanceof $n){let c=s.marks,d;for(;d=r.isInSet(c);)(a||(a=[])).push(d),c=d.removeFromSet(c)}else r?r.isInSet(s.marks)&&(a=[r]):a=s.marks;if(a&&a.length){let c=Math.min(l+s.nodeSize,n);for(let d=0;dt.step(new ut(s.from,s.to,s.style)))}function $i(t,e,n,r=n.contentMatch,o=!0){let i=t.doc.nodeAt(e),s=[],l=e+1;for(let a=0;a=0;a--)t.step(s[a])}function Op(t,e,n){return(e==0||t.canReplace(e,t.childCount))&&(n==t.childCount||t.canReplace(0,n))}function ft(t){let n=t.parent.content.cutByIndex(t.startIndex,t.endIndex);for(let r=t.depth,o=0,i=0;;--r){let s=t.$from.node(r),l=t.$from.index(r)+o,a=t.$to.indexAfter(r)-i;if(rn;p--)m||r.index(p)>0?(m=!0,d=v.from(r.node(p).copy(d)),u++):a--;let f=v.empty,h=0;for(let p=i,m=!1;p>n;p--)m||o.after(p+1)=0;s--){if(r.size){let l=n[s].type.contentMatch.matchFragment(r);if(!l||!l.validEnd)throw new RangeError("Wrapper type given to Transform.wrap does not form valid content of its parent wrapper")}r=v.from(n[s].type.create(n[s].attrs,r))}let o=e.start,i=e.end;t.step(new se(o,i,o,i,new E(r,0,0),n.length,!0))}function Lp(t,e,n,r,o){if(!r.isTextblock)throw new RangeError("Type given to setBlockType should be a textblock");let i=t.steps.length;t.doc.nodesBetween(e,n,(s,l)=>{let a=typeof o=="function"?o(s):o;if(s.isTextblock&&!s.hasMarkup(r,a)&&Bp(t.doc,t.mapping.slice(i).map(l),r)){let c=null;if(r.schema.linebreakReplacement){let h=r.whitespace=="pre",p=!!r.contentMatch.matchType(r.schema.linebreakReplacement);h&&!p?c=!1:!h&&p&&(c=!0)}c===!1&&_a(t,s,l,i),$i(t,t.mapping.slice(i).map(l,1),r,void 0,c===null);let d=t.mapping.slice(i),u=d.map(l,1),f=d.map(l+s.nodeSize,1);return t.step(new se(u,f,u+1,f-1,new E(v.from(r.create(a,null,s.marks)),0,0),1,!0)),c===!0&&Va(t,s,l,i),!1}})}function Va(t,e,n,r){e.forEach((o,i)=>{if(o.isText){let s,l=/\r?\n|\r/g;for(;s=l.exec(o.text);){let a=t.mapping.slice(r).map(n+1+i+s.index);t.replaceWith(a,a+1,e.type.schema.linebreakReplacement.create())}}})}function _a(t,e,n,r){e.forEach((o,i)=>{if(o.type==o.type.schema.linebreakReplacement){let s=t.mapping.slice(r).map(n+1+i);t.replaceWith(s,s+1,e.type.schema.text(` +`))}})}function Bp(t,e,n){let r=t.resolve(e),o=r.index();return r.parent.canReplaceWith(o,o+1,n)}function zp(t,e,n,r,o){let i=t.doc.nodeAt(e);if(!i)throw new RangeError("No node at given position");n||(n=i.type);let s=n.create(r,null,o||i.marks);if(i.isLeaf)return t.replaceWith(e,e+i.nodeSize,s);if(!n.validContent(i.content))throw new RangeError("Invalid content for node type "+n.name);t.step(new se(e,e+i.nodeSize,e+1,e+i.nodeSize-1,new E(v.from(s),0,0),1,!0))}function Ee(t,e,n=1,r){let o=t.resolve(e),i=o.depth-n,s=r&&r[r.length-1]||o.parent;if(i<0||o.parent.type.spec.isolating||!o.parent.canReplace(o.index(),o.parent.childCount)||!s.type.validContent(o.parent.content.cutByIndex(o.index(),o.parent.childCount)))return!1;for(let c=o.depth-1,d=n-2;c>i;c--,d--){let u=o.node(c),f=o.index(c);if(u.type.spec.isolating)return!1;let h=u.content.cutByIndex(f,u.childCount),p=r&&r[d+1];p&&(h=h.replaceChild(0,p.type.create(p.attrs)));let m=r&&r[d]||u;if(!u.canReplace(f+1,u.childCount)||!m.type.validContent(h))return!1}let l=o.indexAfter(i),a=r&&r[0];return o.node(i).canReplaceWith(l,l,a?a.type:o.node(i+1).type)}function Hp(t,e,n=1,r){let o=t.doc.resolve(e),i=v.empty,s=v.empty;for(let l=o.depth,a=o.depth-n,c=n-1;l>a;l--,c--){i=v.from(o.node(l).copy(i));let d=r&&r[c];s=v.from(d?d.type.create(d.attrs,s):o.node(l).copy(s))}t.step(new ye(e,e,new E(i.append(s),n,n),!0))}function Re(t,e){let n=t.resolve(e),r=n.index();return Wa(n.nodeBefore,n.nodeAfter)&&n.parent.canReplace(r,r+1)}function $p(t,e){e.content.size||t.type.compatibleContent(e.type);let n=t.contentMatchAt(t.childCount),{linebreakReplacement:r}=t.type.schema;for(let o=0;o0?(i=r.node(o+1),l++,s=r.node(o).maybeChild(l)):(i=r.node(o).maybeChild(l-1),s=r.node(o+1)),i&&!i.isTextblock&&Wa(i,s)&&r.node(o).canReplace(l,l+1))return e;if(o==0)break;e=n<0?r.before(o):r.after(o)}}function Fp(t,e,n){let r=null,{linebreakReplacement:o}=t.doc.type.schema,i=t.doc.resolve(e-n),s=i.node().type;if(o&&s.inlineContent){let d=s.whitespace=="pre",u=!!s.contentMatch.matchType(o);d&&!u?r=!1:!d&&u&&(r=!0)}let l=t.steps.length;if(r===!1){let d=t.doc.resolve(e+n);_a(t,d.node(),d.before(),l)}s.inlineContent&&$i(t,e+n-1,s,i.node().contentMatchAt(i.index()),r==null);let a=t.mapping.slice(l),c=a.map(e-n);if(t.step(new ye(c,a.map(e+n,-1),E.empty,!0)),r===!0){let d=t.doc.resolve(c);Va(t,d.node(),d.before(),t.steps.length)}return t}function Vp(t,e,n){let r=t.resolve(e);if(r.parent.canReplaceWith(r.index(),r.index(),n))return e;if(r.parentOffset==0)for(let o=r.depth-1;o>=0;o--){let i=r.index(o);if(r.node(o).canReplaceWith(i,i,n))return r.before(o+1);if(i>0)return null}if(r.parentOffset==r.parent.content.size)for(let o=r.depth-1;o>=0;o--){let i=r.indexAfter(o);if(r.node(o).canReplaceWith(i,i,n))return r.after(o+1);if(i=0;s--){let l=s==r.depth?0:r.pos<=(r.start(s+1)+r.end(s+1))/2?-1:1,a=r.index(s)+(l>0?1:0),c=r.node(s),d=!1;if(i==1)d=c.canReplace(a,a,o);else{let u=c.contentMatchAt(a).findWrapping(o.firstChild.type);d=u&&c.canReplaceWith(a,a,u[0])}if(d)return l==0?r.pos:l<0?r.before(s+1):r.after(s+1)}return null}function qn(t,e,n=e,r=E.empty){if(e==n&&!r.size)return null;let o=t.resolve(e),i=t.resolve(n);return ja(o,i,r)?new ye(e,n,r):new zi(o,i,r).fit()}function ja(t,e,n){return!n.openStart&&!n.openEnd&&t.start()==e.start()&&t.parent.canReplace(t.index(),e.index(),n.content)}var zi=class{constructor(e,n,r){this.$from=e,this.$to=n,this.unplaced=r,this.frontier=[],this.placed=v.empty;for(let o=0;o<=e.depth;o++){let i=e.node(o);this.frontier.push({type:i.type,match:i.contentMatchAt(e.indexAfter(o))})}for(let o=e.depth;o>0;o--)this.placed=v.from(e.node(o).copy(this.placed))}get depth(){return this.frontier.length-1}fit(){for(;this.unplaced.size;){let c=this.findFittable();c?this.placeNodes(c):this.openMore()||this.dropNode()}let e=this.mustMoveInline(),n=this.placed.size-this.depth-this.$from.depth,r=this.$from,o=this.close(e<0?this.$to:r.doc.resolve(e));if(!o)return null;let i=this.placed,s=r.depth,l=o.depth;for(;s&&l&&i.childCount==1;)i=i.firstChild.content,s--,l--;let a=new E(i,s,l);return e>-1?new se(r.pos,e,this.$to.pos,this.$to.end(),a,n):a.size||r.pos!=this.$to.pos?new ye(r.pos,o.pos,a):null}findFittable(){let e=this.unplaced.openStart;for(let n=this.unplaced.content,r=0,o=this.unplaced.openEnd;r1&&(o=0),i.type.spec.isolating&&o<=r){e=r;break}n=i.content}for(let n=1;n<=2;n++)for(let r=n==1?e:this.unplaced.openStart;r>=0;r--){let o,i=null;r?(i=Pi(this.unplaced.content,r-1).firstChild,o=i.content):o=this.unplaced.content;let s=o.firstChild;for(let l=this.depth;l>=0;l--){let{type:a,match:c}=this.frontier[l],d,u=null;if(n==1&&(s?c.matchType(s.type)||(u=c.fillBefore(v.from(s),!1)):i&&a.compatibleContent(i.type)))return{sliceDepth:r,frontierDepth:l,parent:i,inject:u};if(n==2&&s&&(d=c.findWrapping(s.type)))return{sliceDepth:r,frontierDepth:l,parent:i,wrap:d};if(i&&c.matchType(i.type))break}}}openMore(){let{content:e,openStart:n,openEnd:r}=this.unplaced,o=Pi(e,n);return!o.childCount||o.firstChild.isLeaf?!1:(this.unplaced=new E(e,n+1,Math.max(r,o.size+n>=e.size-r?n+1:0)),!0)}dropNode(){let{content:e,openStart:n,openEnd:r}=this.unplaced,o=Pi(e,n);if(o.childCount<=1&&n>0){let i=e.size-n<=n+o.size;this.unplaced=new E(Vn(e,n-1,1),n-1,i?n-1:r)}else this.unplaced=new E(Vn(e,n,1),n,r)}placeNodes({sliceDepth:e,frontierDepth:n,parent:r,inject:o,wrap:i}){for(;this.depth>n;)this.closeFrontierNode();if(i)for(let m=0;m1||a==0||m.content.size)&&(u=g,d.push(Ua(m.mark(f.allowedMarks(m.marks)),c==1?a:0,c==l.childCount?h:-1)))}let p=c==l.childCount;p||(h=-1),this.placed=_n(this.placed,n,v.from(d)),this.frontier[n].match=u,p&&h<0&&r&&r.type==this.frontier[this.depth].type&&this.frontier.length>1&&this.closeFrontierNode();for(let m=0,g=l;m1&&o==this.$to.end(--r);)++o;return o}findCloseLevel(e){e:for(let n=Math.min(this.depth,e.depth);n>=0;n--){let{match:r,type:o}=this.frontier[n],i=n=0;l--){let{match:a,type:c}=this.frontier[l],d=Li(e,l,c,a,!0);if(!d||d.childCount)continue e}return{depth:n,fit:s,move:i?e.doc.resolve(e.after(n+1)):e}}}}close(e){let n=this.findCloseLevel(e);if(!n)return null;for(;this.depth>n.depth;)this.closeFrontierNode();n.fit.childCount&&(this.placed=_n(this.placed,n.depth,n.fit)),e=n.move;for(let r=n.depth+1;r<=e.depth;r++){let o=e.node(r),i=o.type.contentMatch.fillBefore(o.content,!0,e.index(r));this.openFrontierNode(o.type,o.attrs,i)}return e}openFrontierNode(e,n=null,r){let o=this.frontier[this.depth];o.match=o.match.matchType(e),this.placed=_n(this.placed,this.depth,v.from(e.create(n,r))),this.frontier.push({type:e,match:e.contentMatch})}closeFrontierNode(){let n=this.frontier.pop().match.fillBefore(v.empty,!0);n.childCount&&(this.placed=_n(this.placed,this.frontier.length,n))}};function Vn(t,e,n){return e==0?t.cutByIndex(n,t.childCount):t.replaceChild(0,t.firstChild.copy(Vn(t.firstChild.content,e-1,n)))}function _n(t,e,n){return e==0?t.append(n):t.replaceChild(t.childCount-1,t.lastChild.copy(_n(t.lastChild.content,e-1,n)))}function Pi(t,e){for(let n=0;n1&&(r=r.replaceChild(0,Ua(r.firstChild,e-1,r.childCount==1?n-1:0))),e>0&&(r=t.type.contentMatch.fillBefore(r).append(r),n<=0&&(r=r.append(t.type.contentMatch.matchFragment(r).fillBefore(v.empty,!0)))),t.copy(r)}function Li(t,e,n,r,o){let i=t.node(e),s=o?t.indexAfter(e):t.index(e);if(s==i.childCount&&!n.compatibleContent(i.type))return null;let l=r.fillBefore(i.content,!0,s);return l&&!_p(n,i.content,s)?l:null}function _p(t,e,n){for(let r=n;r0;f--,h--){let p=o.node(f).type.spec;if(p.defining||p.definingAsContext||p.isolating)break;s.indexOf(f)>-1?l=f:o.before(f)==h&&s.splice(1,0,-f)}let a=s.indexOf(l),c=[],d=r.openStart;for(let f=r.content,h=0;;h++){let p=f.firstChild;if(c.push(p),h==r.openStart)break;f=p.content}for(let f=d-1;f>=0;f--){let h=c[f],p=Wp(h.type);if(p&&!h.sameMarkup(o.node(Math.abs(l)-1)))d=f;else if(p||!h.type.isTextblock)break}for(let f=r.openStart;f>=0;f--){let h=(f+d+1)%(r.openStart+1),p=c[h];if(p)for(let m=0;m=0&&(t.replace(e,n,r),!(t.steps.length>u));f--){let h=s[f];h<0||(e=o.before(h),n=i.after(h))}}function Ka(t,e,n,r,o){if(er){let i=o.contentMatchAt(0),s=i.fillBefore(t).append(t);t=s.append(i.matchFragment(s).fillBefore(v.empty,!0))}return t}function Up(t,e,n,r){if(!r.isInline&&e==n&&t.doc.resolve(e).parent.content.size){let o=Vp(t.doc,e,r.type);o!=null&&(e=n=o)}t.replaceRange(e,n,new E(v.from(r),0,0))}function Kp(t,e,n){let r=t.doc.resolve(e),o=t.doc.resolve(n),i=qa(r,o);for(let s=0;s0&&(a||r.node(l-1).canReplace(r.index(l-1),o.indexAfter(l-1))))return t.delete(r.before(l),o.after(l))}for(let s=1;s<=r.depth&&s<=o.depth;s++)if(e-r.start(s)==r.depth-s&&n>r.end(s)&&o.end(s)-n!=o.depth-s&&r.start(s-1)==o.start(s-1)&&r.node(s-1).canReplace(r.index(s-1),o.index(s-1)))return t.delete(r.before(s),n);t.delete(e,n)}function qa(t,e){let n=[],r=Math.min(t.depth,e.depth);for(let o=r;o>=0;o--){let i=t.start(o);if(ie.pos+(e.depth-o)||t.node(o).type.spec.isolating||e.node(o).type.spec.isolating)break;(i==e.start(o)||o==t.depth&&o==e.depth&&t.parent.inlineContent&&e.parent.inlineContent&&o&&e.start(o-1)==i-1)&&n.push(o)}return n}var Lr=class t extends ce{constructor(e,n,r){super(),this.pos=e,this.attr=n,this.value=r}apply(e){let n=e.nodeAt(this.pos);if(!n)return ue.fail("No node at attribute step's position");let r=Object.create(null);for(let i in n.attrs)r[i]=n.attrs[i];r[this.attr]=this.value;let o=n.type.create(r,null,n.marks);return ue.fromReplace(e,this.pos,this.pos+1,new E(v.from(o),0,n.isLeaf?0:1))}getMap(){return dt.empty}invert(e){return new t(this.pos,this.attr,e.nodeAt(this.pos).attrs[this.attr])}map(e){let n=e.mapResult(this.pos,1);return n.deletedAfter?null:new t(n.pos,this.attr,this.value)}toJSON(){return{stepType:"attr",pos:this.pos,attr:this.attr,value:this.value}}static fromJSON(e,n){if(typeof n.pos!="number"||typeof n.attr!="string")throw new RangeError("Invalid input for AttrStep.fromJSON");return new t(n.pos,n.attr,n.value)}};ce.jsonID("attr",Lr);var Br=class t extends ce{constructor(e,n){super(),this.attr=e,this.value=n}apply(e){let n=Object.create(null);for(let o in e.attrs)n[o]=e.attrs[o];n[this.attr]=this.value;let r=e.type.create(n,e.content,e.marks);return ue.ok(r)}getMap(){return dt.empty}invert(e){return new t(this.attr,e.attrs[this.attr])}map(e){return this}toJSON(){return{stepType:"docAttr",attr:this.attr,value:this.value}}static fromJSON(e,n){if(typeof n.attr!="string")throw new RangeError("Invalid input for DocAttrStep.fromJSON");return new t(n.attr,n.value)}};ce.jsonID("docAttr",Br);var pn=class extends Error{};pn=function t(e){let n=Error.call(this,e);return n.__proto__=t.prototype,n};pn.prototype=Object.create(Error.prototype);pn.prototype.constructor=pn;pn.prototype.name="TransformError";var Tt=class{constructor(e){this.doc=e,this.steps=[],this.docs=[],this.mapping=new jn}get before(){return this.docs.length?this.docs[0]:this.doc}step(e){let n=this.maybeStep(e);if(n.failed)throw new pn(n.failed);return this}maybeStep(e){let n=e.apply(this.doc);return n.failed||this.addStep(e,n.doc),n}get docChanged(){return this.steps.length>0}addStep(e,n){this.docs.push(this.doc),this.steps.push(e),this.mapping.appendMap(e.getMap()),this.doc=n}replace(e,n=e,r=E.empty){let o=qn(this.doc,e,n,r);return o&&this.step(o),this}replaceWith(e,n,r){return this.replace(e,n,new E(v.from(r),0,0))}delete(e,n){return this.replace(e,n,E.empty)}insert(e,n){return this.replaceWith(e,e,n)}replaceRange(e,n,r){return jp(this,e,n,r),this}replaceRangeWith(e,n,r){return Up(this,e,n,r),this}deleteRange(e,n){return Kp(this,e,n),this}lift(e,n){return Rp(this,e,n),this}join(e,n=1){return Fp(this,e,n),this}wrap(e,n){return Pp(this,e,n),this}setBlockType(e,n=e,r,o=null){return Lp(this,e,n,r,o),this}setNodeMarkup(e,n,r=null,o){return zp(this,e,n,r,o),this}setNodeAttribute(e,n,r){return this.step(new Lr(e,n,r)),this}setDocAttribute(e,n){return this.step(new Br(e,n)),this}addNodeMark(e,n){return this.step(new Kn(e,n)),this}removeNodeMark(e,n){let r=this.doc.nodeAt(e);if(!r)throw new RangeError("No node at position "+e);if(n instanceof J)n.isInSet(r.marks)&&this.step(new hn(e,n));else{let o=r.marks,i,s=[];for(;i=n.isInSet(o);)s.push(new hn(e,i)),o=i.removeFromSet(o);for(let l=s.length-1;l>=0;l--)this.step(s[l])}return this}split(e,n=1,r){return Hp(this,e,n,r),this}addMark(e,n,r){return Ep(this,e,n,r),this}removeMark(e,n,r){return Np(this,e,n,r),this}clearIncompatible(e,n,r){return $i(this,e,n,r),this}};var Fi=Object.create(null),I=class{constructor(e,n,r){this.$anchor=e,this.$head=n,this.ranges=r||[new yn(e.min(n),e.max(n))]}get anchor(){return this.$anchor.pos}get head(){return this.$head.pos}get from(){return this.$from.pos}get to(){return this.$to.pos}get $from(){return this.ranges[0].$from}get $to(){return this.ranges[0].$to}get empty(){let e=this.ranges;for(let n=0;n=0;i--){let s=n<0?gn(e.node(0),e.node(i),e.before(i+1),e.index(i),n,r):gn(e.node(0),e.node(i),e.after(i+1),e.index(i)+1,n,r);if(s)return s}return null}static near(e,n=1){return this.findFrom(e,n)||this.findFrom(e,-n)||new ke(e.node(0))}static atStart(e){return gn(e,e,0,0,1)||new ke(e)}static atEnd(e){return gn(e,e,e.content.size,e.childCount,-1)||new ke(e)}static fromJSON(e,n){if(!n||!n.type)throw new RangeError("Invalid input for Selection.fromJSON");let r=Fi[n.type];if(!r)throw new RangeError(`No selection type ${n.type} defined`);return r.fromJSON(e,n)}static jsonID(e,n){if(e in Fi)throw new RangeError("Duplicate use of selection JSON ID "+e);return Fi[e]=n,n.prototype.jsonID=e,n}getBookmark(){return D.between(this.$anchor,this.$head).getBookmark()}};I.prototype.visible=!0;var yn=class{constructor(e,n){this.$from=e,this.$to=n}},Ja=!1;function Ga(t){!Ja&&!t.parent.inlineContent&&(Ja=!0,console.warn("TextSelection endpoint not pointing into a node with inline content ("+t.parent.type.name+")"))}var D=class t extends I{constructor(e,n=e){Ga(e),Ga(n),super(e,n)}get $cursor(){return this.$anchor.pos==this.$head.pos?this.$head:null}map(e,n){let r=e.resolve(n.map(this.head));if(!r.parent.inlineContent)return I.near(r);let o=e.resolve(n.map(this.anchor));return new t(o.parent.inlineContent?o:r,r)}replace(e,n=E.empty){if(super.replace(e,n),n==E.empty){let r=this.$from.marksAcross(this.$to);r&&e.ensureMarks(r)}}eq(e){return e instanceof t&&e.anchor==this.anchor&&e.head==this.head}getBookmark(){return new $r(this.anchor,this.head)}toJSON(){return{type:"text",anchor:this.anchor,head:this.head}}static fromJSON(e,n){if(typeof n.anchor!="number"||typeof n.head!="number")throw new RangeError("Invalid input for TextSelection.fromJSON");return new t(e.resolve(n.anchor),e.resolve(n.head))}static create(e,n,r=n){let o=e.resolve(n);return new this(o,r==n?o:e.resolve(r))}static between(e,n,r){let o=e.pos-n.pos;if((!r||o)&&(r=o>=0?1:-1),!n.parent.inlineContent){let i=I.findFrom(n,r,!0)||I.findFrom(n,-r,!0);if(i)n=i.$head;else return I.near(n,r)}return e.parent.inlineContent||(o==0?e=n:(e=(I.findFrom(e,-r,!0)||I.findFrom(e,r,!0)).$anchor,e.pos0?0:1);o>0?s=0;s+=o){let l=e.child(s);if(l.isAtom){if(!i&&L.isSelectable(l))return L.create(t,n-(o<0?l.nodeSize:0))}else{let a=gn(t,l,n+o,o<0?l.childCount:0,o,i);if(a)return a}n+=l.nodeSize*o}return null}function Xa(t,e,n){let r=t.steps.length-1;if(r{s==null&&(s=d)}),t.setSelection(I.near(t.doc.resolve(s),n))}var Ya=1,Hr=2,Qa=4,Wi=class extends Tt{constructor(e){super(e.doc),this.curSelectionFor=0,this.updated=0,this.meta=Object.create(null),this.time=Date.now(),this.curSelection=e.selection,this.storedMarks=e.storedMarks}get selection(){return this.curSelectionFor0}setStoredMarks(e){return this.storedMarks=e,this.updated|=Hr,this}ensureMarks(e){return J.sameSet(this.storedMarks||this.selection.$from.marks(),e)||this.setStoredMarks(e),this}addStoredMark(e){return this.ensureMarks(e.addToSet(this.storedMarks||this.selection.$head.marks()))}removeStoredMark(e){return this.ensureMarks(e.removeFromSet(this.storedMarks||this.selection.$head.marks()))}get storedMarksSet(){return(this.updated&Hr)>0}addStep(e,n){super.addStep(e,n),this.updated=this.updated&~Hr,this.storedMarks=null}setTime(e){return this.time=e,this}replaceSelection(e){return this.selection.replace(this,e),this}replaceSelectionWith(e,n=!0){let r=this.selection;return n&&(e=e.mark(this.storedMarks||(r.empty?r.$from.marks():r.$from.marksAcross(r.$to)||J.none))),r.replaceWith(this,e),this}deleteSelection(){return this.selection.replace(this),this}insertText(e,n,r){let o=this.doc.type.schema;if(n==null)return e?this.replaceSelectionWith(o.text(e),!0):this.deleteSelection();{if(r==null&&(r=n),!e)return this.deleteRange(n,r);let i=this.storedMarks;if(!i){let s=this.doc.resolve(n);i=r==n?s.marks():s.marksAcross(this.doc.resolve(r))}return this.replaceRangeWith(n,r,o.text(e,i)),!this.selection.empty&&this.selection.to==n+e.length&&this.setSelection(I.near(this.selection.$to)),this}}setMeta(e,n){return this.meta[typeof e=="string"?e:e.key]=n,this}getMeta(e){return this.meta[typeof e=="string"?e:e.key]}get isGeneric(){for(let e in this.meta)return!1;return!0}scrollIntoView(){return this.updated|=Qa,this}get scrolledIntoView(){return(this.updated&Qa)>0}};function Za(t,e){return!e||!t?t:t.bind(e)}var jt=class{constructor(e,n,r){this.name=e,this.init=Za(n.init,r),this.apply=Za(n.apply,r)}},Jp=[new jt("doc",{init(t){return t.doc||t.schema.topNodeType.createAndFill()},apply(t){return t.doc}}),new jt("selection",{init(t,e){return t.selection||I.atStart(e.doc)},apply(t){return t.selection}}),new jt("storedMarks",{init(t){return t.storedMarks||null},apply(t,e,n,r){return r.selection.$cursor?t.storedMarks:null}}),new jt("scrollToSelection",{init(){return 0},apply(t,e){return t.scrolledIntoView?e+1:e}})],Jn=class{constructor(e,n){this.schema=e,this.plugins=[],this.pluginsByKey=Object.create(null),this.fields=Jp.slice(),n&&n.forEach(r=>{if(this.pluginsByKey[r.key])throw new RangeError("Adding different instances of a keyed plugin ("+r.key+")");this.plugins.push(r),this.pluginsByKey[r.key]=r,r.spec.state&&this.fields.push(new jt(r.key,r.spec.state,r))})}},Fr=class t{constructor(e){this.config=e}get schema(){return this.config.schema}get plugins(){return this.config.plugins}apply(e){return this.applyTransaction(e).state}filterTransaction(e,n=-1){for(let r=0;rr.toJSON())),e&&typeof e=="object")for(let r in e){if(r=="doc"||r=="selection")throw new RangeError("The JSON fields `doc` and `selection` are reserved");let o=e[r],i=o.spec.state;i&&i.toJSON&&(n[r]=i.toJSON.call(o,this[o.key]))}return n}static fromJSON(e,n,r){if(!n)throw new RangeError("Invalid input for EditorState.fromJSON");if(!e.schema)throw new RangeError("Required config field 'schema' missing");let o=new Jn(e.schema,e.plugins),i=new t(o);return o.fields.forEach(s=>{if(s.name=="doc")i.doc=ie.fromJSON(e.schema,n.doc);else if(s.name=="selection")i.selection=I.fromJSON(i.doc,n.selection);else if(s.name=="storedMarks")n.storedMarks&&(i.storedMarks=n.storedMarks.map(e.schema.markFromJSON));else{if(r)for(let l in r){let a=r[l],c=a.spec.state;if(a.key==s.name&&c&&c.fromJSON&&Object.prototype.hasOwnProperty.call(n,l)){i[s.name]=c.fromJSON.call(a,e,n[l],i);return}}i[s.name]=s.init(e,i)}}),i}};function ec(t,e,n){for(let r in t){let o=t[r];o instanceof Function?o=o.bind(e):r=="handleDOMEvents"&&(o=ec(o,e,{})),n[r]=o}return n}var P=class{constructor(e){this.spec=e,this.props={},e.props&&ec(e.props,this,this.props),this.key=e.key?e.key.key:tc("plugin")}getState(e){return e[this.key]}},Vi=Object.create(null);function tc(t){return t in Vi?t+"$"+ ++Vi[t]:(Vi[t]=0,t+"$")}var H=class{constructor(e="key"){this.key=tc(e)}get(e){return e.config.pluginsByKey[this.key]}getState(e){return e[this.key]}};var Vr=(t,e)=>t.selection.empty?!1:(e&&e(t.tr.deleteSelection().scrollIntoView()),!0);function rc(t,e){let{$cursor:n}=t.selection;return!n||(e?!e.endOfTextblock("backward",t):n.parentOffset>0)?null:n}var Ui=(t,e,n)=>{let r=rc(t,n);if(!r)return!1;let o=qi(r);if(!o){let s=r.blockRange(),l=s&&ft(s);return l==null?!1:(e&&e(t.tr.lift(s,l).scrollIntoView()),!0)}let i=o.nodeBefore;if(fc(t,o,e,-1))return!0;if(r.parent.content.size==0&&(bn(i,"end")||L.isSelectable(i)))for(let s=r.depth;;s--){let l=qn(t.doc,r.before(s),r.after(s),E.empty);if(l&&l.slice.size1)break}return i.isAtom&&o.depth==r.depth-1?(e&&e(t.tr.delete(o.pos-i.nodeSize,o.pos).scrollIntoView()),!0):!1},oc=(t,e,n)=>{let r=rc(t,n);if(!r)return!1;let o=qi(r);return o?sc(t,o,e):!1},ic=(t,e,n)=>{let r=lc(t,n);if(!r)return!1;let o=Xi(r);return o?sc(t,o,e):!1};function sc(t,e,n){let r=e.nodeBefore,o=r,i=e.pos-1;for(;!o.isTextblock;i--){if(o.type.spec.isolating)return!1;let d=o.lastChild;if(!d)return!1;o=d}let s=e.nodeAfter,l=s,a=e.pos+1;for(;!l.isTextblock;a++){if(l.type.spec.isolating)return!1;let d=l.firstChild;if(!d)return!1;l=d}let c=qn(t.doc,i,a,E.empty);if(!c||c.from!=i||c instanceof ye&&c.slice.size>=a-i)return!1;if(n){let d=t.tr.step(c);d.setSelection(D.create(d.doc,i)),n(d.scrollIntoView())}return!0}function bn(t,e,n=!1){for(let r=t;r;r=e=="start"?r.firstChild:r.lastChild){if(r.isTextblock)return!0;if(n&&r.childCount!=1)return!1}return!1}var Ki=(t,e,n)=>{let{$head:r,empty:o}=t.selection,i=r;if(!o)return!1;if(r.parent.isTextblock){if(n?!n.endOfTextblock("backward",t):r.parentOffset>0)return!1;i=qi(r)}let s=i&&i.nodeBefore;return!s||!L.isSelectable(s)?!1:(e&&e(t.tr.setSelection(L.create(t.doc,i.pos-s.nodeSize)).scrollIntoView()),!0)};function qi(t){if(!t.parent.type.spec.isolating)for(let e=t.depth-1;e>=0;e--){if(t.index(e)>0)return t.doc.resolve(t.before(e+1));if(t.node(e).type.spec.isolating)break}return null}function lc(t,e){let{$cursor:n}=t.selection;return!n||(e?!e.endOfTextblock("forward",t):n.parentOffset{let r=lc(t,n);if(!r)return!1;let o=Xi(r);if(!o)return!1;let i=o.nodeAfter;if(fc(t,o,e,1))return!0;if(r.parent.content.size==0&&(bn(i,"start")||L.isSelectable(i))){let s=qn(t.doc,r.before(),r.after(),E.empty);if(s&&s.slice.size{let{$head:r,empty:o}=t.selection,i=r;if(!o)return!1;if(r.parent.isTextblock){if(n?!n.endOfTextblock("forward",t):r.parentOffset=0;e--){let n=t.node(e);if(t.index(e)+1{let n=t.selection,r=n instanceof L,o;if(r){if(n.node.isTextblock||!Re(t.doc,n.from))return!1;o=n.from}else if(o=Wt(t.doc,n.from,-1),o==null)return!1;if(e){let i=t.tr.join(o);r&&i.setSelection(L.create(i.doc,o-t.doc.resolve(o).nodeBefore.nodeSize)),e(i.scrollIntoView())}return!0},cc=(t,e)=>{let n=t.selection,r;if(n instanceof L){if(n.node.isTextblock||!Re(t.doc,n.to))return!1;r=n.to}else if(r=Wt(t.doc,n.to,1),r==null)return!1;return e&&e(t.tr.join(r).scrollIntoView()),!0},dc=(t,e)=>{let{$from:n,$to:r}=t.selection,o=n.blockRange(r),i=o&&ft(o);return i==null?!1:(e&&e(t.tr.lift(o,i).scrollIntoView()),!0)},Yi=(t,e)=>{let{$head:n,$anchor:r}=t.selection;return!n.parent.type.spec.code||!n.sameParent(r)?!1:(e&&e(t.tr.insertText(` +`).scrollIntoView()),!0)};function Qi(t){for(let e=0;e{let{$head:n,$anchor:r}=t.selection;if(!n.parent.type.spec.code||!n.sameParent(r))return!1;let o=n.node(-1),i=n.indexAfter(-1),s=Qi(o.contentMatchAt(i));if(!s||!o.canReplaceWith(i,i,s))return!1;if(e){let l=n.after(),a=t.tr.replaceWith(l,l,s.createAndFill());a.setSelection(I.near(a.doc.resolve(l),1)),e(a.scrollIntoView())}return!0},es=(t,e)=>{let n=t.selection,{$from:r,$to:o}=n;if(n instanceof ke||r.parent.inlineContent||o.parent.inlineContent)return!1;let i=Qi(o.parent.contentMatchAt(o.indexAfter()));if(!i||!i.isTextblock)return!1;if(e){let s=(!r.parentOffset&&o.index(){let{$cursor:n}=t.selection;if(!n||n.parent.content.size)return!1;if(n.depth>1&&n.after()!=n.end(-1)){let i=n.before();if(Ee(t.doc,i))return e&&e(t.tr.split(i).scrollIntoView()),!0}let r=n.blockRange(),o=r&&ft(r);return o==null?!1:(e&&e(t.tr.lift(r,o).scrollIntoView()),!0)};function Gp(t){return(e,n)=>{let{$from:r,$to:o}=e.selection;if(e.selection instanceof L&&e.selection.node.isBlock)return!r.parentOffset||!Ee(e.doc,r.pos)?!1:(n&&n(e.tr.split(r.pos).scrollIntoView()),!0);if(!r.depth)return!1;let i=[],s,l,a=!1,c=!1;for(let h=r.depth;;h--)if(r.node(h).isBlock){a=r.end(h)==r.pos+(r.depth-h),c=r.start(h)==r.pos-(r.depth-h),l=Qi(r.node(h-1).contentMatchAt(r.indexAfter(h-1)));let m=t&&t(o.parent,a,r);i.unshift(m||(a&&l?{type:l}:null)),s=h;break}else{if(h==1)return!1;i.unshift(null)}let d=e.tr;(e.selection instanceof D||e.selection instanceof ke)&&d.deleteSelection();let u=d.mapping.map(r.pos),f=Ee(d.doc,u,i.length,i);if(f||(i[0]=l?{type:l}:null,f=Ee(d.doc,u,i.length,i)),!f)return!1;if(d.split(u,i.length,i),!a&&c&&r.node(s).type!=l){let h=d.mapping.map(r.before(s)),p=d.doc.resolve(h);l&&r.node(s-1).canReplaceWith(p.index(),p.index()+1,l)&&d.setNodeMarkup(d.mapping.map(r.before(s)),l)}return n&&n(d.scrollIntoView()),!0}}var Xp=Gp();var uc=(t,e)=>{let{$from:n,to:r}=t.selection,o,i=n.sharedDepth(r);return i==0?!1:(o=n.before(i),e&&e(t.tr.setSelection(L.create(t.doc,o))),!0)},Yp=(t,e)=>(e&&e(t.tr.setSelection(new ke(t.doc))),!0);function Qp(t,e,n){let r=e.nodeBefore,o=e.nodeAfter,i=e.index();return!r||!o||!r.type.compatibleContent(o.type)?!1:!r.content.size&&e.parent.canReplace(i-1,i)?(n&&n(t.tr.delete(e.pos-r.nodeSize,e.pos).scrollIntoView()),!0):!e.parent.canReplace(i,i+1)||!(o.isTextblock||Re(t.doc,e.pos))?!1:(n&&n(t.tr.join(e.pos).scrollIntoView()),!0)}function fc(t,e,n,r){let o=e.nodeBefore,i=e.nodeAfter,s,l,a=o.type.spec.isolating||i.type.spec.isolating;if(!a&&Qp(t,e,n))return!0;let c=!a&&e.parent.canReplace(e.index(),e.index()+1);if(c&&(s=(l=o.contentMatchAt(o.childCount)).findWrapping(i.type))&&l.matchType(s[0]||i.type).validEnd){if(n){let h=e.pos+i.nodeSize,p=v.empty;for(let y=s.length-1;y>=0;y--)p=v.from(s[y].create(null,p));p=v.from(o.copy(p));let m=t.tr.step(new se(e.pos-1,h,e.pos,h,new E(p,1,0),s.length,!0)),g=m.doc.resolve(h+2*s.length);g.nodeAfter&&g.nodeAfter.type==o.type&&Re(m.doc,g.pos)&&m.join(g.pos),n(m.scrollIntoView())}return!0}let d=i.type.spec.isolating||r>0&&a?null:I.findFrom(e,1),u=d&&d.$from.blockRange(d.$to),f=u&&ft(u);if(f!=null&&f>=e.depth)return n&&n(t.tr.lift(u,f).scrollIntoView()),!0;if(c&&bn(i,"start",!0)&&bn(o,"end")){let h=o,p=[];for(;p.push(h),!h.isTextblock;)h=h.lastChild;let m=i,g=1;for(;!m.isTextblock;m=m.firstChild)g++;if(h.canReplace(h.childCount,h.childCount,m.content)){if(n){let y=v.empty;for(let b=p.length-1;b>=0;b--)y=v.from(p[b].copy(y));let w=t.tr.step(new se(e.pos-p.length,e.pos+i.nodeSize,e.pos+g,e.pos+i.nodeSize-g,new E(y,p.length,0),0,!0));n(w.scrollIntoView())}return!0}}return!1}function hc(t){return function(e,n){let r=e.selection,o=t<0?r.$from:r.$to,i=o.depth;for(;o.node(i).isInline;){if(!i)return!1;i--}return o.node(i).isTextblock?(n&&n(e.tr.setSelection(D.create(e.doc,t<0?o.start(i):o.end(i)))),!0):!1}}var ns=hc(-1),rs=hc(1);function pc(t,e=null){return function(n,r){let{$from:o,$to:i}=n.selection,s=o.blockRange(i),l=s&&mn(s,t,e);return l?(r&&r(n.tr.wrap(s,l).scrollIntoView()),!0):!1}}function is(t,e=null){return function(n,r){let o=!1;for(let i=0;i{if(o)return!1;if(!(!a.isTextblock||a.hasMarkup(t,e)))if(a.type==t)o=!0;else{let d=n.doc.resolve(c),u=d.index();o=d.parent.canReplaceWith(u,u+1,t)}})}if(!o)return!1;if(r){let i=n.tr;for(let s=0;s=2&&e.$from.node(e.depth-1).type.compatibleContent(n)&&e.startIndex==0){if(e.$from.index(e.depth-1)==0)return!1;let a=s.resolve(e.start-2);i=new Vt(a,a,e.depth),e.endIndex=0;d--)i=v.from(n[d].type.create(n[d].attrs,i));t.step(new se(e.start-(r?2:0),e.end,e.start,e.end,new E(i,0,0),n.length,!0));let s=0;for(let d=0;ds.childCount>0&&s.firstChild.type==t);return i?n?r.node(i.depth-1).type==t?nm(e,n,t,i):rm(e,n,i):!0:!1}}function nm(t,e,n,r){let o=t.tr,i=r.end,s=r.$to.end(r.depth);im;p--)h-=o.child(p).nodeSize,r.delete(h-1,h+1);let i=r.doc.resolve(n.start),s=i.nodeAfter;if(r.mapping.map(n.end)!=n.start+i.nodeAfter.nodeSize)return!1;let l=n.startIndex==0,a=n.endIndex==o.childCount,c=i.node(-1),d=i.index(-1);if(!c.canReplace(d+(l?0:1),d+1,s.content.append(a?v.empty:v.from(o))))return!1;let u=i.pos,f=u+s.nodeSize;return r.step(new se(u-(l?1:0),f+(a?1:0),u+1,f-1,new E((l?v.empty:v.from(o.copy(v.empty))).append(a?v.empty:v.from(o.copy(v.empty))),l?0:1,a?0:1),l?0:1)),e(r.scrollIntoView()),!0}function yc(t){return function(e,n){let{$from:r,$to:o}=e.selection,i=r.blockRange(o,c=>c.childCount>0&&c.firstChild.type==t);if(!i)return!1;let s=i.startIndex;if(s==0)return!1;let l=i.parent,a=l.child(s-1);if(a.type!=t)return!1;if(n){let c=a.lastChild&&a.lastChild.type==l.type,d=v.from(c?t.create():null),u=new E(v.from(t.create(null,v.from(l.type.create(null,d)))),c?3:1,0),f=i.start,h=i.end;n(e.tr.step(new se(f-(c?3:1),h,f,h,u,1,!0)).scrollIntoView())}return!0}}var fe=function(t){for(var e=0;;e++)if(t=t.previousSibling,!t)return e},Cn=function(t){let e=t.assignedSlot||t.parentNode;return e&&e.nodeType==11?e.host:e},fs=null,pt=function(t,e,n){let r=fs||(fs=document.createRange());return r.setEnd(t,n??t.nodeValue.length),r.setStart(t,e||0),r},om=function(){fs=null},Yt=function(t,e,n,r){return n&&(bc(t,e,n,r,-1)||bc(t,e,n,r,1))},im=/^(img|br|input|textarea|hr)$/i;function bc(t,e,n,r,o){for(var i;;){if(t==n&&e==r)return!0;if(e==(o<0?0:Ie(t))){let s=t.parentNode;if(!s||s.nodeType!=1||nr(t)||im.test(t.nodeName)||t.contentEditable=="false")return!1;e=fe(t)+(o<0?0:1),t=s}else if(t.nodeType==1){let s=t.childNodes[e+(o<0?-1:0)];if(s.nodeType==1&&s.contentEditable=="false")if(!((i=s.pmViewDesc)===null||i===void 0)&&i.ignoreForSelection)e+=o;else return!1;else t=s,e=o<0?Ie(t):0}else return!1}}function Ie(t){return t.nodeType==3?t.nodeValue.length:t.childNodes.length}function sm(t,e){for(;;){if(t.nodeType==3&&e)return t;if(t.nodeType==1&&e>0){if(t.contentEditable=="false")return null;t=t.childNodes[e-1],e=Ie(t)}else if(t.parentNode&&!nr(t))e=fe(t),t=t.parentNode;else return null}}function lm(t,e){for(;;){if(t.nodeType==3&&e2),De=vn||(Ye?/Mac/.test(Ye.platform):!1),Zc=Ye?/Win/.test(Ye.platform):!1,mt=/Android \d/.test(It),rr=!!wc&&"webkitFontSmoothing"in wc.documentElement.style,um=rr?+(/\bAppleWebKit\/(\d+)/.exec(navigator.userAgent)||[0,0])[1]:0;function fm(t){let e=t.defaultView&&t.defaultView.visualViewport;return e?{left:0,right:e.width,top:0,bottom:e.height}:{left:0,right:t.documentElement.clientWidth,top:0,bottom:t.documentElement.clientHeight}}function ht(t,e){return typeof t=="number"?t:t[e]}function hm(t){let e=t.getBoundingClientRect(),n=e.width/t.offsetWidth||1,r=e.height/t.offsetHeight||1;return{left:e.left,right:e.left+t.clientWidth*n,top:e.top,bottom:e.top+t.clientHeight*r}}function xc(t,e,n){let r=t.someProp("scrollThreshold")||0,o=t.someProp("scrollMargin")||5,i=t.dom.ownerDocument;for(let s=n||t.dom;s;){if(s.nodeType!=1){s=Cn(s);continue}let l=s,a=l==i.body,c=a?fm(i):hm(l),d=0,u=0;if(e.topc.bottom-ht(r,"bottom")&&(u=e.bottom-e.top>c.bottom-c.top?e.top+ht(o,"top")-c.top:e.bottom-c.bottom+ht(o,"bottom")),e.leftc.right-ht(r,"right")&&(d=e.right-c.right+ht(o,"right")),d||u)if(a)i.defaultView.scrollBy(d,u);else{let h=l.scrollLeft,p=l.scrollTop;u&&(l.scrollTop+=u),d&&(l.scrollLeft+=d);let m=l.scrollLeft-h,g=l.scrollTop-p;e={left:e.left-m,top:e.top-g,right:e.right-m,bottom:e.bottom-g}}let f=a?"fixed":getComputedStyle(s).position;if(/^(fixed|sticky)$/.test(f))break;s=f=="absolute"?s.offsetParent:Cn(s)}}function pm(t){let e=t.dom.getBoundingClientRect(),n=Math.max(0,e.top),r,o;for(let i=(e.left+e.right)/2,s=n+1;s=n-20){r=l,o=a.top;break}}return{refDOM:r,refTop:o,stack:ed(t.dom)}}function ed(t){let e=[],n=t.ownerDocument;for(let r=t;r&&(e.push({dom:r,top:r.scrollTop,left:r.scrollLeft}),t!=n);r=Cn(r));return e}function mm({refDOM:t,refTop:e,stack:n}){let r=t?t.getBoundingClientRect().top:0;td(n,r==0?0:r-e)}function td(t,e){for(let n=0;n=l){s=Math.max(p.bottom,s),l=Math.min(p.top,l);let m=p.left>e.left?p.left-e.left:p.right=(p.left+p.right)/2?1:0));continue}}else p.top>e.top&&!a&&p.left<=e.left&&p.right>=e.left&&(a=d,c={left:Math.max(p.left,Math.min(p.right,e.left)),top:p.top});!n&&(e.left>=p.right&&e.top>=p.top||e.left>=p.left&&e.top>=p.bottom)&&(i=u+1)}}return!n&&a&&(n=a,o=c,r=0),n&&n.nodeType==3?ym(n,o):!n||r&&n.nodeType==1?{node:t,offset:i}:nd(n,o)}function ym(t,e){let n=t.nodeValue.length,r=document.createRange(),o;for(let i=0;i=(s.left+s.right)/2?1:0)};break}}return r.detach(),o||{node:t,offset:0}}function Os(t,e){return t.left>=e.left-1&&t.left<=e.right+1&&t.top>=e.top-1&&t.top<=e.bottom+1}function bm(t,e){let n=t.parentNode;return n&&/^li$/i.test(n.nodeName)&&e.left(s.left+s.right)/2?1:-1}return t.docView.posFromDOM(r,o,i)}function xm(t,e,n,r){let o=-1;for(let i=e,s=!1;i!=t.dom;){let l=t.docView.nearestDesc(i,!0),a;if(!l)return null;if(l.dom.nodeType==1&&(l.node.isBlock&&l.parent||!l.contentDOM)&&((a=l.dom.getBoundingClientRect()).width||a.height)&&(l.node.isBlock&&l.parent&&!/^T(R|BODY|HEAD|FOOT)$/.test(l.dom.nodeName)&&(!s&&a.left>r.left||a.top>r.top?o=l.posBefore:(!s&&a.right-1?o:t.docView.posFromDOM(e,n,-1)}function rd(t,e,n){let r=t.childNodes.length;if(r&&n.tope.top&&o++}let c;rr&&o&&r.nodeType==1&&(c=r.childNodes[o-1]).nodeType==1&&c.contentEditable=="false"&&c.getBoundingClientRect().top>=e.top&&o--,r==t.dom&&o==r.childNodes.length-1&&r.lastChild.nodeType==1&&e.top>r.lastChild.getBoundingClientRect().bottom?l=t.state.doc.content.size:(o==0||r.nodeType!=1||r.childNodes[o-1].nodeName!="BR")&&(l=xm(t,r,o,e))}l==null&&(l=wm(t,s,e));let a=t.docView.nearestDesc(s,!0);return{pos:l,inside:a?a.posAtStart-a.border:-1}}function kc(t){return t.top=0&&o==r.nodeValue.length?(a--,d=1):n<0?a--:c++,Gn(Et(pt(r,a,c),d),d<0)}if(!t.state.doc.resolve(e-(i||0)).parent.inlineContent){if(i==null&&o&&(n<0||o==Ie(r))){let a=r.childNodes[o-1];if(a.nodeType==1)return ls(a.getBoundingClientRect(),!1)}if(i==null&&o=0)}if(i==null&&o&&(n<0||o==Ie(r))){let a=r.childNodes[o-1],c=a.nodeType==3?pt(a,Ie(a)-(s?0:1)):a.nodeType==1&&(a.nodeName!="BR"||!a.nextSibling)?a:null;if(c)return Gn(Et(c,1),!1)}if(i==null&&o=0)}function Gn(t,e){if(t.width==0)return t;let n=e?t.left:t.right;return{top:t.top,bottom:t.bottom,left:n,right:n}}function ls(t,e){if(t.height==0)return t;let n=e?t.top:t.bottom;return{top:n,bottom:n,left:t.left,right:t.right}}function id(t,e,n){let r=t.state,o=t.root.activeElement;r!=e&&t.updateState(e),o!=t.dom&&t.focus();try{return n()}finally{r!=e&&t.updateState(r),o!=t.dom&&o&&o.focus()}}function Cm(t,e,n){let r=e.selection,o=n=="up"?r.$from:r.$to;return id(t,e,()=>{let{node:i}=t.docView.domFromPos(o.pos,n=="up"?-1:1);for(;;){let l=t.docView.nearestDesc(i,!0);if(!l)break;if(l.node.isBlock){i=l.contentDOM||l.dom;break}i=l.dom.parentNode}let s=od(t,o.pos,1);for(let l=i.firstChild;l;l=l.nextSibling){let a;if(l.nodeType==1)a=l.getClientRects();else if(l.nodeType==3)a=pt(l,0,l.nodeValue.length).getClientRects();else continue;for(let c=0;cd.top+1&&(n=="up"?s.top-d.top>(d.bottom-s.top)*2:d.bottom-s.bottom>(s.bottom-d.top)*2))return!1}}return!0})}var vm=/[\u0590-\u08ac]/;function Mm(t,e,n){let{$head:r}=e.selection;if(!r.parent.isTextblock)return!1;let o=r.parentOffset,i=!o,s=o==r.parent.content.size,l=t.domSelection();return l?!vm.test(r.parent.textContent)||!l.modify?n=="left"||n=="backward"?i:s:id(t,e,()=>{let{focusNode:a,focusOffset:c,anchorNode:d,anchorOffset:u}=t.domSelectionRange(),f=l.caretBidiLevel;l.modify("move",n,"character");let h=r.depth?t.docView.domAfterPos(r.before()):t.dom,{focusNode:p,focusOffset:m}=t.domSelectionRange(),g=p&&!h.contains(p.nodeType==1?p:p.parentNode)||a==p&&c==m;try{l.collapse(d,u),a&&(a!=d||c!=u)&&l.extend&&l.extend(a,c)}catch{}return f!=null&&(l.caretBidiLevel=f),g}):r.pos==r.start()||r.pos==r.end()}var Sc=null,Cc=null,vc=!1;function Tm(t,e,n){return Sc==e&&Cc==n?vc:(Sc=e,Cc=n,vc=n=="up"||n=="down"?Cm(t,e,n):Mm(t,e,n))}var Le=0,Mc=1,Kt=2,Qe=3,Qt=class{constructor(e,n,r,o){this.parent=e,this.children=n,this.dom=r,this.contentDOM=o,this.dirty=Le,r.pmViewDesc=this}matchesWidget(e){return!1}matchesMark(e){return!1}matchesNode(e,n,r){return!1}matchesHack(e){return!1}parseRule(){return null}stopEvent(e){return!1}get size(){let e=0;for(let n=0;nfe(this.contentDOM);else if(this.contentDOM&&this.contentDOM!=this.dom&&this.dom.contains(this.contentDOM))o=e.compareDocumentPosition(this.contentDOM)&2;else if(this.dom.firstChild){if(n==0)for(let i=e;;i=i.parentNode){if(i==this.dom){o=!1;break}if(i.previousSibling)break}if(o==null&&n==e.childNodes.length)for(let i=e;;i=i.parentNode){if(i==this.dom){o=!0;break}if(i.nextSibling)break}}return o??r>0?this.posAtEnd:this.posAtStart}nearestDesc(e,n=!1){for(let r=!0,o=e;o;o=o.parentNode){let i=this.getDesc(o),s;if(i&&(!n||i.node))if(r&&(s=i.nodeDOM)&&!(s.nodeType==1?s.contains(e.nodeType==1?e:e.parentNode):s==e))r=!1;else return i}}getDesc(e){let n=e.pmViewDesc;for(let r=n;r;r=r.parent)if(r==this)return n}posFromDOM(e,n,r){for(let o=e;o;o=o.parentNode){let i=this.getDesc(o);if(i)return i.localPosFromDOM(e,n,r)}return-1}descAt(e){for(let n=0,r=0;ne||s instanceof jr){o=e-i;break}i=l}if(o)return this.children[r].domFromPos(o-this.children[r].border,n);for(let i;r&&!(i=this.children[r-1]).size&&i instanceof _r&&i.side>=0;r--);if(n<=0){let i,s=!0;for(;i=r?this.children[r-1]:null,!(!i||i.dom.parentNode==this.contentDOM);r--,s=!1);return i&&n&&s&&!i.border&&!i.domAtom?i.domFromPos(i.size,n):{node:this.contentDOM,offset:i?fe(i.dom)+1:0}}else{let i,s=!0;for(;i=r=d&&n<=c-a.border&&a.node&&a.contentDOM&&this.contentDOM.contains(a.contentDOM))return a.parseRange(e,n,d);e=s;for(let u=l;u>0;u--){let f=this.children[u-1];if(f.size&&f.dom.parentNode==this.contentDOM&&!f.emptyChildAt(1)){o=fe(f.dom)+1;break}e-=f.size}o==-1&&(o=0)}if(o>-1&&(c>n||l==this.children.length-1)){n=c;for(let d=l+1;dp&&sn){let p=l;l=a,a=p}let h=document.createRange();h.setEnd(a.node,a.offset),h.setStart(l.node,l.offset),c.removeAllRanges(),c.addRange(h)}}ignoreMutation(e){return!this.contentDOM&&e.type!="selection"}get contentLost(){return this.contentDOM&&this.contentDOM!=this.dom&&!this.dom.contains(this.contentDOM)}markDirty(e,n){for(let r=0,o=0;o=r:er){let l=r+i.border,a=s-i.border;if(e>=l&&n<=a){this.dirty=e==r||n==s?Kt:Mc,e==l&&n==a&&(i.contentLost||i.dom.parentNode!=this.contentDOM)?i.dirty=Qe:i.markDirty(e-l,n-l);return}else i.dirty=i.dom==i.contentDOM&&i.dom.parentNode==this.contentDOM&&!i.children.length?Kt:Qe}r=s}this.dirty=Kt}markParentsDirty(){let e=1;for(let n=this.parent;n;n=n.parent,e++){let r=e==1?Kt:Mc;n.dirty{if(!i)return o;if(i.parent)return i.parent.posBeforeChild(i)})),!n.type.spec.raw){if(s.nodeType!=1){let l=document.createElement("span");l.appendChild(s),s=l}s.contentEditable="false",s.classList.add("ProseMirror-widget")}super(e,[],s,null),this.widget=n,this.widget=n,i=this}matchesWidget(e){return this.dirty==Le&&e.type.eq(this.widget.type)}parseRule(){return{ignore:!0}}stopEvent(e){let n=this.widget.spec.stopEvent;return n?n(e):!1}ignoreMutation(e){return e.type!="selection"||this.widget.spec.ignoreSelection}destroy(){this.widget.type.destroy(this.dom),super.destroy()}get domAtom(){return!0}get ignoreForSelection(){return!!this.widget.type.spec.relaxedSide}get side(){return this.widget.type.side}},gs=class extends Qt{constructor(e,n,r,o){super(e,[],n,null),this.textDOM=r,this.text=o}get size(){return this.text.length}localPosFromDOM(e,n){return e!=this.textDOM?this.posAtStart+(n?this.size:0):this.posAtStart+n}domFromPos(e){return{node:this.textDOM,offset:e}}ignoreMutation(e){return e.type==="characterData"&&e.target.nodeValue==e.oldValue}},Mn=class t extends Qt{constructor(e,n,r,o,i){super(e,[],r,o),this.mark=n,this.spec=i}static create(e,n,r,o){let i=o.nodeViews[n.type.name],s=i&&i(n,o,r);return(!s||!s.dom)&&(s=ct.renderSpec(document,n.type.spec.toDOM(n,r),null,n.attrs)),new t(e,n,s.dom,s.contentDOM||s.dom,s)}parseRule(){return this.dirty&Qe||this.mark.type.spec.reparseInView?null:{mark:this.mark.type.name,attrs:this.mark.attrs,contentElement:this.contentDOM}}matchesMark(e){return this.dirty!=Qe&&this.mark.eq(e)}markDirty(e,n){if(super.markDirty(e,n),this.dirty!=Le){let r=this.parent;for(;!r.node;)r=r.parent;r.dirty0&&(i=xs(i,0,e,r));for(let l=0;l{if(!a)return s;if(a.parent)return a.parent.posBeforeChild(a)},r,o),d=c&&c.dom,u=c&&c.contentDOM;if(n.isText){if(!d)d=document.createTextNode(n.text);else if(d.nodeType!=3)throw new RangeError("Text must be rendered as a DOM text node")}else d||({dom:d,contentDOM:u}=ct.renderSpec(document,n.type.spec.toDOM(n),null,n.attrs));!u&&!n.isText&&d.nodeName!="BR"&&(d.hasAttribute("contenteditable")||(d.contentEditable="false"),n.type.spec.draggable&&(d.draggable=!0));let f=d;return d=ad(d,r,n),c?a=new ys(e,n,r,o,d,u||null,f,c,i,s+1):n.isText?new Wr(e,n,r,o,d,f,i):new t(e,n,r,o,d,u||null,f,i,s+1)}parseRule(){if(this.node.type.spec.reparseInView)return null;let e={node:this.node.type.name,attrs:this.node.attrs};if(this.node.type.whitespace=="pre"&&(e.preserveWhitespace="full"),!this.contentDOM)e.getContent=()=>this.node.content;else if(!this.contentLost)e.contentElement=this.contentDOM;else{for(let n=this.children.length-1;n>=0;n--){let r=this.children[n];if(this.dom.contains(r.dom.parentNode)){e.contentElement=r.dom.parentNode;break}}e.contentElement||(e.getContent=()=>v.empty)}return e}matchesNode(e,n,r){return this.dirty==Le&&e.eq(this.node)&&Ur(n,this.outerDeco)&&r.eq(this.innerDeco)}get size(){return this.node.nodeSize}get border(){return this.node.isLeaf?0:1}updateChildren(e,n){let r=this.node.inlineContent,o=n,i=e.composing?this.localCompositionInfo(e,n):null,s=i&&i.pos>-1?i:null,l=i&&i.pos<0,a=new ws(this,s&&s.node,e);Om(this.node,this.innerDeco,(c,d,u)=>{c.spec.marks?a.syncToMarks(c.spec.marks,r,e):c.type.side>=0&&!u&&a.syncToMarks(d==this.node.childCount?J.none:this.node.child(d).marks,r,e),a.placeWidget(c,e,o)},(c,d,u,f)=>{a.syncToMarks(c.marks,r,e);let h;a.findNodeMatch(c,d,u,f)||l&&e.state.selection.from>o&&e.state.selection.to-1&&a.updateNodeAt(c,d,u,h,e)||a.updateNextNode(c,d,u,e,f,o)||a.addNode(c,d,u,e,o),o+=c.nodeSize}),a.syncToMarks([],r,e),this.node.isTextblock&&a.addTextblockHacks(),a.destroyRest(),(a.changed||this.dirty==Kt)&&(s&&this.protectLocalComposition(e,s),sd(this.contentDOM,this.children,e),vn&&Rm(this.dom))}localCompositionInfo(e,n){let{from:r,to:o}=e.state.selection;if(!(e.state.selection instanceof D)||rn+this.node.content.size)return null;let i=e.input.compositionNode;if(!i||!this.dom.contains(i.parentNode))return null;if(this.node.inlineContent){let s=i.nodeValue,l=Dm(this.node.content,s,r-n,o-n);return l<0?null:{node:i,pos:l,text:s}}else return{node:i,pos:-1,text:""}}protectLocalComposition(e,{node:n,pos:r,text:o}){if(this.getDesc(n))return;let i=n;for(;i.parentNode!=this.contentDOM;i=i.parentNode){for(;i.previousSibling;)i.parentNode.removeChild(i.previousSibling);for(;i.nextSibling;)i.parentNode.removeChild(i.nextSibling);i.pmViewDesc&&(i.pmViewDesc=void 0)}let s=new gs(this,i,n,o);e.input.compositionNodes.push(s),this.children=xs(this.children,r,r+o.length,e,s)}update(e,n,r,o){return this.dirty==Qe||!e.sameMarkup(this.node)?!1:(this.updateInner(e,n,r,o),!0)}updateInner(e,n,r,o){this.updateOuterDeco(n),this.node=e,this.innerDeco=r,this.contentDOM&&this.updateChildren(o,this.posAtStart),this.dirty=Le}updateOuterDeco(e){if(Ur(e,this.outerDeco))return;let n=this.nodeDOM.nodeType!=1,r=this.dom;this.dom=ld(this.dom,this.nodeDOM,bs(this.outerDeco,this.node,n),bs(e,this.node,n)),this.dom!=r&&(r.pmViewDesc=void 0,this.dom.pmViewDesc=this),this.outerDeco=e}selectNode(){this.nodeDOM.nodeType==1&&(this.nodeDOM.classList.add("ProseMirror-selectednode"),(this.contentDOM||!this.node.type.spec.draggable)&&(this.nodeDOM.draggable=!0))}deselectNode(){this.nodeDOM.nodeType==1&&(this.nodeDOM.classList.remove("ProseMirror-selectednode"),(this.contentDOM||!this.node.type.spec.draggable)&&this.nodeDOM.removeAttribute("draggable"))}get domAtom(){return this.node.isAtom}};function Tc(t,e,n,r,o){ad(r,e,t);let i=new Dt(void 0,t,e,n,r,r,r,o,0);return i.contentDOM&&i.updateChildren(o,0),i}var Wr=class t extends Dt{constructor(e,n,r,o,i,s,l){super(e,n,r,o,i,null,s,l,0)}parseRule(){let e=this.nodeDOM.parentNode;for(;e&&e!=this.dom&&!e.pmIsDeco;)e=e.parentNode;return{skip:e||!0}}update(e,n,r,o){return this.dirty==Qe||this.dirty!=Le&&!this.inParent()||!e.sameMarkup(this.node)?!1:(this.updateOuterDeco(n),(this.dirty!=Le||e.text!=this.node.text)&&e.text!=this.nodeDOM.nodeValue&&(this.nodeDOM.nodeValue=e.text,o.trackWrites==this.nodeDOM&&(o.trackWrites=null)),this.node=e,this.dirty=Le,!0)}inParent(){let e=this.parent.contentDOM;for(let n=this.nodeDOM;n;n=n.parentNode)if(n==e)return!0;return!1}domFromPos(e){return{node:this.nodeDOM,offset:e}}localPosFromDOM(e,n,r){return e==this.nodeDOM?this.posAtStart+Math.min(n,this.node.text.length):super.localPosFromDOM(e,n,r)}ignoreMutation(e){return e.type!="characterData"&&e.type!="selection"}slice(e,n,r){let o=this.node.cut(e,n),i=document.createTextNode(o.text);return new t(this.parent,o,this.outerDeco,this.innerDeco,i,i,r)}markDirty(e,n){super.markDirty(e,n),this.dom!=this.nodeDOM&&(e==0||n==this.nodeDOM.nodeValue.length)&&(this.dirty=Qe)}get domAtom(){return!1}isText(e){return this.node.text==e}},jr=class extends Qt{parseRule(){return{ignore:!0}}matchesHack(e){return this.dirty==Le&&this.dom.nodeName==e}get domAtom(){return!0}get ignoreForCoords(){return this.dom.nodeName=="IMG"}},ys=class extends Dt{constructor(e,n,r,o,i,s,l,a,c,d){super(e,n,r,o,i,s,l,c,d),this.spec=a}update(e,n,r,o){if(this.dirty==Qe)return!1;if(this.spec.update&&(this.node.type==e.type||this.spec.multiType)){let i=this.spec.update(e,n,r);return i&&this.updateInner(e,n,r,o),i}else return!this.contentDOM&&!e.isLeaf?!1:super.update(e,n,r,o)}selectNode(){this.spec.selectNode?this.spec.selectNode():super.selectNode()}deselectNode(){this.spec.deselectNode?this.spec.deselectNode():super.deselectNode()}setSelection(e,n,r,o){this.spec.setSelection?this.spec.setSelection(e,n,r.root):super.setSelection(e,n,r,o)}destroy(){this.spec.destroy&&this.spec.destroy(),super.destroy()}stopEvent(e){return this.spec.stopEvent?this.spec.stopEvent(e):!1}ignoreMutation(e){return this.spec.ignoreMutation?this.spec.ignoreMutation(e):super.ignoreMutation(e)}};function sd(t,e,n){let r=t.firstChild,o=!1;for(let i=0;i>1,s=Math.min(i,e.length);for(;o-1)l>this.index&&(this.changed=!0,this.destroyBetween(this.index,l)),this.top=this.top.children[this.index];else{let a=Mn.create(this.top,e[i],n,r);this.top.children.splice(this.index,0,a),this.top=a,this.changed=!0}this.index=0,i++}}findNodeMatch(e,n,r,o){let i=-1,s;if(o>=this.preMatch.index&&(s=this.preMatch.matches[o-this.preMatch.index]).parent==this.top&&s.matchesNode(e,n,r))i=this.top.children.indexOf(s,this.index);else for(let l=this.index,a=Math.min(this.top.children.length,l+5);l0;){let l;for(;;)if(r){let c=n.children[r-1];if(c instanceof Mn)n=c,r=c.children.length;else{l=c,r--;break}}else{if(n==e)break e;r=n.parent.children.indexOf(n),n=n.parent}let a=l.node;if(a){if(a!=t.child(o-1))break;--o,i.set(l,o),s.push(l)}}return{index:o,matched:i,matches:s.reverse()}}function Nm(t,e){return t.type.side-e.type.side}function Om(t,e,n,r){let o=e.locals(t),i=0;if(o.length==0){for(let c=0;ci;)l.push(o[s++]);let p=i+f.nodeSize;if(f.isText){let g=p;s!g.inline):l.slice();r(f,m,e.forChild(i,f),h),i=p}}function Rm(t){if(t.nodeName=="UL"||t.nodeName=="OL"){let e=t.style.cssText;t.style.cssText=e+"; list-style: square !important",window.getComputedStyle(t).listStyle,t.style.cssText=e}}function Dm(t,e,n,r){for(let o=0,i=0;o=n){if(i>=r&&a.slice(r-e.length-l,r-l)==e)return r-e.length;let c=l=0&&c+e.length+l>=n)return l+c;if(n==r&&a.length>=r+e.length-l&&a.slice(r-l,r-l+e.length)==e)return r}}return-1}function xs(t,e,n,r,o){let i=[];for(let s=0,l=0;s=n||d<=e?i.push(a):(cn&&i.push(a.slice(n-c,a.size,r)))}return i}function Rs(t,e=null){let n=t.domSelectionRange(),r=t.state.doc;if(!n.focusNode)return null;let o=t.docView.nearestDesc(n.focusNode),i=o&&o.size==0,s=t.docView.posFromDOM(n.focusNode,n.focusOffset,1);if(s<0)return null;let l=r.resolve(s),a,c;if(Qr(n)){for(a=s;o&&!o.node;)o=o.parent;let u=o.node;if(o&&u.isAtom&&L.isSelectable(u)&&o.parent&&!(u.isInline&&am(n.focusNode,n.focusOffset,o.dom))){let f=o.posBefore;c=new L(s==f?l:r.resolve(f))}}else{if(n instanceof t.dom.ownerDocument.defaultView.Selection&&n.rangeCount>1){let u=s,f=s;for(let h=0;h{(n.anchorNode!=r||n.anchorOffset!=o)&&(e.removeEventListener("selectionchange",t.input.hideSelectionGuard),setTimeout(()=>{(!cd(t)||t.state.selection.visible)&&t.dom.classList.remove("ProseMirror-hideselection")},20))})}function Pm(t){let e=t.domSelection();if(!e)return;let n=t.cursorWrapper.dom,r=n.nodeName=="IMG";r?e.collapse(n.parentNode,fe(n)+1):e.collapse(n,0),!r&&!t.state.selection.visible&&ve&&Rt<=11&&(n.disabled=!0,n.disabled=!1)}function dd(t,e){if(e instanceof L){let n=t.docView.descAt(e.from);n!=t.lastSelectedViewDesc&&(Rc(t),n&&n.selectNode(),t.lastSelectedViewDesc=n)}else Rc(t)}function Rc(t){t.lastSelectedViewDesc&&(t.lastSelectedViewDesc.parent&&t.lastSelectedViewDesc.deselectNode(),t.lastSelectedViewDesc=void 0)}function Ds(t,e,n,r){return t.someProp("createSelectionBetween",o=>o(t,e,n))||D.between(e,n,r)}function Dc(t){return t.editable&&!t.hasFocus()?!1:ud(t)}function ud(t){let e=t.domSelectionRange();if(!e.anchorNode)return!1;try{return t.dom.contains(e.anchorNode.nodeType==3?e.anchorNode.parentNode:e.anchorNode)&&(t.editable||t.dom.contains(e.focusNode.nodeType==3?e.focusNode.parentNode:e.focusNode))}catch{return!1}}function Lm(t){let e=t.docView.domFromPos(t.state.selection.anchor,0),n=t.domSelectionRange();return Yt(e.node,e.offset,n.anchorNode,n.anchorOffset)}function ks(t,e){let{$anchor:n,$head:r}=t.selection,o=e>0?n.max(r):n.min(r),i=o.parent.inlineContent?o.depth?t.doc.resolve(e>0?o.after():o.before()):null:o;return i&&I.findFrom(i,e)}function Nt(t,e){return t.dispatch(t.state.tr.setSelection(e).scrollIntoView()),!0}function Ic(t,e,n){let r=t.state.selection;if(r instanceof D)if(n.indexOf("s")>-1){let{$head:o}=r,i=o.textOffset?null:e<0?o.nodeBefore:o.nodeAfter;if(!i||i.isText||!i.isLeaf)return!1;let s=t.state.doc.resolve(o.pos+i.nodeSize*(e<0?-1:1));return Nt(t,new D(r.$anchor,s))}else if(r.empty){if(t.endOfTextblock(e>0?"forward":"backward")){let o=ks(t.state,e);return o&&o instanceof L?Nt(t,o):!1}else if(!(De&&n.indexOf("m")>-1)){let o=r.$head,i=o.textOffset?null:e<0?o.nodeBefore:o.nodeAfter,s;if(!i||i.isText)return!1;let l=e<0?o.pos-i.nodeSize:o.pos;return i.isAtom||(s=t.docView.descAt(l))&&!s.contentDOM?L.isSelectable(i)?Nt(t,new L(e<0?t.state.doc.resolve(o.pos-i.nodeSize):o)):rr?Nt(t,new D(t.state.doc.resolve(e<0?l:l+i.nodeSize))):!1:!1}}else return!1;else{if(r instanceof L&&r.node.isInline)return Nt(t,new D(e>0?r.$to:r.$from));{let o=ks(t.state,e);return o?Nt(t,o):!1}}}function Kr(t){return t.nodeType==3?t.nodeValue.length:t.childNodes.length}function Yn(t,e){let n=t.pmViewDesc;return n&&n.size==0&&(e<0||t.nextSibling||t.nodeName!="BR")}function xn(t,e){return e<0?Bm(t):zm(t)}function Bm(t){let e=t.domSelectionRange(),n=e.focusNode,r=e.focusOffset;if(!n)return;let o,i,s=!1;for(Pe&&n.nodeType==1&&r0){if(n.nodeType!=1)break;{let l=n.childNodes[r-1];if(Yn(l,-1))o=n,i=--r;else if(l.nodeType==3)n=l,r=n.nodeValue.length;else break}}else{if(fd(n))break;{let l=n.previousSibling;for(;l&&Yn(l,-1);)o=n.parentNode,i=fe(l),l=l.previousSibling;if(l)n=l,r=Kr(n);else{if(n=n.parentNode,n==t.dom)break;r=0}}}s?Ss(t,n,r):o&&Ss(t,o,i)}function zm(t){let e=t.domSelectionRange(),n=e.focusNode,r=e.focusOffset;if(!n)return;let o=Kr(n),i,s;for(;;)if(r{t.state==o&>(t)},50)}function Pc(t,e){let n=t.state.doc.resolve(e);if(!(de||Zc)&&n.parent.inlineContent){let o=t.coordsAtPos(e);if(e>n.start()){let i=t.coordsAtPos(e-1),s=(i.top+i.bottom)/2;if(s>o.top&&s1)return i.lefto.top&&s1)return i.left>o.left?"ltr":"rtl"}}return getComputedStyle(t.dom).direction=="rtl"?"rtl":"ltr"}function Lc(t,e,n){let r=t.state.selection;if(r instanceof D&&!r.empty||n.indexOf("s")>-1||De&&n.indexOf("m")>-1)return!1;let{$from:o,$to:i}=r;if(!o.parent.inlineContent||t.endOfTextblock(e<0?"up":"down")){let s=ks(t.state,e);if(s&&s instanceof L)return Nt(t,s)}if(!o.parent.inlineContent){let s=e<0?o:i,l=r instanceof ke?I.near(s,e):I.findFrom(s,e);return l?Nt(t,l):!1}return!1}function Bc(t,e){if(!(t.state.selection instanceof D))return!0;let{$head:n,$anchor:r,empty:o}=t.state.selection;if(!n.sameParent(r))return!0;if(!o)return!1;if(t.endOfTextblock(e>0?"forward":"backward"))return!0;let i=!n.textOffset&&(e<0?n.nodeBefore:n.nodeAfter);if(i&&!i.isText){let s=t.state.tr;return e<0?s.delete(n.pos-i.nodeSize,n.pos):s.delete(n.pos,n.pos+i.nodeSize),t.dispatch(s),!0}return!1}function zc(t,e,n){t.domObserver.stop(),e.contentEditable=n,t.domObserver.start()}function Fm(t){if(!we||t.state.selection.$head.parentOffset>0)return!1;let{focusNode:e,focusOffset:n}=t.domSelectionRange();if(e&&e.nodeType==1&&n==0&&e.firstChild&&e.firstChild.contentEditable=="false"){let r=e.firstChild;zc(t,r,"true"),setTimeout(()=>zc(t,r,"false"),20)}return!1}function Vm(t){let e="";return t.ctrlKey&&(e+="c"),t.metaKey&&(e+="m"),t.altKey&&(e+="a"),t.shiftKey&&(e+="s"),e}function _m(t,e){let n=e.keyCode,r=Vm(e);if(n==8||De&&n==72&&r=="c")return Bc(t,-1)||xn(t,-1);if(n==46&&!e.shiftKey||De&&n==68&&r=="c")return Bc(t,1)||xn(t,1);if(n==13||n==27)return!0;if(n==37||De&&n==66&&r=="c"){let o=n==37?Pc(t,t.state.selection.from)=="ltr"?-1:1:-1;return Ic(t,o,r)||xn(t,o)}else if(n==39||De&&n==70&&r=="c"){let o=n==39?Pc(t,t.state.selection.from)=="ltr"?1:-1:1;return Ic(t,o,r)||xn(t,o)}else{if(n==38||De&&n==80&&r=="c")return Lc(t,-1,r)||xn(t,-1);if(n==40||De&&n==78&&r=="c")return Fm(t)||Lc(t,1,r)||xn(t,1);if(r==(De?"m":"c")&&(n==66||n==73||n==89||n==90))return!0}return!1}function Is(t,e){t.someProp("transformCopied",h=>{e=h(e,t)});let n=[],{content:r,openStart:o,openEnd:i}=e;for(;o>1&&i>1&&r.childCount==1&&r.firstChild.childCount==1;){o--,i--;let h=r.firstChild;n.push(h.type.name,h.attrs!=h.type.defaultAttrs?h.attrs:null),r=h.content}let s=t.someProp("clipboardSerializer")||ct.fromSchema(t.state.schema),l=bd(),a=l.createElement("div");a.appendChild(s.serializeFragment(r,{document:l}));let c=a.firstChild,d,u=0;for(;c&&c.nodeType==1&&(d=yd[c.nodeName.toLowerCase()]);){for(let h=d.length-1;h>=0;h--){let p=l.createElement(d[h]);for(;a.firstChild;)p.appendChild(a.firstChild);a.appendChild(p),u++}c=a.firstChild}c&&c.nodeType==1&&c.setAttribute("data-pm-slice",`${o} ${i}${u?` -${u}`:""} ${JSON.stringify(n)}`);let f=t.someProp("clipboardTextSerializer",h=>h(e,t))||e.content.textBetween(0,e.content.size,` + +`);return{dom:a,text:f,slice:e}}function hd(t,e,n,r,o){let i=o.parent.type.spec.code,s,l;if(!n&&!e)return null;let a=!!e&&(r||i||!n);if(a){if(t.someProp("transformPastedText",f=>{e=f(e,i||r,t)}),i)return l=new E(v.from(t.state.schema.text(e.replace(/\r\n?/g,` +`))),0,0),t.someProp("transformPasted",f=>{l=f(l,t,!0)}),l;let u=t.someProp("clipboardTextParser",f=>f(e,o,r,t));if(u)l=u;else{let f=o.marks(),{schema:h}=t.state,p=ct.fromSchema(h);s=document.createElement("div"),e.split(/(?:\r\n?|\n)+/).forEach(m=>{let g=s.appendChild(document.createElement("p"));m&&g.appendChild(p.serializeNode(h.text(m,f)))})}}else t.someProp("transformPastedHTML",u=>{n=u(n,t)}),s=Km(n),rr&&qm(s);let c=s&&s.querySelector("[data-pm-slice]"),d=c&&/^(\d+) (\d+)(?: -(\d+))? (.*)/.exec(c.getAttribute("data-pm-slice")||"");if(d&&d[3])for(let u=+d[3];u>0;u--){let f=s.firstChild;for(;f&&f.nodeType!=1;)f=f.nextSibling;if(!f)break;s=f}if(l||(l=(t.someProp("clipboardParser")||t.someProp("domParser")||Xe.fromSchema(t.state.schema)).parseSlice(s,{preserveWhitespace:!!(a||d),context:o,ruleFromNode(f){return f.nodeName=="BR"&&!f.nextSibling&&f.parentNode&&!Wm.test(f.parentNode.nodeName)?{ignore:!0}:null}})),d)l=Jm(Hc(l,+d[1],+d[2]),d[4]);else if(l=E.maxOpen(jm(l.content,o),!0),l.openStart||l.openEnd){let u=0,f=0;for(let h=l.content.firstChild;u{l=u(l,t,a)}),l}var Wm=/^(a|abbr|acronym|b|cite|code|del|em|i|ins|kbd|label|output|q|ruby|s|samp|span|strong|sub|sup|time|u|tt|var)$/i;function jm(t,e){if(t.childCount<2)return t;for(let n=e.depth;n>=0;n--){let o=e.node(n).contentMatchAt(e.index(n)),i,s=[];if(t.forEach(l=>{if(!s)return;let a=o.findWrapping(l.type),c;if(!a)return s=null;if(c=s.length&&i.length&&md(a,i,l,s[s.length-1],0))s[s.length-1]=c;else{s.length&&(s[s.length-1]=gd(s[s.length-1],i.length));let d=pd(l,a);s.push(d),o=o.matchType(d.type),i=a}}),s)return v.from(s)}return t}function pd(t,e,n=0){for(let r=e.length-1;r>=n;r--)t=e[r].create(null,v.from(t));return t}function md(t,e,n,r,o){if(o1&&(i=0),o=n&&(l=e<0?s.contentMatchAt(0).fillBefore(l,i<=o).append(l):l.append(s.contentMatchAt(s.childCount).fillBefore(v.empty,!0))),t.replaceChild(e<0?0:t.childCount-1,s.copy(l))}function Hc(t,e,n){return en})),cs.createHTML(t)):t}function Km(t){let e=/^(\s*]*>)*/.exec(t);e&&(t=t.slice(e[0].length));let n=bd().createElement("div"),r=/<([a-z][^>\s]+)/i.exec(t),o;if((o=r&&yd[r[1].toLowerCase()])&&(t=o.map(i=>"<"+i+">").join("")+t+o.map(i=>"").reverse().join("")),n.innerHTML=Um(t),o)for(let i=0;i=0;l-=2){let a=n.nodes[r[l]];if(!a||a.hasRequiredAttrs())break;o=v.from(a.create(r[l+1],o)),i++,s++}return new E(o,i,s)}var Se={},Ce={},Gm={touchstart:!0,touchmove:!0},vs=class{constructor(){this.shiftKey=!1,this.mouseDown=null,this.lastKeyCode=null,this.lastKeyCodeTime=0,this.lastClick={time:0,x:0,y:0,type:"",button:0},this.lastSelectionOrigin=null,this.lastSelectionTime=0,this.lastIOSEnter=0,this.lastIOSEnterFallbackTimeout=-1,this.lastFocus=0,this.lastTouch=0,this.lastChromeDelete=0,this.composing=!1,this.compositionNode=null,this.composingTimeout=-1,this.compositionNodes=[],this.compositionEndedAt=-2e8,this.compositionID=1,this.compositionPendingChanges=0,this.domChangeCount=0,this.eventHandlers=Object.create(null),this.hideSelectionGuard=null}};function Xm(t){for(let e in Se){let n=Se[e];t.dom.addEventListener(e,t.input.eventHandlers[e]=r=>{Qm(t,r)&&!Ps(t,r)&&(t.editable||!(r.type in Ce))&&n(t,r)},Gm[e]?{passive:!0}:void 0)}we&&t.dom.addEventListener("input",()=>null),Ms(t)}function Ot(t,e){t.input.lastSelectionOrigin=e,t.input.lastSelectionTime=Date.now()}function Ym(t){t.domObserver.stop();for(let e in t.input.eventHandlers)t.dom.removeEventListener(e,t.input.eventHandlers[e]);clearTimeout(t.input.composingTimeout),clearTimeout(t.input.lastIOSEnterFallbackTimeout)}function Ms(t){t.someProp("handleDOMEvents",e=>{for(let n in e)t.input.eventHandlers[n]||t.dom.addEventListener(n,t.input.eventHandlers[n]=r=>Ps(t,r))})}function Ps(t,e){return t.someProp("handleDOMEvents",n=>{let r=n[e.type];return r?r(t,e)||e.defaultPrevented:!1})}function Qm(t,e){if(!e.bubbles)return!0;if(e.defaultPrevented)return!1;for(let n=e.target;n!=t.dom;n=n.parentNode)if(!n||n.nodeType==11||n.pmViewDesc&&n.pmViewDesc.stopEvent(e))return!1;return!0}function Zm(t,e){!Ps(t,e)&&Se[e.type]&&(t.editable||!(e.type in Ce))&&Se[e.type](t,e)}Ce.keydown=(t,e)=>{let n=e;if(t.input.shiftKey=n.keyCode==16||n.shiftKey,!xd(t,n)&&(t.input.lastKeyCode=n.keyCode,t.input.lastKeyCodeTime=Date.now(),!(mt&&de&&n.keyCode==13)))if(n.keyCode!=229&&t.domObserver.forceFlush(),vn&&n.keyCode==13&&!n.ctrlKey&&!n.altKey&&!n.metaKey){let r=Date.now();t.input.lastIOSEnter=r,t.input.lastIOSEnterFallbackTimeout=setTimeout(()=>{t.input.lastIOSEnter==r&&(t.someProp("handleKeyDown",o=>o(t,Ut(13,"Enter"))),t.input.lastIOSEnter=0)},200)}else t.someProp("handleKeyDown",r=>r(t,n))||_m(t,n)?n.preventDefault():Ot(t,"key")};Ce.keyup=(t,e)=>{e.keyCode==16&&(t.input.shiftKey=!1)};Ce.keypress=(t,e)=>{let n=e;if(xd(t,n)||!n.charCode||n.ctrlKey&&!n.altKey||De&&n.metaKey)return;if(t.someProp("handleKeyPress",o=>o(t,n))){n.preventDefault();return}let r=t.state.selection;if(!(r instanceof D)||!r.$from.sameParent(r.$to)){let o=String.fromCharCode(n.charCode),i=()=>t.state.tr.insertText(o).scrollIntoView();!/[\r\n]/.test(o)&&!t.someProp("handleTextInput",s=>s(t,r.$from.pos,r.$to.pos,o,i))&&t.dispatch(i()),n.preventDefault()}};function Zr(t){return{left:t.clientX,top:t.clientY}}function eg(t,e){let n=e.x-t.clientX,r=e.y-t.clientY;return n*n+r*r<100}function Ls(t,e,n,r,o){if(r==-1)return!1;let i=t.state.doc.resolve(r);for(let s=i.depth+1;s>0;s--)if(t.someProp(e,l=>s>i.depth?l(t,n,i.nodeAfter,i.before(s),o,!0):l(t,n,i.node(s),i.before(s),o,!1)))return!0;return!1}function Sn(t,e,n){if(t.focused||t.focus(),t.state.selection.eq(e))return;let r=t.state.tr.setSelection(e);n=="pointer"&&r.setMeta("pointer",!0),t.dispatch(r)}function tg(t,e){if(e==-1)return!1;let n=t.state.doc.resolve(e),r=n.nodeAfter;return r&&r.isAtom&&L.isSelectable(r)?(Sn(t,new L(n),"pointer"),!0):!1}function ng(t,e){if(e==-1)return!1;let n=t.state.selection,r,o;n instanceof L&&(r=n.node);let i=t.state.doc.resolve(e);for(let s=i.depth+1;s>0;s--){let l=s>i.depth?i.nodeAfter:i.node(s);if(L.isSelectable(l)){r&&n.$from.depth>0&&s>=n.$from.depth&&i.before(n.$from.depth+1)==n.$from.pos?o=i.before(n.$from.depth):o=i.before(s);break}}return o!=null?(Sn(t,L.create(t.state.doc,o),"pointer"),!0):!1}function rg(t,e,n,r,o){return Ls(t,"handleClickOn",e,n,r)||t.someProp("handleClick",i=>i(t,e,r))||(o?ng(t,n):tg(t,n))}function og(t,e,n,r){return Ls(t,"handleDoubleClickOn",e,n,r)||t.someProp("handleDoubleClick",o=>o(t,e,r))}function ig(t,e,n,r){return Ls(t,"handleTripleClickOn",e,n,r)||t.someProp("handleTripleClick",o=>o(t,e,r))||sg(t,n,r)}function sg(t,e,n){if(n.button!=0)return!1;let r=t.state.doc;if(e==-1)return r.inlineContent?(Sn(t,D.create(r,0,r.content.size),"pointer"),!0):!1;let o=r.resolve(e);for(let i=o.depth+1;i>0;i--){let s=i>o.depth?o.nodeAfter:o.node(i),l=o.before(i);if(s.inlineContent)Sn(t,D.create(r,l+1,l+1+s.content.size),"pointer");else if(L.isSelectable(s))Sn(t,L.create(r,l),"pointer");else continue;return!0}}function Bs(t){return qr(t)}var wd=De?"metaKey":"ctrlKey";Se.mousedown=(t,e)=>{let n=e;t.input.shiftKey=n.shiftKey;let r=Bs(t),o=Date.now(),i="singleClick";o-t.input.lastClick.time<500&&eg(n,t.input.lastClick)&&!n[wd]&&t.input.lastClick.button==n.button&&(t.input.lastClick.type=="singleClick"?i="doubleClick":t.input.lastClick.type=="doubleClick"&&(i="tripleClick")),t.input.lastClick={time:o,x:n.clientX,y:n.clientY,type:i,button:n.button};let s=t.posAtCoords(Zr(n));s&&(i=="singleClick"?(t.input.mouseDown&&t.input.mouseDown.done(),t.input.mouseDown=new Ts(t,s,n,!!r)):(i=="doubleClick"?og:ig)(t,s.pos,s.inside,n)?n.preventDefault():Ot(t,"pointer"))};var Ts=class{constructor(e,n,r,o){this.view=e,this.pos=n,this.event=r,this.flushed=o,this.delayedSelectionSync=!1,this.mightDrag=null,this.startDoc=e.state.doc,this.selectNode=!!r[wd],this.allowDefault=r.shiftKey;let i,s;if(n.inside>-1)i=e.state.doc.nodeAt(n.inside),s=n.inside;else{let d=e.state.doc.resolve(n.pos);i=d.parent,s=d.depth?d.before():0}let l=o?null:r.target,a=l?e.docView.nearestDesc(l,!0):null;this.target=a&&a.nodeDOM.nodeType==1?a.nodeDOM:null;let{selection:c}=e.state;(r.button==0&&i.type.spec.draggable&&i.type.spec.selectable!==!1||c instanceof L&&c.from<=s&&c.to>s)&&(this.mightDrag={node:i,pos:s,addAttr:!!(this.target&&!this.target.draggable),setUneditable:!!(this.target&&Pe&&!this.target.hasAttribute("contentEditable"))}),this.target&&this.mightDrag&&(this.mightDrag.addAttr||this.mightDrag.setUneditable)&&(this.view.domObserver.stop(),this.mightDrag.addAttr&&(this.target.draggable=!0),this.mightDrag.setUneditable&&setTimeout(()=>{this.view.input.mouseDown==this&&this.target.setAttribute("contentEditable","false")},20),this.view.domObserver.start()),e.root.addEventListener("mouseup",this.up=this.up.bind(this)),e.root.addEventListener("mousemove",this.move=this.move.bind(this)),Ot(e,"pointer")}done(){this.view.root.removeEventListener("mouseup",this.up),this.view.root.removeEventListener("mousemove",this.move),this.mightDrag&&this.target&&(this.view.domObserver.stop(),this.mightDrag.addAttr&&this.target.removeAttribute("draggable"),this.mightDrag.setUneditable&&this.target.removeAttribute("contentEditable"),this.view.domObserver.start()),this.delayedSelectionSync&&setTimeout(()=>gt(this.view)),this.view.input.mouseDown=null}up(e){if(this.done(),!this.view.dom.contains(e.target))return;let n=this.pos;this.view.state.doc!=this.startDoc&&(n=this.view.posAtCoords(Zr(e))),this.updateAllowDefault(e),this.allowDefault||!n?Ot(this.view,"pointer"):rg(this.view,n.pos,n.inside,e,this.selectNode)?e.preventDefault():e.button==0&&(this.flushed||we&&this.mightDrag&&!this.mightDrag.node.isAtom||de&&!this.view.state.selection.visible&&Math.min(Math.abs(n.pos-this.view.state.selection.from),Math.abs(n.pos-this.view.state.selection.to))<=2)?(Sn(this.view,I.near(this.view.state.doc.resolve(n.pos)),"pointer"),e.preventDefault()):Ot(this.view,"pointer")}move(e){this.updateAllowDefault(e),Ot(this.view,"pointer"),e.buttons==0&&this.done()}updateAllowDefault(e){!this.allowDefault&&(Math.abs(this.event.x-e.clientX)>4||Math.abs(this.event.y-e.clientY)>4)&&(this.allowDefault=!0)}};Se.touchstart=t=>{t.input.lastTouch=Date.now(),Bs(t),Ot(t,"pointer")};Se.touchmove=t=>{t.input.lastTouch=Date.now(),Ot(t,"pointer")};Se.contextmenu=t=>Bs(t);function xd(t,e){return t.composing?!0:we&&Math.abs(e.timeStamp-t.input.compositionEndedAt)<500?(t.input.compositionEndedAt=-2e8,!0):!1}var lg=mt?5e3:-1;Ce.compositionstart=Ce.compositionupdate=t=>{if(!t.composing){t.domObserver.flush();let{state:e}=t,n=e.selection.$to;if(e.selection instanceof D&&(e.storedMarks||!n.textOffset&&n.parentOffset&&n.nodeBefore.marks.some(r=>r.type.spec.inclusive===!1)||de&&Zc&&ag(t)))t.markCursor=t.state.storedMarks||n.marks(),qr(t,!0),t.markCursor=null;else if(qr(t,!e.selection.empty),Pe&&e.selection.empty&&n.parentOffset&&!n.textOffset&&n.nodeBefore.marks.length){let r=t.domSelectionRange();for(let o=r.focusNode,i=r.focusOffset;o&&o.nodeType==1&&i!=0;){let s=i<0?o.lastChild:o.childNodes[i-1];if(!s)break;if(s.nodeType==3){let l=t.domSelection();l&&l.collapse(s,s.nodeValue.length);break}else o=s,i=-1}}t.input.composing=!0}kd(t,lg)};function ag(t){let{focusNode:e,focusOffset:n}=t.domSelectionRange();if(!e||e.nodeType!=1||n>=e.childNodes.length)return!1;let r=e.childNodes[n];return r.nodeType==1&&r.contentEditable=="false"}Ce.compositionend=(t,e)=>{t.composing&&(t.input.composing=!1,t.input.compositionEndedAt=e.timeStamp,t.input.compositionPendingChanges=t.domObserver.pendingRecords().length?t.input.compositionID:0,t.input.compositionNode=null,t.input.compositionPendingChanges&&Promise.resolve().then(()=>t.domObserver.flush()),t.input.compositionID++,kd(t,20))};function kd(t,e){clearTimeout(t.input.composingTimeout),e>-1&&(t.input.composingTimeout=setTimeout(()=>qr(t),e))}function Sd(t){for(t.composing&&(t.input.composing=!1,t.input.compositionEndedAt=dg());t.input.compositionNodes.length>0;)t.input.compositionNodes.pop().markParentsDirty()}function cg(t){let e=t.domSelectionRange();if(!e.focusNode)return null;let n=sm(e.focusNode,e.focusOffset),r=lm(e.focusNode,e.focusOffset);if(n&&r&&n!=r){let o=r.pmViewDesc,i=t.domObserver.lastChangedTextNode;if(n==i||r==i)return i;if(!o||!o.isText(r.nodeValue))return r;if(t.input.compositionNode==r){let s=n.pmViewDesc;if(!(!s||!s.isText(n.nodeValue)))return r}}return n||r}function dg(){let t=document.createEvent("Event");return t.initEvent("event",!0,!0),t.timeStamp}function qr(t,e=!1){if(!(mt&&t.domObserver.flushingSoon>=0)){if(t.domObserver.forceFlush(),Sd(t),e||t.docView&&t.docView.dirty){let n=Rs(t),r=t.state.selection;return n&&!n.eq(r)?t.dispatch(t.state.tr.setSelection(n)):(t.markCursor||e)&&!r.$from.node(r.$from.sharedDepth(r.to)).inlineContent?t.dispatch(t.state.tr.deleteSelection()):t.updateState(t.state),!0}return!1}}function ug(t,e){if(!t.dom.parentNode)return;let n=t.dom.parentNode.appendChild(document.createElement("div"));n.appendChild(e),n.style.cssText="position: fixed; left: -10000px; top: 10px";let r=getSelection(),o=document.createRange();o.selectNodeContents(e),t.dom.blur(),r.removeAllRanges(),r.addRange(o),setTimeout(()=>{n.parentNode&&n.parentNode.removeChild(n),t.focus()},50)}var Qn=ve&&Rt<15||vn&&um<604;Se.copy=Ce.cut=(t,e)=>{let n=e,r=t.state.selection,o=n.type=="cut";if(r.empty)return;let i=Qn?null:n.clipboardData,s=r.content(),{dom:l,text:a}=Is(t,s);i?(n.preventDefault(),i.clearData(),i.setData("text/html",l.innerHTML),i.setData("text/plain",a)):ug(t,l),o&&t.dispatch(t.state.tr.deleteSelection().scrollIntoView().setMeta("uiEvent","cut"))};function fg(t){return t.openStart==0&&t.openEnd==0&&t.content.childCount==1?t.content.firstChild:null}function hg(t,e){if(!t.dom.parentNode)return;let n=t.input.shiftKey||t.state.selection.$from.parent.type.spec.code,r=t.dom.parentNode.appendChild(document.createElement(n?"textarea":"div"));n||(r.contentEditable="true"),r.style.cssText="position: fixed; left: -10000px; top: 10px",r.focus();let o=t.input.shiftKey&&t.input.lastKeyCode!=45;setTimeout(()=>{t.focus(),r.parentNode&&r.parentNode.removeChild(r),n?Zn(t,r.value,null,o,e):Zn(t,r.textContent,r.innerHTML,o,e)},50)}function Zn(t,e,n,r,o){let i=hd(t,e,n,r,t.state.selection.$from);if(t.someProp("handlePaste",a=>a(t,o,i||E.empty)))return!0;if(!i)return!1;let s=fg(i),l=s?t.state.tr.replaceSelectionWith(s,r):t.state.tr.replaceSelection(i);return t.dispatch(l.scrollIntoView().setMeta("paste",!0).setMeta("uiEvent","paste")),!0}function Cd(t){let e=t.getData("text/plain")||t.getData("Text");if(e)return e;let n=t.getData("text/uri-list");return n?n.replace(/\r?\n/g," "):""}Ce.paste=(t,e)=>{let n=e;if(t.composing&&!mt)return;let r=Qn?null:n.clipboardData,o=t.input.shiftKey&&t.input.lastKeyCode!=45;r&&Zn(t,Cd(r),r.getData("text/html"),o,n)?n.preventDefault():hg(t,n)};var Jr=class{constructor(e,n,r){this.slice=e,this.move=n,this.node=r}},pg=De?"altKey":"ctrlKey";function vd(t,e){let n=t.someProp("dragCopies",r=>!r(e));return n??!e[pg]}Se.dragstart=(t,e)=>{let n=e,r=t.input.mouseDown;if(r&&r.done(),!n.dataTransfer)return;let o=t.state.selection,i=o.empty?null:t.posAtCoords(Zr(n)),s;if(!(i&&i.pos>=o.from&&i.pos<=(o instanceof L?o.to-1:o.to))){if(r&&r.mightDrag)s=L.create(t.state.doc,r.mightDrag.pos);else if(n.target&&n.target.nodeType==1){let u=t.docView.nearestDesc(n.target,!0);u&&u.node.type.spec.draggable&&u!=t.docView&&(s=L.create(t.state.doc,u.posBefore))}}let l=(s||t.state.selection).content(),{dom:a,text:c,slice:d}=Is(t,l);(!n.dataTransfer.files.length||!de||Qc>120)&&n.dataTransfer.clearData(),n.dataTransfer.setData(Qn?"Text":"text/html",a.innerHTML),n.dataTransfer.effectAllowed="copyMove",Qn||n.dataTransfer.setData("text/plain",c),t.dragging=new Jr(d,vd(t,n),s)};Se.dragend=t=>{let e=t.dragging;window.setTimeout(()=>{t.dragging==e&&(t.dragging=null)},50)};Ce.dragover=Ce.dragenter=(t,e)=>e.preventDefault();Ce.drop=(t,e)=>{try{mg(t,e,t.dragging)}finally{t.dragging=null}};function mg(t,e,n){if(!e.dataTransfer)return;let r=t.posAtCoords(Zr(e));if(!r)return;let o=t.state.doc.resolve(r.pos),i=n&&n.slice;i?t.someProp("transformPasted",h=>{i=h(i,t,!1)}):i=hd(t,Cd(e.dataTransfer),Qn?null:e.dataTransfer.getData("text/html"),!1,o);let s=!!(n&&vd(t,e));if(t.someProp("handleDrop",h=>h(t,e,i||E.empty,s))){e.preventDefault();return}if(!i)return;e.preventDefault();let l=i?zr(t.state.doc,o.pos,i):o.pos;l==null&&(l=o.pos);let a=t.state.tr;if(s){let{node:h}=n;h?h.replace(a):a.deleteSelection()}let c=a.mapping.map(l),d=i.openStart==0&&i.openEnd==0&&i.content.childCount==1,u=a.doc;if(d?a.replaceRangeWith(c,c,i.content.firstChild):a.replaceRange(c,c,i),a.doc.eq(u))return;let f=a.doc.resolve(c);if(d&&L.isSelectable(i.content.firstChild)&&f.nodeAfter&&f.nodeAfter.sameMarkup(i.content.firstChild))a.setSelection(new L(f));else{let h=a.mapping.map(l);a.mapping.maps[a.mapping.maps.length-1].forEach((p,m,g,y)=>h=y),a.setSelection(Ds(t,f,a.doc.resolve(h)))}t.focus(),t.dispatch(a.setMeta("uiEvent","drop"))}Se.focus=t=>{t.input.lastFocus=Date.now(),t.focused||(t.domObserver.stop(),t.dom.classList.add("ProseMirror-focused"),t.domObserver.start(),t.focused=!0,setTimeout(()=>{t.docView&&t.hasFocus()&&!t.domObserver.currentSelection.eq(t.domSelectionRange())&>(t)},20))};Se.blur=(t,e)=>{let n=e;t.focused&&(t.domObserver.stop(),t.dom.classList.remove("ProseMirror-focused"),t.domObserver.start(),n.relatedTarget&&t.dom.contains(n.relatedTarget)&&t.domObserver.currentSelection.clear(),t.focused=!1)};Se.beforeinput=(t,e)=>{if(de&&mt&&e.inputType=="deleteContentBackward"){t.domObserver.flushSoon();let{domChangeCount:r}=t.input;setTimeout(()=>{if(t.input.domChangeCount!=r||(t.dom.blur(),t.focus(),t.someProp("handleKeyDown",i=>i(t,Ut(8,"Backspace")))))return;let{$cursor:o}=t.state.selection;o&&o.pos>0&&t.dispatch(t.state.tr.delete(o.pos-1,o.pos).scrollIntoView())},50)}};for(let t in Ce)Se[t]=Ce[t];function er(t,e){if(t==e)return!0;for(let n in t)if(t[n]!==e[n])return!1;for(let n in e)if(!(n in t))return!1;return!0}var Gr=class t{constructor(e,n){this.toDOM=e,this.spec=n||Gt,this.side=this.spec.side||0}map(e,n,r,o){let{pos:i,deleted:s}=e.mapResult(n.from+o,this.side<0?-1:1);return s?null:new te(i-r,i-r,this)}valid(){return!0}eq(e){return this==e||e instanceof t&&(this.spec.key&&this.spec.key==e.spec.key||this.toDOM==e.toDOM&&er(this.spec,e.spec))}destroy(e){this.spec.destroy&&this.spec.destroy(e)}},Jt=class t{constructor(e,n){this.attrs=e,this.spec=n||Gt}map(e,n,r,o){let i=e.map(n.from+o,this.spec.inclusiveStart?-1:1)-r,s=e.map(n.to+o,this.spec.inclusiveEnd?1:-1)-r;return i>=s?null:new te(i,s,this)}valid(e,n){return n.from=e&&(!i||i(l.spec))&&r.push(l.copy(l.from+o,l.to+o))}for(let s=0;se){let l=this.children[s]+1;this.children[s+2].findInner(e-l,n-l,r,o+l,i)}}map(e,n,r){return this==be||e.maps.length==0?this:this.mapInner(e,n,0,0,r||Gt)}mapInner(e,n,r,o,i){let s;for(let l=0;l{let c=a+r,d;if(d=Td(n,l,c)){for(o||(o=this.children.slice());il&&u.to=e){this.children[l]==e&&(r=this.children[l+2]);break}let i=e+1,s=i+n.content.size;for(let l=0;li&&a.type instanceof Jt){let c=Math.max(i,a.from)-i,d=Math.min(s,a.to)-i;co.map(e,n,Gt));return t.from(r)}forChild(e,n){if(n.isLeaf)return Y.empty;let r=[];for(let o=0;on instanceof Y)?e:e.reduce((n,r)=>n.concat(r instanceof Y?r:r.members),[]))}}forEachSet(e){for(let n=0;n{let g=m-p-(h-f);for(let y=0;yw+d-u)continue;let b=l[y]+d-u;h>=b?l[y+1]=f<=b?-2:-1:f>=d&&g&&(l[y]+=g,l[y+1]+=g)}u+=g}),d=n.maps[c].map(d,-1)}let a=!1;for(let c=0;c=r.content.size){a=!0;continue}let f=n.map(t[c+1]+i,-1),h=f-o,{index:p,offset:m}=r.content.findIndex(u),g=r.maybeChild(p);if(g&&m==u&&m+g.nodeSize==h){let y=l[c+2].mapInner(n,g,d+1,t[c]+i+1,s);y!=be?(l[c]=u,l[c+1]=h,l[c+2]=y):(l[c+1]=-2,a=!0)}else a=!0}if(a){let c=yg(l,t,e,n,o,i,s),d=Yr(c,r,0,s);e=d.local;for(let u=0;un&&s.to{let c=Td(t,l,a+n);if(c){i=!0;let d=Yr(c,l,n+a+1,r);d!=be&&o.push(a,a+l.nodeSize,d)}});let s=Md(i?Ad(t):t,-n).sort(Xt);for(let l=0;l0;)e++;t.splice(e,0,n)}function ds(t){let e=[];return t.someProp("decorations",n=>{let r=n(t.state);r&&r!=be&&e.push(r)}),t.cursorWrapper&&e.push(Y.create(t.state.doc,[t.cursorWrapper.deco])),Xr.from(e)}var bg={childList:!0,characterData:!0,characterDataOldValue:!0,attributes:!0,attributeOldValue:!0,subtree:!0},wg=ve&&Rt<=11,Es=class{constructor(){this.anchorNode=null,this.anchorOffset=0,this.focusNode=null,this.focusOffset=0}set(e){this.anchorNode=e.anchorNode,this.anchorOffset=e.anchorOffset,this.focusNode=e.focusNode,this.focusOffset=e.focusOffset}clear(){this.anchorNode=this.focusNode=null}eq(e){return e.anchorNode==this.anchorNode&&e.anchorOffset==this.anchorOffset&&e.focusNode==this.focusNode&&e.focusOffset==this.focusOffset}},Ns=class{constructor(e,n){this.view=e,this.handleDOMChange=n,this.queue=[],this.flushingSoon=-1,this.observer=null,this.currentSelection=new Es,this.onCharData=null,this.suppressingSelectionUpdates=!1,this.lastChangedTextNode=null,this.observer=window.MutationObserver&&new window.MutationObserver(r=>{for(let o=0;oo.type=="childList"&&o.removedNodes.length||o.type=="characterData"&&o.oldValue.length>o.target.nodeValue.length)?this.flushSoon():this.flush()}),wg&&(this.onCharData=r=>{this.queue.push({target:r.target,type:"characterData",oldValue:r.prevValue}),this.flushSoon()}),this.onSelectionChange=this.onSelectionChange.bind(this)}flushSoon(){this.flushingSoon<0&&(this.flushingSoon=window.setTimeout(()=>{this.flushingSoon=-1,this.flush()},20))}forceFlush(){this.flushingSoon>-1&&(window.clearTimeout(this.flushingSoon),this.flushingSoon=-1,this.flush())}start(){this.observer&&(this.observer.takeRecords(),this.observer.observe(this.view.dom,bg)),this.onCharData&&this.view.dom.addEventListener("DOMCharacterDataModified",this.onCharData),this.connectSelection()}stop(){if(this.observer){let e=this.observer.takeRecords();if(e.length){for(let n=0;nthis.flush(),20)}this.observer.disconnect()}this.onCharData&&this.view.dom.removeEventListener("DOMCharacterDataModified",this.onCharData),this.disconnectSelection()}connectSelection(){this.view.dom.ownerDocument.addEventListener("selectionchange",this.onSelectionChange)}disconnectSelection(){this.view.dom.ownerDocument.removeEventListener("selectionchange",this.onSelectionChange)}suppressSelectionUpdates(){this.suppressingSelectionUpdates=!0,setTimeout(()=>this.suppressingSelectionUpdates=!1,50)}onSelectionChange(){if(Dc(this.view)){if(this.suppressingSelectionUpdates)return gt(this.view);if(ve&&Rt<=11&&!this.view.state.selection.empty){let e=this.view.domSelectionRange();if(e.focusNode&&Yt(e.focusNode,e.focusOffset,e.anchorNode,e.anchorOffset))return this.flushSoon()}this.flush()}}setCurSelection(){this.currentSelection.set(this.view.domSelectionRange())}ignoreSelectionChange(e){if(!e.focusNode)return!0;let n=new Set,r;for(let i=e.focusNode;i;i=Cn(i))n.add(i);for(let i=e.anchorNode;i;i=Cn(i))if(n.has(i)){r=i;break}let o=r&&this.view.docView.nearestDesc(r);if(o&&o.ignoreMutation({type:"selection",target:r.nodeType==3?r.parentNode:r}))return this.setCurSelection(),!0}pendingRecords(){if(this.observer)for(let e of this.observer.takeRecords())this.queue.push(e);return this.queue}flush(){let{view:e}=this;if(!e.docView||this.flushingSoon>-1)return;let n=this.pendingRecords();n.length&&(this.queue=[]);let r=e.domSelectionRange(),o=!this.suppressingSelectionUpdates&&!this.currentSelection.eq(r)&&Dc(e)&&!this.ignoreSelectionChange(r),i=-1,s=-1,l=!1,a=[];if(e.editable)for(let d=0;du.nodeName=="BR");if(d.length==2){let[u,f]=d;u.parentNode&&u.parentNode.parentNode==f.parentNode?f.remove():u.remove()}else{let{focusNode:u}=this.currentSelection;for(let f of d){let h=f.parentNode;h&&h.nodeName=="LI"&&(!u||Sg(e,u)!=h)&&f.remove()}}}else if((de||we)&&a.some(d=>d.nodeName=="BR")&&(e.input.lastKeyCode==8||e.input.lastKeyCode==46)){for(let d of a)if(d.nodeName=="BR"&&d.parentNode){let u=d.nextSibling;u&&u.nodeType==1&&u.contentEditable=="false"&&d.parentNode.removeChild(d)}}let c=null;i<0&&o&&e.input.lastFocus>Date.now()-200&&Math.max(e.input.lastTouch,e.input.lastClick.time)-1||o)&&(i>-1&&(e.docView.markDirty(i,s),xg(e)),this.handleDOMChange(i,s,l,a),e.docView&&e.docView.dirty?e.updateState(e.state):this.currentSelection.eq(r)||gt(e),this.currentSelection.set(r))}registerMutation(e,n){if(n.indexOf(e.target)>-1)return null;let r=this.view.docView.nearestDesc(e.target);if(e.type=="attributes"&&(r==this.view.docView||e.attributeName=="contenteditable"||e.attributeName=="style"&&!e.oldValue&&!e.target.getAttribute("style"))||!r||r.ignoreMutation(e))return null;if(e.type=="childList"){for(let d=0;do;g--){let y=r.childNodes[g-1],w=y.pmViewDesc;if(y.nodeName=="BR"&&!w){i=g;break}if(!w||w.size)break}let u=t.state.doc,f=t.someProp("domParser")||Xe.fromSchema(t.state.schema),h=u.resolve(s),p=null,m=f.parse(r,{topNode:h.parent,topMatch:h.parent.contentMatchAt(h.index()),topOpen:!0,from:o,to:i,preserveWhitespace:h.parent.type.whitespace=="pre"?"full":!0,findPositions:c,ruleFromNode:vg,context:h});if(c&&c[0].pos!=null){let g=c[0].pos,y=c[1]&&c[1].pos;y==null&&(y=g),p={anchor:g+s,head:y+s}}return{doc:m,sel:p,from:s,to:l}}function vg(t){let e=t.pmViewDesc;if(e)return e.parseRule();if(t.nodeName=="BR"&&t.parentNode){if(we&&/^(ul|ol)$/i.test(t.parentNode.nodeName)){let n=document.createElement("div");return n.appendChild(document.createElement("li")),{skip:n}}else if(t.parentNode.lastChild==t||we&&/^(tr|table)$/i.test(t.parentNode.nodeName))return{ignore:!0}}else if(t.nodeName=="IMG"&&t.getAttribute("mark-placeholder"))return{ignore:!0};return null}var Mg=/^(a|abbr|acronym|b|bd[io]|big|br|button|cite|code|data(list)?|del|dfn|em|i|img|ins|kbd|label|map|mark|meter|output|q|ruby|s|samp|small|span|strong|su[bp]|time|u|tt|var)$/i;function Tg(t,e,n,r,o){let i=t.input.compositionPendingChanges||(t.composing?t.input.compositionID:0);if(t.input.compositionPendingChanges=0,e<0){let k=t.input.lastSelectionTime>Date.now()-50?t.input.lastSelectionOrigin:null,O=Rs(t,k);if(O&&!t.state.selection.eq(O)){if(de&&mt&&t.input.lastKeyCode===13&&Date.now()-100A(t,Ut(13,"Enter"))))return;let T=t.state.tr.setSelection(O);k=="pointer"?T.setMeta("pointer",!0):k=="key"&&T.scrollIntoView(),i&&T.setMeta("composition",i),t.dispatch(T)}return}let s=t.state.doc.resolve(e),l=s.sharedDepth(n);e=s.before(l+1),n=t.state.doc.resolve(n).after(l+1);let a=t.state.selection,c=Cg(t,e,n),d=t.state.doc,u=d.slice(c.from,c.to),f,h;t.input.lastKeyCode===8&&Date.now()-100Date.now()-225||mt)&&o.some(k=>k.nodeType==1&&!Mg.test(k.nodeName))&&(!p||p.endA>=p.endB)&&t.someProp("handleKeyDown",k=>k(t,Ut(13,"Enter")))){t.input.lastIOSEnter=0;return}if(!p)if(r&&a instanceof D&&!a.empty&&a.$head.sameParent(a.$anchor)&&!t.composing&&!(c.sel&&c.sel.anchor!=c.sel.head))p={start:a.from,endA:a.to,endB:a.to};else{if(c.sel){let k=jc(t,t.state.doc,c.sel);if(k&&!k.eq(t.state.selection)){let O=t.state.tr.setSelection(k);i&&O.setMeta("composition",i),t.dispatch(O)}}return}t.state.selection.fromt.state.selection.from&&p.start<=t.state.selection.from+2&&t.state.selection.from>=c.from?p.start=t.state.selection.from:p.endA=t.state.selection.to-2&&t.state.selection.to<=c.to&&(p.endB+=t.state.selection.to-p.endA,p.endA=t.state.selection.to)),ve&&Rt<=11&&p.endB==p.start+1&&p.endA==p.start&&p.start>c.from&&c.doc.textBetween(p.start-c.from-1,p.start-c.from+1)==" \xA0"&&(p.start--,p.endA--,p.endB--);let m=c.doc.resolveNoCache(p.start-c.from),g=c.doc.resolveNoCache(p.endB-c.from),y=d.resolve(p.start),w=m.sameParent(g)&&m.parent.inlineContent&&y.end()>=p.endA;if((vn&&t.input.lastIOSEnter>Date.now()-225&&(!w||o.some(k=>k.nodeName=="DIV"||k.nodeName=="P"))||!w&&m.posk(t,Ut(13,"Enter")))){t.input.lastIOSEnter=0;return}if(t.state.selection.anchor>p.start&&Eg(d,p.start,p.endA,m,g)&&t.someProp("handleKeyDown",k=>k(t,Ut(8,"Backspace")))){mt&&de&&t.domObserver.suppressSelectionUpdates();return}de&&p.endB==p.start&&(t.input.lastChromeDelete=Date.now()),mt&&!w&&m.start()!=g.start()&&g.parentOffset==0&&m.depth==g.depth&&c.sel&&c.sel.anchor==c.sel.head&&c.sel.head==p.endA&&(p.endB-=2,g=c.doc.resolveNoCache(p.endB-c.from),setTimeout(()=>{t.someProp("handleKeyDown",function(k){return k(t,Ut(13,"Enter"))})},20));let b=p.start,C=p.endA,x=k=>{let O=k||t.state.tr.replace(b,C,c.doc.slice(p.start-c.from,p.endB-c.from));if(c.sel){let T=jc(t,O.doc,c.sel);T&&!(de&&t.composing&&T.empty&&(p.start!=p.endB||t.input.lastChromeDeletegt(t),20));let k=x(t.state.tr.delete(b,C)),O=d.resolve(p.start).marksAcross(d.resolve(p.endA));O&&k.ensureMarks(O),t.dispatch(k)}else if(p.endA==p.endB&&(S=Ag(m.parent.content.cut(m.parentOffset,g.parentOffset),y.parent.content.cut(y.parentOffset,p.endA-y.start())))){let k=x(t.state.tr);S.type=="add"?k.addMark(b,C,S.mark):k.removeMark(b,C,S.mark),t.dispatch(k)}else if(m.parent.child(m.index()).isText&&m.index()==g.index()-(g.textOffset?0:1)){let k=m.parent.textBetween(m.parentOffset,g.parentOffset),O=()=>x(t.state.tr.insertText(k,b,C));t.someProp("handleTextInput",T=>T(t,b,C,k,O))||t.dispatch(O())}else t.dispatch(x());else t.dispatch(x())}function jc(t,e,n){return Math.max(n.anchor,n.head)>e.content.size?null:Ds(t,e.resolve(n.anchor),e.resolve(n.head))}function Ag(t,e){let n=t.firstChild.marks,r=e.firstChild.marks,o=n,i=r,s,l,a;for(let d=0;dd.mark(l.addToSet(d.marks));else if(o.length==0&&i.length==1)l=i[0],s="remove",a=d=>d.mark(l.removeFromSet(d.marks));else return null;let c=[];for(let d=0;dn||us(s,!0,!1)0&&(e||t.indexAfter(r)==t.node(r).childCount);)r--,o++,e=!1;if(n){let i=t.node(r).maybeChild(t.indexAfter(r));for(;i&&!i.isLeaf;)i=i.firstChild,o++}return o}function Ng(t,e,n,r,o){let i=t.findDiffStart(e,n);if(i==null)return null;let{a:s,b:l}=t.findDiffEnd(e,n+t.size,n+e.size);if(o=="end"){let a=Math.max(0,i-Math.min(s,l));r-=s+a-i}if(s=s?i-r:0;i-=a,i&&i=l?i-r:0;i-=a,i&&i=56320&&e<=57343&&n>=55296&&n<=56319}var tr=class{constructor(e,n){this._root=null,this.focused=!1,this.trackWrites=null,this.mounted=!1,this.markCursor=null,this.cursorWrapper=null,this.lastSelectedViewDesc=void 0,this.input=new vs,this.prevDirectPlugins=[],this.pluginViews=[],this.requiresGeckoHackNode=!1,this.dragging=null,this._props=n,this.state=n.state,this.directPlugins=n.plugins||[],this.directPlugins.forEach(Xc),this.dispatch=this.dispatch.bind(this),this.dom=e&&e.mount||document.createElement("div"),e&&(e.appendChild?e.appendChild(this.dom):typeof e=="function"?e(this.dom):e.mount&&(this.mounted=!0)),this.editable=Jc(this),qc(this),this.nodeViews=Gc(this),this.docView=Tc(this.state.doc,Kc(this),ds(this),this.dom,this),this.domObserver=new Ns(this,(r,o,i,s)=>Tg(this,r,o,i,s)),this.domObserver.start(),Xm(this),this.updatePluginViews()}get composing(){return this.input.composing}get props(){if(this._props.state!=this.state){let e=this._props;this._props={};for(let n in e)this._props[n]=e[n];this._props.state=this.state}return this._props}update(e){e.handleDOMEvents!=this._props.handleDOMEvents&&Ms(this);let n=this._props;this._props=e,e.plugins&&(e.plugins.forEach(Xc),this.directPlugins=e.plugins),this.updateStateInner(e.state,n)}setProps(e){let n={};for(let r in this._props)n[r]=this._props[r];n.state=this.state;for(let r in e)n[r]=e[r];this.update(n)}updateState(e){this.updateStateInner(e,this._props)}updateStateInner(e,n){var r;let o=this.state,i=!1,s=!1;e.storedMarks&&this.composing&&(Sd(this),s=!0),this.state=e;let l=o.plugins!=e.plugins||this._props.plugins!=n.plugins;if(l||this._props.plugins!=n.plugins||this._props.nodeViews!=n.nodeViews){let h=Gc(this);Rg(h,this.nodeViews)&&(this.nodeViews=h,i=!0)}(l||n.handleDOMEvents!=this._props.handleDOMEvents)&&Ms(this),this.editable=Jc(this),qc(this);let a=ds(this),c=Kc(this),d=o.plugins!=e.plugins&&!o.doc.eq(e.doc)?"reset":e.scrollToSelection>o.scrollToSelection?"to selection":"preserve",u=i||!this.docView.matchesNode(e.doc,c,a);(u||!e.selection.eq(o.selection))&&(s=!0);let f=d=="preserve"&&s&&this.dom.style.overflowAnchor==null&&pm(this);if(s){this.domObserver.stop();let h=u&&(ve||de)&&!this.composing&&!o.selection.empty&&!e.selection.empty&&Og(o.selection,e.selection);if(u){let p=de?this.trackWrites=this.domSelectionRange().focusNode:null;this.composing&&(this.input.compositionNode=cg(this)),(i||!this.docView.update(e.doc,c,a,this))&&(this.docView.updateOuterDeco(c),this.docView.destroy(),this.docView=Tc(e.doc,c,a,this.dom,this)),p&&!this.trackWrites&&(h=!0)}h||!(this.input.mouseDown&&this.domObserver.currentSelection.eq(this.domSelectionRange())&&Lm(this))?gt(this,h):(dd(this,e.selection),this.domObserver.setCurSelection()),this.domObserver.start()}this.updatePluginViews(o),!((r=this.dragging)===null||r===void 0)&&r.node&&!o.doc.eq(e.doc)&&this.updateDraggedNode(this.dragging,o),d=="reset"?this.dom.scrollTop=0:d=="to selection"?this.scrollToSelection():f&&mm(f)}scrollToSelection(){let e=this.domSelectionRange().focusNode;if(!(!e||!this.dom.contains(e.nodeType==1?e:e.parentNode))){if(!this.someProp("handleScrollToSelection",n=>n(this)))if(this.state.selection instanceof L){let n=this.docView.domAfterPos(this.state.selection.from);n.nodeType==1&&xc(this,n.getBoundingClientRect(),e)}else xc(this,this.coordsAtPos(this.state.selection.head,1),e)}}destroyPluginViews(){let e;for(;e=this.pluginViews.pop();)e.destroy&&e.destroy()}updatePluginViews(e){if(!e||e.plugins!=this.state.plugins||this.directPlugins!=this.prevDirectPlugins){this.prevDirectPlugins=this.directPlugins,this.destroyPluginViews();for(let n=0;n0&&this.state.doc.nodeAt(i))==r.node&&(o=i)}this.dragging=new Jr(e.slice,e.move,o<0?void 0:L.create(this.state.doc,o))}someProp(e,n){let r=this._props&&this._props[e],o;if(r!=null&&(o=n?n(r):r))return o;for(let s=0;sn.ownerDocument.getSelection()),this._root=n}return e||document}updateRoot(){this._root=null}posAtCoords(e){return km(this,e)}coordsAtPos(e,n=1){return od(this,e,n)}domAtPos(e,n=0){return this.docView.domFromPos(e,n)}nodeDOM(e){let n=this.docView.descAt(e);return n?n.nodeDOM:null}posAtDOM(e,n,r=-1){let o=this.docView.posFromDOM(e,n,r);if(o==null)throw new RangeError("DOM position not inside the editor");return o}endOfTextblock(e,n){return Tm(this,n||this.state,e)}pasteHTML(e,n){return Zn(this,"",e,!1,n||new ClipboardEvent("paste"))}pasteText(e,n){return Zn(this,e,null,!0,n||new ClipboardEvent("paste"))}serializeForClipboard(e){return Is(this,e)}destroy(){this.docView&&(Ym(this),this.destroyPluginViews(),this.mounted?(this.docView.update(this.state.doc,[],ds(this),this),this.dom.textContent=""):this.dom.parentNode&&this.dom.parentNode.removeChild(this.dom),this.docView.destroy(),this.docView=null,om())}get isDestroyed(){return this.docView==null}dispatchEvent(e){return Zm(this,e)}domSelectionRange(){let e=this.domSelection();return e?we&&this.root.nodeType===11&&cm(this.dom.ownerDocument)==this.dom&&kg(this,e)||e:{focusNode:null,focusOffset:0,anchorNode:null,anchorOffset:0}}domSelection(){return this.root.getSelection()}};tr.prototype.dispatch=function(t){let e=this._props.dispatchTransaction;e?e.call(this,t):this.updateState(this.state.apply(t))};function Kc(t){let e=Object.create(null);return e.class="ProseMirror",e.contenteditable=String(t.editable),t.someProp("attributes",n=>{if(typeof n=="function"&&(n=n(t.state)),n)for(let r in n)r=="class"?e.class+=" "+n[r]:r=="style"?e.style=(e.style?e.style+";":"")+n[r]:!e[r]&&r!="contenteditable"&&r!="nodeName"&&(e[r]=String(n[r]))}),e.translate||(e.translate="no"),[te.node(0,t.state.doc.content.size,e)]}function qc(t){if(t.markCursor){let e=document.createElement("img");e.className="ProseMirror-separator",e.setAttribute("mark-placeholder","true"),e.setAttribute("alt",""),t.cursorWrapper={dom:e,deco:te.widget(t.state.selection.from,e,{raw:!0,marks:t.markCursor})}}else t.cursorWrapper=null}function Jc(t){return!t.someProp("editable",e=>e(t.state)===!1)}function Og(t,e){let n=Math.min(t.$anchor.sharedDepth(t.head),e.$anchor.sharedDepth(e.head));return t.$anchor.start(n)!=e.$anchor.start(n)}function Gc(t){let e=Object.create(null);function n(r){for(let o in r)Object.prototype.hasOwnProperty.call(e,o)||(e[o]=r[o])}return t.someProp("nodeViews",n),t.someProp("markViews",n),e}function Rg(t,e){let n=0,r=0;for(let o in t){if(t[o]!=e[o])return!0;n++}for(let o in e)r++;return n!=r}function Xc(t){if(t.spec.state||t.spec.filterTransaction||t.spec.appendTransaction)throw new RangeError("Plugins passed directly to the view must not have a state component")}var yt={8:"Backspace",9:"Tab",10:"Enter",12:"NumLock",13:"Enter",16:"Shift",17:"Control",18:"Alt",20:"CapsLock",27:"Escape",32:" ",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"ArrowLeft",38:"ArrowUp",39:"ArrowRight",40:"ArrowDown",44:"PrintScreen",45:"Insert",46:"Delete",59:";",61:"=",91:"Meta",92:"Meta",106:"*",107:"+",108:",",109:"-",110:".",111:"/",144:"NumLock",145:"ScrollLock",160:"Shift",161:"Shift",162:"Control",163:"Control",164:"Alt",165:"Alt",173:"-",186:";",187:"=",188:",",189:"-",190:".",191:"/",192:"`",219:"[",220:"\\",221:"]",222:"'"},to={48:")",49:"!",50:"@",51:"#",52:"$",53:"%",54:"^",55:"&",56:"*",57:"(",59:":",61:"+",173:"_",186:":",187:"+",188:"<",189:"_",190:">",191:"?",192:"~",219:"{",220:"|",221:"}",222:'"'},Dg=typeof navigator<"u"&&/Mac/.test(navigator.platform),Ig=typeof navigator<"u"&&/MSIE \d|Trident\/(?:[7-9]|\d{2,})\..*rv:(\d+)/.exec(navigator.userAgent);for(le=0;le<10;le++)yt[48+le]=yt[96+le]=String(le);var le;for(le=1;le<=24;le++)yt[le+111]="F"+le;var le;for(le=65;le<=90;le++)yt[le]=String.fromCharCode(le+32),to[le]=String.fromCharCode(le);var le;for(eo in yt)to.hasOwnProperty(eo)||(to[eo]=yt[eo]);var eo;function Ed(t){var e=Dg&&t.metaKey&&t.shiftKey&&!t.ctrlKey&&!t.altKey||Ig&&t.shiftKey&&t.key&&t.key.length==1||t.key=="Unidentified",n=!e&&t.key||(t.shiftKey?to:yt)[t.keyCode]||t.key||"Unidentified";return n=="Esc"&&(n="Escape"),n=="Del"&&(n="Delete"),n=="Left"&&(n="ArrowLeft"),n=="Up"&&(n="ArrowUp"),n=="Right"&&(n="ArrowRight"),n=="Down"&&(n="ArrowDown"),n}var Pg=typeof navigator<"u"&&/Mac|iP(hone|[oa]d)/.test(navigator.platform),Lg=typeof navigator<"u"&&/Win/.test(navigator.platform);function Bg(t){let e=t.split(/-(?!$)/),n=e[e.length-1];n=="Space"&&(n=" ");let r,o,i,s;for(let l=0;l{for(var n in e)Hg(t,n,{get:e[n],enumerable:!0})};function co(t){let{state:e,transaction:n}=t,{selection:r}=n,{doc:o}=n,{storedMarks:i}=n;return{...e,apply:e.apply.bind(e),applyTransaction:e.applyTransaction.bind(e),plugins:e.plugins,schema:e.schema,reconfigure:e.reconfigure.bind(e),toJSON:e.toJSON.bind(e),get storedMarks(){return i},get selection(){return r},get doc(){return o},get tr(){return r=n.selection,o=n.doc,i=n.storedMarks,n}}}var uo=class{constructor(t){this.editor=t.editor,this.rawCommands=this.editor.extensionManager.commands,this.customState=t.state}get hasCustomState(){return!!this.customState}get state(){return this.customState||this.editor.state}get commands(){let{rawCommands:t,editor:e,state:n}=this,{view:r}=e,{tr:o}=n,i=this.buildProps(o);return Object.fromEntries(Object.entries(t).map(([s,l])=>[s,(...c)=>{let d=l(...c)(i);return!o.getMeta("preventDispatch")&&!this.hasCustomState&&r.dispatch(o),d}]))}get chain(){return()=>this.createChain()}get can(){return()=>this.createCan()}createChain(t,e=!0){let{rawCommands:n,editor:r,state:o}=this,{view:i}=r,s=[],l=!!t,a=t||o.tr,c=()=>(!l&&e&&!a.getMeta("preventDispatch")&&!this.hasCustomState&&i.dispatch(a),s.every(u=>u===!0)),d={...Object.fromEntries(Object.entries(n).map(([u,f])=>[u,(...p)=>{let m=this.buildProps(a,e),g=f(...p)(m);return s.push(g),d}])),run:c};return d}createCan(t){let{rawCommands:e,state:n}=this,r=!1,o=t||n.tr,i=this.buildProps(o,r);return{...Object.fromEntries(Object.entries(e).map(([l,a])=>[l,(...c)=>a(...c)({...i,dispatch:void 0})])),chain:()=>this.createChain(o,r)}}buildProps(t,e=!0){let{rawCommands:n,editor:r,state:o}=this,{view:i}=r,s={tr:t,editor:r,view:i,state:co({state:o,transaction:t}),dispatch:e?()=>{}:void 0,chain:()=>this.createChain(t,e),can:()=>this.createCan(t),get commands(){return Object.fromEntries(Object.entries(n).map(([l,a])=>[l,(...c)=>a(...c)(s)]))}};return s}},Hd={};js(Hd,{blur:()=>$g,clearContent:()=>Fg,clearNodes:()=>Vg,command:()=>_g,createParagraphNear:()=>Wg,cut:()=>jg,deleteCurrentNode:()=>Ug,deleteNode:()=>Kg,deleteRange:()=>qg,deleteSelection:()=>Jg,enter:()=>Gg,exitCode:()=>Xg,extendMarkRange:()=>Yg,first:()=>Qg,focus:()=>ey,forEach:()=>ty,insertContent:()=>ny,insertContentAt:()=>iy,joinBackward:()=>ay,joinDown:()=>ly,joinForward:()=>cy,joinItemBackward:()=>dy,joinItemForward:()=>uy,joinTextblockBackward:()=>fy,joinTextblockForward:()=>hy,joinUp:()=>sy,keyboardShortcut:()=>my,lift:()=>gy,liftEmptyBlock:()=>yy,liftListItem:()=>by,newlineInCode:()=>wy,resetAttributes:()=>xy,scrollIntoView:()=>ky,selectAll:()=>Sy,selectNodeBackward:()=>Cy,selectNodeForward:()=>vy,selectParentNode:()=>My,selectTextblockEnd:()=>Ty,selectTextblockStart:()=>Ay,setContent:()=>Ey,setMark:()=>Vy,setMeta:()=>_y,setNode:()=>Wy,setNodeSelection:()=>jy,setTextDirection:()=>Uy,setTextSelection:()=>Ky,sinkListItem:()=>qy,splitBlock:()=>Jy,splitListItem:()=>Gy,toggleList:()=>Xy,toggleMark:()=>Yy,toggleNode:()=>Qy,toggleWrap:()=>Zy,undoInputRule:()=>eb,unsetAllMarks:()=>tb,unsetMark:()=>nb,unsetTextDirection:()=>rb,updateAttributes:()=>ob,wrapIn:()=>ib,wrapInList:()=>sb});var $g=()=>({editor:t,view:e})=>(requestAnimationFrame(()=>{var n;t.isDestroyed||(e.dom.blur(),(n=window?.getSelection())==null||n.removeAllRanges())}),!0),Fg=(t=!0)=>({commands:e})=>e.setContent("",{emitUpdate:t}),Vg=()=>({state:t,tr:e,dispatch:n})=>{let{selection:r}=e,{ranges:o}=r;return n&&o.forEach(({$from:i,$to:s})=>{t.doc.nodesBetween(i.pos,s.pos,(l,a)=>{if(l.type.isText)return;let{doc:c,mapping:d}=e,u=c.resolve(d.map(a)),f=c.resolve(d.map(a+l.nodeSize)),h=u.blockRange(f);if(!h)return;let p=ft(h);if(l.type.isTextblock){let{defaultType:m}=u.parent.contentMatchAt(u.index());e.setNodeMarkup(h.start,m)}(p||p===0)&&e.lift(h,p)})}),!0},_g=t=>e=>t(e),Wg=()=>({state:t,dispatch:e})=>es(t,e),jg=(t,e)=>({editor:n,tr:r})=>{let{state:o}=n,i=o.doc.slice(t.from,t.to);r.deleteRange(t.from,t.to);let s=r.mapping.map(e);return r.insert(s,i.content),r.setSelection(new D(r.doc.resolve(Math.max(s-1,0)))),!0},Ug=()=>({tr:t,dispatch:e})=>{let{selection:n}=t,r=n.$anchor.node();if(r.content.size>0)return!1;let o=t.selection.$anchor;for(let i=o.depth;i>0;i-=1)if(o.node(i).type===r.type){if(e){let l=o.before(i),a=o.after(i);t.delete(l,a).scrollIntoView()}return!0}return!1};function ne(t,e){if(typeof t=="string"){if(!e.nodes[t])throw Error(`There is no node type named '${t}'. Maybe you forgot to add the extension?`);return e.nodes[t]}return t}var Kg=t=>({tr:e,state:n,dispatch:r})=>{let o=ne(t,n.schema),i=e.selection.$anchor;for(let s=i.depth;s>0;s-=1)if(i.node(s).type===o){if(r){let a=i.before(s),c=i.after(s);e.delete(a,c).scrollIntoView()}return!0}return!1},qg=t=>({tr:e,dispatch:n})=>{let{from:r,to:o}=t;return n&&e.delete(r,o),!0},Jg=()=>({state:t,dispatch:e})=>Vr(t,e),Gg=()=>({commands:t})=>t.keyboardShortcut("Enter"),Xg=()=>({state:t,dispatch:e})=>Zi(t,e);function Us(t){return Object.prototype.toString.call(t)==="[object RegExp]"}function lo(t,e,n={strict:!0}){let r=Object.keys(e);return r.length?r.every(o=>n.strict?e[o]===t[o]:Us(e[o])?e[o].test(t[o]):e[o]===t[o]):!0}function $d(t,e,n={}){return t.find(r=>r.type===e&&lo(Object.fromEntries(Object.keys(n).map(o=>[o,r.attrs[o]])),n))}function Od(t,e,n={}){return!!$d(t,e,n)}function Ks(t,e,n){var r;if(!t||!e)return;let o=t.parent.childAfter(t.parentOffset);if((!o.node||!o.node.marks.some(d=>d.type===e))&&(o=t.parent.childBefore(t.parentOffset)),!o.node||!o.node.marks.some(d=>d.type===e)||(n=n||((r=o.node.marks[0])==null?void 0:r.attrs),!$d([...o.node.marks],e,n)))return;let s=o.index,l=t.start()+o.offset,a=s+1,c=l+o.node.nodeSize;for(;s>0&&Od([...t.parent.child(s-1).marks],e,n);)s-=1,l-=t.parent.child(s).nodeSize;for(;a({tr:n,state:r,dispatch:o})=>{let i=wt(t,r.schema),{doc:s,selection:l}=n,{$from:a,from:c,to:d}=l;if(o){let u=Ks(a,i,e);if(u&&u.from<=c&&u.to>=d){let f=D.create(s,u.from,u.to);n.setSelection(f)}}return!0},Qg=t=>e=>{let n=typeof t=="function"?t(e):t;for(let r=0;r({editor:n,view:r,tr:o,dispatch:i})=>{e={scrollIntoView:!0,...e};let s=()=>{(qs()||Zg())&&r.dom.focus(),requestAnimationFrame(()=>{n.isDestroyed||(r.focus(),e?.scrollIntoView&&n.commands.scrollIntoView())})};if(r.hasFocus()&&t===null||t===!1)return!0;if(i&&t===null&&!fo(n.state.selection))return s(),!0;let l=Fd(o.doc,t)||n.state.selection,a=n.state.selection.eq(l);return i&&(a||o.setSelection(l),a&&o.storedMarks&&o.setStoredMarks(o.storedMarks),s()),!0},ty=(t,e)=>n=>t.every((r,o)=>e(r,{...n,index:o})),ny=(t,e)=>({tr:n,commands:r})=>r.insertContentAt({from:n.selection.from,to:n.selection.to},t,e),Vd=t=>{let e=t.childNodes;for(let n=e.length-1;n>=0;n-=1){let r=e[n];r.nodeType===3&&r.nodeValue&&/^(\n\s\s|\n)$/.test(r.nodeValue)?t.removeChild(r):r.nodeType===1&&Vd(r)}return t};function no(t){if(typeof window>"u")throw new Error("[tiptap error]: there is no window object available, so this function cannot be used");let e=`${t}`,n=new window.DOMParser().parseFromString(e,"text/html").body;return Vd(n)}function ir(t,e,n){if(t instanceof ie||t instanceof v)return t;n={slice:!0,parseOptions:{},...n};let r=typeof t=="object"&&t!==null,o=typeof t=="string";if(r)try{if(Array.isArray(t)&&t.length>0)return v.fromArray(t.map(l=>e.nodeFromJSON(l)));let s=e.nodeFromJSON(t);return n.errorOnInvalidContent&&s.check(),s}catch(i){if(n.errorOnInvalidContent)throw new Error("[tiptap error]: Invalid JSON content",{cause:i});return console.warn("[tiptap warn]: Invalid content.","Passed value:",t,"Error:",i),ir("",e,n)}if(o){if(n.errorOnInvalidContent){let s=!1,l="",a=new fn({topNode:e.spec.topNode,marks:e.spec.marks,nodes:e.spec.nodes.append({__tiptap__private__unknown__catch__all__node:{content:"inline*",group:"block",parseDOM:[{tag:"*",getAttrs:c=>(s=!0,l=typeof c=="string"?c:c.outerHTML,null)}]}})});if(n.slice?Xe.fromSchema(a).parseSlice(no(t),n.parseOptions):Xe.fromSchema(a).parse(no(t),n.parseOptions),n.errorOnInvalidContent&&s)throw new Error("[tiptap error]: Invalid HTML content",{cause:new Error(`Invalid element found: ${l}`)})}let i=Xe.fromSchema(e);return n.slice?i.parseSlice(no(t),n.parseOptions).content:i.parse(no(t),n.parseOptions)}return ir("",e,n)}function ry(t,e,n){let r=t.steps.length-1;if(r{s===0&&(s=d)}),t.setSelection(I.near(t.doc.resolve(s),n))}var oy=t=>!("type"in t),iy=(t,e,n)=>({tr:r,dispatch:o,editor:i})=>{var s;if(o){n={parseOptions:i.options.parseOptions,updateSelection:!0,applyInputRules:!1,applyPasteRules:!1,...n};let l,a=g=>{i.emit("contentError",{editor:i,error:g,disableCollaboration:()=>{"collaboration"in i.storage&&typeof i.storage.collaboration=="object"&&i.storage.collaboration&&(i.storage.collaboration.isDisabled=!0)}})},c={preserveWhitespace:"full",...n.parseOptions};if(!n.errorOnInvalidContent&&!i.options.enableContentCheck&&i.options.emitContentError)try{ir(e,i.schema,{parseOptions:c,errorOnInvalidContent:!0})}catch(g){a(g)}try{l=ir(e,i.schema,{parseOptions:c,errorOnInvalidContent:(s=n.errorOnInvalidContent)!=null?s:i.options.enableContentCheck})}catch(g){return a(g),!1}let{from:d,to:u}=typeof t=="number"?{from:t,to:t}:{from:t.from,to:t.to},f=!0,h=!0;if((oy(l)?l:[l]).forEach(g=>{g.check(),f=f?g.isText&&g.marks.length===0:!1,h=h?g.isBlock:!1}),d===u&&h){let{parent:g}=r.doc.resolve(d);g.isTextblock&&!g.type.spec.code&&!g.childCount&&(d-=1,u+=1)}let m;if(f){if(Array.isArray(e))m=e.map(g=>g.text||"").join("");else if(e instanceof v){let g="";e.forEach(y=>{y.text&&(g+=y.text)}),m=g}else typeof e=="object"&&e&&e.text?m=e.text:m=e;r.insertText(m,d,u)}else{m=l;let g=r.doc.resolve(d),y=g.node(),w=g.parentOffset===0,b=y.isText||y.isTextblock,C=y.content.size>0;w&&b&&C&&(d=Math.max(0,d-1)),r.replaceWith(d,u,m)}n.updateSelection&&ry(r,r.steps.length-1,-1),n.applyInputRules&&r.setMeta("applyInputRules",{from:d,text:m}),n.applyPasteRules&&r.setMeta("applyPasteRules",{from:d,text:m})}return!0},sy=()=>({state:t,dispatch:e})=>ac(t,e),ly=()=>({state:t,dispatch:e})=>cc(t,e),ay=()=>({state:t,dispatch:e})=>Ui(t,e),cy=()=>({state:t,dispatch:e})=>Ji(t,e),dy=()=>({state:t,dispatch:e,tr:n})=>{try{let r=Wt(t.doc,t.selection.$from.pos,-1);return r==null?!1:(n.join(r,2),e&&e(n),!0)}catch{return!1}},uy=()=>({state:t,dispatch:e,tr:n})=>{try{let r=Wt(t.doc,t.selection.$from.pos,1);return r==null?!1:(n.join(r,2),e&&e(n),!0)}catch{return!1}},fy=()=>({state:t,dispatch:e})=>oc(t,e),hy=()=>({state:t,dispatch:e})=>ic(t,e);function _d(){return typeof navigator<"u"?/Mac/.test(navigator.platform):!1}function py(t){let e=t.split(/-(?!$)/),n=e[e.length-1];n==="Space"&&(n=" ");let r,o,i,s;for(let l=0;l({editor:e,view:n,tr:r,dispatch:o})=>{let i=py(t).split(/-(?!$)/),s=i.find(c=>!["Alt","Ctrl","Meta","Shift"].includes(c)),l=new KeyboardEvent("keydown",{key:s==="Space"?" ":s,altKey:i.includes("Alt"),ctrlKey:i.includes("Ctrl"),metaKey:i.includes("Meta"),shiftKey:i.includes("Shift"),bubbles:!0,cancelable:!0}),a=e.captureTransaction(()=>{n.someProp("handleKeyDown",c=>c(n,l))});return a?.steps.forEach(c=>{let d=c.map(r.mapping);d&&o&&r.maybeStep(d)}),!0};function Ze(t,e,n={}){let{from:r,to:o,empty:i}=t.selection,s=e?ne(e,t.schema):null,l=[];t.doc.nodesBetween(r,o,(u,f)=>{if(u.isText)return;let h=Math.max(r,f),p=Math.min(o,f+u.nodeSize);l.push({node:u,from:h,to:p})});let a=o-r,c=l.filter(u=>s?s.name===u.node.type.name:!0).filter(u=>lo(u.node.attrs,n,{strict:!1}));return i?!!c.length:c.reduce((u,f)=>u+f.to-f.from,0)>=a}var gy=(t,e={})=>({state:n,dispatch:r})=>{let o=ne(t,n.schema);return Ze(n,o,e)?dc(n,r):!1},yy=()=>({state:t,dispatch:e})=>ts(t,e),by=t=>({state:e,dispatch:n})=>{let r=ne(t,e.schema);return gc(r)(e,n)},wy=()=>({state:t,dispatch:e})=>Yi(t,e);function ho(t,e){return e.nodes[t]?"node":e.marks[t]?"mark":null}function Rd(t,e){let n=typeof e=="string"?[e]:e;return Object.keys(t).reduce((r,o)=>(n.includes(o)||(r[o]=t[o]),r),{})}var xy=(t,e)=>({tr:n,state:r,dispatch:o})=>{let i=null,s=null,l=ho(typeof t=="string"?t:t.name,r.schema);if(!l)return!1;l==="node"&&(i=ne(t,r.schema)),l==="mark"&&(s=wt(t,r.schema));let a=!1;return n.selection.ranges.forEach(c=>{r.doc.nodesBetween(c.$from.pos,c.$to.pos,(d,u)=>{i&&i===d.type&&(a=!0,o&&n.setNodeMarkup(u,void 0,Rd(d.attrs,e))),s&&d.marks.length&&d.marks.forEach(f=>{s===f.type&&(a=!0,o&&n.addMark(u,u+d.nodeSize,s.create(Rd(f.attrs,e))))})})}),a},ky=()=>({tr:t,dispatch:e})=>(e&&t.scrollIntoView(),!0),Sy=()=>({tr:t,dispatch:e})=>{if(e){let n=new ke(t.doc);t.setSelection(n)}return!0},Cy=()=>({state:t,dispatch:e})=>Ki(t,e),vy=()=>({state:t,dispatch:e})=>Gi(t,e),My=()=>({state:t,dispatch:e})=>uc(t,e),Ty=()=>({state:t,dispatch:e})=>rs(t,e),Ay=()=>({state:t,dispatch:e})=>ns(t,e);function _s(t,e,n={},r={}){return ir(t,e,{slice:!1,parseOptions:n,errorOnInvalidContent:r.errorOnInvalidContent})}var Ey=(t,{errorOnInvalidContent:e,emitUpdate:n=!0,parseOptions:r={}}={})=>({editor:o,tr:i,dispatch:s,commands:l})=>{let{doc:a}=i;if(r.preserveWhitespace!=="full"){let c=_s(t,o.schema,r,{errorOnInvalidContent:e??o.options.enableContentCheck});return s&&i.replaceWith(0,a.content.size,c).setMeta("preventUpdate",!n),!0}return s&&i.setMeta("preventUpdate",!n),l.insertContentAt({from:0,to:a.content.size},t,{parseOptions:r,errorOnInvalidContent:e??o.options.enableContentCheck})};function Wd(t,e){let n=wt(e,t.schema),{from:r,to:o,empty:i}=t.selection,s=[];i?(t.storedMarks&&s.push(...t.storedMarks),s.push(...t.selection.$head.marks())):t.doc.nodesBetween(r,o,a=>{s.push(...a.marks)});let l=s.find(a=>a.type.name===n.name);return l?{...l.attrs}:{}}function Js(t,e){let n=new Tt(t);return e.forEach(r=>{r.steps.forEach(o=>{n.step(o)})}),n}function sr(t){for(let e=0;e{e(r)&&n.push({node:r,pos:o})}),n}function jd(t,e,n){let r=[];return t.nodesBetween(e.from,e.to,(o,i)=>{n(o)&&r.push({node:o,pos:i})}),r}function Gs(t,e){for(let n=t.depth;n>0;n-=1){let r=t.node(n);if(e(r))return{pos:n>0?t.before(n):0,start:t.start(n),depth:n,node:r}}}function et(t){return e=>Gs(e.$from,t)}function B(t,e,n){return t.config[e]===void 0&&t.parent?B(t.parent,e,n):typeof t.config[e]=="function"?t.config[e].bind({...n,parent:t.parent?B(t.parent,e,n):null}):t.config[e]}function Xs(t){return t.map(e=>{let n={name:e.name,options:e.options,storage:e.storage},r=B(e,"addExtensions",n);return r?[e,...Xs(r())]:e}).flat(10)}function Ys(t,e){let n=ct.fromSchema(e).serializeFragment(t),o=document.implementation.createHTMLDocument().createElement("div");return o.appendChild(n),o.innerHTML}function Ud(t){return typeof t=="function"}function G(t,e=void 0,...n){return Ud(t)?e?t.bind(e)(...n):t(...n):t}function Ny(t={}){return Object.keys(t).length===0&&t.constructor===Object}function An(t){let e=t.filter(o=>o.type==="extension"),n=t.filter(o=>o.type==="node"),r=t.filter(o=>o.type==="mark");return{baseExtensions:e,nodeExtensions:n,markExtensions:r}}function Kd(t){let e=[],{nodeExtensions:n,markExtensions:r}=An(t),o=[...n,...r],i={default:null,validate:void 0,rendered:!0,renderHTML:null,parseHTML:null,keepOnSplit:!0,isRequired:!1};return t.forEach(s=>{let l={name:s.name,options:s.options,storage:s.storage,extensions:o},a=B(s,"addGlobalAttributes",l);if(!a)return;a().forEach(d=>{d.types.forEach(u=>{Object.entries(d.attributes).forEach(([f,h])=>{e.push({type:u,name:f,attribute:{...i,...h}})})})})}),o.forEach(s=>{let l={name:s.name,options:s.options,storage:s.storage},a=B(s,"addAttributes",l);if(!a)return;let c=a();Object.entries(c).forEach(([d,u])=>{let f={...i,...u};typeof f?.default=="function"&&(f.default=f.default()),f?.isRequired&&f?.default===void 0&&delete f.default,e.push({type:s.name,name:d,attribute:f})})}),e}function R(...t){return t.filter(e=>!!e).reduce((e,n)=>{let r={...e};return Object.entries(n).forEach(([o,i])=>{if(!r[o]){r[o]=i;return}if(o==="class"){let l=i?String(i).split(" "):[],a=r[o]?r[o].split(" "):[],c=l.filter(d=>!a.includes(d));r[o]=[...a,...c].join(" ")}else if(o==="style"){let l=i?i.split(";").map(d=>d.trim()).filter(Boolean):[],a=r[o]?r[o].split(";").map(d=>d.trim()).filter(Boolean):[],c=new Map;a.forEach(d=>{let[u,f]=d.split(":").map(h=>h.trim());c.set(u,f)}),l.forEach(d=>{let[u,f]=d.split(":").map(h=>h.trim());c.set(u,f)}),r[o]=Array.from(c.entries()).map(([d,u])=>`${d}: ${u}`).join("; ")}else r[o]=i}),r},{})}function ao(t,e){return e.filter(n=>n.type===t.type.name).filter(n=>n.attribute.rendered).map(n=>n.attribute.renderHTML?n.attribute.renderHTML(t.attrs)||{}:{[n.name]:t.attrs[n.name]}).reduce((n,r)=>R(n,r),{})}function Oy(t){return typeof t!="string"?t:t.match(/^[+-]?(?:\d*\.)?\d+$/)?Number(t):t==="true"?!0:t==="false"?!1:t}function Dd(t,e){return"style"in t?t:{...t,getAttrs:n=>{let r=t.getAttrs?t.getAttrs(n):t.attrs;if(r===!1)return!1;let o=e.reduce((i,s)=>{let l=s.attribute.parseHTML?s.attribute.parseHTML(n):Oy(n.getAttribute(s.name));return l==null?i:{...i,[s.name]:l}},{});return{...r,...o}}}}function Id(t){return Object.fromEntries(Object.entries(t).filter(([e,n])=>e==="attrs"&&Ny(n)?!1:n!=null))}function Pd(t){var e,n;let r={};return!((e=t?.attribute)!=null&&e.isRequired)&&"default"in(t?.attribute||{})&&(r.default=t.attribute.default),((n=t?.attribute)==null?void 0:n.validate)!==void 0&&(r.validate=t.attribute.validate),[t.name,r]}function Ry(t,e){var n;let r=Kd(t),{nodeExtensions:o,markExtensions:i}=An(t),s=(n=o.find(c=>B(c,"topNode")))==null?void 0:n.name,l=Object.fromEntries(o.map(c=>{let d=r.filter(y=>y.type===c.name),u={name:c.name,options:c.options,storage:c.storage,editor:e},f=t.reduce((y,w)=>{let b=B(w,"extendNodeSchema",u);return{...y,...b?b(c):{}}},{}),h=Id({...f,content:G(B(c,"content",u)),marks:G(B(c,"marks",u)),group:G(B(c,"group",u)),inline:G(B(c,"inline",u)),atom:G(B(c,"atom",u)),selectable:G(B(c,"selectable",u)),draggable:G(B(c,"draggable",u)),code:G(B(c,"code",u)),whitespace:G(B(c,"whitespace",u)),linebreakReplacement:G(B(c,"linebreakReplacement",u)),defining:G(B(c,"defining",u)),isolating:G(B(c,"isolating",u)),attrs:Object.fromEntries(d.map(Pd))}),p=G(B(c,"parseHTML",u));p&&(h.parseDOM=p.map(y=>Dd(y,d)));let m=B(c,"renderHTML",u);m&&(h.toDOM=y=>m({node:y,HTMLAttributes:ao(y,d)}));let g=B(c,"renderText",u);return g&&(h.toText=g),[c.name,h]})),a=Object.fromEntries(i.map(c=>{let d=r.filter(g=>g.type===c.name),u={name:c.name,options:c.options,storage:c.storage,editor:e},f=t.reduce((g,y)=>{let w=B(y,"extendMarkSchema",u);return{...g,...w?w(c):{}}},{}),h=Id({...f,inclusive:G(B(c,"inclusive",u)),excludes:G(B(c,"excludes",u)),group:G(B(c,"group",u)),spanning:G(B(c,"spanning",u)),code:G(B(c,"code",u)),attrs:Object.fromEntries(d.map(Pd))}),p=G(B(c,"parseHTML",u));p&&(h.parseDOM=p.map(g=>Dd(g,d)));let m=B(c,"renderHTML",u);return m&&(h.toDOM=g=>m({mark:g,HTMLAttributes:ao(g,d)})),[c.name,h]}));return new fn({topNode:s,nodes:l,marks:a})}function Dy(t){let e=t.filter((n,r)=>t.indexOf(n)!==r);return Array.from(new Set(e))}function Qs(t){return t.sort((n,r)=>{let o=B(n,"priority")||100,i=B(r,"priority")||100;return o>i?-1:or.name));return n.length&&console.warn(`[tiptap warn]: Duplicate extension names found: [${n.map(r=>`'${r}'`).join(", ")}]. This can lead to issues.`),e}function Jd(t,e,n){let{from:r,to:o}=e,{blockSeparator:i=` + +`,textSerializers:s={}}=n||{},l="";return t.nodesBetween(r,o,(a,c,d,u)=>{var f;a.isBlock&&c>r&&(l+=i);let h=s?.[a.type.name];if(h)return d&&(l+=h({node:a,pos:c,parent:d,index:u,range:e})),!1;a.isText&&(l+=(f=a?.text)==null?void 0:f.slice(Math.max(r,c)-c,o-c))}),l}function Iy(t,e){let n={from:0,to:t.content.size};return Jd(t,n,e)}function Gd(t){return Object.fromEntries(Object.entries(t.nodes).filter(([,e])=>e.spec.toText).map(([e,n])=>[e,n.spec.toText]))}function Py(t,e){let n=ne(e,t.schema),{from:r,to:o}=t.selection,i=[];t.doc.nodesBetween(r,o,l=>{i.push(l)});let s=i.reverse().find(l=>l.type.name===n.name);return s?{...s.attrs}:{}}function Zs(t,e){let n=ho(typeof e=="string"?e:e.name,t.schema);return n==="node"?Py(t,e):n==="mark"?Wd(t,e):{}}function Ly(t,e=JSON.stringify){let n={};return t.filter(r=>{let o=e(r);return Object.prototype.hasOwnProperty.call(n,o)?!1:n[o]=!0})}function By(t){let e=Ly(t);return e.length===1?e:e.filter((n,r)=>!e.filter((i,s)=>s!==r).some(i=>n.oldRange.from>=i.oldRange.from&&n.oldRange.to<=i.oldRange.to&&n.newRange.from>=i.newRange.from&&n.newRange.to<=i.newRange.to))}function el(t){let{mapping:e,steps:n}=t,r=[];return e.maps.forEach((o,i)=>{let s=[];if(o.ranges.length)o.forEach((l,a)=>{s.push({from:l,to:a})});else{let{from:l,to:a}=n[i];if(l===void 0||a===void 0)return;s.push({from:l,to:a})}s.forEach(({from:l,to:a})=>{let c=e.slice(i).map(l,-1),d=e.slice(i).map(a),u=e.invert().map(c,-1),f=e.invert().map(d);r.push({oldRange:{from:u,to:f},newRange:{from:c,to:d}})})}),By(r)}function po(t,e,n){let r=[];return t===e?n.resolve(t).marks().forEach(o=>{let i=n.resolve(t),s=Ks(i,o.type);s&&r.push({mark:o,...s})}):n.nodesBetween(t,e,(o,i)=>{!o||o?.nodeSize===void 0||r.push(...o.marks.map(s=>({from:i,to:i+o.nodeSize,mark:s})))}),r}var Xd=(t,e,n,r=20)=>{let o=t.doc.resolve(n),i=r,s=null;for(;i>0&&s===null;){let l=o.node(i);l?.type.name===e?s=l:i-=1}return[s,i]};function $s(t,e){return e.nodes[t]||e.marks[t]||null}function so(t,e,n){return Object.fromEntries(Object.entries(n).filter(([r])=>{let o=t.find(i=>i.type===e&&i.name===r);return o?o.attribute.keepOnSplit:!1}))}var zy=(t,e=500)=>{let n="",r=t.parentOffset;return t.parent.nodesBetween(Math.max(0,r-e),r,(o,i,s,l)=>{var a,c;let d=((c=(a=o.type.spec).toText)==null?void 0:c.call(a,{node:o,pos:i,parent:s,index:l}))||o.textContent||"%leaf%";n+=o.isAtom&&!o.isText?d:d.slice(0,Math.max(0,r-i))}),n};function Ws(t,e,n={}){let{empty:r,ranges:o}=t.selection,i=e?wt(e,t.schema):null;if(r)return!!(t.storedMarks||t.selection.$from.marks()).filter(u=>i?i.name===u.type.name:!0).find(u=>lo(u.attrs,n,{strict:!1}));let s=0,l=[];if(o.forEach(({$from:u,$to:f})=>{let h=u.pos,p=f.pos;t.doc.nodesBetween(h,p,(m,g)=>{if(!m.isText&&!m.marks.length)return;let y=Math.max(h,g),w=Math.min(p,g+m.nodeSize),b=w-y;s+=b,l.push(...m.marks.map(C=>({mark:C,from:y,to:w})))})}),s===0)return!1;let a=l.filter(u=>i?i.name===u.mark.type.name:!0).filter(u=>lo(u.mark.attrs,n,{strict:!1})).reduce((u,f)=>u+f.to-f.from,0),c=l.filter(u=>i?u.mark.type!==i&&u.mark.type.excludes(i):!0).reduce((u,f)=>u+f.to-f.from,0);return(a>0?a+c:a)>=s}function tl(t,e,n={}){if(!e)return Ze(t,null,n)||Ws(t,null,n);let r=ho(e,t.schema);return r==="node"?Ze(t,e,n):r==="mark"?Ws(t,e,n):!1}var Yd=(t,e)=>{let{$from:n,$to:r,$anchor:o}=t.selection;if(e){let i=et(l=>l.type.name===e)(t.selection);if(!i)return!1;let s=t.doc.resolve(i.pos+1);return o.pos+1===s.end()}return!(r.parentOffset{let{$from:e,$to:n}=t.selection;return!(e.parentOffset>0||e.pos!==n.pos)};function Ld(t,e){return Array.isArray(e)?e.some(n=>(typeof n=="string"?n:n.name)===t.name):e}function Bd(t,e){let{nodeExtensions:n}=An(e),r=n.find(s=>s.name===t);if(!r)return!1;let o={name:r.name,options:r.options,storage:r.storage},i=G(B(r,"group",o));return typeof i!="string"?!1:i.split(" ").includes("list")}function lr(t,{checkChildren:e=!0,ignoreWhitespace:n=!1}={}){var r;if(n){if(t.type.name==="hardBreak")return!0;if(t.isText)return/^\s*$/m.test((r=t.text)!=null?r:"")}if(t.isText)return!t.text;if(t.isAtom||t.isLeaf)return!1;if(t.content.childCount===0)return!0;if(e){let o=!0;return t.content.forEach(i=>{o!==!1&&(lr(i,{ignoreWhitespace:n,checkChildren:e})||(o=!1))}),o}return!1}function mo(t){return t instanceof L}var Zd=class eu{constructor(e){this.position=e}static fromJSON(e){return new eu(e.position)}toJSON(){return{position:this.position}}};function Hy(t,e){let n=e.mapping.mapResult(t.position);return{position:new Zd(n.pos),mapResult:n}}function $y(t){return new Zd(t)}function tu(t,e,n){let o=t.state.doc.content.size,i=bt(e,0,o),s=bt(n,0,o),l=t.coordsAtPos(i),a=t.coordsAtPos(s,-1),c=Math.min(l.top,a.top),d=Math.max(l.bottom,a.bottom),u=Math.min(l.left,a.left),f=Math.max(l.right,a.right),h=f-u,p=d-c,y={top:c,bottom:d,left:u,right:f,width:h,height:p,x:u,y:c};return{...y,toJSON:()=>y}}function Fy(t,e,n){var r;let{selection:o}=e,i=null;if(fo(o)&&(i=o.$cursor),i){let l=(r=t.storedMarks)!=null?r:i.marks();return i.parent.type.allowsMarkType(n)&&(!!n.isInSet(l)||!l.some(c=>c.type.excludes(n)))}let{ranges:s}=o;return s.some(({$from:l,$to:a})=>{let c=l.depth===0?t.doc.inlineContent&&t.doc.type.allowsMarkType(n):!1;return t.doc.nodesBetween(l.pos,a.pos,(d,u,f)=>{if(c)return!1;if(d.isInline){let h=!f||f.type.allowsMarkType(n),p=!!n.isInSet(d.marks)||!d.marks.some(m=>m.type.excludes(n));c=h&&p}return!c}),c})}var Vy=(t,e={})=>({tr:n,state:r,dispatch:o})=>{let{selection:i}=n,{empty:s,ranges:l}=i,a=wt(t,r.schema);if(o)if(s){let c=Wd(r,a);n.addStoredMark(a.create({...c,...e}))}else l.forEach(c=>{let d=c.$from.pos,u=c.$to.pos;r.doc.nodesBetween(d,u,(f,h)=>{let p=Math.max(h,d),m=Math.min(h+f.nodeSize,u);f.marks.find(y=>y.type===a)?f.marks.forEach(y=>{a===y.type&&n.addMark(p,m,a.create({...y.attrs,...e}))}):n.addMark(p,m,a.create(e))})});return Fy(r,n,a)},_y=(t,e)=>({tr:n})=>(n.setMeta(t,e),!0),Wy=(t,e={})=>({state:n,dispatch:r,chain:o})=>{let i=ne(t,n.schema),s;return n.selection.$anchor.sameParent(n.selection.$head)&&(s=n.selection.$anchor.parent.attrs),i.isTextblock?o().command(({commands:l})=>is(i,{...s,...e})(n)?!0:l.clearNodes()).command(({state:l})=>is(i,{...s,...e})(l,r)).run():(console.warn('[tiptap warn]: Currently "setNode()" only supports text block nodes.'),!1)},jy=t=>({tr:e,dispatch:n})=>{if(n){let{doc:r}=e,o=bt(t,0,r.content.size),i=L.create(r,o);e.setSelection(i)}return!0},Uy=(t,e)=>({tr:n,state:r,dispatch:o})=>{let{selection:i}=r,s,l;return typeof e=="number"?(s=e,l=e):e&&"from"in e&&"to"in e?(s=e.from,l=e.to):(s=i.from,l=i.to),o&&n.doc.nodesBetween(s,l,(a,c)=>{a.isText||n.setNodeMarkup(c,void 0,{...a.attrs,dir:t})}),!0},Ky=t=>({tr:e,dispatch:n})=>{if(n){let{doc:r}=e,{from:o,to:i}=typeof t=="number"?{from:t,to:t}:t,s=D.atStart(r).from,l=D.atEnd(r).to,a=bt(o,s,l),c=bt(i,s,l),d=D.create(r,a,c);e.setSelection(d)}return!0},qy=t=>({state:e,dispatch:n})=>{let r=ne(t,e.schema);return yc(r)(e,n)};function zd(t,e){let n=t.storedMarks||t.selection.$to.parentOffset&&t.selection.$from.marks();if(n){let r=n.filter(o=>e?.includes(o.type.name));t.tr.ensureMarks(r)}}var Jy=({keepMarks:t=!0}={})=>({tr:e,state:n,dispatch:r,editor:o})=>{let{selection:i,doc:s}=e,{$from:l,$to:a}=i,c=o.extensionManager.attributes,d=so(c,l.node().type.name,l.node().attrs);if(i instanceof L&&i.node.isBlock)return!l.parentOffset||!Ee(s,l.pos)?!1:(r&&(t&&zd(n,o.extensionManager.splittableMarks),e.split(l.pos).scrollIntoView()),!0);if(!l.parent.isBlock)return!1;let u=a.parentOffset===a.parent.content.size,f=l.depth===0?void 0:sr(l.node(-1).contentMatchAt(l.indexAfter(-1))),h=u&&f?[{type:f,attrs:d}]:void 0,p=Ee(e.doc,e.mapping.map(l.pos),1,h);if(!h&&!p&&Ee(e.doc,e.mapping.map(l.pos),1,f?[{type:f}]:void 0)&&(p=!0,h=f?[{type:f,attrs:d}]:void 0),r){if(p&&(i instanceof D&&e.deleteSelection(),e.split(e.mapping.map(l.pos),1,h),f&&!u&&!l.parentOffset&&l.parent.type!==f)){let m=e.mapping.map(l.before()),g=e.doc.resolve(m);l.node(-1).canReplaceWith(g.index(),g.index()+1,f)&&e.setNodeMarkup(e.mapping.map(l.before()),f)}t&&zd(n,o.extensionManager.splittableMarks),e.scrollIntoView()}return p},Gy=(t,e={})=>({tr:n,state:r,dispatch:o,editor:i})=>{var s;let l=ne(t,r.schema),{$from:a,$to:c}=r.selection,d=r.selection.node;if(d&&d.isBlock||a.depth<2||!a.sameParent(c))return!1;let u=a.node(-1);if(u.type!==l)return!1;let f=i.extensionManager.attributes;if(a.parent.content.size===0&&a.node(-1).childCount===a.indexAfter(-1)){if(a.depth===2||a.node(-3).type!==l||a.index(-2)!==a.node(-2).childCount-1)return!1;if(o){let y=v.empty,w=a.index(-1)?1:a.index(-2)?2:3;for(let O=a.depth-w;O>=a.depth-3;O-=1)y=v.from(a.node(O).copy(y));let b=a.indexAfter(-1){if(k>-1)return!1;O.isTextblock&&O.content.size===0&&(k=T+1)}),k>-1&&n.setSelection(D.near(n.doc.resolve(k))),n.scrollIntoView()}return!0}let h=c.pos===a.end()?u.contentMatchAt(0).defaultType:null,p={...so(f,u.type.name,u.attrs),...e},m={...so(f,a.node().type.name,a.node().attrs),...e};n.delete(a.pos,c.pos);let g=h?[{type:l,attrs:p},{type:h,attrs:m}]:[{type:l,attrs:p}];if(!Ee(n.doc,a.pos,2))return!1;if(o){let{selection:y,storedMarks:w}=r,{splittableMarks:b}=i.extensionManager,C=w||y.$to.parentOffset&&y.$from.marks();if(n.split(a.pos,2,g).scrollIntoView(),!C||!o)return!0;let x=C.filter(S=>b.includes(S.type.name));n.ensureMarks(x)}return!0},Fs=(t,e)=>{let n=et(s=>s.type===e)(t.selection);if(!n)return!0;let r=t.doc.resolve(Math.max(0,n.pos-1)).before(n.depth);if(r===void 0)return!0;let o=t.doc.nodeAt(r);return n.node.type===o?.type&&Re(t.doc,n.pos)&&t.join(n.pos),!0},Vs=(t,e)=>{let n=et(s=>s.type===e)(t.selection);if(!n)return!0;let r=t.doc.resolve(n.start).after(n.depth);if(r===void 0)return!0;let o=t.doc.nodeAt(r);return n.node.type===o?.type&&Re(t.doc,r)&&t.join(r),!0},Xy=(t,e,n,r={})=>({editor:o,tr:i,state:s,dispatch:l,chain:a,commands:c,can:d})=>{let{extensions:u,splittableMarks:f}=o.extensionManager,h=ne(t,s.schema),p=ne(e,s.schema),{selection:m,storedMarks:g}=s,{$from:y,$to:w}=m,b=y.blockRange(w),C=g||m.$to.parentOffset&&m.$from.marks();if(!b)return!1;let x=et(S=>Bd(S.type.name,u))(m);if(b.depth>=1&&x&&b.depth-x.depth<=1){if(x.node.type===h)return c.liftListItem(p);if(Bd(x.node.type.name,u)&&h.validContent(x.node.content)&&l)return a().command(()=>(i.setNodeMarkup(x.pos,h),!0)).command(()=>Fs(i,h)).command(()=>Vs(i,h)).run()}return!n||!C||!l?a().command(()=>d().wrapInList(h,r)?!0:c.clearNodes()).wrapInList(h,r).command(()=>Fs(i,h)).command(()=>Vs(i,h)).run():a().command(()=>{let S=d().wrapInList(h,r),k=C.filter(O=>f.includes(O.type.name));return i.ensureMarks(k),S?!0:c.clearNodes()}).wrapInList(h,r).command(()=>Fs(i,h)).command(()=>Vs(i,h)).run()},Yy=(t,e={},n={})=>({state:r,commands:o})=>{let{extendEmptyMarkRange:i=!1}=n,s=wt(t,r.schema);return Ws(r,s,e)?o.unsetMark(s,{extendEmptyMarkRange:i}):o.setMark(s,e)},Qy=(t,e,n={})=>({state:r,commands:o})=>{let i=ne(t,r.schema),s=ne(e,r.schema),l=Ze(r,i,n),a;return r.selection.$anchor.sameParent(r.selection.$head)&&(a=r.selection.$anchor.parent.attrs),l?o.setNode(s,a):o.setNode(i,{...a,...n})},Zy=(t,e={})=>({state:n,commands:r})=>{let o=ne(t,n.schema);return Ze(n,o,e)?r.lift(o):r.wrapIn(o,e)},eb=()=>({state:t,dispatch:e})=>{let n=t.plugins;for(let r=0;r=0;a-=1)s.step(l.steps[a].invert(l.docs[a]));if(i.text){let a=s.doc.resolve(i.from).marks();s.replaceWith(i.from,i.to,t.schema.text(i.text,a))}else s.delete(i.from,i.to)}return!0}}return!1},tb=()=>({tr:t,dispatch:e})=>{let{selection:n}=t,{empty:r,ranges:o}=n;return r||e&&o.forEach(i=>{t.removeMark(i.$from.pos,i.$to.pos)}),!0},nb=(t,e={})=>({tr:n,state:r,dispatch:o})=>{var i;let{extendEmptyMarkRange:s=!1}=e,{selection:l}=n,a=wt(t,r.schema),{$from:c,empty:d,ranges:u}=l;if(!o)return!0;if(d&&s){let{from:f,to:h}=l,p=(i=c.marks().find(g=>g.type===a))==null?void 0:i.attrs,m=Ks(c,a,p);m&&(f=m.from,h=m.to),n.removeMark(f,h,a)}else u.forEach(f=>{n.removeMark(f.$from.pos,f.$to.pos,a)});return n.removeStoredMark(a),!0},rb=t=>({tr:e,state:n,dispatch:r})=>{let{selection:o}=n,i,s;return typeof t=="number"?(i=t,s=t):t&&"from"in t&&"to"in t?(i=t.from,s=t.to):(i=o.from,s=o.to),r&&e.doc.nodesBetween(i,s,(l,a)=>{if(l.isText)return;let c={...l.attrs};delete c.dir,e.setNodeMarkup(a,void 0,c)}),!0},ob=(t,e={})=>({tr:n,state:r,dispatch:o})=>{let i=null,s=null,l=ho(typeof t=="string"?t:t.name,r.schema);if(!l)return!1;l==="node"&&(i=ne(t,r.schema)),l==="mark"&&(s=wt(t,r.schema));let a=!1;return n.selection.ranges.forEach(c=>{let d=c.$from.pos,u=c.$to.pos,f,h,p,m;n.selection.empty?r.doc.nodesBetween(d,u,(g,y)=>{i&&i===g.type&&(a=!0,p=Math.max(y,d),m=Math.min(y+g.nodeSize,u),f=y,h=g)}):r.doc.nodesBetween(d,u,(g,y)=>{y=d&&y<=u&&(i&&i===g.type&&(a=!0,o&&n.setNodeMarkup(y,void 0,{...g.attrs,...e})),s&&g.marks.length&&g.marks.forEach(w=>{if(s===w.type&&(a=!0,o)){let b=Math.max(y,d),C=Math.min(y+g.nodeSize,u);n.addMark(b,C,s.create({...w.attrs,...e}))}}))}),h&&(f!==void 0&&o&&n.setNodeMarkup(f,void 0,{...h.attrs,...e}),s&&h.marks.length&&h.marks.forEach(g=>{s===g.type&&o&&n.addMark(p,m,s.create({...g.attrs,...e}))}))}),a},ib=(t,e={})=>({state:n,dispatch:r})=>{let o=ne(t,n.schema);return pc(o,e)(n,r)},sb=(t,e={})=>({state:n,dispatch:r})=>{let o=ne(t,n.schema);return mc(o,e)(n,r)},lb=class{constructor(){this.callbacks={}}on(t,e){return this.callbacks[t]||(this.callbacks[t]=[]),this.callbacks[t].push(e),this}emit(t,...e){let n=this.callbacks[t];return n&&n.forEach(r=>r.apply(this,e)),this}off(t,e){let n=this.callbacks[t];return n&&(e?this.callbacks[t]=n.filter(r=>r!==e):delete this.callbacks[t]),this}once(t,e){let n=(...r)=>{this.off(t,n),e.apply(this,r)};return this.on(t,n)}removeAllListeners(){this.callbacks={}}},go=class{constructor(t){var e;this.find=t.find,this.handler=t.handler,this.undoable=(e=t.undoable)!=null?e:!0}},ab=(t,e)=>{if(Us(e))return e.exec(t);let n=e(t);if(!n)return null;let r=[n.text];return r.index=n.index,r.input=t,r.data=n.data,n.replaceWith&&(n.text.includes(n.replaceWith)||console.warn('[tiptap warn]: "inputRuleMatch.replaceWith" must be part of "inputRuleMatch.text".'),r.push(n.replaceWith)),r};function ro(t){var e;let{editor:n,from:r,to:o,text:i,rules:s,plugin:l}=t,{view:a}=n;if(a.composing)return!1;let c=a.state.doc.resolve(r);if(c.parent.type.spec.code||(e=c.nodeBefore||c.nodeAfter)!=null&&e.marks.find(f=>f.type.spec.code))return!1;let d=!1,u=zy(c)+i;return s.forEach(f=>{if(d)return;let h=ab(u,f.find);if(!h)return;let p=a.state.tr,m=co({state:a.state,transaction:p}),g={from:r-(h[0].length-i.length),to:o},{commands:y,chain:w,can:b}=new uo({editor:n,state:m});f.handler({state:m,range:g,match:h,commands:y,chain:w,can:b})===null||!p.steps.length||(f.undoable&&p.setMeta(l,{transform:p,from:r,to:o,text:i}),a.dispatch(p),d=!0)}),d}function cb(t){let{editor:e,rules:n}=t,r=new P({state:{init(){return null},apply(o,i,s){let l=o.getMeta(r);if(l)return l;let a=o.getMeta("applyInputRules");return!!a&&setTimeout(()=>{let{text:d}=a;typeof d=="string"?d=d:d=Ys(v.from(d),s.schema);let{from:u}=a,f=u+d.length;ro({editor:e,from:u,to:f,text:d,rules:n,plugin:r})}),o.selectionSet||o.docChanged?null:i}},props:{handleTextInput(o,i,s,l){return ro({editor:e,from:i,to:s,text:l,rules:n,plugin:r})},handleDOMEvents:{compositionend:o=>(setTimeout(()=>{let{$cursor:i}=o.state.selection;i&&ro({editor:e,from:i.pos,to:i.pos,text:"",rules:n,plugin:r})}),!1)},handleKeyDown(o,i){if(i.key!=="Enter")return!1;let{$cursor:s}=o.state.selection;return s?ro({editor:e,from:s.pos,to:s.pos,text:` +`,rules:n,plugin:r}):!1}},isInputRules:!0});return r}function db(t){return Object.prototype.toString.call(t).slice(8,-1)}function oo(t){return db(t)!=="Object"?!1:t.constructor===Object&&Object.getPrototypeOf(t)===Object.prototype}function nu(t,e){let n={...t};return oo(t)&&oo(e)&&Object.keys(e).forEach(r=>{oo(e[r])&&oo(t[r])?n[r]=nu(t[r],e[r]):n[r]=e[r]}),n}var nl=class{constructor(t={}){this.type="extendable",this.parent=null,this.child=null,this.name="",this.config={name:this.name},this.config={...this.config,...t},this.name=this.config.name}get options(){return{...G(B(this,"addOptions",{name:this.name}))||{}}}get storage(){return{...G(B(this,"addStorage",{name:this.name,options:this.options}))||{}}}configure(t={}){let e=this.extend({...this.config,addOptions:()=>nu(this.options,t)});return e.name=this.name,e.parent=this.parent,e}extend(t={}){let e=new this.constructor({...this.config,...t});return e.parent=this,this.child=e,e.name="name"in t?t.name:e.parent.name,e}},ee=class ru extends nl{constructor(){super(...arguments),this.type="mark"}static create(e={}){let n=typeof e=="function"?e():e;return new ru(n)}static handleExit({editor:e,mark:n}){let{tr:r}=e.state,o=e.state.selection.$from;if(o.pos===o.end()){let s=o.marks();if(!!!s.find(c=>c?.type.name===n.name))return!1;let a=s.find(c=>c?.type.name===n.name);return a&&r.removeStoredMark(a),r.insertText(" ",o.pos),e.view.dispatch(r),!0}return!1}configure(e){return super.configure(e)}extend(e){let n=typeof e=="function"?e():e;return super.extend(n)}};function ub(t){return typeof t=="number"}var fb=class{constructor(t){this.find=t.find,this.handler=t.handler}},hb=(t,e,n)=>{if(Us(e))return[...t.matchAll(e)];let r=e(t,n);return r?r.map(o=>{let i=[o.text];return i.index=o.index,i.input=t,i.data=o.data,o.replaceWith&&(o.text.includes(o.replaceWith)||console.warn('[tiptap warn]: "pasteRuleMatch.replaceWith" must be part of "pasteRuleMatch.text".'),i.push(o.replaceWith)),i}):[]};function pb(t){let{editor:e,state:n,from:r,to:o,rule:i,pasteEvent:s,dropEvent:l}=t,{commands:a,chain:c,can:d}=new uo({editor:e,state:n}),u=[];return n.doc.nodesBetween(r,o,(h,p)=>{var m,g,y,w,b;if((g=(m=h.type)==null?void 0:m.spec)!=null&&g.code||!(h.isText||h.isTextblock||h.isInline))return;let C=(b=(w=(y=h.content)==null?void 0:y.size)!=null?w:h.nodeSize)!=null?b:0,x=Math.max(r,p),S=Math.min(o,p+C);if(x>=S)return;let k=h.isText?h.text||"":h.textBetween(x-p,S-p,void 0,"\uFFFC");hb(k,i.find,s).forEach(T=>{if(T.index===void 0)return;let A=x+T.index+1,$=A+T[0].length,z={from:n.tr.mapping.map(A),to:n.tr.mapping.map($)},K=i.handler({state:n,range:z,match:T,commands:a,chain:c,can:d,pasteEvent:s,dropEvent:l});u.push(K)})}),u.every(h=>h!==null)}var io=null,mb=t=>{var e;let n=new ClipboardEvent("paste",{clipboardData:new DataTransfer});return(e=n.clipboardData)==null||e.setData("text/html",t),n};function gb(t){let{editor:e,rules:n}=t,r=null,o=!1,i=!1,s=typeof ClipboardEvent<"u"?new ClipboardEvent("paste"):null,l;try{l=typeof DragEvent<"u"?new DragEvent("drop"):null}catch{l=null}let a=({state:d,from:u,to:f,rule:h,pasteEvt:p})=>{let m=d.tr,g=co({state:d,transaction:m});if(!(!pb({editor:e,state:g,from:Math.max(u-1,0),to:f.b-1,rule:h,pasteEvent:p,dropEvent:l})||!m.steps.length)){try{l=typeof DragEvent<"u"?new DragEvent("drop"):null}catch{l=null}return s=typeof ClipboardEvent<"u"?new ClipboardEvent("paste"):null,m}};return n.map(d=>new P({view(u){let f=p=>{var m;r=(m=u.dom.parentElement)!=null&&m.contains(p.target)?u.dom.parentElement:null,r&&(io=e)},h=()=>{io&&(io=null)};return window.addEventListener("dragstart",f),window.addEventListener("dragend",h),{destroy(){window.removeEventListener("dragstart",f),window.removeEventListener("dragend",h)}}},props:{handleDOMEvents:{drop:(u,f)=>{if(i=r===u.dom.parentElement,l=f,!i){let h=io;h?.isEditable&&setTimeout(()=>{let p=h.state.selection;p&&h.commands.deleteRange({from:p.from,to:p.to})},10)}return!1},paste:(u,f)=>{var h;let p=(h=f.clipboardData)==null?void 0:h.getData("text/html");return s=f,o=!!p?.includes("data-pm-slice"),!1}}},appendTransaction:(u,f,h)=>{let p=u[0],m=p.getMeta("uiEvent")==="paste"&&!o,g=p.getMeta("uiEvent")==="drop"&&!i,y=p.getMeta("applyPasteRules"),w=!!y;if(!m&&!g&&!w)return;if(w){let{text:x}=y;typeof x=="string"?x=x:x=Ys(v.from(x),h.schema);let{from:S}=y,k=S+x.length,O=mb(x);return a({rule:d,state:h,from:S,to:{b:k},pasteEvt:O})}let b=f.doc.content.findDiffStart(h.doc.content),C=f.doc.content.findDiffEnd(h.doc.content);if(!(!ub(b)||!C||b===C.b))return a({rule:d,state:h,from:b,to:C,pasteEvt:s})}}))}var yo=class{constructor(t,e){this.splittableMarks=[],this.editor=e,this.baseExtensions=t,this.extensions=qd(t),this.schema=Ry(this.extensions,e),this.setupExtensions()}get commands(){return this.extensions.reduce((t,e)=>{let n={name:e.name,options:e.options,storage:this.editor.extensionStorage[e.name],editor:this.editor,type:$s(e.name,this.schema)},r=B(e,"addCommands",n);return r?{...t,...r()}:t},{})}get plugins(){let{editor:t}=this;return Qs([...this.extensions].reverse()).flatMap(r=>{let o={name:r.name,options:r.options,storage:this.editor.extensionStorage[r.name],editor:t,type:$s(r.name,this.schema)},i=[],s=B(r,"addKeyboardShortcuts",o),l={};if(r.type==="mark"&&B(r,"exitable",o)&&(l.ArrowRight=()=>ee.handleExit({editor:t,mark:r})),s){let f=Object.fromEntries(Object.entries(s()).map(([h,p])=>[h,()=>p({editor:t})]));l={...l,...f}}let a=Nd(l);i.push(a);let c=B(r,"addInputRules",o);if(Ld(r,t.options.enableInputRules)&&c){let f=c();if(f&&f.length){let h=cb({editor:t,rules:f}),p=Array.isArray(h)?h:[h];i.push(...p)}}let d=B(r,"addPasteRules",o);if(Ld(r,t.options.enablePasteRules)&&d){let f=d();if(f&&f.length){let h=gb({editor:t,rules:f});i.push(...h)}}let u=B(r,"addProseMirrorPlugins",o);if(u){let f=u();i.push(...f)}return i})}get attributes(){return Kd(this.extensions)}get nodeViews(){let{editor:t}=this,{nodeExtensions:e}=An(this.extensions);return Object.fromEntries(e.filter(n=>!!B(n,"addNodeView")).map(n=>{let r=this.attributes.filter(a=>a.type===n.name),o={name:n.name,options:n.options,storage:this.editor.extensionStorage[n.name],editor:t,type:ne(n.name,this.schema)},i=B(n,"addNodeView",o);if(!i)return[];let s=i();if(!s)return[];let l=(a,c,d,u,f)=>{let h=ao(a,r);return s({node:a,view:c,getPos:d,decorations:u,innerDecorations:f,editor:t,extension:n,HTMLAttributes:h})};return[n.name,l]}))}get markViews(){let{editor:t}=this,{markExtensions:e}=An(this.extensions);return Object.fromEntries(e.filter(n=>!!B(n,"addMarkView")).map(n=>{let r=this.attributes.filter(l=>l.type===n.name),o={name:n.name,options:n.options,storage:this.editor.extensionStorage[n.name],editor:t,type:wt(n.name,this.schema)},i=B(n,"addMarkView",o);if(!i)return[];let s=(l,a,c)=>{let d=ao(l,r);return i()({mark:l,view:a,inline:c,editor:t,extension:n,HTMLAttributes:d,updateAttributes:u=>{Ab(l,t,u)}})};return[n.name,s]}))}setupExtensions(){let t=this.extensions;this.editor.extensionStorage=Object.fromEntries(t.map(e=>[e.name,e.storage])),t.forEach(e=>{var n;let r={name:e.name,options:e.options,storage:this.editor.extensionStorage[e.name],editor:this.editor,type:$s(e.name,this.schema)};e.type==="mark"&&((n=G(B(e,"keepOnSplit",r)))==null||n)&&this.splittableMarks.push(e.name);let o=B(e,"onBeforeCreate",r),i=B(e,"onCreate",r),s=B(e,"onUpdate",r),l=B(e,"onSelectionUpdate",r),a=B(e,"onTransaction",r),c=B(e,"onFocus",r),d=B(e,"onBlur",r),u=B(e,"onDestroy",r);o&&this.editor.on("beforeCreate",o),i&&this.editor.on("create",i),s&&this.editor.on("update",s),l&&this.editor.on("selectionUpdate",l),a&&this.editor.on("transaction",a),c&&this.editor.on("focus",c),d&&this.editor.on("blur",d),u&&this.editor.on("destroy",u)})}};yo.resolve=qd;yo.sort=Qs;yo.flatten=Xs;var yb={};js(yb,{ClipboardTextSerializer:()=>iu,Commands:()=>su,Delete:()=>lu,Drop:()=>au,Editable:()=>cu,FocusEvents:()=>uu,Keymap:()=>fu,Paste:()=>hu,Tabindex:()=>pu,TextDirection:()=>mu,focusEventsPluginKey:()=>du});var U=class ou extends nl{constructor(){super(...arguments),this.type="extension"}static create(e={}){let n=typeof e=="function"?e():e;return new ou(n)}configure(e){return super.configure(e)}extend(e){let n=typeof e=="function"?e():e;return super.extend(n)}},iu=U.create({name:"clipboardTextSerializer",addOptions(){return{blockSeparator:void 0}},addProseMirrorPlugins(){return[new P({key:new H("clipboardTextSerializer"),props:{clipboardTextSerializer:()=>{let{editor:t}=this,{state:e,schema:n}=t,{doc:r,selection:o}=e,{ranges:i}=o,s=Math.min(...i.map(d=>d.$from.pos)),l=Math.max(...i.map(d=>d.$to.pos)),a=Gd(n);return Jd(r,{from:s,to:l},{...this.options.blockSeparator!==void 0?{blockSeparator:this.options.blockSeparator}:{},textSerializers:a})}}})]}}),su=U.create({name:"commands",addCommands(){return{...Hd}}}),lu=U.create({name:"delete",onUpdate({transaction:t,appendedTransactions:e}){var n,r,o;let i=()=>{var s,l,a,c;if((c=(a=(l=(s=this.editor.options.coreExtensionOptions)==null?void 0:s.delete)==null?void 0:l.filterTransaction)==null?void 0:a.call(l,t))!=null?c:t.getMeta("y-sync$"))return;let d=Js(t.before,[t,...e]);el(d).forEach(h=>{d.mapping.mapResult(h.oldRange.from).deletedAfter&&d.mapping.mapResult(h.oldRange.to).deletedBefore&&d.before.nodesBetween(h.oldRange.from,h.oldRange.to,(p,m)=>{let g=m+p.nodeSize-2,y=h.oldRange.from<=m&&g<=h.oldRange.to;this.editor.emit("delete",{type:"node",node:p,from:m,to:g,newFrom:d.mapping.map(m),newTo:d.mapping.map(g),deletedRange:h.oldRange,newRange:h.newRange,partial:!y,editor:this.editor,transaction:t,combinedTransform:d})})});let f=d.mapping;d.steps.forEach((h,p)=>{var m,g;if(h instanceof ut){let y=f.slice(p).map(h.from,-1),w=f.slice(p).map(h.to),b=f.invert().map(y,-1),C=f.invert().map(w),x=(m=d.doc.nodeAt(y-1))==null?void 0:m.marks.some(k=>k.eq(h.mark)),S=(g=d.doc.nodeAt(w))==null?void 0:g.marks.some(k=>k.eq(h.mark));this.editor.emit("delete",{type:"mark",mark:h.mark,from:h.from,to:h.to,deletedRange:{from:b,to:C},newRange:{from:y,to:w},partial:!!(S||x),editor:this.editor,transaction:t,combinedTransform:d})}})};(o=(r=(n=this.editor.options.coreExtensionOptions)==null?void 0:n.delete)==null?void 0:r.async)==null||o?setTimeout(i,0):i()}}),au=U.create({name:"drop",addProseMirrorPlugins(){return[new P({key:new H("tiptapDrop"),props:{handleDrop:(t,e,n,r)=>{this.editor.emit("drop",{editor:this.editor,event:e,slice:n,moved:r})}}})]}}),cu=U.create({name:"editable",addProseMirrorPlugins(){return[new P({key:new H("editable"),props:{editable:()=>this.editor.options.editable}})]}}),du=new H("focusEvents"),uu=U.create({name:"focusEvents",addProseMirrorPlugins(){let{editor:t}=this;return[new P({key:du,props:{handleDOMEvents:{focus:(e,n)=>{t.isFocused=!0;let r=t.state.tr.setMeta("focus",{event:n}).setMeta("addToHistory",!1);return e.dispatch(r),!1},blur:(e,n)=>{t.isFocused=!1;let r=t.state.tr.setMeta("blur",{event:n}).setMeta("addToHistory",!1);return e.dispatch(r),!1}}}})]}}),fu=U.create({name:"keymap",addKeyboardShortcuts(){let t=()=>this.editor.commands.first(({commands:s})=>[()=>s.undoInputRule(),()=>s.command(({tr:l})=>{let{selection:a,doc:c}=l,{empty:d,$anchor:u}=a,{pos:f,parent:h}=u,p=u.parent.isTextblock&&f>0?l.doc.resolve(f-1):u,m=p.parent.type.spec.isolating,g=u.pos-u.parentOffset,y=m&&p.parent.childCount===1?g===u.pos:I.atStart(c).from===f;return!d||!h.type.isTextblock||h.textContent.length||!y||y&&u.parent.type.name==="paragraph"?!1:s.clearNodes()}),()=>s.deleteSelection(),()=>s.joinBackward(),()=>s.selectNodeBackward()]),e=()=>this.editor.commands.first(({commands:s})=>[()=>s.deleteSelection(),()=>s.deleteCurrentNode(),()=>s.joinForward(),()=>s.selectNodeForward()]),r={Enter:()=>this.editor.commands.first(({commands:s})=>[()=>s.newlineInCode(),()=>s.createParagraphNear(),()=>s.liftEmptyBlock(),()=>s.splitBlock()]),"Mod-Enter":()=>this.editor.commands.exitCode(),Backspace:t,"Mod-Backspace":t,"Shift-Backspace":t,Delete:e,"Mod-Delete":e,"Mod-a":()=>this.editor.commands.selectAll()},o={...r},i={...r,"Ctrl-h":t,"Alt-Backspace":t,"Ctrl-d":e,"Ctrl-Alt-Backspace":e,"Alt-Delete":e,"Alt-d":e,"Ctrl-a":()=>this.editor.commands.selectTextblockStart(),"Ctrl-e":()=>this.editor.commands.selectTextblockEnd()};return qs()||_d()?i:o},addProseMirrorPlugins(){return[new P({key:new H("clearDocument"),appendTransaction:(t,e,n)=>{if(t.some(m=>m.getMeta("composition")))return;let r=t.some(m=>m.docChanged)&&!e.doc.eq(n.doc),o=t.some(m=>m.getMeta("preventClearDocument"));if(!r||o)return;let{empty:i,from:s,to:l}=e.selection,a=I.atStart(e.doc).from,c=I.atEnd(e.doc).to;if(i||!(s===a&&l===c)||!lr(n.doc))return;let f=n.tr,h=co({state:n,transaction:f}),{commands:p}=new uo({editor:this.editor,state:h});if(p.clearNodes(),!!f.steps.length)return f}})]}}),hu=U.create({name:"paste",addProseMirrorPlugins(){return[new P({key:new H("tiptapPaste"),props:{handlePaste:(t,e,n)=>{this.editor.emit("paste",{editor:this.editor,event:e,slice:n})}}})]}}),pu=U.create({name:"tabindex",addProseMirrorPlugins(){return[new P({key:new H("tabindex"),props:{attributes:()=>this.editor.isEditable?{tabindex:"0"}:{}}})]}}),mu=U.create({name:"textDirection",addOptions(){return{direction:void 0}},addGlobalAttributes(){if(!this.options.direction)return[];let{nodeExtensions:t}=An(this.extensions);return[{types:t.filter(e=>e.name!=="text").map(e=>e.name),attributes:{dir:{default:this.options.direction,parseHTML:e=>{let n=e.getAttribute("dir");return n&&(n==="ltr"||n==="rtl"||n==="auto")?n:this.options.direction},renderHTML:e=>e.dir?{dir:e.dir}:{}}}}]},addProseMirrorPlugins(){return[new P({key:new H("textDirection"),props:{attributes:()=>{let t=this.options.direction;return t?{dir:t}:{}}}})]}}),bb=class Tn{constructor(e,n,r=!1,o=null){this.currentNode=null,this.actualDepth=null,this.isBlock=r,this.resolvedPos=e,this.editor=n,this.currentNode=o}get name(){return this.node.type.name}get node(){return this.currentNode||this.resolvedPos.node()}get element(){return this.editor.view.domAtPos(this.pos).node}get depth(){var e;return(e=this.actualDepth)!=null?e:this.resolvedPos.depth}get pos(){return this.resolvedPos.pos}get content(){return this.node.content}set content(e){let n=this.from,r=this.to;if(this.isBlock){if(this.content.size===0){console.error(`You can\u2019t set content on a block node. Tried to set content on ${this.name} at ${this.pos}`);return}n=this.from+1,r=this.to-1}this.editor.commands.insertContentAt({from:n,to:r},e)}get attributes(){return this.node.attrs}get textContent(){return this.node.textContent}get size(){return this.node.nodeSize}get from(){return this.isBlock?this.pos:this.resolvedPos.start(this.resolvedPos.depth)}get range(){return{from:this.from,to:this.to}}get to(){return this.isBlock?this.pos+this.size:this.resolvedPos.end(this.resolvedPos.depth)+(this.node.isText?0:1)}get parent(){if(this.depth===0)return null;let e=this.resolvedPos.start(this.resolvedPos.depth-1),n=this.resolvedPos.doc.resolve(e);return new Tn(n,this.editor)}get before(){let e=this.resolvedPos.doc.resolve(this.from-(this.isBlock?1:2));return e.depth!==this.depth&&(e=this.resolvedPos.doc.resolve(this.from-3)),new Tn(e,this.editor)}get after(){let e=this.resolvedPos.doc.resolve(this.to+(this.isBlock?2:1));return e.depth!==this.depth&&(e=this.resolvedPos.doc.resolve(this.to+3)),new Tn(e,this.editor)}get children(){let e=[];return this.node.content.forEach((n,r)=>{let o=n.isBlock&&!n.isTextblock,i=n.isAtom&&!n.isText,s=this.pos+r+(i?0:1);if(s<0||s>this.resolvedPos.doc.nodeSize-2)return;let l=this.resolvedPos.doc.resolve(s);if(!o&&l.depth<=this.depth)return;let a=new Tn(l,this.editor,o,o?n:null);o&&(a.actualDepth=this.depth+1),e.push(new Tn(l,this.editor,o,o?n:null))}),e}get firstChild(){return this.children[0]||null}get lastChild(){let e=this.children;return e[e.length-1]||null}closest(e,n={}){let r=null,o=this.parent;for(;o&&!r;){if(o.node.type.name===e)if(Object.keys(n).length>0){let i=o.node.attrs,s=Object.keys(n);for(let l=0;l{r&&o.length>0||(s.node.type.name===e&&i.every(a=>n[a]===s.node.attrs[a])&&o.push(s),!(r&&o.length>0)&&(o=o.concat(s.querySelectorAll(e,n,r))))}),o}setAttribute(e){let{tr:n}=this.editor.state;n.setNodeMarkup(this.from,void 0,{...this.node.attrs,...e}),this.editor.view.dispatch(n)}},wb=`.ProseMirror { + position: relative; +} + +.ProseMirror { + word-wrap: break-word; + white-space: pre-wrap; + white-space: break-spaces; + -webkit-font-variant-ligatures: none; + font-variant-ligatures: none; + font-feature-settings: "liga" 0; /* the above doesn't seem to work in Edge */ +} + +.ProseMirror [contenteditable="false"] { + white-space: normal; +} + +.ProseMirror [contenteditable="false"] [contenteditable="true"] { + white-space: pre-wrap; +} + +.ProseMirror pre { + white-space: pre-wrap; +} + +img.ProseMirror-separator { + display: inline !important; + border: none !important; + margin: 0 !important; + width: 0 !important; + height: 0 !important; +} + +.ProseMirror-gapcursor { + display: none; + pointer-events: none; + position: absolute; + margin: 0; +} + +.ProseMirror-gapcursor:after { + content: ""; + display: block; + position: absolute; + top: -2px; + width: 20px; + border-top: 1px solid black; + animation: ProseMirror-cursor-blink 1.1s steps(2, start) infinite; +} + +@keyframes ProseMirror-cursor-blink { + to { + visibility: hidden; + } +} + +.ProseMirror-hideselection *::selection { + background: transparent; +} + +.ProseMirror-hideselection *::-moz-selection { + background: transparent; +} + +.ProseMirror-hideselection * { + caret-color: transparent; +} + +.ProseMirror-focused .ProseMirror-gapcursor { + display: block; +}`;function xb(t,e,n){let r=document.querySelector(`style[data-tiptap-style${n?`-${n}`:""}]`);if(r!==null)return r;let o=document.createElement("style");return e&&o.setAttribute("nonce",e),o.setAttribute(`data-tiptap-style${n?`-${n}`:""}`,""),o.innerHTML=t,document.getElementsByTagName("head")[0].appendChild(o),o}var gu=class extends lb{constructor(t={}){super(),this.css=null,this.className="tiptap",this.editorView=null,this.isFocused=!1,this.isInitialized=!1,this.extensionStorage={},this.instanceId=Math.random().toString(36).slice(2,9),this.options={element:typeof document<"u"?document.createElement("div"):null,content:"",injectCSS:!0,injectNonce:void 0,extensions:[],autofocus:!1,editable:!0,textDirection:void 0,editorProps:{},parseOptions:{},coreExtensionOptions:{},enableInputRules:!0,enablePasteRules:!0,enableCoreExtensions:!0,enableContentCheck:!1,emitContentError:!1,onBeforeCreate:()=>null,onCreate:()=>null,onMount:()=>null,onUnmount:()=>null,onUpdate:()=>null,onSelectionUpdate:()=>null,onTransaction:()=>null,onFocus:()=>null,onBlur:()=>null,onDestroy:()=>null,onContentError:({error:r})=>{throw r},onPaste:()=>null,onDrop:()=>null,onDelete:()=>null},this.isCapturingTransaction=!1,this.capturedTransaction=null,this.utils={getUpdatedPosition:Hy,createMappablePosition:$y},this.setOptions(t),this.createExtensionManager(),this.createCommandManager(),this.createSchema(),this.on("beforeCreate",this.options.onBeforeCreate),this.emit("beforeCreate",{editor:this}),this.on("mount",this.options.onMount),this.on("unmount",this.options.onUnmount),this.on("contentError",this.options.onContentError),this.on("create",this.options.onCreate),this.on("update",this.options.onUpdate),this.on("selectionUpdate",this.options.onSelectionUpdate),this.on("transaction",this.options.onTransaction),this.on("focus",this.options.onFocus),this.on("blur",this.options.onBlur),this.on("destroy",this.options.onDestroy),this.on("drop",({event:r,slice:o,moved:i})=>this.options.onDrop(r,o,i)),this.on("paste",({event:r,slice:o})=>this.options.onPaste(r,o)),this.on("delete",this.options.onDelete);let e=this.createDoc(),n=Fd(e,this.options.autofocus);this.editorState=Fr.create({doc:e,schema:this.schema,selection:n||void 0}),this.options.element&&this.mount(this.options.element)}mount(t){if(typeof document>"u")throw new Error("[tiptap error]: The editor cannot be mounted because there is no 'document' defined in this environment.");this.createView(t),this.emit("mount",{editor:this}),this.css&&!document.head.contains(this.css)&&document.head.appendChild(this.css),window.setTimeout(()=>{this.isDestroyed||(this.options.autofocus!==!1&&this.options.autofocus!==null&&this.commands.focus(this.options.autofocus),this.emit("create",{editor:this}),this.isInitialized=!0)},0)}unmount(){if(this.editorView){let t=this.editorView.dom;t?.editor&&delete t.editor,this.editorView.destroy()}if(this.editorView=null,this.isInitialized=!1,this.css&&!document.querySelectorAll(`.${this.className}`).length)try{typeof this.css.remove=="function"?this.css.remove():this.css.parentNode&&this.css.parentNode.removeChild(this.css)}catch(t){console.warn("Failed to remove CSS element:",t)}this.css=null,this.emit("unmount",{editor:this})}get storage(){return this.extensionStorage}get commands(){return this.commandManager.commands}chain(){return this.commandManager.chain()}can(){return this.commandManager.can()}injectCSS(){this.options.injectCSS&&typeof document<"u"&&(this.css=xb(wb,this.options.injectNonce))}setOptions(t={}){this.options={...this.options,...t},!(!this.editorView||!this.state||this.isDestroyed)&&(this.options.editorProps&&this.view.setProps(this.options.editorProps),this.view.updateState(this.state))}setEditable(t,e=!0){this.setOptions({editable:t}),e&&this.emit("update",{editor:this,transaction:this.state.tr,appendedTransactions:[]})}get isEditable(){return this.options.editable&&this.view&&this.view.editable}get view(){return this.editorView?this.editorView:new Proxy({state:this.editorState,updateState:t=>{this.editorState=t},dispatch:t=>{this.dispatchTransaction(t)},composing:!1,dragging:null,editable:!0,isDestroyed:!1},{get:(t,e)=>{if(this.editorView)return this.editorView[e];if(e==="state")return this.editorState;if(e in t)return Reflect.get(t,e);throw new Error(`[tiptap error]: The editor view is not available. Cannot access view['${e}']. The editor may not be mounted yet.`)}})}get state(){return this.editorView&&(this.editorState=this.view.state),this.editorState}registerPlugin(t,e){let n=Ud(e)?e(t,[...this.state.plugins]):[...this.state.plugins,t],r=this.state.reconfigure({plugins:n});return this.view.updateState(r),r}unregisterPlugin(t){if(this.isDestroyed)return;let e=this.state.plugins,n=e;if([].concat(t).forEach(o=>{let i=typeof o=="string"?`${o}$`:o.key;n=n.filter(s=>!s.key.startsWith(i))}),e.length===n.length)return;let r=this.state.reconfigure({plugins:n});return this.view.updateState(r),r}createExtensionManager(){var t,e;let r=[...this.options.enableCoreExtensions?[cu,iu.configure({blockSeparator:(e=(t=this.options.coreExtensionOptions)==null?void 0:t.clipboardTextSerializer)==null?void 0:e.blockSeparator}),su,uu,fu,pu,au,hu,lu,mu.configure({direction:this.options.textDirection})].filter(o=>typeof this.options.enableCoreExtensions=="object"?this.options.enableCoreExtensions[o.name]!==!1:!0):[],...this.options.extensions].filter(o=>["extension","node","mark"].includes(o?.type));this.extensionManager=new yo(r,this)}createCommandManager(){this.commandManager=new uo({editor:this})}createSchema(){this.schema=this.extensionManager.schema}createDoc(){let t;try{t=_s(this.options.content,this.schema,this.options.parseOptions,{errorOnInvalidContent:this.options.enableContentCheck})}catch(e){if(!(e instanceof Error)||!["[tiptap error]: Invalid JSON content","[tiptap error]: Invalid HTML content"].includes(e.message))throw e;this.emit("contentError",{editor:this,error:e,disableCollaboration:()=>{"collaboration"in this.storage&&typeof this.storage.collaboration=="object"&&this.storage.collaboration&&(this.storage.collaboration.isDisabled=!0),this.options.extensions=this.options.extensions.filter(n=>n.name!=="collaboration"),this.createExtensionManager()}}),t=_s(this.options.content,this.schema,this.options.parseOptions,{errorOnInvalidContent:!1})}return t}createView(t){var e;this.editorView=new tr(t,{...this.options.editorProps,attributes:{role:"textbox",...(e=this.options.editorProps)==null?void 0:e.attributes},dispatchTransaction:this.dispatchTransaction.bind(this),state:this.editorState,markViews:this.extensionManager.markViews,nodeViews:this.extensionManager.nodeViews});let n=this.state.reconfigure({plugins:this.extensionManager.plugins});this.view.updateState(n),this.prependClass(),this.injectCSS();let r=this.view.dom;r.editor=this}createNodeViews(){this.view.isDestroyed||this.view.setProps({markViews:this.extensionManager.markViews,nodeViews:this.extensionManager.nodeViews})}prependClass(){this.view.dom.className=`${this.className} ${this.view.dom.className}`}captureTransaction(t){this.isCapturingTransaction=!0,t(),this.isCapturingTransaction=!1;let e=this.capturedTransaction;return this.capturedTransaction=null,e}dispatchTransaction(t){if(this.view.isDestroyed)return;if(this.isCapturingTransaction){if(!this.capturedTransaction){this.capturedTransaction=t;return}t.steps.forEach(c=>{var d;return(d=this.capturedTransaction)==null?void 0:d.step(c)});return}let{state:e,transactions:n}=this.state.applyTransaction(t),r=!this.state.selection.eq(e.selection),o=n.includes(t),i=this.state;if(this.emit("beforeTransaction",{editor:this,transaction:t,nextState:e}),!o)return;this.view.updateState(e),this.emit("transaction",{editor:this,transaction:t,appendedTransactions:n.slice(1)}),r&&this.emit("selectionUpdate",{editor:this,transaction:t});let s=n.findLast(c=>c.getMeta("focus")||c.getMeta("blur")),l=s?.getMeta("focus"),a=s?.getMeta("blur");l&&this.emit("focus",{editor:this,event:l.event,transaction:s}),a&&this.emit("blur",{editor:this,event:a.event,transaction:s}),!(t.getMeta("preventUpdate")||!n.some(c=>c.docChanged)||i.doc.eq(e.doc))&&this.emit("update",{editor:this,transaction:t,appendedTransactions:n.slice(1)})}getAttributes(t){return Zs(this.state,t)}isActive(t,e){let n=typeof t=="string"?t:null,r=typeof t=="string"?e:t;return tl(this.state,n,r)}getJSON(){return this.state.doc.toJSON()}getHTML(){return Ys(this.state.doc.content,this.schema)}getText(t){let{blockSeparator:e=` + +`,textSerializers:n={}}=t||{};return Iy(this.state.doc,{blockSeparator:e,textSerializers:{...Gd(this.schema),...n}})}get isEmpty(){return lr(this.state.doc)}destroy(){this.emit("destroy"),this.unmount(),this.removeAllListeners()}get isDestroyed(){var t,e;return(e=(t=this.editorView)==null?void 0:t.isDestroyed)!=null?e:!0}$node(t,e){var n;return((n=this.$doc)==null?void 0:n.querySelector(t,e))||null}$nodes(t,e){var n;return((n=this.$doc)==null?void 0:n.querySelectorAll(t,e))||null}$pos(t){let e=this.state.doc.resolve(t);return new bb(e,this)}get $doc(){return this.$pos(0)}};function Be(t){return new go({find:t.find,handler:({state:e,range:n,match:r})=>{let o=G(t.getAttributes,void 0,r);if(o===!1||o===null)return null;let{tr:i}=e,s=r[r.length-1],l=r[0];if(s){let a=l.search(/\S/),c=n.from+l.indexOf(s),d=c+s.length;if(po(n.from,n.to,e.doc).filter(h=>h.mark.type.excluded.find(m=>m===t.type&&m!==h.mark.type)).filter(h=>h.to>c).length)return null;dn.from&&i.delete(n.from+a,c);let f=n.from+a+s.length;i.addMark(n.from+a,f,t.type.create(o||{})),i.removeStoredMark(t.type)}},undoable:t.undoable})}function bo(t){return new go({find:t.find,handler:({state:e,range:n,match:r})=>{let o=G(t.getAttributes,void 0,r)||{},{tr:i}=e,s=n.from,l=n.to,a=t.type.create(o);if(r[1]){let c=r[0].lastIndexOf(r[1]),d=s+c;d>l?d=l:l=d+r[1].length;let u=r[0][r[0].length-1];i.insertText(u,s+r[0].length-1),i.replaceWith(d,l,a)}else if(r[0]){let c=t.type.isInline?s:s-1;i.insert(c,t.type.create(o)).delete(i.mapping.map(s),i.mapping.map(l))}i.scrollIntoView()},undoable:t.undoable})}function ar(t){return new go({find:t.find,handler:({state:e,range:n,match:r})=>{let o=e.doc.resolve(n.from),i=G(t.getAttributes,void 0,r)||{};if(!o.node(-1).canReplaceWith(o.index(-1),o.indexAfter(-1),t.type))return null;e.tr.delete(n.from,n.to).setBlockType(n.from,n.from,t.type,i)},undoable:t.undoable})}function tt(t){return new go({find:t.find,handler:({state:e,range:n,match:r,chain:o})=>{let i=G(t.getAttributes,void 0,r)||{},s=e.tr.delete(n.from,n.to),a=s.doc.resolve(n.from).blockRange(),c=a&&mn(a,t.type,i);if(!c)return null;if(s.wrap(a,c),t.keepMarks&&t.editor){let{selection:u,storedMarks:f}=e,{splittableMarks:h}=t.editor.extensionManager,p=f||u.$to.parentOffset&&u.$from.marks();if(p){let m=p.filter(g=>h.includes(g.type.name));s.ensureMarks(m)}}if(t.keepAttributes){let u=t.type.name==="bulletList"||t.type.name==="orderedList"?"listItem":"taskList";o().updateAttributes(u,i).run()}let d=s.doc.resolve(n.from-1).nodeBefore;d&&d.type===t.type&&Re(s.doc,n.from-1)&&(!t.joinPredicate||t.joinPredicate(r,d))&&s.join(n.from-1)},undoable:t.undoable})}var kb=t=>"touches"in t,yu=class{constructor(t){this.directions=["bottom-left","bottom-right","top-left","top-right"],this.minSize={height:8,width:8},this.preserveAspectRatio=!1,this.classNames={container:"",wrapper:"",handle:"",resizing:""},this.initialWidth=0,this.initialHeight=0,this.aspectRatio=1,this.isResizing=!1,this.activeHandle=null,this.startX=0,this.startY=0,this.startWidth=0,this.startHeight=0,this.isShiftKeyPressed=!1,this.lastEditableState=void 0,this.handleMap=new Map,this.handleMouseMove=l=>{if(!this.isResizing||!this.activeHandle)return;let a=l.clientX-this.startX,c=l.clientY-this.startY;this.handleResize(a,c)},this.handleTouchMove=l=>{if(!this.isResizing||!this.activeHandle)return;let a=l.touches[0];if(!a)return;let c=a.clientX-this.startX,d=a.clientY-this.startY;this.handleResize(c,d)},this.handleMouseUp=()=>{if(!this.isResizing)return;let l=this.element.offsetWidth,a=this.element.offsetHeight;this.onCommit(l,a),this.isResizing=!1,this.activeHandle=null,this.container.dataset.resizeState="false",this.classNames.resizing&&this.container.classList.remove(this.classNames.resizing),document.removeEventListener("mousemove",this.handleMouseMove),document.removeEventListener("mouseup",this.handleMouseUp),document.removeEventListener("keydown",this.handleKeyDown),document.removeEventListener("keyup",this.handleKeyUp)},this.handleKeyDown=l=>{l.key==="Shift"&&(this.isShiftKeyPressed=!0)},this.handleKeyUp=l=>{l.key==="Shift"&&(this.isShiftKeyPressed=!1)};var e,n,r,o,i,s;this.node=t.node,this.editor=t.editor,this.element=t.element,this.contentElement=t.contentElement,this.getPos=t.getPos,this.onResize=t.onResize,this.onCommit=t.onCommit,this.onUpdate=t.onUpdate,(e=t.options)!=null&&e.min&&(this.minSize={...this.minSize,...t.options.min}),(n=t.options)!=null&&n.max&&(this.maxSize=t.options.max),(r=t?.options)!=null&&r.directions&&(this.directions=t.options.directions),(o=t.options)!=null&&o.preserveAspectRatio&&(this.preserveAspectRatio=t.options.preserveAspectRatio),(i=t.options)!=null&&i.className&&(this.classNames={container:t.options.className.container||"",wrapper:t.options.className.wrapper||"",handle:t.options.className.handle||"",resizing:t.options.className.resizing||""}),(s=t.options)!=null&&s.createCustomHandle&&(this.createCustomHandle=t.options.createCustomHandle),this.wrapper=this.createWrapper(),this.container=this.createContainer(),this.applyInitialSize(),this.attachHandles(),this.editor.on("update",this.handleEditorUpdate.bind(this))}get dom(){return this.container}get contentDOM(){return this.contentElement}handleEditorUpdate(){let t=this.editor.isEditable;t!==this.lastEditableState&&(this.lastEditableState=t,t?t&&this.handleMap.size===0&&this.attachHandles():this.removeHandles())}update(t,e,n){return t.type!==this.node.type?!1:(this.node=t,this.onUpdate?this.onUpdate(t,e,n):!0)}destroy(){this.isResizing&&(this.container.dataset.resizeState="false",this.classNames.resizing&&this.container.classList.remove(this.classNames.resizing),document.removeEventListener("mousemove",this.handleMouseMove),document.removeEventListener("mouseup",this.handleMouseUp),document.removeEventListener("keydown",this.handleKeyDown),document.removeEventListener("keyup",this.handleKeyUp),this.isResizing=!1,this.activeHandle=null),this.editor.off("update",this.handleEditorUpdate.bind(this)),this.container.remove()}createContainer(){let t=document.createElement("div");return t.dataset.resizeContainer="",t.dataset.node=this.node.type.name,t.style.display="flex",this.classNames.container&&(t.className=this.classNames.container),t.appendChild(this.wrapper),t}createWrapper(){let t=document.createElement("div");return t.style.position="relative",t.style.display="block",t.dataset.resizeWrapper="",this.classNames.wrapper&&(t.className=this.classNames.wrapper),t.appendChild(this.element),t}createHandle(t){let e=document.createElement("div");return e.dataset.resizeHandle=t,e.style.position="absolute",this.classNames.handle&&(e.className=this.classNames.handle),e}positionHandle(t,e){let n=e.includes("top"),r=e.includes("bottom"),o=e.includes("left"),i=e.includes("right");n&&(t.style.top="0"),r&&(t.style.bottom="0"),o&&(t.style.left="0"),i&&(t.style.right="0"),(e==="top"||e==="bottom")&&(t.style.left="0",t.style.right="0"),(e==="left"||e==="right")&&(t.style.top="0",t.style.bottom="0")}attachHandles(){this.directions.forEach(t=>{let e;this.createCustomHandle?e=this.createCustomHandle(t):e=this.createHandle(t),e instanceof HTMLElement||(console.warn(`[ResizableNodeView] createCustomHandle("${t}") did not return an HTMLElement. Falling back to default handle.`),e=this.createHandle(t)),this.createCustomHandle||this.positionHandle(e,t),e.addEventListener("mousedown",n=>this.handleResizeStart(n,t)),e.addEventListener("touchstart",n=>this.handleResizeStart(n,t)),this.handleMap.set(t,e),this.wrapper.appendChild(e)})}removeHandles(){this.handleMap.forEach(t=>t.remove()),this.handleMap.clear()}applyInitialSize(){let t=this.node.attrs.width,e=this.node.attrs.height;t?(this.element.style.width=`${t}px`,this.initialWidth=t):this.initialWidth=this.element.offsetWidth,e?(this.element.style.height=`${e}px`,this.initialHeight=e):this.initialHeight=this.element.offsetHeight,this.initialWidth>0&&this.initialHeight>0&&(this.aspectRatio=this.initialWidth/this.initialHeight)}handleResizeStart(t,e){t.preventDefault(),t.stopPropagation(),this.isResizing=!0,this.activeHandle=e,kb(t)?(this.startX=t.touches[0].clientX,this.startY=t.touches[0].clientY):(this.startX=t.clientX,this.startY=t.clientY),this.startWidth=this.element.offsetWidth,this.startHeight=this.element.offsetHeight,this.startWidth>0&&this.startHeight>0&&(this.aspectRatio=this.startWidth/this.startHeight);let n=this.getPos();this.container.dataset.resizeState="true",this.classNames.resizing&&this.container.classList.add(this.classNames.resizing),document.addEventListener("mousemove",this.handleMouseMove),document.addEventListener("touchmove",this.handleTouchMove),document.addEventListener("mouseup",this.handleMouseUp),document.addEventListener("keydown",this.handleKeyDown),document.addEventListener("keyup",this.handleKeyUp)}handleResize(t,e){if(!this.activeHandle)return;let n=this.preserveAspectRatio||this.isShiftKeyPressed,{width:r,height:o}=this.calculateNewDimensions(this.activeHandle,t,e),i=this.applyConstraints(r,o,n);this.element.style.width=`${i.width}px`,this.element.style.height=`${i.height}px`,this.onResize&&this.onResize(i.width,i.height)}calculateNewDimensions(t,e,n){let r=this.startWidth,o=this.startHeight,i=t.includes("right"),s=t.includes("left"),l=t.includes("bottom"),a=t.includes("top");return i?r=this.startWidth+e:s&&(r=this.startWidth-e),l?o=this.startHeight+n:a&&(o=this.startHeight-n),(t==="right"||t==="left")&&(r=this.startWidth+(i?e:-e)),(t==="top"||t==="bottom")&&(o=this.startHeight+(l?n:-n)),this.preserveAspectRatio||this.isShiftKeyPressed?this.applyAspectRatio(r,o,t):{width:r,height:o}}applyConstraints(t,e,n){var r,o,i,s;if(!n){let c=Math.max(this.minSize.width,t),d=Math.max(this.minSize.height,e);return(r=this.maxSize)!=null&&r.width&&(c=Math.min(this.maxSize.width,c)),(o=this.maxSize)!=null&&o.height&&(d=Math.min(this.maxSize.height,d)),{width:c,height:d}}let l=t,a=e;return lthis.maxSize.width&&(l=this.maxSize.width,a=l/this.aspectRatio),(s=this.maxSize)!=null&&s.height&&a>this.maxSize.height&&(a=this.maxSize.height,l=a*this.aspectRatio),{width:l,height:a}}applyAspectRatio(t,e,n){let r=n==="left"||n==="right",o=n==="top"||n==="bottom";return r?{width:t,height:t/this.aspectRatio}:o?{width:e*this.aspectRatio,height:e}:{width:t,height:t/this.aspectRatio}}};function bu(t,e){let{selection:n}=t,{$from:r}=n;if(n instanceof L){let i=r.index();return r.parent.canReplaceWith(i,i+1,e)}let o=r.depth;for(;o>=0;){let i=r.index(o);if(r.node(o).contentMatchAt(i).matchType(e))return!0;o-=1}return!1}function wu(t){return t.replace(/[-/\\^$*+?.()|[\]{}]/g,"\\$&")}var Sb={};js(Sb,{createAtomBlockMarkdownSpec:()=>Cb,createBlockMarkdownSpec:()=>Zt,createInlineMarkdownSpec:()=>Tb,parseAttributes:()=>rl,parseIndentedBlocks:()=>wo,renderNestedMarkdownContent:()=>cr,serializeAttributes:()=>ol});function rl(t){if(!t?.trim())return{};let e={},n=[],r=t.replace(/["']([^"']*)["']/g,c=>(n.push(c),`__QUOTED_${n.length-1}__`)),o=r.match(/(?:^|\s)\.([a-zA-Z][\w-]*)/g);if(o){let c=o.map(d=>d.trim().slice(1));e.class=c.join(" ")}let i=r.match(/(?:^|\s)#([a-zA-Z][\w-]*)/);i&&(e.id=i[1]);let s=/([a-zA-Z][\w-]*)\s*=\s*(__QUOTED_\d+__)/g;Array.from(r.matchAll(s)).forEach(([,c,d])=>{var u;let f=parseInt(((u=d.match(/__QUOTED_(\d+)__/))==null?void 0:u[1])||"0",10),h=n[f];h&&(e[c]=h.slice(1,-1))});let a=r.replace(/(?:^|\s)\.([a-zA-Z][\w-]*)/g,"").replace(/(?:^|\s)#([a-zA-Z][\w-]*)/g,"").replace(/([a-zA-Z][\w-]*)\s*=\s*__QUOTED_\d+__/g,"").trim();return a&&a.split(/\s+/).filter(Boolean).forEach(d=>{d.match(/^[a-zA-Z][\w-]*$/)&&(e[d]=!0)}),e}function ol(t){if(!t||Object.keys(t).length===0)return"";let e=[];return t.class&&String(t.class).split(/\s+/).filter(Boolean).forEach(r=>e.push(`.${r}`)),t.id&&e.push(`#${t.id}`),Object.entries(t).forEach(([n,r])=>{n==="class"||n==="id"||(r===!0?e.push(n):r!==!1&&r!=null&&e.push(`${n}="${String(r)}"`))}),e.join(" ")}function Cb(t){let{nodeName:e,name:n,parseAttributes:r=rl,serializeAttributes:o=ol,defaultAttributes:i={},requiredAttributes:s=[],allowedAttributes:l}=t,a=n||e,c=d=>{if(!l)return d;let u={};return l.forEach(f=>{f in d&&(u[f]=d[f])}),u};return{parseMarkdown:(d,u)=>{let f={...i,...d.attributes};return u.createNode(e,f,[])},markdownTokenizer:{name:e,level:"block",start(d){var u;let f=new RegExp(`^:::${a}(?:\\s|$)`,"m"),h=(u=d.match(f))==null?void 0:u.index;return h!==void 0?h:-1},tokenize(d,u,f){let h=new RegExp(`^:::${a}(?:\\s+\\{([^}]*)\\})?\\s*:::(?:\\n|$)`),p=d.match(h);if(!p)return;let m=p[1]||"",g=r(m);if(!s.find(w=>!(w in g)))return{type:e,raw:p[0],attributes:g}}},renderMarkdown:d=>{let u=c(d.attrs||{}),f=o(u),h=f?` {${f}}`:"";return`:::${a}${h} :::`}}}function Zt(t){let{nodeName:e,name:n,getContent:r,parseAttributes:o=rl,serializeAttributes:i=ol,defaultAttributes:s={},content:l="block",allowedAttributes:a}=t,c=n||e,d=u=>{if(!a)return u;let f={};return a.forEach(h=>{h in u&&(f[h]=u[h])}),f};return{parseMarkdown:(u,f)=>{let h;if(r){let m=r(u);h=typeof m=="string"?[{type:"text",text:m}]:m}else l==="block"?h=f.parseChildren(u.tokens||[]):h=f.parseInline(u.tokens||[]);let p={...s,...u.attributes};return f.createNode(e,p,h)},markdownTokenizer:{name:e,level:"block",start(u){var f;let h=new RegExp(`^:::${c}`,"m"),p=(f=u.match(h))==null?void 0:f.index;return p!==void 0?p:-1},tokenize(u,f,h){var p;let m=new RegExp(`^:::${c}(?:\\s+\\{([^}]*)\\})?\\s*\\n`),g=u.match(m);if(!g)return;let[y,w=""]=g,b=o(w),C=1,x=y.length,S="",k=/^:::([\w-]*)(\s.*)?/gm,O=u.slice(x);for(k.lastIndex=0;;){let T=k.exec(O);if(T===null)break;let A=T.index,$=T[1];if(!((p=T[2])!=null&&p.endsWith(":::"))){if($)C+=1;else if(C-=1,C===0){let z=O.slice(0,A);S=z.trim();let K=u.slice(0,x+A+T[0].length),V=[];if(S)if(l==="block")for(V=h.blockTokens(z),V.forEach(N=>{N.text&&(!N.tokens||N.tokens.length===0)&&(N.tokens=h.inlineTokens(N.text))});V.length>0;){let N=V[V.length-1];if(N.type==="paragraph"&&(!N.text||N.text.trim()===""))V.pop();else break}else V=h.inlineTokens(S);return{type:e,raw:K,attributes:b,content:S,tokens:V}}}}}},renderMarkdown:(u,f)=>{let h=d(u.attrs||{}),p=i(h),m=p?` {${p}}`:"",g=f.renderChildren(u.content||[],` + +`);return`:::${c}${m} + +${g} + +:::`}}}function vb(t){if(!t.trim())return{};let e={},n=/(\w+)=(?:"([^"]*)"|'([^']*)')/g,r=n.exec(t);for(;r!==null;){let[,o,i,s]=r;e[o]=i||s,r=n.exec(t)}return e}function Mb(t){return Object.entries(t).filter(([,e])=>e!=null).map(([e,n])=>`${e}="${n}"`).join(" ")}function Tb(t){let{nodeName:e,name:n,getContent:r,parseAttributes:o=vb,serializeAttributes:i=Mb,defaultAttributes:s={},selfClosing:l=!1,allowedAttributes:a}=t,c=n||e,d=f=>{if(!a)return f;let h={};return a.forEach(p=>{let m=typeof p=="string"?p:p.name,g=typeof p=="string"?void 0:p.skipIfDefault;if(m in f){let y=f[m];if(g!==void 0&&y===g)return;h[m]=y}}),h},u=c.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return{parseMarkdown:(f,h)=>{let p={...s,...f.attributes};if(l)return h.createNode(e,p);let m=r?r(f):f.content||"";return m?h.createNode(e,p,[h.createTextNode(m)]):h.createNode(e,p,[])},markdownTokenizer:{name:e,level:"inline",start(f){let h=l?new RegExp(`\\[${u}\\s*[^\\]]*\\]`):new RegExp(`\\[${u}\\s*[^\\]]*\\][\\s\\S]*?\\[\\/${u}\\]`),p=f.match(h),m=p?.index;return m!==void 0?m:-1},tokenize(f,h,p){let m=l?new RegExp(`^\\[${u}\\s*([^\\]]*)\\]`):new RegExp(`^\\[${u}\\s*([^\\]]*)\\]([\\s\\S]*?)\\[\\/${u}\\]`),g=f.match(m);if(!g)return;let y="",w="";if(l){let[,C]=g;w=C}else{let[,C,x]=g;w=C,y=x||""}let b=o(w.trim());return{type:e,raw:g[0],content:y.trim(),attributes:b}}},renderMarkdown:f=>{let h="";r?h=r(f):f.content&&f.content.length>0&&(h=f.content.filter(y=>y.type==="text").map(y=>y.text).join(""));let p=d(f.attrs||{}),m=i(p),g=m?` ${m}`:"";return l?`[${c}${g}]`:`[${c}${g}]${h}[/${c}]`}}}function wo(t,e,n){var r,o,i,s;let l=t.split(` +`),a=[],c="",d=0,u=e.baseIndentSize||2;for(;d0)break;if(f.trim()===""){d+=1,c=`${c}${f} +`;continue}else return}let p=e.extractItemData(h),{indentLevel:m,mainContent:g}=p;c=`${c}${f} +`;let y=[g];for(d+=1;dA.trim()!=="");if(k===-1)break;if((((o=(r=l[d+1+k].match(/^(\s*)/))==null?void 0:r[1])==null?void 0:o.length)||0)>m){y.push(x),c=`${c}${x} +`,d+=1;continue}else break}if((((s=(i=x.match(/^(\s*)/))==null?void 0:i[1])==null?void 0:s.length)||0)>m)y.push(x),c=`${c}${x} +`,d+=1;else break}let w,b=y.slice(1);if(b.length>0){let x=b.map(S=>S.slice(m+u)).join(` +`);x.trim()&&(e.customNestedParser?w=e.customNestedParser(x):w=n.blockTokens(x))}let C=e.createToken(p,w);a.push(C)}if(a.length!==0)return{items:a,raw:c}}function cr(t,e,n,r){if(!t||!Array.isArray(t.content))return"";let o=typeof n=="function"?n(r):n,[i,...s]=t.content,l=e.renderChildren([i]),a=[`${o}${l}`];return s&&s.length>0&&s.forEach(c=>{let d=e.renderChildren([c]);if(d){let u=d.split(` +`).map(f=>f?e.indent(f):"").join(` +`);a.push(u)}}),a.join(` +`)}function Ab(t,e,n={}){let{state:r}=e,{doc:o,tr:i}=r,s=t;o.descendants((l,a)=>{let c=i.mapping.map(a),d=i.mapping.map(a)+l.nodeSize,u=null;if(l.marks.forEach(h=>{if(h!==s)return!1;u=h}),!u)return;let f=!1;if(Object.keys(n).forEach(h=>{n[h]!==u.attrs[h]&&(f=!0)}),f){let h=t.type.create({...t.attrs,...n});i.removeMark(c,d,t.type),i.addMark(c,d,h)}}),i.docChanged&&e.view.dispatch(i)}var F=class xu extends nl{constructor(){super(...arguments),this.type="node"}static create(e={}){let n=typeof e=="function"?e():e;return new xu(n)}configure(e){return super.configure(e)}extend(e){let n=typeof e=="function"?e():e;return super.extend(n)}};function Me(t){return new fb({find:t.find,handler:({state:e,range:n,match:r,pasteEvent:o})=>{let i=G(t.getAttributes,void 0,r,o);if(i===!1||i===null)return null;let{tr:s}=e,l=r[r.length-1],a=r[0],c=n.to;if(l){let d=a.search(/\S/),u=n.from+a.indexOf(l),f=u+l.length;if(po(n.from,n.to,e.doc).filter(p=>p.mark.type.excluded.find(g=>g===t.type&&g!==p.mark.type)).filter(p=>p.to>u).length)return null;fn.from&&s.delete(n.from+d,u),c=n.from+d+l.length,s.addMark(n.from+d,c,t.type.create(i||{})),s.removeStoredMark(t.type)}}})}function ku(t={}){return new P({view(e){return new il(e,t)}})}var il=class{constructor(e,n){var r;this.editorView=e,this.cursorPos=null,this.element=null,this.timeout=-1,this.width=(r=n.width)!==null&&r!==void 0?r:1,this.color=n.color===!1?void 0:n.color||"black",this.class=n.class,this.handlers=["dragover","dragend","drop","dragleave"].map(o=>{let i=s=>{this[o](s)};return e.dom.addEventListener(o,i),{name:o,handler:i}})}destroy(){this.handlers.forEach(({name:e,handler:n})=>this.editorView.dom.removeEventListener(e,n))}update(e,n){this.cursorPos!=null&&n.doc!=e.state.doc&&(this.cursorPos>e.state.doc.content.size?this.setCursor(null):this.updateOverlay())}setCursor(e){e!=this.cursorPos&&(this.cursorPos=e,e==null?(this.element.parentNode.removeChild(this.element),this.element=null):this.updateOverlay())}updateOverlay(){let e=this.editorView.state.doc.resolve(this.cursorPos),n=!e.parent.inlineContent,r,o=this.editorView.dom,i=o.getBoundingClientRect(),s=i.width/o.offsetWidth,l=i.height/o.offsetHeight;if(n){let u=e.nodeBefore,f=e.nodeAfter;if(u||f){let h=this.editorView.nodeDOM(this.cursorPos-(u?u.nodeSize:0));if(h){let p=h.getBoundingClientRect(),m=u?p.bottom:p.top;u&&f&&(m=(m+this.editorView.nodeDOM(this.cursorPos).getBoundingClientRect().top)/2);let g=this.width/2*l;r={left:p.left,right:p.right,top:m-g,bottom:m+g}}}}if(!r){let u=this.editorView.coordsAtPos(this.cursorPos),f=this.width/2*s;r={left:u.left-f,right:u.left+f,top:u.top,bottom:u.bottom}}let a=this.editorView.dom.offsetParent;this.element||(this.element=a.appendChild(document.createElement("div")),this.class&&(this.element.className=this.class),this.element.style.cssText="position: absolute; z-index: 50; pointer-events: none;",this.color&&(this.element.style.backgroundColor=this.color)),this.element.classList.toggle("prosemirror-dropcursor-block",n),this.element.classList.toggle("prosemirror-dropcursor-inline",!n);let c,d;if(!a||a==document.body&&getComputedStyle(a).position=="static")c=-pageXOffset,d=-pageYOffset;else{let u=a.getBoundingClientRect(),f=u.width/a.offsetWidth,h=u.height/a.offsetHeight;c=u.left-a.scrollLeft*f,d=u.top-a.scrollTop*h}this.element.style.left=(r.left-c)/s+"px",this.element.style.top=(r.top-d)/l+"px",this.element.style.width=(r.right-r.left)/s+"px",this.element.style.height=(r.bottom-r.top)/l+"px"}scheduleRemoval(e){clearTimeout(this.timeout),this.timeout=setTimeout(()=>this.setCursor(null),e)}dragover(e){if(!this.editorView.editable)return;let n=this.editorView.posAtCoords({left:e.clientX,top:e.clientY}),r=n&&n.inside>=0&&this.editorView.state.doc.nodeAt(n.inside),o=r&&r.type.spec.disableDropCursor,i=typeof o=="function"?o(this.editorView,n,e):o;if(n&&!i){let s=n.pos;if(this.editorView.dragging&&this.editorView.dragging.slice){let l=zr(this.editorView.state.doc,s,this.editorView.dragging.slice);l!=null&&(s=l)}this.setCursor(s),this.scheduleRemoval(5e3)}}dragend(){this.scheduleRemoval(20)}drop(){this.scheduleRemoval(20)}dragleave(e){this.editorView.dom.contains(e.relatedTarget)||this.setCursor(null)}};var ae=class t extends I{constructor(e){super(e,e)}map(e,n){let r=e.resolve(n.map(this.head));return t.valid(r)?new t(r):I.near(r)}content(){return E.empty}eq(e){return e instanceof t&&e.head==this.head}toJSON(){return{type:"gapcursor",pos:this.head}}static fromJSON(e,n){if(typeof n.pos!="number")throw new RangeError("Invalid input for GapCursor.fromJSON");return new t(e.resolve(n.pos))}getBookmark(){return new sl(this.anchor)}static valid(e){let n=e.parent;if(n.isTextblock||!Eb(e)||!Nb(e))return!1;let r=n.type.spec.allowGapCursor;if(r!=null)return r;let o=n.contentMatchAt(e.index()).defaultType;return o&&o.isTextblock}static findGapCursorFrom(e,n,r=!1){e:for(;;){if(!r&&t.valid(e))return e;let o=e.pos,i=null;for(let s=e.depth;;s--){let l=e.node(s);if(n>0?e.indexAfter(s)0){i=l.child(n>0?e.indexAfter(s):e.index(s)-1);break}else if(s==0)return null;o+=n;let a=e.doc.resolve(o);if(t.valid(a))return a}for(;;){let s=n>0?i.firstChild:i.lastChild;if(!s){if(i.isAtom&&!i.isText&&!L.isSelectable(i)){e=e.doc.resolve(o+i.nodeSize*n),r=!1;continue e}break}i=s,o+=n;let l=e.doc.resolve(o);if(t.valid(l))return l}return null}}};ae.prototype.visible=!1;ae.findFrom=ae.findGapCursorFrom;I.jsonID("gapcursor",ae);var sl=class t{constructor(e){this.pos=e}map(e){return new t(e.map(this.pos))}resolve(e){let n=e.resolve(this.pos);return ae.valid(n)?new ae(n):I.near(n)}};function Su(t){return t.isAtom||t.spec.isolating||t.spec.createGapCursor}function Eb(t){for(let e=t.depth;e>=0;e--){let n=t.index(e),r=t.node(e);if(n==0){if(r.type.spec.isolating)return!0;continue}for(let o=r.child(n-1);;o=o.lastChild){if(o.childCount==0&&!o.inlineContent||Su(o.type))return!0;if(o.inlineContent)return!1}}return!0}function Nb(t){for(let e=t.depth;e>=0;e--){let n=t.indexAfter(e),r=t.node(e);if(n==r.childCount){if(r.type.spec.isolating)return!0;continue}for(let o=r.child(n);;o=o.firstChild){if(o.childCount==0&&!o.inlineContent||Su(o.type))return!0;if(o.inlineContent)return!1}}return!0}function Cu(){return new P({props:{decorations:Ib,createSelectionBetween(t,e,n){return e.pos==n.pos&&ae.valid(n)?new ae(n):null},handleClick:Rb,handleKeyDown:Ob,handleDOMEvents:{beforeinput:Db}}})}var Ob=or({ArrowLeft:xo("horiz",-1),ArrowRight:xo("horiz",1),ArrowUp:xo("vert",-1),ArrowDown:xo("vert",1)});function xo(t,e){let n=t=="vert"?e>0?"down":"up":e>0?"right":"left";return function(r,o,i){let s=r.selection,l=e>0?s.$to:s.$from,a=s.empty;if(s instanceof D){if(!i.endOfTextblock(n)||l.depth==0)return!1;a=!1,l=r.doc.resolve(e>0?l.after():l.before())}let c=ae.findGapCursorFrom(l,e,a);return c?(o&&o(r.tr.setSelection(new ae(c))),!0):!1}}function Rb(t,e,n){if(!t||!t.editable)return!1;let r=t.state.doc.resolve(e);if(!ae.valid(r))return!1;let o=t.posAtCoords({left:n.clientX,top:n.clientY});return o&&o.inside>-1&&L.isSelectable(t.state.doc.nodeAt(o.inside))?!1:(t.dispatch(t.state.tr.setSelection(new ae(r))),!0)}function Db(t,e){if(e.inputType!="insertCompositionText"||!(t.state.selection instanceof ae))return!1;let{$from:n}=t.state.selection,r=n.parent.contentMatchAt(n.index()).findWrapping(t.state.schema.nodes.text);if(!r)return!1;let o=v.empty;for(let s=r.length-1;s>=0;s--)o=v.from(r[s].createAndFill(null,o));let i=t.state.tr.replace(n.pos,n.pos,new E(o,0,0));return i.setSelection(D.near(i.doc.resolve(n.pos+1))),t.dispatch(i),!1}function Ib(t){if(!(t.selection instanceof ae))return null;let e=document.createElement("div");return e.className="ProseMirror-gapcursor",Y.create(t.doc,[te.widget(t.selection.head,e,{key:"gapcursor"})])}var ko=200,he=function(){};he.prototype.append=function(e){return e.length?(e=he.from(e),!this.length&&e||e.length=n?he.empty:this.sliceInner(Math.max(0,e),Math.min(this.length,n))};he.prototype.get=function(e){if(!(e<0||e>=this.length))return this.getInner(e)};he.prototype.forEach=function(e,n,r){n===void 0&&(n=0),r===void 0&&(r=this.length),n<=r?this.forEachInner(e,n,r,0):this.forEachInvertedInner(e,n,r,0)};he.prototype.map=function(e,n,r){n===void 0&&(n=0),r===void 0&&(r=this.length);var o=[];return this.forEach(function(i,s){return o.push(e(i,s))},n,r),o};he.from=function(e){return e instanceof he?e:e&&e.length?new vu(e):he.empty};var vu=(function(t){function e(r){t.call(this),this.values=r}t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e;var n={length:{configurable:!0},depth:{configurable:!0}};return e.prototype.flatten=function(){return this.values},e.prototype.sliceInner=function(o,i){return o==0&&i==this.length?this:new e(this.values.slice(o,i))},e.prototype.getInner=function(o){return this.values[o]},e.prototype.forEachInner=function(o,i,s,l){for(var a=i;a=s;a--)if(o(this.values[a],l+a)===!1)return!1},e.prototype.leafAppend=function(o){if(this.length+o.length<=ko)return new e(this.values.concat(o.flatten()))},e.prototype.leafPrepend=function(o){if(this.length+o.length<=ko)return new e(o.flatten().concat(this.values))},n.length.get=function(){return this.values.length},n.depth.get=function(){return 0},Object.defineProperties(e.prototype,n),e})(he);he.empty=new vu([]);var Pb=(function(t){function e(n,r){t.call(this),this.left=n,this.right=r,this.length=n.length+r.length,this.depth=Math.max(n.depth,r.depth)+1}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.flatten=function(){return this.left.flatten().concat(this.right.flatten())},e.prototype.getInner=function(r){return rl&&this.right.forEachInner(r,Math.max(o-l,0),Math.min(this.length,i)-l,s+l)===!1)return!1},e.prototype.forEachInvertedInner=function(r,o,i,s){var l=this.left.length;if(o>l&&this.right.forEachInvertedInner(r,o-l,Math.max(i,l)-l,s+l)===!1||i=i?this.right.slice(r-i,o-i):this.left.slice(r,i).append(this.right.slice(0,o-i))},e.prototype.leafAppend=function(r){var o=this.right.leafAppend(r);if(o)return new e(this.left,o)},e.prototype.leafPrepend=function(r){var o=this.left.leafPrepend(r);if(o)return new e(o,this.right)},e.prototype.appendInner=function(r){return this.left.depth>=Math.max(this.right.depth,r.depth)+1?new e(this.left,new e(this.right,r)):new e(this,r)},e})(he),ll=he;var Lb=500,tn=class t{constructor(e,n){this.items=e,this.eventCount=n}popEvent(e,n){if(this.eventCount==0)return null;let r=this.items.length;for(;;r--)if(this.items.get(r-1).selection){--r;break}let o,i;n&&(o=this.remapping(r,this.items.length),i=o.maps.length);let s=e.tr,l,a,c=[],d=[];return this.items.forEach((u,f)=>{if(!u.step){o||(o=this.remapping(r,f+1),i=o.maps.length),i--,d.push(u);return}if(o){d.push(new nt(u.map));let h=u.step.map(o.slice(i)),p;h&&s.maybeStep(h).doc&&(p=s.mapping.maps[s.mapping.maps.length-1],c.push(new nt(p,void 0,void 0,c.length+d.length))),i--,p&&o.appendMap(p,i)}else s.maybeStep(u.step);if(u.selection)return l=o?u.selection.map(o.slice(i)):u.selection,a=new t(this.items.slice(0,r).append(d.reverse().concat(c)),this.eventCount-1),!1},this.items.length,0),{remaining:a,transform:s,selection:l}}addTransform(e,n,r,o){let i=[],s=this.eventCount,l=this.items,a=!o&&l.length?l.get(l.length-1):null;for(let d=0;dzb&&(l=Bb(l,c),s-=c),new t(l.append(i),s)}remapping(e,n){let r=new jn;return this.items.forEach((o,i)=>{let s=o.mirrorOffset!=null&&i-o.mirrorOffset>=e?r.maps.length-o.mirrorOffset:void 0;r.appendMap(o.map,s)},e,n),r}addMaps(e){return this.eventCount==0?this:new t(this.items.append(e.map(n=>new nt(n))),this.eventCount)}rebased(e,n){if(!this.eventCount)return this;let r=[],o=Math.max(0,this.items.length-n),i=e.mapping,s=e.steps.length,l=this.eventCount;this.items.forEach(f=>{f.selection&&l--},o);let a=n;this.items.forEach(f=>{let h=i.getMirror(--a);if(h==null)return;s=Math.min(s,h);let p=i.maps[h];if(f.step){let m=e.steps[h].invert(e.docs[h]),g=f.selection&&f.selection.map(i.slice(a+1,h));g&&l++,r.push(new nt(p,m,g))}else r.push(new nt(p))},o);let c=[];for(let f=n;fLb&&(u=u.compress(this.items.length-r.length)),u}emptyItemCount(){let e=0;return this.items.forEach(n=>{n.step||e++}),e}compress(e=this.items.length){let n=this.remapping(0,e),r=n.maps.length,o=[],i=0;return this.items.forEach((s,l)=>{if(l>=e)o.push(s),s.selection&&i++;else if(s.step){let a=s.step.map(n.slice(r)),c=a&&a.getMap();if(r--,c&&n.appendMap(c,r),a){let d=s.selection&&s.selection.map(n.slice(r));d&&i++;let u=new nt(c.invert(),a,d),f,h=o.length-1;(f=o.length&&o[h].merge(u))?o[h]=f:o.push(u)}}else s.map&&r--},this.items.length,0),new t(ll.from(o.reverse()),i)}};tn.empty=new tn(ll.empty,0);function Bb(t,e){let n;return t.forEach((r,o)=>{if(r.selection&&e--==0)return n=o,!1}),t.slice(n)}var nt=class t{constructor(e,n,r,o){this.map=e,this.step=n,this.selection=r,this.mirrorOffset=o}merge(e){if(this.step&&e.step&&!e.selection){let n=e.step.merge(this.step);if(n)return new t(n.getMap().invert(),n,this.selection)}}},rt=class{constructor(e,n,r,o,i){this.done=e,this.undone=n,this.prevRanges=r,this.prevTime=o,this.prevComposition=i}},zb=20;function Hb(t,e,n,r){let o=n.getMeta(en),i;if(o)return o.historyState;n.getMeta(Vb)&&(t=new rt(t.done,t.undone,null,0,-1));let s=n.getMeta("appendedTransaction");if(n.steps.length==0)return t;if(s&&s.getMeta(en))return s.getMeta(en).redo?new rt(t.done.addTransform(n,void 0,r,So(e)),t.undone,Mu(n.mapping.maps),t.prevTime,t.prevComposition):new rt(t.done,t.undone.addTransform(n,void 0,r,So(e)),null,t.prevTime,t.prevComposition);if(n.getMeta("addToHistory")!==!1&&!(s&&s.getMeta("addToHistory")===!1)){let l=n.getMeta("composition"),a=t.prevTime==0||!s&&t.prevComposition!=l&&(t.prevTime<(n.time||0)-r.newGroupDelay||!$b(n,t.prevRanges)),c=s?al(t.prevRanges,n.mapping):Mu(n.mapping.maps);return new rt(t.done.addTransform(n,a?e.selection.getBookmark():void 0,r,So(e)),tn.empty,c,n.time,l??t.prevComposition)}else return(i=n.getMeta("rebased"))?new rt(t.done.rebased(n,i),t.undone.rebased(n,i),al(t.prevRanges,n.mapping),t.prevTime,t.prevComposition):new rt(t.done.addMaps(n.mapping.maps),t.undone.addMaps(n.mapping.maps),al(t.prevRanges,n.mapping),t.prevTime,t.prevComposition)}function $b(t,e){if(!e)return!1;if(!t.docChanged)return!0;let n=!1;return t.mapping.maps[0].forEach((r,o)=>{for(let i=0;i=e[i]&&(n=!0)}),n}function Mu(t){let e=[];for(let n=t.length-1;n>=0&&e.length==0;n--)t[n].forEach((r,o,i,s)=>e.push(i,s));return e}function al(t,e){if(!t)return null;let n=[];for(let r=0;r{let o=en.getState(n);if(!o||(t?o.undone:o.done).eventCount==0)return!1;if(r){let i=Fb(o,n,t);i&&r(e?i.scrollIntoView():i)}return!0}}var dl=Co(!1,!0),ul=Co(!0,!0),g1=Co(!1,!1),y1=Co(!0,!1);var C1=U.create({name:"characterCount",addOptions(){return{limit:null,mode:"textSize",textCounter:t=>t.length,wordCounter:t=>t.split(" ").filter(e=>e!=="").length}},addStorage(){return{characters:()=>0,words:()=>0}},onBeforeCreate(){this.storage.characters=t=>{let e=t?.node||this.editor.state.doc;if((t?.mode||this.options.mode)==="textSize"){let r=e.textBetween(0,e.content.size,void 0," ");return this.options.textCounter(r)}return e.nodeSize},this.storage.words=t=>{let e=t?.node||this.editor.state.doc,n=e.textBetween(0,e.content.size," "," ");return this.options.wordCounter(n)}},addProseMirrorPlugins(){let t=!1;return[new P({key:new H("characterCount"),appendTransaction:(e,n,r)=>{if(t)return;let o=this.options.limit;if(o==null||o===0){t=!0;return}let i=this.storage.characters({node:r.doc});if(i>o){let s=i-o,l=0,a=s;console.warn(`[CharacterCount] Initial content exceeded limit of ${o} characters. Content was automatically trimmed.`);let c=r.tr.deleteRange(l,a);return t=!0,c}t=!0},filterTransaction:(e,n)=>{let r=this.options.limit;if(!e.docChanged||r===0||r===null||r===void 0)return!0;let o=this.storage.characters({node:n.doc}),i=this.storage.characters({node:e.doc});if(i<=r||o>r&&i>r&&i<=o)return!0;if(o>r&&i>r&&i>o||!e.getMeta("paste"))return!1;let l=e.selection.$head.pos,a=i-r,c=l-a,d=l;return e.deleteRange(c,d),!(this.storage.characters({node:e.doc})>r)}})]}}),Nu=U.create({name:"dropCursor",addOptions(){return{color:"currentColor",width:1,class:void 0}},addProseMirrorPlugins(){return[ku(this.options)]}}),N1=U.create({name:"focus",addOptions(){return{className:"has-focus",mode:"all"}},addProseMirrorPlugins(){return[new P({key:new H("focus"),props:{decorations:({doc:t,selection:e})=>{let{isEditable:n,isFocused:r}=this.editor,{anchor:o}=e,i=[];if(!n||!r)return Y.create(t,[]);let s=0;this.options.mode==="deepest"&&t.descendants((a,c)=>{if(a.isText)return;if(!(o>=c&&o<=c+a.nodeSize-1))return!1;s+=1});let l=0;return t.descendants((a,c)=>{if(a.isText||!(o>=c&&o<=c+a.nodeSize-1))return!1;if(l+=1,this.options.mode==="deepest"&&s-l>0||this.options.mode==="shallowest"&&l>1)return this.options.mode==="deepest";i.push(te.node(c,c+a.nodeSize,{class:this.options.className}))}),Y.create(t,i)}}})]}}),Ou=U.create({name:"gapCursor",addProseMirrorPlugins(){return[Cu()]},extendNodeSchema(t){var e;let n={name:t.name,options:t.options,storage:t.storage};return{allowGapCursor:(e=G(B(t,"allowGapCursor",n)))!=null?e:null}}}),fl=U.create({name:"placeholder",addOptions(){return{emptyEditorClass:"is-editor-empty",emptyNodeClass:"is-empty",placeholder:"Write something \u2026",showOnlyWhenEditable:!0,showOnlyCurrent:!0,includeChildren:!1}},addProseMirrorPlugins(){return[new P({key:new H("placeholder"),props:{decorations:({doc:t,selection:e})=>{let n=this.editor.isEditable||!this.options.showOnlyWhenEditable,{anchor:r}=e,o=[];if(!n)return null;let i=this.editor.isEmpty;return t.descendants((s,l)=>{let a=r>=l&&r<=l+s.nodeSize,c=!s.isLeaf&&lr(s);if((a||!this.options.showOnlyCurrent)&&c){let d=[this.options.emptyNodeClass];i&&d.push(this.options.emptyEditorClass);let u=te.node(l,l+s.nodeSize,{class:d.join(" "),"data-placeholder":typeof this.options.placeholder=="function"?this.options.placeholder({editor:this.editor,node:s,pos:l,hasAnchor:a}):this.options.placeholder});o.push(u)}return this.options.includeChildren}),Y.create(t,o)}}})]}}),H1=U.create({name:"selection",addOptions(){return{className:"selection"}},addProseMirrorPlugins(){let{editor:t,options:e}=this;return[new P({key:new H("selection"),props:{decorations(n){return n.selection.empty||t.isFocused||!t.isEditable||mo(n.selection)||t.view.dragging?null:Y.create(n.doc,[te.inline(n.selection.from,n.selection.to,{class:e.className})])}}})]}});function Eu({types:t,node:e}){return e&&Array.isArray(t)&&t.includes(e.type)||e?.type===t}var V1=U.create({name:"trailingNode",addOptions(){return{node:void 0,notAfter:[]}},addProseMirrorPlugins(){var t;let e=new H(this.name),n=this.options.node||((t=this.editor.schema.topNodeType.contentMatch.defaultType)==null?void 0:t.name)||"paragraph",r=Object.entries(this.editor.schema.nodes).map(([,o])=>o).filter(o=>(this.options.notAfter||[]).concat(n).includes(o.name));return[new P({key:e,appendTransaction:(o,i,s)=>{let{doc:l,tr:a,schema:c}=s,d=e.getState(s),u=l.content.size,f=c.nodes[n];if(d)return a.insert(u,f.create())},state:{init:(o,i)=>{let s=i.tr.doc.lastChild;return!Eu({node:s,types:r})},apply:(o,i)=>{if(!o.docChanged||o.getMeta("__uniqueIDTransaction"))return i;let s=o.doc.lastChild;return!Eu({node:s,types:r})}}})]}}),Ru=U.create({name:"undoRedo",addOptions(){return{depth:100,newGroupDelay:500}},addCommands(){return{undo:()=>({state:t,dispatch:e})=>dl(t,e),redo:()=>({state:t,dispatch:e})=>ul(t,e)}},addProseMirrorPlugins(){return[Au(this.options)]},addKeyboardShortcuts(){return{"Mod-z":()=>this.editor.commands.undo(),"Shift-Mod-z":()=>this.editor.commands.redo(),"Mod-y":()=>this.editor.commands.redo(),"Mod-\u044F":()=>this.editor.commands.undo(),"Shift-Mod-\u044F":()=>this.editor.commands.redo()}}});var Nn=(t,e)=>{if(t==="slot")return 0;if(t instanceof Function)return t(e);let{children:n,...r}=e??{};if(t==="svg")throw new Error("SVG elements are not supported in the JSX syntax, use the array syntax instead");return[t,r,n]};var _b=/^\s*>\s$/,Wb=F.create({name:"blockquote",addOptions(){return{HTMLAttributes:{}}},content:"block+",group:"block",defining:!0,parseHTML(){return[{tag:"blockquote"}]},renderHTML({HTMLAttributes:t}){return Nn("blockquote",{...R(this.options.HTMLAttributes,t),children:Nn("slot",{})})},parseMarkdown:(t,e)=>e.createNode("blockquote",void 0,e.parseChildren(t.tokens||[])),renderMarkdown:(t,e)=>{if(!t.content)return"";let n=">",r=[];return t.content.forEach(o=>{let l=e.renderChildren([o]).split(` +`).map(a=>a.trim()===""?n:`${n} ${a}`);r.push(l.join(` +`))}),r.join(` +${n} +`)},addCommands(){return{setBlockquote:()=>({commands:t})=>t.wrapIn(this.name),toggleBlockquote:()=>({commands:t})=>t.toggleWrap(this.name),unsetBlockquote:()=>({commands:t})=>t.lift(this.name)}},addKeyboardShortcuts(){return{"Mod-Shift-b":()=>this.editor.commands.toggleBlockquote()}},addInputRules(){return[tt({find:_b,type:this.type})]}}),Du=Wb;var jb=/(?:^|\s)(\*\*(?!\s+\*\*)((?:[^*]+))\*\*(?!\s+\*\*))$/,Ub=/(?:^|\s)(\*\*(?!\s+\*\*)((?:[^*]+))\*\*(?!\s+\*\*))/g,Kb=/(?:^|\s)(__(?!\s+__)((?:[^_]+))__(?!\s+__))$/,qb=/(?:^|\s)(__(?!\s+__)((?:[^_]+))__(?!\s+__))/g,Jb=ee.create({name:"bold",addOptions(){return{HTMLAttributes:{}}},parseHTML(){return[{tag:"strong"},{tag:"b",getAttrs:t=>t.style.fontWeight!=="normal"&&null},{style:"font-weight=400",clearMark:t=>t.type.name===this.name},{style:"font-weight",getAttrs:t=>/^(bold(er)?|[5-9]\d{2,})$/.test(t)&&null}]},renderHTML({HTMLAttributes:t}){return Nn("strong",{...R(this.options.HTMLAttributes,t),children:Nn("slot",{})})},markdownTokenName:"strong",parseMarkdown:(t,e)=>e.applyMark("bold",e.parseInline(t.tokens||[])),renderMarkdown:(t,e)=>`**${e.renderChildren(t)}**`,addCommands(){return{setBold:()=>({commands:t})=>t.setMark(this.name),toggleBold:()=>({commands:t})=>t.toggleMark(this.name),unsetBold:()=>({commands:t})=>t.unsetMark(this.name)}},addKeyboardShortcuts(){return{"Mod-b":()=>this.editor.commands.toggleBold(),"Mod-B":()=>this.editor.commands.toggleBold()}},addInputRules(){return[Be({find:jb,type:this.type}),Be({find:Kb,type:this.type})]},addPasteRules(){return[Me({find:Ub,type:this.type}),Me({find:qb,type:this.type})]}}),Iu=Jb;var Gb=/(^|[^`])`([^`]+)`(?!`)$/,Xb=/(^|[^`])`([^`]+)`(?!`)/g,Yb=ee.create({name:"code",addOptions(){return{HTMLAttributes:{}}},excludes:"_",code:!0,exitable:!0,parseHTML(){return[{tag:"code"}]},renderHTML({HTMLAttributes:t}){return["code",R(this.options.HTMLAttributes,t),0]},markdownTokenName:"codespan",parseMarkdown:(t,e)=>e.applyMark("code",[{type:"text",text:t.text||""}]),renderMarkdown:(t,e)=>t.content?`\`${e.renderChildren(t.content)}\``:"",addCommands(){return{setCode:()=>({commands:t})=>t.setMark(this.name),toggleCode:()=>({commands:t})=>t.toggleMark(this.name),unsetCode:()=>({commands:t})=>t.unsetMark(this.name)}},addKeyboardShortcuts(){return{"Mod-e":()=>this.editor.commands.toggleCode()}},addInputRules(){return[Be({find:Gb,type:this.type})]},addPasteRules(){return[Me({find:Xb,type:this.type})]}}),Pu=Yb;var hl=4,Qb=/^```([a-z]+)?[\s\n]$/,Zb=/^~~~([a-z]+)?[\s\n]$/,e0=F.create({name:"codeBlock",addOptions(){return{languageClassPrefix:"language-",exitOnTripleEnter:!0,exitOnArrowDown:!0,defaultLanguage:null,enableTabIndentation:!1,tabSize:hl,HTMLAttributes:{}}},content:"text*",marks:"",group:"block",code:!0,defining:!0,addAttributes(){return{language:{default:this.options.defaultLanguage,parseHTML:t=>{var e;let{languageClassPrefix:n}=this.options;if(!n)return null;let i=[...((e=t.firstElementChild)==null?void 0:e.classList)||[]].filter(s=>s.startsWith(n)).map(s=>s.replace(n,""))[0];return i||null},rendered:!1}}},parseHTML(){return[{tag:"pre",preserveWhitespace:"full"}]},renderHTML({node:t,HTMLAttributes:e}){return["pre",R(this.options.HTMLAttributes,e),["code",{class:t.attrs.language?this.options.languageClassPrefix+t.attrs.language:null},0]]},markdownTokenName:"code",parseMarkdown:(t,e)=>{var n;return((n=t.raw)==null?void 0:n.startsWith("```"))===!1&&t.codeBlockStyle!=="indented"?[]:e.createNode("codeBlock",{language:t.lang||null},t.text?[e.createTextNode(t.text)]:[])},renderMarkdown:(t,e)=>{var n;let r="",o=((n=t.attrs)==null?void 0:n.language)||"";return t.content?r=[`\`\`\`${o}`,e.renderChildren(t.content),"```"].join(` +`):r=`\`\`\`${o} + +\`\`\``,r},addCommands(){return{setCodeBlock:t=>({commands:e})=>e.setNode(this.name,t),toggleCodeBlock:t=>({commands:e})=>e.toggleNode(this.name,"paragraph",t)}},addKeyboardShortcuts(){return{"Mod-Alt-c":()=>this.editor.commands.toggleCodeBlock(),Backspace:()=>{let{empty:t,$anchor:e}=this.editor.state.selection,n=e.pos===1;return!t||e.parent.type.name!==this.name?!1:n||!e.parent.textContent.length?this.editor.commands.clearNodes():!1},Tab:({editor:t})=>{var e;if(!this.options.enableTabIndentation)return!1;let n=(e=this.options.tabSize)!=null?e:hl,{state:r}=t,{selection:o}=r,{$from:i,empty:s}=o;if(i.parent.type!==this.type)return!1;let l=" ".repeat(n);return s?t.commands.insertContent(l):t.commands.command(({tr:a})=>{let{from:c,to:d}=o,h=r.doc.textBetween(c,d,` +`,` +`).split(` +`).map(p=>l+p).join(` +`);return a.replaceWith(c,d,r.schema.text(h)),!0})},"Shift-Tab":({editor:t})=>{var e;if(!this.options.enableTabIndentation)return!1;let n=(e=this.options.tabSize)!=null?e:hl,{state:r}=t,{selection:o}=r,{$from:i,empty:s}=o;return i.parent.type!==this.type?!1:s?t.commands.command(({tr:l})=>{var a;let{pos:c}=i,d=i.start(),u=i.end(),h=r.doc.textBetween(d,u,` +`,` +`).split(` +`),p=0,m=0,g=c-d;for(let S=0;S=g){p=S;break}m+=h[S].length+1}let w=((a=h[p].match(/^ */))==null?void 0:a[0])||"",b=Math.min(w.length,n);if(b===0)return!0;let C=d;for(let S=0;S{let{from:a,to:c}=o,f=r.doc.textBetween(a,c,` +`,` +`).split(` +`).map(h=>{var p;let m=((p=h.match(/^ */))==null?void 0:p[0])||"",g=Math.min(m.length,n);return h.slice(g)}).join(` +`);return l.replaceWith(a,c,r.schema.text(f)),!0})},Enter:({editor:t})=>{if(!this.options.exitOnTripleEnter)return!1;let{state:e}=t,{selection:n}=e,{$from:r,empty:o}=n;if(!o||r.parent.type!==this.type)return!1;let i=r.parentOffset===r.parent.nodeSize-2,s=r.parent.textContent.endsWith(` + +`);return!i||!s?!1:t.chain().command(({tr:l})=>(l.delete(r.pos-2,r.pos),!0)).exitCode().run()},ArrowDown:({editor:t})=>{if(!this.options.exitOnArrowDown)return!1;let{state:e}=t,{selection:n,doc:r}=e,{$from:o,empty:i}=n;if(!i||o.parent.type!==this.type||!(o.parentOffset===o.parent.nodeSize-2))return!1;let l=o.after();return l===void 0?!1:r.nodeAt(l)?t.commands.command(({tr:c})=>(c.setSelection(I.near(r.resolve(l))),!0)):t.commands.exitCode()}}},addInputRules(){return[ar({find:Qb,type:this.type,getAttributes:t=>({language:t[1]})}),ar({find:Zb,type:this.type,getAttributes:t=>({language:t[1]})})]},addProseMirrorPlugins(){return[new P({key:new H("codeBlockVSCodeHandler"),props:{handlePaste:(t,e)=>{if(!e.clipboardData||this.editor.isActive(this.type.name))return!1;let n=e.clipboardData.getData("text/plain"),r=e.clipboardData.getData("vscode-editor-data"),o=r?JSON.parse(r):void 0,i=o?.mode;if(!n||!i)return!1;let{tr:s,schema:l}=t.state,a=l.text(n.replace(/\r\n?/g,` +`));return s.replaceSelectionWith(this.type.create({language:i},a)),s.selection.$from.parent.type!==this.type&&s.setSelection(D.near(s.doc.resolve(Math.max(0,s.selection.from-2)))),s.setMeta("paste",!0),t.dispatch(s),!0}}})]}}),Lu=e0;var Bu=F.create({name:"customBlock",group:"block",atom:!0,defining:!0,draggable:!0,selectable:!0,isolating:!0,allowGapCursor:!0,inline:!1,addNodeView(){return({editor:t,node:e,getPos:n,HTMLAttributes:r,decorations:o,extension:i})=>{let s=document.createElement("div");s.setAttribute("data-config",e.attrs.config),s.setAttribute("data-id",e.attrs.id),s.setAttribute("data-type","customBlock");let l=document.createElement("div");if(l.className="fi-fo-rich-editor-custom-block-header fi-not-prose",s.appendChild(l),t.isEditable&&typeof e.attrs.config=="object"&&e.attrs.config!==null&&Object.keys(e.attrs.config).length>0){let c=document.createElement("div");c.className="fi-fo-rich-editor-custom-block-edit-btn-ctn",l.appendChild(c);let d=document.createElement("button");d.className="fi-icon-btn",d.type="button",d.innerHTML=i.options.editCustomBlockButtonIconHtml,d.addEventListener("click",()=>i.options.editCustomBlockUsing(e.attrs.id,e.attrs.config)),c.appendChild(d)}let a=document.createElement("p");if(a.className="fi-fo-rich-editor-custom-block-heading",a.textContent=e.attrs.label,l.appendChild(a),t.isEditable){let c=document.createElement("div");c.className="fi-fo-rich-editor-custom-block-delete-btn-ctn",l.appendChild(c);let d=document.createElement("button");d.className="fi-icon-btn",d.type="button",d.innerHTML=i.options.deleteCustomBlockButtonIconHtml,d.addEventListener("click",()=>t.chain().setNodeSelection(n()).deleteSelection().run()),c.appendChild(d)}if(e.attrs.preview){let c=document.createElement("div");c.className="fi-fo-rich-editor-custom-block-preview fi-not-prose",c.innerHTML=new TextDecoder().decode(Uint8Array.from(atob(e.attrs.preview),d=>d.charCodeAt(0))),s.appendChild(c)}return{dom:s}}},addOptions(){return{deleteCustomBlockButtonIconHtml:null,editCustomBlockButtonIconHtml:null,editCustomBlockUsing:()=>{},insertCustomBlockUsing:()=>{}}},addAttributes(){return{config:{default:null,parseHTML:t=>JSON.parse(t.getAttribute("data-config"))},id:{default:null,parseHTML:t=>t.getAttribute("data-id"),renderHTML:t=>t.id?{"data-id":t.id}:{}},label:{default:null,parseHTML:t=>t.getAttribute("data-label"),rendered:!1},preview:{default:null,parseHTML:t=>t.getAttribute("data-preview"),rendered:!1}}},parseHTML(){return[{tag:`div[data-type="${this.name}"]`}]},renderHTML({HTMLAttributes:t}){return["div",R(t)]},addKeyboardShortcuts(){return{Backspace:()=>this.editor.commands.command(({tr:t,state:e})=>{let n=!1,{selection:r}=e,{empty:o,anchor:i}=r;if(!o)return!1;let s=new ie,l=0;return e.doc.nodesBetween(i-1,i,(a,c)=>{if(a.type.name===this.name)return n=!0,s=a,l=c,!1}),n})}},addProseMirrorPlugins(){let{insertCustomBlockUsing:t}=this.options;return[new P({props:{handleDrop(e,n){if(!n||(n.preventDefault(),!n.dataTransfer.getData("customBlock")))return!1;let r=n.dataTransfer.getData("customBlock");return t(r,e.posAtCoords({left:n.clientX,top:n.clientY}).pos),!1}}})]}});var vo=(t,e)=>e.view.domAtPos(t).node.offsetParent!==null,t0=(t,e,n)=>{for(let r=t.depth;r>0;r-=1){let o=t.node(r),i=e(o),s=vo(t.start(r),n);if(i&&s)return{pos:r>0?t.before(r):0,start:t.start(r),depth:r,node:o}}},zu=(t,e)=>{let{state:n,view:r,extensionManager:o}=t,{schema:i,selection:s}=n,{empty:l,$anchor:a}=s,c=!!o.extensions.find(y=>y.name==="gapCursor");if(!l||a.parent.type!==i.nodes.detailsSummary||!c||e==="right"&&a.parentOffset!==a.parent.nodeSize-2)return!1;let d=et(y=>y.type===i.nodes.details)(s);if(!d)return!1;let u=En(d.node,y=>y.type===i.nodes.detailsContent);if(!u.length||vo(d.start+u[0].pos+1,t))return!1;let h=n.doc.resolve(d.pos+d.node.nodeSize),p=ae.findFrom(h,1,!1);if(!p)return!1;let{tr:m}=n,g=new ae(p);return m.setSelection(g),m.scrollIntoView(),r.dispatch(m),!0},Hu=F.create({name:"details",content:"detailsSummary detailsContent",group:"block",defining:!0,isolating:!0,allowGapCursor:!1,addOptions(){return{persist:!1,openClassName:"is-open",HTMLAttributes:{}}},addAttributes(){return this.options.persist?{open:{default:!1,parseHTML:t=>t.hasAttribute("open"),renderHTML:({open:t})=>t?{open:""}:{}}}:[]},parseHTML(){return[{tag:"details"}]},renderHTML({HTMLAttributes:t}){return["details",R(this.options.HTMLAttributes,t),0]},...Zt({nodeName:"details",content:"block"}),addNodeView(){return({editor:t,getPos:e,node:n,HTMLAttributes:r})=>{let o=document.createElement("div"),i=R(this.options.HTMLAttributes,r,{"data-type":this.name});Object.entries(i).forEach(([c,d])=>o.setAttribute(c,d));let s=document.createElement("button");s.type="button",o.append(s);let l=document.createElement("div");o.append(l);let a=c=>{if(c!==void 0)if(c){if(o.classList.contains(this.options.openClassName))return;o.classList.add(this.options.openClassName)}else{if(!o.classList.contains(this.options.openClassName))return;o.classList.remove(this.options.openClassName)}else o.classList.toggle(this.options.openClassName);let d=new Event("toggleDetailsContent"),u=l.querySelector(':scope > div[data-type="detailsContent"]');u?.dispatchEvent(d)};return n.attrs.open&&setTimeout(()=>a()),s.addEventListener("click",()=>{if(a(),!this.options.persist){t.commands.focus(void 0,{scrollIntoView:!1});return}if(t.isEditable&&typeof e=="function"){let{from:c,to:d}=t.state.selection;t.chain().command(({tr:u})=>{let f=e();if(!f)return!1;let h=u.doc.nodeAt(f);return h?.type!==this.type?!1:(u.setNodeMarkup(f,void 0,{open:!h.attrs.open}),!0)}).setTextSelection({from:c,to:d}).focus(void 0,{scrollIntoView:!1}).run()}}),{dom:o,contentDOM:l,ignoreMutation(c){return c.type==="selection"?!1:!o.contains(c.target)||o===c.target},update:c=>c.type!==this.type?!1:(c.attrs.open!==void 0&&a(c.attrs.open),!0)}}},addCommands(){return{setDetails:()=>({state:t,chain:e})=>{var n;let{schema:r,selection:o}=t,{$from:i,$to:s}=o,l=i.blockRange(s);if(!l)return!1;let a=t.doc.slice(l.start,l.end);if(!r.nodes.detailsContent.contentMatch.matchFragment(a.content))return!1;let d=((n=a.toJSON())==null?void 0:n.content)||[];return e().insertContentAt({from:l.start,to:l.end},{type:this.name,content:[{type:"detailsSummary"},{type:"detailsContent",content:d}]}).setTextSelection(l.start+2).run()},unsetDetails:()=>({state:t,chain:e})=>{let{selection:n,schema:r}=t,o=et(y=>y.type===this.type)(n);if(!o)return!1;let i=En(o.node,y=>y.type===r.nodes.detailsSummary),s=En(o.node,y=>y.type===r.nodes.detailsContent);if(!i.length||!s.length)return!1;let l=i[0],a=s[0],c=o.pos,d=t.doc.resolve(c),u=c+o.node.nodeSize,f={from:c,to:u},h=a.node.content.toJSON()||[],p=d.parent.type.contentMatch.defaultType,g=[p?.create(null,l.node.content).toJSON(),...h];return e().insertContentAt(f,g).setTextSelection(c+1).run()}}},addKeyboardShortcuts(){return{Backspace:()=>{let{schema:t,selection:e}=this.editor.state,{empty:n,$anchor:r}=e;return!n||r.parent.type!==t.nodes.detailsSummary?!1:r.parentOffset!==0?this.editor.commands.command(({tr:o})=>{let i=r.pos-1,s=r.pos;return o.delete(i,s),!0}):this.editor.commands.unsetDetails()},Enter:({editor:t})=>{let{state:e,view:n}=t,{schema:r,selection:o}=e,{$head:i}=o;if(i.parent.type!==r.nodes.detailsSummary)return!1;let s=vo(i.after()+1,t),l=s?e.doc.nodeAt(i.after()):i.node(-2);if(!l)return!1;let a=s?0:i.indexAfter(-1),c=sr(l.contentMatchAt(a));if(!c||!l.canReplaceWith(a,a,c))return!1;let d=c.createAndFill();if(!d)return!1;let u=s?i.after()+1:i.after(-1),f=e.tr.replaceWith(u,u,d),h=f.doc.resolve(u),p=I.near(h,1);return f.setSelection(p),f.scrollIntoView(),n.dispatch(f),!0},ArrowRight:({editor:t})=>zu(t,"right"),ArrowDown:({editor:t})=>zu(t,"down")}},addProseMirrorPlugins(){return[new P({key:new H("detailsSelection"),appendTransaction:(t,e,n)=>{let{editor:r,type:o}=this;if(r.view.composing||!t.some(y=>y.selectionSet)||!e.selection.empty||!n.selection.empty||!tl(n,o.name))return;let{$from:a}=n.selection;if(vo(a.pos,r))return;let d=t0(a,y=>y.type===o,r);if(!d)return;let u=En(d.node,y=>y.type===n.schema.nodes.detailsSummary);if(!u.length)return;let f=u[0],p=(e.selection.from{let e=document.createElement("div"),n=R(this.options.HTMLAttributes,t,{"data-type":this.name,hidden:"hidden"});return Object.entries(n).forEach(([r,o])=>e.setAttribute(r,o)),e.addEventListener("toggleDetailsContent",()=>{e.toggleAttribute("hidden")}),{dom:e,contentDOM:e,ignoreMutation(r){return r.type==="selection"?!1:!e.contains(r.target)||e===r.target},update:r=>r.type===this.type}}},addKeyboardShortcuts(){return{Enter:({editor:t})=>{let{state:e,view:n}=t,{selection:r}=e,{$from:o,empty:i}=r,s=et($=>$.type===this.type)(r);if(!i||!s||!s.node.childCount)return!1;let l=o.index(s.depth),{childCount:a}=s.node;if(!(a===l+1))return!1;let d=s.node.type.contentMatch.defaultType,u=d?.createAndFill();if(!u)return!1;let f=e.doc.resolve(s.pos+1),h=a-1,p=s.node.child(h),m=f.posAtIndex(h,s.depth);if(!p.eq(u))return!1;let y=o.node(-3);if(!y)return!1;let w=o.indexAfter(-3),b=sr(y.contentMatchAt(w));if(!b||!y.canReplaceWith(w,w,b))return!1;let C=b.createAndFill();if(!C)return!1;let{tr:x}=e,S=o.after(-2);x.replaceWith(S,S,C);let k=x.doc.resolve(S),O=I.near(k,1);x.setSelection(O);let T=m,A=m+p.nodeSize;return x.delete(T,A),x.scrollIntoView(),n.dispatch(x),!0}}},...Zt({nodeName:"detailsContent"})}),Fu=F.create({name:"detailsSummary",content:"text*",defining:!0,selectable:!1,isolating:!0,addOptions(){return{HTMLAttributes:{}}},parseHTML(){return[{tag:"summary"}]},renderHTML({HTMLAttributes:t}){return["summary",R(this.options.HTMLAttributes,t),0]},...Zt({nodeName:"detailsSummary",content:"inline"})});var n0=F.create({name:"doc",topNode:!0,content:"block+",renderMarkdown:(t,e)=>t.content?e.renderChildren(t.content,` + +`):""}),Vu=n0;var _u=F.create({name:"grid",group:"block",defining:!0,isolating:!0,allowGapCursor:!1,content:"gridColumn+",addOptions(){return{HTMLAttributes:{class:"grid-layout"}}},addAttributes(){return{"data-cols":{default:2,parseHTML:t=>t.getAttribute("data-cols")},"data-from-breakpoint":{default:"md",parseHTML:t=>t.getAttribute("data-from-breakpoint")},style:{default:null,parseHTML:t=>t.getAttribute("style"),renderHTML:t=>({style:`grid-template-columns: repeat(${t["data-cols"]}, 1fr)`})}}},parseHTML(){return[{tag:"div",getAttrs:t=>t.classList.contains("grid-layout")&&null}]},renderHTML({HTMLAttributes:t}){return["div",R(this.options.HTMLAttributes,t),0]},addCommands(){return{insertGrid:({columns:t=[1,1],fromBreakpoint:e,coordinates:n=null}={})=>({tr:r,dispatch:o,editor:i})=>{let s=i.schema.nodes.gridColumn,l=Array.isArray(t)&&t.length?t:[1,1],a=[];for(let u=0;uNumber(u)||1).reduce((u,f)=>u+f,0),d=i.schema.nodes.grid.createChecked({"data-cols":c,"data-from-breakpoint":e},a);if(o){let u=r.selection.anchor+1;[null,void 0].includes(n?.from)?r.replaceSelectionWith(d).scrollIntoView().setSelection(D.near(r.doc.resolve(u))):r.replaceRangeWith(n.from,n.to,d).scrollIntoView().setSelection(D.near(r.doc.resolve(n.from)))}return!0}}}});var Wu=F.create({name:"gridColumn",content:"block+",isolating:!0,addOptions(){return{HTMLAttributes:{class:"grid-layout-col"}}},addAttributes(){return{"data-col-span":{default:1,parseHTML:t=>t.getAttribute("data-col-span"),renderHTML:t=>({"data-col-span":t["data-col-span"]??1})},style:{default:null,parseHTML:t=>t.getAttribute("style"),renderHTML:t=>({style:`grid-column: span ${t["data-col-span"]??1};`})}}},parseHTML(){return[{tag:"div",getAttrs:t=>t.classList.contains("grid-layout-col")&&null}]},renderHTML({HTMLAttributes:t}){return["div",R(this.options.HTMLAttributes,t),0]}});var r0=F.create({name:"hardBreak",markdownTokenName:"br",addOptions(){return{keepMarks:!0,HTMLAttributes:{}}},inline:!0,group:"inline",selectable:!1,linebreakReplacement:!0,parseHTML(){return[{tag:"br"}]},renderHTML({HTMLAttributes:t}){return["br",R(this.options.HTMLAttributes,t)]},renderText(){return` +`},renderMarkdown:()=>` +`,parseMarkdown:()=>({type:"hardBreak"}),addCommands(){return{setHardBreak:()=>({commands:t,chain:e,state:n,editor:r})=>t.first([()=>t.exitCode(),()=>t.command(()=>{let{selection:o,storedMarks:i}=n;if(o.$from.parent.type.spec.isolating)return!1;let{keepMarks:s}=this.options,{splittableMarks:l}=r.extensionManager,a=i||o.$to.parentOffset&&o.$from.marks();return e().insertContent({type:this.name}).command(({tr:c,dispatch:d})=>{if(d&&a&&s){let u=a.filter(f=>l.includes(f.type.name));c.ensureMarks(u)}return!0}).run()})])}},addKeyboardShortcuts(){return{"Mod-Enter":()=>this.editor.commands.setHardBreak(),"Shift-Enter":()=>this.editor.commands.setHardBreak()}}}),ju=r0;var o0=F.create({name:"heading",addOptions(){return{levels:[1,2,3,4,5,6],HTMLAttributes:{}}},content:"inline*",group:"block",defining:!0,addAttributes(){return{level:{default:1,rendered:!1}}},parseHTML(){return this.options.levels.map(t=>({tag:`h${t}`,attrs:{level:t}}))},renderHTML({node:t,HTMLAttributes:e}){return[`h${this.options.levels.includes(t.attrs.level)?t.attrs.level:this.options.levels[0]}`,R(this.options.HTMLAttributes,e),0]},parseMarkdown:(t,e)=>e.createNode("heading",{level:t.depth||1},e.parseInline(t.tokens||[])),renderMarkdown:(t,e)=>{var n;let r=(n=t.attrs)!=null&&n.level?parseInt(t.attrs.level,10):1,o="#".repeat(r);return t.content?`${o} ${e.renderChildren(t.content)}`:""},addCommands(){return{setHeading:t=>({commands:e})=>this.options.levels.includes(t.level)?e.setNode(this.name,t):!1,toggleHeading:t=>({commands:e})=>this.options.levels.includes(t.level)?e.toggleNode(this.name,"paragraph",t):!1}},addKeyboardShortcuts(){return this.options.levels.reduce((t,e)=>({...t,[`Mod-Alt-${e}`]:()=>this.editor.commands.toggleHeading({level:e})}),{})},addInputRules(){return this.options.levels.map(t=>ar({find:new RegExp(`^(#{${Math.min(...this.options.levels)},${t}})\\s$`),type:this.type,getAttributes:{level:t}}))}}),Uu=o0;var i0=/(?:^|\s)(==(?!\s+==)((?:[^=]+))==(?!\s+==))$/,s0=/(?:^|\s)(==(?!\s+==)((?:[^=]+))==(?!\s+==))/g,l0=ee.create({name:"highlight",addOptions(){return{multicolor:!1,HTMLAttributes:{}}},addAttributes(){return this.options.multicolor?{color:{default:null,parseHTML:t=>t.getAttribute("data-color")||t.style.backgroundColor,renderHTML:t=>t.color?{"data-color":t.color,style:`background-color: ${t.color}; color: inherit`}:{}}}:{}},parseHTML(){return[{tag:"mark"}]},renderHTML({HTMLAttributes:t}){return["mark",R(this.options.HTMLAttributes,t),0]},renderMarkdown:(t,e)=>`==${e.renderChildren(t)}==`,parseMarkdown:(t,e)=>e.applyMark("highlight",e.parseInline(t.tokens||[])),markdownTokenizer:{name:"highlight",level:"inline",start:t=>t.indexOf("=="),tokenize(t,e,n){let o=/^(==)([^=]+)(==)/.exec(t);if(o){let i=o[2].trim(),s=n.inlineTokens(i);return{type:"highlight",raw:o[0],text:i,tokens:s}}}},addCommands(){return{setHighlight:t=>({commands:e})=>e.setMark(this.name,t),toggleHighlight:t=>({commands:e})=>e.toggleMark(this.name,t),unsetHighlight:()=>({commands:t})=>t.unsetMark(this.name)}},addKeyboardShortcuts(){return{"Mod-Shift-h":()=>this.editor.commands.toggleHighlight()}},addInputRules(){return[Be({find:i0,type:this.type})]},addPasteRules(){return[Me({find:s0,type:this.type})]}}),Ku=l0;var a0=F.create({name:"horizontalRule",addOptions(){return{HTMLAttributes:{},nextNodeType:"paragraph"}},group:"block",parseHTML(){return[{tag:"hr"}]},renderHTML({HTMLAttributes:t}){return["hr",R(this.options.HTMLAttributes,t)]},markdownTokenName:"hr",parseMarkdown:(t,e)=>e.createNode("horizontalRule"),renderMarkdown:()=>"---",addCommands(){return{setHorizontalRule:()=>({chain:t,state:e})=>{if(!bu(e,e.schema.nodes[this.name]))return!1;let{selection:n}=e,{$to:r}=n,o=t();return mo(n)?o.insertContentAt(r.pos,{type:this.name}):o.insertContent({type:this.name}),o.command(({state:i,tr:s,dispatch:l})=>{if(l){let{$to:a}=s.selection,c=a.end();if(a.nodeAfter)a.nodeAfter.isTextblock?s.setSelection(D.create(s.doc,a.pos+1)):a.nodeAfter.isBlock?s.setSelection(L.create(s.doc,a.pos)):s.setSelection(D.create(s.doc,a.pos));else{let d=i.schema.nodes[this.options.nextNodeType]||a.parent.type.contentMatch.defaultType,u=d?.create();u&&(s.insert(c,u),s.setSelection(D.create(s.doc,c+1)))}s.scrollIntoView()}return!0}).run()}}},addInputRules(){return[bo({find:/^(?:---|—-|___\s|\*\*\*\s)$/,type:this.type})]}}),qu=a0;var c0=/(?:^|\s)(\*(?!\s+\*)((?:[^*]+))\*(?!\s+\*))$/,d0=/(?:^|\s)(\*(?!\s+\*)((?:[^*]+))\*(?!\s+\*))/g,u0=/(?:^|\s)(_(?!\s+_)((?:[^_]+))_(?!\s+_))$/,f0=/(?:^|\s)(_(?!\s+_)((?:[^_]+))_(?!\s+_))/g,h0=ee.create({name:"italic",addOptions(){return{HTMLAttributes:{}}},parseHTML(){return[{tag:"em"},{tag:"i",getAttrs:t=>t.style.fontStyle!=="normal"&&null},{style:"font-style=normal",clearMark:t=>t.type.name===this.name},{style:"font-style=italic"}]},renderHTML({HTMLAttributes:t}){return["em",R(this.options.HTMLAttributes,t),0]},addCommands(){return{setItalic:()=>({commands:t})=>t.setMark(this.name),toggleItalic:()=>({commands:t})=>t.toggleMark(this.name),unsetItalic:()=>({commands:t})=>t.unsetMark(this.name)}},markdownTokenName:"em",parseMarkdown:(t,e)=>e.applyMark("italic",e.parseInline(t.tokens||[])),renderMarkdown:(t,e)=>`*${e.renderChildren(t)}*`,addKeyboardShortcuts(){return{"Mod-i":()=>this.editor.commands.toggleItalic(),"Mod-I":()=>this.editor.commands.toggleItalic()}},addInputRules(){return[Be({find:c0,type:this.type}),Be({find:u0,type:this.type})]},addPasteRules(){return[Me({find:d0,type:this.type}),Me({find:f0,type:this.type})]}}),Ju=h0;var p0=/(?:^|\s)(!\[(.+|:?)]\((\S+)(?:(?:\s+)["'](\S+)["'])?\))$/,m0=F.create({name:"image",addOptions(){return{inline:!1,allowBase64:!1,HTMLAttributes:{},resize:!1}},inline(){return this.options.inline},group(){return this.options.inline?"inline":"block"},draggable:!0,addAttributes(){return{src:{default:null},alt:{default:null},title:{default:null},width:{default:null},height:{default:null}}},parseHTML(){return[{tag:this.options.allowBase64?"img[src]":'img[src]:not([src^="data:"])'}]},renderHTML({HTMLAttributes:t}){return["img",R(this.options.HTMLAttributes,t)]},parseMarkdown:(t,e)=>e.createNode("image",{src:t.href,title:t.title,alt:t.text}),renderMarkdown:t=>{var e,n,r,o,i,s;let l=(n=(e=t.attrs)==null?void 0:e.src)!=null?n:"",a=(o=(r=t.attrs)==null?void 0:r.alt)!=null?o:"",c=(s=(i=t.attrs)==null?void 0:i.title)!=null?s:"";return c?`![${a}](${l} "${c}")`:`![${a}](${l})`},addNodeView(){if(!this.options.resize||!this.options.resize.enabled||typeof document>"u")return null;let{directions:t,minWidth:e,minHeight:n,alwaysPreserveAspectRatio:r}=this.options.resize;return({node:o,getPos:i,HTMLAttributes:s,editor:l})=>{let a=document.createElement("img");Object.entries(s).forEach(([u,f])=>{if(f!=null)switch(u){case"width":case"height":break;default:a.setAttribute(u,f);break}}),a.src=s.src;let c=new yu({element:a,editor:l,node:o,getPos:i,onResize:(u,f)=>{a.style.width=`${u}px`,a.style.height=`${f}px`},onCommit:(u,f)=>{let h=i();h!==void 0&&this.editor.chain().setNodeSelection(h).updateAttributes(this.name,{width:u,height:f}).run()},onUpdate:(u,f,h)=>u.type===o.type,options:{directions:t,min:{width:e,height:n},preserveAspectRatio:r===!0}}),d=c.dom;return d.style.visibility="hidden",d.style.pointerEvents="none",a.onload=()=>{d.style.visibility="",d.style.pointerEvents=""},c}},addCommands(){return{setImage:t=>({commands:e})=>e.insertContent({type:this.name,attrs:t})}},addInputRules(){return[bo({find:p0,type:this.type,getAttributes:t=>{let[,,e,n,r]=t;return{src:n,alt:e,title:r}}})]}}),Gu=m0;var Xu=Gu.extend({addAttributes(){return{...this.parent?.(),id:{default:null,parseHTML:t=>t.getAttribute("data-id"),renderHTML:t=>t.id?{"data-id":t.id}:{}},width:{default:null,parseHTML:t=>t.getAttribute("width")||t.style.width||null,renderHTML:t=>t.width?{width:t.width,style:`width: ${t.width}`}:{}},height:{default:null,parseHTML:t=>t.getAttribute("height")||t.style.height||null,renderHTML:t=>t.height?{height:t.height,style:`height: ${t.height}`}:{}}}}});var Yu=F.create({name:"lead",group:"block",content:"block+",addOptions(){return{HTMLAttributes:{class:"lead"}}},parseHTML(){return[{tag:"div",getAttrs:t=>t.classList.contains("lead")}]},renderHTML({HTMLAttributes:t}){return["div",R(this.options.HTMLAttributes,t),0]},addCommands(){return{toggleLead:()=>({commands:t})=>t.toggleWrap(this.name)}}});var g0="aaa1rp3bb0ott3vie4c1le2ogado5udhabi7c0ademy5centure6ountant0s9o1tor4d0s1ult4e0g1ro2tna4f0l1rica5g0akhan5ency5i0g1rbus3force5tel5kdn3l0ibaba4pay4lfinanz6state5y2sace3tom5m0azon4ericanexpress7family11x2fam3ica3sterdam8nalytics7droid5quan4z2o0l2partments8p0le4q0uarelle8r0ab1mco4chi3my2pa2t0e3s0da2ia2sociates9t0hleta5torney7u0ction5di0ble3o3spost5thor3o0s4w0s2x0a2z0ure5ba0by2idu3namex4d1k2r0celona5laycard4s5efoot5gains6seball5ketball8uhaus5yern5b0c1t1va3cg1n2d1e0ats2uty4er2rlin4st0buy5t2f1g1h0arti5i0ble3d1ke2ng0o3o1z2j1lack0friday9ockbuster8g1omberg7ue3m0s1w2n0pparibas9o0ats3ehringer8fa2m1nd2o0k0ing5sch2tik2on4t1utique6x2r0adesco6idgestone9oadway5ker3ther5ussels7s1t1uild0ers6siness6y1zz3v1w1y1z0h3ca0b1fe2l0l1vinklein9m0era3p2non3petown5ital0one8r0avan4ds2e0er0s4s2sa1e1h1ino4t0ering5holic7ba1n1re3c1d1enter4o1rn3f0a1d2g1h0anel2nel4rity4se2t2eap3intai5ristmas6ome4urch5i0priani6rcle4sco3tadel4i0c2y3k1l0aims4eaning6ick2nic1que6othing5ud3ub0med6m1n1o0ach3des3ffee4llege4ogne5m0mbank4unity6pany2re3uter5sec4ndos3struction8ulting7tact3ractors9oking4l1p2rsica5untry4pon0s4rses6pa2r0edit0card4union9icket5own3s1uise0s6u0isinella9v1w1x1y0mru3ou3z2dad1nce3ta1e1ing3sun4y2clk3ds2e0al0er2s3gree4livery5l1oitte5ta3mocrat6ntal2ist5si0gn4v2hl2iamonds6et2gital5rect0ory7scount3ver5h2y2j1k1m1np2o0cs1tor4g1mains5t1wnload7rive4tv2ubai3nlop4pont4rban5vag2r2z2earth3t2c0o2deka3u0cation8e1g1mail3erck5nergy4gineer0ing9terprises10pson4quipment8r0icsson6ni3s0q1tate5t1u0rovision8s2vents5xchange6pert3osed4ress5traspace10fage2il1rwinds6th3mily4n0s2rm0ers5shion4t3edex3edback6rrari3ero6i0delity5o2lm2nal1nce1ial7re0stone6mdale6sh0ing5t0ness6j1k1lickr3ghts4r2orist4wers5y2m1o0o0d1tball6rd1ex2sale4um3undation8x2r0ee1senius7l1ogans4ntier7tr2ujitsu5n0d2rniture7tbol5yi3ga0l0lery3o1up4me0s3p1rden4y2b0iz3d0n2e0a1nt0ing5orge5f1g0ee3h1i0ft0s3ves2ing5l0ass3e1obal2o4m0ail3bh2o1x2n1odaddy5ld0point6f2o0dyear5g0le4p1t1v2p1q1r0ainger5phics5tis4een3ipe3ocery4up4s1t1u0cci3ge2ide2tars5ru3w1y2hair2mburg5ngout5us3bo2dfc0bank7ealth0care8lp1sinki6re1mes5iphop4samitsu7tachi5v2k0t2m1n1ockey4ldings5iday5medepot5goods5s0ense7nda3rse3spital5t0ing5t0els3mail5use3w2r1sbc3t1u0ghes5yatt3undai7ibm2cbc2e1u2d1e0ee3fm2kano4l1m0amat4db2mo0bilien9n0c1dustries8finiti5o2g1k1stitute6urance4e4t0ernational10uit4vestments10o1piranga7q1r0ish4s0maili5t0anbul7t0au2v3jaguar4va3cb2e0ep2tzt3welry6io2ll2m0p2nj2o0bs1urg4t1y2p0morgan6rs3uegos4niper7kaufen5ddi3e0rryhotels6properties14fh2g1h1i0a1ds2m1ndle4tchen5wi3m1n1oeln3matsu5sher5p0mg2n2r0d1ed3uokgroup8w1y0oto4z2la0caixa5mborghini8er3nd0rover6xess5salle5t0ino3robe5w0yer5b1c1ds2ease3clerc5frak4gal2o2xus4gbt3i0dl2fe0insurance9style7ghting6ke2lly3mited4o2ncoln4k2ve1ing5k1lc1p2oan0s3cker3us3l1ndon4tte1o3ve3pl0financial11r1s1t0d0a3u0ndbeck6xe1ury5v1y2ma0drid4if1son4keup4n0agement7go3p1rket0ing3s4riott5shalls7ttel5ba2c0kinsey7d1e0d0ia3et2lbourne7me1orial6n0u2rckmsd7g1h1iami3crosoft7l1ni1t2t0subishi9k1l0b1s2m0a2n1o0bi0le4da2e1i1m1nash3ey2ster5rmon3tgage6scow4to0rcycles9v0ie4p1q1r1s0d2t0n1r2u0seum3ic4v1w1x1y1z2na0b1goya4me2vy3ba2c1e0c1t0bank4flix4work5ustar5w0s2xt0direct7us4f0l2g0o2hk2i0co2ke1on3nja3ssan1y5l1o0kia3rton4w0ruz3tv4p1r0a1w2tt2u1yc2z2obi1server7ffice5kinawa6layan0group9lo3m0ega4ne1g1l0ine5oo2pen3racle3nge4g0anic5igins6saka4tsuka4t2vh3pa0ge2nasonic7ris2s1tners4s1y3y2ccw3e0t2f0izer5g1h0armacy6d1ilips5one2to0graphy6s4ysio5ics1tet2ures6d1n0g1k2oneer5zza4k1l0ace2y0station9umbing5s3m1n0c2ohl2ker3litie5rn2st3r0axi3ess3ime3o0d0uctions8f1gressive8mo2perties3y5tection8u0dential9s1t1ub2w0c2y2qa1pon3uebec3st5racing4dio4e0ad1lestate6tor2y4cipes5d0stone5umbrella9hab3ise0n3t2liance6n0t0als5pair3ort3ublican8st0aurant8view0s5xroth6ich0ardli6oh3l1o1p2o0cks3deo3gers4om3s0vp3u0gby3hr2n2w0e2yukyu6sa0arland6fe0ty4kura4le1on3msclub4ung5ndvik0coromant12ofi4p1rl2s1ve2xo3b0i1s2c0b1haeffler7midt4olarships8ol3ule3warz5ience5ot3d1e0arch3t2cure1ity6ek2lect4ner3rvices6ven3w1x0y3fr2g1h0angrila6rp3ell3ia1ksha5oes2p0ping5uji3w3i0lk2na1gles5te3j1k0i0n2y0pe4l0ing4m0art3ile4n0cf3o0ccer3ial4ftbank4ware6hu2lar2utions7ng1y2y2pa0ce3ort2t3r0l2s1t0ada2ples4r1tebank4farm7c0group6ockholm6rage3e3ream4udio2y3yle4u0cks3pplies3y2ort5rf1gery5zuki5v1watch4iss4x1y0dney4stems6z2tab1ipei4lk2obao4rget4tamotors6r2too4x0i3c0i2d0k2eam2ch0nology8l1masek5nnis4va3f1g1h0d1eater2re6iaa2ckets5enda4ps2res2ol4j0maxx4x2k0maxx5l1m0all4n1o0day3kyo3ols3p1ray3shiba5tal3urs3wn2yota3s3r0ade1ing4ining5vel0ers0insurance16ust3v2t1ube2i1nes3shu4v0s2w1z2ua1bank3s2g1k1nicom3versity8o2ol2ps2s1y1z2va0cations7na1guard7c1e0gas3ntures6risign5m\xF6gensberater2ung14sicherung10t2g1i0ajes4deo3g1king4llas4n1p1rgin4sa1ion4va1o3laanderen9n1odka3lvo3te1ing3o2yage5u2wales2mart4ter4ng0gou5tch0es6eather0channel12bcam3er2site5d0ding5ibo2r3f1hoswho6ien2ki2lliamhill9n0dows4e1ners6me2olterskluwer11odside6rk0s2ld3w2s1tc1f3xbox3erox4ihuan4n2xx2yz3yachts4hoo3maxun5ndex5e1odobashi7ga2kohama6u0tube6t1un3za0ppos4ra3ero3ip2m1one3uerich6w2",y0="\u03B5\u03BB1\u03C52\u0431\u04331\u0435\u043B3\u0434\u0435\u0442\u04384\u0435\u044E2\u043A\u0430\u0442\u043E\u043B\u0438\u043A6\u043E\u043C3\u043C\u043A\u04342\u043E\u043D1\u0441\u043A\u0432\u04306\u043E\u043D\u043B\u0430\u0439\u043D5\u0440\u04333\u0440\u0443\u04412\u04442\u0441\u0430\u0439\u04423\u0440\u04313\u0443\u043A\u04403\u049B\u0430\u04373\u0570\u0561\u05753\u05D9\u05E9\u05E8\u05D0\u05DC5\u05E7\u05D5\u05DD3\u0627\u0628\u0648\u0638\u0628\u064A5\u0631\u0627\u0645\u0643\u06485\u0644\u0627\u0631\u062F\u06464\u0628\u062D\u0631\u064A\u06465\u062C\u0632\u0627\u0626\u06315\u0633\u0639\u0648\u062F\u064A\u06296\u0639\u0644\u064A\u0627\u06465\u0645\u063A\u0631\u06285\u0645\u0627\u0631\u0627\u062A5\u06CC\u0631\u0627\u06465\u0628\u0627\u0631\u062A2\u0632\u0627\u06314\u064A\u062A\u06433\u06BE\u0627\u0631\u062A5\u062A\u0648\u0646\u06334\u0633\u0648\u062F\u0627\u06463\u0631\u064A\u06295\u0634\u0628\u0643\u06294\u0639\u0631\u0627\u06422\u06282\u0645\u0627\u06464\u0641\u0644\u0633\u0637\u064A\u06466\u0642\u0637\u06313\u0643\u0627\u062B\u0648\u0644\u064A\u06436\u0648\u06453\u0645\u0635\u06312\u0644\u064A\u0633\u064A\u06275\u0648\u0631\u064A\u062A\u0627\u0646\u064A\u06277\u0642\u06394\u0647\u0645\u0631\u0627\u06475\u067E\u0627\u06A9\u0633\u062A\u0627\u06467\u0680\u0627\u0631\u062A4\u0915\u0949\u092E3\u0928\u0947\u091F3\u092D\u093E\u0930\u09240\u092E\u094D3\u094B\u09245\u0938\u0902\u0917\u0920\u09285\u09AC\u09BE\u0982\u09B2\u09BE5\u09AD\u09BE\u09B0\u09A42\u09F0\u09A44\u0A2D\u0A3E\u0A30\u0A244\u0AAD\u0ABE\u0AB0\u0AA44\u0B2D\u0B3E\u0B30\u0B244\u0B87\u0BA8\u0BCD\u0BA4\u0BBF\u0BAF\u0BBE6\u0BB2\u0B99\u0BCD\u0B95\u0BC86\u0B9A\u0BBF\u0B99\u0BCD\u0B95\u0BAA\u0BCD\u0BAA\u0BC2\u0BB0\u0BCD11\u0C2D\u0C3E\u0C30\u0C24\u0C4D5\u0CAD\u0CBE\u0CB0\u0CA44\u0D2D\u0D3E\u0D30\u0D24\u0D025\u0DBD\u0D82\u0D9A\u0DCF4\u0E04\u0E2D\u0E213\u0E44\u0E17\u0E223\u0EA5\u0EB2\u0EA73\u10D2\u10D42\u307F\u3093\u306A3\u30A2\u30DE\u30BE\u30F34\u30AF\u30E9\u30A6\u30C94\u30B0\u30FC\u30B0\u30EB4\u30B3\u30E02\u30B9\u30C8\u30A23\u30BB\u30FC\u30EB3\u30D5\u30A1\u30C3\u30B7\u30E7\u30F36\u30DD\u30A4\u30F3\u30C84\u4E16\u754C2\u4E2D\u4FE11\u56FD1\u570B1\u6587\u7F513\u4E9A\u9A6C\u900A3\u4F01\u4E1A2\u4F5B\u5C712\u4FE1\u606F2\u5065\u5EB72\u516B\u53662\u516C\u53F81\u76CA2\u53F0\u6E7E1\u70632\u5546\u57CE1\u5E971\u68072\u5609\u91CC0\u5927\u9152\u5E975\u5728\u7EBF2\u5927\u62FF2\u5929\u4E3B\u65593\u5A31\u4E502\u5BB6\u96FB2\u5E7F\u4E1C2\u5FAE\u535A2\u6148\u55842\u6211\u7231\u4F603\u624B\u673A2\u62DB\u80582\u653F\u52A11\u5E9C2\u65B0\u52A0\u57612\u95FB2\u65F6\u5C1A2\u66F8\u7C4D2\u673A\u67842\u6DE1\u9A6C\u95213\u6E38\u620F2\u6FB3\u95802\u70B9\u770B2\u79FB\u52A82\u7EC4\u7EC7\u673A\u67844\u7F51\u57401\u5E971\u7AD91\u7EDC2\u8054\u901A2\u8C37\u6B4C2\u8D2D\u72692\u901A\u8CA92\u96C6\u56E22\u96FB\u8A0A\u76C8\u79D14\u98DE\u5229\u6D663\u98DF\u54C12\u9910\u53852\u9999\u683C\u91CC\u62C93\u6E2F2\uB2F7\uB1371\uCEF42\uC0BC\uC1312\uD55C\uAD6D2",xl="numeric",kl="ascii",Sl="alpha",fr="asciinumeric",ur="alphanumeric",Cl="domain",of="emoji",b0="scheme",w0="slashscheme",pl="whitespace";function x0(t,e){return t in e||(e[t]=[]),e[t]}function nn(t,e,n){e[xl]&&(e[fr]=!0,e[ur]=!0),e[kl]&&(e[fr]=!0,e[Sl]=!0),e[fr]&&(e[ur]=!0),e[Sl]&&(e[ur]=!0),e[ur]&&(e[Cl]=!0),e[of]&&(e[Cl]=!0);for(let r in e){let o=x0(r,n);o.indexOf(t)<0&&o.push(t)}}function k0(t,e){let n={};for(let r in e)e[r].indexOf(t)>=0&&(n[r]=!0);return n}function Te(t=null){this.j={},this.jr=[],this.jd=null,this.t=t}Te.groups={};Te.prototype={accepts(){return!!this.t},go(t){let e=this,n=e.j[t];if(n)return n;for(let r=0;rt.ta(e,n,r,o),re=(t,e,n,r,o)=>t.tr(e,n,r,o),Qu=(t,e,n,r,o)=>t.ts(e,n,r,o),M=(t,e,n,r,o)=>t.tt(e,n,r,o),St="WORD",vl="UWORD",sf="ASCIINUMERICAL",lf="ALPHANUMERICAL",br="LOCALHOST",Ml="TLD",Tl="UTLD",Eo="SCHEME",On="SLASH_SCHEME",El="NUM",Al="WS",Nl="NL",hr="OPENBRACE",pr="CLOSEBRACE",No="OPENBRACKET",Oo="CLOSEBRACKET",Ro="OPENPAREN",Do="CLOSEPAREN",Io="OPENANGLEBRACKET",Po="CLOSEANGLEBRACKET",Lo="FULLWIDTHLEFTPAREN",Bo="FULLWIDTHRIGHTPAREN",zo="LEFTCORNERBRACKET",Ho="RIGHTCORNERBRACKET",$o="LEFTWHITECORNERBRACKET",Fo="RIGHTWHITECORNERBRACKET",Vo="FULLWIDTHLESSTHAN",_o="FULLWIDTHGREATERTHAN",Wo="AMPERSAND",jo="APOSTROPHE",Uo="ASTERISK",Lt="AT",Ko="BACKSLASH",qo="BACKTICK",Jo="CARET",Bt="COLON",Ol="COMMA",Go="DOLLAR",ot="DOT",Xo="EQUALS",Rl="EXCLAMATION",He="HYPHEN",mr="PERCENT",Yo="PIPE",Qo="PLUS",Zo="POUND",gr="QUERY",Dl="QUOTE",af="FULLWIDTHMIDDLEDOT",Il="SEMI",it="SLASH",yr="TILDE",ei="UNDERSCORE",cf="EMOJI",ti="SYM",df=Object.freeze({__proto__:null,ALPHANUMERICAL:lf,AMPERSAND:Wo,APOSTROPHE:jo,ASCIINUMERICAL:sf,ASTERISK:Uo,AT:Lt,BACKSLASH:Ko,BACKTICK:qo,CARET:Jo,CLOSEANGLEBRACKET:Po,CLOSEBRACE:pr,CLOSEBRACKET:Oo,CLOSEPAREN:Do,COLON:Bt,COMMA:Ol,DOLLAR:Go,DOT:ot,EMOJI:cf,EQUALS:Xo,EXCLAMATION:Rl,FULLWIDTHGREATERTHAN:_o,FULLWIDTHLEFTPAREN:Lo,FULLWIDTHLESSTHAN:Vo,FULLWIDTHMIDDLEDOT:af,FULLWIDTHRIGHTPAREN:Bo,HYPHEN:He,LEFTCORNERBRACKET:zo,LEFTWHITECORNERBRACKET:$o,LOCALHOST:br,NL:Nl,NUM:El,OPENANGLEBRACKET:Io,OPENBRACE:hr,OPENBRACKET:No,OPENPAREN:Ro,PERCENT:mr,PIPE:Yo,PLUS:Qo,POUND:Zo,QUERY:gr,QUOTE:Dl,RIGHTCORNERBRACKET:Ho,RIGHTWHITECORNERBRACKET:Fo,SCHEME:Eo,SEMI:Il,SLASH:it,SLASH_SCHEME:On,SYM:ti,TILDE:yr,TLD:Ml,UNDERSCORE:ei,UTLD:Tl,UWORD:vl,WORD:St,WS:Al}),xt=/[a-z]/,dr=/\p{L}/u,ml=/\p{Emoji}/u;var kt=/\d/,gl=/\s/;var Zu="\r",yl=` +`,S0="\uFE0F",C0="\u200D",bl="\uFFFC",Mo=null,To=null;function v0(t=[]){let e={};Te.groups=e;let n=new Te;Mo==null&&(Mo=ef(g0)),To==null&&(To=ef(y0)),M(n,"'",jo),M(n,"{",hr),M(n,"}",pr),M(n,"[",No),M(n,"]",Oo),M(n,"(",Ro),M(n,")",Do),M(n,"<",Io),M(n,">",Po),M(n,"\uFF08",Lo),M(n,"\uFF09",Bo),M(n,"\u300C",zo),M(n,"\u300D",Ho),M(n,"\u300E",$o),M(n,"\u300F",Fo),M(n,"\uFF1C",Vo),M(n,"\uFF1E",_o),M(n,"&",Wo),M(n,"*",Uo),M(n,"@",Lt),M(n,"`",qo),M(n,"^",Jo),M(n,":",Bt),M(n,",",Ol),M(n,"$",Go),M(n,".",ot),M(n,"=",Xo),M(n,"!",Rl),M(n,"-",He),M(n,"%",mr),M(n,"|",Yo),M(n,"+",Qo),M(n,"#",Zo),M(n,"?",gr),M(n,'"',Dl),M(n,"/",it),M(n,";",Il),M(n,"~",yr),M(n,"_",ei),M(n,"\\",Ko),M(n,"\u30FB",af);let r=re(n,kt,El,{[xl]:!0});re(r,kt,r);let o=re(r,xt,sf,{[fr]:!0}),i=re(r,dr,lf,{[ur]:!0}),s=re(n,xt,St,{[kl]:!0});re(s,kt,o),re(s,xt,s),re(o,kt,o),re(o,xt,o);let l=re(n,dr,vl,{[Sl]:!0});re(l,xt),re(l,kt,i),re(l,dr,l),re(i,kt,i),re(i,xt),re(i,dr,i);let a=M(n,yl,Nl,{[pl]:!0}),c=M(n,Zu,Al,{[pl]:!0}),d=re(n,gl,Al,{[pl]:!0});M(n,bl,d),M(c,yl,a),M(c,bl,d),re(c,gl,d),M(d,Zu),M(d,yl),re(d,gl,d),M(d,bl,d);let u=re(n,ml,cf,{[of]:!0});M(u,"#"),re(u,ml,u),M(u,S0,u);let f=M(u,C0);M(f,"#"),re(f,ml,u);let h=[[xt,s],[kt,o]],p=[[xt,null],[dr,l],[kt,i]];for(let m=0;mm[0]>g[0]?1:-1);for(let m=0;m=0?w[Cl]=!0:xt.test(g)?kt.test(g)?w[fr]=!0:w[kl]=!0:w[xl]=!0,Qu(n,g,g,w)}return Qu(n,"localhost",br,{ascii:!0}),n.jd=new Te(ti),{start:n,tokens:Object.assign({groups:e},df)}}function uf(t,e){let n=M0(e.replace(/[A-Z]/g,l=>l.toLowerCase())),r=n.length,o=[],i=0,s=0;for(;s=0&&(u+=n[s].length,f++),c+=n[s].length,i+=n[s].length,s++;i-=u,s-=f,c-=u,o.push({t:d.t,v:e.slice(i-c,i),s:i-c,e:i})}return o}function M0(t){let e=[],n=t.length,r=0;for(;r56319||r+1===n||(i=t.charCodeAt(r+1))<56320||i>57343?t[r]:t.slice(r,r+2);e.push(s),r+=s.length}return e}function Pt(t,e,n,r,o){let i,s=e.length;for(let l=0;l=0;)i++;if(i>0){e.push(n.join(""));for(let s=parseInt(t.substring(r,r+i),10);s>0;s--)n.pop();r+=i}else n.push(t[r]),r++}return e}var wr={defaultProtocol:"http",events:null,format:tf,formatHref:tf,nl2br:!1,tagName:"a",target:null,rel:null,validate:!0,truncate:1/0,className:null,attributes:null,ignoreTags:[],render:null};function Pl(t,e=null){let n=Object.assign({},wr);t&&(n=Object.assign(n,t instanceof Pl?t.o:t));let r=n.ignoreTags,o=[];for(let i=0;in?r.substring(0,n)+"\u2026":r},toFormattedHref(t){return t.get("formatHref",this.toHref(t.get("defaultProtocol")),this)},startIndex(){return this.tk[0].s},endIndex(){return this.tk[this.tk.length-1].e},toObject(t=wr.defaultProtocol){return{type:this.t,value:this.toString(),isLink:this.isLink,href:this.toHref(t),start:this.startIndex(),end:this.endIndex()}},toFormattedObject(t){return{type:this.t,value:this.toFormattedString(t),isLink:this.isLink,href:this.toFormattedHref(t),start:this.startIndex(),end:this.endIndex()}},validate(t){return t.get("validate",this.toString(),this)},render(t){let e=this,n=this.toHref(t.get("defaultProtocol")),r=t.get("formatHref",n,this),o=t.get("tagName",n,e),i=this.toFormattedString(t),s={},l=t.get("className",n,e),a=t.get("target",n,e),c=t.get("rel",n,e),d=t.getObj("attributes",n,e),u=t.getObj("events",n,e);return s.href=r,l&&(s.class=l),a&&(s.target=a),c&&(s.rel=c),d&&Object.assign(s,d),{tagName:o,attributes:s,content:i,eventListeners:u}}};function ni(t,e){class n extends ff{constructor(o,i){super(o,i),this.t=t}}for(let r in e)n.prototype[r]=e[r];return n.t=t,n}var nf=ni("email",{isLink:!0,toHref(){return"mailto:"+this.toString()}}),rf=ni("text"),T0=ni("nl"),Ao=ni("url",{isLink:!0,toHref(t=wr.defaultProtocol){return this.hasProtocol()?this.v:`${t}://${this.v}`},hasProtocol(){let t=this.tk;return t.length>=2&&t[0].t!==br&&t[1].t===Bt}});var ze=t=>new Te(t);function A0({groups:t}){let e=t.domain.concat([Wo,Uo,Lt,Ko,qo,Jo,Go,Xo,He,El,mr,Yo,Qo,Zo,it,ti,yr,ei]),n=[jo,Bt,Ol,ot,Rl,mr,gr,Dl,Il,Io,Po,hr,pr,Oo,No,Ro,Do,Lo,Bo,zo,Ho,$o,Fo,Vo,_o],r=[Wo,jo,Uo,Ko,qo,Jo,Go,Xo,He,hr,pr,mr,Yo,Qo,Zo,gr,it,ti,yr,ei],o=ze(),i=M(o,yr);j(i,r,i),j(i,t.domain,i);let s=ze(),l=ze(),a=ze();j(o,t.domain,s),j(o,t.scheme,l),j(o,t.slashscheme,a),j(s,r,i),j(s,t.domain,s);let c=M(s,Lt);M(i,Lt,c),M(l,Lt,c),M(a,Lt,c);let d=M(i,ot);j(d,r,i),j(d,t.domain,i);let u=ze();j(c,t.domain,u),j(u,t.domain,u);let f=M(u,ot);j(f,t.domain,u);let h=ze(nf);j(f,t.tld,h),j(f,t.utld,h),M(c,br,h);let p=M(u,He);M(p,He,p),j(p,t.domain,u),j(h,t.domain,u),M(h,ot,f),M(h,He,p);let m=M(h,Bt);j(m,t.numeric,nf);let g=M(s,He),y=M(s,ot);M(g,He,g),j(g,t.domain,s),j(y,r,i),j(y,t.domain,s);let w=ze(Ao);j(y,t.tld,w),j(y,t.utld,w),j(w,t.domain,s),j(w,r,i),M(w,ot,y),M(w,He,g),M(w,Lt,c);let b=M(w,Bt),C=ze(Ao);j(b,t.numeric,C);let x=ze(Ao),S=ze();j(x,e,x),j(x,n,S),j(S,e,x),j(S,n,S),M(w,it,x),M(C,it,x);let k=M(l,Bt),O=M(a,Bt),T=M(O,it),A=M(T,it);j(l,t.domain,s),M(l,ot,y),M(l,He,g),j(a,t.domain,s),M(a,ot,y),M(a,He,g),j(k,t.domain,x),M(k,it,x),M(k,gr,x),j(A,t.domain,x),j(A,e,x),M(A,it,x);let $=[[hr,pr],[No,Oo],[Ro,Do],[Io,Po],[Lo,Bo],[zo,Ho],[$o,Fo],[Vo,_o]];for(let z=0;z<$.length;z++){let[K,V]=$[z],N=M(x,K);M(S,K,N),M(N,V,x);let _=ze(Ao);j(N,e,_);let W=ze();j(N,n),j(_,e,_),j(_,n,W),j(W,e,_),j(W,n,W),M(_,V,x),M(W,V,x)}return M(o,br,w),M(o,Nl,T0),{start:o,tokens:df}}function E0(t,e,n){let r=n.length,o=0,i=[],s=[];for(;o=0&&f++,o++,d++;if(f<0)o-=d,o0&&(i.push(wl(rf,e,s)),s=[]),o-=f,d-=f;let h=u.t,p=n.slice(o-d,o);i.push(wl(h,e,p))}}return s.length>0&&i.push(wl(rf,e,s)),i}function wl(t,e,n){let r=n[0].s,o=n[n.length-1].e,i=e.slice(r,o);return new t(i,n)}var N0=typeof console<"u"&&console&&console.warn||(()=>{}),O0="until manual call of linkify.init(). Register all schemes and plugins before invoking linkify the first time.",Z={scanner:null,parser:null,tokenQueue:[],pluginQueue:[],customSchemes:[],initialized:!1};function hf(){return Te.groups={},Z.scanner=null,Z.parser=null,Z.tokenQueue=[],Z.pluginQueue=[],Z.customSchemes=[],Z.initialized=!1,Z}function Ll(t,e=!1){if(Z.initialized&&N0(`linkifyjs: already initialized - will not register custom scheme "${t}" ${O0}`),!/^[0-9a-z]+(-[0-9a-z]+)*$/.test(t))throw new Error(`linkifyjs: incorrect scheme format. +1. Must only contain digits, lowercase ASCII letters or "-" +2. Cannot start or end with "-" +3. "-" cannot repeat`);Z.customSchemes.push([t,e])}function R0(){Z.scanner=v0(Z.customSchemes);for(let t=0;t{let o=e.some(c=>c.docChanged)&&!n.doc.eq(r.doc),i=e.some(c=>c.getMeta("preventAutolink"));if(!o||i)return;let{tr:s}=r,l=Js(n.doc,[...e]);if(el(l).forEach(({newRange:c})=>{let d=jd(r.doc,c,h=>h.isTextblock),u,f;if(d.length>1)u=d[0],f=r.doc.textBetween(u.pos,u.pos+u.node.nodeSize,void 0," ");else if(d.length){let h=r.doc.textBetween(c.from,c.to," "," ");if(!I0.test(h))return;u=d[0],f=r.doc.textBetween(u.pos,c.to,void 0," ")}if(u&&f){let h=f.split(D0).filter(Boolean);if(h.length<=0)return!1;let p=h[h.length-1],m=u.pos+f.lastIndexOf(p);if(!p)return!1;let g=ri(p).map(y=>y.toObject(t.defaultProtocol));if(!L0(g))return!1;g.filter(y=>y.isLink).map(y=>({...y,from:m+y.start+1,to:m+y.end+1})).filter(y=>r.schema.marks.code?!r.doc.rangeHasMark(y.from,y.to,r.schema.marks.code):!0).filter(y=>t.validate(y.value)).filter(y=>t.shouldAutoLink(y.value)).forEach(y=>{po(y.from,y.to,r.doc).some(w=>w.mark.type===t.type)||s.addMark(y.from,y.to,t.type.create({href:y.href}))})}}),!!s.steps.length)return s}})}function z0(t){return new P({key:new H("handleClickLink"),props:{handleClick:(e,n,r)=>{var o,i;if(r.button!==0||!e.editable)return!1;let s=!1;if(t.enableClickSelection&&(s=t.editor.commands.extendMarkRange(t.type.name)),t.openOnClick){let l=null;if(r.target instanceof HTMLAnchorElement)l=r.target;else{let u=r.target,f=[];for(;u.nodeName!=="DIV";)f.push(u),u=u.parentNode;l=f.find(h=>h.nodeName==="A")}if(!l)return s;let a=Zs(e.state,t.type.name),c=(o=l?.href)!=null?o:a.href,d=(i=l?.target)!=null?i:a.target;l&&c&&(window.open(c,d),s=!0)}return s}}})}function H0(t){return new P({key:new H("handlePasteLink"),props:{handlePaste:(e,n,r)=>{let{shouldAutoLink:o}=t,{state:i}=e,{selection:s}=i,{empty:l}=s;if(l)return!1;let a="";r.content.forEach(d=>{a+=d.textContent});let c=oi(a,{defaultProtocol:t.defaultProtocol}).find(d=>d.isLink&&d.value===a);return!a||!c||o!==void 0&&!o(c.href)?!1:t.editor.commands.setMark(t.type,{href:c.href})}}})}function rn(t,e){let n=["http","https","ftp","ftps","mailto","tel","callto","sms","cid","xmpp"];return e&&e.forEach(r=>{let o=typeof r=="string"?r:r.scheme;o&&n.push(o)}),!t||t.replace(P0,"").match(new RegExp(`^(?:(?:${n.join("|")}):|[^a-z]|[a-z0-9+.-]+(?:[^a-z+.-:]|$))`,"i"))}var $0=ee.create({name:"link",priority:1e3,keepOnSplit:!1,exitable:!0,onCreate(){this.options.validate&&!this.options.shouldAutoLink&&(this.options.shouldAutoLink=this.options.validate,console.warn("The `validate` option is deprecated. Rename to the `shouldAutoLink` option instead.")),this.options.protocols.forEach(t=>{if(typeof t=="string"){Ll(t);return}Ll(t.scheme,t.optionalSlashes)})},onDestroy(){hf()},inclusive(){return this.options.autolink},addOptions(){return{openOnClick:!0,enableClickSelection:!1,linkOnPaste:!0,autolink:!0,protocols:[],defaultProtocol:"http",HTMLAttributes:{target:"_blank",rel:"noopener noreferrer nofollow",class:null},isAllowedUri:(t,e)=>!!rn(t,e.protocols),validate:t=>!!t,shouldAutoLink:t=>!!t}},addAttributes(){return{href:{default:null,parseHTML(t){return t.getAttribute("href")}},target:{default:this.options.HTMLAttributes.target},rel:{default:this.options.HTMLAttributes.rel},class:{default:this.options.HTMLAttributes.class}}},parseHTML(){return[{tag:"a[href]",getAttrs:t=>{let e=t.getAttribute("href");return!e||!this.options.isAllowedUri(e,{defaultValidate:n=>!!rn(n,this.options.protocols),protocols:this.options.protocols,defaultProtocol:this.options.defaultProtocol})?!1:null}}]},renderHTML({HTMLAttributes:t}){return this.options.isAllowedUri(t.href,{defaultValidate:e=>!!rn(e,this.options.protocols),protocols:this.options.protocols,defaultProtocol:this.options.defaultProtocol})?["a",R(this.options.HTMLAttributes,t),0]:["a",R(this.options.HTMLAttributes,{...t,href:""}),0]},markdownTokenName:"link",parseMarkdown:(t,e)=>e.applyMark("link",e.parseInline(t.tokens||[]),{href:t.href,title:t.title||null}),renderMarkdown:(t,e)=>{var n;let r=((n=t.attrs)==null?void 0:n.href)||"";return`[${e.renderChildren(t)}](${r})`},addCommands(){return{setLink:t=>({chain:e})=>{let{href:n}=t;return this.options.isAllowedUri(n,{defaultValidate:r=>!!rn(r,this.options.protocols),protocols:this.options.protocols,defaultProtocol:this.options.defaultProtocol})?e().setMark(this.name,t).setMeta("preventAutolink",!0).run():!1},toggleLink:t=>({chain:e})=>{let{href:n}=t||{};return n&&!this.options.isAllowedUri(n,{defaultValidate:r=>!!rn(r,this.options.protocols),protocols:this.options.protocols,defaultProtocol:this.options.defaultProtocol})?!1:e().toggleMark(this.name,t,{extendEmptyMarkRange:!0}).setMeta("preventAutolink",!0).run()},unsetLink:()=>({chain:t})=>t().unsetMark(this.name,{extendEmptyMarkRange:!0}).setMeta("preventAutolink",!0).run()}},addPasteRules(){return[Me({find:t=>{let e=[];if(t){let{protocols:n,defaultProtocol:r}=this.options,o=oi(t).filter(i=>i.isLink&&this.options.isAllowedUri(i.value,{defaultValidate:s=>!!rn(s,n),protocols:n,defaultProtocol:r}));o.length&&o.forEach(i=>{this.options.shouldAutoLink(i.value)&&e.push({text:i.value,data:{href:i.href},index:i.start})})}return e},type:this.type,getAttributes:t=>{var e;return{href:(e=t.data)==null?void 0:e.href}}})]},addProseMirrorPlugins(){let t=[],{protocols:e,defaultProtocol:n}=this.options;return this.options.autolink&&t.push(B0({type:this.type,defaultProtocol:this.options.defaultProtocol,validate:r=>this.options.isAllowedUri(r,{defaultValidate:o=>!!rn(o,e),protocols:e,defaultProtocol:n}),shouldAutoLink:this.options.shouldAutoLink})),t.push(z0({type:this.type,editor:this.editor,openOnClick:this.options.openOnClick==="whenNotEditable"?!0:this.options.openOnClick,enableClickSelection:this.options.enableClickSelection})),this.options.linkOnPaste&&t.push(H0({editor:this.editor,defaultProtocol:this.options.defaultProtocol,type:this.type,shouldAutoLink:this.options.shouldAutoLink})),t}}),pf=$0;var F0=Object.defineProperty,V0=(t,e)=>{for(var n in e)F0(t,n,{get:e[n],enumerable:!0})},_0="listItem",mf="textStyle",gf=/^\s*([-+*])\s$/,$l=F.create({name:"bulletList",addOptions(){return{itemTypeName:"listItem",HTMLAttributes:{},keepMarks:!1,keepAttributes:!1}},group:"block list",content(){return`${this.options.itemTypeName}+`},parseHTML(){return[{tag:"ul"}]},renderHTML({HTMLAttributes:t}){return["ul",R(this.options.HTMLAttributes,t),0]},markdownTokenName:"list",parseMarkdown:(t,e)=>t.type!=="list"||t.ordered?[]:{type:"bulletList",content:t.items?e.parseChildren(t.items):[]},renderMarkdown:(t,e)=>t.content?e.renderChildren(t.content,` +`):"",markdownOptions:{indentsContent:!0},addCommands(){return{toggleBulletList:()=>({commands:t,chain:e})=>this.options.keepAttributes?e().toggleList(this.name,this.options.itemTypeName,this.options.keepMarks).updateAttributes(_0,this.editor.getAttributes(mf)).run():t.toggleList(this.name,this.options.itemTypeName,this.options.keepMarks)}},addKeyboardShortcuts(){return{"Mod-Shift-8":()=>this.editor.commands.toggleBulletList()}},addInputRules(){let t=tt({find:gf,type:this.type});return(this.options.keepMarks||this.options.keepAttributes)&&(t=tt({find:gf,type:this.type,keepMarks:this.options.keepMarks,keepAttributes:this.options.keepAttributes,getAttributes:()=>this.editor.getAttributes(mf),editor:this.editor})),[t]}}),Fl=F.create({name:"listItem",addOptions(){return{HTMLAttributes:{},bulletListTypeName:"bulletList",orderedListTypeName:"orderedList"}},content:"paragraph block*",defining:!0,parseHTML(){return[{tag:"li"}]},renderHTML({HTMLAttributes:t}){return["li",R(this.options.HTMLAttributes,t),0]},markdownTokenName:"list_item",parseMarkdown:(t,e)=>{if(t.type!=="list_item")return[];let n=[];if(t.tokens&&t.tokens.length>0)if(t.tokens.some(o=>o.type==="paragraph"))n=e.parseChildren(t.tokens);else{let o=t.tokens[0];if(o&&o.type==="text"&&o.tokens&&o.tokens.length>0){if(n=[{type:"paragraph",content:e.parseInline(o.tokens)}],t.tokens.length>1){let s=t.tokens.slice(1),l=e.parseChildren(s);n.push(...l)}}else n=e.parseChildren(t.tokens)}return n.length===0&&(n=[{type:"paragraph",content:[]}]),{type:"listItem",content:n}},renderMarkdown:(t,e,n)=>cr(t,e,r=>r.parentType==="bulletList"?"- ":r.parentType==="orderedList"?`${r.index+1}. `:"- ",n),addKeyboardShortcuts(){return{Enter:()=>this.editor.commands.splitListItem(this.name),Tab:()=>this.editor.commands.sinkListItem(this.name),"Shift-Tab":()=>this.editor.commands.liftListItem(this.name)}}}),W0={};V0(W0,{findListItemPos:()=>xr,getNextListDepth:()=>Vl,handleBackspace:()=>zl,handleDelete:()=>Hl,hasListBefore:()=>xf,hasListItemAfter:()=>j0,hasListItemBefore:()=>kf,listItemHasSubList:()=>Sf,nextListIsDeeper:()=>Cf,nextListIsHigher:()=>vf});var xr=(t,e)=>{let{$from:n}=e.selection,r=ne(t,e.schema),o=null,i=n.depth,s=n.pos,l=null;for(;i>0&&l===null;)o=n.node(i),o.type===r?l=i:(i-=1,s-=1);return l===null?null:{$pos:e.doc.resolve(s),depth:l}},Vl=(t,e)=>{let n=xr(t,e);if(!n)return!1;let[,r]=Xd(e,t,n.$pos.pos+4);return r},xf=(t,e,n)=>{let{$anchor:r}=t.selection,o=Math.max(0,r.pos-2),i=t.doc.resolve(o).node();return!(!i||!n.includes(i.type.name))},kf=(t,e)=>{var n;let{$anchor:r}=e.selection,o=e.doc.resolve(r.pos-2);return!(o.index()===0||((n=o.nodeBefore)==null?void 0:n.type.name)!==t)},Sf=(t,e,n)=>{if(!n)return!1;let r=ne(t,e.schema),o=!1;return n.descendants(i=>{i.type===r&&(o=!0)}),o},zl=(t,e,n)=>{if(t.commands.undoInputRule())return!0;if(t.state.selection.from!==t.state.selection.to)return!1;if(!Ze(t.state,e)&&xf(t.state,e,n)){let{$anchor:l}=t.state.selection,a=t.state.doc.resolve(l.before()-1),c=[];a.node().descendants((f,h)=>{f.type.name===e&&c.push({node:f,pos:h})});let d=c.at(-1);if(!d)return!1;let u=t.state.doc.resolve(a.start()+d.pos+1);return t.chain().cut({from:l.start()-1,to:l.end()+1},u.end()).joinForward().run()}if(!Ze(t.state,e)||!Qd(t.state))return!1;let r=xr(e,t.state);if(!r)return!1;let i=t.state.doc.resolve(r.$pos.pos-2).node(r.depth),s=Sf(e,t.state,i);return kf(e,t.state)&&!s?t.commands.joinItemBackward():t.chain().liftListItem(e).run()},Cf=(t,e)=>{let n=Vl(t,e),r=xr(t,e);return!r||!n?!1:n>r.depth},vf=(t,e)=>{let n=Vl(t,e),r=xr(t,e);return!r||!n?!1:n{if(!Ze(t.state,e)||!Yd(t.state,e))return!1;let{selection:n}=t.state,{$from:r,$to:o}=n;return!n.empty&&r.sameParent(o)?!1:Cf(e,t.state)?t.chain().focus(t.state.selection.from+4).lift(e).joinBackward().run():vf(e,t.state)?t.chain().joinForward().joinBackward().run():t.commands.joinItemForward()},j0=(t,e)=>{var n;let{$anchor:r}=e.selection,o=e.doc.resolve(r.pos-r.parentOffset-2);return!(o.index()===o.parent.childCount-1||((n=o.nodeAfter)==null?void 0:n.type.name)!==t)},U0=U.create({name:"listKeymap",addOptions(){return{listTypes:[{itemName:"listItem",wrapperNames:["bulletList","orderedList"]},{itemName:"taskItem",wrapperNames:["taskList"]}]}},addKeyboardShortcuts(){return{Delete:({editor:t})=>{let e=!1;return this.options.listTypes.forEach(({itemName:n})=>{t.state.schema.nodes[n]!==void 0&&Hl(t,n)&&(e=!0)}),e},"Mod-Delete":({editor:t})=>{let e=!1;return this.options.listTypes.forEach(({itemName:n})=>{t.state.schema.nodes[n]!==void 0&&Hl(t,n)&&(e=!0)}),e},Backspace:({editor:t})=>{let e=!1;return this.options.listTypes.forEach(({itemName:n,wrapperNames:r})=>{t.state.schema.nodes[n]!==void 0&&zl(t,n,r)&&(e=!0)}),e},"Mod-Backspace":({editor:t})=>{let e=!1;return this.options.listTypes.forEach(({itemName:n,wrapperNames:r})=>{t.state.schema.nodes[n]!==void 0&&zl(t,n,r)&&(e=!0)}),e}}}}),yf=/^(\s*)(\d+)\.\s+(.*)$/,K0=/^\s/;function q0(t){let e=[],n=0,r=0;for(;ne;)f.push(t[u]),u+=1;if(f.length>0){let h=Math.min(...f.map(m=>m.indent)),p=Mf(f,h,n);c.push({type:"list",ordered:!0,start:f[0].number,items:p,raw:f.map(m=>m.raw).join(` +`)})}o.push({type:"list_item",raw:s.raw,tokens:c}),i=u}else i+=1}return o}function J0(t,e){return t.map(n=>{if(n.type!=="list_item")return e.parseChildren([n])[0];let r=[];return n.tokens&&n.tokens.length>0&&n.tokens.forEach(o=>{if(o.type==="paragraph"||o.type==="list"||o.type==="blockquote"||o.type==="code")r.push(...e.parseChildren([o]));else if(o.type==="text"&&o.tokens){let i=e.parseChildren([o]);r.push({type:"paragraph",content:i})}else{let i=e.parseChildren([o]);i.length>0&&r.push(...i)}}),{type:"listItem",content:r}})}var G0="listItem",bf="textStyle",wf=/^(\d+)\.\s$/,_l=F.create({name:"orderedList",addOptions(){return{itemTypeName:"listItem",HTMLAttributes:{},keepMarks:!1,keepAttributes:!1}},group:"block list",content(){return`${this.options.itemTypeName}+`},addAttributes(){return{start:{default:1,parseHTML:t=>t.hasAttribute("start")?parseInt(t.getAttribute("start")||"",10):1},type:{default:null,parseHTML:t=>t.getAttribute("type")}}},parseHTML(){return[{tag:"ol"}]},renderHTML({HTMLAttributes:t}){let{start:e,...n}=t;return e===1?["ol",R(this.options.HTMLAttributes,n),0]:["ol",R(this.options.HTMLAttributes,t),0]},markdownTokenName:"list",parseMarkdown:(t,e)=>{if(t.type!=="list"||!t.ordered)return[];let n=t.start||1,r=t.items?J0(t.items,e):[];return n!==1?{type:"orderedList",attrs:{start:n},content:r}:{type:"orderedList",content:r}},renderMarkdown:(t,e)=>t.content?e.renderChildren(t.content,` +`):"",markdownTokenizer:{name:"orderedList",level:"block",start:t=>{let e=t.match(/^(\s*)(\d+)\.\s+/),n=e?.index;return n!==void 0?n:-1},tokenize:(t,e,n)=>{var r;let o=t.split(` +`),[i,s]=q0(o);if(i.length===0)return;let l=Mf(i,0,n);return l.length===0?void 0:{type:"list",ordered:!0,start:((r=i[0])==null?void 0:r.number)||1,items:l,raw:o.slice(0,s).join(` +`)}}},markdownOptions:{indentsContent:!0},addCommands(){return{toggleOrderedList:()=>({commands:t,chain:e})=>this.options.keepAttributes?e().toggleList(this.name,this.options.itemTypeName,this.options.keepMarks).updateAttributes(G0,this.editor.getAttributes(bf)).run():t.toggleList(this.name,this.options.itemTypeName,this.options.keepMarks)}},addKeyboardShortcuts(){return{"Mod-Shift-7":()=>this.editor.commands.toggleOrderedList()}},addInputRules(){let t=tt({find:wf,type:this.type,getAttributes:e=>({start:+e[1]}),joinPredicate:(e,n)=>n.childCount+n.attrs.start===+e[1]});return(this.options.keepMarks||this.options.keepAttributes)&&(t=tt({find:wf,type:this.type,keepMarks:this.options.keepMarks,keepAttributes:this.options.keepAttributes,getAttributes:e=>({start:+e[1],...this.editor.getAttributes(bf)}),joinPredicate:(e,n)=>n.childCount+n.attrs.start===+e[1],editor:this.editor})),[t]}}),X0=/^\s*(\[([( |x])?\])\s$/,Y0=F.create({name:"taskItem",addOptions(){return{nested:!1,HTMLAttributes:{},taskListTypeName:"taskList",a11y:void 0}},content(){return this.options.nested?"paragraph block*":"paragraph+"},defining:!0,addAttributes(){return{checked:{default:!1,keepOnSplit:!1,parseHTML:t=>{let e=t.getAttribute("data-checked");return e===""||e==="true"},renderHTML:t=>({"data-checked":t.checked})}}},parseHTML(){return[{tag:`li[data-type="${this.name}"]`,priority:51}]},renderHTML({node:t,HTMLAttributes:e}){return["li",R(this.options.HTMLAttributes,e,{"data-type":this.name}),["label",["input",{type:"checkbox",checked:t.attrs.checked?"checked":null}],["span"]],["div",0]]},parseMarkdown:(t,e)=>{let n=[];if(t.tokens&&t.tokens.length>0?n.push(e.createNode("paragraph",{},e.parseInline(t.tokens))):t.text?n.push(e.createNode("paragraph",{},[e.createNode("text",{text:t.text})])):n.push(e.createNode("paragraph",{},[])),t.nestedTokens&&t.nestedTokens.length>0){let r=e.parseChildren(t.nestedTokens);n.push(...r)}return e.createNode("taskItem",{checked:t.checked||!1},n)},renderMarkdown:(t,e)=>{var n;let o=`- [${(n=t.attrs)!=null&&n.checked?"x":" "}] `;return cr(t,e,o)},addKeyboardShortcuts(){let t={Enter:()=>this.editor.commands.splitListItem(this.name),"Shift-Tab":()=>this.editor.commands.liftListItem(this.name)};return this.options.nested?{...t,Tab:()=>this.editor.commands.sinkListItem(this.name)}:t},addNodeView(){return({node:t,HTMLAttributes:e,getPos:n,editor:r})=>{let o=document.createElement("li"),i=document.createElement("label"),s=document.createElement("span"),l=document.createElement("input"),a=document.createElement("div"),c=d=>{var u,f;l.ariaLabel=((f=(u=this.options.a11y)==null?void 0:u.checkboxLabel)==null?void 0:f.call(u,d,l.checked))||`Task item checkbox for ${d.textContent||"empty task item"}`};return c(t),i.contentEditable="false",l.type="checkbox",l.addEventListener("mousedown",d=>d.preventDefault()),l.addEventListener("change",d=>{if(!r.isEditable&&!this.options.onReadOnlyChecked){l.checked=!l.checked;return}let{checked:u}=d.target;r.isEditable&&typeof n=="function"&&r.chain().focus(void 0,{scrollIntoView:!1}).command(({tr:f})=>{let h=n();if(typeof h!="number")return!1;let p=f.doc.nodeAt(h);return f.setNodeMarkup(h,void 0,{...p?.attrs,checked:u}),!0}).run(),!r.isEditable&&this.options.onReadOnlyChecked&&(this.options.onReadOnlyChecked(t,u)||(l.checked=!l.checked))}),Object.entries(this.options.HTMLAttributes).forEach(([d,u])=>{o.setAttribute(d,u)}),o.dataset.checked=t.attrs.checked,l.checked=t.attrs.checked,i.append(l,s),o.append(i,a),Object.entries(e).forEach(([d,u])=>{o.setAttribute(d,u)}),{dom:o,contentDOM:a,update:d=>d.type!==this.type?!1:(o.dataset.checked=d.attrs.checked,l.checked=d.attrs.checked,c(d),!0)}}},addInputRules(){return[tt({find:X0,type:this.type,getAttributes:t=>({checked:t[t.length-1]==="x"})})]}}),Q0=F.create({name:"taskList",addOptions(){return{itemTypeName:"taskItem",HTMLAttributes:{}}},group:"block list",content(){return`${this.options.itemTypeName}+`},parseHTML(){return[{tag:`ul[data-type="${this.name}"]`,priority:51}]},renderHTML({HTMLAttributes:t}){return["ul",R(this.options.HTMLAttributes,t,{"data-type":this.name}),0]},parseMarkdown:(t,e)=>e.createNode("taskList",{},e.parseChildren(t.items||[])),renderMarkdown:(t,e)=>t.content?e.renderChildren(t.content,` +`):"",markdownTokenizer:{name:"taskList",level:"block",start(t){var e;let n=(e=t.match(/^\s*[-+*]\s+\[([ xX])\]\s+/))==null?void 0:e.index;return n!==void 0?n:-1},tokenize(t,e,n){let r=i=>{let s=wo(i,{itemPattern:/^(\s*)([-+*])\s+\[([ xX])\]\s+(.*)$/,extractItemData:l=>({indentLevel:l[1].length,mainContent:l[4],checked:l[3].toLowerCase()==="x"}),createToken:(l,a)=>({type:"taskItem",raw:"",mainContent:l.mainContent,indentLevel:l.indentLevel,checked:l.checked,text:l.mainContent,tokens:n.inlineTokens(l.mainContent),nestedTokens:a}),customNestedParser:r},n);return s?[{type:"taskList",raw:s.raw,items:s.items}]:n.blockTokens(i)},o=wo(t,{itemPattern:/^(\s*)([-+*])\s+\[([ xX])\]\s+(.*)$/,extractItemData:i=>({indentLevel:i[1].length,mainContent:i[4],checked:i[3].toLowerCase()==="x"}),createToken:(i,s)=>({type:"taskItem",raw:"",mainContent:i.mainContent,indentLevel:i.indentLevel,checked:i.checked,text:i.mainContent,tokens:n.inlineTokens(i.mainContent),nestedTokens:s}),customNestedParser:r},n);if(o)return{type:"taskList",raw:o.raw,items:o.items}}},markdownOptions:{indentsContent:!0},addCommands(){return{toggleTaskList:()=>({commands:t})=>t.toggleList(this.name,this.options.itemTypeName)}},addKeyboardShortcuts(){return{"Mod-Shift-9":()=>this.editor.commands.toggleTaskList()}}}),dv=U.create({name:"listKit",addExtensions(){let t=[];return this.options.bulletList!==!1&&t.push($l.configure(this.options.bulletList)),this.options.listItem!==!1&&t.push(Fl.configure(this.options.listItem)),this.options.listKeymap!==!1&&t.push(U0.configure(this.options.listKeymap)),this.options.orderedList!==!1&&t.push(_l.configure(this.options.orderedList)),this.options.taskItem!==!1&&t.push(Y0.configure(this.options.taskItem)),this.options.taskList!==!1&&t.push(Q0.configure(this.options.taskList)),t}});var ii=(t,e,n={})=>{t.dom.closest("form")?.dispatchEvent(new CustomEvent(e,{composed:!0,cancelable:!0,detail:n}))},Tf=({files:t,acceptedTypes:e,acceptedTypesValidationMessage:n,maxSize:r,maxSizeValidationMessage:o})=>{for(let i of t){if(e&&!e.includes(i.type))return n;if(r&&i.size>+r*1024)return o}return null},Z0=({editor:t,acceptedTypes:e,acceptedTypesValidationMessage:n,get$WireUsing:r,key:o,maxSize:i,maxSizeValidationMessage:s,statePath:l,uploadingMessage:a})=>{let c=d=>Livewire.fireAction(r().__instance,"callSchemaComponentMethod",[o,"getUploadedFileAttachmentTemporaryUrl",{attachment:d}],{async:!0});return new P({key:new H("localFiles"),props:{handleDrop(d,u){if(!u.dataTransfer?.files.length)return!1;let f=Array.from(u.dataTransfer.files),h=Tf({files:f,acceptedTypes:e,acceptedTypesValidationMessage:n,maxSize:i,maxSizeValidationMessage:s});if(h)return d.dom.dispatchEvent(new CustomEvent("rich-editor-file-validation-message",{bubbles:!0,detail:{key:o,livewireId:r().id,validationMessage:h}})),!1;if(!f.length)return!1;ii(d,"form-processing-started",{message:a}),u.preventDefault(),u.stopPropagation();let p=d.posAtCoords({left:u.clientX,top:u.clientY});return f.forEach((m,g)=>{t.setEditable(!1),d.dom.dispatchEvent(new CustomEvent("rich-editor-uploading-file",{bubbles:!0,detail:{key:o,livewireId:r().id}}));let y=("10000000-1000-4000-8000"+-1e11).replace(/[018]/g,w=>(w^crypto.getRandomValues(new Uint8Array(1))[0]&15>>w/4).toString(16));r().upload(`componentFileAttachments.${l}.${y}`,m,()=>{c(y).then(w=>{w&&(t.chain().insertContentAt(p?.pos??0,{type:"image",attrs:{id:y,src:w}}).run(),t.setEditable(!0),d.dom.dispatchEvent(new CustomEvent("rich-editor-uploaded-file",{bubbles:!0,detail:{key:o,livewireId:r().id}})),g===f.length-1&&ii(d,"form-processing-finished"))})})}),!0},handlePaste(d,u){if(!u.clipboardData?.files.length||u.clipboardData?.getData("text").length)return!1;let f=Array.from(u.clipboardData.files),h=Tf({files:f,acceptedTypes:e,acceptedTypesValidationMessage:n,maxSize:i,maxSizeValidationMessage:s});return h?(d.dom.dispatchEvent(new CustomEvent("rich-editor-file-validation-message",{bubbles:!0,detail:{key:o,livewireId:r().id,validationMessage:h}})),!1):f.length?(u.preventDefault(),u.stopPropagation(),ii(d,"form-processing-started",{message:a}),f.forEach((p,m)=>{t.setEditable(!1),d.dom.dispatchEvent(new CustomEvent("rich-editor-uploading-file",{bubbles:!0,detail:{key:o,livewireId:r().id}}));let g=("10000000-1000-4000-8000"+-1e11).replace(/[018]/g,y=>(y^crypto.getRandomValues(new Uint8Array(1))[0]&15>>y/4).toString(16));r().upload(`componentFileAttachments.${l}.${g}`,p,()=>{c(g).then(y=>{y&&(t.chain().insertContentAt(t.state.selection.anchor,{type:"image",attrs:{id:g,src:y}}).run(),t.setEditable(!0),d.dom.dispatchEvent(new CustomEvent("rich-editor-uploaded-file",{bubbles:!0,detail:{key:o,livewireId:r().id}})),m===f.length-1&&ii(d,"form-processing-finished"))})})}),!0):!1}}})},Af=U.create({name:"localFiles",addOptions(){return{acceptedTypes:[],acceptedTypesValidationMessage:null,key:null,maxSize:null,maxSizeValidationMessage:null,statePath:null,uploadingMessage:null,get$WireUsing:null}},addProseMirrorPlugins(){return[Z0({editor:this.editor,...this.options})]}});function ew(t){var e;let{char:n,allowSpaces:r,allowToIncludeChar:o,allowedPrefixes:i,startOfLine:s,$position:l}=t,a=r&&!o,c=wu(n),d=new RegExp(`\\s${c}$`),u=s?"^":"",f=o?"":c,h=a?new RegExp(`${u}${c}.*?(?=\\s${f}|$)`,"gm"):new RegExp(`${u}(?:^)?${c}[^\\s${f}]*`,"gm"),p=((e=l.nodeBefore)==null?void 0:e.isText)&&l.nodeBefore.text;if(!p)return null;let m=l.pos-p.length,g=Array.from(p.matchAll(h)).pop();if(!g||g.input===void 0||g.index===void 0)return null;let y=g.input.slice(Math.max(0,g.index-1),g.index),w=new RegExp(`^[${i?.join("")}\0]?$`).test(y);if(i!==null&&!w)return null;let b=m+g.index,C=b+g[0].length;return a&&d.test(p.slice(C-1,C+1))&&(g[0]+=" ",C+=1),b=l.pos?{range:{from:b,to:C},query:g[0].slice(n.length),text:g[0]}:null}var tw=new H("suggestion");function nw({pluginKey:t=tw,editor:e,char:n="@",allowSpaces:r=!1,allowToIncludeChar:o=!1,allowedPrefixes:i=[" "],startOfLine:s=!1,decorationTag:l="span",decorationClass:a="suggestion",decorationContent:c="",decorationEmptyClass:d="is-empty",command:u=()=>null,items:f=()=>[],render:h=()=>({}),allow:p=()=>!0,findSuggestionMatch:m=ew}){let g,y=h?.(),w=()=>{let S=e.state.selection.$anchor.pos,k=e.view.coordsAtPos(S),{top:O,right:T,bottom:A,left:$}=k;try{return new DOMRect($,O,T-$,A-O)}catch{return null}},b=(S,k)=>k?()=>{let O=t.getState(e.state),T=O?.decorationId,A=S.dom.querySelector(`[data-decoration-id="${T}"]`);return A?.getBoundingClientRect()||null}:w;function C(S,k){var O;try{let A=t.getState(S.state),$=A?.decorationId?S.dom.querySelector(`[data-decoration-id="${A.decorationId}"]`):null,z={editor:e,range:A?.range||{from:0,to:0},query:A?.query||null,text:A?.text||null,items:[],command:K=>u({editor:e,range:A?.range||{from:0,to:0},props:K}),decorationNode:$,clientRect:b(S,$)};(O=y?.onExit)==null||O.call(y,z)}catch{}let T=S.state.tr.setMeta(k,{exit:!0});S.dispatch(T)}let x=new P({key:t,view(){return{update:async(S,k)=>{var O,T,A,$,z,K,V;let N=(O=this.key)==null?void 0:O.getState(k),_=(T=this.key)==null?void 0:T.getState(S.state),W=N.active&&_.active&&N.range.from!==_.range.from,Q=!N.active&&_.active,me=N.active&&!_.active,Ge=!Q&&!me&&N.query!==_.query,q=Q||W&&Ge,We=Ge||W,je=me||W&&Ge;if(!q&&!We&&!je)return;let dn=je&&!q?N:_,aa=S.dom.querySelector(`[data-decoration-id="${dn.decorationId}"]`);g={editor:e,range:dn.range,query:dn.query,text:dn.text,items:[],command:ip=>u({editor:e,range:dn.range,props:ip}),decorationNode:aa,clientRect:b(S,aa)},q&&((A=y?.onBeforeStart)==null||A.call(y,g)),We&&(($=y?.onBeforeUpdate)==null||$.call(y,g)),(We||q)&&(g.items=await f({editor:e,query:dn.query})),je&&((z=y?.onExit)==null||z.call(y,g)),We&&((K=y?.onUpdate)==null||K.call(y,g)),q&&((V=y?.onStart)==null||V.call(y,g))},destroy:()=>{var S;g&&((S=y?.onExit)==null||S.call(y,g))}}},state:{init(){return{active:!1,range:{from:0,to:0},query:null,text:null,composing:!1}},apply(S,k,O,T){let{isEditable:A}=e,{composing:$}=e.view,{selection:z}=S,{empty:K,from:V}=z,N={...k},_=S.getMeta(t);if(_&&_.exit)return N.active=!1,N.decorationId=null,N.range={from:0,to:0},N.query=null,N.text=null,N;if(N.composing=$,A&&(K||e.view.composing)){(Vk.range.to)&&!$&&!k.composing&&(N.active=!1);let W=m({char:n,allowSpaces:r,allowToIncludeChar:o,allowedPrefixes:i,startOfLine:s,$position:z.$from}),Q=`id_${Math.floor(Math.random()*4294967295)}`;W&&p({editor:e,state:T,range:W.range,isActive:k.active})?(N.active=!0,N.decorationId=k.decorationId?k.decorationId:Q,N.range=W.range,N.query=W.query,N.text=W.text):N.active=!1}else N.active=!1;return N.active||(N.decorationId=null,N.range={from:0,to:0},N.query=null,N.text=null),N}},props:{handleKeyDown(S,k){var O,T,A,$;let{active:z,range:K}=x.getState(S.state);if(!z)return!1;if(k.key==="Escape"||k.key==="Esc"){let N=x.getState(S.state),_=(O=g?.decorationNode)!=null?O:null,W=_??(N?.decorationId?S.dom.querySelector(`[data-decoration-id="${N.decorationId}"]`):null);if(((T=y?.onKeyDown)==null?void 0:T.call(y,{view:S,event:k,range:N.range}))||!1)return!0;let me={editor:e,range:N.range,query:N.query,text:N.text,items:[],command:Ge=>u({editor:e,range:N.range,props:Ge}),decorationNode:W,clientRect:W?()=>W.getBoundingClientRect()||null:null};return(A=y?.onExit)==null||A.call(y,me),C(S,t),!0}return(($=y?.onKeyDown)==null?void 0:$.call(y,{view:S,event:k,range:K}))||!1},decorations(S){let{active:k,range:O,decorationId:T,query:A}=x.getState(S);if(!k)return null;let $=!A?.length,z=[a];return $&&z.push(d),Y.create(S.doc,[te.inline(O.from,O.to,{nodeName:l,class:z.join(" "),"data-decoration-id":T,"data-decoration-content":c})])}}});return x}var si=nw;var rw=function({editor:t,overrideSuggestionOptions:e,extensionName:n}){let r=new H;return{editor:t,char:"{{",pluginKey:r,command:({editor:o,range:i,props:s})=>{o.view.state.selection.$to.nodeAfter?.text?.startsWith(" ")&&(i.to+=1),o.chain().focus().insertContentAt(i,[{type:n,attrs:{...s}},{type:"text",text:" "}]).run(),o.view.dom.ownerDocument.defaultView?.getSelection()?.collapseToEnd()},allow:({state:o,range:i})=>{let s=o.doc.resolve(i.from),l=o.schema.nodes[n];return!!s.parent.type.contentMatch.matchType(l)},...e}},Ef=F.create({name:"mergeTag",priority:101,addStorage(){return{mergeTags:[],suggestions:[],getSuggestionFromChar:()=>null}},addOptions(){return{HTMLAttributes:{},renderText({node:t}){return`{{ ${this.mergeTags[t.attrs.id]} }}`},deleteTriggerWithBackspace:!1,renderHTML({options:t,node:e}){return["span",R(this.HTMLAttributes,t.HTMLAttributes),`${this.mergeTags[e.attrs.id]}`]},suggestions:[],suggestion:{}}},group:"inline",inline:!0,selectable:!1,atom:!0,addAttributes(){return{id:{default:null,parseHTML:t=>t.getAttribute("data-id"),renderHTML:t=>t.id?{"data-id":t.id}:{}}}},parseHTML(){return[{tag:`span[data-type="${this.name}"]`}]},renderHTML({node:t,HTMLAttributes:e}){let n=this.editor?.extensionStorage?.[this.name]?.getSuggestionFromChar("{{"),r={...this.options};r.HTMLAttributes=R({"data-type":this.name},this.options.HTMLAttributes,e);let o=this.options.renderHTML({options:r,node:t,suggestion:n});return typeof o=="string"?["span",R({"data-type":this.name},this.options.HTMLAttributes,e),o]:o},renderText({node:t}){let e={options:this.options,node:t,suggestion:this.editor?.extensionStorage?.[this.name]?.getSuggestionFromChar("{{")};return this.options.renderText(e)},addKeyboardShortcuts(){return{Backspace:()=>this.editor.commands.command(({tr:t,state:e})=>{let n=!1,{selection:r}=e,{empty:o,anchor:i}=r;if(!o)return!1;let s=new ie,l=0;return e.doc.nodesBetween(i-1,i,(a,c)=>{if(a.type.name===this.name)return n=!0,s=a,l=c,!1}),n&&t.insertText(this.options.deleteTriggerWithBackspace?"":"{{",l,l+s.nodeSize),n})}},addProseMirrorPlugins(){return[...this.storage.suggestions.map(si),new P({props:{handleDrop(t,e){if(!e||(e.preventDefault(),!e.dataTransfer.getData("mergeTag")))return!1;let n=e.dataTransfer.getData("mergeTag");return t.dispatch(t.state.tr.insert(t.posAtCoords({left:e.clientX,top:e.clientY}).pos,t.state.schema.nodes.mergeTag.create({id:n}))),!1}}})]},onBeforeCreate(){this.storage.suggestions=(this.options.suggestions.length?this.options.suggestions:[this.options.suggestion]).map(t=>rw({editor:this.editor,overrideSuggestionOptions:t,extensionName:this.name})),this.storage.getSuggestionFromChar=t=>{let e=this.storage.suggestions.find(n=>n.char===t);return e||(this.storage.suggestions.length?this.storage.suggestions[0]:null)}}});var Wl=["top","right","bottom","left"],Nf=["start","end"],jl=Wl.reduce((t,e)=>t.concat(e,e+"-"+Nf[0],e+"-"+Nf[1]),[]),$e=Math.min,pe=Math.max,Cr=Math.round;var Ue=t=>({x:t,y:t}),ow={left:"right",right:"left",bottom:"top",top:"bottom"},iw={start:"end",end:"start"};function li(t,e,n){return pe(t,$e(e,n))}function st(t,e){return typeof t=="function"?t(e):t}function Ne(t){return t.split("-")[0]}function Fe(t){return t.split("-")[1]}function Ul(t){return t==="x"?"y":"x"}function ai(t){return t==="y"?"height":"width"}var sw=new Set(["top","bottom"]);function Ke(t){return sw.has(Ne(t))?"y":"x"}function ci(t){return Ul(Ke(t))}function Kl(t,e,n){n===void 0&&(n=!1);let r=Fe(t),o=ci(t),i=ai(o),s=o==="x"?r===(n?"end":"start")?"right":"left":r==="start"?"bottom":"top";return e.reference[i]>e.floating[i]&&(s=Sr(s)),[s,Sr(s)]}function Df(t){let e=Sr(t);return[kr(t),e,kr(e)]}function kr(t){return t.replace(/start|end/g,e=>iw[e])}var Of=["left","right"],Rf=["right","left"],lw=["top","bottom"],aw=["bottom","top"];function cw(t,e,n){switch(t){case"top":case"bottom":return n?e?Rf:Of:e?Of:Rf;case"left":case"right":return e?lw:aw;default:return[]}}function If(t,e,n,r){let o=Fe(t),i=cw(Ne(t),n==="start",r);return o&&(i=i.map(s=>s+"-"+o),e&&(i=i.concat(i.map(kr)))),i}function Sr(t){return t.replace(/left|right|bottom|top/g,e=>ow[e])}function dw(t){return{top:0,right:0,bottom:0,left:0,...t}}function di(t){return typeof t!="number"?dw(t):{top:t,right:t,bottom:t,left:t}}function Ct(t){let{x:e,y:n,width:r,height:o}=t;return{width:r,height:o,top:n,left:e,right:e+r,bottom:n+o,x:e,y:n}}function Pf(t,e,n){let{reference:r,floating:o}=t,i=Ke(e),s=ci(e),l=ai(s),a=Ne(e),c=i==="y",d=r.x+r.width/2-o.width/2,u=r.y+r.height/2-o.height/2,f=r[l]/2-o[l]/2,h;switch(a){case"top":h={x:d,y:r.y-o.height};break;case"bottom":h={x:d,y:r.y+r.height};break;case"right":h={x:r.x+r.width,y:u};break;case"left":h={x:r.x-o.width,y:u};break;default:h={x:r.x,y:r.y}}switch(Fe(e)){case"start":h[s]-=f*(n&&c?-1:1);break;case"end":h[s]+=f*(n&&c?-1:1);break}return h}var zf=async(t,e,n)=>{let{placement:r="bottom",strategy:o="absolute",middleware:i=[],platform:s}=n,l=i.filter(Boolean),a=await(s.isRTL==null?void 0:s.isRTL(e)),c=await s.getElementRects({reference:t,floating:e,strategy:o}),{x:d,y:u}=Pf(c,r,a),f=r,h={},p=0;for(let m=0;m({name:"arrow",options:t,async fn(e){let{x:n,y:r,placement:o,rects:i,platform:s,elements:l,middlewareData:a}=e,{element:c,padding:d=0}=st(t,e)||{};if(c==null)return{};let u=di(d),f={x:n,y:r},h=ci(o),p=ai(h),m=await s.getDimensions(c),g=h==="y",y=g?"top":"left",w=g?"bottom":"right",b=g?"clientHeight":"clientWidth",C=i.reference[p]+i.reference[h]-f[h]-i.floating[p],x=f[h]-i.reference[h],S=await(s.getOffsetParent==null?void 0:s.getOffsetParent(c)),k=S?S[b]:0;(!k||!await(s.isElement==null?void 0:s.isElement(S)))&&(k=l.floating[b]||i.floating[p]);let O=C/2-x/2,T=k/2-m[p]/2-1,A=$e(u[y],T),$=$e(u[w],T),z=A,K=k-m[p]-$,V=k/2-m[p]/2+O,N=li(z,V,K),_=!a.arrow&&Fe(o)!=null&&V!==N&&i.reference[p]/2-(VFe(o)===t),...n.filter(o=>Fe(o)!==t)]:n.filter(o=>Ne(o)===o)).filter(o=>t?Fe(o)===t||(e?kr(o)!==o:!1):!0)}var $f=function(t){return t===void 0&&(t={}),{name:"autoPlacement",options:t,async fn(e){var n,r,o;let{rects:i,middlewareData:s,placement:l,platform:a,elements:c}=e,{crossAxis:d=!1,alignment:u,allowedPlacements:f=jl,autoAlignment:h=!0,...p}=st(t,e),m=u!==void 0||f===jl?uw(u||null,h,f):f,g=await on(e,p),y=((n=s.autoPlacement)==null?void 0:n.index)||0,w=m[y];if(w==null)return{};let b=Kl(w,i,await(a.isRTL==null?void 0:a.isRTL(c.floating)));if(l!==w)return{reset:{placement:m[0]}};let C=[g[Ne(w)],g[b[0]],g[b[1]]],x=[...((r=s.autoPlacement)==null?void 0:r.overflows)||[],{placement:w,overflows:C}],S=m[y+1];if(S)return{data:{index:y+1,overflows:x},reset:{placement:S}};let k=x.map(A=>{let $=Fe(A.placement);return[A.placement,$&&d?A.overflows.slice(0,2).reduce((z,K)=>z+K,0):A.overflows[0],A.overflows]}).sort((A,$)=>A[1]-$[1]),T=((o=k.filter(A=>A[2].slice(0,Fe(A[0])?2:3).every($=>$<=0))[0])==null?void 0:o[0])||k[0][0];return T!==l?{data:{index:y+1,overflows:x},reset:{placement:T}}:{}}}},Ff=function(t){return t===void 0&&(t={}),{name:"flip",options:t,async fn(e){var n,r;let{placement:o,middlewareData:i,rects:s,initialPlacement:l,platform:a,elements:c}=e,{mainAxis:d=!0,crossAxis:u=!0,fallbackPlacements:f,fallbackStrategy:h="bestFit",fallbackAxisSideDirection:p="none",flipAlignment:m=!0,...g}=st(t,e);if((n=i.arrow)!=null&&n.alignmentOffset)return{};let y=Ne(o),w=Ke(l),b=Ne(l)===l,C=await(a.isRTL==null?void 0:a.isRTL(c.floating)),x=f||(b||!m?[Sr(l)]:Df(l)),S=p!=="none";!f&&S&&x.push(...If(l,m,p,C));let k=[l,...x],O=await on(e,g),T=[],A=((r=i.flip)==null?void 0:r.overflows)||[];if(d&&T.push(O[y]),u){let V=Kl(o,s,C);T.push(O[V[0]],O[V[1]])}if(A=[...A,{placement:o,overflows:T}],!T.every(V=>V<=0)){var $,z;let V=((($=i.flip)==null?void 0:$.index)||0)+1,N=k[V];if(N&&(!(u==="alignment"?w!==Ke(N):!1)||A.every(Q=>Ke(Q.placement)===w?Q.overflows[0]>0:!0)))return{data:{index:V,overflows:A},reset:{placement:N}};let _=(z=A.filter(W=>W.overflows[0]<=0).sort((W,Q)=>W.overflows[1]-Q.overflows[1])[0])==null?void 0:z.placement;if(!_)switch(h){case"bestFit":{var K;let W=(K=A.filter(Q=>{if(S){let me=Ke(Q.placement);return me===w||me==="y"}return!0}).map(Q=>[Q.placement,Q.overflows.filter(me=>me>0).reduce((me,Ge)=>me+Ge,0)]).sort((Q,me)=>Q[1]-me[1])[0])==null?void 0:K[0];W&&(_=W);break}case"initialPlacement":_=l;break}if(o!==_)return{reset:{placement:_}}}return{}}}};function Lf(t,e){return{top:t.top-e.height,right:t.right-e.width,bottom:t.bottom-e.height,left:t.left-e.width}}function Bf(t){return Wl.some(e=>t[e]>=0)}var Vf=function(t){return t===void 0&&(t={}),{name:"hide",options:t,async fn(e){let{rects:n}=e,{strategy:r="referenceHidden",...o}=st(t,e);switch(r){case"referenceHidden":{let i=await on(e,{...o,elementContext:"reference"}),s=Lf(i,n.reference);return{data:{referenceHiddenOffsets:s,referenceHidden:Bf(s)}}}case"escaped":{let i=await on(e,{...o,altBoundary:!0}),s=Lf(i,n.floating);return{data:{escapedOffsets:s,escaped:Bf(s)}}}default:return{}}}}};function _f(t){let e=$e(...t.map(i=>i.left)),n=$e(...t.map(i=>i.top)),r=pe(...t.map(i=>i.right)),o=pe(...t.map(i=>i.bottom));return{x:e,y:n,width:r-e,height:o-n}}function fw(t){let e=t.slice().sort((o,i)=>o.y-i.y),n=[],r=null;for(let o=0;or.height/2?n.push([i]):n[n.length-1].push(i),r=i}return n.map(o=>Ct(_f(o)))}var Wf=function(t){return t===void 0&&(t={}),{name:"inline",options:t,async fn(e){let{placement:n,elements:r,rects:o,platform:i,strategy:s}=e,{padding:l=2,x:a,y:c}=st(t,e),d=Array.from(await(i.getClientRects==null?void 0:i.getClientRects(r.reference))||[]),u=fw(d),f=Ct(_f(d)),h=di(l);function p(){if(u.length===2&&u[0].left>u[1].right&&a!=null&&c!=null)return u.find(g=>a>g.left-h.left&&ag.top-h.top&&c=2){if(Ke(n)==="y"){let A=u[0],$=u[u.length-1],z=Ne(n)==="top",K=A.top,V=$.bottom,N=z?A.left:$.left,_=z?A.right:$.right,W=_-N,Q=V-K;return{top:K,bottom:V,left:N,right:_,width:W,height:Q,x:N,y:K}}let g=Ne(n)==="left",y=pe(...u.map(A=>A.right)),w=$e(...u.map(A=>A.left)),b=u.filter(A=>g?A.left===w:A.right===y),C=b[0].top,x=b[b.length-1].bottom,S=w,k=y,O=k-S,T=x-C;return{top:C,bottom:x,left:S,right:k,width:O,height:T,x:S,y:C}}return f}let m=await i.getElementRects({reference:{getBoundingClientRect:p},floating:r.floating,strategy:s});return o.reference.x!==m.reference.x||o.reference.y!==m.reference.y||o.reference.width!==m.reference.width||o.reference.height!==m.reference.height?{reset:{rects:m}}:{}}}},hw=new Set(["left","top"]);async function pw(t,e){let{placement:n,platform:r,elements:o}=t,i=await(r.isRTL==null?void 0:r.isRTL(o.floating)),s=Ne(n),l=Fe(n),a=Ke(n)==="y",c=hw.has(s)?-1:1,d=i&&a?-1:1,u=st(e,t),{mainAxis:f,crossAxis:h,alignmentAxis:p}=typeof u=="number"?{mainAxis:u,crossAxis:0,alignmentAxis:null}:{mainAxis:u.mainAxis||0,crossAxis:u.crossAxis||0,alignmentAxis:u.alignmentAxis};return l&&typeof p=="number"&&(h=l==="end"?p*-1:p),a?{x:h*d,y:f*c}:{x:f*c,y:h*d}}var jf=function(t){return t===void 0&&(t=0),{name:"offset",options:t,async fn(e){var n,r;let{x:o,y:i,placement:s,middlewareData:l}=e,a=await pw(e,t);return s===((n=l.offset)==null?void 0:n.placement)&&(r=l.arrow)!=null&&r.alignmentOffset?{}:{x:o+a.x,y:i+a.y,data:{...a,placement:s}}}}},Uf=function(t){return t===void 0&&(t={}),{name:"shift",options:t,async fn(e){let{x:n,y:r,placement:o}=e,{mainAxis:i=!0,crossAxis:s=!1,limiter:l={fn:g=>{let{x:y,y:w}=g;return{x:y,y:w}}},...a}=st(t,e),c={x:n,y:r},d=await on(e,a),u=Ke(Ne(o)),f=Ul(u),h=c[f],p=c[u];if(i){let g=f==="y"?"top":"left",y=f==="y"?"bottom":"right",w=h+d[g],b=h-d[y];h=li(w,h,b)}if(s){let g=u==="y"?"top":"left",y=u==="y"?"bottom":"right",w=p+d[g],b=p-d[y];p=li(w,p,b)}let m=l.fn({...e,[f]:h,[u]:p});return{...m,data:{x:m.x-n,y:m.y-r,enabled:{[f]:i,[u]:s}}}}}};var Kf=function(t){return t===void 0&&(t={}),{name:"size",options:t,async fn(e){var n,r;let{placement:o,rects:i,platform:s,elements:l}=e,{apply:a=()=>{},...c}=st(t,e),d=await on(e,c),u=Ne(o),f=Fe(o),h=Ke(o)==="y",{width:p,height:m}=i.floating,g,y;u==="top"||u==="bottom"?(g=u,y=f===(await(s.isRTL==null?void 0:s.isRTL(l.floating))?"start":"end")?"left":"right"):(y=u,g=f==="end"?"top":"bottom");let w=m-d.top-d.bottom,b=p-d.left-d.right,C=$e(m-d[g],w),x=$e(p-d[y],b),S=!e.middlewareData.shift,k=C,O=x;if((n=e.middlewareData.shift)!=null&&n.enabled.x&&(O=b),(r=e.middlewareData.shift)!=null&&r.enabled.y&&(k=w),S&&!f){let A=pe(d.left,0),$=pe(d.right,0),z=pe(d.top,0),K=pe(d.bottom,0);h?O=p-2*(A!==0||$!==0?A+$:pe(d.left,d.right)):k=m-2*(z!==0||K!==0?z+K:pe(d.top,d.bottom))}await a({...e,availableWidth:O,availableHeight:k});let T=await s.getDimensions(l.floating);return p!==T.width||m!==T.height?{reset:{rects:!0}}:{}}}};function fi(){return typeof window<"u"}function sn(t){return Jf(t)?(t.nodeName||"").toLowerCase():"#document"}function Ae(t){var e;return(t==null||(e=t.ownerDocument)==null?void 0:e.defaultView)||window}function lt(t){var e;return(e=(Jf(t)?t.ownerDocument:t.document)||window.document)==null?void 0:e.documentElement}function Jf(t){return fi()?t instanceof Node||t instanceof Ae(t).Node:!1}function Ve(t){return fi()?t instanceof Element||t instanceof Ae(t).Element:!1}function qe(t){return fi()?t instanceof HTMLElement||t instanceof Ae(t).HTMLElement:!1}function qf(t){return!fi()||typeof ShadowRoot>"u"?!1:t instanceof ShadowRoot||t instanceof Ae(t).ShadowRoot}var mw=new Set(["inline","contents"]);function Rn(t){let{overflow:e,overflowX:n,overflowY:r,display:o}=_e(t);return/auto|scroll|overlay|hidden|clip/.test(e+r+n)&&!mw.has(o)}var gw=new Set(["table","td","th"]);function Gf(t){return gw.has(sn(t))}var yw=[":popover-open",":modal"];function vr(t){return yw.some(e=>{try{return t.matches(e)}catch{return!1}})}var bw=["transform","translate","scale","rotate","perspective"],ww=["transform","translate","scale","rotate","perspective","filter"],xw=["paint","layout","strict","content"];function hi(t){let e=pi(),n=Ve(t)?_e(t):t;return bw.some(r=>n[r]?n[r]!=="none":!1)||(n.containerType?n.containerType!=="normal":!1)||!e&&(n.backdropFilter?n.backdropFilter!=="none":!1)||!e&&(n.filter?n.filter!=="none":!1)||ww.some(r=>(n.willChange||"").includes(r))||xw.some(r=>(n.contain||"").includes(r))}function Xf(t){let e=vt(t);for(;qe(e)&&!ln(e);){if(hi(e))return e;if(vr(e))return null;e=vt(e)}return null}function pi(){return typeof CSS>"u"||!CSS.supports?!1:CSS.supports("-webkit-backdrop-filter","none")}var kw=new Set(["html","body","#document"]);function ln(t){return kw.has(sn(t))}function _e(t){return Ae(t).getComputedStyle(t)}function Mr(t){return Ve(t)?{scrollLeft:t.scrollLeft,scrollTop:t.scrollTop}:{scrollLeft:t.scrollX,scrollTop:t.scrollY}}function vt(t){if(sn(t)==="html")return t;let e=t.assignedSlot||t.parentNode||qf(t)&&t.host||lt(t);return qf(e)?e.host:e}function Yf(t){let e=vt(t);return ln(e)?t.ownerDocument?t.ownerDocument.body:t.body:qe(e)&&Rn(e)?e:Yf(e)}function ui(t,e,n){var r;e===void 0&&(e=[]),n===void 0&&(n=!0);let o=Yf(t),i=o===((r=t.ownerDocument)==null?void 0:r.body),s=Ae(o);if(i){let l=mi(s);return e.concat(s,s.visualViewport||[],Rn(o)?o:[],l&&n?ui(l):[])}return e.concat(o,ui(o,[],n))}function mi(t){return t.parent&&Object.getPrototypeOf(t.parent)?t.frameElement:null}function th(t){let e=_e(t),n=parseFloat(e.width)||0,r=parseFloat(e.height)||0,o=qe(t),i=o?t.offsetWidth:n,s=o?t.offsetHeight:r,l=Cr(n)!==i||Cr(r)!==s;return l&&(n=i,r=s),{width:n,height:r,$:l}}function nh(t){return Ve(t)?t:t.contextElement}function Dn(t){let e=nh(t);if(!qe(e))return Ue(1);let n=e.getBoundingClientRect(),{width:r,height:o,$:i}=th(e),s=(i?Cr(n.width):n.width)/r,l=(i?Cr(n.height):n.height)/o;return(!s||!Number.isFinite(s))&&(s=1),(!l||!Number.isFinite(l))&&(l=1),{x:s,y:l}}var Sw=Ue(0);function rh(t){let e=Ae(t);return!pi()||!e.visualViewport?Sw:{x:e.visualViewport.offsetLeft,y:e.visualViewport.offsetTop}}function Cw(t,e,n){return e===void 0&&(e=!1),!n||e&&n!==Ae(t)?!1:e}function Tr(t,e,n,r){e===void 0&&(e=!1),n===void 0&&(n=!1);let o=t.getBoundingClientRect(),i=nh(t),s=Ue(1);e&&(r?Ve(r)&&(s=Dn(r)):s=Dn(t));let l=Cw(i,n,r)?rh(i):Ue(0),a=(o.left+l.x)/s.x,c=(o.top+l.y)/s.y,d=o.width/s.x,u=o.height/s.y;if(i){let f=Ae(i),h=r&&Ve(r)?Ae(r):r,p=f,m=mi(p);for(;m&&r&&h!==p;){let g=Dn(m),y=m.getBoundingClientRect(),w=_e(m),b=y.left+(m.clientLeft+parseFloat(w.paddingLeft))*g.x,C=y.top+(m.clientTop+parseFloat(w.paddingTop))*g.y;a*=g.x,c*=g.y,d*=g.x,u*=g.y,a+=b,c+=C,p=Ae(m),m=mi(p)}}return Ct({width:d,height:u,x:a,y:c})}function gi(t,e){let n=Mr(t).scrollLeft;return e?e.left+n:Tr(lt(t)).left+n}function oh(t,e){let n=t.getBoundingClientRect(),r=n.left+e.scrollLeft-gi(t,n),o=n.top+e.scrollTop;return{x:r,y:o}}function vw(t){let{elements:e,rect:n,offsetParent:r,strategy:o}=t,i=o==="fixed",s=lt(r),l=e?vr(e.floating):!1;if(r===s||l&&i)return n;let a={scrollLeft:0,scrollTop:0},c=Ue(1),d=Ue(0),u=qe(r);if((u||!u&&!i)&&((sn(r)!=="body"||Rn(s))&&(a=Mr(r)),qe(r))){let h=Tr(r);c=Dn(r),d.x=h.x+r.clientLeft,d.y=h.y+r.clientTop}let f=s&&!u&&!i?oh(s,a):Ue(0);return{width:n.width*c.x,height:n.height*c.y,x:n.x*c.x-a.scrollLeft*c.x+d.x+f.x,y:n.y*c.y-a.scrollTop*c.y+d.y+f.y}}function Mw(t){return Array.from(t.getClientRects())}function Tw(t){let e=lt(t),n=Mr(t),r=t.ownerDocument.body,o=pe(e.scrollWidth,e.clientWidth,r.scrollWidth,r.clientWidth),i=pe(e.scrollHeight,e.clientHeight,r.scrollHeight,r.clientHeight),s=-n.scrollLeft+gi(t),l=-n.scrollTop;return _e(r).direction==="rtl"&&(s+=pe(e.clientWidth,r.clientWidth)-o),{width:o,height:i,x:s,y:l}}var Qf=25;function Aw(t,e){let n=Ae(t),r=lt(t),o=n.visualViewport,i=r.clientWidth,s=r.clientHeight,l=0,a=0;if(o){i=o.width,s=o.height;let d=pi();(!d||d&&e==="fixed")&&(l=o.offsetLeft,a=o.offsetTop)}let c=gi(r);if(c<=0){let d=r.ownerDocument,u=d.body,f=getComputedStyle(u),h=d.compatMode==="CSS1Compat"&&parseFloat(f.marginLeft)+parseFloat(f.marginRight)||0,p=Math.abs(r.clientWidth-u.clientWidth-h);p<=Qf&&(i-=p)}else c<=Qf&&(i+=c);return{width:i,height:s,x:l,y:a}}var Ew=new Set(["absolute","fixed"]);function Nw(t,e){let n=Tr(t,!0,e==="fixed"),r=n.top+t.clientTop,o=n.left+t.clientLeft,i=qe(t)?Dn(t):Ue(1),s=t.clientWidth*i.x,l=t.clientHeight*i.y,a=o*i.x,c=r*i.y;return{width:s,height:l,x:a,y:c}}function Zf(t,e,n){let r;if(e==="viewport")r=Aw(t,n);else if(e==="document")r=Tw(lt(t));else if(Ve(e))r=Nw(e,n);else{let o=rh(t);r={x:e.x-o.x,y:e.y-o.y,width:e.width,height:e.height}}return Ct(r)}function ih(t,e){let n=vt(t);return n===e||!Ve(n)||ln(n)?!1:_e(n).position==="fixed"||ih(n,e)}function Ow(t,e){let n=e.get(t);if(n)return n;let r=ui(t,[],!1).filter(l=>Ve(l)&&sn(l)!=="body"),o=null,i=_e(t).position==="fixed",s=i?vt(t):t;for(;Ve(s)&&!ln(s);){let l=_e(s),a=hi(s);!a&&l.position==="fixed"&&(o=null),(i?!a&&!o:!a&&l.position==="static"&&!!o&&Ew.has(o.position)||Rn(s)&&!a&&ih(t,s))?r=r.filter(d=>d!==s):o=l,s=vt(s)}return e.set(t,r),r}function Rw(t){let{element:e,boundary:n,rootBoundary:r,strategy:o}=t,s=[...n==="clippingAncestors"?vr(e)?[]:Ow(e,this._c):[].concat(n),r],l=s[0],a=s.reduce((c,d)=>{let u=Zf(e,d,o);return c.top=pe(u.top,c.top),c.right=$e(u.right,c.right),c.bottom=$e(u.bottom,c.bottom),c.left=pe(u.left,c.left),c},Zf(e,l,o));return{width:a.right-a.left,height:a.bottom-a.top,x:a.left,y:a.top}}function Dw(t){let{width:e,height:n}=th(t);return{width:e,height:n}}function Iw(t,e,n){let r=qe(e),o=lt(e),i=n==="fixed",s=Tr(t,!0,i,e),l={scrollLeft:0,scrollTop:0},a=Ue(0);function c(){a.x=gi(o)}if(r||!r&&!i)if((sn(e)!=="body"||Rn(o))&&(l=Mr(e)),r){let h=Tr(e,!0,i,e);a.x=h.x+e.clientLeft,a.y=h.y+e.clientTop}else o&&c();i&&!r&&o&&c();let d=o&&!r&&!i?oh(o,l):Ue(0),u=s.left+l.scrollLeft-a.x-d.x,f=s.top+l.scrollTop-a.y-d.y;return{x:u,y:f,width:s.width,height:s.height}}function ql(t){return _e(t).position==="static"}function eh(t,e){if(!qe(t)||_e(t).position==="fixed")return null;if(e)return e(t);let n=t.offsetParent;return lt(t)===n&&(n=n.ownerDocument.body),n}function sh(t,e){let n=Ae(t);if(vr(t))return n;if(!qe(t)){let o=vt(t);for(;o&&!ln(o);){if(Ve(o)&&!ql(o))return o;o=vt(o)}return n}let r=eh(t,e);for(;r&&Gf(r)&&ql(r);)r=eh(r,e);return r&&ln(r)&&ql(r)&&!hi(r)?n:r||Xf(t)||n}var Pw=async function(t){let e=this.getOffsetParent||sh,n=this.getDimensions,r=await n(t.floating);return{reference:Iw(t.reference,await e(t.floating),t.strategy),floating:{x:0,y:0,width:r.width,height:r.height}}};function Lw(t){return _e(t).direction==="rtl"}var Bw={convertOffsetParentRelativeRectToViewportRelativeRect:vw,getDocumentElement:lt,getClippingRect:Rw,getOffsetParent:sh,getElementRects:Pw,getClientRects:Mw,getDimensions:Dw,getScale:Dn,isElement:Ve,isRTL:Lw};var lh=jf,ah=$f,In=Uf,Pn=Ff,ch=Kf,dh=Vf,uh=Hf,fh=Wf;var Ln=(t,e,n)=>{let r=new Map,o={platform:Bw,...n},i={...o.platform,_c:r};return zf(t,e,{...o,platform:i})};var hh=(t,e)=>{Ln({getBoundingClientRect:()=>{let{from:r,to:o}=t.state.selection,i=t.view.coordsAtPos(r),s=t.view.coordsAtPos(o);return{top:Math.min(i.top,s.top),bottom:Math.max(i.bottom,s.bottom),left:Math.min(i.left,s.left),right:Math.max(i.right,s.right),width:Math.abs(s.right-i.left),height:Math.abs(s.bottom-i.top),x:Math.min(i.left,s.left),y:Math.min(i.top,s.top)}}},e,{placement:"bottom-start",strategy:"absolute",middleware:[In(),Pn()]}).then(({x:r,y:o,strategy:i})=>{e.style.width="max-content",e.style.position=i,e.style.left=`${r}px`,e.style.top=`${o}px`})},ph=({items:t=[],noOptionsMessage:e=null,noSearchResultsMessage:n=null,searchPrompt:r=null,searchingMessage:o=null,isSearchable:i=!1})=>{let s=null;return{items:async({query:l})=>{if(typeof t=="function"){s&&i&&s.setLoading(!0);try{let c=t({query:l}),d=Array.isArray(c)?c:await c;return s&&s.setLoading(!1),d}catch{return s&&s.setLoading(!1),[]}}if(!l)return t;let a=String(l).toLowerCase();return t.filter(c=>{let d=typeof c=="string"?c:c?.label??c?.name??"";return String(d).toLowerCase().includes(a)})},render:()=>{let l,a=0,c=null,d=!1;s={setLoading:b=>{d=b,f()}};let u=()=>{let b=document.createElement("div");return b.className="fi-dropdown-panel fi-dropdown-list fi-scrollable",b.style.maxHeight="15rem",b.style.minWidth="12rem",b},f=()=>{if(!l||!c)return;let b=Array.isArray(c.items)?c.items:[],C=c.query??"";if(l.innerHTML="",d){let x=o??"Searching...",S=document.createElement("div");S.className="fi-dropdown-header";let k=document.createElement("span");k.style.whiteSpace="normal",k.textContent=x,S.appendChild(k),l.appendChild(S);return}if(b.length)b.forEach((x,S)=>{let k=typeof x=="string"?x:x?.label??x?.name??String(x?.id??""),O=typeof x=="object"?x?.id??k:k,T=document.createElement("button");T.className=`fi-dropdown-list-item ${S===a?"fi-selected":""}`,T.type="button",T.addEventListener("click",()=>p(O,k));let A=document.createElement("span");A.className="fi-dropdown-list-item-label",A.textContent=k,T.appendChild(A),l.appendChild(T)});else{let x=h(C);if(x){let S=document.createElement("div");S.className="fi-dropdown-header";let k=document.createElement("span");k.style.whiteSpace="normal",k.textContent=x,S.appendChild(k),l.appendChild(S)}}},h=b=>b?n:i?r:e,p=(b,C)=>{c&&c.command({id:b,label:C})},m=()=>{if(!l||!c||(c.items||[]).length===0)return;let C=l.children[a];if(C){let x=C.getBoundingClientRect(),S=l.getBoundingClientRect();(x.topS.bottom)&&C.scrollIntoView({block:"nearest"})}},g=()=>{if(!c)return;let b=Array.isArray(c.items)?c.items:[];b.length!==0&&(a=(a+b.length-1)%b.length,f(),m())},y=()=>{if(!c)return;let b=c.items||[];b.length!==0&&(a=(a+1)%b.length,f(),m())},w=()=>{let b=c?.items||[];if(b.length===0)return;let C=b[a],x=typeof C=="string"?C:C?.label??C?.name??String(C?.id??""),S=typeof C=="object"?C?.id??x:x;p(S,x)};return{onStart:b=>{c=b,a=0,l=u(),l.style.position="absolute",l.style.zIndex="50",f(),document.body.appendChild(l),b.clientRect&&hh(b.editor,l)},onUpdate:b=>{c=b,a=0,f(),m(),b.clientRect&&hh(b.editor,l)},onKeyDown:b=>b.event.key==="Escape"?(l&&l.parentNode&&l.parentNode.removeChild(l),!0):b.event.key==="ArrowUp"?(g(),!0):b.event.key==="ArrowDown"?(y(),!0):b.event.key==="Enter"?(w(),!0):!1,onExit:()=>{l&&l.parentNode&&l.parentNode.removeChild(l),s=null}}}}};var zw=function({editor:t,overrideSuggestionOptions:e,extensionName:n}){let r=new H,o=e?.char??"@",i=e?.extraAttributes??{};return{editor:t,char:o,pluginKey:r,command:({editor:s,range:l,props:a})=>{s.view.state.selection.$to.nodeAfter?.text?.startsWith(" ")&&(l.to+=1);let u={...a,char:o,extra:i};s.chain().focus().insertContentAt(l,[{type:n,attrs:u},{type:"text",text:" "}]).run(),s.view.dom.ownerDocument.defaultView?.getSelection()?.collapseToEnd()},allow:({state:s,range:l})=>{let a=s.doc.resolve(l.from),c=s.schema.nodes[n];return!!a.parent.type.contentMatch.matchType(c)},...e}},mh=F.create({name:"mention",priority:101,addStorage(){return{suggestions:[],getSuggestionFromChar:()=>null}},addOptions(){return{HTMLAttributes:{},renderText({node:t}){return`${t.attrs.char??"@"}`},deleteTriggerWithBackspace:!0,renderHTML({options:t,node:e}){return["span",R(this.HTMLAttributes,t.HTMLAttributes),`${e.attrs.char??"@"}${e.attrs.label??""}`]},suggestions:[],suggestion:{},getMentionLabelsUsing:null}},group:"inline",inline:!0,selectable:!1,atom:!0,addAttributes(){return{id:{default:null,parseHTML:t=>t.getAttribute("data-id"),renderHTML:t=>t.id?{"data-id":t.id}:{}},label:{default:null,keepOnSplit:!1,parseHTML:t=>t.getAttribute("data-label"),renderHTML:t=>t.label?{"data-label":t.label}:{}},char:{default:"@",parseHTML:t=>t.getAttribute("data-char")??"@",renderHTML:t=>t.char?{"data-char":t.char}:{}},extra:{default:null,renderHTML:t=>{let e=t?.extra;return!e||typeof e!="object"?{}:e}}}},parseHTML(){return[{tag:`span[data-type="${this.name}"]`}]},renderHTML({node:t,HTMLAttributes:e}){let n=this.editor?.extensionStorage?.[this.name]?.getSuggestionFromChar(t?.attrs?.char??"@"),r={...this.options};r.HTMLAttributes=R({"data-type":this.name},this.options.HTMLAttributes,e);let o=this.options.renderHTML({options:r,node:t,suggestion:n});return typeof o=="string"?["span",R({"data-type":this.name},this.options.HTMLAttributes,e),o]:o},renderText({node:t}){let e={options:this.options,node:t,suggestion:this.editor?.extensionStorage?.[this.name]?.getSuggestionFromChar(t?.attrs?.char??"@")};return this.options.renderText(e)},addKeyboardShortcuts(){return{Backspace:()=>this.editor.commands.command(({tr:t,state:e})=>{let n=!1,{selection:r}=e,{empty:o,anchor:i}=r;if(!o)return!1;let s=new ie,l=0;if(e.doc.nodesBetween(i-1,i,(a,c)=>{if(a.type.name===this.name)return n=!0,s=a,l=c,!1}),n){let a=s?.attrs?.char??"@";t.insertText(this.options.deleteTriggerWithBackspace?"":a,l,l+s.nodeSize)}return n})}},addProseMirrorPlugins(){let t=async e=>{let{state:n,dispatch:r}=e,o=[];if(n.doc.descendants((s,l)=>{if(s.type.name!==this.name||s.attrs?.label)return;let a=s.attrs?.id,c=s.attrs?.char??"@";a&&o.push({id:a,char:c,pos:l})}),o.length===0)return;let i=this.options.getMentionLabelsUsing;if(typeof i=="function")try{let s=o.map(({id:a,char:c})=>({id:a,char:c})),l=await i(s);o.forEach(({id:a,pos:c})=>{let d=l[a];if(!d)return;let u=e.state.doc.nodeAt(c);if(!u||u.type.name!==this.name)return;let f={...u.attrs,label:d},h=e.state.tr.setNodeMarkup(c,void 0,f);r(h)})}catch{}};return[...this.storage.suggestions.map(si),new P({view:e=>(setTimeout(()=>t(e),0),{update:n=>t(n)})})]},onBeforeCreate(){let t=n=>Array.isArray(n)?n:n&&typeof n=="object"?Object.entries(n).map(([r,o])=>({id:r,label:o})):[],e=this.options.suggestions.length?this.options.suggestions:[this.options.suggestion];this.storage.suggestions=e.map(n=>{let r=n?.char??"@",o=n?.items??[],i=n?.noOptionsMessage??null,s=n?.noSearchResultsMessage??null,l=n?.isSearchable??!1,a=this.options.getMentionSearchResultsUsing,c=n;if(typeof n?.items=="function"){let d=n.items;c={...n,items:async u=>{if(u?.query&&typeof a=="function")try{let f=await a(u?.query,r);return t(f)}catch{}return await d(u)}}}else{let d=n?.extraAttributes,u=n?.searchPrompt??null,f=n?.searchingMessage??null;c={...ph({items:async({query:h})=>{if(!(Array.isArray(o)?o.length>0:o&&typeof o=="object"&&Object.keys(o).length>0)&&!h)return[];let m=t(o);if(h&&typeof a=="function")try{let y=await a(h,r);return t(y)}catch{}if(!h)return m;let g=String(h).toLowerCase();return m.filter(y=>{let w=typeof y=="string"?y:y?.label??y?.name??"";return String(w).toLowerCase().includes(g)})},isSearchable:l,noOptionsMessage:i,noSearchResultsMessage:s,searchPrompt:u,searchingMessage:f}),char:r,...d?{extraAttributes:d}:{}}}return zw({editor:this.editor,overrideSuggestionOptions:c,extensionName:this.name})}),this.storage.getSuggestionFromChar=n=>this.storage.suggestions.find(r=>r.char===n)??this.storage.suggestions[0]??null}});var Hw=F.create({name:"paragraph",priority:1e3,addOptions(){return{HTMLAttributes:{}}},group:"block",content:"inline*",parseHTML(){return[{tag:"p"}]},renderHTML({HTMLAttributes:t}){return["p",R(this.options.HTMLAttributes,t),0]},parseMarkdown:(t,e)=>{let n=t.tokens||[];return n.length===1&&n[0].type==="image"?e.parseChildren([n[0]]):e.createNode("paragraph",void 0,e.parseInline(n))},renderMarkdown:(t,e)=>!t||!Array.isArray(t.content)?"":e.renderChildren(t.content),addCommands(){return{setParagraph:()=>({commands:t})=>t.setNode(this.name)}},addKeyboardShortcuts(){return{"Mod-Alt-0":()=>this.editor.commands.setParagraph()}}}),gh=Hw;var Jl=fl;var yh=ee.create({name:"small",parseHTML(){return[{tag:"small"}]},renderHTML({HTMLAttributes:t}){return["small",t,0]},addCommands(){return{setSmall:()=>({commands:t})=>t.setMark(this.name),toggleSmall:()=>({commands:t})=>t.toggleMark(this.name),unsetSmall:()=>({commands:t})=>t.unsetMark(this.name)}}});var bh=ee.create({name:"textColor",addOptions(){return{textColors:{}}},parseHTML(){return[{tag:"span",getAttrs:t=>t.classList?.contains("color")}]},renderHTML({HTMLAttributes:t}){let e={...t},n=t.class;e.class=["color",n].filter(Boolean).join(" ");let r=t["data-color"],i=(this.options.textColors||{})[r],s=typeof r=="string"&&r.length>0,l=i?`--color: ${i.color}; --dark-color: ${i.darkColor}`:s?`--color: ${r}; --dark-color: ${r}`:null;if(l){let a=typeof t.style=="string"?t.style:"";e.style=a?`${l}; ${a}`:l}return["span",e,0]},addAttributes(){return{"data-color":{default:null,parseHTML:t=>t.getAttribute("data-color"),renderHTML:t=>t["data-color"]?{"data-color":t["data-color"]}:{}}}},addCommands(){return{setTextColor:({color:t})=>({commands:e})=>e.setMark(this.name,{"data-color":t}),unsetTextColor:()=>({commands:t})=>t.unsetMark(this.name)}}});var $w=/(?:^|\s)(~~(?!\s+~~)((?:[^~]+))~~(?!\s+~~))$/,Fw=/(?:^|\s)(~~(?!\s+~~)((?:[^~]+))~~(?!\s+~~))/g,Vw=ee.create({name:"strike",addOptions(){return{HTMLAttributes:{}}},parseHTML(){return[{tag:"s"},{tag:"del"},{tag:"strike"},{style:"text-decoration",consuming:!1,getAttrs:t=>t.includes("line-through")?{}:!1}]},renderHTML({HTMLAttributes:t}){return["s",R(this.options.HTMLAttributes,t),0]},markdownTokenName:"del",parseMarkdown:(t,e)=>e.applyMark("strike",e.parseInline(t.tokens||[])),renderMarkdown:(t,e)=>`~~${e.renderChildren(t)}~~`,addCommands(){return{setStrike:()=>({commands:t})=>t.setMark(this.name),toggleStrike:()=>({commands:t})=>t.toggleMark(this.name),unsetStrike:()=>({commands:t})=>t.unsetMark(this.name)}},addKeyboardShortcuts(){return{"Mod-Shift-s":()=>this.editor.commands.toggleStrike()}},addInputRules(){return[Be({find:$w,type:this.type})]},addPasteRules(){return[Me({find:Fw,type:this.type})]}}),wh=Vw;var _w=ee.create({name:"subscript",addOptions(){return{HTMLAttributes:{}}},parseHTML(){return[{tag:"sub"},{style:"vertical-align",getAttrs(t){return t!=="sub"?!1:null}}]},renderHTML({HTMLAttributes:t}){return["sub",R(this.options.HTMLAttributes,t),0]},addCommands(){return{setSubscript:()=>({commands:t})=>t.setMark(this.name),toggleSubscript:()=>({commands:t})=>t.toggleMark(this.name),unsetSubscript:()=>({commands:t})=>t.unsetMark(this.name)}},addKeyboardShortcuts(){return{"Mod-,":()=>this.editor.commands.toggleSubscript()}}}),xh=_w;var Ww=ee.create({name:"superscript",addOptions(){return{HTMLAttributes:{}}},parseHTML(){return[{tag:"sup"},{style:"vertical-align",getAttrs(t){return t!=="super"?!1:null}}]},renderHTML({HTMLAttributes:t}){return["sup",R(this.options.HTMLAttributes,t),0]},addCommands(){return{setSuperscript:()=>({commands:t})=>t.setMark(this.name),toggleSuperscript:()=>({commands:t})=>t.toggleMark(this.name),unsetSuperscript:()=>({commands:t})=>t.unsetMark(this.name)}},addKeyboardShortcuts(){return{"Mod-.":()=>this.editor.commands.toggleSuperscript()}}}),kh=Ww;var Xl,Yl;if(typeof WeakMap<"u"){let t=new WeakMap;Xl=e=>t.get(e),Yl=(e,n)=>(t.set(e,n),n)}else{let t=[],n=0;Xl=r=>{for(let o=0;o(n==10&&(n=0),t[n++]=r,t[n++]=o)}var oe=class{constructor(t,e,n,r){this.width=t,this.height=e,this.map=n,this.problems=r}findCell(t){for(let e=0;e=n){(i||(i=[])).push({type:"overlong_rowspan",pos:d,n:y-b});break}let C=o+b*e;for(let x=0;xr&&(i+=c.attrs.colspan)}}for(let s=0;s1&&(n=!0)}e==-1?e=i:e!=i&&(e=Math.max(e,i))}return e}function Kw(t,e,n){t.problems||(t.problems=[]);let r={};for(let o=0;o0;e--)if(t.node(e).type.spec.tableRole=="row")return t.node(0).resolve(t.before(e+1));return null}function Jw(t){for(let e=t.depth;e>0;e--){let n=t.node(e).type.spec.tableRole;if(n==="cell"||n==="header_cell")return t.node(e)}return null}function Je(t){let e=t.selection.$head;for(let n=e.depth;n>0;n--)if(e.node(n).type.spec.tableRole=="row")return!0;return!1}function Si(t){let e=t.selection;if("$anchorCell"in e&&e.$anchorCell)return e.$anchorCell.pos>e.$headCell.pos?e.$anchorCell:e.$headCell;if("node"in e&&e.node&&e.node.type.spec.tableRole=="cell")return e.$anchor;let n=an(e.$head)||Gw(e.$head);if(n)return n;throw new RangeError(`No cell found around position ${e.head}`)}function Gw(t){for(let e=t.nodeAfter,n=t.pos;e;e=e.firstChild,n++){let r=e.type.spec.tableRole;if(r=="cell"||r=="header_cell")return t.doc.resolve(n)}for(let e=t.nodeBefore,n=t.pos;e;e=e.lastChild,n--){let r=e.type.spec.tableRole;if(r=="cell"||r=="header_cell")return t.doc.resolve(n-e.nodeSize)}}function Ql(t){return t.parent.type.spec.tableRole=="row"&&!!t.nodeAfter}function Xw(t){return t.node(0).resolve(t.pos+t.nodeAfter.nodeSize)}function ta(t,e){return t.depth==e.depth&&t.pos>=e.start(-1)&&t.pos<=e.end(-1)}function Rh(t,e,n){let r=t.node(-1),o=oe.get(r),i=t.start(-1),s=o.nextCell(t.pos-i,e,n);return s==null?null:t.node(0).resolve(i+s)}function cn(t,e,n=1){let r={...t,colspan:t.colspan-n};return r.colwidth&&(r.colwidth=r.colwidth.slice(),r.colwidth.splice(e,n),r.colwidth.some(o=>o>0)||(r.colwidth=null)),r}function Dh(t,e,n=1){let r={...t,colspan:t.colspan+n};if(r.colwidth){r.colwidth=r.colwidth.slice();for(let o=0;od!=n.pos-i);a.unshift(n.pos-i);let c=a.map(d=>{let u=r.nodeAt(d);if(!u)throw new RangeError(`No cell with offset ${d} found`);let f=i+d+1;return new yn(l.resolve(f),l.resolve(f+u.content.size))});super(c[0].$from,c[0].$to,c),this.$anchorCell=e,this.$headCell=n}map(e,n){let r=e.resolve(n.map(this.$anchorCell.pos)),o=e.resolve(n.map(this.$headCell.pos));if(Ql(r)&&Ql(o)&&ta(r,o)){let i=this.$anchorCell.node(-1)!=r.node(-1);return i&&this.isRowSelection()?Mt.rowSelection(r,o):i&&this.isColSelection()?Mt.colSelection(r,o):new Mt(r,o)}return D.between(r,o)}content(){let e=this.$anchorCell.node(-1),n=oe.get(e),r=this.$anchorCell.start(-1),o=n.rectBetween(this.$anchorCell.pos-r,this.$headCell.pos-r),i={},s=[];for(let a=o.top;a0||g>0){let y=p.attrs;if(m>0&&(y=cn(y,0,m)),g>0&&(y=cn(y,y.colspan-g,g)),h.lefto.bottom){let y={...p.attrs,rowspan:Math.min(h.bottom,o.bottom)-Math.max(h.top,o.top)};h.top0)return!1;let r=e+this.$anchorCell.nodeAfter.attrs.rowspan,o=n+this.$headCell.nodeAfter.attrs.rowspan;return Math.max(r,o)==this.$headCell.node(-1).childCount}static colSelection(e,n=e){let r=e.node(-1),o=oe.get(r),i=e.start(-1),s=o.findCell(e.pos-i),l=o.findCell(n.pos-i),a=e.node(0);return s.top<=l.top?(s.top>0&&(e=a.resolve(i+o.map[s.left])),l.bottom0&&(n=a.resolve(i+o.map[l.left])),s.bottom0)return!1;let s=o+this.$anchorCell.nodeAfter.attrs.colspan,l=i+this.$headCell.nodeAfter.attrs.colspan;return Math.max(s,l)==n.width}eq(e){return e instanceof Mt&&e.$anchorCell.pos==this.$anchorCell.pos&&e.$headCell.pos==this.$headCell.pos}static rowSelection(e,n=e){let r=e.node(-1),o=oe.get(r),i=e.start(-1),s=o.findCell(e.pos-i),l=o.findCell(n.pos-i),a=e.node(0);return s.left<=l.left?(s.left>0&&(e=a.resolve(i+o.map[s.top*o.width])),l.right0&&(n=a.resolve(i+o.map[l.top*o.width])),s.right{e.push(te.node(r,r+n.nodeSize,{class:"selectedCell"}))}),Y.create(t.doc,e)}function ex({$from:t,$to:e}){if(t.pos==e.pos||t.pos=0&&!(t.after(o+1)=0&&!(e.before(i+1)>e.start(i));i--,r--);return n==r&&/row|table/.test(t.node(o).type.spec.tableRole)}function tx({$from:t,$to:e}){let n,r;for(let o=t.depth;o>0;o--){let i=t.node(o);if(i.type.spec.tableRole==="cell"||i.type.spec.tableRole==="header_cell"){n=i;break}}for(let o=e.depth;o>0;o--){let i=e.node(o);if(i.type.spec.tableRole==="cell"||i.type.spec.tableRole==="header_cell"){r=i;break}}return n!==r&&e.parentOffset===0}function nx(t,e,n){let r=(e||t).selection,o=(e||t).doc,i,s;if(r instanceof L&&(s=r.node.type.spec.tableRole)){if(s=="cell"||s=="header_cell")i=X.create(o,r.from);else if(s=="row"){let l=o.resolve(r.from+1);i=X.rowSelection(l,l)}else if(!n){let l=oe.get(r.node),a=r.from+1,c=a+l.map[l.width*l.height-1];i=X.create(o,a+1,c)}}else r instanceof D&&ex(r)?i=D.create(o,r.from):r instanceof D&&tx(r)&&(i=D.create(o,r.$from.start(),r.$from.end()));return i&&(e||(e=t.tr)).setSelection(i),e}var rx=new H("fix-tables");function Ph(t,e,n,r){let o=t.childCount,i=e.childCount;e:for(let s=0,l=0;s{o.type.spec.tableRole=="table"&&(n=ox(t,o,i,n))};return e?e.doc!=t.doc&&Ph(e.doc,t.doc,0,r):t.doc.descendants(r),n}function ox(t,e,n,r){let o=oe.get(e);if(!o.problems)return r;r||(r=t.tr);let i=[];for(let a=0;a0){let h="cell";d.firstChild&&(h=d.firstChild.type.spec.tableRole);let p=[];for(let g=0;g0?-1:0;Yw(e,r,o+i)&&(i=o==0||o==e.width?null:0);for(let s=0;s0&&o0&&e.map[l-1]==a||o0?-1:0;sx(e,r,o+l)&&(l=o==0||o==e.height?null:0);for(let c=0,d=e.width*o;c0&&o0&&u==e.map[d-e.width]){let f=n.nodeAt(u).attrs;t.setNodeMarkup(t.mapping.slice(l).map(u+r),null,{...f,rowspan:f.rowspan-1}),c+=f.colspan-1}else if(o0&&n[i]==n[i-1]||r.right0&&n[o]==n[o-t]||r.bottom0){let d=a+1+c.content.size,u=Sh(c)?a+1:d;i.replaceWith(u+r.tableStart,d+r.tableStart,l)}i.setSelection(new X(i.doc.resolve(a+r.tableStart))),e(i)}return!0}function oa(t,e){let n=xe(t.schema);return cx(({node:r})=>n[r.type.spec.tableRole])(t,e)}function cx(t){return(e,n)=>{let r=e.selection,o,i;if(r instanceof X){if(r.$anchorCell.pos!=r.$headCell.pos)return!1;o=r.$anchorCell.nodeAfter,i=r.$anchorCell.pos}else{var s;if(o=Jw(r.$from),!o)return!1;i=(s=an(r.$from))===null||s===void 0?void 0:s.pos}if(o==null||i==null||o.attrs.colspan==1&&o.attrs.rowspan==1)return!1;if(n){let l=o.attrs,a=[],c=l.colwidth;l.rowspan>1&&(l={...l,rowspan:1}),l.colspan>1&&(l={...l,colspan:1});let d=at(e),u=e.tr;for(let h=0;h{s.attrs[t]!==e&&i.setNodeMarkup(l,null,{...s.attrs,[t]:e})}):i.setNodeMarkup(o.pos,null,{...o.nodeAfter.attrs,[t]:e}),r(i)}return!0}}function dx(t){return function(e,n){if(!Je(e))return!1;if(n){let r=xe(e.schema),o=at(e),i=e.tr,s=o.map.cellsInRect(t=="column"?{left:o.left,top:0,right:o.right,bottom:o.map.height}:t=="row"?{left:0,top:o.top,right:o.map.width,bottom:o.bottom}:o),l=s.map(a=>o.table.nodeAt(a));for(let a=0;a{let h=f+i.tableStart,p=s.doc.nodeAt(h);p&&s.setNodeMarkup(h,u,p.attrs)}),r(s)}return!0}}var cM=Bn("row",{useDeprecatedLogic:!0}),dM=Bn("column",{useDeprecatedLogic:!0}),jh=Bn("cell",{useDeprecatedLogic:!0});function ux(t,e){if(e<0){let n=t.nodeBefore;if(n)return t.pos-n.nodeSize;for(let r=t.index(-1)-1,o=t.before();r>=0;r--){let i=t.node(-1).child(r),s=i.lastChild;if(s)return o-1-s.nodeSize;o-=i.nodeSize}}else{if(t.index()0;r--)if(n.node(r).type.spec.tableRole=="table")return e&&e(t.tr.delete(n.before(r),n.after(r)).scrollIntoView()),!0;return!1}function yi(t,e){let n=t.selection;if(!(n instanceof X))return!1;if(e){let r=t.tr,o=xe(t.schema).cell.createAndFill().content;n.forEachCell((i,s)=>{i.content.eq(o)||r.replace(r.mapping.map(s+1),r.mapping.map(s+i.nodeSize-1),new E(o,0,0))}),r.docChanged&&e(r)}return!0}function fx(t){if(t.size===0)return null;let{content:e,openStart:n,openEnd:r}=t;for(;e.childCount==1&&(n>0&&r>0||e.child(0).type.spec.tableRole=="table");)n--,r--,e=e.child(0).content;let o=e.child(0),i=o.type.spec.tableRole,s=o.type.schema,l=[];if(i=="row")for(let a=0;a=0;s--){let{rowspan:l,colspan:a}=i.child(s).attrs;for(let c=o;c=e.length&&e.push(v.empty),n[o]r&&(f=f.type.createChecked(cn(f.attrs,f.attrs.colspan,d+f.attrs.colspan-r),f.content)),c.push(f),d+=f.attrs.colspan;for(let h=1;ho&&(u=u.type.create({...u.attrs,rowspan:Math.max(1,o-u.attrs.rowspan)},u.content)),a.push(u)}i.push(v.from(a))}n=i,e=o}return{width:t,height:e,rows:n}}function mx(t,e,n,r,o,i,s){let l=t.doc.type.schema,a=xe(l),c,d;if(o>e.width)for(let u=0,f=0;ue.height){let u=[];for(let p=0,m=(e.height-1)*e.width;p=e.width?!1:n.nodeAt(e.map[m+p]).type==a.header_cell;u.push(g?d||(d=a.header_cell.createAndFill()):c||(c=a.cell.createAndFill()))}let f=a.row.create(null,v.from(u)),h=[];for(let p=e.height;p{if(!o)return!1;let i=n.selection;if(i instanceof X)return xi(n,r,I.near(i.$headCell,e));if(t!="horiz"&&!i.empty)return!1;let s=Kh(o,t,e);if(s==null)return!1;if(t=="horiz")return xi(n,r,I.near(n.doc.resolve(i.head+e),e));{let l=n.doc.resolve(s),a=Rh(l,t,e),c;return a?c=I.near(a,1):e<0?c=I.near(n.doc.resolve(l.before(-1)),-1):c=I.near(n.doc.resolve(l.after(-1)),1),xi(n,r,c)}}}function wi(t,e){return(n,r,o)=>{if(!o)return!1;let i=n.selection,s;if(i instanceof X)s=i;else{let a=Kh(o,t,e);if(a==null)return!1;s=new X(n.doc.resolve(a))}let l=Rh(s.$headCell,t,e);return l?xi(n,r,new X(s.$anchorCell,l)):!1}}function yx(t,e){let n=t.state.doc,r=an(n.resolve(e));return r?(t.dispatch(t.state.tr.setSelection(new X(r))),!0):!1}function bx(t,e,n){if(!Je(t.state))return!1;let r=fx(n),o=t.state.selection;if(o instanceof X){r||(r={width:1,height:1,rows:[v.from(Zl(xe(t.state.schema).cell,n))]});let i=o.$anchorCell.node(-1),s=o.$anchorCell.start(-1),l=oe.get(i).rectBetween(o.$anchorCell.pos-s,o.$headCell.pos-s);return r=px(r,l.right-l.left,l.bottom-l.top),Th(t.state,t.dispatch,s,l,r),!0}else if(r){let i=Si(t.state),s=i.start(-1);return Th(t.state,t.dispatch,s,oe.get(i.node(-1)).findCell(i.pos-s),r),!0}else return!1}function wx(t,e){var n;if(e.button!=0||e.ctrlKey||e.metaKey)return;let r=Ah(t,e.target),o;if(e.shiftKey&&t.state.selection instanceof X)i(t.state.selection.$anchorCell,e),e.preventDefault();else if(e.shiftKey&&r&&(o=an(t.state.selection.$anchor))!=null&&((n=Gl(t,e))===null||n===void 0?void 0:n.pos)!=o.pos)i(o,e),e.preventDefault();else if(!r)return;function i(a,c){let d=Gl(t,c),u=zt.getState(t.state)==null;if(!d||!ta(a,d))if(u)d=a;else return;let f=new X(a,d);if(u||!t.state.selection.eq(f)){let h=t.state.tr.setSelection(f);u&&h.setMeta(zt,a.pos),t.dispatch(h)}}function s(){t.root.removeEventListener("mouseup",s),t.root.removeEventListener("dragstart",s),t.root.removeEventListener("mousemove",l),zt.getState(t.state)!=null&&t.dispatch(t.state.tr.setMeta(zt,-1))}function l(a){let c=a,d=zt.getState(t.state),u;if(d!=null)u=t.state.doc.resolve(d);else if(Ah(t,c.target)!=r&&(u=Gl(t,e),!u))return s();u&&i(u,c)}t.root.addEventListener("mouseup",s),t.root.addEventListener("dragstart",s),t.root.addEventListener("mousemove",l)}function Kh(t,e,n){if(!(t.state.selection instanceof D))return null;let{$head:r}=t.state.selection;for(let o=r.depth-1;o>=0;o--){let i=r.node(o);if((n<0?r.index(o):r.indexAfter(o))!=(n<0?0:i.childCount))return null;if(i.type.spec.tableRole=="cell"||i.type.spec.tableRole=="header_cell"){let s=r.before(o),l=e=="vert"?n>0?"down":"up":n>0?"right":"left";return t.endOfTextblock(l)?s:null}}return null}function Ah(t,e){for(;e&&e!=t.dom;e=e.parentNode)if(e.nodeName=="TD"||e.nodeName=="TH")return e;return null}function Gl(t,e){let n=t.posAtCoords({left:e.clientX,top:e.clientY});if(!n)return null;let{inside:r,pos:o}=n;return r>=0&&an(t.state.doc.resolve(r))||an(t.state.doc.resolve(o))}var xx=class{constructor(t,e){this.node=t,this.defaultCellMinWidth=e,this.dom=document.createElement("div"),this.dom.className="tableWrapper",this.table=this.dom.appendChild(document.createElement("table")),this.table.style.setProperty("--default-cell-min-width",`${e}px`),this.colgroup=this.table.appendChild(document.createElement("colgroup")),ea(t,this.colgroup,this.table,e),this.contentDOM=this.table.appendChild(document.createElement("tbody"))}update(t){return t.type!=this.node.type?!1:(this.node=t,ea(t,this.colgroup,this.table,this.defaultCellMinWidth),!0)}ignoreMutation(t){return t.type=="attributes"&&(t.target==this.table||this.colgroup.contains(t.target))}};function ea(t,e,n,r,o,i){let s=0,l=!0,a=e.firstChild,c=t.firstChild;if(c){for(let u=0,f=0;unew r(u,n,f)),new kx(-1,!1)},apply(s,l){return l.apply(s)}},props:{attributes:s=>{let l=Oe.getState(s);return l&&l.activeHandle>-1?{class:"resize-cursor"}:{}},handleDOMEvents:{mousemove:(s,l)=>{Sx(s,l,t,o)},mouseleave:s=>{Cx(s)},mousedown:(s,l)=>{vx(s,l,e,n)}},decorations:s=>{let l=Oe.getState(s);if(l&&l.activeHandle>-1)return Nx(s,l.activeHandle)},nodeViews:{}}});return i}var kx=class ki{constructor(e,n){this.activeHandle=e,this.dragging=n}apply(e){let n=this,r=e.getMeta(Oe);if(r&&r.setHandle!=null)return new ki(r.setHandle,!1);if(r&&r.setDragging!==void 0)return new ki(n.activeHandle,r.setDragging);if(n.activeHandle>-1&&e.docChanged){let o=e.mapping.map(n.activeHandle,-1);return Ql(e.doc.resolve(o))||(o=-1),new ki(o,n.dragging)}return n}};function Sx(t,e,n,r){if(!t.editable)return;let o=Oe.getState(t.state);if(o&&!o.dragging){let i=Tx(e.target),s=-1;if(i){let{left:l,right:a}=i.getBoundingClientRect();e.clientX-l<=n?s=Eh(t,e,"left",n):a-e.clientX<=n&&(s=Eh(t,e,"right",n))}if(s!=o.activeHandle){if(!r&&s!==-1){let l=t.state.doc.resolve(s),a=l.node(-1),c=oe.get(a),d=l.start(-1);if(c.colCount(l.pos-d)+l.nodeAfter.attrs.colspan-1==c.width-1)return}Jh(t,s)}}}function Cx(t){if(!t.editable)return;let e=Oe.getState(t.state);e&&e.activeHandle>-1&&!e.dragging&&Jh(t,-1)}function vx(t,e,n,r){var o;if(!t.editable)return!1;let i=(o=t.dom.ownerDocument.defaultView)!==null&&o!==void 0?o:window,s=Oe.getState(t.state);if(!s||s.activeHandle==-1||s.dragging)return!1;let l=t.state.doc.nodeAt(s.activeHandle),a=Mx(t,s.activeHandle,l.attrs);t.dispatch(t.state.tr.setMeta(Oe,{setDragging:{startX:e.clientX,startWidth:a}}));function c(u){i.removeEventListener("mouseup",c),i.removeEventListener("mousemove",d);let f=Oe.getState(t.state);f?.dragging&&(Ax(t,f.activeHandle,Nh(f.dragging,u,n)),t.dispatch(t.state.tr.setMeta(Oe,{setDragging:null})))}function d(u){if(!u.which)return c(u);let f=Oe.getState(t.state);if(f&&f.dragging){let h=Nh(f.dragging,u,n);Oh(t,f.activeHandle,h,r)}}return Oh(t,s.activeHandle,a,r),i.addEventListener("mouseup",c),i.addEventListener("mousemove",d),e.preventDefault(),!0}function Mx(t,e,{colspan:n,colwidth:r}){let o=r&&r[r.length-1];if(o)return o;let i=t.domAtPos(e),s=i.node.childNodes[i.offset].offsetWidth,l=n;if(r)for(let a=0;a{var e,n;let r=t.getAttribute("colwidth"),o=r?r.split(",").map(i=>parseInt(i,10)):null;if(!o){let i=(e=t.closest("table"))==null?void 0:e.querySelectorAll("colgroup > col"),s=Array.from(((n=t.parentElement)==null?void 0:n.children)||[]).indexOf(t);if(s&&s>-1&&i&&i[s]){let l=i[s].getAttribute("width");return l?[parseInt(l,10)]:null}}return o}}}},tableRole:"cell",isolating:!0,parseHTML(){return[{tag:"td"}]},renderHTML({HTMLAttributes:t}){return["td",R(this.options.HTMLAttributes,t),0]}}),Rx=F.create({name:"tableHeader",addOptions(){return{HTMLAttributes:{}}},content:"block+",addAttributes(){return{colspan:{default:1},rowspan:{default:1},colwidth:{default:null,parseHTML:t=>{let e=t.getAttribute("colwidth");return e?e.split(",").map(r=>parseInt(r,10)):null}}}},tableRole:"header_cell",isolating:!0,parseHTML(){return[{tag:"th"}]},renderHTML({HTMLAttributes:t}){return["th",R(this.options.HTMLAttributes,t),0]}}),Dx=F.create({name:"tableRow",addOptions(){return{HTMLAttributes:{}}},content:"(tableCell | tableHeader)*",tableRole:"row",parseHTML(){return[{tag:"tr"}]},renderHTML({HTMLAttributes:t}){return["tr",R(this.options.HTMLAttributes,t),0]}});function sa(t,e){return e?["width",`${Math.max(e,t)}px`]:["min-width",`${t}px`]}function Xh(t,e,n,r,o,i){var s;let l=0,a=!0,c=e.firstChild,d=t.firstChild;if(d!==null)for(let f=0,h=0;f{let r=t.nodes[n];r.spec.tableRole&&(e[r.spec.tableRole]=r)}),t.cached.tableNodeTypes=e,e}function Bx(t,e,n,r,o){let i=Lx(t),s=[],l=[];for(let c=0;c{let{selection:e}=t.state;if(!zx(e))return!1;let n=0,r=Gs(e.ranges[0].$from,i=>i.type.name==="table");return r?.node.descendants(i=>{if(i.type.name==="table")return!1;["tableCell","tableHeader"].includes(i.type.name)&&(n+=1)}),n===e.ranges.length?(t.commands.deleteTable(),!0):!1},Hx="";function $x(t){return(t||"").replace(/\s+/g," ").trim()}function Fx(t,e,n={}){var r;let o=(r=n.cellLineSeparator)!=null?r:Hx;if(!t||!t.content||t.content.length===0)return"";let i=[];t.content.forEach(p=>{let m=[];p.content&&p.content.forEach(g=>{let y="";g.content&&Array.isArray(g.content)&&g.content.length>1?y=g.content.map(x=>e.renderChildren(x)).join(o):y=g.content?e.renderChildren(g.content):"";let w=$x(y),b=g.type==="tableHeader";m.push({text:w,isHeader:b})}),i.push(m)});let s=i.reduce((p,m)=>Math.max(p,m.length),0);if(s===0)return"";let l=new Array(s).fill(0);i.forEach(p=>{var m;for(let g=0;gl[g]&&(l[g]=w),l[g]<3&&(l[g]=3)}});let a=(p,m)=>p+" ".repeat(Math.max(0,m-p.length)),c=i[0],d=c.some(p=>p.isHeader),u=` +`,f=new Array(s).fill(0).map((p,m)=>d&&c[m]&&c[m].text||"");return u+=`| ${f.map((p,m)=>a(p,l[m])).join(" | ")} | +`,u+=`| ${l.map(p=>"-".repeat(Math.max(3,p))).join(" | ")} | +`,(d?i.slice(1):i).forEach(p=>{u+=`| ${new Array(s).fill(0).map((m,g)=>a(p[g]&&p[g].text||"",l[g])).join(" | ")} | +`}),u}var Vx=Fx,_x=F.create({name:"table",addOptions(){return{HTMLAttributes:{},resizable:!1,renderWrapper:!1,handleWidth:5,cellMinWidth:25,View:Ix,lastColumnResizable:!0,allowTableNodeSelection:!1}},content:"tableRow+",tableRole:"table",isolating:!0,group:"block",parseHTML(){return[{tag:"table"}]},renderHTML({node:t,HTMLAttributes:e}){let{colgroup:n,tableWidth:r,tableMinWidth:o}=Px(t,this.options.cellMinWidth),i=e.style;function s(){return i||(r?`width: ${r}`:`min-width: ${o}`)}let l=["table",R(this.options.HTMLAttributes,e,{style:s()}),n,["tbody",0]];return this.options.renderWrapper?["div",{class:"tableWrapper"},l]:l},parseMarkdown:(t,e)=>{let n=[];if(t.header){let r=[];t.header.forEach(o=>{r.push(e.createNode("tableHeader",{},[{type:"paragraph",content:e.parseInline(o.tokens)}]))}),n.push(e.createNode("tableRow",{},r))}return t.rows&&t.rows.forEach(r=>{let o=[];r.forEach(i=>{o.push(e.createNode("tableCell",{},[{type:"paragraph",content:e.parseInline(i.tokens)}]))}),n.push(e.createNode("tableRow",{},o))}),e.createNode("table",void 0,n)},renderMarkdown:(t,e)=>Vx(t,e),addCommands(){return{insertTable:({rows:t=3,cols:e=3,withHeaderRow:n=!0}={})=>({tr:r,dispatch:o,editor:i})=>{let s=Bx(i.schema,t,e,n);if(o){let l=r.selection.from+1;r.replaceSelectionWith(s).scrollIntoView().setSelection(D.near(r.doc.resolve(l)))}return!0},addColumnBefore:()=>({state:t,dispatch:e})=>Bh(t,e),addColumnAfter:()=>({state:t,dispatch:e})=>zh(t,e),deleteColumn:()=>({state:t,dispatch:e})=>Hh(t,e),addRowBefore:()=>({state:t,dispatch:e})=>Fh(t,e),addRowAfter:()=>({state:t,dispatch:e})=>Vh(t,e),deleteRow:()=>({state:t,dispatch:e})=>_h(t,e),deleteTable:()=>({state:t,dispatch:e})=>Uh(t,e),mergeCells:()=>({state:t,dispatch:e})=>ra(t,e),splitCell:()=>({state:t,dispatch:e})=>oa(t,e),toggleHeaderColumn:()=>({state:t,dispatch:e})=>Bn("column")(t,e),toggleHeaderRow:()=>({state:t,dispatch:e})=>Bn("row")(t,e),toggleHeaderCell:()=>({state:t,dispatch:e})=>jh(t,e),mergeOrSplit:()=>({state:t,dispatch:e})=>ra(t,e)?!0:oa(t,e),setCellAttribute:(t,e)=>({state:n,dispatch:r})=>Wh(t,e)(n,r),goToNextCell:()=>({state:t,dispatch:e})=>ia(1)(t,e),goToPreviousCell:()=>({state:t,dispatch:e})=>ia(-1)(t,e),fixTables:()=>({state:t,dispatch:e})=>(e&&na(t),!0),setCellSelection:t=>({tr:e,dispatch:n})=>{if(n){let r=X.create(e.doc,t.anchorCell,t.headCell);e.setSelection(r)}return!0}}},addKeyboardShortcuts(){return{Tab:()=>this.editor.commands.goToNextCell()?!0:this.editor.can().addRowAfter()?this.editor.chain().addRowAfter().goToNextCell().run():!1,"Shift-Tab":()=>this.editor.commands.goToPreviousCell(),Backspace:Ci,"Mod-Backspace":Ci,Delete:Ci,"Mod-Delete":Ci}},addProseMirrorPlugins(){return[...this.options.resizable&&this.editor.isEditable?[qh({handleWidth:this.options.handleWidth,cellMinWidth:this.options.cellMinWidth,defaultCellMinWidth:this.options.cellMinWidth,View:this.options.View,lastColumnResizable:this.options.lastColumnResizable})]:[],Gh({allowTableNodeSelection:this.options.allowTableNodeSelection})]},extendNodeSchema(t){let e={name:t.name,options:t.options,storage:t.storage};return{tableRole:G(B(t,"tableRole",e))}}}),Qh=U.create({name:"tableKit",addExtensions(){let t=[];return this.options.table!==!1&&t.push(_x.configure(this.options.table)),this.options.tableCell!==!1&&t.push(Ox.configure(this.options.tableCell)),this.options.tableHeader!==!1&&t.push(Rx.configure(this.options.tableHeader)),this.options.tableRow!==!1&&t.push(Dx.configure(this.options.tableRow)),t}});var Wx=F.create({name:"text",group:"inline",parseMarkdown:t=>({type:"text",text:t.text||""}),renderMarkdown:t=>t.text||""}),Zh=Wx;var jx=U.create({name:"textAlign",addOptions(){return{types:[],alignments:["left","center","right","justify"],defaultAlignment:null}},addGlobalAttributes(){return[{types:this.options.types,attributes:{textAlign:{default:this.options.defaultAlignment,parseHTML:t=>{let e=t.style.textAlign;return this.options.alignments.includes(e)?e:this.options.defaultAlignment},renderHTML:t=>t.textAlign?{style:`text-align: ${t.textAlign}`}:{}}}}]},addCommands(){return{setTextAlign:t=>({commands:e})=>this.options.alignments.includes(t)?this.options.types.map(n=>e.updateAttributes(n,{textAlign:t})).some(n=>n):!1,unsetTextAlign:()=>({commands:t})=>this.options.types.map(e=>t.resetAttributes(e,"textAlign")).some(e=>e),toggleTextAlign:t=>({editor:e,commands:n})=>this.options.alignments.includes(t)?e.isActive({textAlign:t})?n.unsetTextAlign():n.setTextAlign(t):!1}},addKeyboardShortcuts(){return{"Mod-Shift-l":()=>this.editor.commands.setTextAlign("left"),"Mod-Shift-e":()=>this.editor.commands.setTextAlign("center"),"Mod-Shift-r":()=>this.editor.commands.setTextAlign("right"),"Mod-Shift-j":()=>this.editor.commands.setTextAlign("justify")}}}),ep=jx;var Ux=ee.create({name:"underline",addOptions(){return{HTMLAttributes:{}}},parseHTML(){return[{tag:"u"},{style:"text-decoration",consuming:!1,getAttrs:t=>t.includes("underline")?{}:!1}]},renderHTML({HTMLAttributes:t}){return["u",R(this.options.HTMLAttributes,t),0]},parseMarkdown(t,e){return e.applyMark(this.name||"underline",e.parseInline(t.tokens||[]))},renderMarkdown(t,e){return`++${e.renderChildren(t)}++`},markdownTokenizer:{name:"underline",level:"inline",start(t){return t.indexOf("++")},tokenize(t,e,n){let o=/^(\+\+)([\s\S]+?)(\+\+)/.exec(t);if(!o)return;let i=o[2].trim();return{type:"underline",raw:o[0],text:i,tokens:n.inlineTokens(i)}}},addCommands(){return{setUnderline:()=>({commands:t})=>t.setMark(this.name),toggleUnderline:()=>({commands:t})=>t.toggleMark(this.name),unsetUnderline:()=>({commands:t})=>t.unsetMark(this.name)}},addKeyboardShortcuts(){return{"Mod-u":()=>this.editor.commands.toggleUnderline(),"Mod-U":()=>this.editor.commands.toggleUnderline()}}}),tp=Ux;var np=(t,e)=>{Ln({getBoundingClientRect:()=>{let{from:r,to:o}=t.state.selection,i=t.view.coordsAtPos(r),s=t.view.coordsAtPos(o);return{top:Math.min(i.top,s.top),bottom:Math.max(i.bottom,s.bottom),left:Math.min(i.left,s.left),right:Math.max(i.right,s.right),width:Math.abs(s.right-i.left),height:Math.abs(s.bottom-i.top),x:Math.min(i.left,s.left),y:Math.min(i.top,s.top)}}},e,{placement:"bottom-start",strategy:"absolute",middleware:[In(),Pn()]}).then(({x:r,y:o,strategy:i})=>{e.style.width="max-content",e.style.position=i,e.style.left=`${r}px`,e.style.top=`${o}px`})},rp=({mergeTags:t,noMergeTagSearchResultsMessage:e})=>({items:({query:n})=>Object.entries(t).filter(([r,o])=>r.toLowerCase().replace(/\s/g,"").includes(n.toLowerCase())||o.toLowerCase().replace(/\s/g,"").includes(n.toLowerCase())).map(([r,o])=>({id:r,label:o})),render:()=>{let n,r=0,o=null,i=()=>{let f=document.createElement("div");return f.className="fi-dropdown-panel fi-dropdown-list",f.style.minWidth="12rem",f},s=()=>{if(!n||!o)return;let f=o.items||[];if(n.innerHTML="",f.length)f.forEach((h,p)=>{let m=document.createElement("button");m.className=`fi-dropdown-list-item fi-dropdown-list-item-label ${p===r?"fi-selected":""}`,m.textContent=h.label,m.type="button",m.addEventListener("click",()=>l(p)),n.appendChild(m)});else{let h=document.createElement("div");h.className="fi-dropdown-header";let p=document.createElement("span");p.style.whiteSpace="normal",p.textContent=e,h.appendChild(p),n.appendChild(h)}},l=f=>{if(!o)return;let p=(o.items||[])[f];p&&o.command({id:p.id})},a=()=>{if(!n||!o||o.items.length===0)return;let f=n.children[r];if(f){let h=f.getBoundingClientRect(),p=n.getBoundingClientRect();(h.topp.bottom)&&f.scrollIntoView({block:"nearest"})}},c=()=>{if(!o)return;let f=o.items||[];f.length!==0&&(r=(r+f.length-1)%f.length,s(),a())},d=()=>{if(!o)return;let f=o.items||[];f.length!==0&&(r=(r+1)%f.length,s(),a())},u=()=>{l(r)};return{onStart:f=>{o=f,r=0,n=i(),n.style.position="absolute",n.style.zIndex="50",s(),document.body.appendChild(n),f.clientRect&&np(f.editor,n)},onUpdate:f=>{o=f,r=0,s(),a(),f.clientRect&&np(f.editor,n)},onKeyDown:f=>f.event.key==="Escape"?(n&&n.parentNode&&n.parentNode.removeChild(n),!0):f.event.key==="ArrowUp"?(c(),!0):f.event.key==="ArrowDown"?(d(),!0):f.event.key==="Enter"?(u(),!0):!1,onExit:()=>{n&&n.parentNode&&n.parentNode.removeChild(n)}}}});var op=async({$wire:t,acceptedFileTypes:e,acceptedFileTypesValidationMessage:n,canAttachFiles:r,customExtensionUrls:o,deleteCustomBlockButtonIconHtml:i,editCustomBlockButtonIconHtml:s,editCustomBlockUsing:l,getMentionLabelsUsing:a,getMentionSearchResultsUsing:c,hasResizableImages:d,insertCustomBlockUsing:u,key:f,linkProtocols:h,maxFileSize:p,maxFileSizeValidationMessage:m,mentions:g,mergeTags:y,noMergeTagSearchResultsMessage:w,placeholder:b,statePath:C,textColors:x,uploadingFileMessage:S})=>{let k=[Du,Iu,$l,Pu,Lu,Bu.configure({deleteCustomBlockButtonIconHtml:i,editCustomBlockButtonIconHtml:s,editCustomBlockUsing:l,insertCustomBlockUsing:u}),Hu,Fu,$u,Vu,Nu.configure({class:"fi-not-prose"}),Ou,_u,Wu,ju,Uu,Ku,qu,Ju,Xu.configure({inline:!0,resize:{enabled:d,alwaysPreserveAspectRatio:!0,allowBase64:!0}}),Yu,pf.configure({autolink:!0,openOnClick:!1,protocols:h}),Fl,...r?[Af.configure({acceptedTypes:e,acceptedTypesValidationMessage:n,get$WireUsing:()=>t,key:f,maxSize:p,maxSizeValidationMessage:m,statePath:C,uploadingMessage:S})]:[],...Object.keys(y).length?[Ef.configure({deleteTriggerWithBackspace:!0,suggestion:rp({mergeTags:y,noMergeTagSearchResultsMessage:w}),mergeTags:y})]:[],...g.length?[mh.configure({HTMLAttributes:{class:"fi-fo-rich-editor-mention"},suggestions:g,getMentionSearchResultsUsing:c,getMentionLabelsUsing:a})]:[],_l,gh,Jl.configure({placeholder:b}),bh.configure({textColors:x}),yh,wh,xh,kh,Qh.configure({table:{resizable:!0}}),Zh,ep.configure({types:["heading","paragraph"],alignments:["start","center","end","justify"],defaultAlignment:"start"}),tp,Ru],O=await Promise.all(o.map(async T=>{new RegExp("^(?:[a-z+]+:)?//","i").test(T)||(T=new URL(T,document.baseURI).href);try{let $=(await import(T)).default;return typeof $=="function"?$():$}catch($){return console.error(`Failed to load rich editor custom extension from [${T}]:`,$),null}}));for(let T of O){if(!T||!T.name)continue;let A=k.findIndex($=>$.name===T.name);T.name==="placeholder"&&T.parent===null&&(T=Jl.configure(T.options)),A!==-1?k[A]=T:k.push(T)}return k};function Kx(t,e){let n=Math.min(t.top,e.top),r=Math.max(t.bottom,e.bottom),o=Math.min(t.left,e.left),s=Math.max(t.right,e.right)-o,l=r-n,a=o,c=n;return new DOMRect(a,c,s,l)}var qx=class{constructor({editor:t,element:e,view:n,updateDelay:r=250,resizeDelay:o=60,shouldShow:i,appendTo:s,getReferencedVirtualElement:l,options:a}){this.preventHide=!1,this.isVisible=!1,this.scrollTarget=window,this.floatingUIOptions={strategy:"absolute",placement:"top",offset:8,flip:{},shift:{},arrow:!1,size:!1,autoPlacement:!1,hide:!1,inline:!1,onShow:void 0,onHide:void 0,onUpdate:void 0,onDestroy:void 0},this.shouldShow=({view:d,state:u,from:f,to:h})=>{let{doc:p,selection:m}=u,{empty:g}=m,y=!p.textBetween(f,h).length&&fo(u.selection),w=this.element.contains(document.activeElement);return!(!(d.hasFocus()||w)||g||y||!this.editor.isEditable)},this.mousedownHandler=()=>{this.preventHide=!0},this.dragstartHandler=()=>{this.hide()},this.resizeHandler=()=>{this.resizeDebounceTimer&&clearTimeout(this.resizeDebounceTimer),this.resizeDebounceTimer=window.setTimeout(()=>{this.updatePosition()},this.resizeDelay)},this.focusHandler=()=>{setTimeout(()=>this.update(this.editor.view))},this.blurHandler=({event:d})=>{var u;if(this.editor.isDestroyed){this.destroy();return}if(this.preventHide){this.preventHide=!1;return}d?.relatedTarget&&((u=this.element.parentNode)!=null&&u.contains(d.relatedTarget))||d?.relatedTarget!==this.editor.view.dom&&this.hide()},this.handleDebouncedUpdate=(d,u)=>{let f=!u?.selection.eq(d.state.selection),h=!u?.doc.eq(d.state.doc);!f&&!h||(this.updateDebounceTimer&&clearTimeout(this.updateDebounceTimer),this.updateDebounceTimer=window.setTimeout(()=>{this.updateHandler(d,f,h,u)},this.updateDelay))},this.updateHandler=(d,u,f,h)=>{let{composing:p}=d;if(p||!u&&!f)return;if(!this.getShouldShow(h)){this.hide();return}this.updatePosition(),this.show()},this.transactionHandler=({transaction:d})=>{d.getMeta("bubbleMenu")==="updatePosition"&&this.updatePosition()};var c;this.editor=t,this.element=e,this.view=n,this.updateDelay=r,this.resizeDelay=o,this.appendTo=s,this.scrollTarget=(c=a?.scrollTarget)!=null?c:window,this.getReferencedVirtualElement=l,this.floatingUIOptions={...this.floatingUIOptions,...a},this.element.tabIndex=0,i&&(this.shouldShow=i),this.element.addEventListener("mousedown",this.mousedownHandler,{capture:!0}),this.view.dom.addEventListener("dragstart",this.dragstartHandler),this.editor.on("focus",this.focusHandler),this.editor.on("blur",this.blurHandler),this.editor.on("transaction",this.transactionHandler),window.addEventListener("resize",this.resizeHandler),this.scrollTarget.addEventListener("scroll",this.resizeHandler),this.update(n,n.state),this.getShouldShow()&&(this.show(),this.updatePosition())}get middlewares(){let t=[];return this.floatingUIOptions.flip&&t.push(Pn(typeof this.floatingUIOptions.flip!="boolean"?this.floatingUIOptions.flip:void 0)),this.floatingUIOptions.shift&&t.push(In(typeof this.floatingUIOptions.shift!="boolean"?this.floatingUIOptions.shift:void 0)),this.floatingUIOptions.offset&&t.push(lh(typeof this.floatingUIOptions.offset!="boolean"?this.floatingUIOptions.offset:void 0)),this.floatingUIOptions.arrow&&t.push(uh(this.floatingUIOptions.arrow)),this.floatingUIOptions.size&&t.push(ch(typeof this.floatingUIOptions.size!="boolean"?this.floatingUIOptions.size:void 0)),this.floatingUIOptions.autoPlacement&&t.push(ah(typeof this.floatingUIOptions.autoPlacement!="boolean"?this.floatingUIOptions.autoPlacement:void 0)),this.floatingUIOptions.hide&&t.push(dh(typeof this.floatingUIOptions.hide!="boolean"?this.floatingUIOptions.hide:void 0)),this.floatingUIOptions.inline&&t.push(fh(typeof this.floatingUIOptions.inline!="boolean"?this.floatingUIOptions.inline:void 0)),t}get virtualElement(){var t;let{selection:e}=this.editor.state,n=(t=this.getReferencedVirtualElement)==null?void 0:t.call(this);if(n)return n;let r=tu(this.view,e.from,e.to),o={getBoundingClientRect:()=>r,getClientRects:()=>[r]};if(e instanceof L){let i=this.view.nodeDOM(e.from),s=i.dataset.nodeViewWrapper?i:i.querySelector("[data-node-view-wrapper]");s&&(i=s),i&&(o={getBoundingClientRect:()=>i.getBoundingClientRect(),getClientRects:()=>[i.getBoundingClientRect()]})}if(e instanceof X){let{$anchorCell:i,$headCell:s}=e,l=i?i.pos:s.pos,a=s?s.pos:i.pos,c=this.view.nodeDOM(l),d=this.view.nodeDOM(a);if(!c||!d)return;let u=c===d?c.getBoundingClientRect():Kx(c.getBoundingClientRect(),d.getBoundingClientRect());o={getBoundingClientRect:()=>u,getClientRects:()=>[u]}}return o}updatePosition(){let t=this.virtualElement;t&&Ln(t,this.element,{placement:this.floatingUIOptions.placement,strategy:this.floatingUIOptions.strategy,middleware:this.middlewares}).then(({x:e,y:n,strategy:r})=>{this.element.style.width="max-content",this.element.style.position=r,this.element.style.left=`${e}px`,this.element.style.top=`${n}px`,this.isVisible&&this.floatingUIOptions.onUpdate&&this.floatingUIOptions.onUpdate()})}update(t,e){let{state:n}=t,r=n.selection.from!==n.selection.to;if(this.updateDelay>0&&r){this.handleDebouncedUpdate(t,e);return}let o=!e?.selection.eq(t.state.selection),i=!e?.doc.eq(t.state.doc);this.updateHandler(t,o,i,e)}getShouldShow(t){var e;let{state:n}=this.view,{selection:r}=n,{ranges:o}=r,i=Math.min(...o.map(a=>a.$from.pos)),s=Math.max(...o.map(a=>a.$to.pos));return((e=this.shouldShow)==null?void 0:e.call(this,{editor:this.editor,element:this.element,view:this.view,state:n,oldState:t,from:i,to:s}))||!1}show(){var t;if(this.isVisible)return;this.element.style.visibility="visible",this.element.style.opacity="1";let e=typeof this.appendTo=="function"?this.appendTo():this.appendTo;(t=e??this.view.dom.parentElement)==null||t.appendChild(this.element),this.floatingUIOptions.onShow&&this.floatingUIOptions.onShow(),this.isVisible=!0}hide(){this.isVisible&&(this.element.style.visibility="hidden",this.element.style.opacity="0",this.element.remove(),this.floatingUIOptions.onHide&&this.floatingUIOptions.onHide(),this.isVisible=!1)}destroy(){this.hide(),this.element.removeEventListener("mousedown",this.mousedownHandler,{capture:!0}),this.view.dom.removeEventListener("dragstart",this.dragstartHandler),window.removeEventListener("resize",this.resizeHandler),this.scrollTarget.removeEventListener("scroll",this.resizeHandler),this.editor.off("focus",this.focusHandler),this.editor.off("blur",this.blurHandler),this.editor.off("transaction",this.transactionHandler),this.floatingUIOptions.onDestroy&&this.floatingUIOptions.onDestroy()}},la=t=>new P({key:typeof t.pluginKey=="string"?new H(t.pluginKey):t.pluginKey,view:e=>new qx({view:e,...t})}),kT=U.create({name:"bubbleMenu",addOptions(){return{element:null,pluginKey:"bubbleMenu",updateDelay:void 0,appendTo:void 0,shouldShow:null}},addProseMirrorPlugins(){return this.options.element?[la({pluginKey:this.options.pluginKey,editor:this.editor,element:this.options.element,updateDelay:this.options.updateDelay,options:this.options.options,appendTo:this.options.appendTo,getReferencedVirtualElement:this.options.getReferencedVirtualElement,shouldShow:this.options.shouldShow})]:[]}});function Jx({acceptedFileTypes:t,acceptedFileTypesValidationMessage:e,activePanel:n,canAttachFiles:r,deleteCustomBlockButtonIconHtml:o,editCustomBlockButtonIconHtml:i,extensions:s,floatingToolbars:l,hasResizableImages:a,isDisabled:c,isLiveDebounced:d,isLiveOnBlur:u,key:f,linkProtocols:h,liveDebounce:p,livewireId:m,maxFileSize:g,maxFileSizeValidationMessage:y,mergeTags:w,mentions:b,getMentionSearchResultsUsing:C,getMentionLabelsUsing:x,noMergeTagSearchResultsMessage:S,placeholder:k,state:O,statePath:T,textColors:A,uploadingFileMessage:$}){let z,K=[],V=!1;return{state:O,activePanel:n,editorSelection:{type:"text",anchor:1,head:1},isUploadingFile:!1,fileValidationMessage:null,shouldUpdateState:!0,editorUpdatedAt:Date.now(),async init(){z=new gu({editable:!c,element:this.$refs.editor,extensions:await op({acceptedFileTypes:t,acceptedFileTypesValidationMessage:e,canAttachFiles:r,customExtensionUrls:s,deleteCustomBlockButtonIconHtml:o,editCustomBlockButtonIconHtml:i,editCustomBlockUsing:(q,We)=>this.$wire.mountAction("customBlock",{editorSelection:this.editorSelection,id:q,config:We,mode:"edit"},{schemaComponent:f}),floatingToolbars:l,hasResizableImages:a,insertCustomBlockUsing:(q,We=null)=>this.$wire.mountAction("customBlock",{id:q,dragPosition:We,mode:"insert"},{schemaComponent:f}),key:f,linkProtocols:h,maxFileSize:g,maxFileSizeValidationMessage:y,mergeTags:w,mentions:b,getMentionSearchResultsUsing:C,getMentionLabelsUsing:x,noMergeTagSearchResultsMessage:S,placeholder:k,statePath:T,textColors:A,uploadingFileMessage:$,$wire:this.$wire}),content:this.state});let N="paragraph"in l;Object.keys(l).forEach(q=>{let We=this.$refs[`floatingToolbar::${q}`];if(!We){console.warn(`Floating toolbar [${q}] not found.`);return}z.registerPlugin(la({editor:z,element:We,pluginKey:`floatingToolbar::${q}`,shouldShow:({editor:je})=>q==="paragraph"?je.isFocused&&je.isActive(q)&&!je.state.selection.empty:N&&!je.state.selection.empty&&je.isActive("paragraph")?!1:je.isFocused&&je.isActive(q),options:{placement:"bottom",offset:15}}))}),z.on("create",()=>{this.editorUpdatedAt=Date.now()});let _=Alpine.debounce(()=>{V||this.$wire.commit()},p??300);z.on("update",({editor:q})=>this.$nextTick(()=>{V||(this.editorUpdatedAt=Date.now(),this.state=q.getJSON(),this.shouldUpdateState=!1,this.fileValidationMessage=null,d&&_())})),z.on("selectionUpdate",({transaction:q})=>{V||(this.editorUpdatedAt=Date.now(),this.editorSelection=q.selection.toJSON())}),z.on("transaction",()=>{V||(this.editorUpdatedAt=Date.now())}),u&&z.on("blur",()=>{V||this.$wire.commit()}),this.$watch("state",()=>{if(!V){if(!this.shouldUpdateState){this.shouldUpdateState=!0;return}z.commands.setContent(this.state)}});let W=q=>{q.detail.livewireId===m&&q.detail.key===f&&this.runEditorCommands(q.detail)};window.addEventListener("run-rich-editor-commands",W),K.push(["run-rich-editor-commands",W]);let Q=q=>{q.detail.livewireId===m&&q.detail.key===f&&(this.isUploadingFile=!0,this.fileValidationMessage=null,q.stopPropagation())};window.addEventListener("rich-editor-uploading-file",Q),K.push(["rich-editor-uploading-file",Q]);let me=q=>{q.detail.livewireId===m&&q.detail.key===f&&(this.isUploadingFile=!1,q.stopPropagation())};window.addEventListener("rich-editor-uploaded-file",me),K.push(["rich-editor-uploaded-file",me]);let Ge=q=>{q.detail.livewireId===m&&q.detail.key===f&&(this.isUploadingFile=!1,this.fileValidationMessage=q.detail.validationMessage,q.stopPropagation())};window.addEventListener("rich-editor-file-validation-message",Ge),K.push(["rich-editor-file-validation-message",Ge]),window.dispatchEvent(new CustomEvent(`schema-component-${m}-${f}-loaded`))},getEditor(){return z},$getEditor(){return this.getEditor()},setEditorSelection(N){N&&(this.editorSelection=N,z.chain().command(({tr:_})=>(_.setSelection(I.fromJSON(z.state.doc,this.editorSelection)),!0)).run())},runEditorCommands({commands:N,editorSelection:_}){this.setEditorSelection(_);let W=z.chain();N.forEach(Q=>W=W[Q.name](...Q.arguments??[])),W.run()},togglePanel(N=null){if(this.isPanelActive(N)){this.activePanel=null;return}this.activePanel=N},isPanelActive(N=null){return N===null?this.activePanel!==null:this.activePanel===N},insertMergeTag(N){z.chain().focus().insertContent([{type:"mergeTag",attrs:{id:N}},{type:"text",text:" "}]).run()},destroy(){V=!0,K.forEach(([N,_])=>{window.removeEventListener(N,_)}),K=[],z&&(z.destroy(),z=null),this.shouldUpdateState=!0}}}export{Jx as default}; diff --git a/public/js/filament/forms/components/select.js b/public/js/filament/forms/components/select.js new file mode 100644 index 00000000..df50c93c --- /dev/null +++ b/public/js/filament/forms/components/select.js @@ -0,0 +1,11 @@ +var Ft=Math.min,vt=Math.max,Ht=Math.round;var st=n=>({x:n,y:n}),ji={left:"right",right:"left",bottom:"top",top:"bottom"},qi={start:"end",end:"start"};function De(n,t,e){return vt(n,Ft(t,e))}function Vt(n,t){return typeof n=="function"?n(t):n}function yt(n){return n.split("-")[0]}function Wt(n){return n.split("-")[1]}function Ae(n){return n==="x"?"y":"x"}function Ce(n){return n==="y"?"height":"width"}var Ji=new Set(["top","bottom"]);function ht(n){return Ji.has(yt(n))?"y":"x"}function Le(n){return Ae(ht(n))}function Je(n,t,e){e===void 0&&(e=!1);let i=Wt(n),o=Le(n),s=Ce(o),r=o==="x"?i===(e?"end":"start")?"right":"left":i==="start"?"bottom":"top";return t.reference[s]>t.floating[s]&&(r=Bt(r)),[r,Bt(r)]}function Qe(n){let t=Bt(n);return[ie(n),t,ie(t)]}function ie(n){return n.replace(/start|end/g,t=>qi[t])}var je=["left","right"],qe=["right","left"],Qi=["top","bottom"],Zi=["bottom","top"];function tn(n,t,e){switch(n){case"top":case"bottom":return e?t?qe:je:t?je:qe;case"left":case"right":return t?Qi:Zi;default:return[]}}function Ze(n,t,e,i){let o=Wt(n),s=tn(yt(n),e==="start",i);return o&&(s=s.map(r=>r+"-"+o),t&&(s=s.concat(s.map(ie)))),s}function Bt(n){return n.replace(/left|right|bottom|top/g,t=>ji[t])}function en(n){return{top:0,right:0,bottom:0,left:0,...n}}function ti(n){return typeof n!="number"?en(n):{top:n,right:n,bottom:n,left:n}}function Ot(n){let{x:t,y:e,width:i,height:o}=n;return{width:i,height:o,top:e,left:t,right:t+i,bottom:e+o,x:t,y:e}}function ei(n,t,e){let{reference:i,floating:o}=n,s=ht(t),r=Le(t),a=Ce(r),l=yt(t),c=s==="y",f=i.x+i.width/2-o.width/2,d=i.y+i.height/2-o.height/2,p=i[a]/2-o[a]/2,u;switch(l){case"top":u={x:f,y:i.y-o.height};break;case"bottom":u={x:f,y:i.y+i.height};break;case"right":u={x:i.x+i.width,y:d};break;case"left":u={x:i.x-o.width,y:d};break;default:u={x:i.x,y:i.y}}switch(Wt(t)){case"start":u[r]-=p*(e&&c?-1:1);break;case"end":u[r]+=p*(e&&c?-1:1);break}return u}var ii=async(n,t,e)=>{let{placement:i="bottom",strategy:o="absolute",middleware:s=[],platform:r}=e,a=s.filter(Boolean),l=await(r.isRTL==null?void 0:r.isRTL(t)),c=await r.getElementRects({reference:n,floating:t,strategy:o}),{x:f,y:d}=ei(c,i,l),p=i,u={},g=0;for(let m=0;mk<=0)){var W,tt;let k=(((W=s.flip)==null?void 0:W.index)||0)+1,Y=q[k];if(Y&&(!(d==="alignment"?w!==ht(Y):!1)||V.every(L=>ht(L.placement)===w?L.overflows[0]>0:!0)))return{data:{index:k,overflows:V},reset:{placement:Y}};let B=(tt=V.filter(X=>X.overflows[0]<=0).sort((X,L)=>X.overflows[1]-L.overflows[1])[0])==null?void 0:tt.placement;if(!B)switch(u){case"bestFit":{var z;let X=(z=V.filter(L=>{if(F){let et=ht(L.placement);return et===w||et==="y"}return!0}).map(L=>[L.placement,L.overflows.filter(et=>et>0).reduce((et,ee)=>et+ee,0)]).sort((L,et)=>L[1]-et[1])[0])==null?void 0:z[0];X&&(B=X);break}case"initialPlacement":B=a;break}if(o!==B)return{reset:{placement:B}}}return{}}}};var nn=new Set(["left","top"]);async function on(n,t){let{placement:e,platform:i,elements:o}=n,s=await(i.isRTL==null?void 0:i.isRTL(o.floating)),r=yt(e),a=Wt(e),l=ht(e)==="y",c=nn.has(r)?-1:1,f=s&&l?-1:1,d=Vt(t,n),{mainAxis:p,crossAxis:u,alignmentAxis:g}=typeof d=="number"?{mainAxis:d,crossAxis:0,alignmentAxis:null}:{mainAxis:d.mainAxis||0,crossAxis:d.crossAxis||0,alignmentAxis:d.alignmentAxis};return a&&typeof g=="number"&&(u=a==="end"?g*-1:g),l?{x:u*f,y:p*c}:{x:p*c,y:u*f}}var oi=function(n){return n===void 0&&(n=0),{name:"offset",options:n,async fn(t){var e,i;let{x:o,y:s,placement:r,middlewareData:a}=t,l=await on(t,n);return r===((e=a.offset)==null?void 0:e.placement)&&(i=a.arrow)!=null&&i.alignmentOffset?{}:{x:o+l.x,y:s+l.y,data:{...l,placement:r}}}}},si=function(n){return n===void 0&&(n={}),{name:"shift",options:n,async fn(t){let{x:e,y:i,placement:o}=t,{mainAxis:s=!0,crossAxis:r=!1,limiter:a={fn:S=>{let{x:E,y:w}=S;return{x:E,y:w}}},...l}=Vt(n,t),c={x:e,y:i},f=await Ie(t,l),d=ht(yt(o)),p=Ae(d),u=c[p],g=c[d];if(s){let S=p==="y"?"top":"left",E=p==="y"?"bottom":"right",w=u+f[S],D=u-f[E];u=De(w,u,D)}if(r){let S=d==="y"?"top":"left",E=d==="y"?"bottom":"right",w=g+f[S],D=g-f[E];g=De(w,g,D)}let m=a.fn({...t,[p]:u,[d]:g});return{...m,data:{x:m.x-e,y:m.y-i,enabled:{[p]:s,[d]:r}}}}}};function oe(){return typeof window<"u"}function Et(n){return ai(n)?(n.nodeName||"").toLowerCase():"#document"}function U(n){var t;return(n==null||(t=n.ownerDocument)==null?void 0:t.defaultView)||window}function ct(n){var t;return(t=(ai(n)?n.ownerDocument:n.document)||window.document)==null?void 0:t.documentElement}function ai(n){return oe()?n instanceof Node||n instanceof U(n).Node:!1}function it(n){return oe()?n instanceof Element||n instanceof U(n).Element:!1}function rt(n){return oe()?n instanceof HTMLElement||n instanceof U(n).HTMLElement:!1}function ri(n){return!oe()||typeof ShadowRoot>"u"?!1:n instanceof ShadowRoot||n instanceof U(n).ShadowRoot}var sn=new Set(["inline","contents"]);function It(n){let{overflow:t,overflowX:e,overflowY:i,display:o}=nt(n);return/auto|scroll|overlay|hidden|clip/.test(t+i+e)&&!sn.has(o)}var rn=new Set(["table","td","th"]);function li(n){return rn.has(Et(n))}var an=[":popover-open",":modal"];function zt(n){return an.some(t=>{try{return n.matches(t)}catch{return!1}})}var ln=["transform","translate","scale","rotate","perspective"],cn=["transform","translate","scale","rotate","perspective","filter"],dn=["paint","layout","strict","content"];function se(n){let t=re(),e=it(n)?nt(n):n;return ln.some(i=>e[i]?e[i]!=="none":!1)||(e.containerType?e.containerType!=="normal":!1)||!t&&(e.backdropFilter?e.backdropFilter!=="none":!1)||!t&&(e.filter?e.filter!=="none":!1)||cn.some(i=>(e.willChange||"").includes(i))||dn.some(i=>(e.contain||"").includes(i))}function ci(n){let t=ut(n);for(;rt(t)&&!Dt(t);){if(se(t))return t;if(zt(t))return null;t=ut(t)}return null}function re(){return typeof CSS>"u"||!CSS.supports?!1:CSS.supports("-webkit-backdrop-filter","none")}var fn=new Set(["html","body","#document"]);function Dt(n){return fn.has(Et(n))}function nt(n){return U(n).getComputedStyle(n)}function Xt(n){return it(n)?{scrollLeft:n.scrollLeft,scrollTop:n.scrollTop}:{scrollLeft:n.scrollX,scrollTop:n.scrollY}}function ut(n){if(Et(n)==="html")return n;let t=n.assignedSlot||n.parentNode||ri(n)&&n.host||ct(n);return ri(t)?t.host:t}function di(n){let t=ut(n);return Dt(t)?n.ownerDocument?n.ownerDocument.body:n.body:rt(t)&&It(t)?t:di(t)}function ne(n,t,e){var i;t===void 0&&(t=[]),e===void 0&&(e=!0);let o=di(n),s=o===((i=n.ownerDocument)==null?void 0:i.body),r=U(o);if(s){let a=ae(r);return t.concat(r,r.visualViewport||[],It(o)?o:[],a&&e?ne(a):[])}return t.concat(o,ne(o,[],e))}function ae(n){return n.parent&&Object.getPrototypeOf(n.parent)?n.frameElement:null}function pi(n){let t=nt(n),e=parseFloat(t.width)||0,i=parseFloat(t.height)||0,o=rt(n),s=o?n.offsetWidth:e,r=o?n.offsetHeight:i,a=Ht(e)!==s||Ht(i)!==r;return a&&(e=s,i=r),{width:e,height:i,$:a}}function gi(n){return it(n)?n:n.contextElement}function Tt(n){let t=gi(n);if(!rt(t))return st(1);let e=t.getBoundingClientRect(),{width:i,height:o,$:s}=pi(t),r=(s?Ht(e.width):e.width)/i,a=(s?Ht(e.height):e.height)/o;return(!r||!Number.isFinite(r))&&(r=1),(!a||!Number.isFinite(a))&&(a=1),{x:r,y:a}}var hn=st(0);function mi(n){let t=U(n);return!re()||!t.visualViewport?hn:{x:t.visualViewport.offsetLeft,y:t.visualViewport.offsetTop}}function un(n,t,e){return t===void 0&&(t=!1),!e||t&&e!==U(n)?!1:t}function $t(n,t,e,i){t===void 0&&(t=!1),e===void 0&&(e=!1);let o=n.getBoundingClientRect(),s=gi(n),r=st(1);t&&(i?it(i)&&(r=Tt(i)):r=Tt(n));let a=un(s,e,i)?mi(s):st(0),l=(o.left+a.x)/r.x,c=(o.top+a.y)/r.y,f=o.width/r.x,d=o.height/r.y;if(s){let p=U(s),u=i&&it(i)?U(i):i,g=p,m=ae(g);for(;m&&i&&u!==g;){let S=Tt(m),E=m.getBoundingClientRect(),w=nt(m),D=E.left+(m.clientLeft+parseFloat(w.paddingLeft))*S.x,A=E.top+(m.clientTop+parseFloat(w.paddingTop))*S.y;l*=S.x,c*=S.y,f*=S.x,d*=S.y,l+=D,c+=A,g=U(m),m=ae(g)}}return Ot({width:f,height:d,x:l,y:c})}function le(n,t){let e=Xt(n).scrollLeft;return t?t.left+e:$t(ct(n)).left+e}function bi(n,t){let e=n.getBoundingClientRect(),i=e.left+t.scrollLeft-le(n,e),o=e.top+t.scrollTop;return{x:i,y:o}}function pn(n){let{elements:t,rect:e,offsetParent:i,strategy:o}=n,s=o==="fixed",r=ct(i),a=t?zt(t.floating):!1;if(i===r||a&&s)return e;let l={scrollLeft:0,scrollTop:0},c=st(1),f=st(0),d=rt(i);if((d||!d&&!s)&&((Et(i)!=="body"||It(r))&&(l=Xt(i)),rt(i))){let u=$t(i);c=Tt(i),f.x=u.x+i.clientLeft,f.y=u.y+i.clientTop}let p=r&&!d&&!s?bi(r,l):st(0);return{width:e.width*c.x,height:e.height*c.y,x:e.x*c.x-l.scrollLeft*c.x+f.x+p.x,y:e.y*c.y-l.scrollTop*c.y+f.y+p.y}}function gn(n){return Array.from(n.getClientRects())}function mn(n){let t=ct(n),e=Xt(n),i=n.ownerDocument.body,o=vt(t.scrollWidth,t.clientWidth,i.scrollWidth,i.clientWidth),s=vt(t.scrollHeight,t.clientHeight,i.scrollHeight,i.clientHeight),r=-e.scrollLeft+le(n),a=-e.scrollTop;return nt(i).direction==="rtl"&&(r+=vt(t.clientWidth,i.clientWidth)-o),{width:o,height:s,x:r,y:a}}var fi=25;function bn(n,t){let e=U(n),i=ct(n),o=e.visualViewport,s=i.clientWidth,r=i.clientHeight,a=0,l=0;if(o){s=o.width,r=o.height;let f=re();(!f||f&&t==="fixed")&&(a=o.offsetLeft,l=o.offsetTop)}let c=le(i);if(c<=0){let f=i.ownerDocument,d=f.body,p=getComputedStyle(d),u=f.compatMode==="CSS1Compat"&&parseFloat(p.marginLeft)+parseFloat(p.marginRight)||0,g=Math.abs(i.clientWidth-d.clientWidth-u);g<=fi&&(s-=g)}else c<=fi&&(s+=c);return{width:s,height:r,x:a,y:l}}var vn=new Set(["absolute","fixed"]);function yn(n,t){let e=$t(n,!0,t==="fixed"),i=e.top+n.clientTop,o=e.left+n.clientLeft,s=rt(n)?Tt(n):st(1),r=n.clientWidth*s.x,a=n.clientHeight*s.y,l=o*s.x,c=i*s.y;return{width:r,height:a,x:l,y:c}}function hi(n,t,e){let i;if(t==="viewport")i=bn(n,e);else if(t==="document")i=mn(ct(n));else if(it(t))i=yn(t,e);else{let o=mi(n);i={x:t.x-o.x,y:t.y-o.y,width:t.width,height:t.height}}return Ot(i)}function vi(n,t){let e=ut(n);return e===t||!it(e)||Dt(e)?!1:nt(e).position==="fixed"||vi(e,t)}function wn(n,t){let e=t.get(n);if(e)return e;let i=ne(n,[],!1).filter(a=>it(a)&&Et(a)!=="body"),o=null,s=nt(n).position==="fixed",r=s?ut(n):n;for(;it(r)&&!Dt(r);){let a=nt(r),l=se(r);!l&&a.position==="fixed"&&(o=null),(s?!l&&!o:!l&&a.position==="static"&&!!o&&vn.has(o.position)||It(r)&&!l&&vi(n,r))?i=i.filter(f=>f!==r):o=a,r=ut(r)}return t.set(n,i),i}function Sn(n){let{element:t,boundary:e,rootBoundary:i,strategy:o}=n,r=[...e==="clippingAncestors"?zt(t)?[]:wn(t,this._c):[].concat(e),i],a=r[0],l=r.reduce((c,f)=>{let d=hi(t,f,o);return c.top=vt(d.top,c.top),c.right=Ft(d.right,c.right),c.bottom=Ft(d.bottom,c.bottom),c.left=vt(d.left,c.left),c},hi(t,a,o));return{width:l.right-l.left,height:l.bottom-l.top,x:l.left,y:l.top}}function xn(n){let{width:t,height:e}=pi(n);return{width:t,height:e}}function On(n,t,e){let i=rt(t),o=ct(t),s=e==="fixed",r=$t(n,!0,s,t),a={scrollLeft:0,scrollTop:0},l=st(0);function c(){l.x=le(o)}if(i||!i&&!s)if((Et(t)!=="body"||It(o))&&(a=Xt(t)),i){let u=$t(t,!0,s,t);l.x=u.x+t.clientLeft,l.y=u.y+t.clientTop}else o&&c();s&&!i&&o&&c();let f=o&&!i&&!s?bi(o,a):st(0),d=r.left+a.scrollLeft-l.x-f.x,p=r.top+a.scrollTop-l.y-f.y;return{x:d,y:p,width:r.width,height:r.height}}function Te(n){return nt(n).position==="static"}function ui(n,t){if(!rt(n)||nt(n).position==="fixed")return null;if(t)return t(n);let e=n.offsetParent;return ct(n)===e&&(e=e.ownerDocument.body),e}function yi(n,t){let e=U(n);if(zt(n))return e;if(!rt(n)){let o=ut(n);for(;o&&!Dt(o);){if(it(o)&&!Te(o))return o;o=ut(o)}return e}let i=ui(n,t);for(;i&&li(i)&&Te(i);)i=ui(i,t);return i&&Dt(i)&&Te(i)&&!se(i)?e:i||ci(n)||e}var En=async function(n){let t=this.getOffsetParent||yi,e=this.getDimensions,i=await e(n.floating);return{reference:On(n.reference,await t(n.floating),n.strategy),floating:{x:0,y:0,width:i.width,height:i.height}}};function Dn(n){return nt(n).direction==="rtl"}var An={convertOffsetParentRelativeRectToViewportRelativeRect:pn,getDocumentElement:ct,getClippingRect:Sn,getOffsetParent:yi,getElementRects:En,getClientRects:gn,getDimensions:xn,getScale:Tt,isElement:it,isRTL:Dn};var wi=oi;var Si=si,xi=ni;var Oi=(n,t,e)=>{let i=new Map,o={platform:An,...e},s={...o.platform,_c:i};return ii(n,t,{...o,platform:s})};function Ei(n,t){var e=Object.keys(n);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(n);t&&(i=i.filter(function(o){return Object.getOwnPropertyDescriptor(n,o).enumerable})),e.push.apply(e,i)}return e}function ft(n){for(var t=1;t=0)&&(e[o]=n[o]);return e}function In(n,t){if(n==null)return{};var e=Ln(n,t),i,o;if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(n);for(o=0;o=0)&&Object.prototype.propertyIsEnumerable.call(n,i)&&(e[i]=n[i])}return e}var Tn="1.15.6";function pt(n){if(typeof window<"u"&&window.navigator)return!!navigator.userAgent.match(n)}var mt=pt(/(?:Trident.*rv[ :]?11\.|msie|iemobile|Windows Phone)/i),Zt=pt(/Edge/i),Di=pt(/firefox/i),Gt=pt(/safari/i)&&!pt(/chrome/i)&&!pt(/android/i),$e=pt(/iP(ad|od|hone)/i),Pi=pt(/chrome/i)&&pt(/android/i),Mi={capture:!1,passive:!1};function O(n,t,e){n.addEventListener(t,e,!mt&&Mi)}function x(n,t,e){n.removeEventListener(t,e,!mt&&Mi)}function ve(n,t){if(t){if(t[0]===">"&&(t=t.substring(1)),n)try{if(n.matches)return n.matches(t);if(n.msMatchesSelector)return n.msMatchesSelector(t);if(n.webkitMatchesSelector)return n.webkitMatchesSelector(t)}catch{return!1}return!1}}function Ni(n){return n.host&&n!==document&&n.host.nodeType?n.host:n.parentNode}function lt(n,t,e,i){if(n){e=e||document;do{if(t!=null&&(t[0]===">"?n.parentNode===e&&ve(n,t):ve(n,t))||i&&n===e)return n;if(n===e)break}while(n=Ni(n))}return null}var Ai=/\s+/g;function Q(n,t,e){if(n&&t)if(n.classList)n.classList[e?"add":"remove"](t);else{var i=(" "+n.className+" ").replace(Ai," ").replace(" "+t+" "," ");n.className=(i+(e?" "+t:"")).replace(Ai," ")}}function b(n,t,e){var i=n&&n.style;if(i){if(e===void 0)return document.defaultView&&document.defaultView.getComputedStyle?e=document.defaultView.getComputedStyle(n,""):n.currentStyle&&(e=n.currentStyle),t===void 0?e:e[t];!(t in i)&&t.indexOf("webkit")===-1&&(t="-webkit-"+t),i[t]=e+(typeof e=="string"?"":"px")}}function Nt(n,t){var e="";if(typeof n=="string")e=n;else do{var i=b(n,"transform");i&&i!=="none"&&(e=i+" "+e)}while(!t&&(n=n.parentNode));var o=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return o&&new o(e)}function ki(n,t,e){if(n){var i=n.getElementsByTagName(t),o=0,s=i.length;if(e)for(;o=s:r=o<=s,!r)return i;if(i===dt())break;i=xt(i,!1)}return!1}function kt(n,t,e,i){for(var o=0,s=0,r=n.children;s2&&arguments[2]!==void 0?arguments[2]:{},o=i.evt,s=In(i,Fn);te.pluginEvent.bind(v)(t,e,ft({dragEl:h,parentEl:R,ghostEl:y,rootEl:I,nextEl:Lt,lastDownEl:pe,cloneEl:T,cloneHidden:St,dragStarted:Kt,putSortable:H,activeSortable:v.active,originalEvent:o,oldIndex:Mt,oldDraggableIndex:qt,newIndex:Z,newDraggableIndex:wt,hideGhostForTarget:$i,unhideGhostForTarget:Ki,cloneNowHidden:function(){St=!0},cloneNowShown:function(){St=!1},dispatchSortableEvent:function(a){K({sortable:e,name:a,originalEvent:o})}},s))};function K(n){Bn(ft({putSortable:H,cloneEl:T,targetEl:h,rootEl:I,oldIndex:Mt,oldDraggableIndex:qt,newIndex:Z,newDraggableIndex:wt},n))}var h,R,y,I,Lt,pe,T,St,Mt,Z,qt,wt,ce,H,Pt=!1,ye=!1,we=[],At,at,Pe,Me,Ii,Ti,Kt,Rt,Jt,Qt=!1,de=!1,ge,$,Ne=[],Ve=!1,Se=[],Oe=typeof document<"u",fe=$e,_i=Zt||mt?"cssFloat":"float",Hn=Oe&&!Pi&&!$e&&"draggable"in document.createElement("div"),Wi=(function(){if(Oe){if(mt)return!1;var n=document.createElement("x");return n.style.cssText="pointer-events:auto",n.style.pointerEvents==="auto"}})(),zi=function(t,e){var i=b(t),o=parseInt(i.width)-parseInt(i.paddingLeft)-parseInt(i.paddingRight)-parseInt(i.borderLeftWidth)-parseInt(i.borderRightWidth),s=kt(t,0,e),r=kt(t,1,e),a=s&&b(s),l=r&&b(r),c=a&&parseInt(a.marginLeft)+parseInt(a.marginRight)+N(s).width,f=l&&parseInt(l.marginLeft)+parseInt(l.marginRight)+N(r).width;if(i.display==="flex")return i.flexDirection==="column"||i.flexDirection==="column-reverse"?"vertical":"horizontal";if(i.display==="grid")return i.gridTemplateColumns.split(" ").length<=1?"vertical":"horizontal";if(s&&a.float&&a.float!=="none"){var d=a.float==="left"?"left":"right";return r&&(l.clear==="both"||l.clear===d)?"vertical":"horizontal"}return s&&(a.display==="block"||a.display==="flex"||a.display==="table"||a.display==="grid"||c>=o&&i[_i]==="none"||r&&i[_i]==="none"&&c+f>o)?"vertical":"horizontal"},Vn=function(t,e,i){var o=i?t.left:t.top,s=i?t.right:t.bottom,r=i?t.width:t.height,a=i?e.left:e.top,l=i?e.right:e.bottom,c=i?e.width:e.height;return o===a||s===l||o+r/2===a+c/2},Wn=function(t,e){var i;return we.some(function(o){var s=o[j].options.emptyInsertThreshold;if(!(!s||Ke(o))){var r=N(o),a=t>=r.left-s&&t<=r.right+s,l=e>=r.top-s&&e<=r.bottom+s;if(a&&l)return i=o}}),i},Xi=function(t){function e(s,r){return function(a,l,c,f){var d=a.options.group.name&&l.options.group.name&&a.options.group.name===l.options.group.name;if(s==null&&(r||d))return!0;if(s==null||s===!1)return!1;if(r&&s==="clone")return s;if(typeof s=="function")return e(s(a,l,c,f),r)(a,l,c,f);var p=(r?a:l).options.group.name;return s===!0||typeof s=="string"&&s===p||s.join&&s.indexOf(p)>-1}}var i={},o=t.group;(!o||ue(o)!="object")&&(o={name:o}),i.name=o.name,i.checkPull=e(o.pull,!0),i.checkPut=e(o.put),i.revertClone=o.revertClone,t.group=i},$i=function(){!Wi&&y&&b(y,"display","none")},Ki=function(){!Wi&&y&&b(y,"display","")};Oe&&!Pi&&document.addEventListener("click",function(n){if(ye)return n.preventDefault(),n.stopPropagation&&n.stopPropagation(),n.stopImmediatePropagation&&n.stopImmediatePropagation(),ye=!1,!1},!0);var Ct=function(t){if(h){t=t.touches?t.touches[0]:t;var e=Wn(t.clientX,t.clientY);if(e){var i={};for(var o in t)t.hasOwnProperty(o)&&(i[o]=t[o]);i.target=i.rootEl=e,i.preventDefault=void 0,i.stopPropagation=void 0,e[j]._onDragOver(i)}}},zn=function(t){h&&h.parentNode[j]._isOutsideThisEl(t.target)};function v(n,t){if(!(n&&n.nodeType&&n.nodeType===1))throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(n));this.el=n,this.options=t=gt({},t),n[j]=this;var e={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(n.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return zi(n,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(r,a){r.setData("Text",a.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:v.supportPointer!==!1&&"PointerEvent"in window&&(!Gt||$e),emptyInsertThreshold:5};te.initializePlugins(this,n,e);for(var i in e)!(i in t)&&(t[i]=e[i]);Xi(t);for(var o in this)o.charAt(0)==="_"&&typeof this[o]=="function"&&(this[o]=this[o].bind(this));this.nativeDraggable=t.forceFallback?!1:Hn,this.nativeDraggable&&(this.options.touchStartThreshold=1),t.supportPointer?O(n,"pointerdown",this._onTapStart):(O(n,"mousedown",this._onTapStart),O(n,"touchstart",this._onTapStart)),this.nativeDraggable&&(O(n,"dragover",this),O(n,"dragenter",this)),we.push(this.el),t.store&&t.store.get&&this.sort(t.store.get(this)||[]),gt(this,Mn())}v.prototype={constructor:v,_isOutsideThisEl:function(t){!this.el.contains(t)&&t!==this.el&&(Rt=null)},_getDirection:function(t,e){return typeof this.options.direction=="function"?this.options.direction.call(this,t,e,h):this.options.direction},_onTapStart:function(t){if(t.cancelable){var e=this,i=this.el,o=this.options,s=o.preventOnFilter,r=t.type,a=t.touches&&t.touches[0]||t.pointerType&&t.pointerType==="touch"&&t,l=(a||t).target,c=t.target.shadowRoot&&(t.path&&t.path[0]||t.composedPath&&t.composedPath()[0])||l,f=o.filter;if(qn(i),!h&&!(/mousedown|pointerdown/.test(r)&&t.button!==0||o.disabled)&&!c.isContentEditable&&!(!this.nativeDraggable&&Gt&&l&&l.tagName.toUpperCase()==="SELECT")&&(l=lt(l,o.draggable,i,!1),!(l&&l.animated)&&pe!==l)){if(Mt=ot(l),qt=ot(l,o.draggable),typeof f=="function"){if(f.call(this,t,l,this)){K({sortable:e,rootEl:c,name:"filter",targetEl:l,toEl:i,fromEl:i}),G("filter",e,{evt:t}),s&&t.preventDefault();return}}else if(f&&(f=f.split(",").some(function(d){if(d=lt(c,d.trim(),i,!1),d)return K({sortable:e,rootEl:d,name:"filter",targetEl:l,fromEl:i,toEl:i}),G("filter",e,{evt:t}),!0}),f)){s&&t.preventDefault();return}o.handle&&!lt(c,o.handle,i,!1)||this._prepareDragStart(t,a,l)}}},_prepareDragStart:function(t,e,i){var o=this,s=o.el,r=o.options,a=s.ownerDocument,l;if(i&&!h&&i.parentNode===s){var c=N(i);if(I=s,h=i,R=h.parentNode,Lt=h.nextSibling,pe=i,ce=r.group,v.dragged=h,At={target:h,clientX:(e||t).clientX,clientY:(e||t).clientY},Ii=At.clientX-c.left,Ti=At.clientY-c.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,h.style["will-change"]="all",l=function(){if(G("delayEnded",o,{evt:t}),v.eventCanceled){o._onDrop();return}o._disableDelayedDragEvents(),!Di&&o.nativeDraggable&&(h.draggable=!0),o._triggerDragStart(t,e),K({sortable:o,name:"choose",originalEvent:t}),Q(h,r.chosenClass,!0)},r.ignore.split(",").forEach(function(f){ki(h,f.trim(),ke)}),O(a,"dragover",Ct),O(a,"mousemove",Ct),O(a,"touchmove",Ct),r.supportPointer?(O(a,"pointerup",o._onDrop),!this.nativeDraggable&&O(a,"pointercancel",o._onDrop)):(O(a,"mouseup",o._onDrop),O(a,"touchend",o._onDrop),O(a,"touchcancel",o._onDrop)),Di&&this.nativeDraggable&&(this.options.touchStartThreshold=4,h.draggable=!0),G("delayStart",this,{evt:t}),r.delay&&(!r.delayOnTouchOnly||e)&&(!this.nativeDraggable||!(Zt||mt))){if(v.eventCanceled){this._onDrop();return}r.supportPointer?(O(a,"pointerup",o._disableDelayedDrag),O(a,"pointercancel",o._disableDelayedDrag)):(O(a,"mouseup",o._disableDelayedDrag),O(a,"touchend",o._disableDelayedDrag),O(a,"touchcancel",o._disableDelayedDrag)),O(a,"mousemove",o._delayedDragTouchMoveHandler),O(a,"touchmove",o._delayedDragTouchMoveHandler),r.supportPointer&&O(a,"pointermove",o._delayedDragTouchMoveHandler),o._dragStartTimer=setTimeout(l,r.delay)}else l()}},_delayedDragTouchMoveHandler:function(t){var e=t.touches?t.touches[0]:t;Math.max(Math.abs(e.clientX-this._lastX),Math.abs(e.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){h&&ke(h),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;x(t,"mouseup",this._disableDelayedDrag),x(t,"touchend",this._disableDelayedDrag),x(t,"touchcancel",this._disableDelayedDrag),x(t,"pointerup",this._disableDelayedDrag),x(t,"pointercancel",this._disableDelayedDrag),x(t,"mousemove",this._delayedDragTouchMoveHandler),x(t,"touchmove",this._delayedDragTouchMoveHandler),x(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||t.pointerType=="touch"&&t,!this.nativeDraggable||e?this.options.supportPointer?O(document,"pointermove",this._onTouchMove):e?O(document,"touchmove",this._onTouchMove):O(document,"mousemove",this._onTouchMove):(O(h,"dragend",this),O(I,"dragstart",this._onDragStart));try{document.selection?me(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch{}},_dragStarted:function(t,e){if(Pt=!1,I&&h){G("dragStarted",this,{evt:e}),this.nativeDraggable&&O(document,"dragover",zn);var i=this.options;!t&&Q(h,i.dragClass,!1),Q(h,i.ghostClass,!0),v.active=this,t&&this._appendGhost(),K({sortable:this,name:"start",originalEvent:e})}else this._nulling()},_emulateDragOver:function(){if(at){this._lastX=at.clientX,this._lastY=at.clientY,$i();for(var t=document.elementFromPoint(at.clientX,at.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(at.clientX,at.clientY),t!==e);)e=t;if(h.parentNode[j]._isOutsideThisEl(t),e)do{if(e[j]){var i=void 0;if(i=e[j]._onDragOver({clientX:at.clientX,clientY:at.clientY,target:t,rootEl:e}),i&&!this.options.dragoverBubble)break}t=e}while(e=Ni(e));Ki()}},_onTouchMove:function(t){if(At){var e=this.options,i=e.fallbackTolerance,o=e.fallbackOffset,s=t.touches?t.touches[0]:t,r=y&&Nt(y,!0),a=y&&r&&r.a,l=y&&r&&r.d,c=fe&&$&&Li($),f=(s.clientX-At.clientX+o.x)/(a||1)+(c?c[0]-Ne[0]:0)/(a||1),d=(s.clientY-At.clientY+o.y)/(l||1)+(c?c[1]-Ne[1]:0)/(l||1);if(!v.active&&!Pt){if(i&&Math.max(Math.abs(s.clientX-this._lastX),Math.abs(s.clientY-this._lastY))=0&&(K({rootEl:R,name:"add",toEl:R,fromEl:I,originalEvent:t}),K({sortable:this,name:"remove",toEl:R,originalEvent:t}),K({rootEl:R,name:"sort",toEl:R,fromEl:I,originalEvent:t}),K({sortable:this,name:"sort",toEl:R,originalEvent:t})),H&&H.save()):Z!==Mt&&Z>=0&&(K({sortable:this,name:"update",toEl:R,originalEvent:t}),K({sortable:this,name:"sort",toEl:R,originalEvent:t})),v.active&&((Z==null||Z===-1)&&(Z=Mt,wt=qt),K({sortable:this,name:"end",toEl:R,originalEvent:t}),this.save()))),this._nulling()},_nulling:function(){G("nulling",this),I=h=R=y=Lt=T=pe=St=At=at=Kt=Z=wt=Mt=qt=Rt=Jt=H=ce=v.dragged=v.ghost=v.clone=v.active=null,Se.forEach(function(t){t.checked=!0}),Se.length=Pe=Me=0},handleEvent:function(t){switch(t.type){case"drop":case"dragend":this._onDrop(t);break;case"dragenter":case"dragover":h&&(this._onDragOver(t),Xn(t));break;case"selectstart":t.preventDefault();break}},toArray:function(){for(var t=[],e,i=this.el.children,o=0,s=i.length,r=this.options;oo.right+s||n.clientY>i.bottom&&n.clientX>i.left:n.clientY>o.bottom+s||n.clientX>i.right&&n.clientY>i.top}function Un(n,t,e,i,o,s,r,a){var l=i?n.clientY:n.clientX,c=i?e.height:e.width,f=i?e.top:e.left,d=i?e.bottom:e.right,p=!1;if(!r){if(a&&gef+c*s/2:ld-ge)return-Jt}else if(l>f+c*(1-o)/2&&ld-c*s/2)?l>f+c/2?1:-1:0}function Gn(n){return ot(h){},options:W,optionsLimit:tt=null,placeholder:z,position:k=null,searchableOptionFields:Y=["label"],searchDebounce:B=1e3,searchingMessage:X="Searching...",searchPrompt:L="Search...",state:et,statePath:ee=null}){this.canOptionLabelsWrap=t,this.canSelectPlaceholder=e,this.element=i,this.getOptionLabelUsing=o,this.getOptionLabelsUsing=s,this.getOptionsUsing=r,this.getSearchResultsUsing=a,this.hasDynamicOptions=l,this.hasDynamicSearchResults=c,this.hasInitialNoOptionsMessage=f,this.initialOptionLabel=d,this.initialOptionLabels=p,this.initialState=u,this.isAutofocused=g,this.isDisabled=m,this.isHtmlAllowed=S,this.isMultiple=E,this.isReorderable=w,this.isSearchable=D,this.livewireId=A,this.loadingMessage=C,this.maxItems=F,this.maxItemsMessage=q,this.noOptionsMessage=J,this.noSearchResultsMessage=_,this.onStateChange=V,this.options=W,this.optionsLimit=tt,this.originalOptions=JSON.parse(JSON.stringify(W)),this.placeholder=z,this.position=k,this.searchableOptionFields=Array.isArray(Y)?Y:["label"],this.searchDebounce=B,this.searchingMessage=X,this.searchPrompt=L,this.state=et,this.statePath=ee,this.activeSearchId=0,this.labelRepository={},this.isOpen=!1,this.selectedIndex=-1,this.searchQuery="",this.searchTimeout=null,this.isSearching=!1,this.selectedDisplayVersion=0,this.render(),this.setUpEventListeners(),this.isAutofocused&&this.selectButton.focus()}populateLabelRepositoryFromOptions(t){if(!(!t||!Array.isArray(t)))for(let e of t)e.options&&Array.isArray(e.options)?this.populateLabelRepositoryFromOptions(e.options):e.value!==void 0&&e.label!==void 0&&(this.labelRepository[e.value]=e.label)}render(){this.populateLabelRepositoryFromOptions(this.options),this.container=document.createElement("div"),this.container.className="fi-select-input-ctn",this.canOptionLabelsWrap||this.container.classList.add("fi-select-input-ctn-option-labels-not-wrapped"),this.container.setAttribute("aria-haspopup","listbox"),this.selectButton=document.createElement("button"),this.selectButton.className="fi-select-input-btn",this.selectButton.type="button",this.selectButton.setAttribute("aria-expanded","false"),this.selectedDisplay=document.createElement("div"),this.selectedDisplay.className="fi-select-input-value-ctn",this.updateSelectedDisplay(),this.selectButton.appendChild(this.selectedDisplay),this.dropdown=document.createElement("div"),this.dropdown.className="fi-dropdown-panel fi-scrollable",this.dropdown.setAttribute("role","listbox"),this.dropdown.setAttribute("tabindex","-1"),this.dropdown.style.display="none",this.dropdownId=`fi-select-input-dropdown-${Math.random().toString(36).substring(2,11)}`,this.dropdown.id=this.dropdownId,this.isMultiple&&this.dropdown.setAttribute("aria-multiselectable","true"),this.isSearchable&&(this.searchContainer=document.createElement("div"),this.searchContainer.className="fi-select-input-search-ctn",this.searchInput=document.createElement("input"),this.searchInput.className="fi-input",this.searchInput.type="text",this.searchInput.placeholder=this.searchPrompt,this.searchInput.setAttribute("aria-label","Search"),this.searchContainer.appendChild(this.searchInput),this.dropdown.appendChild(this.searchContainer),this.searchInput.addEventListener("input",t=>{this.isDisabled||this.handleSearch(t)}),this.searchInput.addEventListener("keydown",t=>{if(!this.isDisabled){if(t.key==="Tab"){t.preventDefault();let e=this.getVisibleOptions();if(e.length===0)return;t.shiftKey?this.selectedIndex=e.length-1:this.selectedIndex=0,e.forEach(i=>{i.classList.remove("fi-selected")}),e[this.selectedIndex].classList.add("fi-selected"),e[this.selectedIndex].focus()}else if(t.key==="ArrowDown"){if(t.preventDefault(),t.stopPropagation(),this.getVisibleOptions().length===0)return;this.selectedIndex=-1,this.searchInput.blur(),this.focusNextOption()}else if(t.key==="ArrowUp"){t.preventDefault(),t.stopPropagation();let e=this.getVisibleOptions();if(e.length===0)return;this.selectedIndex=e.length-1,this.searchInput.blur(),e[this.selectedIndex].classList.add("fi-selected"),e[this.selectedIndex].focus(),e[this.selectedIndex].id&&this.dropdown.setAttribute("aria-activedescendant",e[this.selectedIndex].id),this.scrollOptionIntoView(e[this.selectedIndex])}else if(t.key==="Enter"){if(t.preventDefault(),t.stopPropagation(),this.isSearching)return;let e=this.getVisibleOptions();if(e.length===0)return;let i=e.find(s=>{let r=s.getAttribute("aria-disabled")==="true",a=s.classList.contains("fi-disabled"),l=s.offsetParent===null;return!(r||a||l)});if(!i)return;let o=i.getAttribute("data-value");if(o===null)return;this.selectOption(o)}}})),this.optionsList=document.createElement("ul"),this.renderOptions(),this.container.appendChild(this.selectButton),this.container.appendChild(this.dropdown),this.element.appendChild(this.container),this.applyDisabledState()}renderOptions(){this.optionsList.innerHTML="";let t=0,e=this.options,i=0,o=!1;this.options.forEach(a=>{a.options&&Array.isArray(a.options)?(i+=a.options.length,o=!0):i++}),o?this.optionsList.className="fi-select-input-options-ctn":i>0&&(this.optionsList.className="fi-dropdown-list");let s=o?null:this.optionsList,r=0;for(let a of e){if(this.optionsLimit>0&&r>=this.optionsLimit)break;if(a.options&&Array.isArray(a.options)){let l=a.options;if(this.isMultiple&&Array.isArray(this.state)&&this.state.length>0&&(l=a.options.filter(c=>!this.state.includes(c.value))),l.length>0){if(this.optionsLimit>0){let c=this.optionsLimit-r;c{let a=this.createOptionElement(r.value,r);s.appendChild(a)}),i.appendChild(o),i.appendChild(s),this.optionsList.appendChild(i)}createOptionElement(t,e){let i=t,o=e,s=!1;typeof e=="object"&&e!==null&&"label"in e&&"value"in e&&(i=e.value,o=e.label,s=e.isDisabled||!1);let r=document.createElement("li");r.className="fi-dropdown-list-item fi-select-input-option",s&&r.classList.add("fi-disabled");let a=`fi-select-input-option-${Math.random().toString(36).substring(2,11)}`;if(r.id=a,r.setAttribute("role","option"),r.setAttribute("data-value",i),r.setAttribute("tabindex","0"),s&&r.setAttribute("aria-disabled","true"),this.isHtmlAllowed&&typeof o=="string"){let f=document.createElement("div");f.innerHTML=o;let d=f.textContent||f.innerText||o;r.setAttribute("aria-label",d)}let l=this.isMultiple?Array.isArray(this.state)&&this.state.includes(i):this.state===i;r.setAttribute("aria-selected",l?"true":"false"),l&&r.classList.add("fi-selected");let c=document.createElement("span");return this.isHtmlAllowed?c.innerHTML=o:c.textContent=o,r.appendChild(c),s||r.addEventListener("click",f=>{f.preventDefault(),f.stopPropagation(),this.selectOption(i),this.isMultiple&&(this.isSearchable&&this.searchInput?setTimeout(()=>{this.searchInput.focus()},0):setTimeout(()=>{r.focus()},0))}),r}async updateSelectedDisplay(){this.selectedDisplayVersion=this.selectedDisplayVersion+1;let t=this.selectedDisplayVersion,e=document.createDocumentFragment();if(this.isMultiple){if(!Array.isArray(this.state)||this.state.length===0){let o=document.createElement("span");o.textContent=this.placeholder,o.classList.add("fi-select-input-placeholder"),e.appendChild(o)}else{let o=await this.getLabelsForMultipleSelection();if(t!==this.selectedDisplayVersion)return;this.addBadgesForSelectedOptions(o,e)}t===this.selectedDisplayVersion&&(this.selectedDisplay.replaceChildren(e),this.isOpen&&this.deferPositionDropdown());return}if(this.state===null||this.state===""){let o=document.createElement("span");if(o.textContent=this.placeholder,o.classList.add("fi-select-input-placeholder"),e.appendChild(o),t===this.selectedDisplayVersion){this.selectedDisplay.replaceChildren(e);let s=this.container.querySelector(".fi-select-input-value-remove-btn");s&&s.remove(),this.container.classList.remove("fi-select-input-ctn-clearable")}return}let i=await this.getLabelForSingleSelection();t===this.selectedDisplayVersion&&(this.addSingleSelectionDisplay(i,e),t===this.selectedDisplayVersion&&this.selectedDisplay.replaceChildren(e))}async getLabelsForMultipleSelection(){let t=this.getSelectedOptionLabels(),e=[];if(Array.isArray(this.state)){for(let o of this.state)if(!P(this.labelRepository[o])){if(P(t[o])){this.labelRepository[o]=t[o];continue}e.push(o.toString())}}if(e.length>0&&P(this.initialOptionLabels)&&JSON.stringify(this.state)===JSON.stringify(this.initialState)){if(Array.isArray(this.initialOptionLabels))for(let o of this.initialOptionLabels)P(o)&&o.value!==void 0&&o.label!==void 0&&e.includes(o.value)&&(this.labelRepository[o.value]=o.label)}else if(e.length>0&&this.getOptionLabelsUsing)try{let o=await this.getOptionLabelsUsing();for(let s of o)P(s)&&s.value!==void 0&&s.label!==void 0&&(this.labelRepository[s.value]=s.label)}catch(o){console.error("Error fetching option labels:",o)}let i=[];if(Array.isArray(this.state))for(let o of this.state)P(this.labelRepository[o])?i.push(this.labelRepository[o]):P(t[o])?i.push(t[o]):i.push(o);return i}createBadgeElement(t,e){let i=document.createElement("span");i.className="fi-badge fi-size-md fi-color fi-color-primary fi-text-color-600 dark:fi-text-color-200",P(t)&&i.setAttribute("data-value",t);let o=document.createElement("span");o.className="fi-badge-label-ctn";let s=document.createElement("span");s.className="fi-badge-label",this.canOptionLabelsWrap&&s.classList.add("fi-wrapped"),this.isHtmlAllowed?s.innerHTML=e:s.textContent=e,o.appendChild(s),i.appendChild(o);let r=this.createRemoveButton(t,e);return i.appendChild(r),i}createRemoveButton(t,e){let i=document.createElement("button");return i.type="button",i.className="fi-badge-delete-btn",i.innerHTML='',i.setAttribute("aria-label","Remove "+(this.isHtmlAllowed?e.replace(/<[^>]*>/g,""):e)),i.addEventListener("click",o=>{o.stopPropagation(),P(t)&&this.selectOption(t)}),i.addEventListener("keydown",o=>{(o.key===" "||o.key==="Enter")&&(o.preventDefault(),o.stopPropagation(),P(t)&&this.selectOption(t))}),i}addBadgesForSelectedOptions(t,e=this.selectedDisplay){let i=document.createElement("div");i.className="fi-select-input-value-badges-ctn",t.forEach((o,s)=>{let r=Array.isArray(this.state)?this.state[s]:null,a=this.createBadgeElement(r,o);i.appendChild(a)}),e.appendChild(i),this.isReorderable&&(i.addEventListener("click",o=>{o.stopPropagation()}),i.addEventListener("mousedown",o=>{o.stopPropagation()}),new Ui(i,{animation:150,onEnd:()=>{let o=[];i.querySelectorAll("[data-value]").forEach(s=>{o.push(s.getAttribute("data-value"))}),this.state=o,this.onStateChange(this.state)}}))}async getLabelForSingleSelection(){let t=this.labelRepository[this.state];if(bt(t)&&(t=this.getSelectedOptionLabel(this.state)),bt(t)&&P(this.initialOptionLabel)&&this.state===this.initialState)t=this.initialOptionLabel,P(this.state)&&(this.labelRepository[this.state]=t);else if(bt(t)&&this.getOptionLabelUsing)try{t=await this.getOptionLabelUsing(),P(t)&&P(this.state)&&(this.labelRepository[this.state]=t)}catch(e){console.error("Error fetching option label:",e),t=this.state}else bt(t)&&(t=this.state);return t}addSingleSelectionDisplay(t,e=this.selectedDisplay){let i=document.createElement("span");if(i.className="fi-select-input-value-label",this.isHtmlAllowed?i.innerHTML=t:i.textContent=t,e.appendChild(i),!this.canSelectPlaceholder||this.container.querySelector(".fi-select-input-value-remove-btn"))return;let o=document.createElement("button");o.type="button",o.className="fi-select-input-value-remove-btn",o.innerHTML='',o.setAttribute("aria-label","Clear selection"),o.addEventListener("click",s=>{s.stopPropagation(),this.selectOption("")}),o.addEventListener("keydown",s=>{(s.key===" "||s.key==="Enter")&&(s.preventDefault(),s.stopPropagation(),this.selectOption(""))}),this.container.appendChild(o),this.container.classList.add("fi-select-input-ctn-clearable")}getSelectedOptionLabel(t){if(P(this.labelRepository[t]))return this.labelRepository[t];let e="";for(let i of this.options)if(i.options&&Array.isArray(i.options)){for(let o of i.options)if(o.value===t){e=o.label,this.labelRepository[t]=e;break}}else if(i.value===t){e=i.label,this.labelRepository[t]=e;break}return e}setUpEventListeners(){this.buttonClickListener=()=>{this.toggleDropdown()},this.documentClickListener=t=>{!this.container.contains(t.target)&&this.isOpen&&this.closeDropdown()},this.buttonKeydownListener=t=>{this.isDisabled||this.handleSelectButtonKeydown(t)},this.dropdownKeydownListener=t=>{this.isDisabled||this.isSearchable&&document.activeElement===this.searchInput&&!["Tab","Escape"].includes(t.key)||this.handleDropdownKeydown(t)},this.selectButton.addEventListener("click",this.buttonClickListener),document.addEventListener("click",this.documentClickListener),this.selectButton.addEventListener("keydown",this.buttonKeydownListener),this.dropdown.addEventListener("keydown",this.dropdownKeydownListener),!this.isMultiple&&this.livewireId&&this.statePath&&this.getOptionLabelUsing&&(this.refreshOptionLabelListener=async t=>{if(t.detail.livewireId===this.livewireId&&t.detail.statePath===this.statePath&&P(this.state))try{delete this.labelRepository[this.state];let e=await this.getOptionLabelUsing();P(e)&&(this.labelRepository[this.state]=e);let i=this.selectedDisplay.querySelector(".fi-select-input-value-label");P(i)&&(this.isHtmlAllowed?i.innerHTML=e:i.textContent=e),this.updateOptionLabelInList(this.state,e)}catch(e){console.error("Error refreshing option label:",e)}},window.addEventListener("filament-forms::select.refreshSelectedOptionLabel",this.refreshOptionLabelListener))}updateOptionLabelInList(t,e){this.labelRepository[t]=e;let i=this.getVisibleOptions();for(let o of i)if(o.getAttribute("data-value")===String(t)){if(o.innerHTML="",this.isHtmlAllowed){let s=document.createElement("span");s.innerHTML=e,o.appendChild(s)}else o.appendChild(document.createTextNode(e));break}for(let o of this.options)if(o.options&&Array.isArray(o.options)){for(let s of o.options)if(s.value===t){s.label=e;break}}else if(o.value===t){o.label=e;break}for(let o of this.originalOptions)if(o.options&&Array.isArray(o.options)){for(let s of o.options)if(s.value===t){s.label=e;break}}else if(o.value===t){o.label=e;break}}handleSelectButtonKeydown(t){switch(t.key){case"ArrowDown":t.preventDefault(),t.stopPropagation(),this.isOpen?this.focusNextOption():this.openDropdown();break;case"ArrowUp":t.preventDefault(),t.stopPropagation(),this.isOpen?this.focusPreviousOption():this.openDropdown();break;case" ":if(t.preventDefault(),this.isOpen){if(this.selectedIndex>=0){let e=this.getVisibleOptions()[this.selectedIndex];e&&e.click()}}else this.openDropdown();break;case"Enter":break;case"Escape":this.isOpen&&(t.preventDefault(),this.closeDropdown());break;case"Tab":this.isOpen&&this.closeDropdown();break;default:if(this.isSearchable&&!t.ctrlKey&&!t.metaKey&&!t.altKey&&typeof t.key=="string"&&t.key.length===1){t.preventDefault();let e=t.key;this.isOpen||this.openDropdown(),this.searchInput&&(this.searchInput.focus(),this.searchInput.value=(this.searchInput.value||"")+e,this.searchInput.dispatchEvent(new Event("input",{bubbles:!0})))}break}}handleDropdownKeydown(t){switch(t.key){case"ArrowDown":t.preventDefault(),t.stopPropagation(),this.focusNextOption();break;case"ArrowUp":t.preventDefault(),t.stopPropagation(),this.focusPreviousOption();break;case" ":if(t.preventDefault(),this.selectedIndex>=0){let e=this.getVisibleOptions()[this.selectedIndex];e&&e.click()}break;case"Enter":if(t.preventDefault(),this.selectedIndex>=0){let e=this.getVisibleOptions()[this.selectedIndex];e&&e.click()}else{let e=this.element.closest("form");e&&e.submit()}break;case"Escape":t.preventDefault(),this.closeDropdown(),this.selectButton.focus();break;case"Tab":this.closeDropdown();break;default:if(this.isSearchable&&!t.ctrlKey&&!t.metaKey&&!t.altKey&&typeof t.key=="string"&&t.key.length===1){t.preventDefault();let e=t.key;this.searchInput&&(this.searchInput.focus(),this.searchInput.value=(this.searchInput.value||"")+e,this.searchInput.dispatchEvent(new Event("input",{bubbles:!0})))}break}}toggleDropdown(){if(!this.isDisabled){if(this.isOpen){this.closeDropdown();return}this.isMultiple&&!this.isSearchable&&!this.hasAvailableOptions()||this.openDropdown()}}hasAvailableOptions(){for(let t of this.options)if(t.options&&Array.isArray(t.options)){for(let e of t.options)if(!Array.isArray(this.state)||!this.state.includes(e.value))return!0}else if(!Array.isArray(this.state)||!this.state.includes(t.value))return!0;return!1}async openDropdown(){this.dropdown.style.display="block",this.dropdown.style.opacity="0";let t=this.selectButton.closest(".fi-fixed-positioning-context")!==null&&this.selectButton.closest(".fi-absolute-positioning-context")===null;if(this.dropdown.style.position=t?"fixed":"absolute",this.dropdown.style.width=`${this.selectButton.offsetWidth}px`,this.selectButton.setAttribute("aria-expanded","true"),this.isOpen=!0,this.positionDropdown(),this.resizeListener||(this.resizeListener=()=>{this.dropdown.style.width=`${this.selectButton.offsetWidth}px`,this.positionDropdown()},window.addEventListener("resize",this.resizeListener)),this.scrollListener||(this.scrollListener=()=>this.positionDropdown(),window.addEventListener("scroll",this.scrollListener,!0)),this.dropdown.style.opacity="1",this.isSearchable&&this.searchInput&&(this.searchInput.value="",this.searchQuery="",this.hasDynamicOptions||(this.options=JSON.parse(JSON.stringify(this.originalOptions)),this.renderOptions())),this.hasDynamicOptions&&this.getOptionsUsing){this.showLoadingState(!1);try{let e=await this.getOptionsUsing(),i=Array.isArray(e)?e:e&&Array.isArray(e.options)?e.options:[];if(this.options=i,this.originalOptions=JSON.parse(JSON.stringify(i)),this.populateLabelRepositoryFromOptions(i),this.isSearchable&&this.searchInput&&(this.searchInput.value&&this.searchInput.value.trim()!==""||this.searchQuery&&this.searchQuery.trim()!=="")){let o=(this.searchInput.value||this.searchQuery||"").trim().toLowerCase();this.hideLoadingState(),this.filterOptions(o)}else this.renderOptions()}catch(e){console.error("Error fetching options:",e),this.hideLoadingState()}}else(!this.hasInitialNoOptionsMessage||this.searchQuery)&&this.hideLoadingState();if(this.isSearchable&&this.searchInput)this.searchInput.focus();else{this.selectedIndex=-1;let e=this.getVisibleOptions();if(this.isMultiple){if(Array.isArray(this.state)&&this.state.length>0){for(let i=0;i0&&(this.selectedIndex=0),this.selectedIndex>=0&&(e[this.selectedIndex].classList.add("fi-selected"),e[this.selectedIndex].focus())}}positionDropdown(){let t=this.position==="top"?"top-start":"bottom-start",e=[wi(4),Si({padding:5})];this.position!=="top"&&this.position!=="bottom"&&e.push(xi());let i=this.selectButton.closest(".fi-fixed-positioning-context")!==null&&this.selectButton.closest(".fi-absolute-positioning-context")===null;Oi(this.selectButton,this.dropdown,{placement:t,middleware:e,strategy:i?"fixed":"absolute"}).then(({x:o,y:s})=>{Object.assign(this.dropdown.style,{left:`${o}px`,top:`${s}px`})})}deferPositionDropdown(){this.isOpen&&(this.positioningRequestAnimationFrame&&(cancelAnimationFrame(this.positioningRequestAnimationFrame),this.positioningRequestAnimationFrame=null),this.positioningRequestAnimationFrame=requestAnimationFrame(()=>{this.positionDropdown(),this.positioningRequestAnimationFrame=null}))}closeDropdown(){this.dropdown.style.display="none",this.selectButton.setAttribute("aria-expanded","false"),this.isOpen=!1,this.searchTimeout&&(clearTimeout(this.searchTimeout),this.searchTimeout=null),this.activeSearchId++,this.isSearching=!1,this.hideLoadingState(),this.resizeListener&&(window.removeEventListener("resize",this.resizeListener),this.resizeListener=null),this.scrollListener&&(window.removeEventListener("scroll",this.scrollListener,!0),this.scrollListener=null),this.getVisibleOptions().forEach(e=>{e.classList.remove("fi-selected")}),this.dropdown.removeAttribute("aria-activedescendant")}focusNextOption(){let t=this.getVisibleOptions();if(t.length!==0){if(this.selectedIndex>=0&&this.selectedIndex=0&&this.selectedIndexe.bottom?this.dropdown.scrollTop+=i.bottom-e.bottom:i.top li[role="option"]')):t=Array.from(this.optionsList.querySelectorAll(':scope > ul.fi-dropdown-list > li[role="option"]'));let e=Array.from(this.optionsList.querySelectorAll('li.fi-select-input-option-group > ul > li[role="option"]'));return[...t,...e]}getSelectedOptionLabels(){if(!Array.isArray(this.state)||this.state.length===0)return{};let t={};for(let e of this.state){let i=!1;for(let o of this.options)if(o.options&&Array.isArray(o.options)){for(let s of o.options)if(s.value===e){t[e]=s.label,i=!0;break}if(i)break}else if(o.value===e){t[e]=o.label,i=!0;break}}return t}handleSearch(t){let e=t.target.value.trim();if(this.searchQuery=e,this.searchTimeout&&clearTimeout(this.searchTimeout),e===""){this.options=JSON.parse(JSON.stringify(this.originalOptions)),this.renderOptions();return}if(!this.getSearchResultsUsing||typeof this.getSearchResultsUsing!="function"||!this.hasDynamicSearchResults){this.filterOptions(e);return}this.searchTimeout=setTimeout(async()=>{this.searchTimeout=null;let i=++this.activeSearchId;this.isSearching=!0;try{this.showLoadingState(!0);let o=await this.getSearchResultsUsing(e);if(i!==this.activeSearchId||!this.isOpen)return;let s=Array.isArray(o)?o:o&&Array.isArray(o.options)?o.options:[];this.options=s,this.populateLabelRepositoryFromOptions(s),this.hideLoadingState(),this.renderOptions(),this.isOpen&&this.deferPositionDropdown(),this.options.length===0&&this.showNoResultsMessage()}catch(o){i===this.activeSearchId&&(console.error("Error fetching search results:",o),this.hideLoadingState(),this.options=JSON.parse(JSON.stringify(this.originalOptions)),this.renderOptions())}finally{i===this.activeSearchId&&(this.isSearching=!1)}},this.searchDebounce)}showLoadingState(t=!1){this.optionsList.parentNode===this.dropdown&&this.dropdown.removeChild(this.optionsList),this.hideLoadingState();let e=document.createElement("div");e.className="fi-select-input-message",e.textContent=t?this.searchingMessage:this.loadingMessage,this.dropdown.appendChild(e),this.isOpen&&this.deferPositionDropdown()}hideLoadingState(){let t=this.dropdown.querySelector(".fi-select-input-message");t&&t.remove()}showNoOptionsMessage(){this.optionsList.parentNode===this.dropdown&&this.dropdown.removeChild(this.optionsList),this.hideLoadingState();let t=document.createElement("div");t.className="fi-select-input-message",t.textContent=this.noOptionsMessage,this.dropdown.appendChild(t),this.isOpen&&this.deferPositionDropdown()}showNoResultsMessage(){this.optionsList.parentNode===this.dropdown&&this.dropdown.removeChild(this.optionsList),this.hideLoadingState();let t=document.createElement("div");t.className="fi-select-input-message",t.textContent=this.noSearchResultsMessage,this.dropdown.appendChild(t),this.isOpen&&this.deferPositionDropdown()}filterOptions(t){let e=this.searchableOptionFields.includes("label"),i=this.searchableOptionFields.includes("value");t=t.toLowerCase();let o=[];for(let s of this.originalOptions)if(s.options&&Array.isArray(s.options)){let r=s.options.filter(a=>e&&a.label.toLowerCase().includes(t)||i&&String(a.value).toLowerCase().includes(t));r.length>0&&o.push({label:s.label,options:r})}else(e&&s.label.toLowerCase().includes(t)||i&&String(s.value).toLowerCase().includes(t))&&o.push(s);this.options=o,this.renderOptions(),this.options.length===0&&this.showNoResultsMessage(),this.isOpen&&this.positionDropdown()}selectOption(t){if(this.isDisabled)return;if(!this.isMultiple){this.state=t,this.updateSelectedDisplay(),this.renderOptions(),this.closeDropdown(),this.selectButton.focus(),this.onStateChange(this.state);return}let e=Array.isArray(this.state)?[...this.state]:[];if(e.includes(t)){let o=this.selectedDisplay.querySelector(`[data-value="${t}"]`);if(P(o)){let s=o.parentElement;P(s)&&s.children.length===1?(e=e.filter(r=>r!==t),this.state=e,this.updateSelectedDisplay()):(o.remove(),e=e.filter(r=>r!==t),this.state=e)}else e=e.filter(s=>s!==t),this.state=e,this.updateSelectedDisplay();this.renderOptions(),this.isOpen&&this.deferPositionDropdown(),this.maintainFocusInMultipleMode(),this.onStateChange(this.state);return}if(this.maxItems&&e.length>=this.maxItems){this.maxItemsMessage&&alert(this.maxItemsMessage);return}e.push(t),this.state=e;let i=this.selectedDisplay.querySelector(".fi-select-input-value-badges-ctn");bt(i)?this.updateSelectedDisplay():this.addSingleBadge(t,i),this.renderOptions(),this.isOpen&&this.deferPositionDropdown(),this.maintainFocusInMultipleMode(),this.onStateChange(this.state)}async addSingleBadge(t,e){let i=this.labelRepository[t];if(bt(i)&&(i=this.getSelectedOptionLabel(t),P(i)&&(this.labelRepository[t]=i)),bt(i)&&this.getOptionLabelsUsing)try{let s=await this.getOptionLabelsUsing();for(let r of s)if(P(r)&&r.value===t&&r.label!==void 0){i=r.label,this.labelRepository[t]=i;break}}catch(s){console.error("Error fetching option label:",s)}bt(i)&&(i=t);let o=this.createBadgeElement(t,i);e.appendChild(o)}maintainFocusInMultipleMode(){if(this.isSearchable&&this.searchInput){this.searchInput.focus();return}let t=this.getVisibleOptions();if(t.length!==0){if(this.selectedIndex=-1,Array.isArray(this.state)&&this.state.length>0){for(let e=0;e{e.setAttribute("disabled","disabled"),e.classList.add("fi-disabled")}),!this.isMultiple&&this.canSelectPlaceholder){let t=this.container.querySelector(".fi-select-input-value-remove-btn");t&&(t.setAttribute("disabled","disabled"),t.classList.add("fi-disabled"))}this.isSearchable&&this.searchInput&&(this.searchInput.setAttribute("disabled","disabled"),this.searchInput.classList.add("fi-disabled"))}else{if(this.selectButton.removeAttribute("disabled"),this.selectButton.removeAttribute("aria-disabled"),this.selectButton.classList.remove("fi-disabled"),this.isMultiple&&this.container.querySelectorAll(".fi-select-input-badge-remove").forEach(e=>{e.removeAttribute("disabled"),e.classList.remove("fi-disabled")}),!this.isMultiple&&this.canSelectPlaceholder){let t=this.container.querySelector(".fi-select-input-value-remove-btn");t&&(t.removeAttribute("disabled"),t.classList.add("fi-disabled"))}this.isSearchable&&this.searchInput&&(this.searchInput.removeAttribute("disabled"),this.searchInput.classList.remove("fi-disabled"))}}destroy(){this.selectButton&&this.buttonClickListener&&this.selectButton.removeEventListener("click",this.buttonClickListener),this.documentClickListener&&document.removeEventListener("click",this.documentClickListener),this.selectButton&&this.buttonKeydownListener&&this.selectButton.removeEventListener("keydown",this.buttonKeydownListener),this.dropdown&&this.dropdownKeydownListener&&this.dropdown.removeEventListener("keydown",this.dropdownKeydownListener),this.resizeListener&&(window.removeEventListener("resize",this.resizeListener),this.resizeListener=null),this.scrollListener&&(window.removeEventListener("scroll",this.scrollListener,!0),this.scrollListener=null),this.refreshOptionLabelListener&&window.removeEventListener("filament-forms::select.refreshSelectedOptionLabel",this.refreshOptionLabelListener),this.isOpen&&this.closeDropdown(),this.searchTimeout&&(clearTimeout(this.searchTimeout),this.searchTimeout=null),this.container&&this.container.remove()}};function Qn({canOptionLabelsWrap:n,canSelectPlaceholder:t,getOptionLabelUsing:e,getOptionLabelsUsing:i,getOptionsUsing:o,getSearchResultsUsing:s,hasDynamicOptions:r,hasDynamicSearchResults:a,hasInitialNoOptionsMessage:l,initialOptionLabel:c,initialOptionLabels:f,initialState:d,isAutofocused:p,isDisabled:u,isHtmlAllowed:g,isMultiple:m,isReorderable:S,isSearchable:E,livewireId:w,loadingMessage:D,maxItems:A,maxItemsMessage:C,noOptionsMessage:F,noSearchResultsMessage:q,options:J,optionsLimit:_,placeholder:V,position:W,searchDebounce:tt,searchingMessage:z,searchPrompt:k,searchableOptionFields:Y,state:B,statePath:X}){return{select:null,state:B,init(){this.select=new Ee({canOptionLabelsWrap:n,canSelectPlaceholder:t,element:this.$refs.select,getOptionLabelUsing:e,getOptionLabelsUsing:i,getOptionsUsing:o,getSearchResultsUsing:s,hasDynamicOptions:r,hasDynamicSearchResults:a,hasInitialNoOptionsMessage:l,initialOptionLabel:c,initialOptionLabels:f,initialState:d,isAutofocused:p,isDisabled:u,isHtmlAllowed:g,isMultiple:m,isReorderable:S,isSearchable:E,livewireId:w,loadingMessage:D,maxItems:A,maxItemsMessage:C,noOptionsMessage:F,noSearchResultsMessage:q,onStateChange:L=>{this.state=L},options:J,optionsLimit:_,placeholder:V,position:W,searchableOptionFields:Y,searchDebounce:tt,searchingMessage:z,searchPrompt:k,state:this.state,statePath:X}),this.$watch("state",L=>{this.$nextTick(()=>{this.select&&this.select.state!==L&&(this.select.state=L,this.select.updateSelectedDisplay(),this.select.renderOptions())})})},destroy(){this.select&&(this.select.destroy(),this.select=null)}}}export{Qn as default}; +/*! Bundled license information: + +sortablejs/modular/sortable.esm.js: + (**! + * Sortable 1.15.6 + * @author RubaXa + * @author owenm + * @license MIT + *) +*/ diff --git a/public/js/filament/forms/components/slider.js b/public/js/filament/forms/components/slider.js new file mode 100644 index 00000000..c79171c2 --- /dev/null +++ b/public/js/filament/forms/components/slider.js @@ -0,0 +1 @@ +var I;(function(r){r.Range="range",r.Steps="steps",r.Positions="positions",r.Count="count",r.Values="values"})(I||(I={}));var O;(function(r){r[r.None=-1]="None",r[r.NoValue=0]="NoValue",r[r.LargeValue=1]="LargeValue",r[r.SmallValue=2]="SmallValue"})(O||(O={}));function we(r){return rt(r)&&typeof r.from=="function"}function rt(r){return typeof r=="object"&&typeof r.to=="function"}function zt(r){r.parentElement.removeChild(r)}function St(r){return r!=null}function Ft(r){r.preventDefault()}function Ce(r){return r.filter(function(t){return this[t]?!1:this[t]=!0},{})}function Ee(r,t){return Math.round(r/t)*t}function Ae(r,t){var s=r.getBoundingClientRect(),f=r.ownerDocument,u=f.documentElement,d=Bt(f);return/webkit.*Chrome.*Mobile/i.test(navigator.userAgent)&&(d.x=0),t?s.top+d.y-u.clientTop:s.left+d.x-u.clientLeft}function R(r){return typeof r=="number"&&!isNaN(r)&&isFinite(r)}function Rt(r,t,s){s>0&&(L(r,t),setTimeout(function(){et(r,t)},s))}function jt(r){return Math.max(Math.min(r,100),0)}function it(r){return Array.isArray(r)?r:[r]}function Pe(r){r=String(r);var t=r.split(".");return t.length>1?t[1].length:0}function L(r,t){r.classList&&!/\s/.test(t)?r.classList.add(t):r.className+=" "+t}function et(r,t){r.classList&&!/\s/.test(t)?r.classList.remove(t):r.className=r.className.replace(new RegExp("(^|\\b)"+t.split(" ").join("|")+"(\\b|$)","gi")," ")}function Ve(r,t){return r.classList?r.classList.contains(t):new RegExp("\\b"+t+"\\b").test(r.className)}function Bt(r){var t=window.pageXOffset!==void 0,s=(r.compatMode||"")==="CSS1Compat",f=t?window.pageXOffset:s?r.documentElement.scrollLeft:r.body.scrollLeft,u=t?window.pageYOffset:s?r.documentElement.scrollTop:r.body.scrollTop;return{x:f,y:u}}function De(){return window.navigator.pointerEnabled?{start:"pointerdown",move:"pointermove",end:"pointerup"}:window.navigator.msPointerEnabled?{start:"MSPointerDown",move:"MSPointerMove",end:"MSPointerUp"}:{start:"mousedown touchstart",move:"mousemove touchmove",end:"mouseup touchend"}}function ye(){var r=!1;try{var t=Object.defineProperty({},"passive",{get:function(){r=!0}});window.addEventListener("test",null,t)}catch{}return r}function ke(){return window.CSS&&CSS.supports&&CSS.supports("touch-action","none")}function bt(r,t){return 100/(t-r)}function xt(r,t,s){return t*100/(r[s+1]-r[s])}function Ue(r,t){return xt(r,r[0]<0?t+Math.abs(r[0]):t-r[0],0)}function Me(r,t){return t*(r[1]-r[0])/100+r[0]}function G(r,t){for(var s=1;r>=t[s];)s+=1;return s}function _e(r,t,s){if(s>=r.slice(-1)[0])return 100;var f=G(s,r),u=r[f-1],d=r[f],v=t[f-1],w=t[f];return v+Ue([u,d],s)/bt(v,w)}function Le(r,t,s){if(s>=100)return r.slice(-1)[0];var f=G(s,t),u=r[f-1],d=r[f],v=t[f-1],w=t[f];return Me([u,d],(s-v)*bt(v,w))}function Oe(r,t,s,f){if(f===100)return f;var u=G(f,r),d=r[u-1],v=r[u];return s?f-d>(v-d)/2?v:d:t[u-1]?r[u-1]+Ee(f-r[u-1],t[u-1]):f}var Kt=(function(){function r(t,s,f){this.xPct=[],this.xVal=[],this.xSteps=[],this.xNumSteps=[],this.xHighestCompleteStep=[],this.xSteps=[f||!1],this.xNumSteps=[!1],this.snap=s;var u,d=[];for(Object.keys(t).forEach(function(v){d.push([it(t[v]),v])}),d.sort(function(v,w){return v[0][0]-w[0][0]}),u=0;uthis.xPct[u+1];)u++;else t===this.xPct[this.xPct.length-1]&&(u=this.xPct.length-2);!f&&t===this.xPct[u+1]&&u++,s===null&&(s=[]);var d,v=1,w=s[u],C=0,p=0,D=0,y=0;for(f?d=(t-this.xPct[u])/(this.xPct[u+1]-this.xPct[u]):d=(this.xPct[u+1]-t)/(this.xPct[u+1]-this.xPct[u]);w>0;)C=this.xPct[u+1+y]-this.xPct[u+y],s[u+y]*v+100-d*100>100?(p=C*d,v=(w-100*d)/s[u+y],d=1):(p=s[u+y]*C/100*v,v=0),f?(D=D-p,this.xPct.length+y>=1&&y--):(D=D+p,this.xPct.length-y>=1&&y++),w=s[u+y]*v;return t+D},r.prototype.toStepping=function(t){return t=_e(this.xVal,this.xPct,t),t},r.prototype.fromStepping=function(t){return Le(this.xVal,this.xPct,t)},r.prototype.getStep=function(t){return t=Oe(this.xPct,this.xSteps,this.snap,t),t},r.prototype.getDefaultStep=function(t,s,f){var u=G(t,this.xPct);return(t===100||s&&t===this.xPct[u-1])&&(u=Math.max(u-1,1)),(this.xVal[u]-this.xVal[u-1])/f},r.prototype.getNearbySteps=function(t){var s=G(t,this.xPct);return{stepBefore:{startValue:this.xVal[s-2],step:this.xNumSteps[s-2],highestStep:this.xHighestCompleteStep[s-2]},thisStep:{startValue:this.xVal[s-1],step:this.xNumSteps[s-1],highestStep:this.xHighestCompleteStep[s-1]},stepAfter:{startValue:this.xVal[s],step:this.xNumSteps[s],highestStep:this.xHighestCompleteStep[s]}}},r.prototype.countStepDecimals=function(){var t=this.xNumSteps.map(Pe);return Math.max.apply(null,t)},r.prototype.hasNoSize=function(){return this.xVal[0]===this.xVal[this.xVal.length-1]},r.prototype.convert=function(t){return this.getStep(this.toStepping(t))},r.prototype.handleEntryPoint=function(t,s){var f;if(t==="min"?f=0:t==="max"?f=100:f=parseFloat(t),!R(f)||!R(s[0]))throw new Error("noUiSlider: 'range' value isn't numeric.");this.xPct.push(f),this.xVal.push(s[0]);var u=Number(s[1]);f?this.xSteps.push(isNaN(u)?!1:u):isNaN(u)||(this.xSteps[0]=u),this.xHighestCompleteStep.push(0)},r.prototype.handleStepPoint=function(t,s){if(s){if(this.xVal[t]===this.xVal[t+1]){this.xSteps[t]=this.xHighestCompleteStep[t]=this.xVal[t];return}this.xSteps[t]=xt([this.xVal[t],this.xVal[t+1]],s,0)/bt(this.xPct[t],this.xPct[t+1]);var f=(this.xVal[t+1]-this.xVal[t])/this.xNumSteps[t],u=Math.ceil(Number(f.toFixed(3))-1),d=this.xVal[t]+this.xNumSteps[t]*u;this.xHighestCompleteStep[t]=d}},r})(),Nt={to:function(r){return r===void 0?"":r.toFixed(2)},from:Number},It={target:"target",base:"base",origin:"origin",handle:"handle",handleLower:"handle-lower",handleUpper:"handle-upper",touchArea:"touch-area",horizontal:"horizontal",vertical:"vertical",background:"background",connect:"connect",connects:"connects",ltr:"ltr",rtl:"rtl",textDirectionLtr:"txt-dir-ltr",textDirectionRtl:"txt-dir-rtl",draggable:"draggable",drag:"state-drag",tap:"state-tap",active:"active",tooltip:"tooltip",pips:"pips",pipsHorizontal:"pips-horizontal",pipsVertical:"pips-vertical",marker:"marker",markerHorizontal:"marker-horizontal",markerVertical:"marker-vertical",markerNormal:"marker-normal",markerLarge:"marker-large",markerSub:"marker-sub",value:"value",valueHorizontal:"value-horizontal",valueVertical:"value-vertical",valueNormal:"value-normal",valueLarge:"value-large",valueSub:"value-sub"},K={tooltips:".__tooltips",aria:".__aria"};function He(r,t){if(!R(t))throw new Error("noUiSlider: 'step' is not numeric.");r.singleStep=t}function ze(r,t){if(!R(t))throw new Error("noUiSlider: 'keyboardPageMultiplier' is not numeric.");r.keyboardPageMultiplier=t}function Fe(r,t){if(!R(t))throw new Error("noUiSlider: 'keyboardMultiplier' is not numeric.");r.keyboardMultiplier=t}function Re(r,t){if(!R(t))throw new Error("noUiSlider: 'keyboardDefaultStep' is not numeric.");r.keyboardDefaultStep=t}function je(r,t){if(typeof t!="object"||Array.isArray(t))throw new Error("noUiSlider: 'range' is not an object.");if(t.min===void 0||t.max===void 0)throw new Error("noUiSlider: Missing 'min' or 'max' in 'range'.");r.spectrum=new Kt(t,r.snap||!1,r.singleStep)}function Ne(r,t){if(t=it(t),!Array.isArray(t)||!t.length)throw new Error("noUiSlider: 'start' option is incorrect.");r.handles=t.length,r.start=t}function Be(r,t){if(typeof t!="boolean")throw new Error("noUiSlider: 'snap' option must be a boolean.");r.snap=t}function Ke(r,t){if(typeof t!="boolean")throw new Error("noUiSlider: 'animate' option must be a boolean.");r.animate=t}function Ie(r,t){if(typeof t!="number")throw new Error("noUiSlider: 'animationDuration' option must be a number.");r.animationDuration=t}function qt(r,t){var s=[!1],f;if(t==="lower"?t=[!0,!1]:t==="upper"&&(t=[!1,!0]),t===!0||t===!1){for(f=1;f1)throw new Error("noUiSlider: 'padding' option must not exceed 100% of the range.")}}function Ye(r,t){switch(t){case"ltr":r.dir=0;break;case"rtl":r.dir=1;break;default:throw new Error("noUiSlider: 'direction' option was not recognized.")}}function We(r,t){if(typeof t!="string")throw new Error("noUiSlider: 'behaviour' must be a string containing options.");var s=t.indexOf("tap")>=0,f=t.indexOf("drag")>=0,u=t.indexOf("fixed")>=0,d=t.indexOf("snap")>=0,v=t.indexOf("hover")>=0,w=t.indexOf("unconstrained")>=0,C=t.indexOf("invert-connects")>=0,p=t.indexOf("drag-all")>=0,D=t.indexOf("smooth-steps")>=0;if(u){if(r.handles!==2)throw new Error("noUiSlider: 'fixed' behaviour must be used with 2 handles");Tt(r,r.start[1]-r.start[0])}if(C&&r.handles!==2)throw new Error("noUiSlider: 'invert-connects' behaviour must be used with 2 handles");if(w&&(r.margin||r.limit))throw new Error("noUiSlider: 'unconstrained' behaviour cannot be used with margin or limit");r.events={tap:s||d,drag:f,dragAll:p,smoothSteps:D,fixed:u,snap:d,hover:v,unconstrained:w,invertConnects:C}}function $e(r,t){if(t!==!1)if(t===!0||rt(t)){r.tooltips=[];for(var s=0;s= 2) required for mode 'count'.");for(var i=e.values-1,a=100/i,n=[];i--;)n[i]=i*a;return n.push(100),Et(n,e.stepped)}return e.mode===I.Positions?Et(e.values,e.stepped):e.mode===I.Values?e.stepped?e.values.map(function(o){return m.fromStepping(m.getStep(m.toStepping(o)))}):e.values:[]}function Et(e,i){return e.map(function(a){return m.fromStepping(i?m.getStep(a):a)})}function ie(e){function i(x,E){return Number((x+E).toFixed(7))}var a=re(e),n={},o=m.xVal[0],l=m.xVal[m.xVal.length-1],h=!1,c=!1,S=0;return a=Ce(a.slice().sort(function(x,E){return x-E})),a[0]!==o&&(a.unshift(o),h=!0),a[a.length-1]!==l&&(a.push(l),c=!0),a.forEach(function(x,E){var A,g,V,_=x,k=a[E+1],U,dt,pt,mt,Lt,gt,Ot,Ht=e.mode===I.Steps;for(Ht&&(A=m.xNumSteps[E]),A||(A=k-_),k===void 0&&(k=_),A=Math.max(A,1e-7),g=_;g<=k;g=i(g,A)){for(U=m.toStepping(g),dt=U-S,Lt=dt/(e.density||1),gt=Math.round(Lt),Ot=dt/gt,V=1;V<=gt;V+=1)pt=S+V*Ot,n[pt.toFixed(5)]=[m.fromStepping(pt),0];mt=a.indexOf(g)>-1?O.LargeValue:Ht?O.SmallValue:O.NoValue,!E&&h&&g!==k&&(mt=0),g===k&&c||(n[U.toFixed(5)]=[g,mt]),S=U}}),n}function ae(e,i,a){var n,o,l=B.createElement("div"),h=(n={},n[O.None]="",n[O.NoValue]=t.cssClasses.valueNormal,n[O.LargeValue]=t.cssClasses.valueLarge,n[O.SmallValue]=t.cssClasses.valueSub,n),c=(o={},o[O.None]="",o[O.NoValue]=t.cssClasses.markerNormal,o[O.LargeValue]=t.cssClasses.markerLarge,o[O.SmallValue]=t.cssClasses.markerSub,o),S=[t.cssClasses.valueHorizontal,t.cssClasses.valueVertical],x=[t.cssClasses.markerHorizontal,t.cssClasses.markerVertical];L(l,t.cssClasses.pips),L(l,t.ort===0?t.cssClasses.pipsHorizontal:t.cssClasses.pipsVertical);function E(g,V){var _=V===t.cssClasses.value,k=_?S:x,U=_?h:c;return V+" "+k[t.ort]+" "+U[g]}function A(g,V,_){if(_=i?i(V,_):_,_!==O.None){var k=N(l,!1);k.className=E(_,t.cssClasses.marker),k.style[t.style]=g+"%",_>O.NoValue&&(k=N(l,!1),k.className=E(_,t.cssClasses.value),k.setAttribute("data-value",String(V)),k.style[t.style]=g+"%",k.innerHTML=String(a.to(V)))}}return Object.keys(e).forEach(function(g){A(g,e[g][0],e[g][1])}),l}function ot(){y&&(zt(y),y=null)}function lt(e){ot();var i=ie(e),a=e.filter,n=e.format||{to:function(o){return String(Math.round(o))}};return y=v.appendChild(ae(i,a,n)),y}function At(){var e=w.getBoundingClientRect(),i="offset"+["Width","Height"][t.ort];return t.ort===0?e.width||w[i]:e.height||w[i]}function T(e,i,a,n){var o=function(h){var c=ne(h,n.pageOffset,n.target||i);if(!c||wt()&&!n.doNotReject||Ve(v,t.cssClasses.tap)&&!n.doNotReject||e===f.start&&c.buttons!==void 0&&c.buttons>1||n.hover&&c.buttons)return!1;d||c.preventDefault(),c.calcPoint=c.points[t.ort],a(c,n)},l=[];return e.split(" ").forEach(function(h){i.addEventListener(h,o,d?{passive:!0}:!1),l.push([h,o])}),l}function ne(e,i,a){var n=e.type.indexOf("touch")===0,o=e.type.indexOf("mouse")===0,l=e.type.indexOf("pointer")===0,h=0,c=0;if(e.type.indexOf("MSPointer")===0&&(l=!0),e.type==="mousedown"&&!e.buttons&&!e.touches)return!1;if(n){var S=function(A){var g=A.target;return g===a||a.contains(g)||e.composed&&e.composedPath().shift()===a};if(e.type==="touchstart"){var x=Array.prototype.filter.call(e.touches,S);if(x.length>1)return!1;h=x[0].pageX,c=x[0].pageY}else{var E=Array.prototype.find.call(e.changedTouches,S);if(!E)return!1;h=E.pageX,c=E.pageY}}return i=i||Bt(B),(o||l)&&(h=e.clientX+i.x,c=e.clientY+i.y),e.pageOffset=i,e.points=[h,c],e.cursor=o||l,e}function Pt(e){var i=e-Ae(w,t.ort),a=i*100/At();return a=jt(a),t.dir?100-a:a}function se(e){var i=100,a=!1;return p.forEach(function(n,o){if(!nt(o)){var l=b[o],h=Math.abs(l-e),c=h===100&&i===100,S=hl;(S||x||c)&&(a=o,i=h)}}),a}function oe(e,i){e.type==="mouseout"&&e.target.nodeName==="HTML"&&e.relatedTarget===null&&ft(e,i)}function le(e,i){if(navigator.appVersion.indexOf("MSIE 9")===-1&&e.buttons===0&&i.buttonsProperty!==0)return ft(e,i);var a=(t.dir?-1:1)*(e.calcPoint-i.startCalcPoint),n=a*100/i.baseSize;Dt(a>0,n,i.locations,i.handleNumbers,i.connect)}function ft(e,i){i.handle&&(et(i.handle,t.cssClasses.active),Y-=1),i.listeners.forEach(function(a){H.removeEventListener(a[0],a[1])}),Y===0&&(et(v,t.cssClasses.drag),vt(),e.cursor&&(J.style.cursor="",J.removeEventListener("selectstart",Ft))),t.events.smoothSteps&&(i.handleNumbers.forEach(function(a){X(a,b[a],!0,!0,!1,!1)}),i.handleNumbers.forEach(function(a){P("update",a)})),i.handleNumbers.forEach(function(a){P("change",a),P("set",a),P("end",a)})}function ut(e,i){if(!i.handleNumbers.some(nt)){var a;if(i.handleNumbers.length===1){var n=p[i.handleNumbers[0]];a=n.children[0],Y+=1,L(a,t.cssClasses.active)}e.stopPropagation();var o=[],l=T(f.move,H,le,{target:e.target,handle:a,connect:i.connect,listeners:o,startCalcPoint:e.calcPoint,baseSize:At(),pageOffset:e.pageOffset,handleNumbers:i.handleNumbers,buttonsProperty:e.buttons,locations:b.slice()}),h=T(f.end,H,ft,{target:e.target,handle:a,listeners:o,doNotReject:!0,handleNumbers:i.handleNumbers}),c=T("mouseout",H,oe,{target:e.target,handle:a,listeners:o,doNotReject:!0,handleNumbers:i.handleNumbers});o.push.apply(o,l.concat(h,c)),e.cursor&&(J.style.cursor=getComputedStyle(e.target).cursor,p.length>1&&L(v,t.cssClasses.drag),J.addEventListener("selectstart",Ft,!1)),i.handleNumbers.forEach(function(S){P("start",S)})}}function fe(e){e.stopPropagation();var i=Pt(e.calcPoint),a=se(i);a!==!1&&(t.events.snap||Rt(v,t.cssClasses.tap,t.animationDuration),X(a,i,!0,!0),vt(),P("slide",a,!0),P("update",a,!0),t.events.snap?ut(e,{handleNumbers:[a]}):(P("change",a,!0),P("set",a,!0)))}function ue(e){var i=Pt(e.calcPoint),a=m.getStep(i),n=m.fromStepping(a);Object.keys(F).forEach(function(o){o.split(".")[0]==="hover"&&F[o].forEach(function(l){l.call(tt,n)})})}function ce(e,i){if(wt()||nt(i))return!1;var a=["Left","Right"],n=["Down","Up"],o=["PageDown","PageUp"],l=["Home","End"];t.dir&&!t.ort?a.reverse():t.ort&&!t.dir&&(n.reverse(),o.reverse());var h=e.key.replace("Arrow",""),c=h===o[0],S=h===o[1],x=h===n[0]||h===a[0]||c,E=h===n[1]||h===a[1]||S,A=h===l[0],g=h===l[1];if(!x&&!E&&!A&&!g)return!0;e.preventDefault();var V;if(E||x){var _=x?0:1,k=Mt(i),U=k[_];if(U===null)return!1;U===!1&&(U=m.getDefaultStep(b[i],x,t.keyboardDefaultStep)),S||c?U*=t.keyboardPageMultiplier:U*=t.keyboardMultiplier,U=Math.max(U,1e-7),U=(x?-1:1)*U,V=z[i]+U}else g?V=t.spectrum.xVal[t.spectrum.xVal.length-1]:V=t.spectrum.xVal[0];return X(i,m.toStepping(V),!0,!0),P("slide",i),P("update",i),P("change",i),P("set",i),!1}function Vt(e){e.fixed||p.forEach(function(i,a){T(f.start,i.children[0],ut,{handleNumbers:[a]})}),e.tap&&T(f.start,w,fe,{}),e.hover&&T(f.move,w,ue,{hover:!0}),e.drag&&D.forEach(function(i,a){if(!(i===!1||a===0||a===D.length-1)){var n=p[a-1],o=p[a],l=[i],h=[n,o],c=[a-1,a];L(i,t.cssClasses.draggable),e.fixed&&(l.push(n.children[0]),l.push(o.children[0])),e.dragAll&&(h=p,c=M),l.forEach(function(S){T(f.start,S,ut,{handles:h,handleNumbers:c,connect:i})})}})}function ct(e,i){F[e]=F[e]||[],F[e].push(i),e.split(".")[0]==="update"&&p.forEach(function(a,n){P("update",n)})}function he(e){return e===K.aria||e===K.tooltips}function W(e){var i=e&&e.split(".")[0],a=i?e.substring(i.length):e;Object.keys(F).forEach(function(n){var o=n.split(".")[0],l=n.substring(o.length);(!i||i===o)&&(!a||a===l)&&(!he(l)||a===l)&&delete F[n]})}function P(e,i,a){Object.keys(F).forEach(function(n){var o=n.split(".")[0];e===o&&F[n].forEach(function(l){l.call(tt,z.map(t.format.to),i,z.slice(),a||!1,b.slice(),tt)})})}function Z(e,i,a,n,o,l,h){var c;return p.length>1&&!t.events.unconstrained&&(n&&i>0&&(c=m.getAbsoluteDistance(e[i-1],t.margin,!1),a=Math.max(a,c)),o&&i1&&t.limit&&(n&&i>0&&(c=m.getAbsoluteDistance(e[i-1],t.limit,!1),a=Math.min(a,c)),o&&i1?n.forEach(function(A,g){var V=Z(l,A,l[A]+i,S[g],x[g],!1,c);V===!1?i=0:(i=V-l[A],l[A]=V)}):S=x=[!0];var E=!1;n.forEach(function(A,g){E=X(A,a[A]+i,S[g],x[g],!1,c)||E}),E&&(n.forEach(function(A){P("update",A),P("slide",A)}),o!=null&&P("drag",h))}function yt(e,i){return t.dir?100-e-i:e}function ve(e,i){b[e]=i,z[e]=m.fromStepping(i);var a=yt(i,0)-Wt,n="translate("+ht(a+"%","0")+")";if(p[e].style[t.transformRule]=n,t.events.invertConnects&&b.length>1){var o=b.every(function(l,h,c){return h===0||l>=c[h-1]});if(q!==!o){xe();return}}$(e),$(e+1),q&&($(e-1),$(e+2))}function vt(){M.forEach(function(e){var i=b[e]>50?-1:1,a=3+(p.length+i*e);p[e].style.zIndex=String(a)})}function X(e,i,a,n,o,l){return o||(i=Z(b,e,i,a,n,!1,l)),i===!1?!1:(ve(e,i),!0)}function $(e){if(D[e]){var i=b.slice();q&&i.sort(function(c,S){return c-S});var a=0,n=100;e!==0&&(a=i[e-1]),e!==D.length-1&&(n=i[e]);var o=n-a,l="translate("+ht(yt(a,o)+"%","0")+")",h="scale("+ht(o/100,"1")+")";D[e].style[t.transformRule]=l+" "+h}}function kt(e,i){return e===null||e===!1||e===void 0||(typeof e=="number"&&(e=String(e)),e=t.format.from(e),e!==!1&&(e=m.toStepping(e)),e===!1||isNaN(e))?b[i]:e}function Q(e,i,a){var n=it(e),o=b[0]===void 0;i=i===void 0?!0:i,t.animate&&!o&&Rt(v,t.cssClasses.tap,t.animationDuration),M.forEach(function(c){X(c,kt(n[c],c),!0,!1,a)});var l=M.length===1?0:1;if(o&&m.hasNoSize()&&(a=!0,b[0]=0,M.length>1)){var h=100/(M.length-1);M.forEach(function(c){b[c]=c*h})}for(;l=0&&ea.stepAfter.startValue&&(o=a.stepAfter.startValue-n),n>a.thisStep.startValue?l=a.thisStep.step:a.stepBefore.step===!1?l=!1:l=n-a.stepBefore.highestStep,i===100?o=null:i===0&&(l=null);var h=m.countStepDecimals();return o!==null&&o!==!1&&(o=Number(o.toFixed(h))),l!==null&&l!==!1&&(l=Number(l.toFixed(h))),[l,o]}function ge(){return M.map(Mt)}function Se(e,i){var a=Ut(),n=["margin","limit","padding","range","animate","snap","step","format","pips","tooltips","connect"];n.forEach(function(l){e[l]!==void 0&&(s[l]=e[l])});var o=Xt(s);n.forEach(function(l){e[l]!==void 0&&(t[l]=o[l])}),m=o.spectrum,t.margin=o.margin,t.limit=o.limit,t.padding=o.padding,t.pips?lt(t.pips):ot(),t.tooltips?Ct():st(),b=[],Q(St(e.start)?e.start:a,i),e.connect&&_t()}function _t(){for(;C.firstChild;)C.removeChild(C.firstChild);for(var e=0;e<=t.handles;e++)D[e]=at(C,t.connect[e]),$(e);Vt({drag:t.events.drag,fixed:!0})}function xe(){q=!q,qt(t,t.connect.map(function(e){return!e})),_t()}function be(){w=Jt(v),Gt(t.connect,w),Vt(t.events),Q(t.start),t.pips&<(t.pips),t.tooltips&&Ct(),ee()}be();var tt={destroy:me,steps:ge,on:ct,off:W,get:Ut,set:Q,setHandle:pe,reset:de,disable:Qt,enable:te,__moveHandles:function(e,i,a){Dt(e,i,b,a)},options:s,updateOptions:Se,target:v,removePips:ot,removeTooltips:st,getPositions:function(){return b.slice()},getTooltips:function(){return j},getOrigins:function(){return p},pips:lt};return tt}function ar(r,t){if(!r||!r.nodeName)throw new Error("noUiSlider: create requires a single element, got: "+r);if(r.noUiSlider)throw new Error("noUiSlider: Slider was already initialized.");var s=Xt(t),f=ir(r,s,t);return r.noUiSlider=f,f}var Yt={__spectrum:Kt,cssClasses:It,create:ar};function nr({arePipsStepped:r,behavior:t,decimalPlaces:s,fillTrack:f,isDisabled:u,isRtl:d,isVertical:v,maxDifference:w,minDifference:C,maxValue:p,minValue:D,nonLinearPoints:y,pipsDensity:j,pipsFilter:m,pipsFormatter:z,pipsMode:b,pipsValues:M,rangePadding:Y,state:F,step:q,tooltips:B}){return{state:F,slider:null,init(){this.slider=Yt.create(this.$el,{behaviour:t,direction:d?"rtl":"ltr",connect:f,format:{from:H=>+H,to:H=>s!==null?+H.toFixed(s):H},limit:w,margin:C,orientation:v?"vertical":"horizontal",padding:Y,pips:b?{density:j??10,filter:m,format:z,mode:b,stepped:r,values:M}:null,range:{min:D,...y??{},max:p},start:Alpine.raw(this.state),step:q,tooltips:B}),u&&this.slider.disable(),this.slider.on("change",H=>{this.state=H.length>1?H:H[0]}),this.$watch("state",()=>{this.slider.set(Alpine.raw(this.state))})},destroy(){this.slider.destroy(),this.slider=null}}}export{nr as default}; diff --git a/public/js/filament/forms/components/tags-input.js b/public/js/filament/forms/components/tags-input.js new file mode 100644 index 00000000..2266d496 --- /dev/null +++ b/public/js/filament/forms/components/tags-input.js @@ -0,0 +1 @@ +function s({state:n,splitKeys:a}){return{newTag:"",state:n,createTag(){if(this.newTag=this.newTag.trim(),this.newTag!==""){if(this.state.includes(this.newTag)){this.newTag="";return}this.state.push(this.newTag),this.newTag=""}},deleteTag(t){this.state=this.state.filter(e=>e!==t)},reorderTags(t){let e=this.state.splice(t.oldIndex,1)[0];this.state.splice(t.newIndex,0,e),this.state=[...this.state]},input:{"x-on:blur":"createTag()","x-model":"newTag","x-on:keydown"(t){["Enter",...a].includes(t.key)&&(t.preventDefault(),t.stopPropagation(),this.createTag())},"x-on:paste"(){this.$nextTick(()=>{if(a.length===0){this.createTag();return}let t=a.map(e=>e.replace(/[/\-\\^$*+?.()|[\]{}]/g,"\\$&")).join("|");this.newTag.split(new RegExp(t,"g")).forEach(e=>{this.newTag=e,this.createTag()})})}}}}export{s as default}; diff --git a/public/js/filament/forms/components/textarea.js b/public/js/filament/forms/components/textarea.js new file mode 100644 index 00000000..d3491c3c --- /dev/null +++ b/public/js/filament/forms/components/textarea.js @@ -0,0 +1 @@ +function n({initialHeight:e,shouldAutosize:i,state:h}){return{state:h,wrapperEl:null,init(){this.wrapperEl=this.$el.parentNode,this.setInitialHeight(),i?this.$watch("state",()=>{this.resize()}):this.setUpResizeObserver()},setInitialHeight(){this.$el.scrollHeight<=0||(this.wrapperEl.style.height=e+"rem")},resize(){if(this.$el.scrollHeight<=0)return;let t=this.$el.style.height;this.$el.style.height="0px";let r=this.$el.scrollHeight;this.$el.style.height=t;let l=parseFloat(e)*parseFloat(getComputedStyle(document.documentElement).fontSize),s=Math.max(r,l)+"px";this.wrapperEl.style.height!==s&&(this.wrapperEl.style.height=s)},setUpResizeObserver(){new ResizeObserver(()=>{this.wrapperEl.style.height=this.$el.style.height}).observe(this.$el)}}}export{n as default}; diff --git a/public/js/filament/notifications/notifications.js b/public/js/filament/notifications/notifications.js new file mode 100644 index 00000000..efd74b82 --- /dev/null +++ b/public/js/filament/notifications/notifications.js @@ -0,0 +1 @@ +(()=>{function c(s,t=()=>{}){let i=!1;return function(){i?t.apply(this,arguments):(i=!0,s.apply(this,arguments))}}var d=s=>{s.data("notificationComponent",({notification:t})=>({isShown:!1,computedStyle:null,transitionDuration:null,transitionEasing:null,unsubscribeLivewireHook:null,init(){this.computedStyle=window.getComputedStyle(this.$el),this.transitionDuration=parseFloat(this.computedStyle.transitionDuration)*1e3,this.transitionEasing=this.computedStyle.transitionTimingFunction,this.configureTransitions(),this.configureAnimations(),t.duration&&t.duration!=="persistent"&&setTimeout(()=>{if(!this.$el.matches(":hover")){this.close();return}this.$el.addEventListener("mouseleave",()=>this.close())},t.duration),this.isShown=!0},configureTransitions(){let i=this.computedStyle.display,e=()=>{s.mutateDom(()=>{this.$el.style.setProperty("display",i),this.$el.style.setProperty("visibility","visible")}),this.$el._x_isShown=!0},o=()=>{s.mutateDom(()=>{this.$el._x_isShown?this.$el.style.setProperty("visibility","hidden"):this.$el.style.setProperty("display","none")})},r=c(n=>n?e():o(),n=>{this.$el._x_toggleAndCascadeWithTransitions(this.$el,n,e,o)});s.effect(()=>r(this.isShown))},configureAnimations(){let i;this.unsubscribeLivewireHook=Livewire.interceptMessage(({onFinish:e,onSuccess:o})=>{requestAnimationFrame(()=>{let r=()=>this.$el.getBoundingClientRect().top,n=r();e(()=>{i=()=>{this.isShown&&this.$el.animate([{transform:`translateY(${n-r()}px)`},{transform:"translateY(0px)"}],{duration:this.transitionDuration,easing:this.transitionEasing})},this.$el.getAnimations().forEach(l=>l.finish())}),o(({payload:l})=>{l?.snapshot?.data?.isFilamentNotificationsComponent&&typeof i=="function"&&i()})})})},close(){this.isShown=!1,setTimeout(()=>window.dispatchEvent(new CustomEvent("notificationClosed",{detail:{id:t.id}})),this.transitionDuration)},markAsRead(){window.dispatchEvent(new CustomEvent("markedNotificationAsRead",{detail:{id:t.id}}))},markAsUnread(){window.dispatchEvent(new CustomEvent("markedNotificationAsUnread",{detail:{id:t.id}}))},destroy(){this.unsubscribeLivewireHook?.()}}))};var h=class{constructor(){return this.id(crypto.randomUUID?.()??"10000000-1000-4000-8000-100000000000".replace(/[018]/g,t=>(+t^crypto.getRandomValues(new Uint8Array(1))[0]&15>>+t/4).toString(16))),this}id(t){return this.id=t,this}title(t){return this.title=t,this}body(t){return this.body=t,this}actions(t){return this.actions=t,this}status(t){return this.status=t,this}color(t){return this.color=t,this}icon(t){return this.icon=t,this}iconColor(t){return this.iconColor=t,this}duration(t){return this.duration=t,this}seconds(t){return this.duration(t*1e3),this}persistent(){return this.duration("persistent"),this}danger(){return this.status("danger"),this}info(){return this.status("info"),this}success(){return this.status("success"),this}warning(){return this.status("warning"),this}view(t){return this.view=t,this}viewData(t){return this.viewData=t,this}send(){return window.dispatchEvent(new CustomEvent("notificationSent",{detail:{notification:this}})),this}},a=class{constructor(t){return this.name(t),this}name(t){return this.name=t,this}color(t){return this.color=t,this}dispatch(t,i){return this.event(t),this.eventData(i),this}dispatchSelf(t,i){return this.dispatch(t,i),this.dispatchDirection="self",this}dispatchTo(t,i,e){return this.dispatch(i,e),this.dispatchDirection="to",this.dispatchToComponent=t,this}emit(t,i){return this.dispatch(t,i),this}emitSelf(t,i){return this.dispatchSelf(t,i),this}emitTo(t,i,e){return this.dispatchTo(t,i,e),this}dispatchDirection(t){return this.dispatchDirection=t,this}dispatchToComponent(t){return this.dispatchToComponent=t,this}event(t){return this.event=t,this}eventData(t){return this.eventData=t,this}extraAttributes(t){return this.extraAttributes=t,this}icon(t){return this.icon=t,this}iconPosition(t){return this.iconPosition=t,this}outlined(t=!0){return this.isOutlined=t,this}disabled(t=!0){return this.isDisabled=t,this}label(t){return this.label=t,this}close(t=!0){return this.shouldClose=t,this}openUrlInNewTab(t=!0){return this.shouldOpenUrlInNewTab=t,this}size(t){return this.size=t,this}url(t){return this.url=t,this}view(t){return this.view=t,this}button(){return this.view("filament::components.button.index"),this}grouped(){return this.view("filament::components.dropdown.list.item"),this}iconButton(){return this.view("filament::components.icon-button"),this}link(){return this.view("filament::components.link"),this}},u=class{constructor(t){return this.actions(t),this}actions(t){return this.actions=t.map(i=>i.grouped()),this}color(t){return this.color=t,this}icon(t){return this.icon=t,this}iconPosition(t){return this.iconPosition=t,this}label(t){return this.label=t,this}tooltip(t){return this.tooltip=t,this}};window.FilamentNotificationAction=a;window.FilamentNotificationActionGroup=u;window.FilamentNotification=h;document.addEventListener("alpine:init",()=>{window.Alpine.plugin(d)});})(); diff --git a/public/js/filament/schemas/components/actions.js b/public/js/filament/schemas/components/actions.js new file mode 100644 index 00000000..703b92d6 --- /dev/null +++ b/public/js/filament/schemas/components/actions.js @@ -0,0 +1 @@ +var i=()=>({isSticky:!1,width:0,resizeObserver:null,boundUpdateWidth:null,init(){let e=this.$el.parentElement;e&&(this.updateWidth(),this.resizeObserver=new ResizeObserver(()=>this.updateWidth()),this.resizeObserver.observe(e),this.boundUpdateWidth=this.updateWidth.bind(this),window.addEventListener("resize",this.boundUpdateWidth))},enableSticky(){this.isSticky=this.$el.getBoundingClientRect().top>0},disableSticky(){this.isSticky=!1},updateWidth(){let e=this.$el.parentElement;if(!e)return;let t=getComputedStyle(this.$root.querySelector(".fi-ac"));this.width=e.offsetWidth+parseInt(t.marginInlineStart,10)*-1+parseInt(t.marginInlineEnd,10)*-1},destroy(){this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),this.boundUpdateWidth&&(window.removeEventListener("resize",this.boundUpdateWidth),this.boundUpdateWidth=null)}});export{i as default}; diff --git a/public/js/filament/schemas/components/tabs.js b/public/js/filament/schemas/components/tabs.js new file mode 100644 index 00000000..0fbab11b --- /dev/null +++ b/public/js/filament/schemas/components/tabs.js @@ -0,0 +1 @@ +function v({activeTab:w,isScrollable:f,isTabPersistedInQueryString:m,livewireId:g,tab:T,tabQueryStringKey:r}){return{boundResizeHandler:null,isScrollable:f,resizeDebounceTimer:null,tab:T,unsubscribeLivewireHook:null,withinDropdownIndex:null,withinDropdownMounted:!1,init(){let t=this.getTabs(),e=new URLSearchParams(window.location.search);m&&e.has(r)&&t.includes(e.get(r))&&(this.tab=e.get(r)),(!this.tab||!t.includes(this.tab))&&(this.tab=t[w-1]),this.$watch("tab",()=>{this.updateQueryString(),this.autofocusFields()}),this.autofocusFields(!0),this.unsubscribeLivewireHook=Livewire.interceptMessage(({message:i,onSuccess:a})=>{a(()=>{this.$nextTick(()=>{if(i.component.id!==g)return;let l=this.getTabs();l.includes(this.tab)||(this.tab=l[w-1]??this.tab)})})}),f||(this.boundResizeHandler=this.debouncedUpdateTabsWithinDropdown.bind(this),window.addEventListener("resize",this.boundResizeHandler),this.updateTabsWithinDropdown())},calculateAvailableWidth(t){let e=window.getComputedStyle(t);return Math.floor(t.clientWidth)-Math.ceil(parseFloat(e.paddingLeft))*2},calculateContainerGap(t){let e=window.getComputedStyle(t);return Math.ceil(parseFloat(e.columnGap))},calculateDropdownIconWidth(t){let e=t.querySelector(".fi-icon");return Math.ceil(e.clientWidth)},calculateTabItemGap(t){let e=window.getComputedStyle(t);return Math.ceil(parseFloat(e.columnGap)||8)},calculateTabItemPadding(t){let e=window.getComputedStyle(t);return Math.ceil(parseFloat(e.paddingLeft))+Math.ceil(parseFloat(e.paddingRight))},findOverflowIndex(t,e,i,a,l,h){let u=t.map(n=>Math.ceil(n.clientWidth)),b=t.map(n=>{let c=n.querySelector(".fi-tabs-item-label"),s=n.querySelector(".fi-badge"),o=Math.ceil(c.clientWidth),d=s?Math.ceil(s.clientWidth):0;return{label:o,badge:d,total:o+(d>0?a+d:0)}});for(let n=0;np+y,0),s=n*i,o=b.slice(n+1),d=o.length>0,D=d?Math.max(...o.map(p=>p.total)):0,W=d?l+D+a+h+i:0;if(c+s+W>e)return n}return-1},get isDropdownButtonVisible(){return this.withinDropdownMounted?this.withinDropdownIndex===null?!1:this.getTabs().findIndex(e=>e===this.tab){if(t&&document.activeElement&&document.activeElement!==document.body&&this.$el.compareDocumentPosition(document.activeElement)&Node.DOCUMENT_POSITION_PRECEDING)return;let e=this.$el.querySelectorAll(".fi-sc-tabs-tab.fi-active [autofocus]");for(let i of e)if(i.focus(),document.activeElement===i)break})},debouncedUpdateTabsWithinDropdown(){clearTimeout(this.resizeDebounceTimer),this.resizeDebounceTimer=setTimeout(()=>this.updateTabsWithinDropdown(),150)},async updateTabsWithinDropdown(){this.withinDropdownIndex=null,this.withinDropdownMounted=!1,await this.$nextTick();let t=this.$el.querySelector(".fi-tabs"),e=t.querySelector(".fi-tabs-item:last-child"),i=Array.from(t.children).slice(0,-1),a=i.map(s=>s.style.display);i.forEach(s=>s.style.display=""),t.offsetHeight;let l=this.calculateAvailableWidth(t),h=this.calculateContainerGap(t),u=this.calculateDropdownIconWidth(e),b=this.calculateTabItemGap(i[0]),n=this.calculateTabItemPadding(i[0]),c=this.findOverflowIndex(i,l,h,b,n,u);i.forEach((s,o)=>s.style.display=a[o]),c!==-1&&(this.withinDropdownIndex=c),this.withinDropdownMounted=!0},destroy(){this.unsubscribeLivewireHook?.(),this.boundResizeHandler&&window.removeEventListener("resize",this.boundResizeHandler),clearTimeout(this.resizeDebounceTimer)}}}export{v as default}; diff --git a/public/js/filament/schemas/components/wizard.js b/public/js/filament/schemas/components/wizard.js new file mode 100644 index 00000000..56035dd2 --- /dev/null +++ b/public/js/filament/schemas/components/wizard.js @@ -0,0 +1 @@ +function p({isSkippable:i,isStepPersistedInQueryString:n,key:r,startStep:o,stepQueryStringKey:h}){return{step:null,init(){this.step=this.getSteps().at(o-1),this.$watch("step",()=>{this.updateQueryString(),this.autofocusFields()}),this.autofocusFields(!0)},async requestNextStep(){await this.$wire.callSchemaComponentMethod(r,"nextStep",{currentStepIndex:this.getStepIndex(this.step)})},goToNextStep(){let t=this.getStepIndex(this.step)+1;t>=this.getSteps().length||(this.step=this.getSteps()[t],this.scroll())},goToPreviousStep(){let t=this.getStepIndex(this.step)-1;t<0||(this.step=this.getSteps()[t],this.scroll())},goToStep(t){let e=this.getStepIndex(t);e<=-1||!i&&e>this.getStepIndex(this.step)||(this.step=t,this.scroll())},scroll(){this.$nextTick(()=>{this.$refs.header?.children[this.getStepIndex(this.step)].scrollIntoView({behavior:"smooth",block:"start"})})},autofocusFields(t=!1){this.$nextTick(()=>{if(t&&document.activeElement&&document.activeElement!==document.body&&this.$el.compareDocumentPosition(document.activeElement)&Node.DOCUMENT_POSITION_PRECEDING)return;let e=this.$refs[`step-${this.step}`]?.querySelectorAll("[autofocus]")??[];for(let s of e)if(s.focus(),document.activeElement===s)break})},getStepIndex(t){let e=this.getSteps().findIndex(s=>s===t);return e===-1?0:e},getSteps(){return JSON.parse(this.$refs.stepsData.value)},isFirstStep(){return this.getStepIndex(this.step)<=0},isLastStep(){return this.getStepIndex(this.step)+1>=this.getSteps().length},isStepAccessible(t){return i||this.getStepIndex(this.step)>this.getStepIndex(t)},updateQueryString(){if(!n)return;let t=new URL(window.location.href);t.searchParams.set(h,this.step),history.replaceState(null,document.title,t.toString())}}}export{p as default}; diff --git a/public/js/filament/schemas/schemas.js b/public/js/filament/schemas/schemas.js new file mode 100644 index 00000000..53dd5385 --- /dev/null +++ b/public/js/filament/schemas/schemas.js @@ -0,0 +1 @@ +(()=>{var o=()=>({isSticky:!1,width:0,resizeObserver:null,boundUpdateWidth:null,init(){let i=this.$el.parentElement;i&&(this.updateWidth(),this.resizeObserver=new ResizeObserver(()=>this.updateWidth()),this.resizeObserver.observe(i),this.boundUpdateWidth=this.updateWidth.bind(this),window.addEventListener("resize",this.boundUpdateWidth))},enableSticky(){this.isSticky=this.$el.getBoundingClientRect().top>0},disableSticky(){this.isSticky=!1},updateWidth(){let i=this.$el.parentElement;if(!i)return;let e=getComputedStyle(this.$root.querySelector(".fi-ac"));this.width=i.offsetWidth+parseInt(e.marginInlineStart,10)*-1+parseInt(e.marginInlineEnd,10)*-1},destroy(){this.resizeObserver&&(this.resizeObserver.disconnect(),this.resizeObserver=null),this.boundUpdateWidth&&(window.removeEventListener("resize",this.boundUpdateWidth),this.boundUpdateWidth=null)}});var a=function(i,e,n){let t=i;if(e.startsWith("/")&&(n=!0,e=e.slice(1)),n)return e;for(;e.startsWith("../");)t=t.includes(".")?t.slice(0,t.lastIndexOf(".")):null,e=e.slice(3);return["",null,void 0].includes(t)?e:["",null,void 0].includes(e)?t:`${t}.${e}`},d=i=>{let e=Alpine.findClosest(i,n=>n.__livewire);if(!e)throw"Could not find Livewire component in DOM tree.";return e.__livewire};document.addEventListener("alpine:init",()=>{window.Alpine.data("filamentSchema",({livewireId:i})=>({handleFormValidationError(e){e.detail.livewireId===i&&this.$nextTick(()=>{let n=this.$el.querySelector("[data-validation-error]");if(!n)return;let t=n;for(;t;)t.dispatchEvent(new CustomEvent("expand")),t=t.parentNode;setTimeout(()=>n.closest("[data-field-wrapper]").scrollIntoView({behavior:"smooth",block:"start",inline:"start"}),200)})},isStateChanged(e,n){if(e===void 0)return!1;try{return JSON.stringify(e)!==JSON.stringify(n)}catch{return e!==n}}})),window.Alpine.data("filamentSchemaComponent",({path:i,containerPath:e,$wire:n})=>({$statePath:i,$get:(t,r)=>n.$get(a(e,t,r)),$set:(t,r,s,l=!1)=>n.$set(a(e,t,s),r,l),get $state(){return n.$get(i)}})),window.Alpine.data("filamentActionsSchemaComponent",o),Livewire.interceptMessage(({message:i,onSuccess:e})=>{e(({payload:n})=>{n.effects?.dispatches?.forEach(t=>{if(!t.params?.awaitSchemaComponent)return;let r=Array.from(i.component.el.querySelectorAll(`[wire\\:partial="schema-component::${t.params.awaitSchemaComponent}"]`)).filter(s=>d(s)===i.component);if(r.length!==1){if(r.length>1)throw`Multiple schema components found with key [${t.params.awaitSchemaComponent}].`;window.addEventListener(`schema-component-${component.id}-${t.params.awaitSchemaComponent}-loaded`,()=>{window.dispatchEvent(new CustomEvent(t.name,{detail:t.params}))},{once:!0})}})})})});})(); diff --git a/public/js/filament/support/support.js b/public/js/filament/support/support.js new file mode 100644 index 00000000..1847b592 --- /dev/null +++ b/public/js/filament/support/support.js @@ -0,0 +1,46 @@ +(()=>{var qo=Object.create;var Ti=Object.defineProperty;var Go=Object.getOwnPropertyDescriptor;var Ko=Object.getOwnPropertyNames;var Jo=Object.getPrototypeOf,Qo=Object.prototype.hasOwnProperty;var Kr=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports);var Zo=(e,t,r,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let i of Ko(t))!Qo.call(e,i)&&i!==r&&Ti(e,i,{get:()=>t[i],enumerable:!(n=Go(t,i))||n.enumerable});return e};var ea=(e,t,r)=>(r=e!=null?qo(Jo(e)):{},Zo(t||!e||!e.__esModule?Ti(r,"default",{value:e,enumerable:!0}):r,e));var uo=Kr(()=>{});var po=Kr(()=>{});var ho=Kr((Hs,yr)=>{(function(){"use strict";var e="input is invalid type",t="finalize already called",r=typeof window=="object",n=r?window:{};n.JS_MD5_NO_WINDOW&&(r=!1);var i=!r&&typeof self=="object",o=!n.JS_MD5_NO_NODE_JS&&typeof process=="object"&&process.versions&&process.versions.node;o?n=global:i&&(n=self);var a=!n.JS_MD5_NO_COMMON_JS&&typeof yr=="object"&&yr.exports,c=typeof define=="function"&&define.amd,f=!n.JS_MD5_NO_ARRAY_BUFFER&&typeof ArrayBuffer<"u",u="0123456789abcdef".split(""),y=[128,32768,8388608,-2147483648],m=[0,8,16,24],O=["hex","array","digest","buffer","arrayBuffer","base64"],S="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split(""),x=[],M;if(f){var I=new ArrayBuffer(68);M=new Uint8Array(I),x=new Uint32Array(I)}var $=Array.isArray;(n.JS_MD5_NO_NODE_JS||!$)&&($=function(l){return Object.prototype.toString.call(l)==="[object Array]"});var A=ArrayBuffer.isView;f&&(n.JS_MD5_NO_ARRAY_BUFFER_IS_VIEW||!A)&&(A=function(l){return typeof l=="object"&&l.buffer&&l.buffer.constructor===ArrayBuffer});var N=function(l){var h=typeof l;if(h==="string")return[l,!0];if(h!=="object"||l===null)throw new Error(e);if(f&&l.constructor===ArrayBuffer)return[new Uint8Array(l),!1];if(!$(l)&&!A(l))throw new Error(e);return[l,!1]},Y=function(l){return function(h){return new X(!0).update(h)[l]()}},ne=function(){var l=Y("hex");o&&(l=J(l)),l.create=function(){return new X},l.update=function(p){return l.create().update(p)};for(var h=0;h>>6,ze[P++]=128|p&63):p<55296||p>=57344?(ze[P++]=224|p>>>12,ze[P++]=128|p>>>6&63,ze[P++]=128|p&63):(p=65536+((p&1023)<<10|l.charCodeAt(++j)&1023),ze[P++]=240|p>>>18,ze[P++]=128|p>>>12&63,ze[P++]=128|p>>>6&63,ze[P++]=128|p&63);else for(P=this.start;j>>2]|=p<>>2]|=(192|p>>>6)<>>2]|=(128|p&63)<=57344?(Z[P>>>2]|=(224|p>>>12)<>>2]|=(128|p>>>6&63)<>>2]|=(128|p&63)<>>2]|=(240|p>>>18)<>>2]|=(128|p>>>12&63)<>>2]|=(128|p>>>6&63)<>>2]|=(128|p&63)<>>2]|=l[j]<=64?(this.start=P-64,this.hash(),this.hashed=!0):this.start=P}return this.bytes>4294967295&&(this.hBytes+=this.bytes/4294967296<<0,this.bytes=this.bytes%4294967296),this},X.prototype.finalize=function(){if(!this.finalized){this.finalized=!0;var l=this.blocks,h=this.lastByteIndex;l[h>>>2]|=y[h&3],h>=56&&(this.hashed||this.hash(),l[0]=l[16],l[16]=l[1]=l[2]=l[3]=l[4]=l[5]=l[6]=l[7]=l[8]=l[9]=l[10]=l[11]=l[12]=l[13]=l[14]=l[15]=0),l[14]=this.bytes<<3,l[15]=this.hBytes<<3|this.bytes>>>29,this.hash()}},X.prototype.hash=function(){var l,h,v,p,j,P,R=this.blocks;this.first?(l=R[0]-680876937,l=(l<<7|l>>>25)-271733879<<0,p=(-1732584194^l&2004318071)+R[1]-117830708,p=(p<<12|p>>>20)+l<<0,v=(-271733879^p&(l^-271733879))+R[2]-1126478375,v=(v<<17|v>>>15)+p<<0,h=(l^v&(p^l))+R[3]-1316259209,h=(h<<22|h>>>10)+v<<0):(l=this.h0,h=this.h1,v=this.h2,p=this.h3,l+=(p^h&(v^p))+R[0]-680876936,l=(l<<7|l>>>25)+h<<0,p+=(v^l&(h^v))+R[1]-389564586,p=(p<<12|p>>>20)+l<<0,v+=(h^p&(l^h))+R[2]+606105819,v=(v<<17|v>>>15)+p<<0,h+=(l^v&(p^l))+R[3]-1044525330,h=(h<<22|h>>>10)+v<<0),l+=(p^h&(v^p))+R[4]-176418897,l=(l<<7|l>>>25)+h<<0,p+=(v^l&(h^v))+R[5]+1200080426,p=(p<<12|p>>>20)+l<<0,v+=(h^p&(l^h))+R[6]-1473231341,v=(v<<17|v>>>15)+p<<0,h+=(l^v&(p^l))+R[7]-45705983,h=(h<<22|h>>>10)+v<<0,l+=(p^h&(v^p))+R[8]+1770035416,l=(l<<7|l>>>25)+h<<0,p+=(v^l&(h^v))+R[9]-1958414417,p=(p<<12|p>>>20)+l<<0,v+=(h^p&(l^h))+R[10]-42063,v=(v<<17|v>>>15)+p<<0,h+=(l^v&(p^l))+R[11]-1990404162,h=(h<<22|h>>>10)+v<<0,l+=(p^h&(v^p))+R[12]+1804603682,l=(l<<7|l>>>25)+h<<0,p+=(v^l&(h^v))+R[13]-40341101,p=(p<<12|p>>>20)+l<<0,v+=(h^p&(l^h))+R[14]-1502002290,v=(v<<17|v>>>15)+p<<0,h+=(l^v&(p^l))+R[15]+1236535329,h=(h<<22|h>>>10)+v<<0,l+=(v^p&(h^v))+R[1]-165796510,l=(l<<5|l>>>27)+h<<0,p+=(h^v&(l^h))+R[6]-1069501632,p=(p<<9|p>>>23)+l<<0,v+=(l^h&(p^l))+R[11]+643717713,v=(v<<14|v>>>18)+p<<0,h+=(p^l&(v^p))+R[0]-373897302,h=(h<<20|h>>>12)+v<<0,l+=(v^p&(h^v))+R[5]-701558691,l=(l<<5|l>>>27)+h<<0,p+=(h^v&(l^h))+R[10]+38016083,p=(p<<9|p>>>23)+l<<0,v+=(l^h&(p^l))+R[15]-660478335,v=(v<<14|v>>>18)+p<<0,h+=(p^l&(v^p))+R[4]-405537848,h=(h<<20|h>>>12)+v<<0,l+=(v^p&(h^v))+R[9]+568446438,l=(l<<5|l>>>27)+h<<0,p+=(h^v&(l^h))+R[14]-1019803690,p=(p<<9|p>>>23)+l<<0,v+=(l^h&(p^l))+R[3]-187363961,v=(v<<14|v>>>18)+p<<0,h+=(p^l&(v^p))+R[8]+1163531501,h=(h<<20|h>>>12)+v<<0,l+=(v^p&(h^v))+R[13]-1444681467,l=(l<<5|l>>>27)+h<<0,p+=(h^v&(l^h))+R[2]-51403784,p=(p<<9|p>>>23)+l<<0,v+=(l^h&(p^l))+R[7]+1735328473,v=(v<<14|v>>>18)+p<<0,h+=(p^l&(v^p))+R[12]-1926607734,h=(h<<20|h>>>12)+v<<0,j=h^v,l+=(j^p)+R[5]-378558,l=(l<<4|l>>>28)+h<<0,p+=(j^l)+R[8]-2022574463,p=(p<<11|p>>>21)+l<<0,P=p^l,v+=(P^h)+R[11]+1839030562,v=(v<<16|v>>>16)+p<<0,h+=(P^v)+R[14]-35309556,h=(h<<23|h>>>9)+v<<0,j=h^v,l+=(j^p)+R[1]-1530992060,l=(l<<4|l>>>28)+h<<0,p+=(j^l)+R[4]+1272893353,p=(p<<11|p>>>21)+l<<0,P=p^l,v+=(P^h)+R[7]-155497632,v=(v<<16|v>>>16)+p<<0,h+=(P^v)+R[10]-1094730640,h=(h<<23|h>>>9)+v<<0,j=h^v,l+=(j^p)+R[13]+681279174,l=(l<<4|l>>>28)+h<<0,p+=(j^l)+R[0]-358537222,p=(p<<11|p>>>21)+l<<0,P=p^l,v+=(P^h)+R[3]-722521979,v=(v<<16|v>>>16)+p<<0,h+=(P^v)+R[6]+76029189,h=(h<<23|h>>>9)+v<<0,j=h^v,l+=(j^p)+R[9]-640364487,l=(l<<4|l>>>28)+h<<0,p+=(j^l)+R[12]-421815835,p=(p<<11|p>>>21)+l<<0,P=p^l,v+=(P^h)+R[15]+530742520,v=(v<<16|v>>>16)+p<<0,h+=(P^v)+R[2]-995338651,h=(h<<23|h>>>9)+v<<0,l+=(v^(h|~p))+R[0]-198630844,l=(l<<6|l>>>26)+h<<0,p+=(h^(l|~v))+R[7]+1126891415,p=(p<<10|p>>>22)+l<<0,v+=(l^(p|~h))+R[14]-1416354905,v=(v<<15|v>>>17)+p<<0,h+=(p^(v|~l))+R[5]-57434055,h=(h<<21|h>>>11)+v<<0,l+=(v^(h|~p))+R[12]+1700485571,l=(l<<6|l>>>26)+h<<0,p+=(h^(l|~v))+R[3]-1894986606,p=(p<<10|p>>>22)+l<<0,v+=(l^(p|~h))+R[10]-1051523,v=(v<<15|v>>>17)+p<<0,h+=(p^(v|~l))+R[1]-2054922799,h=(h<<21|h>>>11)+v<<0,l+=(v^(h|~p))+R[8]+1873313359,l=(l<<6|l>>>26)+h<<0,p+=(h^(l|~v))+R[15]-30611744,p=(p<<10|p>>>22)+l<<0,v+=(l^(p|~h))+R[6]-1560198380,v=(v<<15|v>>>17)+p<<0,h+=(p^(v|~l))+R[13]+1309151649,h=(h<<21|h>>>11)+v<<0,l+=(v^(h|~p))+R[4]-145523070,l=(l<<6|l>>>26)+h<<0,p+=(h^(l|~v))+R[11]-1120210379,p=(p<<10|p>>>22)+l<<0,v+=(l^(p|~h))+R[2]+718787259,v=(v<<15|v>>>17)+p<<0,h+=(p^(v|~l))+R[9]-343485551,h=(h<<21|h>>>11)+v<<0,this.first?(this.h0=l+1732584193<<0,this.h1=h-271733879<<0,this.h2=v-1732584194<<0,this.h3=p+271733878<<0,this.first=!1):(this.h0=this.h0+l<<0,this.h1=this.h1+h<<0,this.h2=this.h2+v<<0,this.h3=this.h3+p<<0)},X.prototype.hex=function(){this.finalize();var l=this.h0,h=this.h1,v=this.h2,p=this.h3;return u[l>>>4&15]+u[l&15]+u[l>>>12&15]+u[l>>>8&15]+u[l>>>20&15]+u[l>>>16&15]+u[l>>>28&15]+u[l>>>24&15]+u[h>>>4&15]+u[h&15]+u[h>>>12&15]+u[h>>>8&15]+u[h>>>20&15]+u[h>>>16&15]+u[h>>>28&15]+u[h>>>24&15]+u[v>>>4&15]+u[v&15]+u[v>>>12&15]+u[v>>>8&15]+u[v>>>20&15]+u[v>>>16&15]+u[v>>>28&15]+u[v>>>24&15]+u[p>>>4&15]+u[p&15]+u[p>>>12&15]+u[p>>>8&15]+u[p>>>20&15]+u[p>>>16&15]+u[p>>>28&15]+u[p>>>24&15]},X.prototype.toString=X.prototype.hex,X.prototype.digest=function(){this.finalize();var l=this.h0,h=this.h1,v=this.h2,p=this.h3;return[l&255,l>>>8&255,l>>>16&255,l>>>24&255,h&255,h>>>8&255,h>>>16&255,h>>>24&255,v&255,v>>>8&255,v>>>16&255,v>>>24&255,p&255,p>>>8&255,p>>>16&255,p>>>24&255]},X.prototype.array=X.prototype.digest,X.prototype.arrayBuffer=function(){this.finalize();var l=new ArrayBuffer(16),h=new Uint32Array(l);return h[0]=this.h0,h[1]=this.h1,h[2]=this.h2,h[3]=this.h3,l},X.prototype.buffer=X.prototype.arrayBuffer,X.prototype.base64=function(){for(var l,h,v,p="",j=this.array(),P=0;P<15;)l=j[P++],h=j[P++],v=j[P++],p+=S[l>>>2]+S[(l<<4|h>>>4)&63]+S[(h<<2|v>>>6)&63]+S[v&63];return l=j[P],p+=S[l>>>2]+S[l<<4&63]+"==",p};function Q(l,h){var v,p=N(l);if(l=p[0],p[1]){var j=[],P=l.length,R=0,Z;for(v=0;v>>6,j[R++]=128|Z&63):Z<55296||Z>=57344?(j[R++]=224|Z>>>12,j[R++]=128|Z>>>6&63,j[R++]=128|Z&63):(Z=65536+((Z&1023)<<10|l.charCodeAt(++v)&1023),j[R++]=240|Z>>>18,j[R++]=128|Z>>>12&63,j[R++]=128|Z>>>6&63,j[R++]=128|Z&63);l=j}l.length>64&&(l=new X(!0).update(l).array());var ze=[],Rt=[];for(v=0;v<64;++v){var Ut=l[v]||0;ze[v]=92^Ut,Rt[v]=54^Ut}X.call(this,h),this.update(Rt),this.oKeyPad=ze,this.inner=!0,this.sharedMemory=h}Q.prototype=new X,Q.prototype.finalize=function(){if(X.prototype.finalize.call(this),this.inner){this.inner=!1;var l=this.array();X.call(this,this.sharedMemory),this.update(this.oKeyPad),this.update(l),X.prototype.finalize.call(this)}};var me=ne();me.md5=me,me.md5.hmac=de(),a?yr.exports=me:(n.md5=me,c&&define(function(){return me}))})()});var Hi=["top","right","bottom","left"],Pi=["start","end"],Mi=Hi.reduce((e,t)=>e.concat(t,t+"-"+Pi[0],t+"-"+Pi[1]),[]),Et=Math.min,tt=Math.max,hr=Math.round,pr=Math.floor,nn=e=>({x:e,y:e}),ta={left:"right",right:"left",bottom:"top",top:"bottom"},na={start:"end",end:"start"};function Jr(e,t,r){return tt(e,Et(t,r))}function jt(e,t){return typeof e=="function"?e(t):e}function pt(e){return e.split("-")[0]}function xt(e){return e.split("-")[1]}function $i(e){return e==="x"?"y":"x"}function Qr(e){return e==="y"?"height":"width"}function Pn(e){return["top","bottom"].includes(pt(e))?"y":"x"}function Zr(e){return $i(Pn(e))}function Wi(e,t,r){r===void 0&&(r=!1);let n=xt(e),i=Zr(e),o=Qr(i),a=i==="x"?n===(r?"end":"start")?"right":"left":n==="start"?"bottom":"top";return t.reference[o]>t.floating[o]&&(a=mr(a)),[a,mr(a)]}function ra(e){let t=mr(e);return[vr(e),t,vr(t)]}function vr(e){return e.replace(/start|end/g,t=>na[t])}function ia(e,t,r){let n=["left","right"],i=["right","left"],o=["top","bottom"],a=["bottom","top"];switch(e){case"top":case"bottom":return r?t?i:n:t?n:i;case"left":case"right":return t?o:a;default:return[]}}function oa(e,t,r,n){let i=xt(e),o=ia(pt(e),r==="start",n);return i&&(o=o.map(a=>a+"-"+i),t&&(o=o.concat(o.map(vr)))),o}function mr(e){return e.replace(/left|right|bottom|top/g,t=>ta[t])}function aa(e){return{top:0,right:0,bottom:0,left:0,...e}}function ei(e){return typeof e!="number"?aa(e):{top:e,right:e,bottom:e,left:e}}function Dn(e){return{...e,top:e.y,left:e.x,right:e.x+e.width,bottom:e.y+e.height}}function Ri(e,t,r){let{reference:n,floating:i}=e,o=Pn(t),a=Zr(t),c=Qr(a),f=pt(t),u=o==="y",y=n.x+n.width/2-i.width/2,m=n.y+n.height/2-i.height/2,O=n[c]/2-i[c]/2,S;switch(f){case"top":S={x:y,y:n.y-i.height};break;case"bottom":S={x:y,y:n.y+n.height};break;case"right":S={x:n.x+n.width,y:m};break;case"left":S={x:n.x-i.width,y:m};break;default:S={x:n.x,y:n.y}}switch(xt(t)){case"start":S[a]-=O*(r&&u?-1:1);break;case"end":S[a]+=O*(r&&u?-1:1);break}return S}var sa=async(e,t,r)=>{let{placement:n="bottom",strategy:i="absolute",middleware:o=[],platform:a}=r,c=o.filter(Boolean),f=await(a.isRTL==null?void 0:a.isRTL(t)),u=await a.getElementRects({reference:e,floating:t,strategy:i}),{x:y,y:m}=Ri(u,n,f),O=n,S={},x=0;for(let M=0;M({name:"arrow",options:e,async fn(t){let{x:r,y:n,placement:i,rects:o,platform:a,elements:c,middlewareData:f}=t,{element:u,padding:y=0}=jt(e,t)||{};if(u==null)return{};let m=ei(y),O={x:r,y:n},S=Zr(i),x=Qr(S),M=await a.getDimensions(u),I=S==="y",$=I?"top":"left",A=I?"bottom":"right",N=I?"clientHeight":"clientWidth",Y=o.reference[x]+o.reference[S]-O[S]-o.floating[x],ne=O[S]-o.reference[S],J=await(a.getOffsetParent==null?void 0:a.getOffsetParent(u)),V=J?J[N]:0;(!V||!await(a.isElement==null?void 0:a.isElement(J)))&&(V=c.floating[N]||o.floating[x]);let de=Y/2-ne/2,X=V/2-M[x]/2-1,Q=Et(m[$],X),me=Et(m[A],X),l=Q,h=V-M[x]-me,v=V/2-M[x]/2+de,p=Jr(l,v,h),j=!f.arrow&&xt(i)!=null&&v!==p&&o.reference[x]/2-(vxt(i)===e),...r.filter(i=>xt(i)!==e)]:r.filter(i=>pt(i)===i)).filter(i=>e?xt(i)===e||(t?vr(i)!==i:!1):!0)}var fa=function(e){return e===void 0&&(e={}),{name:"autoPlacement",options:e,async fn(t){var r,n,i;let{rects:o,middlewareData:a,placement:c,platform:f,elements:u}=t,{crossAxis:y=!1,alignment:m,allowedPlacements:O=Mi,autoAlignment:S=!0,...x}=jt(e,t),M=m!==void 0||O===Mi?ca(m||null,S,O):O,I=await _n(t,x),$=((r=a.autoPlacement)==null?void 0:r.index)||0,A=M[$];if(A==null)return{};let N=Wi(A,o,await(f.isRTL==null?void 0:f.isRTL(u.floating)));if(c!==A)return{reset:{placement:M[0]}};let Y=[I[pt(A)],I[N[0]],I[N[1]]],ne=[...((n=a.autoPlacement)==null?void 0:n.overflows)||[],{placement:A,overflows:Y}],J=M[$+1];if(J)return{data:{index:$+1,overflows:ne},reset:{placement:J}};let V=ne.map(Q=>{let me=xt(Q.placement);return[Q.placement,me&&y?Q.overflows.slice(0,2).reduce((l,h)=>l+h,0):Q.overflows[0],Q.overflows]}).sort((Q,me)=>Q[1]-me[1]),X=((i=V.filter(Q=>Q[2].slice(0,xt(Q[0])?2:3).every(me=>me<=0))[0])==null?void 0:i[0])||V[0][0];return X!==c?{data:{index:$+1,overflows:ne},reset:{placement:X}}:{}}}},ua=function(e){return e===void 0&&(e={}),{name:"flip",options:e,async fn(t){var r,n;let{placement:i,middlewareData:o,rects:a,initialPlacement:c,platform:f,elements:u}=t,{mainAxis:y=!0,crossAxis:m=!0,fallbackPlacements:O,fallbackStrategy:S="bestFit",fallbackAxisSideDirection:x="none",flipAlignment:M=!0,...I}=jt(e,t);if((r=o.arrow)!=null&&r.alignmentOffset)return{};let $=pt(i),A=pt(c)===c,N=await(f.isRTL==null?void 0:f.isRTL(u.floating)),Y=O||(A||!M?[mr(c)]:ra(c));!O&&x!=="none"&&Y.push(...oa(c,M,x,N));let ne=[c,...Y],J=await _n(t,I),V=[],de=((n=o.flip)==null?void 0:n.overflows)||[];if(y&&V.push(J[$]),m){let l=Wi(i,a,N);V.push(J[l[0]],J[l[1]])}if(de=[...de,{placement:i,overflows:V}],!V.every(l=>l<=0)){var X,Q;let l=(((X=o.flip)==null?void 0:X.index)||0)+1,h=ne[l];if(h)return{data:{index:l,overflows:de},reset:{placement:h}};let v=(Q=de.filter(p=>p.overflows[0]<=0).sort((p,j)=>p.overflows[1]-j.overflows[1])[0])==null?void 0:Q.placement;if(!v)switch(S){case"bestFit":{var me;let p=(me=de.map(j=>[j.placement,j.overflows.filter(P=>P>0).reduce((P,R)=>P+R,0)]).sort((j,P)=>j[1]-P[1])[0])==null?void 0:me[0];p&&(v=p);break}case"initialPlacement":v=c;break}if(i!==v)return{reset:{placement:v}}}return{}}}};function Ii(e,t){return{top:e.top-t.height,right:e.right-t.width,bottom:e.bottom-t.height,left:e.left-t.width}}function Li(e){return Hi.some(t=>e[t]>=0)}var da=function(e){return e===void 0&&(e={}),{name:"hide",options:e,async fn(t){let{rects:r}=t,{strategy:n="referenceHidden",...i}=jt(e,t);switch(n){case"referenceHidden":{let o=await _n(t,{...i,elementContext:"reference"}),a=Ii(o,r.reference);return{data:{referenceHiddenOffsets:a,referenceHidden:Li(a)}}}case"escaped":{let o=await _n(t,{...i,altBoundary:!0}),a=Ii(o,r.floating);return{data:{escapedOffsets:a,escaped:Li(a)}}}default:return{}}}}};function Ui(e){let t=Et(...e.map(o=>o.left)),r=Et(...e.map(o=>o.top)),n=tt(...e.map(o=>o.right)),i=tt(...e.map(o=>o.bottom));return{x:t,y:r,width:n-t,height:i-r}}function pa(e){let t=e.slice().sort((i,o)=>i.y-o.y),r=[],n=null;for(let i=0;in.height/2?r.push([o]):r[r.length-1].push(o),n=o}return r.map(i=>Dn(Ui(i)))}var ha=function(e){return e===void 0&&(e={}),{name:"inline",options:e,async fn(t){let{placement:r,elements:n,rects:i,platform:o,strategy:a}=t,{padding:c=2,x:f,y:u}=jt(e,t),y=Array.from(await(o.getClientRects==null?void 0:o.getClientRects(n.reference))||[]),m=pa(y),O=Dn(Ui(y)),S=ei(c);function x(){if(m.length===2&&m[0].left>m[1].right&&f!=null&&u!=null)return m.find(I=>f>I.left-S.left&&fI.top-S.top&&u=2){if(Pn(r)==="y"){let Q=m[0],me=m[m.length-1],l=pt(r)==="top",h=Q.top,v=me.bottom,p=l?Q.left:me.left,j=l?Q.right:me.right,P=j-p,R=v-h;return{top:h,bottom:v,left:p,right:j,width:P,height:R,x:p,y:h}}let I=pt(r)==="left",$=tt(...m.map(Q=>Q.right)),A=Et(...m.map(Q=>Q.left)),N=m.filter(Q=>I?Q.left===A:Q.right===$),Y=N[0].top,ne=N[N.length-1].bottom,J=A,V=$,de=V-J,X=ne-Y;return{top:Y,bottom:ne,left:J,right:V,width:de,height:X,x:J,y:Y}}return O}let M=await o.getElementRects({reference:{getBoundingClientRect:x},floating:n.floating,strategy:a});return i.reference.x!==M.reference.x||i.reference.y!==M.reference.y||i.reference.width!==M.reference.width||i.reference.height!==M.reference.height?{reset:{rects:M}}:{}}}};async function va(e,t){let{placement:r,platform:n,elements:i}=e,o=await(n.isRTL==null?void 0:n.isRTL(i.floating)),a=pt(r),c=xt(r),f=Pn(r)==="y",u=["left","top"].includes(a)?-1:1,y=o&&f?-1:1,m=jt(t,e),{mainAxis:O,crossAxis:S,alignmentAxis:x}=typeof m=="number"?{mainAxis:m,crossAxis:0,alignmentAxis:null}:{mainAxis:0,crossAxis:0,alignmentAxis:null,...m};return c&&typeof x=="number"&&(S=c==="end"?x*-1:x),f?{x:S*y,y:O*u}:{x:O*u,y:S*y}}var Vi=function(e){return e===void 0&&(e=0),{name:"offset",options:e,async fn(t){var r,n;let{x:i,y:o,placement:a,middlewareData:c}=t,f=await va(t,e);return a===((r=c.offset)==null?void 0:r.placement)&&(n=c.arrow)!=null&&n.alignmentOffset?{}:{x:i+f.x,y:o+f.y,data:{...f,placement:a}}}}},ma=function(e){return e===void 0&&(e={}),{name:"shift",options:e,async fn(t){let{x:r,y:n,placement:i}=t,{mainAxis:o=!0,crossAxis:a=!1,limiter:c={fn:I=>{let{x:$,y:A}=I;return{x:$,y:A}}},...f}=jt(e,t),u={x:r,y:n},y=await _n(t,f),m=Pn(pt(i)),O=$i(m),S=u[O],x=u[m];if(o){let I=O==="y"?"top":"left",$=O==="y"?"bottom":"right",A=S+y[I],N=S-y[$];S=Jr(A,S,N)}if(a){let I=m==="y"?"top":"left",$=m==="y"?"bottom":"right",A=x+y[I],N=x-y[$];x=Jr(A,x,N)}let M=c.fn({...t,[O]:S,[m]:x});return{...M,data:{x:M.x-r,y:M.y-n}}}}},ga=function(e){return e===void 0&&(e={}),{name:"size",options:e,async fn(t){let{placement:r,rects:n,platform:i,elements:o}=t,{apply:a=()=>{},...c}=jt(e,t),f=await _n(t,c),u=pt(r),y=xt(r),m=Pn(r)==="y",{width:O,height:S}=n.floating,x,M;u==="top"||u==="bottom"?(x=u,M=y===(await(i.isRTL==null?void 0:i.isRTL(o.floating))?"start":"end")?"left":"right"):(M=u,x=y==="end"?"top":"bottom");let I=S-f[x],$=O-f[M],A=!t.middlewareData.shift,N=I,Y=$;if(m){let J=O-f.left-f.right;Y=y||A?Et($,J):J}else{let J=S-f.top-f.bottom;N=y||A?Et(I,J):J}if(A&&!y){let J=tt(f.left,0),V=tt(f.right,0),de=tt(f.top,0),X=tt(f.bottom,0);m?Y=O-2*(J!==0||V!==0?J+V:tt(f.left,f.right)):N=S-2*(de!==0||X!==0?de+X:tt(f.top,f.bottom))}await a({...t,availableWidth:Y,availableHeight:N});let ne=await i.getDimensions(o.floating);return O!==ne.width||S!==ne.height?{reset:{rects:!0}}:{}}}};function rn(e){return zi(e)?(e.nodeName||"").toLowerCase():"#document"}function ct(e){var t;return(e==null||(t=e.ownerDocument)==null?void 0:t.defaultView)||window}function Bt(e){var t;return(t=(zi(e)?e.ownerDocument:e.document)||window.document)==null?void 0:t.documentElement}function zi(e){return e instanceof Node||e instanceof ct(e).Node}function Nt(e){return e instanceof Element||e instanceof ct(e).Element}function Tt(e){return e instanceof HTMLElement||e instanceof ct(e).HTMLElement}function Fi(e){return typeof ShadowRoot>"u"?!1:e instanceof ShadowRoot||e instanceof ct(e).ShadowRoot}function zn(e){let{overflow:t,overflowX:r,overflowY:n,display:i}=ht(e);return/auto|scroll|overlay|hidden|clip/.test(t+n+r)&&!["inline","contents"].includes(i)}function ba(e){return["table","td","th"].includes(rn(e))}function ti(e){let t=ni(),r=ht(e);return r.transform!=="none"||r.perspective!=="none"||(r.containerType?r.containerType!=="normal":!1)||!t&&(r.backdropFilter?r.backdropFilter!=="none":!1)||!t&&(r.filter?r.filter!=="none":!1)||["transform","perspective","filter"].some(n=>(r.willChange||"").includes(n))||["paint","layout","strict","content"].some(n=>(r.contain||"").includes(n))}function ya(e){let t=Tn(e);for(;Tt(t)&&!gr(t);){if(ti(t))return t;t=Tn(t)}return null}function ni(){return typeof CSS>"u"||!CSS.supports?!1:CSS.supports("-webkit-backdrop-filter","none")}function gr(e){return["html","body","#document"].includes(rn(e))}function ht(e){return ct(e).getComputedStyle(e)}function br(e){return Nt(e)?{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}:{scrollLeft:e.pageXOffset,scrollTop:e.pageYOffset}}function Tn(e){if(rn(e)==="html")return e;let t=e.assignedSlot||e.parentNode||Fi(e)&&e.host||Bt(e);return Fi(t)?t.host:t}function Yi(e){let t=Tn(e);return gr(t)?e.ownerDocument?e.ownerDocument.body:e.body:Tt(t)&&zn(t)?t:Yi(t)}function Vn(e,t,r){var n;t===void 0&&(t=[]),r===void 0&&(r=!0);let i=Yi(e),o=i===((n=e.ownerDocument)==null?void 0:n.body),a=ct(i);return o?t.concat(a,a.visualViewport||[],zn(i)?i:[],a.frameElement&&r?Vn(a.frameElement):[]):t.concat(i,Vn(i,[],r))}function Xi(e){let t=ht(e),r=parseFloat(t.width)||0,n=parseFloat(t.height)||0,i=Tt(e),o=i?e.offsetWidth:r,a=i?e.offsetHeight:n,c=hr(r)!==o||hr(n)!==a;return c&&(r=o,n=a),{width:r,height:n,$:c}}function ri(e){return Nt(e)?e:e.contextElement}function Cn(e){let t=ri(e);if(!Tt(t))return nn(1);let r=t.getBoundingClientRect(),{width:n,height:i,$:o}=Xi(t),a=(o?hr(r.width):r.width)/n,c=(o?hr(r.height):r.height)/i;return(!a||!Number.isFinite(a))&&(a=1),(!c||!Number.isFinite(c))&&(c=1),{x:a,y:c}}var wa=nn(0);function qi(e){let t=ct(e);return!ni()||!t.visualViewport?wa:{x:t.visualViewport.offsetLeft,y:t.visualViewport.offsetTop}}function xa(e,t,r){return t===void 0&&(t=!1),!r||t&&r!==ct(e)?!1:t}function vn(e,t,r,n){t===void 0&&(t=!1),r===void 0&&(r=!1);let i=e.getBoundingClientRect(),o=ri(e),a=nn(1);t&&(n?Nt(n)&&(a=Cn(n)):a=Cn(e));let c=xa(o,r,n)?qi(o):nn(0),f=(i.left+c.x)/a.x,u=(i.top+c.y)/a.y,y=i.width/a.x,m=i.height/a.y;if(o){let O=ct(o),S=n&&Nt(n)?ct(n):n,x=O,M=x.frameElement;for(;M&&n&&S!==x;){let I=Cn(M),$=M.getBoundingClientRect(),A=ht(M),N=$.left+(M.clientLeft+parseFloat(A.paddingLeft))*I.x,Y=$.top+(M.clientTop+parseFloat(A.paddingTop))*I.y;f*=I.x,u*=I.y,y*=I.x,m*=I.y,f+=N,u+=Y,x=ct(M),M=x.frameElement}}return Dn({width:y,height:m,x:f,y:u})}var Ea=[":popover-open",":modal"];function Gi(e){return Ea.some(t=>{try{return e.matches(t)}catch{return!1}})}function Oa(e){let{elements:t,rect:r,offsetParent:n,strategy:i}=e,o=i==="fixed",a=Bt(n),c=t?Gi(t.floating):!1;if(n===a||c&&o)return r;let f={scrollLeft:0,scrollTop:0},u=nn(1),y=nn(0),m=Tt(n);if((m||!m&&!o)&&((rn(n)!=="body"||zn(a))&&(f=br(n)),Tt(n))){let O=vn(n);u=Cn(n),y.x=O.x+n.clientLeft,y.y=O.y+n.clientTop}return{width:r.width*u.x,height:r.height*u.y,x:r.x*u.x-f.scrollLeft*u.x+y.x,y:r.y*u.y-f.scrollTop*u.y+y.y}}function Sa(e){return Array.from(e.getClientRects())}function Ki(e){return vn(Bt(e)).left+br(e).scrollLeft}function Aa(e){let t=Bt(e),r=br(e),n=e.ownerDocument.body,i=tt(t.scrollWidth,t.clientWidth,n.scrollWidth,n.clientWidth),o=tt(t.scrollHeight,t.clientHeight,n.scrollHeight,n.clientHeight),a=-r.scrollLeft+Ki(e),c=-r.scrollTop;return ht(n).direction==="rtl"&&(a+=tt(t.clientWidth,n.clientWidth)-i),{width:i,height:o,x:a,y:c}}function Ca(e,t){let r=ct(e),n=Bt(e),i=r.visualViewport,o=n.clientWidth,a=n.clientHeight,c=0,f=0;if(i){o=i.width,a=i.height;let u=ni();(!u||u&&t==="fixed")&&(c=i.offsetLeft,f=i.offsetTop)}return{width:o,height:a,x:c,y:f}}function Da(e,t){let r=vn(e,!0,t==="fixed"),n=r.top+e.clientTop,i=r.left+e.clientLeft,o=Tt(e)?Cn(e):nn(1),a=e.clientWidth*o.x,c=e.clientHeight*o.y,f=i*o.x,u=n*o.y;return{width:a,height:c,x:f,y:u}}function ki(e,t,r){let n;if(t==="viewport")n=Ca(e,r);else if(t==="document")n=Aa(Bt(e));else if(Nt(t))n=Da(t,r);else{let i=qi(e);n={...t,x:t.x-i.x,y:t.y-i.y}}return Dn(n)}function Ji(e,t){let r=Tn(e);return r===t||!Nt(r)||gr(r)?!1:ht(r).position==="fixed"||Ji(r,t)}function _a(e,t){let r=t.get(e);if(r)return r;let n=Vn(e,[],!1).filter(c=>Nt(c)&&rn(c)!=="body"),i=null,o=ht(e).position==="fixed",a=o?Tn(e):e;for(;Nt(a)&&!gr(a);){let c=ht(a),f=ti(a);!f&&c.position==="fixed"&&(i=null),(o?!f&&!i:!f&&c.position==="static"&&!!i&&["absolute","fixed"].includes(i.position)||zn(a)&&!f&&Ji(e,a))?n=n.filter(y=>y!==a):i=c,a=Tn(a)}return t.set(e,n),n}function Ta(e){let{element:t,boundary:r,rootBoundary:n,strategy:i}=e,a=[...r==="clippingAncestors"?_a(t,this._c):[].concat(r),n],c=a[0],f=a.reduce((u,y)=>{let m=ki(t,y,i);return u.top=tt(m.top,u.top),u.right=Et(m.right,u.right),u.bottom=Et(m.bottom,u.bottom),u.left=tt(m.left,u.left),u},ki(t,c,i));return{width:f.right-f.left,height:f.bottom-f.top,x:f.left,y:f.top}}function Pa(e){let{width:t,height:r}=Xi(e);return{width:t,height:r}}function Ma(e,t,r){let n=Tt(t),i=Bt(t),o=r==="fixed",a=vn(e,!0,o,t),c={scrollLeft:0,scrollTop:0},f=nn(0);if(n||!n&&!o)if((rn(t)!=="body"||zn(i))&&(c=br(t)),n){let m=vn(t,!0,o,t);f.x=m.x+t.clientLeft,f.y=m.y+t.clientTop}else i&&(f.x=Ki(i));let u=a.left+c.scrollLeft-f.x,y=a.top+c.scrollTop-f.y;return{x:u,y,width:a.width,height:a.height}}function Ni(e,t){return!Tt(e)||ht(e).position==="fixed"?null:t?t(e):e.offsetParent}function Qi(e,t){let r=ct(e);if(!Tt(e)||Gi(e))return r;let n=Ni(e,t);for(;n&&ba(n)&&ht(n).position==="static";)n=Ni(n,t);return n&&(rn(n)==="html"||rn(n)==="body"&&ht(n).position==="static"&&!ti(n))?r:n||ya(e)||r}var Ra=async function(e){let t=this.getOffsetParent||Qi,r=this.getDimensions;return{reference:Ma(e.reference,await t(e.floating),e.strategy),floating:{x:0,y:0,...await r(e.floating)}}};function Ia(e){return ht(e).direction==="rtl"}var La={convertOffsetParentRelativeRectToViewportRelativeRect:Oa,getDocumentElement:Bt,getClippingRect:Ta,getOffsetParent:Qi,getElementRects:Ra,getClientRects:Sa,getDimensions:Pa,getScale:Cn,isElement:Nt,isRTL:Ia};function Fa(e,t){let r=null,n,i=Bt(e);function o(){var c;clearTimeout(n),(c=r)==null||c.disconnect(),r=null}function a(c,f){c===void 0&&(c=!1),f===void 0&&(f=1),o();let{left:u,top:y,width:m,height:O}=e.getBoundingClientRect();if(c||t(),!m||!O)return;let S=pr(y),x=pr(i.clientWidth-(u+m)),M=pr(i.clientHeight-(y+O)),I=pr(u),A={rootMargin:-S+"px "+-x+"px "+-M+"px "+-I+"px",threshold:tt(0,Et(1,f))||1},N=!0;function Y(ne){let J=ne[0].intersectionRatio;if(J!==f){if(!N)return a();J?a(!1,J):n=setTimeout(()=>{a(!1,1e-7)},100)}N=!1}try{r=new IntersectionObserver(Y,{...A,root:i.ownerDocument})}catch{r=new IntersectionObserver(Y,A)}r.observe(e)}return a(!0),o}function ji(e,t,r,n){n===void 0&&(n={});let{ancestorScroll:i=!0,ancestorResize:o=!0,elementResize:a=typeof ResizeObserver=="function",layoutShift:c=typeof IntersectionObserver=="function",animationFrame:f=!1}=n,u=ri(e),y=i||o?[...u?Vn(u):[],...Vn(t)]:[];y.forEach($=>{i&&$.addEventListener("scroll",r,{passive:!0}),o&&$.addEventListener("resize",r)});let m=u&&c?Fa(u,r):null,O=-1,S=null;a&&(S=new ResizeObserver($=>{let[A]=$;A&&A.target===u&&S&&(S.unobserve(t),cancelAnimationFrame(O),O=requestAnimationFrame(()=>{var N;(N=S)==null||N.observe(t)})),r()}),u&&!f&&S.observe(u),S.observe(t));let x,M=f?vn(e):null;f&&I();function I(){let $=vn(e);M&&($.x!==M.x||$.y!==M.y||$.width!==M.width||$.height!==M.height)&&r(),M=$,x=requestAnimationFrame(I)}return r(),()=>{var $;y.forEach(A=>{i&&A.removeEventListener("scroll",r),o&&A.removeEventListener("resize",r)}),m?.(),($=S)==null||$.disconnect(),S=null,f&&cancelAnimationFrame(x)}}var ii=fa,Zi=ma,eo=ua,to=ga,no=da,ro=la,io=ha,Bi=(e,t,r)=>{let n=new Map,i={platform:La,...r},o={...i.platform,_c:n};return sa(e,t,{...i,platform:o})},ka=e=>{let t={placement:"bottom",strategy:"absolute",middleware:[]},r=Object.keys(e),n=i=>e[i];return r.includes("offset")&&t.middleware.push(Vi(n("offset"))),r.includes("teleport")&&(t.strategy="fixed"),r.includes("placement")&&(t.placement=n("placement")),r.some(i=>/^auto-?placement$/i.test(i))&&!r.includes("flip")&&t.middleware.push(ii(n("autoPlacement"))),r.includes("flip")&&t.middleware.push(eo(n("flip"))),r.includes("shift")&&t.middleware.push(Zi(n("shift"))),r.includes("inline")&&t.middleware.push(io(n("inline"))),r.includes("arrow")&&t.middleware.push(ro(n("arrow"))),r.includes("hide")&&t.middleware.push(no(n("hide"))),r.includes("size")&&t.middleware.push(to(n("size"))),t},Na=(e,t)=>{let r={component:{trap:!1},float:{placement:"bottom",strategy:"absolute",middleware:[]}},n=i=>e[e.indexOf(i)+1];if(e.includes("trap")&&(r.component.trap=!0),e.includes("teleport")&&(r.float.strategy="fixed"),e.includes("offset")&&r.float.middleware.push(Vi(t.offset||10)),e.includes("placement")&&(r.float.placement=n("placement")),e.some(i=>/^auto-?placement$/i.test(i))&&!e.includes("flip")&&r.float.middleware.push(ii(t.autoPlacement)),e.includes("flip")&&r.float.middleware.push(eo(t.flip)),e.includes("shift")&&r.float.middleware.push(Zi(t.shift)),e.includes("inline")&&r.float.middleware.push(io(t.inline)),e.includes("arrow")&&r.float.middleware.push(ro(t.arrow)),e.includes("hide")&&r.float.middleware.push(no(t.hide)),e.includes("size")){let i=t.size?.availableWidth??null,o=t.size?.availableHeight??null;i&&delete t.size.availableWidth,o&&delete t.size.availableHeight,r.float.middleware.push(to({...t.size,apply({availableWidth:a,availableHeight:c,elements:f}){Object.assign(f.floating.style,{maxWidth:`${i??a}px`,maxHeight:`${o??c}px`})}}))}return r},ja=e=>{var t="0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz".split(""),r="";e||(e=Math.floor(Math.random()*t.length));for(var n=0;n{}){let r=!1;return function(){r?t.apply(this,arguments):(r=!0,e.apply(this,arguments))}}function Ha(e){let t={dismissable:!0,trap:!1};function r(n,i=null){if(n){if(n.hasAttribute("aria-expanded")||n.setAttribute("aria-expanded",!1),i.hasAttribute("id"))n.setAttribute("aria-controls",i.getAttribute("id"));else{let o=`panel-${ja(8)}`;n.setAttribute("aria-controls",o),i.setAttribute("id",o)}i.setAttribute("aria-modal",!0),i.setAttribute("role","dialog")}}e.magic("float",n=>(i={},o={})=>{let a={...t,...o},c=Object.keys(i).length>0?ka(i):{middleware:[ii()]},f=n,u=n.parentElement.closest("[x-data]"),y=u.querySelector('[x-ref="panel"]');r(f,y);function m(){return y.style.display=="block"}function O(){y.style.display="none",f.setAttribute("aria-expanded","false"),a.trap&&y.setAttribute("x-trap","false"),ji(n,y,M)}function S(){y.style.display="block",f.setAttribute("aria-expanded","true"),a.trap&&y.setAttribute("x-trap","true"),M()}function x(){m()?O():S()}async function M(){return await Bi(n,y,c).then(({middlewareData:I,placement:$,x:A,y:N})=>{if(I.arrow){let Y=I.arrow?.x,ne=I.arrow?.y,J=c.middleware.filter(de=>de.name=="arrow")[0].options.element,V={top:"bottom",right:"left",bottom:"top",left:"right"}[$.split("-")[0]];Object.assign(J.style,{left:Y!=null?`${Y}px`:"",top:ne!=null?`${ne}px`:"",right:"",bottom:"",[V]:"-4px"})}if(I.hide){let{referenceHidden:Y}=I.hide;Object.assign(y.style,{visibility:Y?"hidden":"visible"})}Object.assign(y.style,{left:`${A}px`,top:`${N}px`})})}a.dismissable&&(window.addEventListener("click",I=>{!u.contains(I.target)&&m()&&x()}),window.addEventListener("keydown",I=>{I.key==="Escape"&&m()&&x()},!0)),x()}),e.directive("float",(n,{modifiers:i,expression:o},{evaluate:a,effect:c})=>{let f=o?a(o):{},u=i.length>0?Na(i,f):{},y=null;u.float.strategy=="fixed"&&(n.style.position="fixed");let m=V=>n.parentElement&&!n.parentElement.closest("[x-data]").contains(V.target)?n.close():null,O=V=>V.key==="Escape"?n.close():null,S=n.getAttribute("x-ref"),x=n.parentElement.closest("[x-data]"),M=x.querySelectorAll(`[\\@click^="$refs.${S}"]`),I=x.querySelectorAll(`[x-on\\:click^="$refs.${S}"]`);n.style.setProperty("display","none"),r([...M,...I][0],n),n._x_isShown=!1,n.trigger=null,n._x_doHide||(n._x_doHide=()=>{n.style.setProperty("display","none",i.includes("important")?"important":void 0)}),n._x_doShow||(n._x_doShow=()=>{n.style.setProperty("display","block",i.includes("important")?"important":void 0)});let $=()=>{n._x_doHide(),n._x_isShown=!1},A=()=>{n._x_doShow(),n._x_isShown=!0},N=()=>setTimeout(A),Y=Ba(V=>V?A():$(),V=>{typeof n._x_toggleAndCascadeWithTransitions=="function"?n._x_toggleAndCascadeWithTransitions(n,V,A,$):V?N():$()}),ne,J=!0;c(()=>a(V=>{!J&&V===ne||(i.includes("immediate")&&(V?N():$()),Y(V),ne=V,J=!1)})),n.open=async function(V){n.trigger=V.currentTarget?V.currentTarget:V,Y(!0),n.trigger.setAttribute("aria-expanded","true"),u.component.trap&&n.setAttribute("x-trap","true"),y=ji(n.trigger,n,()=>{Bi(n.trigger,n,u.float).then(({middlewareData:de,placement:X,x:Q,y:me})=>{if(de.arrow){let l=de.arrow?.x,h=de.arrow?.y,v=u.float.middleware.filter(j=>j.name=="arrow")[0].options.element,p={top:"bottom",right:"left",bottom:"top",left:"right"}[X.split("-")[0]];Object.assign(v.style,{left:l!=null?`${l}px`:"",top:h!=null?`${h}px`:"",right:"",bottom:"",[p]:"-4px"})}if(de.hide){let{referenceHidden:l}=de.hide;Object.assign(n.style,{visibility:l?"hidden":"visible"})}Object.assign(n.style,{left:`${Q}px`,top:`${me}px`})})}),window.addEventListener("click",m),window.addEventListener("keydown",O,!0)},n.close=function(){if(!n._x_isShown)return!1;Y(!1),n.trigger.setAttribute("aria-expanded","false"),u.component.trap&&n.setAttribute("x-trap","false"),y(),window.removeEventListener("click",m),window.removeEventListener("keydown",O,!1)},n.toggle=function(V){n._x_isShown?n.close():n.open(V)}})}var oo=Ha;function $a(e){e.store("lazyLoadedAssets",{loaded:new Set,check(a){return Array.isArray(a)?a.every(c=>this.loaded.has(c)):this.loaded.has(a)},markLoaded(a){Array.isArray(a)?a.forEach(c=>this.loaded.add(c)):this.loaded.add(a)}});let t=a=>new CustomEvent(a,{bubbles:!0,composed:!0,cancelable:!0}),r=(a,c={},f,u)=>{let y=document.createElement(a);return Object.entries(c).forEach(([m,O])=>y[m]=O),f&&(u?f.insertBefore(y,u):f.appendChild(y)),y},n=(a,c,f={},u=null,y=null)=>{let m=a==="link"?`link[href="${c}"]`:`script[src="${c}"]`;if(document.querySelector(m)||e.store("lazyLoadedAssets").check(c))return Promise.resolve();let O=a==="link"?{...f,href:c}:{...f,src:c},S=r(a,O,u,y);return new Promise((x,M)=>{S.onload=()=>{e.store("lazyLoadedAssets").markLoaded(c),x()},S.onerror=()=>{M(new Error(`Failed to load ${a}: ${c}`))}})},i=async(a,c,f=null,u=null)=>{let y={type:"text/css",rel:"stylesheet"};c&&(y.media=c);let m=document.head,O=null;if(f&&u){let S=document.querySelector(`link[href*="${u}"]`);S?(m=S.parentElement,O=f==="before"?S:S.nextSibling):(console.warn(`Target (${u}) not found for ${a}. Appending to head.`),m=document.head,O=null)}await n("link",a,y,m,O)},o=async(a,c,f=null,u=null,y=null)=>{let m=document.head,O=null;if(f&&u){let x=document.querySelector(`script[src*="${u}"]`);x?(m=x.parentElement,O=f==="before"?x:x.nextSibling):(console.warn(`Target (${u}) not found for ${a}. Falling back to head or body.`),m=document.head,O=null)}else(c.has("body-start")||c.has("body-end"))&&(m=document.body,c.has("body-start")&&(O=document.body.firstChild));let S={};y&&(S.type="module"),await n("script",a,S,m,O)};e.directive("load-css",(a,{expression:c},{evaluate:f})=>{let u=f(c),y=a.media,m=a.getAttribute("data-dispatch"),O=a.getAttribute("data-css-before")?"before":a.getAttribute("data-css-after")?"after":null,S=a.getAttribute("data-css-before")||a.getAttribute("data-css-after")||null;Promise.all(u.map(x=>i(x,y,O,S))).then(()=>{m&&window.dispatchEvent(t(`${m}-css`))}).catch(console.error)}),e.directive("load-js",(a,{expression:c,modifiers:f},{evaluate:u})=>{let y=u(c),m=new Set(f),O=a.getAttribute("data-js-before")?"before":a.getAttribute("data-js-after")?"after":null,S=a.getAttribute("data-js-before")||a.getAttribute("data-js-after")||null,x=a.getAttribute("data-js-as-module")||a.getAttribute("data-as-module")||!1,M=a.getAttribute("data-dispatch");Promise.all(y.map(I=>o(I,m,O,S,x))).then(()=>{M&&window.dispatchEvent(t(`${M}-js`))}).catch(console.error)})}var ao=$a;function Wa(){return!0}function Ua({component:e,argument:t}){return new Promise(r=>{if(t)window.addEventListener(t,()=>r(),{once:!0});else{let n=i=>{i.detail.id===e.id&&(window.removeEventListener("async-alpine:load",n),r())};window.addEventListener("async-alpine:load",n)}})}function Va(){return new Promise(e=>{"requestIdleCallback"in window?window.requestIdleCallback(e):setTimeout(e,200)})}function za({argument:e}){return new Promise(t=>{if(!e)return console.log("Async Alpine: media strategy requires a media query. Treating as 'eager'"),t();let r=window.matchMedia(`(${e})`);r.matches?t():r.addEventListener("change",t,{once:!0})})}function Ya({component:e,argument:t}){return new Promise(r=>{let n=t||"0px 0px 0px 0px",i=new IntersectionObserver(o=>{o[0].isIntersecting&&(i.disconnect(),r())},{rootMargin:n});i.observe(e.el)})}var so={eager:Wa,event:Ua,idle:Va,media:za,visible:Ya};async function Xa(e){let t=qa(e.strategy);await oi(e,t)}async function oi(e,t){if(t.type==="expression"){if(t.operator==="&&")return Promise.all(t.parameters.map(r=>oi(e,r)));if(t.operator==="||")return Promise.any(t.parameters.map(r=>oi(e,r)))}return so[t.method]?so[t.method]({component:e,argument:t.argument}):!1}function qa(e){let t=Ga(e),r=co(t);return r.type==="method"?{type:"expression",operator:"&&",parameters:[r]}:r}function Ga(e){let t=/\s*([()])\s*|\s*(\|\||&&|\|)\s*|\s*((?:[^()&|]+\([^()]+\))|[^()&|]+)\s*/g,r=[],n;for(;(n=t.exec(e))!==null;){let[i,o,a,c]=n;if(o!==void 0)r.push({type:"parenthesis",value:o});else if(a!==void 0)r.push({type:"operator",value:a==="|"?"&&":a});else{let f={type:"method",method:c.trim()};c.includes("(")&&(f.method=c.substring(0,c.indexOf("(")).trim(),f.argument=c.substring(c.indexOf("(")+1,c.indexOf(")"))),c.method==="immediate"&&(c.method="eager"),r.push(f)}}return r}function co(e){let t=lo(e);for(;e.length>0&&(e[0].value==="&&"||e[0].value==="|"||e[0].value==="||");){let r=e.shift().value,n=lo(e);t.type==="expression"&&t.operator===r?t.parameters.push(n):t={type:"expression",operator:r,parameters:[t,n]}}return t}function lo(e){if(e[0].value==="("){e.shift();let t=co(e);return e[0].value===")"&&e.shift(),t}else return e.shift()}function fo(e){let t="load",r=e.prefixed("load-src"),n=e.prefixed("ignore"),i={defaultStrategy:"eager",keepRelativeURLs:!1},o=!1,a={},c=0;function f(){return c++}e.asyncOptions=A=>{i={...i,...A}},e.asyncData=(A,N=!1)=>{a[A]={loaded:!1,download:N}},e.asyncUrl=(A,N)=>{!A||!N||a[A]||(a[A]={loaded:!1,download:()=>import($(N))})},e.asyncAlias=A=>{o=A};let u=A=>{e.skipDuringClone(()=>{A._x_async||(A._x_async="init",A._x_ignore=!0,A.setAttribute(n,""))})()},y=async A=>{e.skipDuringClone(async()=>{if(A._x_async!=="init")return;A._x_async="await";let{name:N,strategy:Y}=m(A);await Xa({name:N,strategy:Y,el:A,id:A.id||f()}),A.isConnected&&(await O(N),A.isConnected&&(x(A),A._x_async="loaded"))})()};y.inline=u,e.directive(t,y).before("ignore");function m(A){let N=I(A.getAttribute(e.prefixed("data"))),Y=A.getAttribute(e.prefixed(t))||i.defaultStrategy,ne=A.getAttribute(r);return ne&&e.asyncUrl(N,ne),{name:N,strategy:Y}}async function O(A){if(A.startsWith("_x_async_")||(M(A),!a[A]||a[A].loaded))return;let N=await S(A);e.data(A,N),a[A].loaded=!0}async function S(A){if(!a[A])return;let N=await a[A].download(A);return typeof N=="function"?N:N[A]||N.default||Object.values(N)[0]||!1}function x(A){e.destroyTree(A),A._x_ignore=!1,A.removeAttribute(n),!A.closest(`[${n}]`)&&e.initTree(A)}function M(A){if(!(!o||a[A])){if(typeof o=="function"){e.asyncData(A,o);return}e.asyncUrl(A,o.replaceAll("[name]",A))}}function I(A){return(A||"").trim().split(/[({]/g)[0]||`_x_async_${f()}`}function $(A){return i.keepRelativeURLs||new RegExp("^(?:[a-z+]+:)?//","i").test(A)?A:new URL(A,document.baseURI).href}}var Xo=ea(ho(),1);function vo(e,t){var r=Object.keys(e);if(Object.getOwnPropertySymbols){var n=Object.getOwnPropertySymbols(e);t&&(n=n.filter(function(i){return Object.getOwnPropertyDescriptor(e,i).enumerable})),r.push.apply(r,n)}return r}function Mt(e){for(var t=1;t=0)&&(r[i]=e[i]);return r}function Qa(e,t){if(e==null)return{};var r=Ja(e,t),n,i;if(Object.getOwnPropertySymbols){var o=Object.getOwnPropertySymbols(e);for(i=0;i=0)&&Object.prototype.propertyIsEnumerable.call(e,n)&&(r[n]=e[n])}return r}var Za="1.15.6";function Ht(e){if(typeof window<"u"&&window.navigator)return!!navigator.userAgent.match(e)}var Wt=Ht(/(?:Trident.*rv[ :]?11\.|msie|iemobile|Windows Phone)/i),er=Ht(/Edge/i),mo=Ht(/firefox/i),Gn=Ht(/safari/i)&&!Ht(/chrome/i)&&!Ht(/android/i),yi=Ht(/iP(ad|od|hone)/i),So=Ht(/chrome/i)&&Ht(/android/i),Ao={capture:!1,passive:!1};function Oe(e,t,r){e.addEventListener(t,r,!Wt&&Ao)}function Ee(e,t,r){e.removeEventListener(t,r,!Wt&&Ao)}function Tr(e,t){if(t){if(t[0]===">"&&(t=t.substring(1)),e)try{if(e.matches)return e.matches(t);if(e.msMatchesSelector)return e.msMatchesSelector(t);if(e.webkitMatchesSelector)return e.webkitMatchesSelector(t)}catch{return!1}return!1}}function Co(e){return e.host&&e!==document&&e.host.nodeType?e.host:e.parentNode}function St(e,t,r,n){if(e){r=r||document;do{if(t!=null&&(t[0]===">"?e.parentNode===r&&Tr(e,t):Tr(e,t))||n&&e===r)return e;if(e===r)break}while(e=Co(e))}return null}var go=/\s+/g;function ft(e,t,r){if(e&&t)if(e.classList)e.classList[r?"add":"remove"](t);else{var n=(" "+e.className+" ").replace(go," ").replace(" "+t+" "," ");e.className=(n+(r?" "+t:"")).replace(go," ")}}function ae(e,t,r){var n=e&&e.style;if(n){if(r===void 0)return document.defaultView&&document.defaultView.getComputedStyle?r=document.defaultView.getComputedStyle(e,""):e.currentStyle&&(r=e.currentStyle),t===void 0?r:r[t];!(t in n)&&t.indexOf("webkit")===-1&&(t="-webkit-"+t),n[t]=r+(typeof r=="string"?"":"px")}}function Fn(e,t){var r="";if(typeof e=="string")r=e;else do{var n=ae(e,"transform");n&&n!=="none"&&(r=n+" "+r)}while(!t&&(e=e.parentNode));var i=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return i&&new i(r)}function Do(e,t,r){if(e){var n=e.getElementsByTagName(t),i=0,o=n.length;if(r)for(;i=o:a=i<=o,!a)return n;if(n===Pt())break;n=sn(n,!1)}return!1}function kn(e,t,r,n){for(var i=0,o=0,a=e.children;o2&&arguments[2]!==void 0?arguments[2]:{},i=n.evt,o=Qa(n,ss);tr.pluginEvent.bind(se)(t,r,Mt({dragEl:k,parentEl:Ve,ghostEl:ue,rootEl:Ne,nextEl:bn,lastDownEl:Ar,cloneEl:We,cloneHidden:an,dragStarted:Yn,putSortable:Qe,activeSortable:se.active,originalEvent:i,oldIndex:Ln,oldDraggableIndex:Jn,newIndex:ut,newDraggableIndex:on,hideGhostForTarget:Fo,unhideGhostForTarget:ko,cloneNowHidden:function(){an=!0},cloneNowShown:function(){an=!1},dispatchSortableEvent:function(c){it({sortable:r,name:c,originalEvent:i})}},o))};function it(e){as(Mt({putSortable:Qe,cloneEl:We,targetEl:k,rootEl:Ne,oldIndex:Ln,oldDraggableIndex:Jn,newIndex:ut,newDraggableIndex:on},e))}var k,Ve,ue,Ne,bn,Ar,We,an,Ln,ut,Jn,on,wr,Qe,In=!1,Pr=!1,Mr=[],mn,Ot,li,ci,wo,xo,Yn,Rn,Qn,Zn=!1,xr=!1,Cr,nt,fi=[],vi=!1,Rr=[],Lr=typeof document<"u",Er=yi,Eo=er||Wt?"cssFloat":"float",ls=Lr&&!So&&!yi&&"draggable"in document.createElement("div"),Ro=(function(){if(Lr){if(Wt)return!1;var e=document.createElement("x");return e.style.cssText="pointer-events:auto",e.style.pointerEvents==="auto"}})(),Io=function(t,r){var n=ae(t),i=parseInt(n.width)-parseInt(n.paddingLeft)-parseInt(n.paddingRight)-parseInt(n.borderLeftWidth)-parseInt(n.borderRightWidth),o=kn(t,0,r),a=kn(t,1,r),c=o&&ae(o),f=a&&ae(a),u=c&&parseInt(c.marginLeft)+parseInt(c.marginRight)+qe(o).width,y=f&&parseInt(f.marginLeft)+parseInt(f.marginRight)+qe(a).width;if(n.display==="flex")return n.flexDirection==="column"||n.flexDirection==="column-reverse"?"vertical":"horizontal";if(n.display==="grid")return n.gridTemplateColumns.split(" ").length<=1?"vertical":"horizontal";if(o&&c.float&&c.float!=="none"){var m=c.float==="left"?"left":"right";return a&&(f.clear==="both"||f.clear===m)?"vertical":"horizontal"}return o&&(c.display==="block"||c.display==="flex"||c.display==="table"||c.display==="grid"||u>=i&&n[Eo]==="none"||a&&n[Eo]==="none"&&u+y>i)?"vertical":"horizontal"},cs=function(t,r,n){var i=n?t.left:t.top,o=n?t.right:t.bottom,a=n?t.width:t.height,c=n?r.left:r.top,f=n?r.right:r.bottom,u=n?r.width:r.height;return i===c||o===f||i+a/2===c+u/2},fs=function(t,r){var n;return Mr.some(function(i){var o=i[st].options.emptyInsertThreshold;if(!(!o||wi(i))){var a=qe(i),c=t>=a.left-o&&t<=a.right+o,f=r>=a.top-o&&r<=a.bottom+o;if(c&&f)return n=i}}),n},Lo=function(t){function r(o,a){return function(c,f,u,y){var m=c.options.group.name&&f.options.group.name&&c.options.group.name===f.options.group.name;if(o==null&&(a||m))return!0;if(o==null||o===!1)return!1;if(a&&o==="clone")return o;if(typeof o=="function")return r(o(c,f,u,y),a)(c,f,u,y);var O=(a?c:f).options.group.name;return o===!0||typeof o=="string"&&o===O||o.join&&o.indexOf(O)>-1}}var n={},i=t.group;(!i||Sr(i)!="object")&&(i={name:i}),n.name=i.name,n.checkPull=r(i.pull,!0),n.checkPut=r(i.put),n.revertClone=i.revertClone,t.group=n},Fo=function(){!Ro&&ue&&ae(ue,"display","none")},ko=function(){!Ro&&ue&&ae(ue,"display","")};Lr&&!So&&document.addEventListener("click",function(e){if(Pr)return e.preventDefault(),e.stopPropagation&&e.stopPropagation(),e.stopImmediatePropagation&&e.stopImmediatePropagation(),Pr=!1,!1},!0);var gn=function(t){if(k){t=t.touches?t.touches[0]:t;var r=fs(t.clientX,t.clientY);if(r){var n={};for(var i in t)t.hasOwnProperty(i)&&(n[i]=t[i]);n.target=n.rootEl=r,n.preventDefault=void 0,n.stopPropagation=void 0,r[st]._onDragOver(n)}}},us=function(t){k&&k.parentNode[st]._isOutsideThisEl(t.target)};function se(e,t){if(!(e&&e.nodeType&&e.nodeType===1))throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(e));this.el=e,this.options=t=$t({},t),e[st]=this;var r={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(e.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return Io(e,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(a,c){a.setData("Text",c.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:se.supportPointer!==!1&&"PointerEvent"in window&&(!Gn||yi),emptyInsertThreshold:5};tr.initializePlugins(this,e,r);for(var n in r)!(n in t)&&(t[n]=r[n]);Lo(t);for(var i in this)i.charAt(0)==="_"&&typeof this[i]=="function"&&(this[i]=this[i].bind(this));this.nativeDraggable=t.forceFallback?!1:ls,this.nativeDraggable&&(this.options.touchStartThreshold=1),t.supportPointer?Oe(e,"pointerdown",this._onTapStart):(Oe(e,"mousedown",this._onTapStart),Oe(e,"touchstart",this._onTapStart)),this.nativeDraggable&&(Oe(e,"dragover",this),Oe(e,"dragenter",this)),Mr.push(this.el),t.store&&t.store.get&&this.sort(t.store.get(this)||[]),$t(this,rs())}se.prototype={constructor:se,_isOutsideThisEl:function(t){!this.el.contains(t)&&t!==this.el&&(Rn=null)},_getDirection:function(t,r){return typeof this.options.direction=="function"?this.options.direction.call(this,t,r,k):this.options.direction},_onTapStart:function(t){if(t.cancelable){var r=this,n=this.el,i=this.options,o=i.preventOnFilter,a=t.type,c=t.touches&&t.touches[0]||t.pointerType&&t.pointerType==="touch"&&t,f=(c||t).target,u=t.target.shadowRoot&&(t.path&&t.path[0]||t.composedPath&&t.composedPath()[0])||f,y=i.filter;if(ys(n),!k&&!(/mousedown|pointerdown/.test(a)&&t.button!==0||i.disabled)&&!u.isContentEditable&&!(!this.nativeDraggable&&Gn&&f&&f.tagName.toUpperCase()==="SELECT")&&(f=St(f,i.draggable,n,!1),!(f&&f.animated)&&Ar!==f)){if(Ln=vt(f),Jn=vt(f,i.draggable),typeof y=="function"){if(y.call(this,t,f,this)){it({sortable:r,rootEl:u,name:"filter",targetEl:f,toEl:n,fromEl:n}),at("filter",r,{evt:t}),o&&t.preventDefault();return}}else if(y&&(y=y.split(",").some(function(m){if(m=St(u,m.trim(),n,!1),m)return it({sortable:r,rootEl:m,name:"filter",targetEl:f,fromEl:n,toEl:n}),at("filter",r,{evt:t}),!0}),y)){o&&t.preventDefault();return}i.handle&&!St(u,i.handle,n,!1)||this._prepareDragStart(t,c,f)}}},_prepareDragStart:function(t,r,n){var i=this,o=i.el,a=i.options,c=o.ownerDocument,f;if(n&&!k&&n.parentNode===o){var u=qe(n);if(Ne=o,k=n,Ve=k.parentNode,bn=k.nextSibling,Ar=n,wr=a.group,se.dragged=k,mn={target:k,clientX:(r||t).clientX,clientY:(r||t).clientY},wo=mn.clientX-u.left,xo=mn.clientY-u.top,this._lastX=(r||t).clientX,this._lastY=(r||t).clientY,k.style["will-change"]="all",f=function(){if(at("delayEnded",i,{evt:t}),se.eventCanceled){i._onDrop();return}i._disableDelayedDragEvents(),!mo&&i.nativeDraggable&&(k.draggable=!0),i._triggerDragStart(t,r),it({sortable:i,name:"choose",originalEvent:t}),ft(k,a.chosenClass,!0)},a.ignore.split(",").forEach(function(y){Do(k,y.trim(),ui)}),Oe(c,"dragover",gn),Oe(c,"mousemove",gn),Oe(c,"touchmove",gn),a.supportPointer?(Oe(c,"pointerup",i._onDrop),!this.nativeDraggable&&Oe(c,"pointercancel",i._onDrop)):(Oe(c,"mouseup",i._onDrop),Oe(c,"touchend",i._onDrop),Oe(c,"touchcancel",i._onDrop)),mo&&this.nativeDraggable&&(this.options.touchStartThreshold=4,k.draggable=!0),at("delayStart",this,{evt:t}),a.delay&&(!a.delayOnTouchOnly||r)&&(!this.nativeDraggable||!(er||Wt))){if(se.eventCanceled){this._onDrop();return}a.supportPointer?(Oe(c,"pointerup",i._disableDelayedDrag),Oe(c,"pointercancel",i._disableDelayedDrag)):(Oe(c,"mouseup",i._disableDelayedDrag),Oe(c,"touchend",i._disableDelayedDrag),Oe(c,"touchcancel",i._disableDelayedDrag)),Oe(c,"mousemove",i._delayedDragTouchMoveHandler),Oe(c,"touchmove",i._delayedDragTouchMoveHandler),a.supportPointer&&Oe(c,"pointermove",i._delayedDragTouchMoveHandler),i._dragStartTimer=setTimeout(f,a.delay)}else f()}},_delayedDragTouchMoveHandler:function(t){var r=t.touches?t.touches[0]:t;Math.max(Math.abs(r.clientX-this._lastX),Math.abs(r.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){k&&ui(k),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;Ee(t,"mouseup",this._disableDelayedDrag),Ee(t,"touchend",this._disableDelayedDrag),Ee(t,"touchcancel",this._disableDelayedDrag),Ee(t,"pointerup",this._disableDelayedDrag),Ee(t,"pointercancel",this._disableDelayedDrag),Ee(t,"mousemove",this._delayedDragTouchMoveHandler),Ee(t,"touchmove",this._delayedDragTouchMoveHandler),Ee(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,r){r=r||t.pointerType=="touch"&&t,!this.nativeDraggable||r?this.options.supportPointer?Oe(document,"pointermove",this._onTouchMove):r?Oe(document,"touchmove",this._onTouchMove):Oe(document,"mousemove",this._onTouchMove):(Oe(k,"dragend",this),Oe(Ne,"dragstart",this._onDragStart));try{document.selection?Dr(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch{}},_dragStarted:function(t,r){if(In=!1,Ne&&k){at("dragStarted",this,{evt:r}),this.nativeDraggable&&Oe(document,"dragover",us);var n=this.options;!t&&ft(k,n.dragClass,!1),ft(k,n.ghostClass,!0),se.active=this,t&&this._appendGhost(),it({sortable:this,name:"start",originalEvent:r})}else this._nulling()},_emulateDragOver:function(){if(Ot){this._lastX=Ot.clientX,this._lastY=Ot.clientY,Fo();for(var t=document.elementFromPoint(Ot.clientX,Ot.clientY),r=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(Ot.clientX,Ot.clientY),t!==r);)r=t;if(k.parentNode[st]._isOutsideThisEl(t),r)do{if(r[st]){var n=void 0;if(n=r[st]._onDragOver({clientX:Ot.clientX,clientY:Ot.clientY,target:t,rootEl:r}),n&&!this.options.dragoverBubble)break}t=r}while(r=Co(r));ko()}},_onTouchMove:function(t){if(mn){var r=this.options,n=r.fallbackTolerance,i=r.fallbackOffset,o=t.touches?t.touches[0]:t,a=ue&&Fn(ue,!0),c=ue&&a&&a.a,f=ue&&a&&a.d,u=Er&&nt&&yo(nt),y=(o.clientX-mn.clientX+i.x)/(c||1)+(u?u[0]-fi[0]:0)/(c||1),m=(o.clientY-mn.clientY+i.y)/(f||1)+(u?u[1]-fi[1]:0)/(f||1);if(!se.active&&!In){if(n&&Math.max(Math.abs(o.clientX-this._lastX),Math.abs(o.clientY-this._lastY))=0&&(it({rootEl:Ve,name:"add",toEl:Ve,fromEl:Ne,originalEvent:t}),it({sortable:this,name:"remove",toEl:Ve,originalEvent:t}),it({rootEl:Ve,name:"sort",toEl:Ve,fromEl:Ne,originalEvent:t}),it({sortable:this,name:"sort",toEl:Ve,originalEvent:t})),Qe&&Qe.save()):ut!==Ln&&ut>=0&&(it({sortable:this,name:"update",toEl:Ve,originalEvent:t}),it({sortable:this,name:"sort",toEl:Ve,originalEvent:t})),se.active&&((ut==null||ut===-1)&&(ut=Ln,on=Jn),it({sortable:this,name:"end",toEl:Ve,originalEvent:t}),this.save()))),this._nulling()},_nulling:function(){at("nulling",this),Ne=k=Ve=ue=bn=We=Ar=an=mn=Ot=Yn=ut=on=Ln=Jn=Rn=Qn=Qe=wr=se.dragged=se.ghost=se.clone=se.active=null,Rr.forEach(function(t){t.checked=!0}),Rr.length=li=ci=0},handleEvent:function(t){switch(t.type){case"drop":case"dragend":this._onDrop(t);break;case"dragenter":case"dragover":k&&(this._onDragOver(t),ds(t));break;case"selectstart":t.preventDefault();break}},toArray:function(){for(var t=[],r,n=this.el.children,i=0,o=n.length,a=this.options;ii.right+o||e.clientY>n.bottom&&e.clientX>n.left:e.clientY>i.bottom+o||e.clientX>n.right&&e.clientY>n.top}function ms(e,t,r,n,i,o,a,c){var f=n?e.clientY:e.clientX,u=n?r.height:r.width,y=n?r.top:r.left,m=n?r.bottom:r.right,O=!1;if(!a){if(c&&Cry+u*o/2:fm-Cr)return-Qn}else if(f>y+u*(1-i)/2&&fm-u*o/2)?f>y+u/2?1:-1:0}function gs(e){return vt(k){e.directive("sortable",t=>{let r=parseInt(t.dataset?.sortableAnimationDuration);r!==0&&!r&&(r=300),t.sortable=Oi.create(t,{group:t.getAttribute("x-sortable-group"),draggable:"[x-sortable-item]",handle:"[x-sortable-handle]",dataIdAttr:"x-sortable-item",animation:r,ghostClass:"fi-sortable-ghost",onEnd(n){let{item:i,to:o,oldDraggableIndex:a,newDraggableIndex:c}=n;if(a===c)return;let f=this.options.draggable,u=o.querySelectorAll(`:scope > ${f}`)[c-1];u&&o.insertBefore(i,u.nextSibling)}})})};var xs=Object.create,Ci=Object.defineProperty,Es=Object.getPrototypeOf,Os=Object.prototype.hasOwnProperty,Ss=Object.getOwnPropertyNames,As=Object.getOwnPropertyDescriptor,Cs=e=>Ci(e,"__esModule",{value:!0}),Bo=(e,t)=>()=>(t||(t={exports:{}},e(t.exports,t)),t.exports),Ds=(e,t,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of Ss(t))!Os.call(e,n)&&n!=="default"&&Ci(e,n,{get:()=>t[n],enumerable:!(r=As(t,n))||r.enumerable});return e},Ho=e=>Ds(Cs(Ci(e!=null?xs(Es(e)):{},"default",e&&e.__esModule&&"default"in e?{get:()=>e.default,enumerable:!0}:{value:e,enumerable:!0})),e),_s=Bo(e=>{"use strict";Object.defineProperty(e,"__esModule",{value:!0});function t(d){var s=d.getBoundingClientRect();return{width:s.width,height:s.height,top:s.top,right:s.right,bottom:s.bottom,left:s.left,x:s.left,y:s.top}}function r(d){if(d==null)return window;if(d.toString()!=="[object Window]"){var s=d.ownerDocument;return s&&s.defaultView||window}return d}function n(d){var s=r(d),b=s.pageXOffset,_=s.pageYOffset;return{scrollLeft:b,scrollTop:_}}function i(d){var s=r(d).Element;return d instanceof s||d instanceof Element}function o(d){var s=r(d).HTMLElement;return d instanceof s||d instanceof HTMLElement}function a(d){if(typeof ShadowRoot>"u")return!1;var s=r(d).ShadowRoot;return d instanceof s||d instanceof ShadowRoot}function c(d){return{scrollLeft:d.scrollLeft,scrollTop:d.scrollTop}}function f(d){return d===r(d)||!o(d)?n(d):c(d)}function u(d){return d?(d.nodeName||"").toLowerCase():null}function y(d){return((i(d)?d.ownerDocument:d.document)||window.document).documentElement}function m(d){return t(y(d)).left+n(d).scrollLeft}function O(d){return r(d).getComputedStyle(d)}function S(d){var s=O(d),b=s.overflow,_=s.overflowX,T=s.overflowY;return/auto|scroll|overlay|hidden/.test(b+T+_)}function x(d,s,b){b===void 0&&(b=!1);var _=y(s),T=t(d),F=o(s),U={scrollLeft:0,scrollTop:0},H={x:0,y:0};return(F||!F&&!b)&&((u(s)!=="body"||S(_))&&(U=f(s)),o(s)?(H=t(s),H.x+=s.clientLeft,H.y+=s.clientTop):_&&(H.x=m(_))),{x:T.left+U.scrollLeft-H.x,y:T.top+U.scrollTop-H.y,width:T.width,height:T.height}}function M(d){var s=t(d),b=d.offsetWidth,_=d.offsetHeight;return Math.abs(s.width-b)<=1&&(b=s.width),Math.abs(s.height-_)<=1&&(_=s.height),{x:d.offsetLeft,y:d.offsetTop,width:b,height:_}}function I(d){return u(d)==="html"?d:d.assignedSlot||d.parentNode||(a(d)?d.host:null)||y(d)}function $(d){return["html","body","#document"].indexOf(u(d))>=0?d.ownerDocument.body:o(d)&&S(d)?d:$(I(d))}function A(d,s){var b;s===void 0&&(s=[]);var _=$(d),T=_===((b=d.ownerDocument)==null?void 0:b.body),F=r(_),U=T?[F].concat(F.visualViewport||[],S(_)?_:[]):_,H=s.concat(U);return T?H:H.concat(A(I(U)))}function N(d){return["table","td","th"].indexOf(u(d))>=0}function Y(d){return!o(d)||O(d).position==="fixed"?null:d.offsetParent}function ne(d){var s=navigator.userAgent.toLowerCase().indexOf("firefox")!==-1,b=navigator.userAgent.indexOf("Trident")!==-1;if(b&&o(d)){var _=O(d);if(_.position==="fixed")return null}for(var T=I(d);o(T)&&["html","body"].indexOf(u(T))<0;){var F=O(T);if(F.transform!=="none"||F.perspective!=="none"||F.contain==="paint"||["transform","perspective"].indexOf(F.willChange)!==-1||s&&F.willChange==="filter"||s&&F.filter&&F.filter!=="none")return T;T=T.parentNode}return null}function J(d){for(var s=r(d),b=Y(d);b&&N(b)&&O(b).position==="static";)b=Y(b);return b&&(u(b)==="html"||u(b)==="body"&&O(b).position==="static")?s:b||ne(d)||s}var V="top",de="bottom",X="right",Q="left",me="auto",l=[V,de,X,Q],h="start",v="end",p="clippingParents",j="viewport",P="popper",R="reference",Z=l.reduce(function(d,s){return d.concat([s+"-"+h,s+"-"+v])},[]),ze=[].concat(l,[me]).reduce(function(d,s){return d.concat([s,s+"-"+h,s+"-"+v])},[]),Rt="beforeRead",Ut="read",Fr="afterRead",kr="beforeMain",Nr="main",Vt="afterMain",nr="beforeWrite",jr="write",rr="afterWrite",It=[Rt,Ut,Fr,kr,Nr,Vt,nr,jr,rr];function Br(d){var s=new Map,b=new Set,_=[];d.forEach(function(F){s.set(F.name,F)});function T(F){b.add(F.name);var U=[].concat(F.requires||[],F.requiresIfExists||[]);U.forEach(function(H){if(!b.has(H)){var G=s.get(H);G&&T(G)}}),_.push(F)}return d.forEach(function(F){b.has(F.name)||T(F)}),_}function mt(d){var s=Br(d);return It.reduce(function(b,_){return b.concat(s.filter(function(T){return T.phase===_}))},[])}function zt(d){var s;return function(){return s||(s=new Promise(function(b){Promise.resolve().then(function(){s=void 0,b(d())})})),s}}function At(d){for(var s=arguments.length,b=new Array(s>1?s-1:0),_=1;_=0,_=b&&o(d)?J(d):d;return i(_)?s.filter(function(T){return i(T)&&Nn(T,_)&&u(T)!=="body"}):[]}function wn(d,s,b){var _=s==="clippingParents"?yn(d):[].concat(s),T=[].concat(_,[b]),F=T[0],U=T.reduce(function(H,G){var oe=sr(d,G);return H.top=gt(oe.top,H.top),H.right=ln(oe.right,H.right),H.bottom=ln(oe.bottom,H.bottom),H.left=gt(oe.left,H.left),H},sr(d,F));return U.width=U.right-U.left,U.height=U.bottom-U.top,U.x=U.left,U.y=U.top,U}function cn(d){return d.split("-")[1]}function dt(d){return["top","bottom"].indexOf(d)>=0?"x":"y"}function lr(d){var s=d.reference,b=d.element,_=d.placement,T=_?ot(_):null,F=_?cn(_):null,U=s.x+s.width/2-b.width/2,H=s.y+s.height/2-b.height/2,G;switch(T){case V:G={x:U,y:s.y-b.height};break;case de:G={x:U,y:s.y+s.height};break;case X:G={x:s.x+s.width,y:H};break;case Q:G={x:s.x-b.width,y:H};break;default:G={x:s.x,y:s.y}}var oe=T?dt(T):null;if(oe!=null){var z=oe==="y"?"height":"width";switch(F){case h:G[oe]=G[oe]-(s[z]/2-b[z]/2);break;case v:G[oe]=G[oe]+(s[z]/2-b[z]/2);break}}return G}function cr(){return{top:0,right:0,bottom:0,left:0}}function fr(d){return Object.assign({},cr(),d)}function ur(d,s){return s.reduce(function(b,_){return b[_]=d,b},{})}function qt(d,s){s===void 0&&(s={});var b=s,_=b.placement,T=_===void 0?d.placement:_,F=b.boundary,U=F===void 0?p:F,H=b.rootBoundary,G=H===void 0?j:H,oe=b.elementContext,z=oe===void 0?P:oe,De=b.altBoundary,Fe=De===void 0?!1:De,Ce=b.padding,xe=Ce===void 0?0:Ce,Me=fr(typeof xe!="number"?xe:ur(xe,l)),Se=z===P?R:P,Be=d.elements.reference,Re=d.rects.popper,He=d.elements[Fe?Se:z],ce=wn(i(He)?He:He.contextElement||y(d.elements.popper),U,G),Pe=t(Be),_e=lr({reference:Pe,element:Re,strategy:"absolute",placement:T}),ke=Xt(Object.assign({},Re,_e)),Le=z===P?ke:Pe,Ye={top:ce.top-Le.top+Me.top,bottom:Le.bottom-ce.bottom+Me.bottom,left:ce.left-Le.left+Me.left,right:Le.right-ce.right+Me.right},$e=d.modifiersData.offset;if(z===P&&$e){var Ue=$e[T];Object.keys(Ye).forEach(function(wt){var et=[X,de].indexOf(wt)>=0?1:-1,Ft=[V,de].indexOf(wt)>=0?"y":"x";Ye[wt]+=Ue[Ft]*et})}return Ye}var dr="Popper: Invalid reference or popper argument provided. They must be either a DOM element or virtual element.",Vr="Popper: An infinite loop in the modifiers cycle has been detected! The cycle has been interrupted to prevent a browser crash.",xn={placement:"bottom",modifiers:[],strategy:"absolute"};function fn(){for(var d=arguments.length,s=new Array(d),b=0;b100){console.error(Vr);break}if(z.reset===!0){z.reset=!1,Pe=-1;continue}var _e=z.orderedModifiers[Pe],ke=_e.fn,Le=_e.options,Ye=Le===void 0?{}:Le,$e=_e.name;typeof ke=="function"&&(z=ke({state:z,options:Ye,name:$e,instance:Ce})||z)}}},update:zt(function(){return new Promise(function(Se){Ce.forceUpdate(),Se(z)})}),destroy:function(){Me(),Fe=!0}};if(!fn(H,G))return console.error(dr),Ce;Ce.setOptions(oe).then(function(Se){!Fe&&oe.onFirstUpdate&&oe.onFirstUpdate(Se)});function xe(){z.orderedModifiers.forEach(function(Se){var Be=Se.name,Re=Se.options,He=Re===void 0?{}:Re,ce=Se.effect;if(typeof ce=="function"){var Pe=ce({state:z,name:Be,instance:Ce,options:He}),_e=function(){};De.push(Pe||_e)}})}function Me(){De.forEach(function(Se){return Se()}),De=[]}return Ce}}var On={passive:!0};function zr(d){var s=d.state,b=d.instance,_=d.options,T=_.scroll,F=T===void 0?!0:T,U=_.resize,H=U===void 0?!0:U,G=r(s.elements.popper),oe=[].concat(s.scrollParents.reference,s.scrollParents.popper);return F&&oe.forEach(function(z){z.addEventListener("scroll",b.update,On)}),H&&G.addEventListener("resize",b.update,On),function(){F&&oe.forEach(function(z){z.removeEventListener("scroll",b.update,On)}),H&&G.removeEventListener("resize",b.update,On)}}var jn={name:"eventListeners",enabled:!0,phase:"write",fn:function(){},effect:zr,data:{}};function Yr(d){var s=d.state,b=d.name;s.modifiersData[b]=lr({reference:s.rects.reference,element:s.rects.popper,strategy:"absolute",placement:s.placement})}var Bn={name:"popperOffsets",enabled:!0,phase:"read",fn:Yr,data:{}},Xr={top:"auto",right:"auto",bottom:"auto",left:"auto"};function qr(d){var s=d.x,b=d.y,_=window,T=_.devicePixelRatio||1;return{x:Yt(Yt(s*T)/T)||0,y:Yt(Yt(b*T)/T)||0}}function Hn(d){var s,b=d.popper,_=d.popperRect,T=d.placement,F=d.offsets,U=d.position,H=d.gpuAcceleration,G=d.adaptive,oe=d.roundOffsets,z=oe===!0?qr(F):typeof oe=="function"?oe(F):F,De=z.x,Fe=De===void 0?0:De,Ce=z.y,xe=Ce===void 0?0:Ce,Me=F.hasOwnProperty("x"),Se=F.hasOwnProperty("y"),Be=Q,Re=V,He=window;if(G){var ce=J(b),Pe="clientHeight",_e="clientWidth";ce===r(b)&&(ce=y(b),O(ce).position!=="static"&&(Pe="scrollHeight",_e="scrollWidth")),ce=ce,T===V&&(Re=de,xe-=ce[Pe]-_.height,xe*=H?1:-1),T===Q&&(Be=X,Fe-=ce[_e]-_.width,Fe*=H?1:-1)}var ke=Object.assign({position:U},G&&Xr);if(H){var Le;return Object.assign({},ke,(Le={},Le[Re]=Se?"0":"",Le[Be]=Me?"0":"",Le.transform=(He.devicePixelRatio||1)<2?"translate("+Fe+"px, "+xe+"px)":"translate3d("+Fe+"px, "+xe+"px, 0)",Le))}return Object.assign({},ke,(s={},s[Re]=Se?xe+"px":"",s[Be]=Me?Fe+"px":"",s.transform="",s))}function g(d){var s=d.state,b=d.options,_=b.gpuAcceleration,T=_===void 0?!0:_,F=b.adaptive,U=F===void 0?!0:F,H=b.roundOffsets,G=H===void 0?!0:H,oe=O(s.elements.popper).transitionProperty||"";U&&["transform","top","right","bottom","left"].some(function(De){return oe.indexOf(De)>=0})&&console.warn(["Popper: Detected CSS transitions on at least one of the following",'CSS properties: "transform", "top", "right", "bottom", "left".',` + +`,'Disable the "computeStyles" modifier\'s `adaptive` option to allow',"for smooth transitions, or remove these properties from the CSS","transition declaration on the popper element if only transitioning","opacity or background-color for example.",` + +`,"We recommend using the popper element as a wrapper around an inner","element that can have any CSS property transitioned for animations."].join(" "));var z={placement:ot(s.placement),popper:s.elements.popper,popperRect:s.rects.popper,gpuAcceleration:T};s.modifiersData.popperOffsets!=null&&(s.styles.popper=Object.assign({},s.styles.popper,Hn(Object.assign({},z,{offsets:s.modifiersData.popperOffsets,position:s.options.strategy,adaptive:U,roundOffsets:G})))),s.modifiersData.arrow!=null&&(s.styles.arrow=Object.assign({},s.styles.arrow,Hn(Object.assign({},z,{offsets:s.modifiersData.arrow,position:"absolute",adaptive:!1,roundOffsets:G})))),s.attributes.popper=Object.assign({},s.attributes.popper,{"data-popper-placement":s.placement})}var w={name:"computeStyles",enabled:!0,phase:"beforeWrite",fn:g,data:{}};function C(d){var s=d.state;Object.keys(s.elements).forEach(function(b){var _=s.styles[b]||{},T=s.attributes[b]||{},F=s.elements[b];!o(F)||!u(F)||(Object.assign(F.style,_),Object.keys(T).forEach(function(U){var H=T[U];H===!1?F.removeAttribute(U):F.setAttribute(U,H===!0?"":H)}))})}function L(d){var s=d.state,b={popper:{position:s.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};return Object.assign(s.elements.popper.style,b.popper),s.styles=b,s.elements.arrow&&Object.assign(s.elements.arrow.style,b.arrow),function(){Object.keys(s.elements).forEach(function(_){var T=s.elements[_],F=s.attributes[_]||{},U=Object.keys(s.styles.hasOwnProperty(_)?s.styles[_]:b[_]),H=U.reduce(function(G,oe){return G[oe]="",G},{});!o(T)||!u(T)||(Object.assign(T.style,H),Object.keys(F).forEach(function(G){T.removeAttribute(G)}))})}}var q={name:"applyStyles",enabled:!0,phase:"write",fn:C,effect:L,requires:["computeStyles"]};function W(d,s,b){var _=ot(d),T=[Q,V].indexOf(_)>=0?-1:1,F=typeof b=="function"?b(Object.assign({},s,{placement:d})):b,U=F[0],H=F[1];return U=U||0,H=(H||0)*T,[Q,X].indexOf(_)>=0?{x:H,y:U}:{x:U,y:H}}function B(d){var s=d.state,b=d.options,_=d.name,T=b.offset,F=T===void 0?[0,0]:T,U=ze.reduce(function(z,De){return z[De]=W(De,s.rects,F),z},{}),H=U[s.placement],G=H.x,oe=H.y;s.modifiersData.popperOffsets!=null&&(s.modifiersData.popperOffsets.x+=G,s.modifiersData.popperOffsets.y+=oe),s.modifiersData[_]=U}var be={name:"offset",enabled:!0,phase:"main",requires:["popperOffsets"],fn:B},le={left:"right",right:"left",bottom:"top",top:"bottom"};function pe(d){return d.replace(/left|right|bottom|top/g,function(s){return le[s]})}var ye={start:"end",end:"start"};function Te(d){return d.replace(/start|end/g,function(s){return ye[s]})}function je(d,s){s===void 0&&(s={});var b=s,_=b.placement,T=b.boundary,F=b.rootBoundary,U=b.padding,H=b.flipVariations,G=b.allowedAutoPlacements,oe=G===void 0?ze:G,z=cn(_),De=z?H?Z:Z.filter(function(xe){return cn(xe)===z}):l,Fe=De.filter(function(xe){return oe.indexOf(xe)>=0});Fe.length===0&&(Fe=De,console.error(["Popper: The `allowedAutoPlacements` option did not allow any","placements. Ensure the `placement` option matches the variation","of the allowed placements.",'For example, "auto" cannot be used to allow "bottom-start".','Use "auto-start" instead.'].join(" ")));var Ce=Fe.reduce(function(xe,Me){return xe[Me]=qt(d,{placement:Me,boundary:T,rootBoundary:F,padding:U})[ot(Me)],xe},{});return Object.keys(Ce).sort(function(xe,Me){return Ce[xe]-Ce[Me]})}function Ae(d){if(ot(d)===me)return[];var s=pe(d);return[Te(d),s,Te(s)]}function Ie(d){var s=d.state,b=d.options,_=d.name;if(!s.modifiersData[_]._skip){for(var T=b.mainAxis,F=T===void 0?!0:T,U=b.altAxis,H=U===void 0?!0:U,G=b.fallbackPlacements,oe=b.padding,z=b.boundary,De=b.rootBoundary,Fe=b.altBoundary,Ce=b.flipVariations,xe=Ce===void 0?!0:Ce,Me=b.allowedAutoPlacements,Se=s.options.placement,Be=ot(Se),Re=Be===Se,He=G||(Re||!xe?[pe(Se)]:Ae(Se)),ce=[Se].concat(He).reduce(function(te,ge){return te.concat(ot(ge)===me?je(s,{placement:ge,boundary:z,rootBoundary:De,padding:oe,flipVariations:xe,allowedAutoPlacements:Me}):ge)},[]),Pe=s.rects.reference,_e=s.rects.popper,ke=new Map,Le=!0,Ye=ce[0],$e=0;$e=0,dn=Ft?"width":"height",Qt=qt(s,{placement:Ue,boundary:z,rootBoundary:De,altBoundary:Fe,padding:oe}),kt=Ft?et?X:Q:et?de:V;Pe[dn]>_e[dn]&&(kt=pe(kt));var $n=pe(kt),Zt=[];if(F&&Zt.push(Qt[wt]<=0),H&&Zt.push(Qt[kt]<=0,Qt[$n]<=0),Zt.every(function(te){return te})){Ye=Ue,Le=!1;break}ke.set(Ue,Zt)}if(Le)for(var Sn=xe?3:1,Wn=function(ge){var we=ce.find(function(Ke){var Je=ke.get(Ke);if(Je)return Je.slice(0,ge).every(function(Dt){return Dt})});if(we)return Ye=we,"break"},D=Sn;D>0;D--){var K=Wn(D);if(K==="break")break}s.placement!==Ye&&(s.modifiersData[_]._skip=!0,s.placement=Ye,s.reset=!0)}}var re={name:"flip",enabled:!0,phase:"main",fn:Ie,requiresIfExists:["offset"],data:{_skip:!1}};function he(d){return d==="x"?"y":"x"}function ve(d,s,b){return gt(d,ln(s,b))}function ee(d){var s=d.state,b=d.options,_=d.name,T=b.mainAxis,F=T===void 0?!0:T,U=b.altAxis,H=U===void 0?!1:U,G=b.boundary,oe=b.rootBoundary,z=b.altBoundary,De=b.padding,Fe=b.tether,Ce=Fe===void 0?!0:Fe,xe=b.tetherOffset,Me=xe===void 0?0:xe,Se=qt(s,{boundary:G,rootBoundary:oe,padding:De,altBoundary:z}),Be=ot(s.placement),Re=cn(s.placement),He=!Re,ce=dt(Be),Pe=he(ce),_e=s.modifiersData.popperOffsets,ke=s.rects.reference,Le=s.rects.popper,Ye=typeof Me=="function"?Me(Object.assign({},s.rects,{placement:s.placement})):Me,$e={x:0,y:0};if(_e){if(F||H){var Ue=ce==="y"?V:Q,wt=ce==="y"?de:X,et=ce==="y"?"height":"width",Ft=_e[ce],dn=_e[ce]+Se[Ue],Qt=_e[ce]-Se[wt],kt=Ce?-Le[et]/2:0,$n=Re===h?ke[et]:Le[et],Zt=Re===h?-Le[et]:-ke[et],Sn=s.elements.arrow,Wn=Ce&&Sn?M(Sn):{width:0,height:0},D=s.modifiersData["arrow#persistent"]?s.modifiersData["arrow#persistent"].padding:cr(),K=D[Ue],te=D[wt],ge=ve(0,ke[et],Wn[et]),we=He?ke[et]/2-kt-ge-K-Ye:$n-ge-K-Ye,Ke=He?-ke[et]/2+kt+ge+te+Ye:Zt+ge+te+Ye,Je=s.elements.arrow&&J(s.elements.arrow),Dt=Je?ce==="y"?Je.clientTop||0:Je.clientLeft||0:0,Un=s.modifiersData.offset?s.modifiersData.offset[s.placement][ce]:0,_t=_e[ce]+we-Un-Dt,An=_e[ce]+Ke-Un;if(F){var pn=ve(Ce?ln(dn,_t):dn,Ft,Ce?gt(Qt,An):Qt);_e[ce]=pn,$e[ce]=pn-Ft}if(H){var en=ce==="x"?V:Q,Gr=ce==="x"?de:X,tn=_e[Pe],hn=tn+Se[en],Di=tn-Se[Gr],_i=ve(Ce?ln(hn,_t):hn,tn,Ce?gt(Di,An):Di);_e[Pe]=_i,$e[Pe]=_i-tn}}s.modifiersData[_]=$e}}var ie={name:"preventOverflow",enabled:!0,phase:"main",fn:ee,requiresIfExists:["offset"]},E=function(s,b){return s=typeof s=="function"?s(Object.assign({},b.rects,{placement:b.placement})):s,fr(typeof s!="number"?s:ur(s,l))};function Ge(d){var s,b=d.state,_=d.name,T=d.options,F=b.elements.arrow,U=b.modifiersData.popperOffsets,H=ot(b.placement),G=dt(H),oe=[Q,X].indexOf(H)>=0,z=oe?"height":"width";if(!(!F||!U)){var De=E(T.padding,b),Fe=M(F),Ce=G==="y"?V:Q,xe=G==="y"?de:X,Me=b.rects.reference[z]+b.rects.reference[G]-U[G]-b.rects.popper[z],Se=U[G]-b.rects.reference[G],Be=J(F),Re=Be?G==="y"?Be.clientHeight||0:Be.clientWidth||0:0,He=Me/2-Se/2,ce=De[Ce],Pe=Re-Fe[z]-De[xe],_e=Re/2-Fe[z]/2+He,ke=ve(ce,_e,Pe),Le=G;b.modifiersData[_]=(s={},s[Le]=ke,s.centerOffset=ke-_e,s)}}function fe(d){var s=d.state,b=d.options,_=b.element,T=_===void 0?"[data-popper-arrow]":_;if(T!=null&&!(typeof T=="string"&&(T=s.elements.popper.querySelector(T),!T))){if(o(T)||console.error(['Popper: "arrow" element must be an HTMLElement (not an SVGElement).',"To use an SVG arrow, wrap it in an HTMLElement that will be used as","the arrow."].join(" ")),!Nn(s.elements.popper,T)){console.error(['Popper: "arrow" modifier\'s `element` must be a child of the popper',"element."].join(" "));return}s.elements.arrow=T}}var Lt={name:"arrow",enabled:!0,phase:"main",fn:Ge,effect:fe,requires:["popperOffsets"],requiresIfExists:["preventOverflow"]};function bt(d,s,b){return b===void 0&&(b={x:0,y:0}),{top:d.top-s.height-b.y,right:d.right-s.width+b.x,bottom:d.bottom-s.height+b.y,left:d.left-s.width-b.x}}function Gt(d){return[V,X,de,Q].some(function(s){return d[s]>=0})}function Kt(d){var s=d.state,b=d.name,_=s.rects.reference,T=s.rects.popper,F=s.modifiersData.preventOverflow,U=qt(s,{elementContext:"reference"}),H=qt(s,{altBoundary:!0}),G=bt(U,_),oe=bt(H,T,F),z=Gt(G),De=Gt(oe);s.modifiersData[b]={referenceClippingOffsets:G,popperEscapeOffsets:oe,isReferenceHidden:z,hasPopperEscaped:De},s.attributes.popper=Object.assign({},s.attributes.popper,{"data-popper-reference-hidden":z,"data-popper-escaped":De})}var Jt={name:"hide",enabled:!0,phase:"main",requiresIfExists:["preventOverflow"],fn:Kt},rt=[jn,Bn,w,q],lt=En({defaultModifiers:rt}),yt=[jn,Bn,w,q,be,re,ie,Lt,Jt],un=En({defaultModifiers:yt});e.applyStyles=q,e.arrow=Lt,e.computeStyles=w,e.createPopper=un,e.createPopperLite=lt,e.defaultModifiers=yt,e.detectOverflow=qt,e.eventListeners=jn,e.flip=re,e.hide=Jt,e.offset=be,e.popperGenerator=En,e.popperOffsets=Bn,e.preventOverflow=ie}),$o=Bo(e=>{"use strict";Object.defineProperty(e,"__esModule",{value:!0});var t=_s(),r='',n="tippy-box",i="tippy-content",o="tippy-backdrop",a="tippy-arrow",c="tippy-svg-arrow",f={passive:!0,capture:!0};function u(g,w){return{}.hasOwnProperty.call(g,w)}function y(g,w,C){if(Array.isArray(g)){var L=g[w];return L??(Array.isArray(C)?C[w]:C)}return g}function m(g,w){var C={}.toString.call(g);return C.indexOf("[object")===0&&C.indexOf(w+"]")>-1}function O(g,w){return typeof g=="function"?g.apply(void 0,w):g}function S(g,w){if(w===0)return g;var C;return function(L){clearTimeout(C),C=setTimeout(function(){g(L)},w)}}function x(g,w){var C=Object.assign({},g);return w.forEach(function(L){delete C[L]}),C}function M(g){return g.split(/\s+/).filter(Boolean)}function I(g){return[].concat(g)}function $(g,w){g.indexOf(w)===-1&&g.push(w)}function A(g){return g.filter(function(w,C){return g.indexOf(w)===C})}function N(g){return g.split("-")[0]}function Y(g){return[].slice.call(g)}function ne(g){return Object.keys(g).reduce(function(w,C){return g[C]!==void 0&&(w[C]=g[C]),w},{})}function J(){return document.createElement("div")}function V(g){return["Element","Fragment"].some(function(w){return m(g,w)})}function de(g){return m(g,"NodeList")}function X(g){return m(g,"MouseEvent")}function Q(g){return!!(g&&g._tippy&&g._tippy.reference===g)}function me(g){return V(g)?[g]:de(g)?Y(g):Array.isArray(g)?g:Y(document.querySelectorAll(g))}function l(g,w){g.forEach(function(C){C&&(C.style.transitionDuration=w+"ms")})}function h(g,w){g.forEach(function(C){C&&C.setAttribute("data-state",w)})}function v(g){var w,C=I(g),L=C[0];return!(L==null||(w=L.ownerDocument)==null)&&w.body?L.ownerDocument:document}function p(g,w){var C=w.clientX,L=w.clientY;return g.every(function(q){var W=q.popperRect,B=q.popperState,be=q.props,le=be.interactiveBorder,pe=N(B.placement),ye=B.modifiersData.offset;if(!ye)return!0;var Te=pe==="bottom"?ye.top.y:0,je=pe==="top"?ye.bottom.y:0,Ae=pe==="right"?ye.left.x:0,Ie=pe==="left"?ye.right.x:0,re=W.top-L+Te>le,he=L-W.bottom-je>le,ve=W.left-C+Ae>le,ee=C-W.right-Ie>le;return re||he||ve||ee})}function j(g,w,C){var L=w+"EventListener";["transitionend","webkitTransitionEnd"].forEach(function(q){g[L](q,C)})}var P={isTouch:!1},R=0;function Z(){P.isTouch||(P.isTouch=!0,window.performance&&document.addEventListener("mousemove",ze))}function ze(){var g=performance.now();g-R<20&&(P.isTouch=!1,document.removeEventListener("mousemove",ze)),R=g}function Rt(){var g=document.activeElement;if(Q(g)){var w=g._tippy;g.blur&&!w.state.isVisible&&g.blur()}}function Ut(){document.addEventListener("touchstart",Z,f),window.addEventListener("blur",Rt)}var Fr=typeof window<"u"&&typeof document<"u",kr=Fr?navigator.userAgent:"",Nr=/MSIE |Trident\//.test(kr);function Vt(g){var w=g==="destroy"?"n already-":" ";return[g+"() was called on a"+w+"destroyed instance. This is a no-op but","indicates a potential memory leak."].join(" ")}function nr(g){var w=/[ \t]{2,}/g,C=/^[ \t]*/gm;return g.replace(w," ").replace(C,"").trim()}function jr(g){return nr(` + %ctippy.js + + %c`+nr(g)+` + + %c\u{1F477}\u200D This is a development-only message. It will be removed in production. + `)}function rr(g){return[jr(g),"color: #00C584; font-size: 1.3em; font-weight: bold;","line-height: 1.5","color: #a6a095;"]}var It;Br();function Br(){It=new Set}function mt(g,w){if(g&&!It.has(w)){var C;It.add(w),(C=console).warn.apply(C,rr(w))}}function zt(g,w){if(g&&!It.has(w)){var C;It.add(w),(C=console).error.apply(C,rr(w))}}function At(g){var w=!g,C=Object.prototype.toString.call(g)==="[object Object]"&&!g.addEventListener;zt(w,["tippy() was passed","`"+String(g)+"`","as its targets (first) argument. Valid types are: String, Element,","Element[], or NodeList."].join(" ")),zt(C,["tippy() was passed a plain object which is not supported as an argument","for virtual positioning. Use props.getReferenceClientRect instead."].join(" "))}var Ct={animateFill:!1,followCursor:!1,inlinePositioning:!1,sticky:!1},Hr={allowHTML:!1,animation:"fade",arrow:!0,content:"",inertia:!1,maxWidth:350,role:"tooltip",theme:"",zIndex:9999},Ze=Object.assign({appendTo:function(){return document.body},aria:{content:"auto",expanded:"auto"},delay:0,duration:[300,250],getReferenceClientRect:null,hideOnClick:!0,ignoreAttributes:!1,interactive:!1,interactiveBorder:2,interactiveDebounce:0,moveTransition:"",offset:[0,10],onAfterUpdate:function(){},onBeforeUpdate:function(){},onCreate:function(){},onDestroy:function(){},onHidden:function(){},onHide:function(){},onMount:function(){},onShow:function(){},onShown:function(){},onTrigger:function(){},onUntrigger:function(){},onClickOutside:function(){},placement:"top",plugins:[],popperOptions:{},render:null,showOnCreate:!1,touch:!0,trigger:"mouseenter focus",triggerTarget:null},Ct,{},Hr),$r=Object.keys(Ze),Wr=function(w){gt(w,[]);var C=Object.keys(w);C.forEach(function(L){Ze[L]=w[L]})};function ot(g){var w=g.plugins||[],C=w.reduce(function(L,q){var W=q.name,B=q.defaultValue;return W&&(L[W]=g[W]!==void 0?g[W]:B),L},{});return Object.assign({},g,{},C)}function Ur(g,w){var C=w?Object.keys(ot(Object.assign({},Ze,{plugins:w}))):$r,L=C.reduce(function(q,W){var B=(g.getAttribute("data-tippy-"+W)||"").trim();if(!B)return q;if(W==="content")q[W]=B;else try{q[W]=JSON.parse(B)}catch{q[W]=B}return q},{});return L}function ir(g,w){var C=Object.assign({},w,{content:O(w.content,[g])},w.ignoreAttributes?{}:Ur(g,w.plugins));return C.aria=Object.assign({},Ze.aria,{},C.aria),C.aria={expanded:C.aria.expanded==="auto"?w.interactive:C.aria.expanded,content:C.aria.content==="auto"?w.interactive?null:"describedby":C.aria.content},C}function gt(g,w){g===void 0&&(g={}),w===void 0&&(w=[]);var C=Object.keys(g);C.forEach(function(L){var q=x(Ze,Object.keys(Ct)),W=!u(q,L);W&&(W=w.filter(function(B){return B.name===L}).length===0),mt(W,["`"+L+"`","is not a valid prop. You may have spelled it incorrectly, or if it's","a plugin, forgot to pass it in an array as props.plugins.",` + +`,`All props: https://atomiks.github.io/tippyjs/v6/all-props/ +`,"Plugins: https://atomiks.github.io/tippyjs/v6/plugins/"].join(" "))})}var ln=function(){return"innerHTML"};function Yt(g,w){g[ln()]=w}function or(g){var w=J();return g===!0?w.className=a:(w.className=c,V(g)?w.appendChild(g):Yt(w,g)),w}function Nn(g,w){V(w.content)?(Yt(g,""),g.appendChild(w.content)):typeof w.content!="function"&&(w.allowHTML?Yt(g,w.content):g.textContent=w.content)}function Xt(g){var w=g.firstElementChild,C=Y(w.children);return{box:w,content:C.find(function(L){return L.classList.contains(i)}),arrow:C.find(function(L){return L.classList.contains(a)||L.classList.contains(c)}),backdrop:C.find(function(L){return L.classList.contains(o)})}}function ar(g){var w=J(),C=J();C.className=n,C.setAttribute("data-state","hidden"),C.setAttribute("tabindex","-1");var L=J();L.className=i,L.setAttribute("data-state","hidden"),Nn(L,g.props),w.appendChild(C),C.appendChild(L),q(g.props,g.props);function q(W,B){var be=Xt(w),le=be.box,pe=be.content,ye=be.arrow;B.theme?le.setAttribute("data-theme",B.theme):le.removeAttribute("data-theme"),typeof B.animation=="string"?le.setAttribute("data-animation",B.animation):le.removeAttribute("data-animation"),B.inertia?le.setAttribute("data-inertia",""):le.removeAttribute("data-inertia"),le.style.maxWidth=typeof B.maxWidth=="number"?B.maxWidth+"px":B.maxWidth,B.role?le.setAttribute("role",B.role):le.removeAttribute("role"),(W.content!==B.content||W.allowHTML!==B.allowHTML)&&Nn(pe,g.props),B.arrow?ye?W.arrow!==B.arrow&&(le.removeChild(ye),le.appendChild(or(B.arrow))):le.appendChild(or(B.arrow)):ye&&le.removeChild(ye)}return{popper:w,onUpdate:q}}ar.$$tippy=!0;var sr=1,yn=[],wn=[];function cn(g,w){var C=ir(g,Object.assign({},Ze,{},ot(ne(w)))),L,q,W,B=!1,be=!1,le=!1,pe=!1,ye,Te,je,Ae=[],Ie=S(Re,C.interactiveDebounce),re,he=sr++,ve=null,ee=A(C.plugins),ie={isEnabled:!0,isVisible:!1,isDestroyed:!1,isMounted:!1,isShown:!1},E={id:he,reference:g,popper:J(),popperInstance:ve,props:C,state:ie,plugins:ee,clearDelayTimeouts:Ft,setProps:dn,setContent:Qt,show:kt,hide:$n,hideWithInteractivity:Zt,enable:wt,disable:et,unmount:Sn,destroy:Wn};if(!C.render)return zt(!0,"render() function has not been supplied."),E;var Ge=C.render(E),fe=Ge.popper,Lt=Ge.onUpdate;fe.setAttribute("data-tippy-root",""),fe.id="tippy-"+E.id,E.popper=fe,g._tippy=E,fe._tippy=E;var bt=ee.map(function(D){return D.fn(E)}),Gt=g.hasAttribute("aria-expanded");return Me(),T(),s(),b("onCreate",[E]),C.showOnCreate&&$e(),fe.addEventListener("mouseenter",function(){E.props.interactive&&E.state.isVisible&&E.clearDelayTimeouts()}),fe.addEventListener("mouseleave",function(D){E.props.interactive&&E.props.trigger.indexOf("mouseenter")>=0&&(yt().addEventListener("mousemove",Ie),Ie(D))}),E;function Kt(){var D=E.props.touch;return Array.isArray(D)?D:[D,0]}function Jt(){return Kt()[0]==="hold"}function rt(){var D;return!!((D=E.props.render)!=null&&D.$$tippy)}function lt(){return re||g}function yt(){var D=lt().parentNode;return D?v(D):document}function un(){return Xt(fe)}function d(D){return E.state.isMounted&&!E.state.isVisible||P.isTouch||ye&&ye.type==="focus"?0:y(E.props.delay,D?0:1,Ze.delay)}function s(){fe.style.pointerEvents=E.props.interactive&&E.state.isVisible?"":"none",fe.style.zIndex=""+E.props.zIndex}function b(D,K,te){if(te===void 0&&(te=!0),bt.forEach(function(we){we[D]&&we[D].apply(void 0,K)}),te){var ge;(ge=E.props)[D].apply(ge,K)}}function _(){var D=E.props.aria;if(D.content){var K="aria-"+D.content,te=fe.id,ge=I(E.props.triggerTarget||g);ge.forEach(function(we){var Ke=we.getAttribute(K);if(E.state.isVisible)we.setAttribute(K,Ke?Ke+" "+te:te);else{var Je=Ke&&Ke.replace(te,"").trim();Je?we.setAttribute(K,Je):we.removeAttribute(K)}})}}function T(){if(!(Gt||!E.props.aria.expanded)){var D=I(E.props.triggerTarget||g);D.forEach(function(K){E.props.interactive?K.setAttribute("aria-expanded",E.state.isVisible&&K===lt()?"true":"false"):K.removeAttribute("aria-expanded")})}}function F(){yt().removeEventListener("mousemove",Ie),yn=yn.filter(function(D){return D!==Ie})}function U(D){if(!(P.isTouch&&(le||D.type==="mousedown"))&&!(E.props.interactive&&fe.contains(D.target))){if(lt().contains(D.target)){if(P.isTouch||E.state.isVisible&&E.props.trigger.indexOf("click")>=0)return}else b("onClickOutside",[E,D]);E.props.hideOnClick===!0&&(E.clearDelayTimeouts(),E.hide(),be=!0,setTimeout(function(){be=!1}),E.state.isMounted||z())}}function H(){le=!0}function G(){le=!1}function oe(){var D=yt();D.addEventListener("mousedown",U,!0),D.addEventListener("touchend",U,f),D.addEventListener("touchstart",G,f),D.addEventListener("touchmove",H,f)}function z(){var D=yt();D.removeEventListener("mousedown",U,!0),D.removeEventListener("touchend",U,f),D.removeEventListener("touchstart",G,f),D.removeEventListener("touchmove",H,f)}function De(D,K){Ce(D,function(){!E.state.isVisible&&fe.parentNode&&fe.parentNode.contains(fe)&&K()})}function Fe(D,K){Ce(D,K)}function Ce(D,K){var te=un().box;function ge(we){we.target===te&&(j(te,"remove",ge),K())}if(D===0)return K();j(te,"remove",Te),j(te,"add",ge),Te=ge}function xe(D,K,te){te===void 0&&(te=!1);var ge=I(E.props.triggerTarget||g);ge.forEach(function(we){we.addEventListener(D,K,te),Ae.push({node:we,eventType:D,handler:K,options:te})})}function Me(){Jt()&&(xe("touchstart",Be,{passive:!0}),xe("touchend",He,{passive:!0})),M(E.props.trigger).forEach(function(D){if(D!=="manual")switch(xe(D,Be),D){case"mouseenter":xe("mouseleave",He);break;case"focus":xe(Nr?"focusout":"blur",ce);break;case"focusin":xe("focusout",ce);break}})}function Se(){Ae.forEach(function(D){var K=D.node,te=D.eventType,ge=D.handler,we=D.options;K.removeEventListener(te,ge,we)}),Ae=[]}function Be(D){var K,te=!1;if(!(!E.state.isEnabled||Pe(D)||be)){var ge=((K=ye)==null?void 0:K.type)==="focus";ye=D,re=D.currentTarget,T(),!E.state.isVisible&&X(D)&&yn.forEach(function(we){return we(D)}),D.type==="click"&&(E.props.trigger.indexOf("mouseenter")<0||B)&&E.props.hideOnClick!==!1&&E.state.isVisible?te=!0:$e(D),D.type==="click"&&(B=!te),te&&!ge&&Ue(D)}}function Re(D){var K=D.target,te=lt().contains(K)||fe.contains(K);if(!(D.type==="mousemove"&&te)){var ge=Ye().concat(fe).map(function(we){var Ke,Je=we._tippy,Dt=(Ke=Je.popperInstance)==null?void 0:Ke.state;return Dt?{popperRect:we.getBoundingClientRect(),popperState:Dt,props:C}:null}).filter(Boolean);p(ge,D)&&(F(),Ue(D))}}function He(D){var K=Pe(D)||E.props.trigger.indexOf("click")>=0&&B;if(!K){if(E.props.interactive){E.hideWithInteractivity(D);return}Ue(D)}}function ce(D){E.props.trigger.indexOf("focusin")<0&&D.target!==lt()||E.props.interactive&&D.relatedTarget&&fe.contains(D.relatedTarget)||Ue(D)}function Pe(D){return P.isTouch?Jt()!==D.type.indexOf("touch")>=0:!1}function _e(){ke();var D=E.props,K=D.popperOptions,te=D.placement,ge=D.offset,we=D.getReferenceClientRect,Ke=D.moveTransition,Je=rt()?Xt(fe).arrow:null,Dt=we?{getBoundingClientRect:we,contextElement:we.contextElement||lt()}:g,Un={name:"$$tippy",enabled:!0,phase:"beforeWrite",requires:["computeStyles"],fn:function(pn){var en=pn.state;if(rt()){var Gr=un(),tn=Gr.box;["placement","reference-hidden","escaped"].forEach(function(hn){hn==="placement"?tn.setAttribute("data-placement",en.placement):en.attributes.popper["data-popper-"+hn]?tn.setAttribute("data-"+hn,""):tn.removeAttribute("data-"+hn)}),en.attributes.popper={}}}},_t=[{name:"offset",options:{offset:ge}},{name:"preventOverflow",options:{padding:{top:2,bottom:2,left:5,right:5}}},{name:"flip",options:{padding:5}},{name:"computeStyles",options:{adaptive:!Ke}},Un];rt()&&Je&&_t.push({name:"arrow",options:{element:Je,padding:3}}),_t.push.apply(_t,K?.modifiers||[]),E.popperInstance=t.createPopper(Dt,fe,Object.assign({},K,{placement:te,onFirstUpdate:je,modifiers:_t}))}function ke(){E.popperInstance&&(E.popperInstance.destroy(),E.popperInstance=null)}function Le(){var D=E.props.appendTo,K,te=lt();E.props.interactive&&D===Ze.appendTo||D==="parent"?K=te.parentNode:K=O(D,[te]),K.contains(fe)||K.appendChild(fe),_e(),mt(E.props.interactive&&D===Ze.appendTo&&te.nextElementSibling!==fe,["Interactive tippy element may not be accessible via keyboard","navigation because it is not directly after the reference element","in the DOM source order.",` + +`,"Using a wrapper
or tag around the reference element","solves this by creating a new parentNode context.",` + +`,"Specifying `appendTo: document.body` silences this warning, but it","assumes you are using a focus management solution to handle","keyboard navigation.",` + +`,"See: https://atomiks.github.io/tippyjs/v6/accessibility/#interactivity"].join(" "))}function Ye(){return Y(fe.querySelectorAll("[data-tippy-root]"))}function $e(D){E.clearDelayTimeouts(),D&&b("onTrigger",[E,D]),oe();var K=d(!0),te=Kt(),ge=te[0],we=te[1];P.isTouch&&ge==="hold"&&we&&(K=we),K?L=setTimeout(function(){E.show()},K):E.show()}function Ue(D){if(E.clearDelayTimeouts(),b("onUntrigger",[E,D]),!E.state.isVisible){z();return}if(!(E.props.trigger.indexOf("mouseenter")>=0&&E.props.trigger.indexOf("click")>=0&&["mouseleave","mousemove"].indexOf(D.type)>=0&&B)){var K=d(!1);K?q=setTimeout(function(){E.state.isVisible&&E.hide()},K):W=requestAnimationFrame(function(){E.hide()})}}function wt(){E.state.isEnabled=!0}function et(){E.hide(),E.state.isEnabled=!1}function Ft(){clearTimeout(L),clearTimeout(q),cancelAnimationFrame(W)}function dn(D){if(mt(E.state.isDestroyed,Vt("setProps")),!E.state.isDestroyed){b("onBeforeUpdate",[E,D]),Se();var K=E.props,te=ir(g,Object.assign({},E.props,{},D,{ignoreAttributes:!0}));E.props=te,Me(),K.interactiveDebounce!==te.interactiveDebounce&&(F(),Ie=S(Re,te.interactiveDebounce)),K.triggerTarget&&!te.triggerTarget?I(K.triggerTarget).forEach(function(ge){ge.removeAttribute("aria-expanded")}):te.triggerTarget&&g.removeAttribute("aria-expanded"),T(),s(),Lt&&Lt(K,te),E.popperInstance&&(_e(),Ye().forEach(function(ge){requestAnimationFrame(ge._tippy.popperInstance.forceUpdate)})),b("onAfterUpdate",[E,D])}}function Qt(D){E.setProps({content:D})}function kt(){mt(E.state.isDestroyed,Vt("show"));var D=E.state.isVisible,K=E.state.isDestroyed,te=!E.state.isEnabled,ge=P.isTouch&&!E.props.touch,we=y(E.props.duration,0,Ze.duration);if(!(D||K||te||ge)&&!lt().hasAttribute("disabled")&&(b("onShow",[E],!1),E.props.onShow(E)!==!1)){if(E.state.isVisible=!0,rt()&&(fe.style.visibility="visible"),s(),oe(),E.state.isMounted||(fe.style.transition="none"),rt()){var Ke=un(),Je=Ke.box,Dt=Ke.content;l([Je,Dt],0)}je=function(){var _t;if(!(!E.state.isVisible||pe)){if(pe=!0,fe.offsetHeight,fe.style.transition=E.props.moveTransition,rt()&&E.props.animation){var An=un(),pn=An.box,en=An.content;l([pn,en],we),h([pn,en],"visible")}_(),T(),$(wn,E),(_t=E.popperInstance)==null||_t.forceUpdate(),E.state.isMounted=!0,b("onMount",[E]),E.props.animation&&rt()&&Fe(we,function(){E.state.isShown=!0,b("onShown",[E])})}},Le()}}function $n(){mt(E.state.isDestroyed,Vt("hide"));var D=!E.state.isVisible,K=E.state.isDestroyed,te=!E.state.isEnabled,ge=y(E.props.duration,1,Ze.duration);if(!(D||K||te)&&(b("onHide",[E],!1),E.props.onHide(E)!==!1)){if(E.state.isVisible=!1,E.state.isShown=!1,pe=!1,B=!1,rt()&&(fe.style.visibility="hidden"),F(),z(),s(),rt()){var we=un(),Ke=we.box,Je=we.content;E.props.animation&&(l([Ke,Je],ge),h([Ke,Je],"hidden"))}_(),T(),E.props.animation?rt()&&De(ge,E.unmount):E.unmount()}}function Zt(D){mt(E.state.isDestroyed,Vt("hideWithInteractivity")),yt().addEventListener("mousemove",Ie),$(yn,Ie),Ie(D)}function Sn(){mt(E.state.isDestroyed,Vt("unmount")),E.state.isVisible&&E.hide(),E.state.isMounted&&(ke(),Ye().forEach(function(D){D._tippy.unmount()}),fe.parentNode&&fe.parentNode.removeChild(fe),wn=wn.filter(function(D){return D!==E}),E.state.isMounted=!1,b("onHidden",[E]))}function Wn(){mt(E.state.isDestroyed,Vt("destroy")),!E.state.isDestroyed&&(E.clearDelayTimeouts(),E.unmount(),Se(),delete g._tippy,E.state.isDestroyed=!0,b("onDestroy",[E]))}}function dt(g,w){w===void 0&&(w={});var C=Ze.plugins.concat(w.plugins||[]);At(g),gt(w,C),Ut();var L=Object.assign({},w,{plugins:C}),q=me(g),W=V(L.content),B=q.length>1;mt(W&&B,["tippy() was passed an Element as the `content` prop, but more than","one tippy instance was created by this invocation. This means the","content element will only be appended to the last tippy instance.",` + +`,"Instead, pass the .innerHTML of the element, or use a function that","returns a cloned version of the element instead.",` + +`,`1) content: element.innerHTML +`,"2) content: () => element.cloneNode(true)"].join(" "));var be=q.reduce(function(le,pe){var ye=pe&&cn(pe,L);return ye&&le.push(ye),le},[]);return V(g)?be[0]:be}dt.defaultProps=Ze,dt.setDefaultProps=Wr,dt.currentInput=P;var lr=function(w){var C=w===void 0?{}:w,L=C.exclude,q=C.duration;wn.forEach(function(W){var B=!1;if(L&&(B=Q(L)?W.reference===L:W.popper===L.popper),!B){var be=W.props.duration;W.setProps({duration:q}),W.hide(),W.state.isDestroyed||W.setProps({duration:be})}})},cr=Object.assign({},t.applyStyles,{effect:function(w){var C=w.state,L={popper:{position:C.options.strategy,left:"0",top:"0",margin:"0"},arrow:{position:"absolute"},reference:{}};Object.assign(C.elements.popper.style,L.popper),C.styles=L,C.elements.arrow&&Object.assign(C.elements.arrow.style,L.arrow)}}),fr=function(w,C){var L;C===void 0&&(C={}),zt(!Array.isArray(w),["The first argument passed to createSingleton() must be an array of","tippy instances. The passed value was",String(w)].join(" "));var q=w,W=[],B,be=C.overrides,le=[],pe=!1;function ye(){W=q.map(function(ee){return ee.reference})}function Te(ee){q.forEach(function(ie){ee?ie.enable():ie.disable()})}function je(ee){return q.map(function(ie){var E=ie.setProps;return ie.setProps=function(Ge){E(Ge),ie.reference===B&&ee.setProps(Ge)},function(){ie.setProps=E}})}function Ae(ee,ie){var E=W.indexOf(ie);if(ie!==B){B=ie;var Ge=(be||[]).concat("content").reduce(function(fe,Lt){return fe[Lt]=q[E].props[Lt],fe},{});ee.setProps(Object.assign({},Ge,{getReferenceClientRect:typeof Ge.getReferenceClientRect=="function"?Ge.getReferenceClientRect:function(){return ie.getBoundingClientRect()}}))}}Te(!1),ye();var Ie={fn:function(){return{onDestroy:function(){Te(!0)},onHidden:function(){B=null},onClickOutside:function(E){E.props.showOnCreate&&!pe&&(pe=!0,B=null)},onShow:function(E){E.props.showOnCreate&&!pe&&(pe=!0,Ae(E,W[0]))},onTrigger:function(E,Ge){Ae(E,Ge.currentTarget)}}}},re=dt(J(),Object.assign({},x(C,["overrides"]),{plugins:[Ie].concat(C.plugins||[]),triggerTarget:W,popperOptions:Object.assign({},C.popperOptions,{modifiers:[].concat(((L=C.popperOptions)==null?void 0:L.modifiers)||[],[cr])})})),he=re.show;re.show=function(ee){if(he(),!B&&ee==null)return Ae(re,W[0]);if(!(B&&ee==null)){if(typeof ee=="number")return W[ee]&&Ae(re,W[ee]);if(q.includes(ee)){var ie=ee.reference;return Ae(re,ie)}if(W.includes(ee))return Ae(re,ee)}},re.showNext=function(){var ee=W[0];if(!B)return re.show(0);var ie=W.indexOf(B);re.show(W[ie+1]||ee)},re.showPrevious=function(){var ee=W[W.length-1];if(!B)return re.show(ee);var ie=W.indexOf(B),E=W[ie-1]||ee;re.show(E)};var ve=re.setProps;return re.setProps=function(ee){be=ee.overrides||be,ve(ee)},re.setInstances=function(ee){Te(!0),le.forEach(function(ie){return ie()}),q=ee,Te(!1),ye(),je(re),re.setProps({triggerTarget:W})},le=je(re),re},ur={mouseover:"mouseenter",focusin:"focus",click:"click"};function qt(g,w){zt(!(w&&w.target),["You must specity a `target` prop indicating a CSS selector string matching","the target elements that should receive a tippy."].join(" "));var C=[],L=[],q=!1,W=w.target,B=x(w,["target"]),be=Object.assign({},B,{trigger:"manual",touch:!1}),le=Object.assign({},B,{showOnCreate:!0}),pe=dt(g,be),ye=I(pe);function Te(he){if(!(!he.target||q)){var ve=he.target.closest(W);if(ve){var ee=ve.getAttribute("data-tippy-trigger")||w.trigger||Ze.trigger;if(!ve._tippy&&!(he.type==="touchstart"&&typeof le.touch=="boolean")&&!(he.type!=="touchstart"&&ee.indexOf(ur[he.type])<0)){var ie=dt(ve,le);ie&&(L=L.concat(ie))}}}}function je(he,ve,ee,ie){ie===void 0&&(ie=!1),he.addEventListener(ve,ee,ie),C.push({node:he,eventType:ve,handler:ee,options:ie})}function Ae(he){var ve=he.reference;je(ve,"touchstart",Te,f),je(ve,"mouseover",Te),je(ve,"focusin",Te),je(ve,"click",Te)}function Ie(){C.forEach(function(he){var ve=he.node,ee=he.eventType,ie=he.handler,E=he.options;ve.removeEventListener(ee,ie,E)}),C=[]}function re(he){var ve=he.destroy,ee=he.enable,ie=he.disable;he.destroy=function(E){E===void 0&&(E=!0),E&&L.forEach(function(Ge){Ge.destroy()}),L=[],Ie(),ve()},he.enable=function(){ee(),L.forEach(function(E){return E.enable()}),q=!1},he.disable=function(){ie(),L.forEach(function(E){return E.disable()}),q=!0},Ae(he)}return ye.forEach(re),pe}var dr={name:"animateFill",defaultValue:!1,fn:function(w){var C;if(!((C=w.props.render)!=null&&C.$$tippy))return zt(w.props.animateFill,"The `animateFill` plugin requires the default render function."),{};var L=Xt(w.popper),q=L.box,W=L.content,B=w.props.animateFill?Vr():null;return{onCreate:function(){B&&(q.insertBefore(B,q.firstElementChild),q.setAttribute("data-animatefill",""),q.style.overflow="hidden",w.setProps({arrow:!1,animation:"shift-away"}))},onMount:function(){if(B){var le=q.style.transitionDuration,pe=Number(le.replace("ms",""));W.style.transitionDelay=Math.round(pe/10)+"ms",B.style.transitionDuration=le,h([B],"visible")}},onShow:function(){B&&(B.style.transitionDuration="0ms")},onHide:function(){B&&h([B],"hidden")}}}};function Vr(){var g=J();return g.className=o,h([g],"hidden"),g}var xn={clientX:0,clientY:0},fn=[];function En(g){var w=g.clientX,C=g.clientY;xn={clientX:w,clientY:C}}function On(g){g.addEventListener("mousemove",En)}function zr(g){g.removeEventListener("mousemove",En)}var jn={name:"followCursor",defaultValue:!1,fn:function(w){var C=w.reference,L=v(w.props.triggerTarget||C),q=!1,W=!1,B=!0,be=w.props;function le(){return w.props.followCursor==="initial"&&w.state.isVisible}function pe(){L.addEventListener("mousemove",je)}function ye(){L.removeEventListener("mousemove",je)}function Te(){q=!0,w.setProps({getReferenceClientRect:null}),q=!1}function je(re){var he=re.target?C.contains(re.target):!0,ve=w.props.followCursor,ee=re.clientX,ie=re.clientY,E=C.getBoundingClientRect(),Ge=ee-E.left,fe=ie-E.top;(he||!w.props.interactive)&&w.setProps({getReferenceClientRect:function(){var bt=C.getBoundingClientRect(),Gt=ee,Kt=ie;ve==="initial"&&(Gt=bt.left+Ge,Kt=bt.top+fe);var Jt=ve==="horizontal"?bt.top:Kt,rt=ve==="vertical"?bt.right:Gt,lt=ve==="horizontal"?bt.bottom:Kt,yt=ve==="vertical"?bt.left:Gt;return{width:rt-yt,height:lt-Jt,top:Jt,right:rt,bottom:lt,left:yt}}})}function Ae(){w.props.followCursor&&(fn.push({instance:w,doc:L}),On(L))}function Ie(){fn=fn.filter(function(re){return re.instance!==w}),fn.filter(function(re){return re.doc===L}).length===0&&zr(L)}return{onCreate:Ae,onDestroy:Ie,onBeforeUpdate:function(){be=w.props},onAfterUpdate:function(he,ve){var ee=ve.followCursor;q||ee!==void 0&&be.followCursor!==ee&&(Ie(),ee?(Ae(),w.state.isMounted&&!W&&!le()&&pe()):(ye(),Te()))},onMount:function(){w.props.followCursor&&!W&&(B&&(je(xn),B=!1),le()||pe())},onTrigger:function(he,ve){X(ve)&&(xn={clientX:ve.clientX,clientY:ve.clientY}),W=ve.type==="focus"},onHidden:function(){w.props.followCursor&&(Te(),ye(),B=!0)}}}};function Yr(g,w){var C;return{popperOptions:Object.assign({},g.popperOptions,{modifiers:[].concat((((C=g.popperOptions)==null?void 0:C.modifiers)||[]).filter(function(L){var q=L.name;return q!==w.name}),[w])})}}var Bn={name:"inlinePositioning",defaultValue:!1,fn:function(w){var C=w.reference;function L(){return!!w.props.inlinePositioning}var q,W=-1,B=!1,be={name:"tippyInlinePositioning",enabled:!0,phase:"afterWrite",fn:function(je){var Ae=je.state;L()&&(q!==Ae.placement&&w.setProps({getReferenceClientRect:function(){return le(Ae.placement)}}),q=Ae.placement)}};function le(Te){return Xr(N(Te),C.getBoundingClientRect(),Y(C.getClientRects()),W)}function pe(Te){B=!0,w.setProps(Te),B=!1}function ye(){B||pe(Yr(w.props,be))}return{onCreate:ye,onAfterUpdate:ye,onTrigger:function(je,Ae){if(X(Ae)){var Ie=Y(w.reference.getClientRects()),re=Ie.find(function(he){return he.left-2<=Ae.clientX&&he.right+2>=Ae.clientX&&he.top-2<=Ae.clientY&&he.bottom+2>=Ae.clientY});W=Ie.indexOf(re)}},onUntrigger:function(){W=-1}}}};function Xr(g,w,C,L){if(C.length<2||g===null)return w;if(C.length===2&&L>=0&&C[0].left>C[1].right)return C[L]||w;switch(g){case"top":case"bottom":{var q=C[0],W=C[C.length-1],B=g==="top",be=q.top,le=W.bottom,pe=B?q.left:W.left,ye=B?q.right:W.right,Te=ye-pe,je=le-be;return{top:be,bottom:le,left:pe,right:ye,width:Te,height:je}}case"left":case"right":{var Ae=Math.min.apply(Math,C.map(function(fe){return fe.left})),Ie=Math.max.apply(Math,C.map(function(fe){return fe.right})),re=C.filter(function(fe){return g==="left"?fe.left===Ae:fe.right===Ie}),he=re[0].top,ve=re[re.length-1].bottom,ee=Ae,ie=Ie,E=ie-ee,Ge=ve-he;return{top:he,bottom:ve,left:ee,right:ie,width:E,height:Ge}}default:return w}}var qr={name:"sticky",defaultValue:!1,fn:function(w){var C=w.reference,L=w.popper;function q(){return w.popperInstance?w.popperInstance.state.elements.reference:C}function W(pe){return w.props.sticky===!0||w.props.sticky===pe}var B=null,be=null;function le(){var pe=W("reference")?q().getBoundingClientRect():null,ye=W("popper")?L.getBoundingClientRect():null;(pe&&Hn(B,pe)||ye&&Hn(be,ye))&&w.popperInstance&&w.popperInstance.update(),B=pe,be=ye,w.state.isMounted&&requestAnimationFrame(le)}return{onMount:function(){w.props.sticky&&le()}}}};function Hn(g,w){return g&&w?g.top!==w.top||g.right!==w.right||g.bottom!==w.bottom||g.left!==w.left:!0}dt.setDefaultProps({render:ar}),e.animateFill=dr,e.createSingleton=fr,e.default=dt,e.delegate=qt,e.followCursor=jn,e.hideAll=lr,e.inlinePositioning=Bn,e.roundArrow=r,e.sticky=qr}),Si=Ho($o()),Ts=Ho($o()),Ps=e=>{let t={plugins:[]},r=i=>e[e.indexOf(i)+1];if(e.includes("animation")&&(t.animation=r("animation")),e.includes("duration")&&(t.duration=parseInt(r("duration"))),e.includes("delay")){let i=r("delay");t.delay=i.includes("-")?i.split("-").map(o=>parseInt(o)):parseInt(i)}if(e.includes("cursor")){t.plugins.push(Ts.followCursor);let i=r("cursor");["x","initial"].includes(i)?t.followCursor=i==="x"?"horizontal":"initial":t.followCursor=!0}e.includes("on")&&(t.trigger=r("on")),e.includes("arrowless")&&(t.arrow=!1),e.includes("html")&&(t.allowHTML=!0),e.includes("interactive")&&(t.interactive=!0),e.includes("border")&&t.interactive&&(t.interactiveBorder=parseInt(r("border"))),e.includes("debounce")&&t.interactive&&(t.interactiveDebounce=parseInt(r("debounce"))),e.includes("max-width")&&(t.maxWidth=parseInt(r("max-width"))),e.includes("theme")&&(t.theme=r("theme")),e.includes("placement")&&(t.placement=r("placement"));let n={};return e.includes("no-flip")&&(n.modifiers||(n.modifiers=[]),n.modifiers.push({name:"flip",enabled:!1})),t.popperOptions=n,t};function Ai(e){e.magic("tooltip",t=>(r,n={})=>{let i=n.timeout;delete n.timeout;let o=(0,Si.default)(t,{content:r,trigger:"manual",...n});o.show(),setTimeout(()=>{o.hide(),setTimeout(()=>o.destroy(),n.duration||300)},i||2e3)}),e.directive("tooltip",(t,{modifiers:r,expression:n},{evaluateLater:i,effect:o,cleanup:a})=>{let c=r.length>0?Ps(r):{};t.__x_tippy||(t.__x_tippy=(0,Si.default)(t,c)),a(()=>{t.__x_tippy&&(t.__x_tippy.destroy(),delete t.__x_tippy)});let f=()=>t.__x_tippy.enable(),u=()=>t.__x_tippy.disable(),y=m=>{m?(f(),t.__x_tippy.setContent(m)):u()};if(r.includes("raw"))y(n);else{let m=i(n);o(()=>{m(O=>{typeof O=="object"?(t.__x_tippy.setProps(O),f()):y(O)})})}})}Ai.defaultProps=e=>(Si.default.setDefaultProps(e),Ai);var Ms=Ai,Wo=Ms;var Uo=()=>({toggle(e){this.$refs.panel?.toggle(e)},open(e){this.$refs.panel?.open(e)},close(e){this.$refs.panel?.close(e)}});var Vo=()=>({form:null,isProcessing:!1,processingMessage:null,init(){let e=this.$el.closest("form");e?.addEventListener("form-processing-started",t=>{this.isProcessing=!0,this.processingMessage=t.detail.message}),e?.addEventListener("form-processing-finished",()=>{this.isProcessing=!1})}});var zo=({id:e})=>({isOpen:!1,isWindowVisible:!1,livewire:null,textSelectionClosePreventionMouseDownHandler:null,textSelectionClosePreventionMouseUpHandler:null,textSelectionClosePreventionClickHandler:null,init(){this.$nextTick(()=>{this.isWindowVisible=this.isOpen,this.setUpTextSelectionClosePrevention(),this.$watch("isOpen",()=>this.isWindowVisible=this.isOpen)})},setUpTextSelectionClosePrevention(){let t=".fi-modal-window",r=".fi-modal-close-overlay",i=!1,o=0;this.textSelectionClosePreventionClickHandler=c=>{c.stopPropagation(),c.preventDefault(),document.removeEventListener("click",this.textSelectionClosePreventionClickHandler,!0)};let a=c=>!c.target.closest(t)&&(c.target.closest(r)||c.target.closest("body"));this.textSelectionClosePreventionMouseDownHandler=c=>{o=Date.now(),i=!!c.target.closest(t)},this.textSelectionClosePreventionMouseUpHandler=c=>{let f=Date.now()-o<75;i&&a(c)&&!f?document.addEventListener("click",this.textSelectionClosePreventionClickHandler,!0):document.removeEventListener("click",this.textSelectionClosePreventionClickHandler,!0),i=!1},document.addEventListener("mousedown",this.textSelectionClosePreventionMouseDownHandler,!0),document.addEventListener("mouseup",this.textSelectionClosePreventionMouseUpHandler,!0)},isTopmost(){if(!e)return!0;let t=document.querySelectorAll(".fi-modal-open");return t.length===0?!1:t[t.length-1].id===e},close(){this.closeQuietly(),this.$dispatch("modal-closed",{id:e})},closeQuietly(){this.isOpen=!1},open(){this.$nextTick(()=>{this.isOpen=!0,document.dispatchEvent(new CustomEvent("x-modal-opened",{bubbles:!0,composed:!0,detail:{id:e}}))})},destroy(){this.textSelectionClosePreventionMouseDownHandler&&(document.removeEventListener("mousedown",this.textSelectionClosePreventionMouseDownHandler,!0),this.textSelectionClosePreventionMouseDownHandler=null),this.textSelectionClosePreventionMouseUpHandler&&(document.removeEventListener("mouseup",this.textSelectionClosePreventionMouseUpHandler,!0),this.textSelectionClosePreventionMouseUpHandler=null),this.textSelectionClosePreventionClickHandler&&(document.removeEventListener("click",this.textSelectionClosePreventionClickHandler,!0),this.textSelectionClosePreventionClickHandler=null)}});document.addEventListener("livewire:init",()=>{let e=t=>{let r=Alpine.findClosest(t,n=>n.__livewire);if(!r)throw"Could not find Livewire component in DOM tree.";return r.__livewire};Livewire.interceptMessage(({message:t,onSuccess:r})=>{r(({payload:o})=>{queueMicrotask(()=>{if(!o.effects?.html)for(let[a,c]of Object.entries(o.effects?.partials??{})){let f=Array.from(t.component.el.querySelectorAll(`[wire\\:partial="${a}"]`)).filter(x=>e(x)===t.component);if(!f.length)continue;if(f.length>1)throw`Multiple elements found for partial [${a}].`;let u=f[0],y=u.parentElement?u.parentElement.tagName.toLowerCase():"div",m=document.createElement(y);m.innerHTML=c,m.__livewire=t.component;let O=m.firstElementChild;O.__livewire=t.component;let S={};u.querySelectorAll("[wire\\:id]").forEach(x=>{S[x.getAttribute("wire:id")]=x}),O.querySelectorAll("[wire\\:id]").forEach(x=>{if(x.hasAttribute("wire:snapshot"))return;let M=x.getAttribute("wire:id"),I=S[M];I&&x.replaceWith(I.cloneNode(!0))}),window.Alpine.morph(u,O,{updating:(x,M,I,$)=>{if(!n(x)){if(x.__livewire_replace===!0&&(x.innerHTML=M.innerHTML),x.__livewire_replace_self===!0)return x.outerHTML=M.outerHTML,$();if(x.__livewire_ignore===!0||(x.__livewire_ignore_self===!0&&I(),i(x)&&x.getAttribute("wire:id")!==t.component.id))return $();i(x)&&(M.__livewire=t.component)}},key:x=>{if(!n(x))return x.hasAttribute("wire:key")?x.getAttribute("wire:key"):x.hasAttribute("wire:id")?x.getAttribute("wire:id"):x.id},lookahead:!1})}})});function n(o){return typeof o.hasAttribute!="function"}function i(o){return o.hasAttribute("wire:id")}})});var Yo=(e,t,r)=>{let n=(y,m)=>{for(let O of y){let S=i(O,m);if(S!==null)return S}},i=(y,m)=>{let O=y.match(/^[\{\[]([^\[\]\{\}]*)[\}\]](.*)/s);if(O===null||O.length!==3)return null;let S=O[1],x=O[2];if(S.includes(",")){let[M,I]=S.split(",",2);if(I==="*"&&m>=M)return x;if(M==="*"&&m<=I)return x;if(m>=M&&m<=I)return x}return S==m?x:null},o=y=>y.toString().charAt(0).toUpperCase()+y.toString().slice(1),a=(y,m)=>{if(m.length===0)return y;let O={};for(let[S,x]of Object.entries(m))O[":"+o(S??"")]=o(x??""),O[":"+S.toUpperCase()]=x.toString().toUpperCase(),O[":"+S]=x;return Object.entries(O).forEach(([S,x])=>{y=y.replaceAll(S,x)}),y},c=y=>y.map(m=>m.replace(/^[\{\[]([^\[\]\{\}]*)[\}\]]/,"")),f=e.split("|"),u=n(f,t);return u!=null?a(u.trim(),r):(f=c(f),a(f.length>1&&t>1?f[1]:f[0],r))};document.addEventListener("alpine:init",()=>{window.Alpine.plugin(oo),window.Alpine.plugin(ao),window.Alpine.plugin(fo),window.Alpine.plugin(jo),window.Alpine.plugin(Wo),window.Alpine.data("filamentDropdown",Uo),window.Alpine.data("filamentFormButton",Vo),window.Alpine.data("filamentModal",zo)});window.jsMd5=Xo.md5;window.pluralize=Yo;})(); +/*! Bundled license information: + +js-md5/src/md5.js: + (** + * [js-md5]{@link https://github.com/emn178/js-md5} + * + * @namespace md5 + * @version 0.8.3 + * @author Chen, Yi-Cyuan [emn178@gmail.com] + * @copyright Chen, Yi-Cyuan 2014-2023 + * @license MIT + *) + +sortablejs/modular/sortable.esm.js: + (**! + * Sortable 1.15.6 + * @author RubaXa + * @author owenm + * @license MIT + *) +*/ diff --git a/public/js/filament/tables/components/columns/checkbox.js b/public/js/filament/tables/components/columns/checkbox.js new file mode 100644 index 00000000..b26a4260 --- /dev/null +++ b/public/js/filament/tables/components/columns/checkbox.js @@ -0,0 +1 @@ +function a({name:r,recordKey:s,state:n}){return{error:void 0,isLoading:!1,state:n,unsubscribeLivewireHook:null,init(){this.unsubscribeLivewireHook=Livewire.interceptMessage(({message:e,onSuccess:t})=>{t(()=>{this.$nextTick(()=>{if(this.isLoading||e.component.id!==this.$root.closest("[wire\\:id]")?.attributes["wire:id"].value)return;let i=this.getServerState();i===void 0||Alpine.raw(this.state)===i||(this.state=i)})})}),this.$watch("state",async()=>{let e=this.getServerState();if(e===void 0||Alpine.raw(this.state)===e)return;this.isLoading=!0;let t=await this.$wire.updateTableColumnState(r,s,this.state);this.error=t?.error??void 0,!this.error&&this.$refs.serverState&&(this.$refs.serverState.value=this.state?"1":"0"),this.isLoading=!1})},getServerState(){if(this.$refs.serverState)return[1,"1"].includes(this.$refs.serverState.value)},destroy(){this.unsubscribeLivewireHook?.()}}}export{a as default}; diff --git a/public/js/filament/tables/components/columns/select.js b/public/js/filament/tables/components/columns/select.js new file mode 100644 index 00000000..a75f759f --- /dev/null +++ b/public/js/filament/tables/components/columns/select.js @@ -0,0 +1,11 @@ +var Ft=Math.min,vt=Math.max,Ht=Math.round;var st=n=>({x:n,y:n}),ji={left:"right",right:"left",bottom:"top",top:"bottom"},qi={start:"end",end:"start"};function De(n,t,e){return vt(n,Ft(t,e))}function Vt(n,t){return typeof n=="function"?n(t):n}function wt(n){return n.split("-")[0]}function Wt(n){return n.split("-")[1]}function Ae(n){return n==="x"?"y":"x"}function Ce(n){return n==="y"?"height":"width"}var Ji=new Set(["top","bottom"]);function ht(n){return Ji.has(wt(n))?"y":"x"}function Le(n){return Ae(ht(n))}function Je(n,t,e){e===void 0&&(e=!1);let i=Wt(n),o=Le(n),s=Ce(o),r=o==="x"?i===(e?"end":"start")?"right":"left":i==="start"?"bottom":"top";return t.reference[s]>t.floating[s]&&(r=Bt(r)),[r,Bt(r)]}function Qe(n){let t=Bt(n);return[ie(n),t,ie(t)]}function ie(n){return n.replace(/start|end/g,t=>qi[t])}var je=["left","right"],qe=["right","left"],Qi=["top","bottom"],Zi=["bottom","top"];function tn(n,t,e){switch(n){case"top":case"bottom":return e?t?qe:je:t?je:qe;case"left":case"right":return t?Qi:Zi;default:return[]}}function Ze(n,t,e,i){let o=Wt(n),s=tn(wt(n),e==="start",i);return o&&(s=s.map(r=>r+"-"+o),t&&(s=s.concat(s.map(ie)))),s}function Bt(n){return n.replace(/left|right|bottom|top/g,t=>ji[t])}function en(n){return{top:0,right:0,bottom:0,left:0,...n}}function ti(n){return typeof n!="number"?en(n):{top:n,right:n,bottom:n,left:n}}function Ot(n){let{x:t,y:e,width:i,height:o}=n;return{width:i,height:o,top:e,left:t,right:t+i,bottom:e+o,x:t,y:e}}function ei(n,t,e){let{reference:i,floating:o}=n,s=ht(t),r=Le(t),a=Ce(r),l=wt(t),c=s==="y",f=i.x+i.width/2-o.width/2,d=i.y+i.height/2-o.height/2,p=i[a]/2-o[a]/2,u;switch(l){case"top":u={x:f,y:i.y-o.height};break;case"bottom":u={x:f,y:i.y+i.height};break;case"right":u={x:i.x+i.width,y:d};break;case"left":u={x:i.x-o.width,y:d};break;default:u={x:i.x,y:i.y}}switch(Wt(t)){case"start":u[r]-=p*(e&&c?-1:1);break;case"end":u[r]+=p*(e&&c?-1:1);break}return u}var ii=async(n,t,e)=>{let{placement:i="bottom",strategy:o="absolute",middleware:s=[],platform:r}=e,a=s.filter(Boolean),l=await(r.isRTL==null?void 0:r.isRTL(t)),c=await r.getElementRects({reference:n,floating:t,strategy:o}),{x:f,y:d}=ei(c,i,l),p=i,u={},g=0;for(let m=0;mH<=0)){var L,X;let H=(((L=s.flip)==null?void 0:L.index)||0)+1,tt=q[H];if(tt&&(!(d==="alignment"?y!==ht(tt):!1)||W.every(B=>ht(B.placement)===y?B.overflows[0]>0:!0)))return{data:{index:H,overflows:W},reset:{placement:tt}};let z=(X=W.filter(Y=>Y.overflows[0]<=0).sort((Y,B)=>Y.overflows[1]-B.overflows[1])[0])==null?void 0:X.placement;if(!z)switch(u){case"bestFit":{var M;let Y=(M=W.filter(B=>{if(F){let et=ht(B.placement);return et===y||et==="y"}return!0}).map(B=>[B.placement,B.overflows.filter(et=>et>0).reduce((et,ee)=>et+ee,0)]).sort((B,et)=>B[1]-et[1])[0])==null?void 0:M[0];Y&&(z=Y);break}case"initialPlacement":z=a;break}if(o!==z)return{reset:{placement:z}}}return{}}}};var nn=new Set(["left","top"]);async function on(n,t){let{placement:e,platform:i,elements:o}=n,s=await(i.isRTL==null?void 0:i.isRTL(o.floating)),r=wt(e),a=Wt(e),l=ht(e)==="y",c=nn.has(r)?-1:1,f=s&&l?-1:1,d=Vt(t,n),{mainAxis:p,crossAxis:u,alignmentAxis:g}=typeof d=="number"?{mainAxis:d,crossAxis:0,alignmentAxis:null}:{mainAxis:d.mainAxis||0,crossAxis:d.crossAxis||0,alignmentAxis:d.alignmentAxis};return a&&typeof g=="number"&&(u=a==="end"?g*-1:g),l?{x:u*f,y:p*c}:{x:p*c,y:u*f}}var oi=function(n){return n===void 0&&(n=0),{name:"offset",options:n,async fn(t){var e,i;let{x:o,y:s,placement:r,middlewareData:a}=t,l=await on(t,n);return r===((e=a.offset)==null?void 0:e.placement)&&(i=a.arrow)!=null&&i.alignmentOffset?{}:{x:o+l.x,y:s+l.y,data:{...l,placement:r}}}}},si=function(n){return n===void 0&&(n={}),{name:"shift",options:n,async fn(t){let{x:e,y:i,placement:o}=t,{mainAxis:s=!0,crossAxis:r=!1,limiter:a={fn:S=>{let{x:E,y}=S;return{x:E,y}}},...l}=Vt(n,t),c={x:e,y:i},f=await Ie(t,l),d=ht(wt(o)),p=Ae(d),u=c[p],g=c[d];if(s){let S=p==="y"?"top":"left",E=p==="y"?"bottom":"right",y=u+f[S],D=u-f[E];u=De(y,u,D)}if(r){let S=d==="y"?"top":"left",E=d==="y"?"bottom":"right",y=g+f[S],D=g-f[E];g=De(y,g,D)}let m=a.fn({...t,[p]:u,[d]:g});return{...m,data:{x:m.x-e,y:m.y-i,enabled:{[p]:s,[d]:r}}}}}};function oe(){return typeof window<"u"}function Et(n){return ai(n)?(n.nodeName||"").toLowerCase():"#document"}function U(n){var t;return(n==null||(t=n.ownerDocument)==null?void 0:t.defaultView)||window}function ct(n){var t;return(t=(ai(n)?n.ownerDocument:n.document)||window.document)==null?void 0:t.documentElement}function ai(n){return oe()?n instanceof Node||n instanceof U(n).Node:!1}function it(n){return oe()?n instanceof Element||n instanceof U(n).Element:!1}function rt(n){return oe()?n instanceof HTMLElement||n instanceof U(n).HTMLElement:!1}function ri(n){return!oe()||typeof ShadowRoot>"u"?!1:n instanceof ShadowRoot||n instanceof U(n).ShadowRoot}var sn=new Set(["inline","contents"]);function It(n){let{overflow:t,overflowX:e,overflowY:i,display:o}=nt(n);return/auto|scroll|overlay|hidden|clip/.test(t+i+e)&&!sn.has(o)}var rn=new Set(["table","td","th"]);function li(n){return rn.has(Et(n))}var an=[":popover-open",":modal"];function zt(n){return an.some(t=>{try{return n.matches(t)}catch{return!1}})}var ln=["transform","translate","scale","rotate","perspective"],cn=["transform","translate","scale","rotate","perspective","filter"],dn=["paint","layout","strict","content"];function se(n){let t=re(),e=it(n)?nt(n):n;return ln.some(i=>e[i]?e[i]!=="none":!1)||(e.containerType?e.containerType!=="normal":!1)||!t&&(e.backdropFilter?e.backdropFilter!=="none":!1)||!t&&(e.filter?e.filter!=="none":!1)||cn.some(i=>(e.willChange||"").includes(i))||dn.some(i=>(e.contain||"").includes(i))}function ci(n){let t=ut(n);for(;rt(t)&&!Dt(t);){if(se(t))return t;if(zt(t))return null;t=ut(t)}return null}function re(){return typeof CSS>"u"||!CSS.supports?!1:CSS.supports("-webkit-backdrop-filter","none")}var fn=new Set(["html","body","#document"]);function Dt(n){return fn.has(Et(n))}function nt(n){return U(n).getComputedStyle(n)}function $t(n){return it(n)?{scrollLeft:n.scrollLeft,scrollTop:n.scrollTop}:{scrollLeft:n.scrollX,scrollTop:n.scrollY}}function ut(n){if(Et(n)==="html")return n;let t=n.assignedSlot||n.parentNode||ri(n)&&n.host||ct(n);return ri(t)?t.host:t}function di(n){let t=ut(n);return Dt(t)?n.ownerDocument?n.ownerDocument.body:n.body:rt(t)&&It(t)?t:di(t)}function ne(n,t,e){var i;t===void 0&&(t=[]),e===void 0&&(e=!0);let o=di(n),s=o===((i=n.ownerDocument)==null?void 0:i.body),r=U(o);if(s){let a=ae(r);return t.concat(r,r.visualViewport||[],It(o)?o:[],a&&e?ne(a):[])}return t.concat(o,ne(o,[],e))}function ae(n){return n.parent&&Object.getPrototypeOf(n.parent)?n.frameElement:null}function pi(n){let t=nt(n),e=parseFloat(t.width)||0,i=parseFloat(t.height)||0,o=rt(n),s=o?n.offsetWidth:e,r=o?n.offsetHeight:i,a=Ht(e)!==s||Ht(i)!==r;return a&&(e=s,i=r),{width:e,height:i,$:a}}function gi(n){return it(n)?n:n.contextElement}function Tt(n){let t=gi(n);if(!rt(t))return st(1);let e=t.getBoundingClientRect(),{width:i,height:o,$:s}=pi(t),r=(s?Ht(e.width):e.width)/i,a=(s?Ht(e.height):e.height)/o;return(!r||!Number.isFinite(r))&&(r=1),(!a||!Number.isFinite(a))&&(a=1),{x:r,y:a}}var hn=st(0);function mi(n){let t=U(n);return!re()||!t.visualViewport?hn:{x:t.visualViewport.offsetLeft,y:t.visualViewport.offsetTop}}function un(n,t,e){return t===void 0&&(t=!1),!e||t&&e!==U(n)?!1:t}function Xt(n,t,e,i){t===void 0&&(t=!1),e===void 0&&(e=!1);let o=n.getBoundingClientRect(),s=gi(n),r=st(1);t&&(i?it(i)&&(r=Tt(i)):r=Tt(n));let a=un(s,e,i)?mi(s):st(0),l=(o.left+a.x)/r.x,c=(o.top+a.y)/r.y,f=o.width/r.x,d=o.height/r.y;if(s){let p=U(s),u=i&&it(i)?U(i):i,g=p,m=ae(g);for(;m&&i&&u!==g;){let S=Tt(m),E=m.getBoundingClientRect(),y=nt(m),D=E.left+(m.clientLeft+parseFloat(y.paddingLeft))*S.x,A=E.top+(m.clientTop+parseFloat(y.paddingTop))*S.y;l*=S.x,c*=S.y,f*=S.x,d*=S.y,l+=D,c+=A,g=U(m),m=ae(g)}}return Ot({width:f,height:d,x:l,y:c})}function le(n,t){let e=$t(n).scrollLeft;return t?t.left+e:Xt(ct(n)).left+e}function bi(n,t){let e=n.getBoundingClientRect(),i=e.left+t.scrollLeft-le(n,e),o=e.top+t.scrollTop;return{x:i,y:o}}function pn(n){let{elements:t,rect:e,offsetParent:i,strategy:o}=n,s=o==="fixed",r=ct(i),a=t?zt(t.floating):!1;if(i===r||a&&s)return e;let l={scrollLeft:0,scrollTop:0},c=st(1),f=st(0),d=rt(i);if((d||!d&&!s)&&((Et(i)!=="body"||It(r))&&(l=$t(i)),rt(i))){let u=Xt(i);c=Tt(i),f.x=u.x+i.clientLeft,f.y=u.y+i.clientTop}let p=r&&!d&&!s?bi(r,l):st(0);return{width:e.width*c.x,height:e.height*c.y,x:e.x*c.x-l.scrollLeft*c.x+f.x+p.x,y:e.y*c.y-l.scrollTop*c.y+f.y+p.y}}function gn(n){return Array.from(n.getClientRects())}function mn(n){let t=ct(n),e=$t(n),i=n.ownerDocument.body,o=vt(t.scrollWidth,t.clientWidth,i.scrollWidth,i.clientWidth),s=vt(t.scrollHeight,t.clientHeight,i.scrollHeight,i.clientHeight),r=-e.scrollLeft+le(n),a=-e.scrollTop;return nt(i).direction==="rtl"&&(r+=vt(t.clientWidth,i.clientWidth)-o),{width:o,height:s,x:r,y:a}}var fi=25;function bn(n,t){let e=U(n),i=ct(n),o=e.visualViewport,s=i.clientWidth,r=i.clientHeight,a=0,l=0;if(o){s=o.width,r=o.height;let f=re();(!f||f&&t==="fixed")&&(a=o.offsetLeft,l=o.offsetTop)}let c=le(i);if(c<=0){let f=i.ownerDocument,d=f.body,p=getComputedStyle(d),u=f.compatMode==="CSS1Compat"&&parseFloat(p.marginLeft)+parseFloat(p.marginRight)||0,g=Math.abs(i.clientWidth-d.clientWidth-u);g<=fi&&(s-=g)}else c<=fi&&(s+=c);return{width:s,height:r,x:a,y:l}}var vn=new Set(["absolute","fixed"]);function wn(n,t){let e=Xt(n,!0,t==="fixed"),i=e.top+n.clientTop,o=e.left+n.clientLeft,s=rt(n)?Tt(n):st(1),r=n.clientWidth*s.x,a=n.clientHeight*s.y,l=o*s.x,c=i*s.y;return{width:r,height:a,x:l,y:c}}function hi(n,t,e){let i;if(t==="viewport")i=bn(n,e);else if(t==="document")i=mn(ct(n));else if(it(t))i=wn(t,e);else{let o=mi(n);i={x:t.x-o.x,y:t.y-o.y,width:t.width,height:t.height}}return Ot(i)}function vi(n,t){let e=ut(n);return e===t||!it(e)||Dt(e)?!1:nt(e).position==="fixed"||vi(e,t)}function yn(n,t){let e=t.get(n);if(e)return e;let i=ne(n,[],!1).filter(a=>it(a)&&Et(a)!=="body"),o=null,s=nt(n).position==="fixed",r=s?ut(n):n;for(;it(r)&&!Dt(r);){let a=nt(r),l=se(r);!l&&a.position==="fixed"&&(o=null),(s?!l&&!o:!l&&a.position==="static"&&!!o&&vn.has(o.position)||It(r)&&!l&&vi(n,r))?i=i.filter(f=>f!==r):o=a,r=ut(r)}return t.set(n,i),i}function Sn(n){let{element:t,boundary:e,rootBoundary:i,strategy:o}=n,r=[...e==="clippingAncestors"?zt(t)?[]:yn(t,this._c):[].concat(e),i],a=r[0],l=r.reduce((c,f)=>{let d=hi(t,f,o);return c.top=vt(d.top,c.top),c.right=Ft(d.right,c.right),c.bottom=Ft(d.bottom,c.bottom),c.left=vt(d.left,c.left),c},hi(t,a,o));return{width:l.right-l.left,height:l.bottom-l.top,x:l.left,y:l.top}}function xn(n){let{width:t,height:e}=pi(n);return{width:t,height:e}}function On(n,t,e){let i=rt(t),o=ct(t),s=e==="fixed",r=Xt(n,!0,s,t),a={scrollLeft:0,scrollTop:0},l=st(0);function c(){l.x=le(o)}if(i||!i&&!s)if((Et(t)!=="body"||It(o))&&(a=$t(t)),i){let u=Xt(t,!0,s,t);l.x=u.x+t.clientLeft,l.y=u.y+t.clientTop}else o&&c();s&&!i&&o&&c();let f=o&&!i&&!s?bi(o,a):st(0),d=r.left+a.scrollLeft-l.x-f.x,p=r.top+a.scrollTop-l.y-f.y;return{x:d,y:p,width:r.width,height:r.height}}function Te(n){return nt(n).position==="static"}function ui(n,t){if(!rt(n)||nt(n).position==="fixed")return null;if(t)return t(n);let e=n.offsetParent;return ct(n)===e&&(e=e.ownerDocument.body),e}function wi(n,t){let e=U(n);if(zt(n))return e;if(!rt(n)){let o=ut(n);for(;o&&!Dt(o);){if(it(o)&&!Te(o))return o;o=ut(o)}return e}let i=ui(n,t);for(;i&&li(i)&&Te(i);)i=ui(i,t);return i&&Dt(i)&&Te(i)&&!se(i)?e:i||ci(n)||e}var En=async function(n){let t=this.getOffsetParent||wi,e=this.getDimensions,i=await e(n.floating);return{reference:On(n.reference,await t(n.floating),n.strategy),floating:{x:0,y:0,width:i.width,height:i.height}}};function Dn(n){return nt(n).direction==="rtl"}var An={convertOffsetParentRelativeRectToViewportRelativeRect:pn,getDocumentElement:ct,getClippingRect:Sn,getOffsetParent:wi,getElementRects:En,getClientRects:gn,getDimensions:xn,getScale:Tt,isElement:it,isRTL:Dn};var yi=oi;var Si=si,xi=ni;var Oi=(n,t,e)=>{let i=new Map,o={platform:An,...e},s={...o.platform,_c:i};return ii(n,t,{...o,platform:s})};function Ei(n,t){var e=Object.keys(n);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(n);t&&(i=i.filter(function(o){return Object.getOwnPropertyDescriptor(n,o).enumerable})),e.push.apply(e,i)}return e}function ft(n){for(var t=1;t=0)&&(e[o]=n[o]);return e}function In(n,t){if(n==null)return{};var e=Ln(n,t),i,o;if(Object.getOwnPropertySymbols){var s=Object.getOwnPropertySymbols(n);for(o=0;o=0)&&Object.prototype.propertyIsEnumerable.call(n,i)&&(e[i]=n[i])}return e}var Tn="1.15.6";function pt(n){if(typeof window<"u"&&window.navigator)return!!navigator.userAgent.match(n)}var mt=pt(/(?:Trident.*rv[ :]?11\.|msie|iemobile|Windows Phone)/i),Zt=pt(/Edge/i),Di=pt(/firefox/i),Gt=pt(/safari/i)&&!pt(/chrome/i)&&!pt(/android/i),Xe=pt(/iP(ad|od|hone)/i),Pi=pt(/chrome/i)&&pt(/android/i),Mi={capture:!1,passive:!1};function O(n,t,e){n.addEventListener(t,e,!mt&&Mi)}function x(n,t,e){n.removeEventListener(t,e,!mt&&Mi)}function ve(n,t){if(t){if(t[0]===">"&&(t=t.substring(1)),n)try{if(n.matches)return n.matches(t);if(n.msMatchesSelector)return n.msMatchesSelector(t);if(n.webkitMatchesSelector)return n.webkitMatchesSelector(t)}catch{return!1}return!1}}function Ni(n){return n.host&&n!==document&&n.host.nodeType?n.host:n.parentNode}function lt(n,t,e,i){if(n){e=e||document;do{if(t!=null&&(t[0]===">"?n.parentNode===e&&ve(n,t):ve(n,t))||i&&n===e)return n;if(n===e)break}while(n=Ni(n))}return null}var Ai=/\s+/g;function Q(n,t,e){if(n&&t)if(n.classList)n.classList[e?"add":"remove"](t);else{var i=(" "+n.className+" ").replace(Ai," ").replace(" "+t+" "," ");n.className=(i+(e?" "+t:"")).replace(Ai," ")}}function b(n,t,e){var i=n&&n.style;if(i){if(e===void 0)return document.defaultView&&document.defaultView.getComputedStyle?e=document.defaultView.getComputedStyle(n,""):n.currentStyle&&(e=n.currentStyle),t===void 0?e:e[t];!(t in i)&&t.indexOf("webkit")===-1&&(t="-webkit-"+t),i[t]=e+(typeof e=="string"?"":"px")}}function Nt(n,t){var e="";if(typeof n=="string")e=n;else do{var i=b(n,"transform");i&&i!=="none"&&(e=i+" "+e)}while(!t&&(n=n.parentNode));var o=window.DOMMatrix||window.WebKitCSSMatrix||window.CSSMatrix||window.MSCSSMatrix;return o&&new o(e)}function ki(n,t,e){if(n){var i=n.getElementsByTagName(t),o=0,s=i.length;if(e)for(;o=s:r=o<=s,!r)return i;if(i===dt())break;i=xt(i,!1)}return!1}function kt(n,t,e,i){for(var o=0,s=0,r=n.children;s2&&arguments[2]!==void 0?arguments[2]:{},o=i.evt,s=In(i,Fn);te.pluginEvent.bind(v)(t,e,ft({dragEl:h,parentEl:R,ghostEl:w,rootEl:I,nextEl:Lt,lastDownEl:pe,cloneEl:T,cloneHidden:St,dragStarted:Kt,putSortable:V,activeSortable:v.active,originalEvent:o,oldIndex:Mt,oldDraggableIndex:qt,newIndex:Z,newDraggableIndex:yt,hideGhostForTarget:Xi,unhideGhostForTarget:Ki,cloneNowHidden:function(){St=!0},cloneNowShown:function(){St=!1},dispatchSortableEvent:function(a){K({sortable:e,name:a,originalEvent:o})}},s))};function K(n){Bn(ft({putSortable:V,cloneEl:T,targetEl:h,rootEl:I,oldIndex:Mt,oldDraggableIndex:qt,newIndex:Z,newDraggableIndex:yt},n))}var h,R,w,I,Lt,pe,T,St,Mt,Z,qt,yt,ce,V,Pt=!1,we=!1,ye=[],At,at,Pe,Me,Ii,Ti,Kt,Rt,Jt,Qt=!1,de=!1,ge,$,Ne=[],Ve=!1,Se=[],Oe=typeof document<"u",fe=Xe,_i=Zt||mt?"cssFloat":"float",Hn=Oe&&!Pi&&!Xe&&"draggable"in document.createElement("div"),Wi=(function(){if(Oe){if(mt)return!1;var n=document.createElement("x");return n.style.cssText="pointer-events:auto",n.style.pointerEvents==="auto"}})(),zi=function(t,e){var i=b(t),o=parseInt(i.width)-parseInt(i.paddingLeft)-parseInt(i.paddingRight)-parseInt(i.borderLeftWidth)-parseInt(i.borderRightWidth),s=kt(t,0,e),r=kt(t,1,e),a=s&&b(s),l=r&&b(r),c=a&&parseInt(a.marginLeft)+parseInt(a.marginRight)+k(s).width,f=l&&parseInt(l.marginLeft)+parseInt(l.marginRight)+k(r).width;if(i.display==="flex")return i.flexDirection==="column"||i.flexDirection==="column-reverse"?"vertical":"horizontal";if(i.display==="grid")return i.gridTemplateColumns.split(" ").length<=1?"vertical":"horizontal";if(s&&a.float&&a.float!=="none"){var d=a.float==="left"?"left":"right";return r&&(l.clear==="both"||l.clear===d)?"vertical":"horizontal"}return s&&(a.display==="block"||a.display==="flex"||a.display==="table"||a.display==="grid"||c>=o&&i[_i]==="none"||r&&i[_i]==="none"&&c+f>o)?"vertical":"horizontal"},Vn=function(t,e,i){var o=i?t.left:t.top,s=i?t.right:t.bottom,r=i?t.width:t.height,a=i?e.left:e.top,l=i?e.right:e.bottom,c=i?e.width:e.height;return o===a||s===l||o+r/2===a+c/2},Wn=function(t,e){var i;return ye.some(function(o){var s=o[j].options.emptyInsertThreshold;if(!(!s||Ke(o))){var r=k(o),a=t>=r.left-s&&t<=r.right+s,l=e>=r.top-s&&e<=r.bottom+s;if(a&&l)return i=o}}),i},$i=function(t){function e(s,r){return function(a,l,c,f){var d=a.options.group.name&&l.options.group.name&&a.options.group.name===l.options.group.name;if(s==null&&(r||d))return!0;if(s==null||s===!1)return!1;if(r&&s==="clone")return s;if(typeof s=="function")return e(s(a,l,c,f),r)(a,l,c,f);var p=(r?a:l).options.group.name;return s===!0||typeof s=="string"&&s===p||s.join&&s.indexOf(p)>-1}}var i={},o=t.group;(!o||ue(o)!="object")&&(o={name:o}),i.name=o.name,i.checkPull=e(o.pull,!0),i.checkPut=e(o.put),i.revertClone=o.revertClone,t.group=i},Xi=function(){!Wi&&w&&b(w,"display","none")},Ki=function(){!Wi&&w&&b(w,"display","")};Oe&&!Pi&&document.addEventListener("click",function(n){if(we)return n.preventDefault(),n.stopPropagation&&n.stopPropagation(),n.stopImmediatePropagation&&n.stopImmediatePropagation(),we=!1,!1},!0);var Ct=function(t){if(h){t=t.touches?t.touches[0]:t;var e=Wn(t.clientX,t.clientY);if(e){var i={};for(var o in t)t.hasOwnProperty(o)&&(i[o]=t[o]);i.target=i.rootEl=e,i.preventDefault=void 0,i.stopPropagation=void 0,e[j]._onDragOver(i)}}},zn=function(t){h&&h.parentNode[j]._isOutsideThisEl(t.target)};function v(n,t){if(!(n&&n.nodeType&&n.nodeType===1))throw"Sortable: `el` must be an HTMLElement, not ".concat({}.toString.call(n));this.el=n,this.options=t=gt({},t),n[j]=this;var e={group:null,sort:!0,disabled:!1,store:null,handle:null,draggable:/^[uo]l$/i.test(n.nodeName)?">li":">*",swapThreshold:1,invertSwap:!1,invertedSwapThreshold:null,removeCloneOnHide:!0,direction:function(){return zi(n,this.options)},ghostClass:"sortable-ghost",chosenClass:"sortable-chosen",dragClass:"sortable-drag",ignore:"a, img",filter:null,preventOnFilter:!0,animation:0,easing:null,setData:function(r,a){r.setData("Text",a.textContent)},dropBubble:!1,dragoverBubble:!1,dataIdAttr:"data-id",delay:0,delayOnTouchOnly:!1,touchStartThreshold:(Number.parseInt?Number:window).parseInt(window.devicePixelRatio,10)||1,forceFallback:!1,fallbackClass:"sortable-fallback",fallbackOnBody:!1,fallbackTolerance:0,fallbackOffset:{x:0,y:0},supportPointer:v.supportPointer!==!1&&"PointerEvent"in window&&(!Gt||Xe),emptyInsertThreshold:5};te.initializePlugins(this,n,e);for(var i in e)!(i in t)&&(t[i]=e[i]);$i(t);for(var o in this)o.charAt(0)==="_"&&typeof this[o]=="function"&&(this[o]=this[o].bind(this));this.nativeDraggable=t.forceFallback?!1:Hn,this.nativeDraggable&&(this.options.touchStartThreshold=1),t.supportPointer?O(n,"pointerdown",this._onTapStart):(O(n,"mousedown",this._onTapStart),O(n,"touchstart",this._onTapStart)),this.nativeDraggable&&(O(n,"dragover",this),O(n,"dragenter",this)),ye.push(this.el),t.store&&t.store.get&&this.sort(t.store.get(this)||[]),gt(this,Mn())}v.prototype={constructor:v,_isOutsideThisEl:function(t){!this.el.contains(t)&&t!==this.el&&(Rt=null)},_getDirection:function(t,e){return typeof this.options.direction=="function"?this.options.direction.call(this,t,e,h):this.options.direction},_onTapStart:function(t){if(t.cancelable){var e=this,i=this.el,o=this.options,s=o.preventOnFilter,r=t.type,a=t.touches&&t.touches[0]||t.pointerType&&t.pointerType==="touch"&&t,l=(a||t).target,c=t.target.shadowRoot&&(t.path&&t.path[0]||t.composedPath&&t.composedPath()[0])||l,f=o.filter;if(qn(i),!h&&!(/mousedown|pointerdown/.test(r)&&t.button!==0||o.disabled)&&!c.isContentEditable&&!(!this.nativeDraggable&&Gt&&l&&l.tagName.toUpperCase()==="SELECT")&&(l=lt(l,o.draggable,i,!1),!(l&&l.animated)&&pe!==l)){if(Mt=ot(l),qt=ot(l,o.draggable),typeof f=="function"){if(f.call(this,t,l,this)){K({sortable:e,rootEl:c,name:"filter",targetEl:l,toEl:i,fromEl:i}),G("filter",e,{evt:t}),s&&t.preventDefault();return}}else if(f&&(f=f.split(",").some(function(d){if(d=lt(c,d.trim(),i,!1),d)return K({sortable:e,rootEl:d,name:"filter",targetEl:l,fromEl:i,toEl:i}),G("filter",e,{evt:t}),!0}),f)){s&&t.preventDefault();return}o.handle&&!lt(c,o.handle,i,!1)||this._prepareDragStart(t,a,l)}}},_prepareDragStart:function(t,e,i){var o=this,s=o.el,r=o.options,a=s.ownerDocument,l;if(i&&!h&&i.parentNode===s){var c=k(i);if(I=s,h=i,R=h.parentNode,Lt=h.nextSibling,pe=i,ce=r.group,v.dragged=h,At={target:h,clientX:(e||t).clientX,clientY:(e||t).clientY},Ii=At.clientX-c.left,Ti=At.clientY-c.top,this._lastX=(e||t).clientX,this._lastY=(e||t).clientY,h.style["will-change"]="all",l=function(){if(G("delayEnded",o,{evt:t}),v.eventCanceled){o._onDrop();return}o._disableDelayedDragEvents(),!Di&&o.nativeDraggable&&(h.draggable=!0),o._triggerDragStart(t,e),K({sortable:o,name:"choose",originalEvent:t}),Q(h,r.chosenClass,!0)},r.ignore.split(",").forEach(function(f){ki(h,f.trim(),ke)}),O(a,"dragover",Ct),O(a,"mousemove",Ct),O(a,"touchmove",Ct),r.supportPointer?(O(a,"pointerup",o._onDrop),!this.nativeDraggable&&O(a,"pointercancel",o._onDrop)):(O(a,"mouseup",o._onDrop),O(a,"touchend",o._onDrop),O(a,"touchcancel",o._onDrop)),Di&&this.nativeDraggable&&(this.options.touchStartThreshold=4,h.draggable=!0),G("delayStart",this,{evt:t}),r.delay&&(!r.delayOnTouchOnly||e)&&(!this.nativeDraggable||!(Zt||mt))){if(v.eventCanceled){this._onDrop();return}r.supportPointer?(O(a,"pointerup",o._disableDelayedDrag),O(a,"pointercancel",o._disableDelayedDrag)):(O(a,"mouseup",o._disableDelayedDrag),O(a,"touchend",o._disableDelayedDrag),O(a,"touchcancel",o._disableDelayedDrag)),O(a,"mousemove",o._delayedDragTouchMoveHandler),O(a,"touchmove",o._delayedDragTouchMoveHandler),r.supportPointer&&O(a,"pointermove",o._delayedDragTouchMoveHandler),o._dragStartTimer=setTimeout(l,r.delay)}else l()}},_delayedDragTouchMoveHandler:function(t){var e=t.touches?t.touches[0]:t;Math.max(Math.abs(e.clientX-this._lastX),Math.abs(e.clientY-this._lastY))>=Math.floor(this.options.touchStartThreshold/(this.nativeDraggable&&window.devicePixelRatio||1))&&this._disableDelayedDrag()},_disableDelayedDrag:function(){h&&ke(h),clearTimeout(this._dragStartTimer),this._disableDelayedDragEvents()},_disableDelayedDragEvents:function(){var t=this.el.ownerDocument;x(t,"mouseup",this._disableDelayedDrag),x(t,"touchend",this._disableDelayedDrag),x(t,"touchcancel",this._disableDelayedDrag),x(t,"pointerup",this._disableDelayedDrag),x(t,"pointercancel",this._disableDelayedDrag),x(t,"mousemove",this._delayedDragTouchMoveHandler),x(t,"touchmove",this._delayedDragTouchMoveHandler),x(t,"pointermove",this._delayedDragTouchMoveHandler)},_triggerDragStart:function(t,e){e=e||t.pointerType=="touch"&&t,!this.nativeDraggable||e?this.options.supportPointer?O(document,"pointermove",this._onTouchMove):e?O(document,"touchmove",this._onTouchMove):O(document,"mousemove",this._onTouchMove):(O(h,"dragend",this),O(I,"dragstart",this._onDragStart));try{document.selection?me(function(){document.selection.empty()}):window.getSelection().removeAllRanges()}catch{}},_dragStarted:function(t,e){if(Pt=!1,I&&h){G("dragStarted",this,{evt:e}),this.nativeDraggable&&O(document,"dragover",zn);var i=this.options;!t&&Q(h,i.dragClass,!1),Q(h,i.ghostClass,!0),v.active=this,t&&this._appendGhost(),K({sortable:this,name:"start",originalEvent:e})}else this._nulling()},_emulateDragOver:function(){if(at){this._lastX=at.clientX,this._lastY=at.clientY,Xi();for(var t=document.elementFromPoint(at.clientX,at.clientY),e=t;t&&t.shadowRoot&&(t=t.shadowRoot.elementFromPoint(at.clientX,at.clientY),t!==e);)e=t;if(h.parentNode[j]._isOutsideThisEl(t),e)do{if(e[j]){var i=void 0;if(i=e[j]._onDragOver({clientX:at.clientX,clientY:at.clientY,target:t,rootEl:e}),i&&!this.options.dragoverBubble)break}t=e}while(e=Ni(e));Ki()}},_onTouchMove:function(t){if(At){var e=this.options,i=e.fallbackTolerance,o=e.fallbackOffset,s=t.touches?t.touches[0]:t,r=w&&Nt(w,!0),a=w&&r&&r.a,l=w&&r&&r.d,c=fe&&$&&Li($),f=(s.clientX-At.clientX+o.x)/(a||1)+(c?c[0]-Ne[0]:0)/(a||1),d=(s.clientY-At.clientY+o.y)/(l||1)+(c?c[1]-Ne[1]:0)/(l||1);if(!v.active&&!Pt){if(i&&Math.max(Math.abs(s.clientX-this._lastX),Math.abs(s.clientY-this._lastY))=0&&(K({rootEl:R,name:"add",toEl:R,fromEl:I,originalEvent:t}),K({sortable:this,name:"remove",toEl:R,originalEvent:t}),K({rootEl:R,name:"sort",toEl:R,fromEl:I,originalEvent:t}),K({sortable:this,name:"sort",toEl:R,originalEvent:t})),V&&V.save()):Z!==Mt&&Z>=0&&(K({sortable:this,name:"update",toEl:R,originalEvent:t}),K({sortable:this,name:"sort",toEl:R,originalEvent:t})),v.active&&((Z==null||Z===-1)&&(Z=Mt,yt=qt),K({sortable:this,name:"end",toEl:R,originalEvent:t}),this.save()))),this._nulling()},_nulling:function(){G("nulling",this),I=h=R=w=Lt=T=pe=St=At=at=Kt=Z=yt=Mt=qt=Rt=Jt=V=ce=v.dragged=v.ghost=v.clone=v.active=null,Se.forEach(function(t){t.checked=!0}),Se.length=Pe=Me=0},handleEvent:function(t){switch(t.type){case"drop":case"dragend":this._onDrop(t);break;case"dragenter":case"dragover":h&&(this._onDragOver(t),$n(t));break;case"selectstart":t.preventDefault();break}},toArray:function(){for(var t=[],e,i=this.el.children,o=0,s=i.length,r=this.options;oo.right+s||n.clientY>i.bottom&&n.clientX>i.left:n.clientY>o.bottom+s||n.clientX>i.right&&n.clientY>i.top}function Un(n,t,e,i,o,s,r,a){var l=i?n.clientY:n.clientX,c=i?e.height:e.width,f=i?e.top:e.left,d=i?e.bottom:e.right,p=!1;if(!r){if(a&&gef+c*s/2:ld-ge)return-Jt}else if(l>f+c*(1-o)/2&&ld-c*s/2)?l>f+c/2?1:-1:0}function Gn(n){return ot(h){},options:L,optionsLimit:X=null,placeholder:M,position:H=null,searchableOptionFields:tt=["label"],searchDebounce:z=1e3,searchingMessage:Y="Searching...",searchPrompt:B="Search...",state:et,statePath:ee=null}){this.canOptionLabelsWrap=t,this.canSelectPlaceholder=e,this.element=i,this.getOptionLabelUsing=o,this.getOptionLabelsUsing=s,this.getOptionsUsing=r,this.getSearchResultsUsing=a,this.hasDynamicOptions=l,this.hasDynamicSearchResults=c,this.hasInitialNoOptionsMessage=f,this.initialOptionLabel=d,this.initialOptionLabels=p,this.initialState=u,this.isAutofocused=g,this.isDisabled=m,this.isHtmlAllowed=S,this.isMultiple=E,this.isReorderable=y,this.isSearchable=D,this.livewireId=A,this.loadingMessage=C,this.maxItems=F,this.maxItemsMessage=q,this.noOptionsMessage=J,this.noSearchResultsMessage=_,this.onStateChange=W,this.options=L,this.optionsLimit=X,this.originalOptions=JSON.parse(JSON.stringify(L)),this.placeholder=M,this.position=H,this.searchableOptionFields=Array.isArray(tt)?tt:["label"],this.searchDebounce=z,this.searchingMessage=Y,this.searchPrompt=B,this.state=et,this.statePath=ee,this.activeSearchId=0,this.labelRepository={},this.isOpen=!1,this.selectedIndex=-1,this.searchQuery="",this.searchTimeout=null,this.isSearching=!1,this.selectedDisplayVersion=0,this.render(),this.setUpEventListeners(),this.isAutofocused&&this.selectButton.focus()}populateLabelRepositoryFromOptions(t){if(!(!t||!Array.isArray(t)))for(let e of t)e.options&&Array.isArray(e.options)?this.populateLabelRepositoryFromOptions(e.options):e.value!==void 0&&e.label!==void 0&&(this.labelRepository[e.value]=e.label)}render(){this.populateLabelRepositoryFromOptions(this.options),this.container=document.createElement("div"),this.container.className="fi-select-input-ctn",this.canOptionLabelsWrap||this.container.classList.add("fi-select-input-ctn-option-labels-not-wrapped"),this.container.setAttribute("aria-haspopup","listbox"),this.selectButton=document.createElement("button"),this.selectButton.className="fi-select-input-btn",this.selectButton.type="button",this.selectButton.setAttribute("aria-expanded","false"),this.selectedDisplay=document.createElement("div"),this.selectedDisplay.className="fi-select-input-value-ctn",this.updateSelectedDisplay(),this.selectButton.appendChild(this.selectedDisplay),this.dropdown=document.createElement("div"),this.dropdown.className="fi-dropdown-panel fi-scrollable",this.dropdown.setAttribute("role","listbox"),this.dropdown.setAttribute("tabindex","-1"),this.dropdown.style.display="none",this.dropdownId=`fi-select-input-dropdown-${Math.random().toString(36).substring(2,11)}`,this.dropdown.id=this.dropdownId,this.isMultiple&&this.dropdown.setAttribute("aria-multiselectable","true"),this.isSearchable&&(this.searchContainer=document.createElement("div"),this.searchContainer.className="fi-select-input-search-ctn",this.searchInput=document.createElement("input"),this.searchInput.className="fi-input",this.searchInput.type="text",this.searchInput.placeholder=this.searchPrompt,this.searchInput.setAttribute("aria-label","Search"),this.searchContainer.appendChild(this.searchInput),this.dropdown.appendChild(this.searchContainer),this.searchInput.addEventListener("input",t=>{this.isDisabled||this.handleSearch(t)}),this.searchInput.addEventListener("keydown",t=>{if(!this.isDisabled){if(t.key==="Tab"){t.preventDefault();let e=this.getVisibleOptions();if(e.length===0)return;t.shiftKey?this.selectedIndex=e.length-1:this.selectedIndex=0,e.forEach(i=>{i.classList.remove("fi-selected")}),e[this.selectedIndex].classList.add("fi-selected"),e[this.selectedIndex].focus()}else if(t.key==="ArrowDown"){if(t.preventDefault(),t.stopPropagation(),this.getVisibleOptions().length===0)return;this.selectedIndex=-1,this.searchInput.blur(),this.focusNextOption()}else if(t.key==="ArrowUp"){t.preventDefault(),t.stopPropagation();let e=this.getVisibleOptions();if(e.length===0)return;this.selectedIndex=e.length-1,this.searchInput.blur(),e[this.selectedIndex].classList.add("fi-selected"),e[this.selectedIndex].focus(),e[this.selectedIndex].id&&this.dropdown.setAttribute("aria-activedescendant",e[this.selectedIndex].id),this.scrollOptionIntoView(e[this.selectedIndex])}else if(t.key==="Enter"){if(t.preventDefault(),t.stopPropagation(),this.isSearching)return;let e=this.getVisibleOptions();if(e.length===0)return;let i=e.find(s=>{let r=s.getAttribute("aria-disabled")==="true",a=s.classList.contains("fi-disabled"),l=s.offsetParent===null;return!(r||a||l)});if(!i)return;let o=i.getAttribute("data-value");if(o===null)return;this.selectOption(o)}}})),this.optionsList=document.createElement("ul"),this.renderOptions(),this.container.appendChild(this.selectButton),this.container.appendChild(this.dropdown),this.element.appendChild(this.container),this.applyDisabledState()}renderOptions(){this.optionsList.innerHTML="";let t=0,e=this.options,i=0,o=!1;this.options.forEach(a=>{a.options&&Array.isArray(a.options)?(i+=a.options.length,o=!0):i++}),o?this.optionsList.className="fi-select-input-options-ctn":i>0&&(this.optionsList.className="fi-dropdown-list");let s=o?null:this.optionsList,r=0;for(let a of e){if(this.optionsLimit>0&&r>=this.optionsLimit)break;if(a.options&&Array.isArray(a.options)){let l=a.options;if(this.isMultiple&&Array.isArray(this.state)&&this.state.length>0&&(l=a.options.filter(c=>!this.state.includes(c.value))),l.length>0){if(this.optionsLimit>0){let c=this.optionsLimit-r;c{let a=this.createOptionElement(r.value,r);s.appendChild(a)}),i.appendChild(o),i.appendChild(s),this.optionsList.appendChild(i)}createOptionElement(t,e){let i=t,o=e,s=!1;typeof e=="object"&&e!==null&&"label"in e&&"value"in e&&(i=e.value,o=e.label,s=e.isDisabled||!1);let r=document.createElement("li");r.className="fi-dropdown-list-item fi-select-input-option",s&&r.classList.add("fi-disabled");let a=`fi-select-input-option-${Math.random().toString(36).substring(2,11)}`;if(r.id=a,r.setAttribute("role","option"),r.setAttribute("data-value",i),r.setAttribute("tabindex","0"),s&&r.setAttribute("aria-disabled","true"),this.isHtmlAllowed&&typeof o=="string"){let f=document.createElement("div");f.innerHTML=o;let d=f.textContent||f.innerText||o;r.setAttribute("aria-label",d)}let l=this.isMultiple?Array.isArray(this.state)&&this.state.includes(i):this.state===i;r.setAttribute("aria-selected",l?"true":"false"),l&&r.classList.add("fi-selected");let c=document.createElement("span");return this.isHtmlAllowed?c.innerHTML=o:c.textContent=o,r.appendChild(c),s||r.addEventListener("click",f=>{f.preventDefault(),f.stopPropagation(),this.selectOption(i),this.isMultiple&&(this.isSearchable&&this.searchInput?setTimeout(()=>{this.searchInput.focus()},0):setTimeout(()=>{r.focus()},0))}),r}async updateSelectedDisplay(){this.selectedDisplayVersion=this.selectedDisplayVersion+1;let t=this.selectedDisplayVersion,e=document.createDocumentFragment();if(this.isMultiple){if(!Array.isArray(this.state)||this.state.length===0){let o=document.createElement("span");o.textContent=this.placeholder,o.classList.add("fi-select-input-placeholder"),e.appendChild(o)}else{let o=await this.getLabelsForMultipleSelection();if(t!==this.selectedDisplayVersion)return;this.addBadgesForSelectedOptions(o,e)}t===this.selectedDisplayVersion&&(this.selectedDisplay.replaceChildren(e),this.isOpen&&this.deferPositionDropdown());return}if(this.state===null||this.state===""){let o=document.createElement("span");if(o.textContent=this.placeholder,o.classList.add("fi-select-input-placeholder"),e.appendChild(o),t===this.selectedDisplayVersion){this.selectedDisplay.replaceChildren(e);let s=this.container.querySelector(".fi-select-input-value-remove-btn");s&&s.remove(),this.container.classList.remove("fi-select-input-ctn-clearable")}return}let i=await this.getLabelForSingleSelection();t===this.selectedDisplayVersion&&(this.addSingleSelectionDisplay(i,e),t===this.selectedDisplayVersion&&this.selectedDisplay.replaceChildren(e))}async getLabelsForMultipleSelection(){let t=this.getSelectedOptionLabels(),e=[];if(Array.isArray(this.state)){for(let o of this.state)if(!P(this.labelRepository[o])){if(P(t[o])){this.labelRepository[o]=t[o];continue}e.push(o.toString())}}if(e.length>0&&P(this.initialOptionLabels)&&JSON.stringify(this.state)===JSON.stringify(this.initialState)){if(Array.isArray(this.initialOptionLabels))for(let o of this.initialOptionLabels)P(o)&&o.value!==void 0&&o.label!==void 0&&e.includes(o.value)&&(this.labelRepository[o.value]=o.label)}else if(e.length>0&&this.getOptionLabelsUsing)try{let o=await this.getOptionLabelsUsing();for(let s of o)P(s)&&s.value!==void 0&&s.label!==void 0&&(this.labelRepository[s.value]=s.label)}catch(o){console.error("Error fetching option labels:",o)}let i=[];if(Array.isArray(this.state))for(let o of this.state)P(this.labelRepository[o])?i.push(this.labelRepository[o]):P(t[o])?i.push(t[o]):i.push(o);return i}createBadgeElement(t,e){let i=document.createElement("span");i.className="fi-badge fi-size-md fi-color fi-color-primary fi-text-color-600 dark:fi-text-color-200",P(t)&&i.setAttribute("data-value",t);let o=document.createElement("span");o.className="fi-badge-label-ctn";let s=document.createElement("span");s.className="fi-badge-label",this.canOptionLabelsWrap&&s.classList.add("fi-wrapped"),this.isHtmlAllowed?s.innerHTML=e:s.textContent=e,o.appendChild(s),i.appendChild(o);let r=this.createRemoveButton(t,e);return i.appendChild(r),i}createRemoveButton(t,e){let i=document.createElement("button");return i.type="button",i.className="fi-badge-delete-btn",i.innerHTML='',i.setAttribute("aria-label","Remove "+(this.isHtmlAllowed?e.replace(/<[^>]*>/g,""):e)),i.addEventListener("click",o=>{o.stopPropagation(),P(t)&&this.selectOption(t)}),i.addEventListener("keydown",o=>{(o.key===" "||o.key==="Enter")&&(o.preventDefault(),o.stopPropagation(),P(t)&&this.selectOption(t))}),i}addBadgesForSelectedOptions(t,e=this.selectedDisplay){let i=document.createElement("div");i.className="fi-select-input-value-badges-ctn",t.forEach((o,s)=>{let r=Array.isArray(this.state)?this.state[s]:null,a=this.createBadgeElement(r,o);i.appendChild(a)}),e.appendChild(i),this.isReorderable&&(i.addEventListener("click",o=>{o.stopPropagation()}),i.addEventListener("mousedown",o=>{o.stopPropagation()}),new Ui(i,{animation:150,onEnd:()=>{let o=[];i.querySelectorAll("[data-value]").forEach(s=>{o.push(s.getAttribute("data-value"))}),this.state=o,this.onStateChange(this.state)}}))}async getLabelForSingleSelection(){let t=this.labelRepository[this.state];if(bt(t)&&(t=this.getSelectedOptionLabel(this.state)),bt(t)&&P(this.initialOptionLabel)&&this.state===this.initialState)t=this.initialOptionLabel,P(this.state)&&(this.labelRepository[this.state]=t);else if(bt(t)&&this.getOptionLabelUsing)try{t=await this.getOptionLabelUsing(),P(t)&&P(this.state)&&(this.labelRepository[this.state]=t)}catch(e){console.error("Error fetching option label:",e),t=this.state}else bt(t)&&(t=this.state);return t}addSingleSelectionDisplay(t,e=this.selectedDisplay){let i=document.createElement("span");if(i.className="fi-select-input-value-label",this.isHtmlAllowed?i.innerHTML=t:i.textContent=t,e.appendChild(i),!this.canSelectPlaceholder||this.container.querySelector(".fi-select-input-value-remove-btn"))return;let o=document.createElement("button");o.type="button",o.className="fi-select-input-value-remove-btn",o.innerHTML='',o.setAttribute("aria-label","Clear selection"),o.addEventListener("click",s=>{s.stopPropagation(),this.selectOption("")}),o.addEventListener("keydown",s=>{(s.key===" "||s.key==="Enter")&&(s.preventDefault(),s.stopPropagation(),this.selectOption(""))}),this.container.appendChild(o),this.container.classList.add("fi-select-input-ctn-clearable")}getSelectedOptionLabel(t){if(P(this.labelRepository[t]))return this.labelRepository[t];let e="";for(let i of this.options)if(i.options&&Array.isArray(i.options)){for(let o of i.options)if(o.value===t){e=o.label,this.labelRepository[t]=e;break}}else if(i.value===t){e=i.label,this.labelRepository[t]=e;break}return e}setUpEventListeners(){this.buttonClickListener=()=>{this.toggleDropdown()},this.documentClickListener=t=>{!this.container.contains(t.target)&&this.isOpen&&this.closeDropdown()},this.buttonKeydownListener=t=>{this.isDisabled||this.handleSelectButtonKeydown(t)},this.dropdownKeydownListener=t=>{this.isDisabled||this.isSearchable&&document.activeElement===this.searchInput&&!["Tab","Escape"].includes(t.key)||this.handleDropdownKeydown(t)},this.selectButton.addEventListener("click",this.buttonClickListener),document.addEventListener("click",this.documentClickListener),this.selectButton.addEventListener("keydown",this.buttonKeydownListener),this.dropdown.addEventListener("keydown",this.dropdownKeydownListener),!this.isMultiple&&this.livewireId&&this.statePath&&this.getOptionLabelUsing&&(this.refreshOptionLabelListener=async t=>{if(t.detail.livewireId===this.livewireId&&t.detail.statePath===this.statePath&&P(this.state))try{delete this.labelRepository[this.state];let e=await this.getOptionLabelUsing();P(e)&&(this.labelRepository[this.state]=e);let i=this.selectedDisplay.querySelector(".fi-select-input-value-label");P(i)&&(this.isHtmlAllowed?i.innerHTML=e:i.textContent=e),this.updateOptionLabelInList(this.state,e)}catch(e){console.error("Error refreshing option label:",e)}},window.addEventListener("filament-forms::select.refreshSelectedOptionLabel",this.refreshOptionLabelListener))}updateOptionLabelInList(t,e){this.labelRepository[t]=e;let i=this.getVisibleOptions();for(let o of i)if(o.getAttribute("data-value")===String(t)){if(o.innerHTML="",this.isHtmlAllowed){let s=document.createElement("span");s.innerHTML=e,o.appendChild(s)}else o.appendChild(document.createTextNode(e));break}for(let o of this.options)if(o.options&&Array.isArray(o.options)){for(let s of o.options)if(s.value===t){s.label=e;break}}else if(o.value===t){o.label=e;break}for(let o of this.originalOptions)if(o.options&&Array.isArray(o.options)){for(let s of o.options)if(s.value===t){s.label=e;break}}else if(o.value===t){o.label=e;break}}handleSelectButtonKeydown(t){switch(t.key){case"ArrowDown":t.preventDefault(),t.stopPropagation(),this.isOpen?this.focusNextOption():this.openDropdown();break;case"ArrowUp":t.preventDefault(),t.stopPropagation(),this.isOpen?this.focusPreviousOption():this.openDropdown();break;case" ":if(t.preventDefault(),this.isOpen){if(this.selectedIndex>=0){let e=this.getVisibleOptions()[this.selectedIndex];e&&e.click()}}else this.openDropdown();break;case"Enter":break;case"Escape":this.isOpen&&(t.preventDefault(),this.closeDropdown());break;case"Tab":this.isOpen&&this.closeDropdown();break;default:if(this.isSearchable&&!t.ctrlKey&&!t.metaKey&&!t.altKey&&typeof t.key=="string"&&t.key.length===1){t.preventDefault();let e=t.key;this.isOpen||this.openDropdown(),this.searchInput&&(this.searchInput.focus(),this.searchInput.value=(this.searchInput.value||"")+e,this.searchInput.dispatchEvent(new Event("input",{bubbles:!0})))}break}}handleDropdownKeydown(t){switch(t.key){case"ArrowDown":t.preventDefault(),t.stopPropagation(),this.focusNextOption();break;case"ArrowUp":t.preventDefault(),t.stopPropagation(),this.focusPreviousOption();break;case" ":if(t.preventDefault(),this.selectedIndex>=0){let e=this.getVisibleOptions()[this.selectedIndex];e&&e.click()}break;case"Enter":if(t.preventDefault(),this.selectedIndex>=0){let e=this.getVisibleOptions()[this.selectedIndex];e&&e.click()}else{let e=this.element.closest("form");e&&e.submit()}break;case"Escape":t.preventDefault(),this.closeDropdown(),this.selectButton.focus();break;case"Tab":this.closeDropdown();break;default:if(this.isSearchable&&!t.ctrlKey&&!t.metaKey&&!t.altKey&&typeof t.key=="string"&&t.key.length===1){t.preventDefault();let e=t.key;this.searchInput&&(this.searchInput.focus(),this.searchInput.value=(this.searchInput.value||"")+e,this.searchInput.dispatchEvent(new Event("input",{bubbles:!0})))}break}}toggleDropdown(){if(!this.isDisabled){if(this.isOpen){this.closeDropdown();return}this.isMultiple&&!this.isSearchable&&!this.hasAvailableOptions()||this.openDropdown()}}hasAvailableOptions(){for(let t of this.options)if(t.options&&Array.isArray(t.options)){for(let e of t.options)if(!Array.isArray(this.state)||!this.state.includes(e.value))return!0}else if(!Array.isArray(this.state)||!this.state.includes(t.value))return!0;return!1}async openDropdown(){this.dropdown.style.display="block",this.dropdown.style.opacity="0";let t=this.selectButton.closest(".fi-fixed-positioning-context")!==null&&this.selectButton.closest(".fi-absolute-positioning-context")===null;if(this.dropdown.style.position=t?"fixed":"absolute",this.dropdown.style.width=`${this.selectButton.offsetWidth}px`,this.selectButton.setAttribute("aria-expanded","true"),this.isOpen=!0,this.positionDropdown(),this.resizeListener||(this.resizeListener=()=>{this.dropdown.style.width=`${this.selectButton.offsetWidth}px`,this.positionDropdown()},window.addEventListener("resize",this.resizeListener)),this.scrollListener||(this.scrollListener=()=>this.positionDropdown(),window.addEventListener("scroll",this.scrollListener,!0)),this.dropdown.style.opacity="1",this.isSearchable&&this.searchInput&&(this.searchInput.value="",this.searchQuery="",this.hasDynamicOptions||(this.options=JSON.parse(JSON.stringify(this.originalOptions)),this.renderOptions())),this.hasDynamicOptions&&this.getOptionsUsing){this.showLoadingState(!1);try{let e=await this.getOptionsUsing(),i=Array.isArray(e)?e:e&&Array.isArray(e.options)?e.options:[];if(this.options=i,this.originalOptions=JSON.parse(JSON.stringify(i)),this.populateLabelRepositoryFromOptions(i),this.isSearchable&&this.searchInput&&(this.searchInput.value&&this.searchInput.value.trim()!==""||this.searchQuery&&this.searchQuery.trim()!=="")){let o=(this.searchInput.value||this.searchQuery||"").trim().toLowerCase();this.hideLoadingState(),this.filterOptions(o)}else this.renderOptions()}catch(e){console.error("Error fetching options:",e),this.hideLoadingState()}}else(!this.hasInitialNoOptionsMessage||this.searchQuery)&&this.hideLoadingState();if(this.isSearchable&&this.searchInput)this.searchInput.focus();else{this.selectedIndex=-1;let e=this.getVisibleOptions();if(this.isMultiple){if(Array.isArray(this.state)&&this.state.length>0){for(let i=0;i0&&(this.selectedIndex=0),this.selectedIndex>=0&&(e[this.selectedIndex].classList.add("fi-selected"),e[this.selectedIndex].focus())}}positionDropdown(){let t=this.position==="top"?"top-start":"bottom-start",e=[yi(4),Si({padding:5})];this.position!=="top"&&this.position!=="bottom"&&e.push(xi());let i=this.selectButton.closest(".fi-fixed-positioning-context")!==null&&this.selectButton.closest(".fi-absolute-positioning-context")===null;Oi(this.selectButton,this.dropdown,{placement:t,middleware:e,strategy:i?"fixed":"absolute"}).then(({x:o,y:s})=>{Object.assign(this.dropdown.style,{left:`${o}px`,top:`${s}px`})})}deferPositionDropdown(){this.isOpen&&(this.positioningRequestAnimationFrame&&(cancelAnimationFrame(this.positioningRequestAnimationFrame),this.positioningRequestAnimationFrame=null),this.positioningRequestAnimationFrame=requestAnimationFrame(()=>{this.positionDropdown(),this.positioningRequestAnimationFrame=null}))}closeDropdown(){this.dropdown.style.display="none",this.selectButton.setAttribute("aria-expanded","false"),this.isOpen=!1,this.searchTimeout&&(clearTimeout(this.searchTimeout),this.searchTimeout=null),this.activeSearchId++,this.isSearching=!1,this.hideLoadingState(),this.resizeListener&&(window.removeEventListener("resize",this.resizeListener),this.resizeListener=null),this.scrollListener&&(window.removeEventListener("scroll",this.scrollListener,!0),this.scrollListener=null),this.getVisibleOptions().forEach(e=>{e.classList.remove("fi-selected")}),this.dropdown.removeAttribute("aria-activedescendant")}focusNextOption(){let t=this.getVisibleOptions();if(t.length!==0){if(this.selectedIndex>=0&&this.selectedIndex=0&&this.selectedIndexe.bottom?this.dropdown.scrollTop+=i.bottom-e.bottom:i.top li[role="option"]')):t=Array.from(this.optionsList.querySelectorAll(':scope > ul.fi-dropdown-list > li[role="option"]'));let e=Array.from(this.optionsList.querySelectorAll('li.fi-select-input-option-group > ul > li[role="option"]'));return[...t,...e]}getSelectedOptionLabels(){if(!Array.isArray(this.state)||this.state.length===0)return{};let t={};for(let e of this.state){let i=!1;for(let o of this.options)if(o.options&&Array.isArray(o.options)){for(let s of o.options)if(s.value===e){t[e]=s.label,i=!0;break}if(i)break}else if(o.value===e){t[e]=o.label,i=!0;break}}return t}handleSearch(t){let e=t.target.value.trim();if(this.searchQuery=e,this.searchTimeout&&clearTimeout(this.searchTimeout),e===""){this.options=JSON.parse(JSON.stringify(this.originalOptions)),this.renderOptions();return}if(!this.getSearchResultsUsing||typeof this.getSearchResultsUsing!="function"||!this.hasDynamicSearchResults){this.filterOptions(e);return}this.searchTimeout=setTimeout(async()=>{this.searchTimeout=null;let i=++this.activeSearchId;this.isSearching=!0;try{this.showLoadingState(!0);let o=await this.getSearchResultsUsing(e);if(i!==this.activeSearchId||!this.isOpen)return;let s=Array.isArray(o)?o:o&&Array.isArray(o.options)?o.options:[];this.options=s,this.populateLabelRepositoryFromOptions(s),this.hideLoadingState(),this.renderOptions(),this.isOpen&&this.deferPositionDropdown(),this.options.length===0&&this.showNoResultsMessage()}catch(o){i===this.activeSearchId&&(console.error("Error fetching search results:",o),this.hideLoadingState(),this.options=JSON.parse(JSON.stringify(this.originalOptions)),this.renderOptions())}finally{i===this.activeSearchId&&(this.isSearching=!1)}},this.searchDebounce)}showLoadingState(t=!1){this.optionsList.parentNode===this.dropdown&&this.dropdown.removeChild(this.optionsList),this.hideLoadingState();let e=document.createElement("div");e.className="fi-select-input-message",e.textContent=t?this.searchingMessage:this.loadingMessage,this.dropdown.appendChild(e),this.isOpen&&this.deferPositionDropdown()}hideLoadingState(){let t=this.dropdown.querySelector(".fi-select-input-message");t&&t.remove()}showNoOptionsMessage(){this.optionsList.parentNode===this.dropdown&&this.dropdown.removeChild(this.optionsList),this.hideLoadingState();let t=document.createElement("div");t.className="fi-select-input-message",t.textContent=this.noOptionsMessage,this.dropdown.appendChild(t),this.isOpen&&this.deferPositionDropdown()}showNoResultsMessage(){this.optionsList.parentNode===this.dropdown&&this.dropdown.removeChild(this.optionsList),this.hideLoadingState();let t=document.createElement("div");t.className="fi-select-input-message",t.textContent=this.noSearchResultsMessage,this.dropdown.appendChild(t),this.isOpen&&this.deferPositionDropdown()}filterOptions(t){let e=this.searchableOptionFields.includes("label"),i=this.searchableOptionFields.includes("value");t=t.toLowerCase();let o=[];for(let s of this.originalOptions)if(s.options&&Array.isArray(s.options)){let r=s.options.filter(a=>e&&a.label.toLowerCase().includes(t)||i&&String(a.value).toLowerCase().includes(t));r.length>0&&o.push({label:s.label,options:r})}else(e&&s.label.toLowerCase().includes(t)||i&&String(s.value).toLowerCase().includes(t))&&o.push(s);this.options=o,this.renderOptions(),this.options.length===0&&this.showNoResultsMessage(),this.isOpen&&this.positionDropdown()}selectOption(t){if(this.isDisabled)return;if(!this.isMultiple){this.state=t,this.updateSelectedDisplay(),this.renderOptions(),this.closeDropdown(),this.selectButton.focus(),this.onStateChange(this.state);return}let e=Array.isArray(this.state)?[...this.state]:[];if(e.includes(t)){let o=this.selectedDisplay.querySelector(`[data-value="${t}"]`);if(P(o)){let s=o.parentElement;P(s)&&s.children.length===1?(e=e.filter(r=>r!==t),this.state=e,this.updateSelectedDisplay()):(o.remove(),e=e.filter(r=>r!==t),this.state=e)}else e=e.filter(s=>s!==t),this.state=e,this.updateSelectedDisplay();this.renderOptions(),this.isOpen&&this.deferPositionDropdown(),this.maintainFocusInMultipleMode(),this.onStateChange(this.state);return}if(this.maxItems&&e.length>=this.maxItems){this.maxItemsMessage&&alert(this.maxItemsMessage);return}e.push(t),this.state=e;let i=this.selectedDisplay.querySelector(".fi-select-input-value-badges-ctn");bt(i)?this.updateSelectedDisplay():this.addSingleBadge(t,i),this.renderOptions(),this.isOpen&&this.deferPositionDropdown(),this.maintainFocusInMultipleMode(),this.onStateChange(this.state)}async addSingleBadge(t,e){let i=this.labelRepository[t];if(bt(i)&&(i=this.getSelectedOptionLabel(t),P(i)&&(this.labelRepository[t]=i)),bt(i)&&this.getOptionLabelsUsing)try{let s=await this.getOptionLabelsUsing();for(let r of s)if(P(r)&&r.value===t&&r.label!==void 0){i=r.label,this.labelRepository[t]=i;break}}catch(s){console.error("Error fetching option label:",s)}bt(i)&&(i=t);let o=this.createBadgeElement(t,i);e.appendChild(o)}maintainFocusInMultipleMode(){if(this.isSearchable&&this.searchInput){this.searchInput.focus();return}let t=this.getVisibleOptions();if(t.length!==0){if(this.selectedIndex=-1,Array.isArray(this.state)&&this.state.length>0){for(let e=0;e{e.setAttribute("disabled","disabled"),e.classList.add("fi-disabled")}),!this.isMultiple&&this.canSelectPlaceholder){let t=this.container.querySelector(".fi-select-input-value-remove-btn");t&&(t.setAttribute("disabled","disabled"),t.classList.add("fi-disabled"))}this.isSearchable&&this.searchInput&&(this.searchInput.setAttribute("disabled","disabled"),this.searchInput.classList.add("fi-disabled"))}else{if(this.selectButton.removeAttribute("disabled"),this.selectButton.removeAttribute("aria-disabled"),this.selectButton.classList.remove("fi-disabled"),this.isMultiple&&this.container.querySelectorAll(".fi-select-input-badge-remove").forEach(e=>{e.removeAttribute("disabled"),e.classList.remove("fi-disabled")}),!this.isMultiple&&this.canSelectPlaceholder){let t=this.container.querySelector(".fi-select-input-value-remove-btn");t&&(t.removeAttribute("disabled"),t.classList.add("fi-disabled"))}this.isSearchable&&this.searchInput&&(this.searchInput.removeAttribute("disabled"),this.searchInput.classList.remove("fi-disabled"))}}destroy(){this.selectButton&&this.buttonClickListener&&this.selectButton.removeEventListener("click",this.buttonClickListener),this.documentClickListener&&document.removeEventListener("click",this.documentClickListener),this.selectButton&&this.buttonKeydownListener&&this.selectButton.removeEventListener("keydown",this.buttonKeydownListener),this.dropdown&&this.dropdownKeydownListener&&this.dropdown.removeEventListener("keydown",this.dropdownKeydownListener),this.resizeListener&&(window.removeEventListener("resize",this.resizeListener),this.resizeListener=null),this.scrollListener&&(window.removeEventListener("scroll",this.scrollListener,!0),this.scrollListener=null),this.refreshOptionLabelListener&&window.removeEventListener("filament-forms::select.refreshSelectedOptionLabel",this.refreshOptionLabelListener),this.isOpen&&this.closeDropdown(),this.searchTimeout&&(clearTimeout(this.searchTimeout),this.searchTimeout=null),this.container&&this.container.remove()}};function Qn({canOptionLabelsWrap:n,canSelectPlaceholder:t,getOptionLabelUsing:e,getOptionsUsing:i,getSearchResultsUsing:o,hasDynamicOptions:s,hasDynamicSearchResults:r,hasInitialNoOptionsMessage:a,initialOptionLabel:l,isDisabled:c,isHtmlAllowed:f,isNative:d,isSearchable:p,loadingMessage:u,name:g,noOptionsMessage:m,noSearchResultsMessage:S,options:E,optionsLimit:y,placeholder:D,position:A,recordKey:C,searchableOptionFields:F,searchDebounce:q,searchingMessage:J,searchPrompt:_,state:W}){return{error:void 0,isLoading:!1,select:null,state:W,unsubscribeLivewireHook:null,init(){d||(this.select=new Ee({canOptionLabelsWrap:n,canSelectPlaceholder:t,element:this.$refs.select,getOptionLabelUsing:e,getOptionsUsing:i,getSearchResultsUsing:o,hasDynamicOptions:s,hasDynamicSearchResults:r,hasInitialNoOptionsMessage:a,initialOptionLabel:l,isDisabled:c,isHtmlAllowed:f,isSearchable:p,loadingMessage:u,noOptionsMessage:m,noSearchResultsMessage:S,onStateChange:L=>{this.state=L},options:E,optionsLimit:y,placeholder:D,position:A,searchableOptionFields:F,searchDebounce:q,searchingMessage:J,searchPrompt:_,state:this.state})),this.unsubscribeLivewireHook=Livewire.interceptMessage(({message:L,onSuccess:X})=>{X(()=>{this.$nextTick(()=>{if(this.isLoading||L.component.id!==this.$root.closest("[wire\\:id]")?.attributes["wire:id"].value)return;let M=this.getServerState();M===void 0||this.getNormalizedState()===M||(this.state=M)})})}),this.$watch("state",async L=>{!d&&this.select&&this.select.state!==L&&(this.select.state=L,this.select.updateSelectedDisplay(),this.select.renderOptions());let X=this.getServerState();if(X===void 0||this.getNormalizedState()===X)return;this.isLoading=!0;let M=await this.$wire.updateTableColumnState(g,C,this.state);this.error=M?.error??void 0,!this.error&&this.$refs.serverState&&(this.$refs.serverState.value=this.getNormalizedState()),this.isLoading=!1})},getServerState(){if(this.$refs.serverState)return[null,void 0].includes(this.$refs.serverState.value)?"":this.$refs.serverState.value.replaceAll('\\"','"')},getNormalizedState(){let L=Alpine.raw(this.state);return[null,void 0].includes(L)?"":L},destroy(){this.unsubscribeLivewireHook?.(),this.select&&(this.select.destroy(),this.select=null)}}}export{Qn as default}; +/*! Bundled license information: + +sortablejs/modular/sortable.esm.js: + (**! + * Sortable 1.15.6 + * @author RubaXa + * @author owenm + * @license MIT + *) +*/ diff --git a/public/js/filament/tables/components/columns/text-input.js b/public/js/filament/tables/components/columns/text-input.js new file mode 100644 index 00000000..9f5b1acd --- /dev/null +++ b/public/js/filament/tables/components/columns/text-input.js @@ -0,0 +1 @@ +function a({name:i,recordKey:s,state:n}){return{error:void 0,isLoading:!1,state:n,unsubscribeLivewireHook:null,init(){this.unsubscribeLivewireHook=Livewire.interceptMessage(({message:e,onSuccess:t})=>{t(()=>{this.$nextTick(()=>{if(this.isLoading||e.component.id!==this.$root.closest("[wire\\:id]")?.attributes["wire:id"].value)return;let r=this.getServerState();r===void 0||this.getNormalizedState()===r||(this.state=r)})})}),this.$watch("state",async()=>{let e=this.getServerState();if(e===void 0||this.getNormalizedState()===e)return;this.isLoading=!0;let t=await this.$wire.updateTableColumnState(i,s,this.state);this.error=t?.error??void 0,!this.error&&this.$refs.serverState&&(this.$refs.serverState.value=this.getNormalizedState()),this.isLoading=!1})},getServerState(){if(this.$refs.serverState)return[null,void 0].includes(this.$refs.serverState.value)?"":this.$refs.serverState.value.replaceAll('\\"','"')},getNormalizedState(){let e=Alpine.raw(this.state);return[null,void 0].includes(e)?"":e},destroy(){this.unsubscribeLivewireHook?.()}}}export{a as default}; diff --git a/public/js/filament/tables/components/columns/toggle.js b/public/js/filament/tables/components/columns/toggle.js new file mode 100644 index 00000000..b26a4260 --- /dev/null +++ b/public/js/filament/tables/components/columns/toggle.js @@ -0,0 +1 @@ +function a({name:r,recordKey:s,state:n}){return{error:void 0,isLoading:!1,state:n,unsubscribeLivewireHook:null,init(){this.unsubscribeLivewireHook=Livewire.interceptMessage(({message:e,onSuccess:t})=>{t(()=>{this.$nextTick(()=>{if(this.isLoading||e.component.id!==this.$root.closest("[wire\\:id]")?.attributes["wire:id"].value)return;let i=this.getServerState();i===void 0||Alpine.raw(this.state)===i||(this.state=i)})})}),this.$watch("state",async()=>{let e=this.getServerState();if(e===void 0||Alpine.raw(this.state)===e)return;this.isLoading=!0;let t=await this.$wire.updateTableColumnState(r,s,this.state);this.error=t?.error??void 0,!this.error&&this.$refs.serverState&&(this.$refs.serverState.value=this.state?"1":"0"),this.isLoading=!1})},getServerState(){if(this.$refs.serverState)return[1,"1"].includes(this.$refs.serverState.value)},destroy(){this.unsubscribeLivewireHook?.()}}}export{a as default}; diff --git a/public/js/filament/tables/components/table.js b/public/js/filament/tables/components/table.js new file mode 100644 index 00000000..4e3ce3a9 --- /dev/null +++ b/public/js/filament/tables/components/table.js @@ -0,0 +1 @@ +function d(){return{checkboxClickController:null,collapsedGroups:[],isLoading:!1,selectedRecords:[],shouldCheckUniqueSelection:!0,lastCheckedRecord:null,livewireId:null,init:function(){this.livewireId=this.$root.closest("[wire\\:id]").attributes["wire:id"].value,this.$wire.$on("deselectAllTableRecords",()=>this.deselectAllRecords()),this.$watch("selectedRecords",()=>{if(!this.shouldCheckUniqueSelection){this.shouldCheckUniqueSelection=!0;return}this.selectedRecords=[...new Set(this.selectedRecords)],this.shouldCheckUniqueSelection=!1}),this.$nextTick(()=>this.watchForCheckboxClicks()),Livewire.hook("element.init",({component:e})=>{e.id===this.livewireId&&this.watchForCheckboxClicks()})},mountAction:function(e,t=null){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableAction(e,t)},mountBulkAction:function(e){this.$wire.set("selectedTableRecords",this.selectedRecords,!1),this.$wire.mountTableBulkAction(e)},toggleSelectRecordsOnPage:function(){let e=this.getRecordsOnPage();if(this.areRecordsSelected(e)){this.deselectRecords(e);return}this.selectRecords(e)},toggleSelectRecordsInGroup:async function(e){this.isLoading=!0;let t=await this.$wire.getGroupedSelectableTableRecordKeys(e);this.areRecordsSelected(this.getRecordsInGroupOnPage(e))?this.deselectRecords(t):this.selectRecords(t),this.isLoading=!1},getRecordsInGroupOnPage:function(e){let t=[];for(let s of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])s.dataset.group===e&&t.push(s.value);return t},getRecordsOnPage:function(){let e=[];for(let t of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])e.push(t.value);return e},selectRecords:function(e){for(let t of e)this.isRecordSelected(t)||this.selectedRecords.push(t)},deselectRecords:function(e){for(let t of e){let s=this.selectedRecords.indexOf(t);s!==-1&&this.selectedRecords.splice(s,1)}},selectAllRecords:async function(){this.isLoading=!0,this.selectedRecords=await this.$wire.getAllSelectableTableRecordKeys(),this.isLoading=!1},deselectAllRecords:function(){this.selectedRecords=[]},isRecordSelected:function(e){return this.selectedRecords.includes(e)},areRecordsSelected:function(e){return e.every(t=>this.isRecordSelected(t))},toggleCollapseGroup:function(e){if(this.isGroupCollapsed(e)){this.collapsedGroups.splice(this.collapsedGroups.indexOf(e),1);return}this.collapsedGroups.push(e)},isGroupCollapsed:function(e){return this.collapsedGroups.includes(e)},resetCollapsedGroups:function(){this.collapsedGroups=[]},watchForCheckboxClicks:function(){this.checkboxClickController&&this.checkboxClickController.abort(),this.checkboxClickController=new AbortController;let{signal:e}=this.checkboxClickController;this.$root?.addEventListener("click",t=>t.target?.matches(".fi-ta-record-checkbox")&&this.handleCheckboxClick(t,t.target),{signal:e})},handleCheckboxClick:function(e,t){if(!this.lastChecked){this.lastChecked=t;return}if(e.shiftKey){let s=Array.from(this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[]);if(!s.includes(this.lastChecked)){this.lastChecked=t;return}let l=s.indexOf(this.lastChecked),r=s.indexOf(t),o=[l,r].sort((c,n)=>c-n),i=[];for(let c=o[0];c<=o[1];c++)s[c].checked=t.checked,i.push(s[c].value);t.checked?this.selectRecords(i):this.deselectRecords(i)}this.lastChecked=t}}}export{d as default}; diff --git a/public/js/filament/tables/tables.js b/public/js/filament/tables/tables.js new file mode 100644 index 00000000..ed077fa7 --- /dev/null +++ b/public/js/filament/tables/tables.js @@ -0,0 +1 @@ +(()=>{var M=Math.min,L=Math.max,B=Math.round,H=Math.floor,S=e=>({x:e,y:e});function q(e,t,i){return L(e,M(t,i))}function j(e,t){return typeof e=="function"?e(t):e}function W(e){return e.split("-")[0]}function Q(e){return e.split("-")[1]}function Z(e){return e==="x"?"y":"x"}function oe(e){return e==="y"?"height":"width"}var Pe=new Set(["top","bottom"]);function z(e){return Pe.has(W(e))?"y":"x"}function se(e){return Z(z(e))}function De(e){return{top:0,right:0,bottom:0,left:0,...e}}function re(e){return typeof e!="number"?De(e):{top:e,right:e,bottom:e,left:e}}function E(e){let{x:t,y:i,width:n,height:s}=e;return{width:n,height:s,top:i,left:t,right:t+n,bottom:i+s,x:t,y:i}}function le(e,t,i){let{reference:n,floating:s}=e,l=z(t),o=se(t),r=oe(o),c=W(t),a=l==="y",f=n.x+n.width/2-s.width/2,d=n.y+n.height/2-s.height/2,h=n[r]/2-s[r]/2,u;switch(c){case"top":u={x:f,y:n.y-s.height};break;case"bottom":u={x:f,y:n.y+n.height};break;case"right":u={x:n.x+n.width,y:d};break;case"left":u={x:n.x-s.width,y:d};break;default:u={x:n.x,y:n.y}}switch(Q(t)){case"start":u[o]-=h*(i&&a?-1:1);break;case"end":u[o]+=h*(i&&a?-1:1);break}return u}var ce=async(e,t,i)=>{let{placement:n="bottom",strategy:s="absolute",middleware:l=[],platform:o}=i,r=l.filter(Boolean),c=await(o.isRTL==null?void 0:o.isRTL(t)),a=await o.getElementRects({reference:e,floating:t,strategy:s}),{x:f,y:d}=le(a,n,c),h=n,u={},m=0;for(let p=0;p{let{x:g,y:x}=w;return{x:g,y:x}}},...c}=j(e,t),a={x:i,y:n},f=await ae(t,c),d=z(W(s)),h=Z(d),u=a[h],m=a[d];if(l){let w=h==="y"?"top":"left",g=h==="y"?"bottom":"right",x=u+f[w],y=u-f[g];u=q(x,u,y)}if(o){let w=d==="y"?"top":"left",g=d==="y"?"bottom":"right",x=m+f[w],y=m-f[g];m=q(x,m,y)}let p=r.fn({...t,[h]:u,[d]:m});return{...p,data:{x:p.x-i,y:p.y-n,enabled:{[h]:l,[d]:o}}}}}};function X(){return typeof window<"u"}function P(e){return he(e)?(e.nodeName||"").toLowerCase():"#document"}function v(e){var t;return(e==null||(t=e.ownerDocument)==null?void 0:t.defaultView)||window}function O(e){var t;return(t=(he(e)?e.ownerDocument:e.document)||window.document)==null?void 0:t.documentElement}function he(e){return X()?e instanceof Node||e instanceof v(e).Node:!1}function R(e){return X()?e instanceof Element||e instanceof v(e).Element:!1}function T(e){return X()?e instanceof HTMLElement||e instanceof v(e).HTMLElement:!1}function ue(e){return!X()||typeof ShadowRoot>"u"?!1:e instanceof ShadowRoot||e instanceof v(e).ShadowRoot}var $e=new Set(["inline","contents"]);function N(e){let{overflow:t,overflowX:i,overflowY:n,display:s}=C(e);return/auto|scroll|overlay|hidden|clip/.test(t+n+i)&&!$e.has(s)}var Ne=new Set(["table","td","th"]);function me(e){return Ne.has(P(e))}var Ve=[":popover-open",":modal"];function _(e){return Ve.some(t=>{try{return e.matches(t)}catch{return!1}})}var Be=["transform","translate","scale","rotate","perspective"],He=["transform","translate","scale","rotate","perspective","filter"],We=["paint","layout","strict","content"];function Y(e){let t=G(),i=R(e)?C(e):e;return Be.some(n=>i[n]?i[n]!=="none":!1)||(i.containerType?i.containerType!=="normal":!1)||!t&&(i.backdropFilter?i.backdropFilter!=="none":!1)||!t&&(i.filter?i.filter!=="none":!1)||He.some(n=>(i.willChange||"").includes(n))||We.some(n=>(i.contain||"").includes(n))}function ge(e){let t=k(e);for(;T(t)&&!D(t);){if(Y(t))return t;if(_(t))return null;t=k(t)}return null}function G(){return typeof CSS>"u"||!CSS.supports?!1:CSS.supports("-webkit-backdrop-filter","none")}var ze=new Set(["html","body","#document"]);function D(e){return ze.has(P(e))}function C(e){return v(e).getComputedStyle(e)}function I(e){return R(e)?{scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}:{scrollLeft:e.scrollX,scrollTop:e.scrollY}}function k(e){if(P(e)==="html")return e;let t=e.assignedSlot||e.parentNode||ue(e)&&e.host||O(e);return ue(t)?t.host:t}function pe(e){let t=k(e);return D(t)?e.ownerDocument?e.ownerDocument.body:e.body:T(t)&&N(t)?t:pe(t)}function $(e,t,i){var n;t===void 0&&(t=[]),i===void 0&&(i=!0);let s=pe(e),l=s===((n=e.ownerDocument)==null?void 0:n.body),o=v(s);if(l){let r=J(o);return t.concat(o,o.visualViewport||[],N(s)?s:[],r&&i?$(r):[])}return t.concat(s,$(s,[],i))}function J(e){return e.parent&&Object.getPrototypeOf(e.parent)?e.frameElement:null}function be(e){let t=C(e),i=parseFloat(t.width)||0,n=parseFloat(t.height)||0,s=T(e),l=s?e.offsetWidth:i,o=s?e.offsetHeight:n,r=B(i)!==l||B(n)!==o;return r&&(i=l,n=o),{width:i,height:n,$:r}}function te(e){return R(e)?e:e.contextElement}function V(e){let t=te(e);if(!T(t))return S(1);let i=t.getBoundingClientRect(),{width:n,height:s,$:l}=be(t),o=(l?B(i.width):i.width)/n,r=(l?B(i.height):i.height)/s;return(!o||!Number.isFinite(o))&&(o=1),(!r||!Number.isFinite(r))&&(r=1),{x:o,y:r}}var _e=S(0);function ve(e){let t=v(e);return!G()||!t.visualViewport?_e:{x:t.visualViewport.offsetLeft,y:t.visualViewport.offsetTop}}function Ie(e,t,i){return t===void 0&&(t=!1),!i||t&&i!==v(e)?!1:t}function F(e,t,i,n){t===void 0&&(t=!1),i===void 0&&(i=!1);let s=e.getBoundingClientRect(),l=te(e),o=S(1);t&&(n?R(n)&&(o=V(n)):o=V(e));let r=Ie(l,i,n)?ve(l):S(0),c=(s.left+r.x)/o.x,a=(s.top+r.y)/o.y,f=s.width/o.x,d=s.height/o.y;if(l){let h=v(l),u=n&&R(n)?v(n):n,m=h,p=J(m);for(;p&&n&&u!==m;){let w=V(p),g=p.getBoundingClientRect(),x=C(p),y=g.left+(p.clientLeft+parseFloat(x.paddingLeft))*w.x,A=g.top+(p.clientTop+parseFloat(x.paddingTop))*w.y;c*=w.x,a*=w.y,f*=w.x,d*=w.y,c+=y,a+=A,m=v(p),p=J(m)}}return E({width:f,height:d,x:c,y:a})}function K(e,t){let i=I(e).scrollLeft;return t?t.left+i:F(O(e)).left+i}function Re(e,t){let i=e.getBoundingClientRect(),n=i.left+t.scrollLeft-K(e,i),s=i.top+t.scrollTop;return{x:n,y:s}}function Ue(e){let{elements:t,rect:i,offsetParent:n,strategy:s}=e,l=s==="fixed",o=O(n),r=t?_(t.floating):!1;if(n===o||r&&l)return i;let c={scrollLeft:0,scrollTop:0},a=S(1),f=S(0),d=T(n);if((d||!d&&!l)&&((P(n)!=="body"||N(o))&&(c=I(n)),T(n))){let u=F(n);a=V(n),f.x=u.x+n.clientLeft,f.y=u.y+n.clientTop}let h=o&&!d&&!l?Re(o,c):S(0);return{width:i.width*a.x,height:i.height*a.y,x:i.x*a.x-c.scrollLeft*a.x+f.x+h.x,y:i.y*a.y-c.scrollTop*a.y+f.y+h.y}}function je(e){return Array.from(e.getClientRects())}function Xe(e){let t=O(e),i=I(e),n=e.ownerDocument.body,s=L(t.scrollWidth,t.clientWidth,n.scrollWidth,n.clientWidth),l=L(t.scrollHeight,t.clientHeight,n.scrollHeight,n.clientHeight),o=-i.scrollLeft+K(e),r=-i.scrollTop;return C(n).direction==="rtl"&&(o+=L(t.clientWidth,n.clientWidth)-s),{width:s,height:l,x:o,y:r}}var we=25;function Ye(e,t){let i=v(e),n=O(e),s=i.visualViewport,l=n.clientWidth,o=n.clientHeight,r=0,c=0;if(s){l=s.width,o=s.height;let f=G();(!f||f&&t==="fixed")&&(r=s.offsetLeft,c=s.offsetTop)}let a=K(n);if(a<=0){let f=n.ownerDocument,d=f.body,h=getComputedStyle(d),u=f.compatMode==="CSS1Compat"&&parseFloat(h.marginLeft)+parseFloat(h.marginRight)||0,m=Math.abs(n.clientWidth-d.clientWidth-u);m<=we&&(l-=m)}else a<=we&&(l+=a);return{width:l,height:o,x:r,y:c}}var Ge=new Set(["absolute","fixed"]);function Je(e,t){let i=F(e,!0,t==="fixed"),n=i.top+e.clientTop,s=i.left+e.clientLeft,l=T(e)?V(e):S(1),o=e.clientWidth*l.x,r=e.clientHeight*l.y,c=s*l.x,a=n*l.y;return{width:o,height:r,x:c,y:a}}function xe(e,t,i){let n;if(t==="viewport")n=Ye(e,i);else if(t==="document")n=Xe(O(e));else if(R(t))n=Je(t,i);else{let s=ve(e);n={x:t.x-s.x,y:t.y-s.y,width:t.width,height:t.height}}return E(n)}function Ce(e,t){let i=k(e);return i===t||!R(i)||D(i)?!1:C(i).position==="fixed"||Ce(i,t)}function Ke(e,t){let i=t.get(e);if(i)return i;let n=$(e,[],!1).filter(r=>R(r)&&P(r)!=="body"),s=null,l=C(e).position==="fixed",o=l?k(e):e;for(;R(o)&&!D(o);){let r=C(o),c=Y(o);!c&&r.position==="fixed"&&(s=null),(l?!c&&!s:!c&&r.position==="static"&&!!s&&Ge.has(s.position)||N(o)&&!c&&Ce(e,o))?n=n.filter(f=>f!==o):s=r,o=k(o)}return t.set(e,n),n}function qe(e){let{element:t,boundary:i,rootBoundary:n,strategy:s}=e,o=[...i==="clippingAncestors"?_(t)?[]:Ke(t,this._c):[].concat(i),n],r=o[0],c=o.reduce((a,f)=>{let d=xe(t,f,s);return a.top=L(d.top,a.top),a.right=M(d.right,a.right),a.bottom=M(d.bottom,a.bottom),a.left=L(d.left,a.left),a},xe(t,r,s));return{width:c.right-c.left,height:c.bottom-c.top,x:c.left,y:c.top}}function Qe(e){let{width:t,height:i}=be(e);return{width:t,height:i}}function Ze(e,t,i){let n=T(t),s=O(t),l=i==="fixed",o=F(e,!0,l,t),r={scrollLeft:0,scrollTop:0},c=S(0);function a(){c.x=K(s)}if(n||!n&&!l)if((P(t)!=="body"||N(s))&&(r=I(t)),n){let u=F(t,!0,l,t);c.x=u.x+t.clientLeft,c.y=u.y+t.clientTop}else s&&a();l&&!n&&s&&a();let f=s&&!n&&!l?Re(s,r):S(0),d=o.left+r.scrollLeft-c.x-f.x,h=o.top+r.scrollTop-c.y-f.y;return{x:d,y:h,width:o.width,height:o.height}}function ee(e){return C(e).position==="static"}function ye(e,t){if(!T(e)||C(e).position==="fixed")return null;if(t)return t(e);let i=e.offsetParent;return O(e)===i&&(i=i.ownerDocument.body),i}function Ae(e,t){let i=v(e);if(_(e))return i;if(!T(e)){let s=k(e);for(;s&&!D(s);){if(R(s)&&!ee(s))return s;s=k(s)}return i}let n=ye(e,t);for(;n&&me(n)&&ee(n);)n=ye(n,t);return n&&D(n)&&ee(n)&&!Y(n)?i:n||ge(e)||i}var et=async function(e){let t=this.getOffsetParent||Ae,i=this.getDimensions,n=await i(e.floating);return{reference:Ze(e.reference,await t(e.floating),e.strategy),floating:{x:0,y:0,width:n.width,height:n.height}}};function tt(e){return C(e).direction==="rtl"}var nt={convertOffsetParentRelativeRectToViewportRelativeRect:Ue,getDocumentElement:O,getClippingRect:qe,getOffsetParent:Ae,getElementRects:et,getClientRects:je,getDimensions:Qe,getScale:V,isElement:R,isRTL:tt};function Se(e,t){return e.x===t.x&&e.y===t.y&&e.width===t.width&&e.height===t.height}function it(e,t){let i=null,n,s=O(e);function l(){var r;clearTimeout(n),(r=i)==null||r.disconnect(),i=null}function o(r,c){r===void 0&&(r=!1),c===void 0&&(c=1),l();let a=e.getBoundingClientRect(),{left:f,top:d,width:h,height:u}=a;if(r||t(),!h||!u)return;let m=H(d),p=H(s.clientWidth-(f+h)),w=H(s.clientHeight-(d+u)),g=H(f),y={rootMargin:-m+"px "+-p+"px "+-w+"px "+-g+"px",threshold:L(0,M(1,c))||1},A=!0;function b(ie){let U=ie[0].intersectionRatio;if(U!==c){if(!A)return o();U?o(!1,U):n=setTimeout(()=>{o(!1,1e-7)},1e3)}U===1&&!Se(a,e.getBoundingClientRect())&&o(),A=!1}try{i=new IntersectionObserver(b,{...y,root:s.ownerDocument})}catch{i=new IntersectionObserver(b,y)}i.observe(e)}return o(!0),l}function Oe(e,t,i,n){n===void 0&&(n={});let{ancestorScroll:s=!0,ancestorResize:l=!0,elementResize:o=typeof ResizeObserver=="function",layoutShift:r=typeof IntersectionObserver=="function",animationFrame:c=!1}=n,a=te(e),f=s||l?[...a?$(a):[],...$(t)]:[];f.forEach(g=>{s&&g.addEventListener("scroll",i,{passive:!0}),l&&g.addEventListener("resize",i)});let d=a&&r?it(a,i):null,h=-1,u=null;o&&(u=new ResizeObserver(g=>{let[x]=g;x&&x.target===a&&u&&(u.unobserve(t),cancelAnimationFrame(h),h=requestAnimationFrame(()=>{var y;(y=u)==null||y.observe(t)})),i()}),a&&!c&&u.observe(a),u.observe(t));let m,p=c?F(e):null;c&&w();function w(){let g=F(e);p&&!Se(p,g)&&i(),p=g,m=requestAnimationFrame(w)}return i(),()=>{var g;f.forEach(x=>{s&&x.removeEventListener("scroll",i),l&&x.removeEventListener("resize",i)}),d?.(),(g=u)==null||g.disconnect(),u=null,c&&cancelAnimationFrame(m)}}var Te=fe;var Le=de;var ke=(e,t,i)=>{let n=new Map,s={platform:nt,...i},l={...s.platform,_c:n};return ce(e,t,{...s,platform:l})};var Ee=({areGroupsCollapsedByDefault:e,canTrackDeselectedRecords:t,currentSelectionLivewireProperty:i,maxSelectableRecords:n,selectsCurrentPageOnly:s,$wire:l})=>({areFiltersOpen:!1,checkboxClickController:null,groupVisibility:[],isLoading:!1,selectedRecords:new Set,deselectedRecords:new Set,isTrackingDeselectedRecords:!1,shouldCheckUniqueSelection:!0,lastCheckedRecord:null,livewireId:null,entangledSelectedRecords:i?l.$entangle(i):null,cleanUpFiltersDropdown:null,unsubscribeLivewireHook:null,init(){this.livewireId=this.$root.closest("[wire\\:id]")?.attributes["wire:id"].value,l.$on("deselectAllTableRecords",()=>this.deselectAllRecords()),l.$on("scrollToTopOfTable",()=>this.$root.scrollIntoView({block:"start",inline:"nearest"})),i&&(n!==1?this.selectedRecords=new Set(this.entangledSelectedRecords):this.selectedRecords=new Set(this.entangledSelectedRecords?[this.entangledSelectedRecords]:[])),this.$nextTick(()=>this.watchForCheckboxClicks()),this.unsubscribeLivewireHook=Livewire.hook("element.init",({component:o})=>{o.id===this.livewireId&&this.watchForCheckboxClicks()})},mountAction(...o){l.set("isTrackingDeselectedTableRecords",this.isTrackingDeselectedRecords,!1),l.set("selectedTableRecords",[...this.selectedRecords],!1),l.set("deselectedTableRecords",[...this.deselectedRecords],!1),l.mountAction(...o)},toggleSelectRecordsOnPage(){let o=this.getRecordsOnPage();if(this.areRecordsSelected(o)){this.deselectRecords(o);return}this.selectRecords(o)},toggleSelectRecords(o){this.areRecordsSelected(o)?this.deselectRecords(o):this.selectRecords(o)},getSelectedRecordsCount(){return this.isTrackingDeselectedRecords?(this.$refs.allSelectableRecordsCount?.value??this.deselectedRecords.size)-this.deselectedRecords.size:this.selectedRecords.size},getRecordsOnPage(){let o=[];for(let r of this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[])o.push(r.value);return o},selectRecords(o){n===1&&(this.deselectAllRecords(),o=o.slice(0,1));for(let r of o)if(!this.isRecordSelected(r)){if(this.isTrackingDeselectedRecords){this.deselectedRecords.delete(r);continue}this.selectedRecords.add(r)}this.updatedSelectedRecords()},deselectRecords(o){for(let r of o){if(this.isTrackingDeselectedRecords){this.deselectedRecords.add(r);continue}this.selectedRecords.delete(r)}this.updatedSelectedRecords()},updatedSelectedRecords(){if(n!==1){this.entangledSelectedRecords=[...this.selectedRecords];return}this.entangledSelectedRecords=[...this.selectedRecords][0]??null},toggleSelectedRecord(o){if(this.isRecordSelected(o)){this.deselectRecords([o]);return}this.selectRecords([o])},async selectAllRecords(){if(!t||s){this.isLoading=!0,this.selectedRecords=new Set(await l.getAllSelectableTableRecordKeys()),this.updatedSelectedRecords(),this.isLoading=!1;return}this.isTrackingDeselectedRecords=!0,this.selectedRecords=new Set,this.deselectedRecords=new Set,this.updatedSelectedRecords()},canSelectAllRecords(){if(s){let c=this.getRecordsOnPage();return!this.areRecordsSelected(c)&&this.areRecordsToggleable(c)}let o=parseInt(this.$refs.allSelectableRecordsCount?.value);if(!o)return!1;let r=this.getSelectedRecordsCount();return o===r?!1:n===null||o<=n},deselectAllRecords(){this.isTrackingDeselectedRecords=!1,this.selectedRecords=new Set,this.deselectedRecords=new Set,this.updatedSelectedRecords()},isRecordSelected(o){return this.isTrackingDeselectedRecords?!this.deselectedRecords.has(o):this.selectedRecords.has(o)},areRecordsSelected(o){return o.every(r=>this.isRecordSelected(r))},areRecordsToggleable(o){if(n===null||n===1)return!0;let r=o.filter(c=>this.isRecordSelected(c));return r.length===o.length?!0:this.getSelectedRecordsCount()+(o.length-r.length)<=n},toggleCollapseGroup(o){this.isGroupCollapsed(o)?e?this.groupVisibility.push(o):this.groupVisibility.splice(this.groupVisibility.indexOf(o),1):e?this.groupVisibility.splice(this.groupVisibility.indexOf(o),1):this.groupVisibility.push(o)},isGroupCollapsed(o){return e?!this.groupVisibility.includes(o):this.groupVisibility.includes(o)},resetCollapsedGroups(){this.groupVisibility=[]},watchForCheckboxClicks(){this.checkboxClickController&&this.checkboxClickController.abort(),this.checkboxClickController=new AbortController;let{signal:o}=this.checkboxClickController;this.$root?.addEventListener("click",r=>r.target?.matches(".fi-ta-record-checkbox")&&this.handleCheckboxClick(r,r.target),{signal:o})},handleCheckboxClick(o,r){if(!this.lastChecked){this.lastChecked=r;return}if(o.shiftKey){let c=Array.from(this.$root?.getElementsByClassName("fi-ta-record-checkbox")??[]);if(!c.includes(this.lastChecked)){this.lastChecked=r;return}let a=c.indexOf(this.lastChecked),f=c.indexOf(r),d=[a,f].sort((u,m)=>u-m),h=[];for(let u=d[0];u<=d[1];u++)h.push(c[u].value);if(r.checked){if(!this.areRecordsToggleable(h)){r.checked=!1,this.deselectRecords([r.value]);return}this.selectRecords(h)}else this.deselectRecords(h)}this.lastChecked=r},toggleFiltersDropdown(){if(this.areFiltersOpen=!this.areFiltersOpen,this.areFiltersOpen){let o=Oe(this.$refs.filtersTriggerActionContainer,this.$refs.filtersContentContainer,async()=>{let{x:a,y:f}=await ke(this.$refs.filtersTriggerActionContainer,this.$refs.filtersContentContainer,{placement:"bottom-end",middleware:[Te(8),Le({padding:8})]});Object.assign(this.$refs.filtersContentContainer.style,{left:`${a}px`,top:`${f}px`})}),r=a=>{let f=this.$refs.filtersTriggerActionContainer,d=this.$refs.filtersContentContainer;d&&d.contains(a.target)||f&&f.contains(a.target)||(this.areFiltersOpen=!1,this.cleanUpFiltersDropdown&&(this.cleanUpFiltersDropdown(),this.cleanUpFiltersDropdown=null))};document.addEventListener("mousedown",r),document.addEventListener("touchstart",r,{passive:!0});let c=a=>{a.key==="Escape"&&r(a)};document.addEventListener("keydown",c),this.cleanUpFiltersDropdown=()=>{o(),document.removeEventListener("mousedown",r),document.removeEventListener("touchstart",r,{passive:!0}),document.removeEventListener("keydown",c)}}else this.cleanUpFiltersDropdown&&(this.cleanUpFiltersDropdown(),this.cleanUpFiltersDropdown=null)},destroy(){this.unsubscribeLivewireHook?.()}});function ne({columns:e,isLive:t}){return{error:void 0,isLoading:!1,deferredColumns:[],columns:e,isLive:t,hasReordered:!1,init(){if(!this.columns||this.columns.length===0){this.columns=[];return}this.deferredColumns=JSON.parse(JSON.stringify(this.columns)),this.$watch("columns",()=>{this.resetDeferredColumns()})},get groupedColumns(){let i={};return this.deferredColumns.filter(n=>n.type==="group").forEach(n=>{i[n.name]=this.calculateGroupedColumns(n)}),i},calculateGroupedColumns(i){if((i?.columns?.filter(r=>!r.isHidden)??[]).length===0)return{hidden:!0,checked:!1,disabled:!1,indeterminate:!1};let s=i.columns.filter(r=>!r.isHidden&&r.isToggleable!==!1);if(s.length===0)return{checked:!0,disabled:!0,indeterminate:!1};let l=s.filter(r=>r.isToggled).length,o=i.columns.filter(r=>!r.isHidden&&r.isToggleable===!1);return l===0&&o.length>0?{checked:!0,disabled:!1,indeterminate:!0}:l===0?{checked:!1,disabled:!1,indeterminate:!1}:l===s.length?{checked:!0,disabled:!1,indeterminate:!1}:{checked:!0,disabled:!1,indeterminate:!0}},getColumn(i,n=null){return n?this.deferredColumns.find(l=>l.type==="group"&&l.name===n)?.columns?.find(l=>l.name===i):this.deferredColumns.find(s=>s.name===i)},toggleGroup(i){let n=this.deferredColumns.find(c=>c.type==="group"&&c.name===i);if(!n?.columns)return;let s=this.calculateGroupedColumns(n);if(s.disabled)return;let o=n.columns.filter(c=>c.isToggleable!==!1).some(c=>c.isToggled),r=s.indeterminate?!0:!o;n.columns.filter(c=>c.isToggleable!==!1).forEach(c=>{c.isToggled=r}),this.deferredColumns=[...this.deferredColumns],this.isLive&&this.applyTableColumnManager()},toggleColumn(i,n=null){let s=this.getColumn(i,n);!s||s.isToggleable===!1||(s.isToggled=!s.isToggled,this.deferredColumns=[...this.deferredColumns],this.isLive&&this.applyTableColumnManager())},reorderColumns(i){let n=i.map(s=>s.split("::"));this.reorderTopLevel(n),this.hasReordered=!0,this.isLive&&this.applyTableColumnManager()},reorderGroupColumns(i,n){let s=this.deferredColumns.find(r=>r.type==="group"&&r.name===n);if(!s)return;let l=i.map(r=>r.split("::")),o=[];l.forEach(([r,c])=>{let a=s.columns.find(f=>f.name===c);a&&o.push(a)}),s.columns=o,this.deferredColumns=[...this.deferredColumns],this.hasReordered=!0,this.isLive&&this.applyTableColumnManager()},reorderTopLevel(i){let n=this.deferredColumns,s=[];i.forEach(([l,o])=>{let r=n.find(c=>l==="group"?c.type==="group"&&c.name===o:l==="column"?c.type!=="group"&&c.name===o:!1);r&&s.push(r)}),this.deferredColumns=s},async applyTableColumnManager(){this.isLoading=!0;try{this.columns=JSON.parse(JSON.stringify(this.deferredColumns)),await this.$wire.call("applyTableColumnManager",this.columns,this.hasReordered),this.hasReordered=!1,this.error=void 0}catch(i){this.error="Failed to update column visibility",console.error("Table toggle columns error:",i)}finally{this.isLoading=!1}},resetDeferredColumns(){this.deferredColumns=JSON.parse(JSON.stringify(this.columns)),this.hasReordered=!1}}}document.addEventListener("alpine:init",()=>{window.Alpine.data("filamentTable",Ee),window.Alpine.data("filamentTableColumnManager",ne)});})(); diff --git a/public/js/filament/widgets/components/chart.js b/public/js/filament/widgets/components/chart.js new file mode 100644 index 00000000..c04dfb87 --- /dev/null +++ b/public/js/filament/widgets/components/chart.js @@ -0,0 +1,30 @@ +var Sc=Object.defineProperty;var Mc=(s,t,e)=>t in s?Sc(s,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):s[t]=e;var v=(s,t,e)=>Mc(s,typeof t!="symbol"?t+"":t,e);function us(s){return s+.5|0}var Zt=(s,t,e)=>Math.max(Math.min(s,e),t);function cs(s){return Zt(us(s*2.55),0,255)}function qt(s){return Zt(us(s*255),0,255)}function Nt(s){return Zt(us(s/2.55)/100,0,1)}function No(s){return Zt(us(s*100),0,100)}var pt={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Zi=[..."0123456789ABCDEF"],Oc=s=>Zi[s&15],Tc=s=>Zi[(s&240)>>4]+Zi[s&15],Us=s=>(s&240)>>4===(s&15),Dc=s=>Us(s.r)&&Us(s.g)&&Us(s.b)&&Us(s.a);function Cc(s){var t=s.length,e;return s[0]==="#"&&(t===4||t===5?e={r:255&pt[s[1]]*17,g:255&pt[s[2]]*17,b:255&pt[s[3]]*17,a:t===5?pt[s[4]]*17:255}:(t===7||t===9)&&(e={r:pt[s[1]]<<4|pt[s[2]],g:pt[s[3]]<<4|pt[s[4]],b:pt[s[5]]<<4|pt[s[6]],a:t===9?pt[s[7]]<<4|pt[s[8]]:255})),e}var Pc=(s,t)=>s<255?t(s):"";function Ac(s){var t=Dc(s)?Oc:Tc;return s?"#"+t(s.r)+t(s.g)+t(s.b)+Pc(s.a,t):void 0}var Ic=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Bo(s,t,e){let i=t*Math.min(e,1-e),n=(o,r=(o+s/30)%12)=>e-i*Math.max(Math.min(r-3,9-r,1),-1);return[n(0),n(8),n(4)]}function Ec(s,t,e){let i=(n,o=(n+s/60)%6)=>e-e*t*Math.max(Math.min(o,4-o,1),0);return[i(5),i(3),i(1)]}function Lc(s,t,e){let i=Bo(s,1,.5),n;for(t+e>1&&(n=1/(t+e),t*=n,e*=n),n=0;n<3;n++)i[n]*=1-t-e,i[n]+=t;return i}function Fc(s,t,e,i,n){return s===n?(t-e)/i+(t.5?h/(2-o-r):h/(o+r),l=Fc(e,i,n,h,o),l=l*60+.5),[l|0,c||0,a]}function Gi(s,t,e,i){return(Array.isArray(t)?s(t[0],t[1],t[2]):s(t,e,i)).map(qt)}function Xi(s,t,e){return Gi(Bo,s,t,e)}function Rc(s,t,e){return Gi(Lc,s,t,e)}function Nc(s,t,e){return Gi(Ec,s,t,e)}function Ho(s){return(s%360+360)%360}function zc(s){let t=Ic.exec(s),e=255,i;if(!t)return;t[5]!==i&&(e=t[6]?cs(+t[5]):qt(+t[5]));let n=Ho(+t[2]),o=+t[3]/100,r=+t[4]/100;return t[1]==="hwb"?i=Rc(n,o,r):t[1]==="hsv"?i=Nc(n,o,r):i=Xi(n,o,r),{r:i[0],g:i[1],b:i[2],a:e}}function Vc(s,t){var e=qi(s);e[0]=Ho(e[0]+t),e=Xi(e),s.r=e[0],s.g=e[1],s.b=e[2]}function Wc(s){if(!s)return;let t=qi(s),e=t[0],i=No(t[1]),n=No(t[2]);return s.a<255?`hsla(${e}, ${i}%, ${n}%, ${Nt(s.a)})`:`hsl(${e}, ${i}%, ${n}%)`}var zo={x:"dark",Z:"light",Y:"re",X:"blu",W:"gr",V:"medium",U:"slate",A:"ee",T:"ol",S:"or",B:"ra",C:"lateg",D:"ights",R:"in",Q:"turquois",E:"hi",P:"ro",O:"al",N:"le",M:"de",L:"yello",F:"en",K:"ch",G:"arks",H:"ea",I:"ightg",J:"wh"},Vo={OiceXe:"f0f8ff",antiquewEte:"faebd7",aqua:"ffff",aquamarRe:"7fffd4",azuY:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"0",blanKedOmond:"ffebcd",Xe:"ff",XeviTet:"8a2be2",bPwn:"a52a2a",burlywood:"deb887",caMtXe:"5f9ea0",KartYuse:"7fff00",KocTate:"d2691e",cSO:"ff7f50",cSnflowerXe:"6495ed",cSnsilk:"fff8dc",crimson:"dc143c",cyan:"ffff",xXe:"8b",xcyan:"8b8b",xgTMnPd:"b8860b",xWay:"a9a9a9",xgYF:"6400",xgYy:"a9a9a9",xkhaki:"bdb76b",xmagFta:"8b008b",xTivegYF:"556b2f",xSange:"ff8c00",xScEd:"9932cc",xYd:"8b0000",xsOmon:"e9967a",xsHgYF:"8fbc8f",xUXe:"483d8b",xUWay:"2f4f4f",xUgYy:"2f4f4f",xQe:"ced1",xviTet:"9400d3",dAppRk:"ff1493",dApskyXe:"bfff",dimWay:"696969",dimgYy:"696969",dodgerXe:"1e90ff",fiYbrick:"b22222",flSOwEte:"fffaf0",foYstWAn:"228b22",fuKsia:"ff00ff",gaRsbSo:"dcdcdc",ghostwEte:"f8f8ff",gTd:"ffd700",gTMnPd:"daa520",Way:"808080",gYF:"8000",gYFLw:"adff2f",gYy:"808080",honeyMw:"f0fff0",hotpRk:"ff69b4",RdianYd:"cd5c5c",Rdigo:"4b0082",ivSy:"fffff0",khaki:"f0e68c",lavFMr:"e6e6fa",lavFMrXsh:"fff0f5",lawngYF:"7cfc00",NmoncEffon:"fffacd",ZXe:"add8e6",ZcSO:"f08080",Zcyan:"e0ffff",ZgTMnPdLw:"fafad2",ZWay:"d3d3d3",ZgYF:"90ee90",ZgYy:"d3d3d3",ZpRk:"ffb6c1",ZsOmon:"ffa07a",ZsHgYF:"20b2aa",ZskyXe:"87cefa",ZUWay:"778899",ZUgYy:"778899",ZstAlXe:"b0c4de",ZLw:"ffffe0",lime:"ff00",limegYF:"32cd32",lRF:"faf0e6",magFta:"ff00ff",maPon:"800000",VaquamarRe:"66cdaa",VXe:"cd",VScEd:"ba55d3",VpurpN:"9370db",VsHgYF:"3cb371",VUXe:"7b68ee",VsprRggYF:"fa9a",VQe:"48d1cc",VviTetYd:"c71585",midnightXe:"191970",mRtcYam:"f5fffa",mistyPse:"ffe4e1",moccasR:"ffe4b5",navajowEte:"ffdead",navy:"80",Tdlace:"fdf5e6",Tive:"808000",TivedBb:"6b8e23",Sange:"ffa500",SangeYd:"ff4500",ScEd:"da70d6",pOegTMnPd:"eee8aa",pOegYF:"98fb98",pOeQe:"afeeee",pOeviTetYd:"db7093",papayawEp:"ffefd5",pHKpuff:"ffdab9",peru:"cd853f",pRk:"ffc0cb",plum:"dda0dd",powMrXe:"b0e0e6",purpN:"800080",YbeccapurpN:"663399",Yd:"ff0000",Psybrown:"bc8f8f",PyOXe:"4169e1",saddNbPwn:"8b4513",sOmon:"fa8072",sandybPwn:"f4a460",sHgYF:"2e8b57",sHshell:"fff5ee",siFna:"a0522d",silver:"c0c0c0",skyXe:"87ceeb",UXe:"6a5acd",UWay:"708090",UgYy:"708090",snow:"fffafa",sprRggYF:"ff7f",stAlXe:"4682b4",tan:"d2b48c",teO:"8080",tEstN:"d8bfd8",tomato:"ff6347",Qe:"40e0d0",viTet:"ee82ee",JHt:"f5deb3",wEte:"ffffff",wEtesmoke:"f5f5f5",Lw:"ffff00",LwgYF:"9acd32"};function Bc(){let s={},t=Object.keys(Vo),e=Object.keys(zo),i,n,o,r,a;for(i=0;i>16&255,o>>8&255,o&255]}return s}var Ys;function Hc(s){Ys||(Ys=Bc(),Ys.transparent=[0,0,0,0]);let t=Ys[s.toLowerCase()];return t&&{r:t[0],g:t[1],b:t[2],a:t.length===4?t[3]:255}}var $c=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;function jc(s){let t=$c.exec(s),e=255,i,n,o;if(t){if(t[7]!==i){let r=+t[7];e=t[8]?cs(r):Zt(r*255,0,255)}return i=+t[1],n=+t[3],o=+t[5],i=255&(t[2]?cs(i):Zt(i,0,255)),n=255&(t[4]?cs(n):Zt(n,0,255)),o=255&(t[6]?cs(o):Zt(o,0,255)),{r:i,g:n,b:o,a:e}}}function Uc(s){return s&&(s.a<255?`rgba(${s.r}, ${s.g}, ${s.b}, ${Nt(s.a)})`:`rgb(${s.r}, ${s.g}, ${s.b})`)}var Yi=s=>s<=.0031308?s*12.92:Math.pow(s,1/2.4)*1.055-.055,De=s=>s<=.04045?s/12.92:Math.pow((s+.055)/1.055,2.4);function Yc(s,t,e){let i=De(Nt(s.r)),n=De(Nt(s.g)),o=De(Nt(s.b));return{r:qt(Yi(i+e*(De(Nt(t.r))-i))),g:qt(Yi(n+e*(De(Nt(t.g))-n))),b:qt(Yi(o+e*(De(Nt(t.b))-o))),a:s.a+e*(t.a-s.a)}}function Zs(s,t,e){if(s){let i=qi(s);i[t]=Math.max(0,Math.min(i[t]+i[t]*e,t===0?360:1)),i=Xi(i),s.r=i[0],s.g=i[1],s.b=i[2]}}function $o(s,t){return s&&Object.assign(t||{},s)}function Wo(s){var t={r:0,g:0,b:0,a:255};return Array.isArray(s)?s.length>=3&&(t={r:s[0],g:s[1],b:s[2],a:255},s.length>3&&(t.a=qt(s[3]))):(t=$o(s,{r:0,g:0,b:0,a:1}),t.a=qt(t.a)),t}function Zc(s){return s.charAt(0)==="r"?jc(s):zc(s)}var hs=class s{constructor(t){if(t instanceof s)return t;let e=typeof t,i;e==="object"?i=Wo(t):e==="string"&&(i=Cc(t)||Hc(t)||Zc(t)),this._rgb=i,this._valid=!!i}get valid(){return this._valid}get rgb(){var t=$o(this._rgb);return t&&(t.a=Nt(t.a)),t}set rgb(t){this._rgb=Wo(t)}rgbString(){return this._valid?Uc(this._rgb):void 0}hexString(){return this._valid?Ac(this._rgb):void 0}hslString(){return this._valid?Wc(this._rgb):void 0}mix(t,e){if(t){let i=this.rgb,n=t.rgb,o,r=e===o?.5:e,a=2*r-1,l=i.a-n.a,c=((a*l===-1?a:(a+l)/(1+a*l))+1)/2;o=1-c,i.r=255&c*i.r+o*n.r+.5,i.g=255&c*i.g+o*n.g+.5,i.b=255&c*i.b+o*n.b+.5,i.a=r*i.a+(1-r)*n.a,this.rgb=i}return this}interpolate(t,e){return t&&(this._rgb=Yc(this._rgb,t._rgb,e)),this}clone(){return new s(this.rgb)}alpha(t){return this._rgb.a=qt(t),this}clearer(t){let e=this._rgb;return e.a*=1-t,this}greyscale(){let t=this._rgb,e=us(t.r*.3+t.g*.59+t.b*.11);return t.r=t.g=t.b=e,this}opaquer(t){let e=this._rgb;return e.a*=1+t,this}negate(){let t=this._rgb;return t.r=255-t.r,t.g=255-t.g,t.b=255-t.b,this}lighten(t){return Zs(this._rgb,2,t),this}darken(t){return Zs(this._rgb,2,-t),this}saturate(t){return Zs(this._rgb,1,t),this}desaturate(t){return Zs(this._rgb,1,-t),this}rotate(t){return Vc(this._rgb,t),this}};function At(){}var tr=(()=>{let s=0;return()=>s++})();function I(s){return s==null}function H(s){if(Array.isArray&&Array.isArray(s))return!0;let t=Object.prototype.toString.call(s);return t.slice(0,7)==="[object"&&t.slice(-6)==="Array]"}function E(s){return s!==null&&Object.prototype.toString.call(s)==="[object Object]"}function Z(s){return(typeof s=="number"||s instanceof Number)&&isFinite(+s)}function at(s,t){return Z(s)?s:t}function P(s,t){return typeof s>"u"?t:s}var er=(s,t)=>typeof s=="string"&&s.endsWith("%")?parseFloat(s)/100:+s/t,tn=(s,t)=>typeof s=="string"&&s.endsWith("%")?parseFloat(s)/100*t:+s;function W(s,t,e){if(s&&typeof s.call=="function")return s.apply(e,t)}function z(s,t,e,i){let n,o,r;if(H(s))if(o=s.length,i)for(n=o-1;n>=0;n--)t.call(e,s[n],n);else for(n=0;ns,x:s=>s.x,y:s=>s.y};function Xc(s){let t=s.split("."),e=[],i="";for(let n of t)i+=n,i.endsWith("\\")?i=i.slice(0,-1)+".":(e.push(i),i="");return e}function Jc(s){let t=Xc(s);return e=>{for(let i of t){if(i==="")break;e=e&&e[i]}return e}}function Wt(s,t){return(jo[t]||(jo[t]=Jc(t)))(s)}function ei(s){return s.charAt(0).toUpperCase()+s.slice(1)}var Ee=s=>typeof s<"u",zt=s=>typeof s=="function",en=(s,t)=>{if(s.size!==t.size)return!1;for(let e of s)if(!t.has(e))return!1;return!0};function ir(s){return s.type==="mouseup"||s.type==="click"||s.type==="contextmenu"}var F=Math.PI,$=2*F,Kc=$+F,Ks=Number.POSITIVE_INFINITY,Qc=F/180,q=F/2,he=F/4,Uo=F*2/3,Vt=Math.log10,St=Math.sign;function Le(s,t,e){return Math.abs(s-t)n-o).pop(),t}function th(s){return typeof s=="symbol"||typeof s=="object"&&s!==null&&!(Symbol.toPrimitive in s||"toString"in s||"valueOf"in s)}function fe(s){return!th(s)&&!isNaN(parseFloat(s))&&isFinite(s)}function or(s,t){let e=Math.round(s);return e-t<=s&&e+t>=s}function nn(s,t,e){let i,n,o;for(i=0,n=s.length;il&&c=Math.min(t,e)-i&&s<=Math.max(t,e)+i}function ii(s,t,e){e=e||(r=>s[r]1;)o=n+i>>1,e(o)?n=o:i=o;return{lo:n,hi:i}}var Ct=(s,t,e,i)=>ii(s,e,i?n=>{let o=s[n][t];return os[n][t]ii(s,e,i=>s[i][t]>=e);function lr(s,t,e){let i=0,n=s.length;for(;ii&&s[n-1]>e;)n--;return i>0||n{let i="_onData"+ei(e),n=s[e];Object.defineProperty(s,e,{configurable:!0,enumerable:!1,value(...o){let r=n.apply(this,o);return s._chartjs.listeners.forEach(a=>{typeof a[i]=="function"&&a[i](...o)}),r}})})}function an(s,t){let e=s._chartjs;if(!e)return;let i=e.listeners,n=i.indexOf(t);n!==-1&&i.splice(n,1),!(i.length>0)&&(cr.forEach(o=>{delete s[o]}),delete s._chartjs)}function ln(s){let t=new Set(s);return t.size===s.length?s:Array.from(t)}var cn=(function(){return typeof window>"u"?function(s){return s()}:window.requestAnimationFrame})();function hn(s,t){let e=[],i=!1;return function(...n){e=n,i||(i=!0,cn.call(window,()=>{i=!1,s.apply(t,e)}))}}function ur(s,t){let e;return function(...i){return t?(clearTimeout(e),e=setTimeout(s,t,i)):s.apply(this,i),t}}var ni=s=>s==="start"?"left":s==="end"?"right":"center",it=(s,t,e)=>s==="start"?t:s==="end"?e:(t+e)/2,dr=(s,t,e,i)=>s===(i?"left":"right")?e:s==="center"?(t+e)/2:t;function un(s,t,e){let i=t.length,n=0,o=i;if(s._sorted){let{iScale:r,vScale:a,_parsed:l}=s,c=s.dataset&&s.dataset.options?s.dataset.options.spanGaps:null,h=r.axis,{min:u,max:d,minDefined:f,maxDefined:g}=r.getUserBounds();if(f){if(n=Math.min(Ct(l,h,u).lo,e?i:Ct(t,h,r.getPixelForValue(u)).lo),c){let m=l.slice(0,n+1).reverse().findIndex(p=>!I(p[a.axis]));n-=Math.max(0,m)}n=K(n,0,i-1)}if(g){let m=Math.max(Ct(l,r.axis,d,!0).hi+1,e?0:Ct(t,h,r.getPixelForValue(d),!0).hi+1);if(c){let p=l.slice(m-1).findIndex(b=>!I(b[a.axis]));m+=Math.max(0,p)}o=K(m,n,i)-n}else o=i-n}return{start:n,count:o}}function dn(s){let{xScale:t,yScale:e,_scaleRanges:i}=s,n={xmin:t.min,xmax:t.max,ymin:e.min,ymax:e.max};if(!i)return s._scaleRanges=n,!0;let o=i.xmin!==t.min||i.xmax!==t.max||i.ymin!==e.min||i.ymax!==e.max;return Object.assign(i,n),o}var qs=s=>s===0||s===1,Yo=(s,t,e)=>-(Math.pow(2,10*(s-=1))*Math.sin((s-t)*$/e)),Zo=(s,t,e)=>Math.pow(2,-10*s)*Math.sin((s-t)*$/e)+1,Ce={linear:s=>s,easeInQuad:s=>s*s,easeOutQuad:s=>-s*(s-2),easeInOutQuad:s=>(s/=.5)<1?.5*s*s:-.5*(--s*(s-2)-1),easeInCubic:s=>s*s*s,easeOutCubic:s=>(s-=1)*s*s+1,easeInOutCubic:s=>(s/=.5)<1?.5*s*s*s:.5*((s-=2)*s*s+2),easeInQuart:s=>s*s*s*s,easeOutQuart:s=>-((s-=1)*s*s*s-1),easeInOutQuart:s=>(s/=.5)<1?.5*s*s*s*s:-.5*((s-=2)*s*s*s-2),easeInQuint:s=>s*s*s*s*s,easeOutQuint:s=>(s-=1)*s*s*s*s+1,easeInOutQuint:s=>(s/=.5)<1?.5*s*s*s*s*s:.5*((s-=2)*s*s*s*s+2),easeInSine:s=>-Math.cos(s*q)+1,easeOutSine:s=>Math.sin(s*q),easeInOutSine:s=>-.5*(Math.cos(F*s)-1),easeInExpo:s=>s===0?0:Math.pow(2,10*(s-1)),easeOutExpo:s=>s===1?1:-Math.pow(2,-10*s)+1,easeInOutExpo:s=>qs(s)?s:s<.5?.5*Math.pow(2,10*(s*2-1)):.5*(-Math.pow(2,-10*(s*2-1))+2),easeInCirc:s=>s>=1?s:-(Math.sqrt(1-s*s)-1),easeOutCirc:s=>Math.sqrt(1-(s-=1)*s),easeInOutCirc:s=>(s/=.5)<1?-.5*(Math.sqrt(1-s*s)-1):.5*(Math.sqrt(1-(s-=2)*s)+1),easeInElastic:s=>qs(s)?s:Yo(s,.075,.3),easeOutElastic:s=>qs(s)?s:Zo(s,.075,.3),easeInOutElastic(s){return qs(s)?s:s<.5?.5*Yo(s*2,.1125,.45):.5+.5*Zo(s*2-1,.1125,.45)},easeInBack(s){return s*s*((1.70158+1)*s-1.70158)},easeOutBack(s){return(s-=1)*s*((1.70158+1)*s+1.70158)+1},easeInOutBack(s){let t=1.70158;return(s/=.5)<1?.5*(s*s*(((t*=1.525)+1)*s-t)):.5*((s-=2)*s*(((t*=1.525)+1)*s+t)+2)},easeInBounce:s=>1-Ce.easeOutBounce(1-s),easeOutBounce(s){return s<1/2.75?7.5625*s*s:s<2/2.75?7.5625*(s-=1.5/2.75)*s+.75:s<2.5/2.75?7.5625*(s-=2.25/2.75)*s+.9375:7.5625*(s-=2.625/2.75)*s+.984375},easeInOutBounce:s=>s<.5?Ce.easeInBounce(s*2)*.5:Ce.easeOutBounce(s*2-1)*.5+.5};function fn(s){if(s&&typeof s=="object"){let t=s.toString();return t==="[object CanvasPattern]"||t==="[object CanvasGradient]"}return!1}function gn(s){return fn(s)?s:new hs(s)}function Ji(s){return fn(s)?s:new hs(s).saturate(.5).darken(.1).hexString()}var sh=["x","y","borderWidth","radius","tension"],ih=["color","borderColor","backgroundColor"];function nh(s){s.set("animation",{delay:void 0,duration:1e3,easing:"easeOutQuart",fn:void 0,from:void 0,loop:void 0,to:void 0,type:void 0}),s.describe("animation",{_fallback:!1,_indexable:!1,_scriptable:t=>t!=="onProgress"&&t!=="onComplete"&&t!=="fn"}),s.set("animations",{colors:{type:"color",properties:ih},numbers:{type:"number",properties:sh}}),s.describe("animations",{_fallback:"animation"}),s.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:t=>t|0}}}})}function oh(s){s.set("layout",{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})}var qo=new Map;function rh(s,t){t=t||{};let e=s+JSON.stringify(t),i=qo.get(e);return i||(i=new Intl.NumberFormat(s,t),qo.set(e,i)),i}function Re(s,t,e){return rh(t,e).format(s)}var fr={values(s){return H(s)?s:""+s},numeric(s,t,e){if(s===0)return"0";let i=this.chart.options.locale,n,o=s;if(e.length>1){let c=Math.max(Math.abs(e[0].value),Math.abs(e[e.length-1].value));(c<1e-4||c>1e15)&&(n="scientific"),o=ah(s,e)}let r=Vt(Math.abs(o)),a=isNaN(r)?1:Math.max(Math.min(-1*Math.floor(r),20),0),l={notation:n,minimumFractionDigits:a,maximumFractionDigits:a};return Object.assign(l,this.options.ticks.format),Re(s,i,l)},logarithmic(s,t,e){if(s===0)return"0";let i=e[t].significand||s/Math.pow(10,Math.floor(Vt(s)));return[1,2,3,5,10,15].includes(i)||t>.8*e.length?fr.numeric.call(this,s,t,e):""}};function ah(s,t){let e=t.length>3?t[2].value-t[1].value:t[1].value-t[0].value;return Math.abs(e)>=1&&s!==Math.floor(s)&&(e=s-Math.floor(s)),e}var ms={formatters:fr};function lh(s){s.set("scale",{display:!0,offset:!1,reverse:!1,beginAtZero:!1,bounds:"ticks",clip:!0,grace:0,grid:{display:!0,lineWidth:1,drawOnChartArea:!0,drawTicks:!0,tickLength:8,tickWidth:(t,e)=>e.lineWidth,tickColor:(t,e)=>e.color,offset:!1},border:{display:!0,dash:[],dashOffset:0,width:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:ms.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}}),s.route("scale.ticks","color","","color"),s.route("scale.grid","color","","borderColor"),s.route("scale.border","color","","borderColor"),s.route("scale.title","color","","color"),s.describe("scale",{_fallback:!1,_scriptable:t=>!t.startsWith("before")&&!t.startsWith("after")&&t!=="callback"&&t!=="parser",_indexable:t=>t!=="borderDash"&&t!=="tickBorderDash"&&t!=="dash"}),s.describe("scales",{_fallback:"scale"}),s.describe("scale.ticks",{_scriptable:t=>t!=="backdropPadding"&&t!=="callback",_indexable:t=>t!=="backdropPadding"})}var Jt=Object.create(null),oi=Object.create(null);function ds(s,t){if(!t)return s;let e=t.split(".");for(let i=0,n=e.length;ii.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(i,n)=>Ji(n.backgroundColor),this.hoverBorderColor=(i,n)=>Ji(n.borderColor),this.hoverColor=(i,n)=>Ji(n.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0,includeInvisible:!1},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.drawActiveElementsOnTop=!0,this.describe(t),this.apply(e)}set(t,e){return Ki(this,t,e)}get(t){return ds(this,t)}describe(t,e){return Ki(oi,t,e)}override(t,e){return Ki(Jt,t,e)}route(t,e,i,n){let o=ds(this,t),r=ds(this,i),a="_"+e;Object.defineProperties(o,{[a]:{value:o[e],writable:!0},[e]:{enumerable:!0,get(){let l=this[a],c=r[n];return E(l)?Object.assign({},c,l):P(l,c)},set(l){this[a]=l}}})}apply(t){t.forEach(e=>e(this))}},j=new Qi({_scriptable:s=>!s.startsWith("on"),_indexable:s=>s!=="events",hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}},[nh,oh,lh]);function ch(s){return!s||I(s.size)||I(s.family)?null:(s.style?s.style+" ":"")+(s.weight?s.weight+" ":"")+s.size+"px "+s.family}function fs(s,t,e,i,n){let o=t[n];return o||(o=t[n]=s.measureText(n).width,e.push(n)),o>i&&(i=o),i}function gr(s,t,e,i){i=i||{};let n=i.data=i.data||{},o=i.garbageCollect=i.garbageCollect||[];i.font!==t&&(n=i.data={},o=i.garbageCollect=[],i.font=t),s.save(),s.font=t;let r=0,a=e.length,l,c,h,u,d;for(l=0;le.length){for(l=0;l0&&s.stroke()}}function Pt(s,t,e){return e=e||.5,!t||s&&s.x>t.left-e&&s.xt.top-e&&s.y0&&o.strokeColor!=="",l,c;for(s.save(),s.font=n.string,hh(s,o),l=0;l+s||0;function ai(s,t){let e={},i=E(t),n=i?Object.keys(t):t,o=E(s)?i?r=>P(s[r],s[t[r]]):r=>s[r]:()=>s;for(let r of n)e[r]=ph(o(r));return e}function bn(s){return ai(s,{top:"y",right:"x",bottom:"y",left:"x"})}function te(s){return ai(s,["topLeft","topRight","bottomLeft","bottomRight"])}function nt(s){let t=bn(s);return t.width=t.left+t.right,t.height=t.top+t.bottom,t}function X(s,t){s=s||{},t=t||j.font;let e=P(s.size,t.size);typeof e=="string"&&(e=parseInt(e,10));let i=P(s.style,t.style);i&&!(""+i).match(gh)&&(console.warn('Invalid font style specified: "'+i+'"'),i=void 0);let n={family:P(s.family,t.family),lineHeight:mh(P(s.lineHeight,t.lineHeight),e),size:e,style:i,weight:P(s.weight,t.weight),string:""};return n.string=ch(n),n}function ze(s,t,e,i){let n=!0,o,r,a;for(o=0,r=s.length;oe&&a===0?0:a+l;return{min:r(i,-Math.abs(o)),max:r(n,o)}}function Bt(s,t){return Object.assign(Object.create(s),t)}function li(s,t=[""],e,i,n=()=>s[0]){let o=e||s;typeof i>"u"&&(i=_r("_fallback",s));let r={[Symbol.toStringTag]:"Object",_cacheable:!0,_scopes:s,_rootScopes:o,_fallback:i,_getTarget:n,override:a=>li([a,...s],t,o,i)};return new Proxy(r,{deleteProperty(a,l){return delete a[l],delete a._keys,delete s[0][l],!0},get(a,l){return yr(a,l,()=>Sh(l,t,s,a))},getOwnPropertyDescriptor(a,l){return Reflect.getOwnPropertyDescriptor(a._scopes[0],l)},getPrototypeOf(){return Reflect.getPrototypeOf(s[0])},has(a,l){return Xo(a).includes(l)},ownKeys(a){return Xo(a)},set(a,l,c){let h=a._storage||(a._storage=n());return a[l]=h[l]=c,delete a._keys,!0}})}function de(s,t,e,i){let n={_cacheable:!1,_proxy:s,_context:t,_subProxy:e,_stack:new Set,_descriptors:yn(s,i),setContext:o=>de(s,o,e,i),override:o=>de(s.override(o),t,e,i)};return new Proxy(n,{deleteProperty(o,r){return delete o[r],delete s[r],!0},get(o,r,a){return yr(o,r,()=>yh(o,r,a))},getOwnPropertyDescriptor(o,r){return o._descriptors.allKeys?Reflect.has(s,r)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(s,r)},getPrototypeOf(){return Reflect.getPrototypeOf(s)},has(o,r){return Reflect.has(s,r)},ownKeys(){return Reflect.ownKeys(s)},set(o,r,a){return s[r]=a,delete o[r],!0}})}function yn(s,t={scriptable:!0,indexable:!0}){let{_scriptable:e=t.scriptable,_indexable:i=t.indexable,_allKeys:n=t.allKeys}=s;return{allKeys:n,scriptable:e,indexable:i,isScriptable:zt(e)?e:()=>e,isIndexable:zt(i)?i:()=>i}}var bh=(s,t)=>s?s+ei(t):t,xn=(s,t)=>E(t)&&s!=="adapters"&&(Object.getPrototypeOf(t)===null||t.constructor===Object);function yr(s,t,e){if(Object.prototype.hasOwnProperty.call(s,t)||t==="constructor")return s[t];let i=e();return s[t]=i,i}function yh(s,t,e){let{_proxy:i,_context:n,_subProxy:o,_descriptors:r}=s,a=i[t];return zt(a)&&r.isScriptable(t)&&(a=xh(t,a,s,e)),H(a)&&a.length&&(a=_h(t,a,s,r.isIndexable)),xn(t,a)&&(a=de(a,n,o&&o[t],r)),a}function xh(s,t,e,i){let{_proxy:n,_context:o,_subProxy:r,_stack:a}=e;if(a.has(s))throw new Error("Recursion detected: "+Array.from(a).join("->")+"->"+s);a.add(s);let l=t(o,r||i);return a.delete(s),xn(s,l)&&(l=_n(n._scopes,n,s,l)),l}function _h(s,t,e,i){let{_proxy:n,_context:o,_subProxy:r,_descriptors:a}=e;if(typeof o.index<"u"&&i(s))return t[o.index%t.length];if(E(t[0])){let l=t,c=n._scopes.filter(h=>h!==l);t=[];for(let h of l){let u=_n(c,n,s,h);t.push(de(u,o,r&&r[s],a))}}return t}function xr(s,t,e){return zt(s)?s(t,e):s}var wh=(s,t)=>s===!0?t:typeof s=="string"?Wt(t,s):void 0;function kh(s,t,e,i,n){for(let o of t){let r=wh(e,o);if(r){s.add(r);let a=xr(r._fallback,e,n);if(typeof a<"u"&&a!==e&&a!==i)return a}else if(r===!1&&typeof i<"u"&&e!==i)return null}return!1}function _n(s,t,e,i){let n=t._rootScopes,o=xr(t._fallback,e,i),r=[...s,...n],a=new Set;a.add(i);let l=Go(a,r,e,o||e,i);return l===null||typeof o<"u"&&o!==e&&(l=Go(a,r,o,l,i),l===null)?!1:li(Array.from(a),[""],n,o,()=>vh(t,e,i))}function Go(s,t,e,i,n){for(;e;)e=kh(s,t,e,i,n);return e}function vh(s,t,e){let i=s._getTarget();t in i||(i[t]={});let n=i[t];return H(n)&&E(e)?e:n||{}}function Sh(s,t,e,i){let n;for(let o of t)if(n=_r(bh(o,s),e),typeof n<"u")return xn(s,n)?_n(e,i,s,n):n}function _r(s,t){for(let e of t){if(!e)continue;let i=e[s];if(typeof i<"u")return i}}function Xo(s){let t=s._keys;return t||(t=s._keys=Mh(s._scopes)),t}function Mh(s){let t=new Set;for(let e of s)for(let i of Object.keys(e).filter(n=>!n.startsWith("_")))t.add(i);return Array.from(t)}function wn(s,t,e,i){let{iScale:n}=s,{key:o="r"}=this._parsing,r=new Array(i),a,l,c,h;for(a=0,l=i;ats==="x"?"y":"x";function Th(s,t,e,i){let n=s.skip?t:s,o=t,r=e.skip?t:e,a=Qs(o,n),l=Qs(r,o),c=a/(a+l),h=l/(a+l);c=isNaN(c)?0:c,h=isNaN(h)?0:h;let u=i*c,d=i*h;return{previous:{x:o.x-u*(r.x-n.x),y:o.y-u*(r.y-n.y)},next:{x:o.x+d*(r.x-n.x),y:o.y+d*(r.y-n.y)}}}function Dh(s,t,e){let i=s.length,n,o,r,a,l,c=Ae(s,0);for(let h=0;h!c.skip)),t.cubicInterpolationMode==="monotone")Ph(s,n);else{let c=i?s[s.length-1]:s[0];for(o=0,r=s.length;os.ownerDocument.defaultView.getComputedStyle(s,null);function Ih(s,t){return ui(s).getPropertyValue(t)}var Eh=["top","right","bottom","left"];function ue(s,t,e){let i={};e=e?"-"+e:"";for(let n=0;n<4;n++){let o=Eh[n];i[o]=parseFloat(s[t+"-"+o+e])||0}return i.width=i.left+i.right,i.height=i.top+i.bottom,i}var Lh=(s,t,e)=>(s>0||t>0)&&(!e||!e.shadowRoot);function Fh(s,t){let e=s.touches,i=e&&e.length?e[0]:s,{offsetX:n,offsetY:o}=i,r=!1,a,l;if(Lh(n,o,s.target))a=n,l=o;else{let c=t.getBoundingClientRect();a=i.clientX-c.left,l=i.clientY-c.top,r=!0}return{x:a,y:l,box:r}}function ee(s,t){if("native"in s)return s;let{canvas:e,currentDevicePixelRatio:i}=t,n=ui(e),o=n.boxSizing==="border-box",r=ue(n,"padding"),a=ue(n,"border","width"),{x:l,y:c,box:h}=Fh(s,e),u=r.left+(h&&a.left),d=r.top+(h&&a.top),{width:f,height:g}=t;return o&&(f-=r.width+a.width,g-=r.height+a.height),{x:Math.round((l-u)/f*e.width/i),y:Math.round((c-d)/g*e.height/i)}}function Rh(s,t,e){let i,n;if(t===void 0||e===void 0){let o=s&&hi(s);if(!o)t=s.clientWidth,e=s.clientHeight;else{let r=o.getBoundingClientRect(),a=ui(o),l=ue(a,"border","width"),c=ue(a,"padding");t=r.width-c.width-l.width,e=r.height-c.height-l.height,i=ti(a.maxWidth,o,"clientWidth"),n=ti(a.maxHeight,o,"clientHeight")}}return{width:t,height:e,maxWidth:i||Ks,maxHeight:n||Ks}}var Xt=s=>Math.round(s*10)/10;function vr(s,t,e,i){let n=ui(s),o=ue(n,"margin"),r=ti(n.maxWidth,s,"clientWidth")||Ks,a=ti(n.maxHeight,s,"clientHeight")||Ks,l=Rh(s,t,e),{width:c,height:h}=l;if(n.boxSizing==="content-box"){let d=ue(n,"border","width"),f=ue(n,"padding");c-=f.width+d.width,h-=f.height+d.height}return c=Math.max(0,c-o.width),h=Math.max(0,i?c/i:h-o.height),c=Xt(Math.min(c,r,l.maxWidth)),h=Xt(Math.min(h,a,l.maxHeight)),c&&!h&&(h=Xt(c/2)),(t!==void 0||e!==void 0)&&i&&l.height&&h>l.height&&(h=l.height,c=Xt(Math.floor(h*i))),{width:c,height:h}}function kn(s,t,e){let i=t||1,n=Xt(s.height*i),o=Xt(s.width*i);s.height=Xt(s.height),s.width=Xt(s.width);let r=s.canvas;return r.style&&(e||!r.style.height&&!r.style.width)&&(r.style.height=`${s.height}px`,r.style.width=`${s.width}px`),s.currentDevicePixelRatio!==i||r.height!==n||r.width!==o?(s.currentDevicePixelRatio=i,r.height=n,r.width=o,s.ctx.setTransform(i,0,0,i,0,0),!0):!1}var Sr=(function(){let s=!1;try{let t={get passive(){return s=!0,!1}};ci()&&(window.addEventListener("test",null,t),window.removeEventListener("test",null,t))}catch{}return s})();function vn(s,t){let e=Ih(s,t),i=e&&e.match(/^(\d+)(\.\d+)?px$/);return i?+i[1]:void 0}function Gt(s,t,e,i){return{x:s.x+e*(t.x-s.x),y:s.y+e*(t.y-s.y)}}function Mr(s,t,e,i){return{x:s.x+e*(t.x-s.x),y:i==="middle"?e<.5?s.y:t.y:i==="after"?e<1?s.y:t.y:e>0?t.y:s.y}}function Or(s,t,e,i){let n={x:s.cp2x,y:s.cp2y},o={x:t.cp1x,y:t.cp1y},r=Gt(s,n,e),a=Gt(n,o,e),l=Gt(o,t,e),c=Gt(r,a,e),h=Gt(a,l,e);return Gt(c,h,e)}var Nh=function(s,t){return{x(e){return s+s+t-e},setWidth(e){t=e},textAlign(e){return e==="center"?e:e==="right"?"left":"right"},xPlus(e,i){return e-i},leftForLtr(e,i){return e-i}}},zh=function(){return{x(s){return s},setWidth(s){},textAlign(s){return s},xPlus(s,t){return s+t},leftForLtr(s,t){return s}}};function ge(s,t,e){return s?Nh(t,e):zh()}function Sn(s,t){let e,i;(t==="ltr"||t==="rtl")&&(e=s.canvas.style,i=[e.getPropertyValue("direction"),e.getPropertyPriority("direction")],e.setProperty("direction",t,"important"),s.prevTextDirection=i)}function Mn(s,t){t!==void 0&&(delete s.prevTextDirection,s.canvas.style.setProperty("direction",t[0],t[1]))}function Tr(s){return s==="angle"?{between:Fe,compare:eh,normalize:st}:{between:It,compare:(t,e)=>t-e,normalize:t=>t}}function Jo({start:s,end:t,count:e,loop:i,style:n}){return{start:s%e,end:t%e,loop:i&&(t-s+1)%e===0,style:n}}function Vh(s,t,e){let{property:i,start:n,end:o}=e,{between:r,normalize:a}=Tr(i),l=t.length,{start:c,end:h,loop:u}=s,d,f;if(u){for(c+=l,h+=l,d=0,f=l;dl(n,_,b)&&a(n,_)!==0,x=()=>a(o,b)===0||l(o,_,b),k=()=>m||w(),S=()=>!m||x();for(let M=h,T=h;M<=u;++M)y=t[M%r],!y.skip&&(b=c(y[i]),b!==_&&(m=l(b,n,o),p===null&&k()&&(p=a(b,n)===0?M:T),p!==null&&S()&&(g.push(Jo({start:p,end:M,loop:d,count:r,style:f})),p=null),T=M,_=b));return p!==null&&g.push(Jo({start:p,end:u,loop:d,count:r,style:f})),g}function Tn(s,t){let e=[],i=s.segments;for(let n=0;nn&&s[o%t].skip;)o--;return o%=t,{start:n,end:o}}function Bh(s,t,e,i){let n=s.length,o=[],r=t,a=s[t],l;for(l=t+1;l<=e;++l){let c=s[l%n];c.skip||c.stop?a.skip||(i=!1,o.push({start:t%n,end:(l-1)%n,loop:i}),t=r=c.stop?l:null):(r=l,a.skip&&(t=l)),a=c}return r!==null&&o.push({start:t%n,end:r%n,loop:i}),o}function Dr(s,t){let e=s.points,i=s.options.spanGaps,n=e.length;if(!n)return[];let o=!!s._loop,{start:r,end:a}=Wh(e,n,o,i);if(i===!0)return Ko(s,[{start:r,end:a,loop:o}],e,t);let l=aa({chart:t,initial:e.initial,numSteps:r,currentStep:Math.min(i-e.start,r)}))}_refresh(){this._request||(this._running=!0,this._request=cn.call(window,()=>{this._update(),this._request=null,this._running&&this._refresh()}))}_update(t=Date.now()){let e=0;this._charts.forEach((i,n)=>{if(!i.running||!i.items.length)return;let o=i.items,r=o.length-1,a=!1,l;for(;r>=0;--r)l=o[r],l._active?(l._total>i.duration&&(i.duration=l._total),l.tick(t),a=!0):(o[r]=o[o.length-1],o.pop());a&&(n.draw(),this._notify(n,i,t,"progress")),o.length||(i.running=!1,this._notify(n,i,t,"complete"),i.initial=!1),e+=o.length}),this._lastDate=t,e===0&&(this._running=!1)}_getAnims(t){let e=this._charts,i=e.get(t);return i||(i={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},e.set(t,i)),i}listen(t,e,i){this._getAnims(t).listeners[e].push(i)}add(t,e){!e||!e.length||this._getAnims(t).items.push(...e)}has(t){return this._getAnims(t).items.length>0}start(t){let e=this._charts.get(t);e&&(e.running=!0,e.start=Date.now(),e.duration=e.items.reduce((i,n)=>Math.max(i,n._duration),0),this._refresh())}running(t){if(!this._running)return!1;let e=this._charts.get(t);return!(!e||!e.running||!e.items.length)}stop(t){let e=this._charts.get(t);if(!e||!e.items.length)return;let i=e.items,n=i.length-1;for(;n>=0;--n)i[n].cancel();e.items=[],this._notify(t,e,Date.now(),"complete")}remove(t){return this._charts.delete(t)}},Ht=new Wn,Cr="transparent",Uh={boolean(s,t,e){return e>.5?t:s},color(s,t,e){let i=gn(s||Cr),n=i.valid&&gn(t||Cr);return n&&n.valid?n.mix(i,e).hexString():t},number(s,t,e){return s+(t-s)*e}},Bn=class{constructor(t,e,i,n){let o=e[i];n=ze([t.to,n,o,t.from]);let r=ze([t.from,o,n]);this._active=!0,this._fn=t.fn||Uh[t.type||typeof r],this._easing=Ce[t.easing]||Ce.linear,this._start=Math.floor(Date.now()+(t.delay||0)),this._duration=this._total=Math.floor(t.duration),this._loop=!!t.loop,this._target=e,this._prop=i,this._from=r,this._to=n,this._promises=void 0}active(){return this._active}update(t,e,i){if(this._active){this._notify(!1);let n=this._target[this._prop],o=i-this._start,r=this._duration-o;this._start=i,this._duration=Math.floor(Math.max(r,t.duration)),this._total+=o,this._loop=!!t.loop,this._to=ze([t.to,e,n,t.from]),this._from=ze([t.from,n,e])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(t){let e=t-this._start,i=this._duration,n=this._prop,o=this._from,r=this._loop,a=this._to,l;if(this._active=o!==a&&(r||e1?2-l:l,l=this._easing(Math.min(1,Math.max(0,l))),this._target[n]=this._fn(o,a,l)}wait(){let t=this._promises||(this._promises=[]);return new Promise((e,i)=>{t.push({res:e,rej:i})})}_notify(t){let e=t?"res":"rej",i=this._promises||[];for(let n=0;n{let o=t[n];if(!E(o))return;let r={};for(let a of e)r[a]=o[a];(H(o.properties)&&o.properties||[n]).forEach(a=>{(a===n||!i.has(a))&&i.set(a,r)})})}_animateOptions(t,e){let i=e.options,n=Zh(t,i);if(!n)return[];let o=this._createAnimations(n,i);return i.$shared&&Yh(t.options.$animations,i).then(()=>{t.options=i},()=>{}),o}_createAnimations(t,e){let i=this._properties,n=[],o=t.$animations||(t.$animations={}),r=Object.keys(e),a=Date.now(),l;for(l=r.length-1;l>=0;--l){let c=r[l];if(c.charAt(0)==="$")continue;if(c==="options"){n.push(...this._animateOptions(t,e));continue}let h=e[c],u=o[c],d=i.get(c);if(u)if(d&&u.active()){u.update(d,h,a);continue}else u.cancel();if(!d||!d.duration){t[c]=h;continue}o[c]=u=new Bn(d,t,c,h),n.push(u)}return n}update(t,e){if(this._properties.size===0){Object.assign(t,e);return}let i=this._createAnimations(t,e);if(i.length)return Ht.add(this._chart,i),!0}};function Yh(s,t){let e=[],i=Object.keys(t);for(let n=0;n0||!e&&o<0)return n.index}return null}function Er(s,t){let{chart:e,_cachedMeta:i}=s,n=e._stacks||(e._stacks={}),{iScale:o,vScale:r,index:a}=i,l=o.axis,c=r.axis,h=Jh(o,r,i),u=t.length,d;for(let f=0;fe[i].axis===t).shift()}function tu(s,t){return Bt(s,{active:!1,dataset:void 0,datasetIndex:t,index:t,mode:"default",type:"dataset"})}function eu(s,t,e){return Bt(s,{active:!1,dataIndex:t,parsed:void 0,raw:void 0,element:e,index:t,mode:"default",type:"data"})}function ys(s,t){let e=s.controller.index,i=s.vScale&&s.vScale.axis;if(i){t=t||s._parsed;for(let n of t){let o=n._stacks;if(!o||o[i]===void 0||o[i][e]===void 0)return;delete o[i][e],o[i]._visualValues!==void 0&&o[i]._visualValues[e]!==void 0&&delete o[i]._visualValues[e]}}}var An=s=>s==="reset"||s==="none",Lr=(s,t)=>t?s:Object.assign({},s),su=(s,t,e)=>s&&!t.hidden&&t._stacked&&{keys:Da(e,!0),values:null},ut=class{constructor(t,e){this.chart=t,this._ctx=t.ctx,this.index=e,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.supportsDecimation=!1,this.$context=void 0,this._syncList=[],this.datasetElementType=new.target.datasetElementType,this.dataElementType=new.target.dataElementType,this.initialize()}initialize(){let t=this._cachedMeta;this.configure(),this.linkScales(),t._stacked=Cn(t.vScale,t),this.addElements(),this.options.fill&&!this.chart.isPluginEnabled("filler")&&console.warn("Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options")}updateIndex(t){this.index!==t&&ys(this._cachedMeta),this.index=t}linkScales(){let t=this.chart,e=this._cachedMeta,i=this.getDataset(),n=(u,d,f,g)=>u==="x"?d:u==="r"?g:f,o=e.xAxisID=P(i.xAxisID,Pn(t,"x")),r=e.yAxisID=P(i.yAxisID,Pn(t,"y")),a=e.rAxisID=P(i.rAxisID,Pn(t,"r")),l=e.indexAxis,c=e.iAxisID=n(l,o,r,a),h=e.vAxisID=n(l,r,o,a);e.xScale=this.getScaleForId(o),e.yScale=this.getScaleForId(r),e.rScale=this.getScaleForId(a),e.iScale=this.getScaleForId(c),e.vScale=this.getScaleForId(h)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(t){return this.chart.scales[t]}_getOtherScale(t){let e=this._cachedMeta;return t===e.iScale?e.vScale:e.iScale}reset(){this._update("reset")}_destroy(){let t=this._cachedMeta;this._data&&an(this._data,this),t._stacked&&ys(t)}_dataCheck(){let t=this.getDataset(),e=t.data||(t.data=[]),i=this._data;if(E(e)){let n=this._cachedMeta;this._data=Xh(e,n)}else if(i!==e){if(i){an(i,this);let n=this._cachedMeta;ys(n),n._parsed=[]}e&&Object.isExtensible(e)&&hr(e,this),this._syncList=[],this._data=e}}addElements(){let t=this._cachedMeta;this._dataCheck(),this.datasetElementType&&(t.dataset=new this.datasetElementType)}buildOrUpdateElements(t){let e=this._cachedMeta,i=this.getDataset(),n=!1;this._dataCheck();let o=e._stacked;e._stacked=Cn(e.vScale,e),e.stack!==i.stack&&(n=!0,ys(e),e.stack=i.stack),this._resyncElements(t),(n||o!==e._stacked)&&(Er(this,e._parsed),e._stacked=Cn(e.vScale,e))}configure(){let t=this.chart.config,e=t.datasetScopeKeys(this._type),i=t.getOptionScopes(this.getDataset(),e,!0);this.options=t.createResolver(i,this.getContext()),this._parsing=this.options.parsing,this._cachedDataOpts={}}parse(t,e){let{_cachedMeta:i,_data:n}=this,{iScale:o,_stacked:r}=i,a=o.axis,l=t===0&&e===n.length?!0:i._sorted,c=t>0&&i._parsed[t-1],h,u,d;if(this._parsing===!1)i._parsed=n,i._sorted=!0,d=n;else{H(n[t])?d=this.parseArrayData(i,n,t,e):E(n[t])?d=this.parseObjectData(i,n,t,e):d=this.parsePrimitiveData(i,n,t,e);let f=()=>u[a]===null||c&&u[a]m||u=0;--d)if(!g()){this.updateRangeFromParsed(c,t,f,l);break}}return c}getAllParsedValues(t){let e=this._cachedMeta._parsed,i=[],n,o,r;for(n=0,o=e.length;n=0&&tthis.getContext(i,n,e),m=c.resolveNamedOptions(d,f,g,u);return m.$shared&&(m.$shared=l,o[r]=Object.freeze(Lr(m,l))),m}_resolveAnimations(t,e,i){let n=this.chart,o=this._cachedDataOpts,r=`animation-${e}`,a=o[r];if(a)return a;let l;if(n.options.animation!==!1){let h=this.chart.config,u=h.datasetAnimationScopeKeys(this._type,e),d=h.getOptionScopes(this.getDataset(),u);l=h.createResolver(d,this.getContext(t,i,e))}let c=new _i(n,l&&l.animations);return l&&l._cacheable&&(o[r]=Object.freeze(c)),c}getSharedOptions(t){if(t.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},t))}includeOptions(t,e){return!e||An(t)||this.chart._animationsDisabled}_getSharedOptions(t,e){let i=this.resolveDataElementOptions(t,e),n=this._sharedOptions,o=this.getSharedOptions(i),r=this.includeOptions(e,o)||o!==n;return this.updateSharedOptions(o,e,i),{sharedOptions:o,includeOptions:r}}updateElement(t,e,i,n){An(n)?Object.assign(t,i):this._resolveAnimations(e,n).update(t,i)}updateSharedOptions(t,e,i){t&&!An(e)&&this._resolveAnimations(void 0,e).update(t,i)}_setStyle(t,e,i,n){t.active=n;let o=this.getStyle(e,n);this._resolveAnimations(e,i,n).update(t,{options:!n&&this.getSharedOptions(o)||o})}removeHoverStyle(t,e,i){this._setStyle(t,i,"active",!1)}setHoverStyle(t,e,i){this._setStyle(t,i,"active",!0)}_removeDatasetHoverStyle(){let t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!1)}_setDatasetHoverStyle(){let t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!0)}_resyncElements(t){let e=this._data,i=this._cachedMeta.data;for(let[a,l,c]of this._syncList)this[a](l,c);this._syncList=[];let n=i.length,o=e.length,r=Math.min(o,n);r&&this.parse(0,r),o>n?this._insertElements(n,o-n,t):o{for(c.length+=e,a=c.length-1;a>=r;a--)c[a]=c[a-e]};for(l(o),a=t;an-o))}return s._cache.$bar}function nu(s){let t=s.iScale,e=iu(t,s.type),i=t._length,n,o,r,a,l=()=>{r===32767||r===-32768||(Ee(a)&&(i=Math.min(i,Math.abs(r-a)||i)),a=r)};for(n=0,o=e.length;n0?n[s-1]:null,a=sMath.abs(a)&&(l=a,c=r),t[e.axis]=c,t._custom={barStart:l,barEnd:c,start:n,end:o,min:r,max:a}}function Ca(s,t,e,i){return H(s)?au(s,t,e,i):t[e.axis]=e.parse(s,i),t}function Fr(s,t,e,i){let n=s.iScale,o=s.vScale,r=n.getLabels(),a=n===o,l=[],c,h,u,d;for(c=e,h=e+i;c=e?1:-1)}function cu(s){let t,e,i,n,o;return s.horizontal?(t=s.base>s.x,e="left",i="right"):(t=s.baseh.controller.options.grouped),o=i.options.stacked,r=[],a=this._cachedMeta.controller.getParsed(e),l=a&&a[i.axis],c=h=>{let u=h._parsed.find(f=>f[i.axis]===l),d=u&&u[h.vScale.axis];if(I(d)||isNaN(d))return!0};for(let h of n)if(!(e!==void 0&&c(h))&&((o===!1||r.indexOf(h.stack)===-1||o===void 0&&h.stack===void 0)&&r.push(h.stack),h.index===t))break;return r.length||r.push(void 0),r}_getStackCount(t){return this._getStacks(void 0,t).length}_getAxisCount(){return this._getAxis().length}getFirstScaleIdForIndexAxis(){let t=this.chart.scales,e=this.chart.options.indexAxis;return Object.keys(t).filter(i=>t[i].axis===e).shift()}_getAxis(){let t={},e=this.getFirstScaleIdForIndexAxis();for(let i of this.chart.data.datasets)t[P(this.chart.options.indexAxis==="x"?i.xAxisID:i.yAxisID,e)]=!0;return Object.keys(t)}_getStackIndex(t,e,i){let n=this._getStacks(t,i),o=e!==void 0?n.indexOf(e):-1;return o===-1?n.length-1:o}_getRuler(){let t=this.options,e=this._cachedMeta,i=e.iScale,n=[],o,r;for(o=0,r=e.data.length;o=0;--i)e=Math.max(e,t[i].size(this.resolveDataElementOptions(i))/2);return e>0&&e}getLabelAndValue(t){let e=this._cachedMeta,i=this.chart.data.labels||[],{xScale:n,yScale:o}=e,r=this.getParsed(t),a=n.getLabelForValue(r.x),l=o.getLabelForValue(r.y),c=r._custom;return{label:i[t]||"",value:"("+a+", "+l+(c?", "+c:"")+")"}}update(t){let e=this._cachedMeta.data;this.updateElements(e,0,e.length,t)}updateElements(t,e,i,n){let o=n==="reset",{iScale:r,vScale:a}=this._cachedMeta,{sharedOptions:l,includeOptions:c}=this._getSharedOptions(e,n),h=r.axis,u=a.axis;for(let d=e;dFe(_,a,l,!0)?1:Math.max(w,w*e,x,x*e),g=(_,w,x)=>Fe(_,a,l,!0)?-1:Math.min(w,w*e,x,x*e),m=f(0,c,u),p=f(q,h,d),b=g(F,c,u),y=g(F+q,h,d);i=(m-b)/2,n=(p-y)/2,o=-(m+b)/2,r=-(p+y)/2}return{ratioX:i,ratioY:n,offsetX:o,offsetY:r}}var jt=class extends ut{constructor(t,e){super(t,e),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(t,e){let i=this.getDataset().data,n=this._cachedMeta;if(this._parsing===!1)n._parsed=i;else{let o=l=>+i[l];if(E(i[t])){let{key:l="value"}=this._parsing;o=c=>+Wt(i[c],l)}let r,a;for(r=t,a=t+e;r0&&!isNaN(t)?$*(Math.abs(t)/e):0}getLabelAndValue(t){let e=this._cachedMeta,i=this.chart,n=i.data.labels||[],o=Re(e._parsed[t],i.options.locale);return{label:n[t]||"",value:o}}getMaxBorderWidth(t){let e=0,i=this.chart,n,o,r,a,l;if(!t){for(n=0,o=i.data.datasets.length;nt!=="spacing",_indexable:t=>t!=="spacing"&&!t.startsWith("borderDash")&&!t.startsWith("hoverBorderDash")}),v(jt,"overrides",{aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){let e=t.data,{labels:{pointStyle:i,textAlign:n,color:o,useBorderRadius:r,borderRadius:a}}=t.legend.options;return e.labels.length&&e.datasets.length?e.labels.map((l,c)=>{let u=t.getDatasetMeta(0).controller.getStyle(c);return{text:l,fillStyle:u.backgroundColor,fontColor:o,hidden:!t.getDataVisibility(c),lineDash:u.borderDash,lineDashOffset:u.borderDashOffset,lineJoin:u.borderJoinStyle,lineWidth:u.borderWidth,strokeStyle:u.borderColor,textAlign:n,pointStyle:i,borderRadius:r&&(a||u.borderRadius),index:c}}):[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}}});var He=class extends ut{initialize(){this.enableOptionSharing=!0,this.supportsDecimation=!0,super.initialize()}update(t){let e=this._cachedMeta,{dataset:i,data:n=[],_dataset:o}=e,r=this.chart._animationsDisabled,{start:a,count:l}=un(e,n,r);this._drawStart=a,this._drawCount=l,dn(e)&&(a=0,l=n.length),i._chart=this.chart,i._datasetIndex=this.index,i._decimated=!!o._decimated,i.points=n;let c=this.resolveDatasetElementOptions(t);this.options.showLine||(c.borderWidth=0),c.segment=this.options.segment,this.updateElement(i,void 0,{animated:!r,options:c},t),this.updateElements(n,a,l,t)}updateElements(t,e,i,n){let o=n==="reset",{iScale:r,vScale:a,_stacked:l,_dataset:c}=this._cachedMeta,{sharedOptions:h,includeOptions:u}=this._getSharedOptions(e,n),d=r.axis,f=a.axis,{spanGaps:g,segment:m}=this.options,p=fe(g)?g:Number.POSITIVE_INFINITY,b=this.chart._animationsDisabled||o||n==="none",y=e+i,_=t.length,w=e>0&&this.getParsed(e-1);for(let x=0;x<_;++x){let k=t[x],S=b?k:{};if(x=y){S.skip=!0;continue}let M=this.getParsed(x),T=I(M[f]),C=S[d]=r.getPixelForValue(M[d],x),A=S[f]=o||T?a.getBasePixel():a.getPixelForValue(l?this.applyStack(a,M,l):M[f],x);S.skip=isNaN(C)||isNaN(A)||T,S.stop=x>0&&Math.abs(M[d]-w[d])>p,m&&(S.parsed=M,S.raw=c.data[x]),u&&(S.options=h||this.resolveDataElementOptions(x,k.active?"active":n)),b||this.updateElement(k,x,S,n),w=M}}getMaxOverflow(){let t=this._cachedMeta,e=t.dataset,i=e.options&&e.options.borderWidth||0,n=t.data||[];if(!n.length)return i;let o=n[0].size(this.resolveDataElementOptions(0)),r=n[n.length-1].size(this.resolveDataElementOptions(n.length-1));return Math.max(i,o,r)/2}draw(){let t=this._cachedMeta;t.dataset.updateControlPoints(this.chart.chartArea,t.iScale.axis),super.draw()}};v(He,"id","line"),v(He,"defaults",{datasetElementType:"line",dataElementType:"point",showLine:!0,spanGaps:!1}),v(He,"overrides",{scales:{_index_:{type:"category"},_value_:{type:"linear"}}});var xe=class extends ut{constructor(t,e){super(t,e),this.innerRadius=void 0,this.outerRadius=void 0}getLabelAndValue(t){let e=this._cachedMeta,i=this.chart,n=i.data.labels||[],o=Re(e._parsed[t].r,i.options.locale);return{label:n[t]||"",value:o}}parseObjectData(t,e,i,n){return wn.bind(this)(t,e,i,n)}update(t){let e=this._cachedMeta.data;this._updateRadius(),this.updateElements(e,0,e.length,t)}getMinMax(){let t=this._cachedMeta,e={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY};return t.data.forEach((i,n)=>{let o=this.getParsed(n).r;!isNaN(o)&&this.chart.getDataVisibility(n)&&(oe.max&&(e.max=o))}),e}_updateRadius(){let t=this.chart,e=t.chartArea,i=t.options,n=Math.min(e.right-e.left,e.bottom-e.top),o=Math.max(n/2,0),r=Math.max(i.cutoutPercentage?o/100*i.cutoutPercentage:1,0),a=(o-r)/t.getVisibleDatasetCount();this.outerRadius=o-a*this.index,this.innerRadius=this.outerRadius-a}updateElements(t,e,i,n){let o=n==="reset",r=this.chart,l=r.options.animation,c=this._cachedMeta.rScale,h=c.xCenter,u=c.yCenter,d=c.getIndexAngle(0)-.5*F,f=d,g,m=360/this.countVisibleElements();for(g=0;g{!isNaN(this.getParsed(n).r)&&this.chart.getDataVisibility(n)&&e++}),e}_computeAngle(t,e,i){return this.chart.getDataVisibility(t)?bt(this.resolveDataElementOptions(t,e).angle||i):0}};v(xe,"id","polarArea"),v(xe,"defaults",{dataElementType:"arc",animation:{animateRotate:!0,animateScale:!0},animations:{numbers:{type:"number",properties:["x","y","startAngle","endAngle","innerRadius","outerRadius"]}},indexAxis:"r",startAngle:0}),v(xe,"overrides",{aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){let e=t.data;if(e.labels.length&&e.datasets.length){let{labels:{pointStyle:i,color:n}}=t.legend.options;return e.labels.map((o,r)=>{let l=t.getDatasetMeta(0).controller.getStyle(r);return{text:o,fillStyle:l.backgroundColor,strokeStyle:l.borderColor,fontColor:n,lineWidth:l.borderWidth,pointStyle:i,hidden:!t.getDataVisibility(r),index:r}})}return[]}},onClick(t,e,i){i.chart.toggleDataVisibility(e.index),i.chart.update()}}},scales:{r:{type:"radialLinear",angleLines:{display:!1},beginAtZero:!0,grid:{circular:!0},pointLabels:{display:!1},startAngle:0}}});var vs=class extends jt{};v(vs,"id","pie"),v(vs,"defaults",{cutout:0,rotation:0,circumference:360,radius:"100%"});var $e=class extends ut{getLabelAndValue(t){let e=this._cachedMeta.vScale,i=this.getParsed(t);return{label:e.getLabels()[t],value:""+e.getLabelForValue(i[e.axis])}}parseObjectData(t,e,i,n){return wn.bind(this)(t,e,i,n)}update(t){let e=this._cachedMeta,i=e.dataset,n=e.data||[],o=e.iScale.getLabels();if(i.points=n,t!=="resize"){let r=this.resolveDatasetElementOptions(t);this.options.showLine||(r.borderWidth=0);let a={_loop:!0,_fullLoop:o.length===n.length,options:r};this.updateElement(i,void 0,a,t)}this.updateElements(n,0,n.length,t)}updateElements(t,e,i,n){let o=this._cachedMeta.rScale,r=n==="reset";for(let a=e;a0&&this.getParsed(e-1);for(let w=e;w0&&Math.abs(k[f]-_[f])>b,p&&(S.parsed=k,S.raw=c.data[w]),d&&(S.options=u||this.resolveDataElementOptions(w,x.active?"active":n)),y||this.updateElement(x,w,S,n),_=k}this.updateSharedOptions(u,n,h)}getMaxOverflow(){let t=this._cachedMeta,e=t.data||[];if(!this.options.showLine){let a=0;for(let l=e.length-1;l>=0;--l)a=Math.max(a,e[l].size(this.resolveDataElementOptions(l))/2);return a>0&&a}let i=t.dataset,n=i.options&&i.options.borderWidth||0;if(!e.length)return n;let o=e[0].size(this.resolveDataElementOptions(0)),r=e[e.length-1].size(this.resolveDataElementOptions(e.length-1));return Math.max(n,o,r)/2}};v(je,"id","scatter"),v(je,"defaults",{datasetElementType:!1,dataElementType:"point",showLine:!1,fill:!1}),v(je,"overrides",{interaction:{mode:"point"},scales:{x:{type:"linear"},y:{type:"linear"}}});var gu=Object.freeze({__proto__:null,BarController:We,BubbleController:Be,DoughnutController:jt,LineController:He,PieController:vs,PolarAreaController:xe,RadarController:$e,ScatterController:je});function me(){throw new Error("This method is not implemented: Check that a complete date adapter is provided.")}var Hn=class s{constructor(t){v(this,"options");this.options=t||{}}static override(t){Object.assign(s.prototype,t)}init(){}formats(){return me()}parse(){return me()}format(){return me()}add(){return me()}diff(){return me()}startOf(){return me()}endOf(){return me()}},to={_date:Hn};function mu(s,t,e,i){let{controller:n,data:o,_sorted:r}=s,a=n._cachedMeta.iScale,l=s.dataset&&s.dataset.options?s.dataset.options.spanGaps:null;if(a&&t===a.axis&&t!=="r"&&r&&o.length){let c=a._reversePixels?ar:Ct;if(i){if(n._sharedOptions){let h=o[0],u=typeof h.getRange=="function"&&h.getRange(t);if(u){let d=c(o,t,e-u),f=c(o,t,e+u);return{lo:d.lo,hi:f.hi}}}}else{let h=c(o,t,e);if(l){let{vScale:u}=n._cachedMeta,{_parsed:d}=s,f=d.slice(0,h.lo+1).reverse().findIndex(m=>!I(m[u.axis]));h.lo-=Math.max(0,f);let g=d.slice(h.hi).findIndex(m=>!I(m[u.axis]));h.hi+=Math.max(0,g)}return h}}return{lo:0,hi:o.length-1}}function Ls(s,t,e,i,n){let o=s.getSortedVisibleDatasetMetas(),r=e[t];for(let a=0,l=o.length;a{l[r]&&l[r](t[e],n)&&(o.push({element:l,datasetIndex:c,index:h}),a=a||l.inRange(t.x,t.y,n))}),i&&!a?[]:o}var xu={evaluateInteractionItems:Ls,modes:{index(s,t,e,i){let n=ee(t,s),o=e.axis||"x",r=e.includeInvisible||!1,a=e.intersect?En(s,n,o,i,r):Ln(s,n,o,!1,i,r),l=[];return a.length?(s.getSortedVisibleDatasetMetas().forEach(c=>{let h=a[0].index,u=c.data[h];u&&!u.skip&&l.push({element:u,datasetIndex:c.index,index:h})}),l):[]},dataset(s,t,e,i){let n=ee(t,s),o=e.axis||"xy",r=e.includeInvisible||!1,a=e.intersect?En(s,n,o,i,r):Ln(s,n,o,!1,i,r);if(a.length>0){let l=a[0].datasetIndex,c=s.getDatasetMeta(l).data;a=[];for(let h=0;he.pos===t)}function Vr(s,t){return s.filter(e=>Pa.indexOf(e.pos)===-1&&e.box.axis===t)}function _s(s,t){return s.sort((e,i)=>{let n=t?i:e,o=t?e:i;return n.weight===o.weight?n.index-o.index:n.weight-o.weight})}function _u(s){let t=[],e,i,n,o,r,a;for(e=0,i=(s||[]).length;ec.box.fullSize),!0),i=_s(xs(t,"left"),!0),n=_s(xs(t,"right")),o=_s(xs(t,"top"),!0),r=_s(xs(t,"bottom")),a=Vr(t,"x"),l=Vr(t,"y");return{fullSize:e,leftAndTop:i.concat(o),rightAndBottom:n.concat(l).concat(r).concat(a),chartArea:xs(t,"chartArea"),vertical:i.concat(n).concat(l),horizontal:o.concat(r).concat(a)}}function Wr(s,t,e,i){return Math.max(s[e],t[e])+Math.max(s[i],t[i])}function Aa(s,t){s.top=Math.max(s.top,t.top),s.left=Math.max(s.left,t.left),s.bottom=Math.max(s.bottom,t.bottom),s.right=Math.max(s.right,t.right)}function Su(s,t,e,i){let{pos:n,box:o}=e,r=s.maxPadding;if(!E(n)){e.size&&(s[n]-=e.size);let u=i[e.stack]||{size:0,count:1};u.size=Math.max(u.size,e.horizontal?o.height:o.width),e.size=u.size/u.count,s[n]+=e.size}o.getPadding&&Aa(r,o.getPadding());let a=Math.max(0,t.outerWidth-Wr(r,s,"left","right")),l=Math.max(0,t.outerHeight-Wr(r,s,"top","bottom")),c=a!==s.w,h=l!==s.h;return s.w=a,s.h=l,e.horizontal?{same:c,other:h}:{same:h,other:c}}function Mu(s){let t=s.maxPadding;function e(i){let n=Math.max(t[i]-s[i],0);return s[i]+=n,n}s.y+=e("top"),s.x+=e("left"),e("right"),e("bottom")}function Ou(s,t){let e=t.maxPadding;function i(n){let o={left:0,top:0,right:0,bottom:0};return n.forEach(r=>{o[r]=Math.max(t[r],e[r])}),o}return i(s?["left","right"]:["top","bottom"])}function Ss(s,t,e,i){let n=[],o,r,a,l,c,h;for(o=0,r=s.length,c=0;o{typeof m.beforeLayout=="function"&&m.beforeLayout()});let h=l.reduce((m,p)=>p.box.options&&p.box.options.display===!1?m:m+1,0)||1,u=Object.freeze({outerWidth:t,outerHeight:e,padding:n,availableWidth:o,availableHeight:r,vBoxMaxWidth:o/2/h,hBoxMaxHeight:r/2}),d=Object.assign({},n);Aa(d,nt(i));let f=Object.assign({maxPadding:d,w:o,h:r,x:n.left,y:n.top},n),g=ku(l.concat(c),u);Ss(a.fullSize,f,u,g),Ss(l,f,u,g),Ss(c,f,u,g)&&Ss(l,f,u,g),Mu(f),Br(a.leftAndTop,f,u,g),f.x+=f.w,f.y+=f.h,Br(a.rightAndBottom,f,u,g),s.chartArea={left:f.left,top:f.top,right:f.left+f.w,bottom:f.top+f.h,height:f.h,width:f.w},z(a.chartArea,m=>{let p=m.box;Object.assign(p,s.chartArea),p.update(f.w,f.h,{left:0,top:0,right:0,bottom:0})})}},wi=class{acquireContext(t,e){}releaseContext(t){return!1}addEventListener(t,e,i){}removeEventListener(t,e,i){}getDevicePixelRatio(){return 1}getMaximumSize(t,e,i,n){return e=Math.max(0,e||t.width),i=i||t.height,{width:e,height:Math.max(0,n?Math.floor(e/n):i)}}isAttached(t){return!0}updateConfig(t){}},$n=class extends wi{acquireContext(t){return t&&t.getContext&&t.getContext("2d")||null}updateConfig(t){t.options.animation=!1}},yi="$chartjs",Tu={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},Hr=s=>s===null||s==="";function Du(s,t){let e=s.style,i=s.getAttribute("height"),n=s.getAttribute("width");if(s[yi]={initial:{height:i,width:n,style:{display:e.display,height:e.height,width:e.width}}},e.display=e.display||"block",e.boxSizing=e.boxSizing||"border-box",Hr(n)){let o=vn(s,"width");o!==void 0&&(s.width=o)}if(Hr(i))if(s.style.height==="")s.height=s.width/(t||2);else{let o=vn(s,"height");o!==void 0&&(s.height=o)}return s}var Ia=Sr?{passive:!0}:!1;function Cu(s,t,e){s&&s.addEventListener(t,e,Ia)}function Pu(s,t,e){s&&s.canvas&&s.canvas.removeEventListener(t,e,Ia)}function Au(s,t){let e=Tu[s.type]||s.type,{x:i,y:n}=ee(s,t);return{type:e,chart:t,native:s,x:i!==void 0?i:null,y:n!==void 0?n:null}}function ki(s,t){for(let e of s)if(e===t||e.contains(t))return!0}function Iu(s,t,e){let i=s.canvas,n=new MutationObserver(o=>{let r=!1;for(let a of o)r=r||ki(a.addedNodes,i),r=r&&!ki(a.removedNodes,i);r&&e()});return n.observe(document,{childList:!0,subtree:!0}),n}function Eu(s,t,e){let i=s.canvas,n=new MutationObserver(o=>{let r=!1;for(let a of o)r=r||ki(a.removedNodes,i),r=r&&!ki(a.addedNodes,i);r&&e()});return n.observe(document,{childList:!0,subtree:!0}),n}var As=new Map,$r=0;function Ea(){let s=window.devicePixelRatio;s!==$r&&($r=s,As.forEach((t,e)=>{e.currentDevicePixelRatio!==s&&t()}))}function Lu(s,t){As.size||window.addEventListener("resize",Ea),As.set(s,t)}function Fu(s){As.delete(s),As.size||window.removeEventListener("resize",Ea)}function Ru(s,t,e){let i=s.canvas,n=i&&hi(i);if(!n)return;let o=hn((a,l)=>{let c=n.clientWidth;e(a,l),c{let l=a[0],c=l.contentRect.width,h=l.contentRect.height;c===0&&h===0||o(c,h)});return r.observe(n),Lu(s,o),r}function Fn(s,t,e){e&&e.disconnect(),t==="resize"&&Fu(s)}function Nu(s,t,e){let i=s.canvas,n=hn(o=>{s.ctx!==null&&e(Au(o,s))},s);return Cu(i,t,n),n}var jn=class extends wi{acquireContext(t,e){let i=t&&t.getContext&&t.getContext("2d");return i&&i.canvas===t?(Du(t,e),i):null}releaseContext(t){let e=t.canvas;if(!e[yi])return!1;let i=e[yi].initial;["height","width"].forEach(o=>{let r=i[o];I(r)?e.removeAttribute(o):e.setAttribute(o,r)});let n=i.style||{};return Object.keys(n).forEach(o=>{e.style[o]=n[o]}),e.width=e.width,delete e[yi],!0}addEventListener(t,e,i){this.removeEventListener(t,e);let n=t.$proxies||(t.$proxies={}),r={attach:Iu,detach:Eu,resize:Ru}[e]||Nu;n[e]=r(t,e,i)}removeEventListener(t,e){let i=t.$proxies||(t.$proxies={}),n=i[e];if(!n)return;({attach:Fn,detach:Fn,resize:Fn}[e]||Pu)(t,e,n),i[e]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(t,e,i,n){return vr(t,e,i,n)}isAttached(t){let e=t&&hi(t);return!!(e&&e.isConnected)}};function zu(s){return!ci()||typeof OffscreenCanvas<"u"&&s instanceof OffscreenCanvas?$n:jn}var dt=class{constructor(){v(this,"x");v(this,"y");v(this,"active",!1);v(this,"options");v(this,"$animations")}tooltipPosition(t){let{x:e,y:i}=this.getProps(["x","y"],t);return{x:e,y:i}}hasValue(){return fe(this.x)&&fe(this.y)}getProps(t,e){let i=this.$animations;if(!e||!i)return this;let n={};return t.forEach(o=>{n[o]=i[o]&&i[o].active()?i[o]._to:this[o]}),n}};v(dt,"defaults",{}),v(dt,"defaultRoutes");function Vu(s,t){let e=s.options.ticks,i=Wu(s),n=Math.min(e.maxTicksLimit||i,i),o=e.major.enabled?Hu(t):[],r=o.length,a=o[0],l=o[r-1],c=[];if(r>n)return $u(t,c,o,r/n),c;let h=Bu(o,t,n);if(r>0){let u,d,f=r>1?Math.round((l-a)/(r-1)):null;for(fi(t,c,h,I(f)?0:a-f,a),u=0,d=r-1;un)return l}return Math.max(n,1)}function Hu(s){let t=[],e,i;for(e=0,i=s.length;es==="left"?"right":s==="right"?"left":s,jr=(s,t,e)=>t==="top"||t==="left"?s[t]+e:s[t]-e,Ur=(s,t)=>Math.min(t||s,s);function Yr(s,t){let e=[],i=s.length/t,n=s.length,o=0;for(;or+a)))return l}function Zu(s,t){z(s,e=>{let i=e.gc,n=i.length/2,o;if(n>t){for(o=0;oi?i:e,i=n&&e>i?e:i,{min:at(e,at(i,e)),max:at(i,at(e,i))}}getPadding(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){let t=this.chart.data;return this.options.labels||(this.isHorizontal()?t.xLabels:t.yLabels)||t.labels||[]}getLabelItems(t=this.chart.chartArea){return this._labelItems||(this._labelItems=this._computeLabelItems(t))}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){W(this.options.beforeUpdate,[this])}update(t,e,i){let{beginAtZero:n,grace:o,ticks:r}=this.options,a=r.sampleSize;this.beforeUpdate(),this.maxWidth=t,this.maxHeight=e,this._margins=i=Object.assign({left:0,right:0,top:0,bottom:0},i),this.ticks=null,this._labelSizes=null,this._gridLineItems=null,this._labelItems=null,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this._maxLength=this.isHorizontal()?this.width+i.left+i.right:this.height+i.top+i.bottom,this._dataLimitsCached||(this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this._range=br(this,o,n),this._dataLimitsCached=!0),this.beforeBuildTicks(),this.ticks=this.buildTicks()||[],this.afterBuildTicks();let l=a=o||i<=1||!this.isHorizontal()){this.labelRotation=n;return}let h=this._getLabelSizes(),u=h.widest.width,d=h.highest.height,f=K(this.chart.width-u,0,this.maxWidth);a=t.offset?this.maxWidth/i:f/(i-1),u+6>a&&(a=f/(i-(t.offset?.5:1)),l=this.maxHeight-ws(t.grid)-e.padding-Zr(t.title,this.chart.options.font),c=Math.sqrt(u*u+d*d),r=si(Math.min(Math.asin(K((h.highest.height+6)/a,-1,1)),Math.asin(K(l/c,-1,1))-Math.asin(K(d/c,-1,1)))),r=Math.max(n,Math.min(o,r))),this.labelRotation=r}afterCalculateLabelRotation(){W(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){W(this.options.beforeFit,[this])}fit(){let t={width:0,height:0},{chart:e,options:{ticks:i,title:n,grid:o}}=this,r=this._isVisible(),a=this.isHorizontal();if(r){let l=Zr(n,e.options.font);if(a?(t.width=this.maxWidth,t.height=ws(o)+l):(t.height=this.maxHeight,t.width=ws(o)+l),i.display&&this.ticks.length){let{first:c,last:h,widest:u,highest:d}=this._getLabelSizes(),f=i.padding*2,g=bt(this.labelRotation),m=Math.cos(g),p=Math.sin(g);if(a){let b=i.mirror?0:p*u.width+m*d.height;t.height=Math.min(this.maxHeight,t.height+b+f)}else{let b=i.mirror?0:m*u.width+p*d.height;t.width=Math.min(this.maxWidth,t.width+b+f)}this._calculatePadding(c,h,p,m)}}this._handleMargins(),a?(this.width=this._length=e.width-this._margins.left-this._margins.right,this.height=t.height):(this.width=t.width,this.height=this._length=e.height-this._margins.top-this._margins.bottom)}_calculatePadding(t,e,i,n){let{ticks:{align:o,padding:r},position:a}=this.options,l=this.labelRotation!==0,c=a!=="top"&&this.axis==="x";if(this.isHorizontal()){let h=this.getPixelForTick(0)-this.left,u=this.right-this.getPixelForTick(this.ticks.length-1),d=0,f=0;l?c?(d=n*t.width,f=i*e.height):(d=i*t.height,f=n*e.width):o==="start"?f=e.width:o==="end"?d=t.width:o!=="inner"&&(d=t.width/2,f=e.width/2),this.paddingLeft=Math.max((d-h+r)*this.width/(this.width-h),0),this.paddingRight=Math.max((f-u+r)*this.width/(this.width-u),0)}else{let h=e.height/2,u=t.height/2;o==="start"?(h=0,u=t.height):o==="end"&&(h=e.height,u=0),this.paddingTop=h+r,this.paddingBottom=u+r}}_handleMargins(){this._margins&&(this._margins.left=Math.max(this.paddingLeft,this._margins.left),this._margins.top=Math.max(this.paddingTop,this._margins.top),this._margins.right=Math.max(this.paddingRight,this._margins.right),this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom))}afterFit(){W(this.options.afterFit,[this])}isHorizontal(){let{axis:t,position:e}=this.options;return e==="top"||e==="bottom"||t==="x"}isFullSize(){return this.options.fullSize}_convertTicksToLabels(t){this.beforeTickToLabelConversion(),this.generateTickLabels(t);let e,i;for(e=0,i=t.length;e({width:r[T]||0,height:a[T]||0});return{first:M(0),last:M(e-1),widest:M(k),highest:M(S),widths:r,heights:a}}getLabelForValue(t){return t}getPixelForValue(t,e){return NaN}getValueForPixel(t){}getPixelForTick(t){let e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getPixelForDecimal(t){this._reversePixels&&(t=1-t);let e=this._startPixel+t*this._length;return rr(this._alignToPixels?Kt(this.chart,e,0):e)}getDecimalForPixel(t){let e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){let{min:t,max:e}=this;return t<0&&e<0?e:t>0&&e>0?t:0}getContext(t){let e=this.ticks||[];if(t>=0&&ta*n?a/i:l/n:l*n0}_computeGridLineItems(t){let e=this.axis,i=this.chart,n=this.options,{grid:o,position:r,border:a}=n,l=o.offset,c=this.isHorizontal(),u=this.ticks.length+(l?1:0),d=ws(o),f=[],g=a.setContext(this.getContext()),m=g.display?g.width:0,p=m/2,b=function(U){return Kt(i,U,m)},y,_,w,x,k,S,M,T,C,A,L,et;if(r==="top")y=b(this.bottom),S=this.bottom-d,T=y-p,A=b(t.top)+p,et=t.bottom;else if(r==="bottom")y=b(this.top),A=t.top,et=b(t.bottom)-p,S=y+p,T=this.top+d;else if(r==="left")y=b(this.right),k=this.right-d,M=y-p,C=b(t.left)+p,L=t.right;else if(r==="right")y=b(this.left),C=t.left,L=b(t.right)-p,k=y+p,M=this.left+d;else if(e==="x"){if(r==="center")y=b((t.top+t.bottom)/2+.5);else if(E(r)){let U=Object.keys(r)[0],G=r[U];y=b(this.chart.scales[U].getPixelForValue(G))}A=t.top,et=t.bottom,S=y+p,T=S+d}else if(e==="y"){if(r==="center")y=b((t.left+t.right)/2);else if(E(r)){let U=Object.keys(r)[0],G=r[U];y=b(this.chart.scales[U].getPixelForValue(G))}k=y-p,M=k-d,C=t.left,L=t.right}let ht=P(n.ticks.maxTicksLimit,u),V=Math.max(1,Math.ceil(u/ht));for(_=0;_0&&(ce-=le/2);break}js={left:ce,top:ls,width:le+Te.width,height:as+Te.height,color:V.backdropColor}}p.push({label:w,font:T,textOffset:L,options:{rotation:m,color:G,strokeColor:vt,strokeWidth:ot,textAlign:Oe,textBaseline:et,translation:[x,k],backdrop:js}})}return p}_getXAxisLabelAlignment(){let{position:t,ticks:e}=this.options;if(-bt(this.labelRotation))return t==="top"?"left":"right";let n="center";return e.align==="start"?n="left":e.align==="end"?n="right":e.align==="inner"&&(n="inner"),n}_getYAxisLabelAlignment(t){let{position:e,ticks:{crossAlign:i,mirror:n,padding:o}}=this.options,r=this._getLabelSizes(),a=t+o,l=r.widest.width,c,h;return e==="left"?n?(h=this.right+o,i==="near"?c="left":i==="center"?(c="center",h+=l/2):(c="right",h+=l)):(h=this.right-a,i==="near"?c="right":i==="center"?(c="center",h-=l/2):(c="left",h=this.left)):e==="right"?n?(h=this.left+o,i==="near"?c="right":i==="center"?(c="center",h-=l/2):(c="left",h-=l)):(h=this.left+a,i==="near"?c="left":i==="center"?(c="center",h+=l/2):(c="right",h=this.right)):c="right",{textAlign:c,x:h}}_computeLabelArea(){if(this.options.ticks.mirror)return;let t=this.chart,e=this.options.position;if(e==="left"||e==="right")return{top:0,left:this.left,bottom:t.height,right:this.right};if(e==="top"||e==="bottom")return{top:this.top,left:0,bottom:this.bottom,right:t.width}}drawBackground(){let{ctx:t,options:{backgroundColor:e},left:i,top:n,width:o,height:r}=this;e&&(t.save(),t.fillStyle=e,t.fillRect(i,n,o,r),t.restore())}getLineWidthForValue(t){let e=this.options.grid;if(!this._isVisible()||!e.display)return 0;let n=this.ticks.findIndex(o=>o.value===t);return n>=0?e.setContext(this.getContext(n)).lineWidth:0}drawGrid(t){let e=this.options.grid,i=this.ctx,n=this._gridLineItems||(this._gridLineItems=this._computeGridLineItems(t)),o,r,a=(l,c,h)=>{!h.width||!h.color||(i.save(),i.lineWidth=h.width,i.strokeStyle=h.color,i.setLineDash(h.borderDash||[]),i.lineDashOffset=h.borderDashOffset,i.beginPath(),i.moveTo(l.x,l.y),i.lineTo(c.x,c.y),i.stroke(),i.restore())};if(e.display)for(o=0,r=n.length;o{this.draw(o)}}]:[{z:i,draw:o=>{this.drawBackground(),this.drawGrid(o),this.drawTitle()}},{z:n,draw:()=>{this.drawBorder()}},{z:e,draw:o=>{this.drawLabels(o)}}]}getMatchingVisibleMetas(t){let e=this.chart.getSortedVisibleDatasetMetas(),i=this.axis+"AxisID",n=[],o,r;for(o=0,r=e.length;o{let i=e.split("."),n=i.pop(),o=[s].concat(i).join("."),r=t[e].split("."),a=r.pop(),l=r.join(".");j.route(o,n,l,a)})}function td(s){return"id"in s&&"defaults"in s}var Un=class{constructor(){this.controllers=new Ze(ut,"datasets",!0),this.elements=new Ze(dt,"elements"),this.plugins=new Ze(Object,"plugins"),this.scales=new Ze(we,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...t){this._each("register",t)}remove(...t){this._each("unregister",t)}addControllers(...t){this._each("register",t,this.controllers)}addElements(...t){this._each("register",t,this.elements)}addPlugins(...t){this._each("register",t,this.plugins)}addScales(...t){this._each("register",t,this.scales)}getController(t){return this._get(t,this.controllers,"controller")}getElement(t){return this._get(t,this.elements,"element")}getPlugin(t){return this._get(t,this.plugins,"plugin")}getScale(t){return this._get(t,this.scales,"scale")}removeControllers(...t){this._each("unregister",t,this.controllers)}removeElements(...t){this._each("unregister",t,this.elements)}removePlugins(...t){this._each("unregister",t,this.plugins)}removeScales(...t){this._each("unregister",t,this.scales)}_each(t,e,i){[...e].forEach(n=>{let o=i||this._getRegistryForType(n);i||o.isForType(n)||o===this.plugins&&n.id?this._exec(t,o,n):z(n,r=>{let a=i||this._getRegistryForType(r);this._exec(t,a,r)})})}_exec(t,e,i){let n=ei(t);W(i["before"+n],[],i),e[t](i),W(i["after"+n],[],i)}_getRegistryForType(t){for(let e=0;eo.filter(a=>!r.some(l=>a.plugin.id===l.plugin.id));this._notify(n(e,i),t,"stop"),this._notify(n(i,e),t,"start")}};function ed(s){let t={},e=[],i=Object.keys(Lt.plugins.items);for(let o=0;o1&&qr(s[0].toLowerCase());if(i)return i}throw new Error(`Cannot determine type of '${s}' axis. Please provide 'axis' or 'position' option.`)}function Gr(s,t,e){if(e[t+"AxisID"]===s)return{axis:t}}function ld(s,t){if(t.data&&t.data.datasets){let e=t.data.datasets.filter(i=>i.xAxisID===s||i.yAxisID===s);if(e.length)return Gr(s,"x",e[0])||Gr(s,"y",e[0])}return{}}function cd(s,t){let e=Jt[s.type]||{scales:{}},i=t.scales||{},n=Zn(s.type,t),o=Object.create(null);return Object.keys(i).forEach(r=>{let a=i[r];if(!E(a))return console.error(`Invalid scale configuration for scale: ${r}`);if(a._proxy)return console.warn(`Ignoring resolver passed as options for scale: ${r}`);let l=qn(r,a,ld(r,s),j.scales[a.type]),c=rd(l,n),h=e.scales||{};o[r]=Ie(Object.create(null),[{axis:l},a,h[l],h[c]])}),s.data.datasets.forEach(r=>{let a=r.type||s.type,l=r.indexAxis||Zn(a,t),h=(Jt[a]||{}).scales||{};Object.keys(h).forEach(u=>{let d=od(u,l),f=r[d+"AxisID"]||d;o[f]=o[f]||Object.create(null),Ie(o[f],[{axis:d},i[f],h[u]])})}),Object.keys(o).forEach(r=>{let a=o[r];Ie(a,[j.scales[a.type],j.scale])}),o}function La(s){let t=s.options||(s.options={});t.plugins=P(t.plugins,{}),t.scales=cd(s,t)}function Fa(s){return s=s||{},s.datasets=s.datasets||[],s.labels=s.labels||[],s}function hd(s){return s=s||{},s.data=Fa(s.data),La(s),s}var Xr=new Map,Ra=new Set;function gi(s,t){let e=Xr.get(s);return e||(e=t(),Xr.set(s,e),Ra.add(e)),e}var ks=(s,t,e)=>{let i=Wt(t,e);i!==void 0&&s.add(i)},Gn=class{constructor(t){this._config=hd(t),this._scopeCache=new Map,this._resolverCache=new Map}get platform(){return this._config.platform}get type(){return this._config.type}set type(t){this._config.type=t}get data(){return this._config.data}set data(t){this._config.data=Fa(t)}get options(){return this._config.options}set options(t){this._config.options=t}get plugins(){return this._config.plugins}update(){let t=this._config;this.clearCache(),La(t)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(t){return gi(t,()=>[[`datasets.${t}`,""]])}datasetAnimationScopeKeys(t,e){return gi(`${t}.transition.${e}`,()=>[[`datasets.${t}.transitions.${e}`,`transitions.${e}`],[`datasets.${t}`,""]])}datasetElementScopeKeys(t,e){return gi(`${t}-${e}`,()=>[[`datasets.${t}.elements.${e}`,`datasets.${t}`,`elements.${e}`,""]])}pluginScopeKeys(t){let e=t.id,i=this.type;return gi(`${i}-plugin-${e}`,()=>[[`plugins.${e}`,...t.additionalOptionScopes||[]]])}_cachedScopes(t,e){let i=this._scopeCache,n=i.get(t);return(!n||e)&&(n=new Map,i.set(t,n)),n}getOptionScopes(t,e,i){let{options:n,type:o}=this,r=this._cachedScopes(t,i),a=r.get(e);if(a)return a;let l=new Set;e.forEach(h=>{t&&(l.add(t),h.forEach(u=>ks(l,t,u))),h.forEach(u=>ks(l,n,u)),h.forEach(u=>ks(l,Jt[o]||{},u)),h.forEach(u=>ks(l,j,u)),h.forEach(u=>ks(l,oi,u))});let c=Array.from(l);return c.length===0&&c.push(Object.create(null)),Ra.has(e)&&r.set(e,c),c}chartOptionScopes(){let{options:t,type:e}=this;return[t,Jt[e]||{},j.datasets[e]||{},{type:e},j,oi]}resolveNamedOptions(t,e,i,n=[""]){let o={$shared:!0},{resolver:r,subPrefixes:a}=Jr(this._resolverCache,t,n),l=r;if(dd(r,e)){o.$shared=!1,i=zt(i)?i():i;let c=this.createResolver(t,i,a);l=de(r,i,c)}for(let c of e)o[c]=l[c];return o}createResolver(t,e,i=[""],n){let{resolver:o}=Jr(this._resolverCache,t,i);return E(e)?de(o,e,void 0,n):o}};function Jr(s,t,e){let i=s.get(t);i||(i=new Map,s.set(t,i));let n=e.join(),o=i.get(n);return o||(o={resolver:li(t,e),subPrefixes:e.filter(a=>!a.toLowerCase().includes("hover"))},i.set(n,o)),o}var ud=s=>E(s)&&Object.getOwnPropertyNames(s).some(t=>zt(s[t]));function dd(s,t){let{isScriptable:e,isIndexable:i}=yn(s);for(let n of t){let o=e(n),r=i(n),a=(r||o)&&s[n];if(o&&(zt(a)||ud(a))||r&&H(a))return!0}return!1}var fd="4.5.1",gd=["top","bottom","left","right","chartArea"];function Kr(s,t){return s==="top"||s==="bottom"||gd.indexOf(s)===-1&&t==="x"}function Qr(s,t){return function(e,i){return e[s]===i[s]?e[t]-i[t]:e[s]-i[s]}}function ta(s){let t=s.chart,e=t.options.animation;t.notifyPlugins("afterRender"),W(e&&e.onComplete,[s],t)}function md(s){let t=s.chart,e=t.options.animation;W(e&&e.onProgress,[s],t)}function Na(s){return ci()&&typeof s=="string"?s=document.getElementById(s):s&&s.length&&(s=s[0]),s&&s.canvas&&(s=s.canvas),s}var xi={},ea=s=>{let t=Na(s);return Object.values(xi).filter(e=>e.canvas===t).pop()};function pd(s,t,e){let i=Object.keys(s);for(let n of i){let o=+n;if(o>=t){let r=s[n];delete s[n],(e>0||o>t)&&(s[o+e]=r)}}}function bd(s,t,e,i){return!e||s.type==="mouseout"?null:i?t:s}var yt=class{static register(...t){Lt.add(...t),sa()}static unregister(...t){Lt.remove(...t),sa()}constructor(t,e){let i=this.config=new Gn(e),n=Na(t),o=ea(n);if(o)throw new Error("Canvas is already in use. Chart with ID '"+o.id+"' must be destroyed before the canvas with ID '"+o.canvas.id+"' can be reused.");let r=i.createResolver(i.chartOptionScopes(),this.getContext());this.platform=new(i.platform||zu(n)),this.platform.updateConfig(i);let a=this.platform.acquireContext(n,r.aspectRatio),l=a&&a.canvas,c=l&&l.height,h=l&&l.width;if(this.id=tr(),this.ctx=a,this.canvas=l,this.width=h,this.height=c,this._options=r,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this._plugins=new Yn,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=ur(u=>this.update(u),r.resizeDelay||0),this._dataChanges=[],xi[this.id]=this,!a||!l){console.error("Failed to create chart: can't acquire context from the given item");return}Ht.listen(this,"complete",ta),Ht.listen(this,"progress",md),this._initialize(),this.attached&&this.update()}get aspectRatio(){let{options:{aspectRatio:t,maintainAspectRatio:e},width:i,height:n,_aspectRatio:o}=this;return I(t)?e&&o?o:n?i/n:null:t}get data(){return this.config.data}set data(t){this.config.data=t}get options(){return this._options}set options(t){this.config.options=t}get registry(){return Lt}_initialize(){return this.notifyPlugins("beforeInit"),this.options.responsive?this.resize():kn(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins("afterInit"),this}clear(){return mn(this.canvas,this.ctx),this}stop(){return Ht.stop(this),this}resize(t,e){Ht.running(this)?this._resizeBeforeDraw={width:t,height:e}:this._resize(t,e)}_resize(t,e){let i=this.options,n=this.canvas,o=i.maintainAspectRatio&&this.aspectRatio,r=this.platform.getMaximumSize(n,t,e,o),a=i.devicePixelRatio||this.platform.getDevicePixelRatio(),l=this.width?"resize":"attach";this.width=r.width,this.height=r.height,this._aspectRatio=this.aspectRatio,kn(this,a,!0)&&(this.notifyPlugins("resize",{size:r}),W(i.onResize,[this,r],this),this.attached&&this._doResize(l)&&this.render())}ensureScalesHaveIDs(){let e=this.options.scales||{};z(e,(i,n)=>{i.id=n})}buildOrUpdateScales(){let t=this.options,e=t.scales,i=this.scales,n=Object.keys(i).reduce((r,a)=>(r[a]=!1,r),{}),o=[];e&&(o=o.concat(Object.keys(e).map(r=>{let a=e[r],l=qn(r,a),c=l==="r",h=l==="x";return{options:a,dposition:c?"chartArea":h?"bottom":"left",dtype:c?"radialLinear":h?"category":"linear"}}))),z(o,r=>{let a=r.options,l=a.id,c=qn(l,a),h=P(a.type,r.dtype);(a.position===void 0||Kr(a.position,c)!==Kr(r.dposition))&&(a.position=r.dposition),n[l]=!0;let u=null;if(l in i&&i[l].type===h)u=i[l];else{let d=Lt.getScale(h);u=new d({id:l,type:h,ctx:this.ctx,chart:this}),i[u.id]=u}u.init(a,t)}),z(n,(r,a)=>{r||delete i[a]}),z(i,r=>{rt.configure(this,r,r.options),rt.addBox(this,r)})}_updateMetasets(){let t=this._metasets,e=this.data.datasets.length,i=t.length;if(t.sort((n,o)=>n.index-o.index),i>e){for(let n=e;ne.length&&delete this._stacks,t.forEach((i,n)=>{e.filter(o=>o===i._dataset).length===0&&this._destroyDatasetMeta(n)})}buildOrUpdateControllers(){let t=[],e=this.data.datasets,i,n;for(this._removeUnreferencedMetasets(),i=0,n=e.length;i{this.getDatasetMeta(e).controller.reset()},this)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(t){let e=this.config;e.update();let i=this._options=e.createResolver(e.chartOptionScopes(),this.getContext()),n=this._animationsDisabled=!i.animation;if(this._updateScales(),this._checkEventBindings(),this._updateHiddenIndices(),this._plugins.invalidate(),this.notifyPlugins("beforeUpdate",{mode:t,cancelable:!0})===!1)return;let o=this.buildOrUpdateControllers();this.notifyPlugins("beforeElementsUpdate");let r=0;for(let c=0,h=this.data.datasets.length;c{c.reset()}),this._updateDatasets(t),this.notifyPlugins("afterUpdate",{mode:t}),this._layers.sort(Qr("z","_idx"));let{_active:a,_lastEvent:l}=this;l?this._eventHandler(l,!0):a.length&&this._updateHoverStyles(a,a,!0),this.render()}_updateScales(){z(this.scales,t=>{rt.removeBox(this,t)}),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){let t=this.options,e=new Set(Object.keys(this._listeners)),i=new Set(t.events);(!en(e,i)||!!this._responsiveListeners!==t.responsive)&&(this.unbindEvents(),this.bindEvents())}_updateHiddenIndices(){let{_hiddenIndices:t}=this,e=this._getUniformDataChanges()||[];for(let{method:i,start:n,count:o}of e){let r=i==="_removeElements"?-o:o;pd(t,n,r)}}_getUniformDataChanges(){let t=this._dataChanges;if(!t||!t.length)return;this._dataChanges=[];let e=this.data.datasets.length,i=o=>new Set(t.filter(r=>r[0]===o).map((r,a)=>a+","+r.splice(1).join(","))),n=i(0);for(let o=1;oo.split(",")).map(o=>({method:o[1],start:+o[2],count:+o[3]}))}_updateLayout(t){if(this.notifyPlugins("beforeLayout",{cancelable:!0})===!1)return;rt.update(this,this.width,this.height,t);let e=this.chartArea,i=e.width<=0||e.height<=0;this._layers=[],z(this.boxes,n=>{i&&n.position==="chartArea"||(n.configure&&n.configure(),this._layers.push(...n._layers()))},this),this._layers.forEach((n,o)=>{n._idx=o}),this.notifyPlugins("afterLayout")}_updateDatasets(t){if(this.notifyPlugins("beforeDatasetsUpdate",{mode:t,cancelable:!0})!==!1){for(let e=0,i=this.data.datasets.length;e=0;--e)this._drawDataset(t[e]);this.notifyPlugins("afterDatasetsDraw")}_drawDataset(t){let e=this.ctx,i={meta:t,index:t.index,cancelable:!0},n=Dn(this,t);this.notifyPlugins("beforeDatasetDraw",i)!==!1&&(n&&ps(e,n),t.controller.draw(),n&&bs(e),i.cancelable=!1,this.notifyPlugins("afterDatasetDraw",i))}isPointInArea(t){return Pt(t,this.chartArea,this._minPadding)}getElementsAtEventForMode(t,e,i,n){let o=xu.modes[e];return typeof o=="function"?o(this,t,i,n):[]}getDatasetMeta(t){let e=this.data.datasets[t],i=this._metasets,n=i.filter(o=>o&&o._dataset===e).pop();return n||(n={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e&&e.order||0,index:t,_dataset:e,_parsed:[],_sorted:!1},i.push(n)),n}getContext(){return this.$context||(this.$context=Bt(null,{chart:this,type:"chart"}))}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(t){let e=this.data.datasets[t];if(!e)return!1;let i=this.getDatasetMeta(t);return typeof i.hidden=="boolean"?!i.hidden:!e.hidden}setDatasetVisibility(t,e){let i=this.getDatasetMeta(t);i.hidden=!e}toggleDataVisibility(t){this._hiddenIndices[t]=!this._hiddenIndices[t]}getDataVisibility(t){return!this._hiddenIndices[t]}_updateVisibility(t,e,i){let n=i?"show":"hide",o=this.getDatasetMeta(t),r=o.controller._resolveAnimations(void 0,n);Ee(e)?(o.data[e].hidden=!i,this.update()):(this.setDatasetVisibility(t,i),r.update(o,{visible:i}),this.update(a=>a.datasetIndex===t?n:void 0))}hide(t,e){this._updateVisibility(t,e,!1)}show(t,e){this._updateVisibility(t,e,!0)}_destroyDatasetMeta(t){let e=this._metasets[t];e&&e.controller&&e.controller._destroy(),delete this._metasets[t]}_stop(){let t,e;for(this.stop(),Ht.remove(this),t=0,e=this.data.datasets.length;t{e.addEventListener(this,o,r),t[o]=r},n=(o,r,a)=>{o.offsetX=r,o.offsetY=a,this._eventHandler(o)};z(this.options.events,o=>i(o,n))}bindResponsiveEvents(){this._responsiveListeners||(this._responsiveListeners={});let t=this._responsiveListeners,e=this.platform,i=(l,c)=>{e.addEventListener(this,l,c),t[l]=c},n=(l,c)=>{t[l]&&(e.removeEventListener(this,l,c),delete t[l])},o=(l,c)=>{this.canvas&&this.resize(l,c)},r,a=()=>{n("attach",a),this.attached=!0,this.resize(),i("resize",o),i("detach",r)};r=()=>{this.attached=!1,n("resize",o),this._stop(),this._resize(0,0),i("attach",a)},e.isAttached(this.canvas)?a():r()}unbindEvents(){z(this._listeners,(t,e)=>{this.platform.removeEventListener(this,e,t)}),this._listeners={},z(this._responsiveListeners,(t,e)=>{this.platform.removeEventListener(this,e,t)}),this._responsiveListeners=void 0}updateHoverStyle(t,e,i){let n=i?"set":"remove",o,r,a,l;for(e==="dataset"&&(o=this.getDatasetMeta(t[0].datasetIndex),o.controller["_"+n+"DatasetHoverStyle"]()),a=0,l=t.length;a{let a=this.getDatasetMeta(o);if(!a)throw new Error("No dataset found at index "+o);return{datasetIndex:o,element:a.data[r],index:r}});!gs(i,e)&&(this._active=i,this._lastEvent=null,this._updateHoverStyles(i,e))}notifyPlugins(t,e,i){return this._plugins.notify(this,t,e,i)}isPluginEnabled(t){return this._plugins._cache.filter(e=>e.plugin.id===t).length===1}_updateHoverStyles(t,e,i){let n=this.options.hover,o=(l,c)=>l.filter(h=>!c.some(u=>h.datasetIndex===u.datasetIndex&&h.index===u.index)),r=o(e,t),a=i?t:o(t,e);r.length&&this.updateHoverStyle(r,n.mode,!1),a.length&&n.mode&&this.updateHoverStyle(a,n.mode,!0)}_eventHandler(t,e){let i={event:t,replay:e,cancelable:!0,inChartArea:this.isPointInArea(t)},n=r=>(r.options.events||this.options.events).includes(t.native.type);if(this.notifyPlugins("beforeEvent",i,n)===!1)return;let o=this._handleEvent(t,e,i.inChartArea);return i.cancelable=!1,this.notifyPlugins("afterEvent",i,n),(o||i.changed)&&this.render(),this}_handleEvent(t,e,i){let{_active:n=[],options:o}=this,r=e,a=this._getActiveElements(t,n,i,r),l=ir(t),c=bd(t,this._lastEvent,i,l);i&&(this._lastEvent=null,W(o.onHover,[t,a,this],this),l&&W(o.onClick,[t,a,this],this));let h=!gs(a,n);return(h||e)&&(this._active=a,this._updateHoverStyles(a,n,e)),this._lastEvent=c,h}_getActiveElements(t,e,i,n){if(t.type==="mouseout")return[];if(!i)return e;let o=this.options.hover;return this.getElementsAtEventForMode(t,o.mode,o,n)}};v(yt,"defaults",j),v(yt,"instances",xi),v(yt,"overrides",Jt),v(yt,"registry",Lt),v(yt,"version",fd),v(yt,"getChart",ea);function sa(){return z(yt.instances,s=>s._plugins.invalidate())}function yd(s,t,e){let{startAngle:i,x:n,y:o,outerRadius:r,innerRadius:a,options:l}=t,{borderWidth:c,borderJoinStyle:h}=l,u=Math.min(c/r,st(i-e));if(s.beginPath(),s.arc(n,o,r-c/2,i+u/2,e-u/2),a>0){let d=Math.min(c/a,st(i-e));s.arc(n,o,a+c/2,e-d/2,i+d/2,!0)}else{let d=Math.min(c/2,r*st(i-e));if(h==="round")s.arc(n,o,d,e-F/2,i+F/2,!0);else if(h==="bevel"){let f=2*d*d,g=-f*Math.cos(e+F/2)+n,m=-f*Math.sin(e+F/2)+o,p=f*Math.cos(i+F/2)+n,b=f*Math.sin(i+F/2)+o;s.lineTo(g,m),s.lineTo(p,b)}}s.closePath(),s.moveTo(0,0),s.rect(0,0,s.canvas.width,s.canvas.height),s.clip("evenodd")}function xd(s,t,e){let{startAngle:i,pixelMargin:n,x:o,y:r,outerRadius:a,innerRadius:l}=t,c=n/a;s.beginPath(),s.arc(o,r,a,i-c,e+c),l>n?(c=n/l,s.arc(o,r,l,e+c,i-c,!0)):s.arc(o,r,n,e+q,i-q),s.closePath(),s.clip()}function _d(s){return ai(s,["outerStart","outerEnd","innerStart","innerEnd"])}function wd(s,t,e,i){let n=_d(s.options.borderRadius),o=(e-t)/2,r=Math.min(o,i*t/2),a=l=>{let c=(e-Math.min(o,l))*i/2;return K(l,0,Math.min(o,c))};return{outerStart:a(n.outerStart),outerEnd:a(n.outerEnd),innerStart:K(n.innerStart,0,r),innerEnd:K(n.innerEnd,0,r)}}function Ve(s,t,e,i){return{x:e+s*Math.cos(t),y:i+s*Math.sin(t)}}function vi(s,t,e,i,n,o){let{x:r,y:a,startAngle:l,pixelMargin:c,innerRadius:h}=t,u=Math.max(t.outerRadius+i+e-c,0),d=h>0?h+i+e+c:0,f=0,g=n-l;if(i){let V=h>0?h-i:0,U=u>0?u-i:0,G=(V+U)/2,vt=G!==0?g*G/(G+i):g;f=(g-vt)/2}let m=Math.max(.001,g*u-e/F)/u,p=(g-m)/2,b=l+p+f,y=n-p-f,{outerStart:_,outerEnd:w,innerStart:x,innerEnd:k}=wd(t,d,u,y-b),S=u-_,M=u-w,T=b+_/S,C=y-w/M,A=d+x,L=d+k,et=b+x/A,ht=y-k/L;if(s.beginPath(),o){let V=(T+C)/2;if(s.arc(r,a,u,T,V),s.arc(r,a,u,V,C),w>0){let ot=Ve(M,C,r,a);s.arc(ot.x,ot.y,w,C,y+q)}let U=Ve(L,y,r,a);if(s.lineTo(U.x,U.y),k>0){let ot=Ve(L,ht,r,a);s.arc(ot.x,ot.y,k,y+q,ht+Math.PI)}let G=(y-k/d+(b+x/d))/2;if(s.arc(r,a,d,y-k/d,G,!0),s.arc(r,a,d,G,b+x/d,!0),x>0){let ot=Ve(A,et,r,a);s.arc(ot.x,ot.y,x,et+Math.PI,b-q)}let vt=Ve(S,b,r,a);if(s.lineTo(vt.x,vt.y),_>0){let ot=Ve(S,T,r,a);s.arc(ot.x,ot.y,_,b-q,T)}}else{s.moveTo(r,a);let V=Math.cos(T)*u+r,U=Math.sin(T)*u+a;s.lineTo(V,U);let G=Math.cos(C)*u+r,vt=Math.sin(C)*u+a;s.lineTo(G,vt)}s.closePath()}function kd(s,t,e,i,n){let{fullCircles:o,startAngle:r,circumference:a}=t,l=t.endAngle;if(o){vi(s,t,e,i,l,n);for(let c=0;c=F&&f===0&&h!=="miter"&&yd(s,t,m),o||(vi(s,t,e,i,m,n),s.stroke())}var be=class extends dt{constructor(e){super();v(this,"circumference");v(this,"endAngle");v(this,"fullCircles");v(this,"innerRadius");v(this,"outerRadius");v(this,"pixelMargin");v(this,"startAngle");this.options=void 0,this.circumference=void 0,this.startAngle=void 0,this.endAngle=void 0,this.innerRadius=void 0,this.outerRadius=void 0,this.pixelMargin=0,this.fullCircles=0,e&&Object.assign(this,e)}inRange(e,i,n){let o=this.getProps(["x","y"],n),{angle:r,distance:a}=rn(o,{x:e,y:i}),{startAngle:l,endAngle:c,innerRadius:h,outerRadius:u,circumference:d}=this.getProps(["startAngle","endAngle","innerRadius","outerRadius","circumference"],n),f=(this.options.spacing+this.options.borderWidth)/2,g=P(d,c-l),m=Fe(r,l,c)&&l!==c,p=g>=$||m,b=It(a,h+f,u+f);return p&&b}getCenterPoint(e){let{x:i,y:n,startAngle:o,endAngle:r,innerRadius:a,outerRadius:l}=this.getProps(["x","y","startAngle","endAngle","innerRadius","outerRadius"],e),{offset:c,spacing:h}=this.options,u=(o+r)/2,d=(a+l+h+c)/2;return{x:i+Math.cos(u)*d,y:n+Math.sin(u)*d}}tooltipPosition(e){return this.getCenterPoint(e)}draw(e){let{options:i,circumference:n}=this,o=(i.offset||0)/4,r=(i.spacing||0)/2,a=i.circular;if(this.pixelMargin=i.borderAlign==="inner"?.33:0,this.fullCircles=n>$?Math.floor(n/$):0,n===0||this.innerRadius<0||this.outerRadius<0)return;e.save();let l=(this.startAngle+this.endAngle)/2;e.translate(Math.cos(l)*o,Math.sin(l)*o);let c=1-Math.sin(Math.min(F,n||0)),h=o*c;e.fillStyle=i.backgroundColor,e.strokeStyle=i.borderColor,kd(e,this,h,r,a),vd(e,this,h,r,a),e.restore()}};v(be,"id","arc"),v(be,"defaults",{borderAlign:"center",borderColor:"#fff",borderDash:[],borderDashOffset:0,borderJoinStyle:void 0,borderRadius:0,borderWidth:2,offset:0,spacing:0,angle:void 0,circular:!0,selfJoin:!1}),v(be,"defaultRoutes",{backgroundColor:"backgroundColor"}),v(be,"descriptors",{_scriptable:!0,_indexable:e=>e!=="borderDash"});function za(s,t,e=t){s.lineCap=P(e.borderCapStyle,t.borderCapStyle),s.setLineDash(P(e.borderDash,t.borderDash)),s.lineDashOffset=P(e.borderDashOffset,t.borderDashOffset),s.lineJoin=P(e.borderJoinStyle,t.borderJoinStyle),s.lineWidth=P(e.borderWidth,t.borderWidth),s.strokeStyle=P(e.borderColor,t.borderColor)}function Sd(s,t,e){s.lineTo(e.x,e.y)}function Md(s){return s.stepped?mr:s.tension||s.cubicInterpolationMode==="monotone"?pr:Sd}function Va(s,t,e={}){let i=s.length,{start:n=0,end:o=i-1}=e,{start:r,end:a}=t,l=Math.max(n,r),c=Math.min(o,a),h=na&&o>a;return{count:i,start:l,loop:t.loop,ilen:c(r+(c?a-w:w))%o,_=()=>{m!==p&&(s.lineTo(h,p),s.lineTo(h,m),s.lineTo(h,b))};for(l&&(f=n[y(0)],s.moveTo(f.x,f.y)),d=0;d<=a;++d){if(f=n[y(d)],f.skip)continue;let w=f.x,x=f.y,k=w|0;k===g?(xp&&(p=x),h=(u*h+w)/++u):(_(),s.lineTo(w,x),g=k,u=0,m=p=x),b=x}_()}function Xn(s){let t=s.options,e=t.borderDash&&t.borderDash.length;return!s._decimated&&!s._loop&&!t.tension&&t.cubicInterpolationMode!=="monotone"&&!t.stepped&&!e?Td:Od}function Dd(s){return s.stepped?Mr:s.tension||s.cubicInterpolationMode==="monotone"?Or:Gt}function Cd(s,t,e,i){let n=t._path;n||(n=t._path=new Path2D,t.path(n,e,i)&&n.closePath()),za(s,t.options),s.stroke(n)}function Pd(s,t,e,i){let{segments:n,options:o}=t,r=Xn(t);for(let a of n)za(s,o,a.style),s.beginPath(),r(s,t,a,{start:e,end:e+i-1})&&s.closePath(),s.stroke()}var Ad=typeof Path2D=="function";function Id(s,t,e,i){Ad&&!t.options.segment?Cd(s,t,e,i):Pd(s,t,e,i)}var Ft=class extends dt{constructor(t){super(),this.animated=!0,this.options=void 0,this._chart=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,this._datasetIndex=void 0,t&&Object.assign(this,t)}updateControlPoints(t,e){let i=this.options;if((i.tension||i.cubicInterpolationMode==="monotone")&&!i.stepped&&!this._pointsUpdated){let n=i.spanGaps?this._loop:this._fullLoop;kr(this._points,i,t,n,e),this._pointsUpdated=!0}}set points(t){this._points=t,delete this._segments,delete this._path,this._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=Dr(this,this.options.segment))}first(){let t=this.segments,e=this.points;return t.length&&e[t[0].start]}last(){let t=this.segments,e=this.points,i=t.length;return i&&e[t[i-1].end]}interpolate(t,e){let i=this.options,n=t[e],o=this.points,r=Tn(this,{property:e,start:n,end:n});if(!r.length)return;let a=[],l=Dd(i),c,h;for(c=0,h=r.length;ct!=="borderDash"&&t!=="fill"});function ia(s,t,e,i){let n=s.options,{[e]:o}=s.getProps([e],i);return Math.abs(t-o)s.replace("rgb(","rgba(").replace(")",", 0.5)"));function Ba(s){return Jn[s%Jn.length]}function Ha(s){return na[s%na.length]}function Vd(s,t){return s.borderColor=Ba(t),s.backgroundColor=Ha(t),++t}function Wd(s,t){return s.backgroundColor=s.data.map(()=>Ba(t++)),t}function Bd(s,t){return s.backgroundColor=s.data.map(()=>Ha(t++)),t}function Hd(s){let t=0;return(e,i)=>{let n=s.getDatasetMeta(i).controller;n instanceof jt?t=Wd(e,t):n instanceof xe?t=Bd(e,t):n&&(t=Vd(e,t))}}function oa(s){let t;for(t in s)if(s[t].borderColor||s[t].backgroundColor)return!0;return!1}function $d(s){return s&&(s.borderColor||s.backgroundColor)}function jd(){return j.borderColor!=="rgba(0,0,0,0.1)"||j.backgroundColor!=="rgba(0,0,0,0.1)"}var Ud={id:"colors",defaults:{enabled:!0,forceOverride:!1},beforeLayout(s,t,e){if(!e.enabled)return;let{data:{datasets:i},options:n}=s.config,{elements:o}=n,r=oa(i)||$d(n)||o&&oa(o)||jd();if(!e.forceOverride&&r)return;let a=Hd(s);i.forEach(a)}};function Yd(s,t,e,i,n){let o=n.samples||i;if(o>=e)return s.slice(t,t+e);let r=[],a=(e-2)/(o-2),l=0,c=t+e-1,h=t,u,d,f,g,m;for(r[l++]=s[h],u=0;uf&&(f=g,d=s[y],m=y);r[l++]=d,h=m}return r[l++]=s[c],r}function Zd(s,t,e,i){let n=0,o=0,r,a,l,c,h,u,d,f,g,m,p=[],b=t+e-1,y=s[t].x,w=s[b].x-y;for(r=t;rm&&(m=c,d=r),n=(o*n+a.x)/++o;else{let k=r-1;if(!I(u)&&!I(d)){let S=Math.min(u,d),M=Math.max(u,d);S!==f&&S!==k&&p.push({...s[S],x:n}),M!==f&&M!==k&&p.push({...s[M],x:n})}r>0&&k!==f&&p.push(s[k]),p.push(a),h=x,o=0,g=m=c,u=d=f=r}}return p}function $a(s){if(s._decimated){let t=s._data;delete s._decimated,delete s._data,Object.defineProperty(s,"data",{configurable:!0,enumerable:!0,writable:!0,value:t})}}function ra(s){s.data.datasets.forEach(t=>{$a(t)})}function qd(s,t){let e=t.length,i=0,n,{iScale:o}=s,{min:r,max:a,minDefined:l,maxDefined:c}=o.getUserBounds();return l&&(i=K(Ct(t,o.axis,r).lo,0,e-1)),c?n=K(Ct(t,o.axis,a).hi+1,i,e)-i:n=e-i,{start:i,count:n}}var Gd={id:"decimation",defaults:{algorithm:"min-max",enabled:!1},beforeElementsUpdate:(s,t,e)=>{if(!e.enabled){ra(s);return}let i=s.width;s.data.datasets.forEach((n,o)=>{let{_data:r,indexAxis:a}=n,l=s.getDatasetMeta(o),c=r||n.data;if(ze([a,s.options.indexAxis])==="y"||!l.controller.supportsDecimation)return;let h=s.scales[l.xAxisID];if(h.type!=="linear"&&h.type!=="time"||s.options.parsing)return;let{start:u,count:d}=qd(l,c),f=e.threshold||4*i;if(d<=f){$a(n);return}I(r)&&(n._data=c,delete n.data,Object.defineProperty(n,"data",{configurable:!0,enumerable:!0,get:function(){return this._decimated},set:function(m){this._data=m}}));let g;switch(e.algorithm){case"lttb":g=Yd(c,u,d,i,e);break;case"min-max":g=Zd(c,u,d,i);break;default:throw new Error(`Unsupported decimation algorithm '${e.algorithm}'`)}n._decimated=g})},destroy(s){ra(s)}};function Xd(s,t,e){let i=s.segments,n=s.points,o=t.points,r=[];for(let a of i){let{start:l,end:c}=a;c=Oi(l,c,n);let h=Kn(e,n[l],n[c],a.loop);if(!t.segments){r.push({source:a,target:h,start:n[l],end:n[c]});continue}let u=Tn(t,h);for(let d of u){let f=Kn(e,o[d.start],o[d.end],d.loop),g=On(a,n,f);for(let m of g)r.push({source:m,target:d,start:{[e]:aa(h,f,"start",Math.max)},end:{[e]:aa(h,f,"end",Math.min)}})}}return r}function Kn(s,t,e,i){if(i)return;let n=t[s],o=e[s];return s==="angle"&&(n=st(n),o=st(o)),{property:s,start:n,end:o}}function Jd(s,t){let{x:e=null,y:i=null}=s||{},n=t.points,o=[];return t.segments.forEach(({start:r,end:a})=>{a=Oi(r,a,n);let l=n[r],c=n[a];i!==null?(o.push({x:l.x,y:i}),o.push({x:c.x,y:i})):e!==null&&(o.push({x:e,y:l.y}),o.push({x:e,y:c.y}))}),o}function Oi(s,t,e){for(;t>s;t--){let i=e[t];if(!isNaN(i.x)&&!isNaN(i.y))break}return t}function aa(s,t,e,i){return s&&t?i(s[e],t[e]):s?s[e]:t?t[e]:0}function ja(s,t){let e=[],i=!1;return H(s)?(i=!0,e=s):e=Jd(s,t),e.length?new Ft({points:e,options:{tension:0},_loop:i,_fullLoop:i}):null}function la(s){return s&&s.fill!==!1}function Kd(s,t,e){let n=s[t].fill,o=[t],r;if(!e)return n;for(;n!==!1&&o.indexOf(n)===-1;){if(!Z(n))return n;if(r=s[n],!r)return!1;if(r.visible)return n;o.push(n),n=r.fill}return!1}function Qd(s,t,e){let i=nf(s);if(E(i))return isNaN(i.value)?!1:i;let n=parseFloat(i);return Z(n)&&Math.floor(n)===n?tf(i[0],t,n,e):["origin","start","end","stack","shape"].indexOf(i)>=0&&i}function tf(s,t,e,i){return(s==="-"||s==="+")&&(e=t+e),e===t||e<0||e>=i?!1:e}function ef(s,t){let e=null;return s==="start"?e=t.bottom:s==="end"?e=t.top:E(s)?e=t.getPixelForValue(s.value):t.getBasePixel&&(e=t.getBasePixel()),e}function sf(s,t,e){let i;return s==="start"?i=e:s==="end"?i=t.options.reverse?t.min:t.max:E(s)?i=s.value:i=t.getBaseValue(),i}function nf(s){let t=s.options,e=t.fill,i=P(e&&e.target,e);return i===void 0&&(i=!!t.backgroundColor),i===!1||i===null?!1:i===!0?"origin":i}function of(s){let{scale:t,index:e,line:i}=s,n=[],o=i.segments,r=i.points,a=rf(t,e);a.push(ja({x:null,y:t.bottom},i));for(let l=0;l=0;--r){let a=n[r].$filler;a&&(a.line.updateControlPoints(o,a.axis),i&&a.fill&&zn(s.ctx,a,o))}},beforeDatasetsDraw(s,t,e){if(e.drawTime!=="beforeDatasetsDraw")return;let i=s.getSortedVisibleDatasetMetas();for(let n=i.length-1;n>=0;--n){let o=i[n].$filler;la(o)&&zn(s.ctx,o,s.chartArea)}},beforeDatasetDraw(s,t,e){let i=t.meta.$filler;!la(i)||e.drawTime!=="beforeDatasetDraw"||zn(s.ctx,i,s.chartArea)},defaults:{propagate:!0,drawTime:"beforeDatasetDraw"}},da=(s,t)=>{let{boxHeight:e=t,boxWidth:i=t}=s;return s.usePointStyle&&(e=Math.min(e,t),i=s.pointStyleWidth||Math.min(i,t)),{boxWidth:i,boxHeight:e,itemHeight:Math.max(t,e)}},bf=(s,t)=>s!==null&&t!==null&&s.datasetIndex===t.datasetIndex&&s.index===t.index,Mi=class extends dt{constructor(t){super(),this._added=!1,this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1,this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this.legendItems=void 0,this.columnSizes=void 0,this.lineWidths=void 0,this.maxHeight=void 0,this.maxWidth=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.height=void 0,this.width=void 0,this._margins=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e,i){this.maxWidth=t,this.maxHeight=e,this._margins=i,this.setDimensions(),this.buildLabels(),this.fit()}setDimensions(){this.isHorizontal()?(this.width=this.maxWidth,this.left=this._margins.left,this.right=this.width):(this.height=this.maxHeight,this.top=this._margins.top,this.bottom=this.height)}buildLabels(){let t=this.options.labels||{},e=W(t.generateLabels,[this.chart],this)||[];t.filter&&(e=e.filter(i=>t.filter(i,this.chart.data))),t.sort&&(e=e.sort((i,n)=>t.sort(i,n,this.chart.data))),this.options.reverse&&e.reverse(),this.legendItems=e}fit(){let{options:t,ctx:e}=this;if(!t.display){this.width=this.height=0;return}let i=t.labels,n=X(i.font),o=n.size,r=this._computeTitleHeight(),{boxWidth:a,itemHeight:l}=da(i,o),c,h;e.font=n.string,this.isHorizontal()?(c=this.maxWidth,h=this._fitRows(r,o,a,l)+10):(h=this.maxHeight,c=this._fitCols(r,n,a,l)+10),this.width=Math.min(c,t.maxWidth||this.maxWidth),this.height=Math.min(h,t.maxHeight||this.maxHeight)}_fitRows(t,e,i,n){let{ctx:o,maxWidth:r,options:{labels:{padding:a}}}=this,l=this.legendHitBoxes=[],c=this.lineWidths=[0],h=n+a,u=t;o.textAlign="left",o.textBaseline="middle";let d=-1,f=-h;return this.legendItems.forEach((g,m)=>{let p=i+e/2+o.measureText(g.text).width;(m===0||c[c.length-1]+p+2*a>r)&&(u+=h,c[c.length-(m>0?0:1)]=0,f+=h,d++),l[m]={left:0,top:f,row:d,width:p,height:n},c[c.length-1]+=p+a}),u}_fitCols(t,e,i,n){let{ctx:o,maxHeight:r,options:{labels:{padding:a}}}=this,l=this.legendHitBoxes=[],c=this.columnSizes=[],h=r-t,u=a,d=0,f=0,g=0,m=0;return this.legendItems.forEach((p,b)=>{let{itemWidth:y,itemHeight:_}=yf(i,e,o,p,n);b>0&&f+_+2*a>h&&(u+=d+a,c.push({width:d,height:f}),g+=d+a,m++,d=f=0),l[b]={left:g,top:f,col:m,width:y,height:_},d=Math.max(d,y),f+=_+a}),u+=d,c.push({width:d,height:f}),u}adjustHitBoxes(){if(!this.options.display)return;let t=this._computeTitleHeight(),{legendHitBoxes:e,options:{align:i,labels:{padding:n},rtl:o}}=this,r=ge(o,this.left,this.width);if(this.isHorizontal()){let a=0,l=it(i,this.left+n,this.right-this.lineWidths[a]);for(let c of e)a!==c.row&&(a=c.row,l=it(i,this.left+n,this.right-this.lineWidths[a])),c.top+=this.top+t+n,c.left=r.leftForLtr(r.x(l),c.width),l+=c.width+n}else{let a=0,l=it(i,this.top+t+n,this.bottom-this.columnSizes[a].height);for(let c of e)c.col!==a&&(a=c.col,l=it(i,this.top+t+n,this.bottom-this.columnSizes[a].height)),c.top=l,c.left+=this.left+n,c.left=r.leftForLtr(r.x(c.left),c.width),l+=c.height+n}}isHorizontal(){return this.options.position==="top"||this.options.position==="bottom"}draw(){if(this.options.display){let t=this.ctx;ps(t,this),this._draw(),bs(t)}}_draw(){let{options:t,columnSizes:e,lineWidths:i,ctx:n}=this,{align:o,labels:r}=t,a=j.color,l=ge(t.rtl,this.left,this.width),c=X(r.font),{padding:h}=r,u=c.size,d=u/2,f;this.drawTitle(),n.textAlign=l.textAlign("left"),n.textBaseline="middle",n.lineWidth=.5,n.font=c.string;let{boxWidth:g,boxHeight:m,itemHeight:p}=da(r,u),b=function(k,S,M){if(isNaN(g)||g<=0||isNaN(m)||m<0)return;n.save();let T=P(M.lineWidth,1);if(n.fillStyle=P(M.fillStyle,a),n.lineCap=P(M.lineCap,"butt"),n.lineDashOffset=P(M.lineDashOffset,0),n.lineJoin=P(M.lineJoin,"miter"),n.lineWidth=T,n.strokeStyle=P(M.strokeStyle,a),n.setLineDash(P(M.lineDash,[])),r.usePointStyle){let C={radius:m*Math.SQRT2/2,pointStyle:M.pointStyle,rotation:M.rotation,borderWidth:T},A=l.xPlus(k,g/2),L=S+d;pn(n,C,A,L,r.pointStyleWidth&&g)}else{let C=S+Math.max((u-m)/2,0),A=l.leftForLtr(k,g),L=te(M.borderRadius);n.beginPath(),Object.values(L).some(et=>et!==0)?Ne(n,{x:A,y:C,w:g,h:m,radius:L}):n.rect(A,C,g,m),n.fill(),T!==0&&n.stroke()}n.restore()},y=function(k,S,M){Qt(n,M.text,k,S+p/2,c,{strikethrough:M.hidden,textAlign:l.textAlign(M.textAlign)})},_=this.isHorizontal(),w=this._computeTitleHeight();_?f={x:it(o,this.left+h,this.right-i[0]),y:this.top+h+w,line:0}:f={x:this.left+h,y:it(o,this.top+w+h,this.bottom-e[0].height),line:0},Sn(this.ctx,t.textDirection);let x=p+h;this.legendItems.forEach((k,S)=>{n.strokeStyle=k.fontColor,n.fillStyle=k.fontColor;let M=n.measureText(k.text).width,T=l.textAlign(k.textAlign||(k.textAlign=r.textAlign)),C=g+d+M,A=f.x,L=f.y;l.setWidth(this.width),_?S>0&&A+C+h>this.right&&(L=f.y+=x,f.line++,A=f.x=it(o,this.left+h,this.right-i[f.line])):S>0&&L+x>this.bottom&&(A=f.x=A+e[f.line].width+h,f.line++,L=f.y=it(o,this.top+w+h,this.bottom-e[f.line].height));let et=l.x(A);if(b(et,L,k),A=dr(T,A+g+d,_?A+C:this.right,t.rtl),y(l.x(A),L,k),_)f.x+=C+h;else if(typeof k.text!="string"){let ht=c.lineHeight;f.y+=Ua(k,ht)+h}else f.y+=x}),Mn(this.ctx,t.textDirection)}drawTitle(){let t=this.options,e=t.title,i=X(e.font),n=nt(e.padding);if(!e.display)return;let o=ge(t.rtl,this.left,this.width),r=this.ctx,a=e.position,l=i.size/2,c=n.top+l,h,u=this.left,d=this.width;if(this.isHorizontal())d=Math.max(...this.lineWidths),h=this.top+c,u=it(t.align,u,this.right-d);else{let g=this.columnSizes.reduce((m,p)=>Math.max(m,p.height),0);h=c+it(t.align,this.top,this.bottom-g-t.labels.padding-this._computeTitleHeight())}let f=it(a,u,u+d);r.textAlign=o.textAlign(ni(a)),r.textBaseline="middle",r.strokeStyle=e.color,r.fillStyle=e.color,r.font=i.string,Qt(r,e.text,f,h,i)}_computeTitleHeight(){let t=this.options.title,e=X(t.font),i=nt(t.padding);return t.display?e.lineHeight+i.height:0}_getLegendItemAt(t,e){let i,n,o;if(It(t,this.left,this.right)&&It(e,this.top,this.bottom)){for(o=this.legendHitBoxes,i=0;io.length>r.length?o:r)),t+e.size/2+i.measureText(n).width}function _f(s,t,e){let i=s;return typeof t.text!="string"&&(i=Ua(t,e)),i}function Ua(s,t){let e=s.text?s.text.length:0;return t*e}function wf(s,t){return!!((s==="mousemove"||s==="mouseout")&&(t.onHover||t.onLeave)||t.onClick&&(s==="click"||s==="mouseup"))}var kf={id:"legend",_element:Mi,start(s,t,e){let i=s.legend=new Mi({ctx:s.ctx,options:e,chart:s});rt.configure(s,i,e),rt.addBox(s,i)},stop(s){rt.removeBox(s,s.legend),delete s.legend},beforeUpdate(s,t,e){let i=s.legend;rt.configure(s,i,e),i.options=e},afterUpdate(s){let t=s.legend;t.buildLabels(),t.adjustHitBoxes()},afterEvent(s,t){t.replay||s.legend.handleEvent(t.event)},defaults:{display:!0,position:"top",align:"center",fullSize:!0,reverse:!1,weight:1e3,onClick(s,t,e){let i=t.datasetIndex,n=e.chart;n.isDatasetVisible(i)?(n.hide(i),t.hidden=!0):(n.show(i),t.hidden=!1)},onHover:null,onLeave:null,labels:{color:s=>s.chart.options.color,boxWidth:40,padding:10,generateLabels(s){let t=s.data.datasets,{labels:{usePointStyle:e,pointStyle:i,textAlign:n,color:o,useBorderRadius:r,borderRadius:a}}=s.legend.options;return s._getSortedDatasetMetas().map(l=>{let c=l.controller.getStyle(e?0:void 0),h=nt(c.borderWidth);return{text:t[l.index].label,fillStyle:c.backgroundColor,fontColor:o,hidden:!l.visible,lineCap:c.borderCapStyle,lineDash:c.borderDash,lineDashOffset:c.borderDashOffset,lineJoin:c.borderJoinStyle,lineWidth:(h.width+h.height)/4,strokeStyle:c.borderColor,pointStyle:i||c.pointStyle,rotation:c.rotation,textAlign:n||c.textAlign,borderRadius:r&&(a||c.borderRadius),datasetIndex:l.index}},this)}},title:{color:s=>s.chart.options.color,display:!1,position:"center",text:""}},descriptors:{_scriptable:s=>!s.startsWith("on"),labels:{_scriptable:s=>!["generateLabels","filter","sort"].includes(s)}}},Is=class extends dt{constructor(t){super(),this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this._padding=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e){let i=this.options;if(this.left=0,this.top=0,!i.display){this.width=this.height=this.right=this.bottom=0;return}this.width=this.right=t,this.height=this.bottom=e;let n=H(i.text)?i.text.length:1;this._padding=nt(i.padding);let o=n*X(i.font).lineHeight+this._padding.height;this.isHorizontal()?this.height=o:this.width=o}isHorizontal(){let t=this.options.position;return t==="top"||t==="bottom"}_drawArgs(t){let{top:e,left:i,bottom:n,right:o,options:r}=this,a=r.align,l=0,c,h,u;return this.isHorizontal()?(h=it(a,i,o),u=e+t,c=o-i):(r.position==="left"?(h=i+t,u=it(a,n,e),l=F*-.5):(h=o-t,u=it(a,e,n),l=F*.5),c=n-e),{titleX:h,titleY:u,maxWidth:c,rotation:l}}draw(){let t=this.ctx,e=this.options;if(!e.display)return;let i=X(e.font),o=i.lineHeight/2+this._padding.top,{titleX:r,titleY:a,maxWidth:l,rotation:c}=this._drawArgs(o);Qt(t,e.text,0,0,i,{color:e.color,maxWidth:l,rotation:c,textAlign:ni(e.align),textBaseline:"middle",translation:[r,a]})}};function vf(s,t){let e=new Is({ctx:s.ctx,options:t,chart:s});rt.configure(s,e,t),rt.addBox(s,e),s.titleBlock=e}var Sf={id:"title",_element:Is,start(s,t,e){vf(s,e)},stop(s){let t=s.titleBlock;rt.removeBox(s,t),delete s.titleBlock},beforeUpdate(s,t,e){let i=s.titleBlock;rt.configure(s,i,e),i.options=e},defaults:{align:"center",display:!1,font:{weight:"bold"},fullSize:!0,padding:10,position:"top",text:"",weight:2e3},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}},mi=new WeakMap,Mf={id:"subtitle",start(s,t,e){let i=new Is({ctx:s.ctx,options:e,chart:s});rt.configure(s,i,e),rt.addBox(s,i),mi.set(s,i)},stop(s){rt.removeBox(s,mi.get(s)),mi.delete(s)},beforeUpdate(s,t,e){let i=mi.get(s);rt.configure(s,i,e),i.options=e},defaults:{align:"center",display:!1,font:{weight:"normal"},fullSize:!0,padding:0,position:"top",text:"",weight:1500},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}},Ms={average(s){if(!s.length)return!1;let t,e,i=new Set,n=0,o=0;for(t=0,e=s.length;ta+l)/i.size,y:n/o}},nearest(s,t){if(!s.length)return!1;let e=t.x,i=t.y,n=Number.POSITIVE_INFINITY,o,r,a;for(o=0,r=s.length;o-1?s.split(` +`):s}function Of(s,t){let{element:e,datasetIndex:i,index:n}=t,o=s.getDatasetMeta(i).controller,{label:r,value:a}=o.getLabelAndValue(n);return{chart:s,label:r,parsed:o.getParsed(n),raw:s.data.datasets[i].data[n],formattedValue:a,dataset:o.getDataset(),dataIndex:n,datasetIndex:i,element:e}}function fa(s,t){let e=s.chart.ctx,{body:i,footer:n,title:o}=s,{boxWidth:r,boxHeight:a}=t,l=X(t.bodyFont),c=X(t.titleFont),h=X(t.footerFont),u=o.length,d=n.length,f=i.length,g=nt(t.padding),m=g.height,p=0,b=i.reduce((w,x)=>w+x.before.length+x.lines.length+x.after.length,0);if(b+=s.beforeBody.length+s.afterBody.length,u&&(m+=u*c.lineHeight+(u-1)*t.titleSpacing+t.titleMarginBottom),b){let w=t.displayColors?Math.max(a,l.lineHeight):l.lineHeight;m+=f*w+(b-f)*l.lineHeight+(b-1)*t.bodySpacing}d&&(m+=t.footerMarginTop+d*h.lineHeight+(d-1)*t.footerSpacing);let y=0,_=function(w){p=Math.max(p,e.measureText(w).width+y)};return e.save(),e.font=c.string,z(s.title,_),e.font=l.string,z(s.beforeBody.concat(s.afterBody),_),y=t.displayColors?r+2+t.boxPadding:0,z(i,w=>{z(w.before,_),z(w.lines,_),z(w.after,_)}),y=0,e.font=h.string,z(s.footer,_),e.restore(),p+=g.width,{width:p,height:m}}function Tf(s,t){let{y:e,height:i}=t;return es.height-i/2?"bottom":"center"}function Df(s,t,e,i){let{x:n,width:o}=i,r=e.caretSize+e.caretPadding;if(s==="left"&&n+o+r>t.width||s==="right"&&n-o-r<0)return!0}function Cf(s,t,e,i){let{x:n,width:o}=e,{width:r,chartArea:{left:a,right:l}}=s,c="center";return i==="center"?c=n<=(a+l)/2?"left":"right":n<=o/2?c="left":n>=r-o/2&&(c="right"),Df(c,s,t,e)&&(c="center"),c}function ga(s,t,e){let i=e.yAlign||t.yAlign||Tf(s,e);return{xAlign:e.xAlign||t.xAlign||Cf(s,t,e,i),yAlign:i}}function Pf(s,t){let{x:e,width:i}=s;return t==="right"?e-=i:t==="center"&&(e-=i/2),e}function Af(s,t,e){let{y:i,height:n}=s;return t==="top"?i+=e:t==="bottom"?i-=n+e:i-=n/2,i}function ma(s,t,e,i){let{caretSize:n,caretPadding:o,cornerRadius:r}=s,{xAlign:a,yAlign:l}=e,c=n+o,{topLeft:h,topRight:u,bottomLeft:d,bottomRight:f}=te(r),g=Pf(t,a),m=Af(t,l,c);return l==="center"?a==="left"?g+=c:a==="right"&&(g-=c):a==="left"?g-=Math.max(h,d)+n:a==="right"&&(g+=Math.max(u,f)+n),{x:K(g,0,i.width-t.width),y:K(m,0,i.height-t.height)}}function pi(s,t,e){let i=nt(e.padding);return t==="center"?s.x+s.width/2:t==="right"?s.x+s.width-i.right:s.x+i.left}function pa(s){return Et([],$t(s))}function If(s,t,e){return Bt(s,{tooltip:t,tooltipItems:e,type:"tooltip"})}function ba(s,t){let e=t&&t.dataset&&t.dataset.tooltip&&t.dataset.tooltip.callbacks;return e?s.override(e):s}var Ya={beforeTitle:At,title(s){if(s.length>0){let t=s[0],e=t.chart.data.labels,i=e?e.length:0;if(this&&this.options&&this.options.mode==="dataset")return t.dataset.label||"";if(t.label)return t.label;if(i>0&&t.dataIndex"u"?Ya[t].call(e,i):n}var Ps=class extends dt{constructor(t){super(),this.opacity=0,this._active=[],this._eventPosition=void 0,this._size=void 0,this._cachedAnimations=void 0,this._tooltipItems=[],this.$animations=void 0,this.$context=void 0,this.chart=t.chart,this.options=t.options,this.dataPoints=void 0,this.title=void 0,this.beforeBody=void 0,this.body=void 0,this.afterBody=void 0,this.footer=void 0,this.xAlign=void 0,this.yAlign=void 0,this.x=void 0,this.y=void 0,this.height=void 0,this.width=void 0,this.caretX=void 0,this.caretY=void 0,this.labelColors=void 0,this.labelPointStyles=void 0,this.labelTextColors=void 0}initialize(t){this.options=t,this._cachedAnimations=void 0,this.$context=void 0}_resolveAnimations(){let t=this._cachedAnimations;if(t)return t;let e=this.chart,i=this.options.setContext(this.getContext()),n=i.enabled&&e.options.animation&&i.animations,o=new _i(this.chart,n);return n._cacheable&&(this._cachedAnimations=Object.freeze(o)),o}getContext(){return this.$context||(this.$context=If(this.chart.getContext(),this,this._tooltipItems))}getTitle(t,e){let{callbacks:i}=e,n=lt(i,"beforeTitle",this,t),o=lt(i,"title",this,t),r=lt(i,"afterTitle",this,t),a=[];return a=Et(a,$t(n)),a=Et(a,$t(o)),a=Et(a,$t(r)),a}getBeforeBody(t,e){return pa(lt(e.callbacks,"beforeBody",this,t))}getBody(t,e){let{callbacks:i}=e,n=[];return z(t,o=>{let r={before:[],lines:[],after:[]},a=ba(i,o);Et(r.before,$t(lt(a,"beforeLabel",this,o))),Et(r.lines,lt(a,"label",this,o)),Et(r.after,$t(lt(a,"afterLabel",this,o))),n.push(r)}),n}getAfterBody(t,e){return pa(lt(e.callbacks,"afterBody",this,t))}getFooter(t,e){let{callbacks:i}=e,n=lt(i,"beforeFooter",this,t),o=lt(i,"footer",this,t),r=lt(i,"afterFooter",this,t),a=[];return a=Et(a,$t(n)),a=Et(a,$t(o)),a=Et(a,$t(r)),a}_createItems(t){let e=this._active,i=this.chart.data,n=[],o=[],r=[],a=[],l,c;for(l=0,c=e.length;lt.filter(h,u,d,i))),t.itemSort&&(a=a.sort((h,u)=>t.itemSort(h,u,i))),z(a,h=>{let u=ba(t.callbacks,h);n.push(lt(u,"labelColor",this,h)),o.push(lt(u,"labelPointStyle",this,h)),r.push(lt(u,"labelTextColor",this,h))}),this.labelColors=n,this.labelPointStyles=o,this.labelTextColors=r,this.dataPoints=a,a}update(t,e){let i=this.options.setContext(this.getContext()),n=this._active,o,r=[];if(!n.length)this.opacity!==0&&(o={opacity:0});else{let a=Ms[i.position].call(this,n,this._eventPosition);r=this._createItems(i),this.title=this.getTitle(r,i),this.beforeBody=this.getBeforeBody(r,i),this.body=this.getBody(r,i),this.afterBody=this.getAfterBody(r,i),this.footer=this.getFooter(r,i);let l=this._size=fa(this,i),c=Object.assign({},a,l),h=ga(this.chart,i,c),u=ma(i,c,h,this.chart);this.xAlign=h.xAlign,this.yAlign=h.yAlign,o={opacity:1,x:u.x,y:u.y,width:l.width,height:l.height,caretX:a.x,caretY:a.y}}this._tooltipItems=r,this.$context=void 0,o&&this._resolveAnimations().update(this,o),t&&i.external&&i.external.call(this,{chart:this.chart,tooltip:this,replay:e})}drawCaret(t,e,i,n){let o=this.getCaretPosition(t,i,n);e.lineTo(o.x1,o.y1),e.lineTo(o.x2,o.y2),e.lineTo(o.x3,o.y3)}getCaretPosition(t,e,i){let{xAlign:n,yAlign:o}=this,{caretSize:r,cornerRadius:a}=i,{topLeft:l,topRight:c,bottomLeft:h,bottomRight:u}=te(a),{x:d,y:f}=t,{width:g,height:m}=e,p,b,y,_,w,x;return o==="center"?(w=f+m/2,n==="left"?(p=d,b=p-r,_=w+r,x=w-r):(p=d+g,b=p+r,_=w-r,x=w+r),y=p):(n==="left"?b=d+Math.max(l,h)+r:n==="right"?b=d+g-Math.max(c,u)-r:b=this.caretX,o==="top"?(_=f,w=_-r,p=b-r,y=b+r):(_=f+m,w=_+r,p=b+r,y=b-r),x=_),{x1:p,x2:b,x3:y,y1:_,y2:w,y3:x}}drawTitle(t,e,i){let n=this.title,o=n.length,r,a,l;if(o){let c=ge(i.rtl,this.x,this.width);for(t.x=pi(this,i.titleAlign,i),e.textAlign=c.textAlign(i.titleAlign),e.textBaseline="middle",r=X(i.titleFont),a=i.titleSpacing,e.fillStyle=i.titleColor,e.font=r.string,l=0;ly!==0)?(t.beginPath(),t.fillStyle=o.multiKeyBackground,Ne(t,{x:m,y:g,w:c,h:l,radius:b}),t.fill(),t.stroke(),t.fillStyle=r.backgroundColor,t.beginPath(),Ne(t,{x:p,y:g+1,w:c-2,h:l-2,radius:b}),t.fill()):(t.fillStyle=o.multiKeyBackground,t.fillRect(m,g,c,l),t.strokeRect(m,g,c,l),t.fillStyle=r.backgroundColor,t.fillRect(p,g+1,c-2,l-2))}t.fillStyle=this.labelTextColors[i]}drawBody(t,e,i){let{body:n}=this,{bodySpacing:o,bodyAlign:r,displayColors:a,boxHeight:l,boxWidth:c,boxPadding:h}=i,u=X(i.bodyFont),d=u.lineHeight,f=0,g=ge(i.rtl,this.x,this.width),m=function(M){e.fillText(M,g.x(t.x+f),t.y+d/2),t.y+=d+o},p=g.textAlign(r),b,y,_,w,x,k,S;for(e.textAlign=r,e.textBaseline="middle",e.font=u.string,t.x=pi(this,p,i),e.fillStyle=i.bodyColor,z(this.beforeBody,m),f=a&&p!=="right"?r==="center"?c/2+h:c+2+h:0,w=0,k=n.length;w0&&e.stroke()}_updateAnimationTarget(t){let e=this.chart,i=this.$animations,n=i&&i.x,o=i&&i.y;if(n||o){let r=Ms[t.position].call(this,this._active,this._eventPosition);if(!r)return;let a=this._size=fa(this,t),l=Object.assign({},r,this._size),c=ga(e,t,l),h=ma(t,l,c,e);(n._to!==h.x||o._to!==h.y)&&(this.xAlign=c.xAlign,this.yAlign=c.yAlign,this.width=a.width,this.height=a.height,this.caretX=r.x,this.caretY=r.y,this._resolveAnimations().update(this,h))}}_willRender(){return!!this.opacity}draw(t){let e=this.options.setContext(this.getContext()),i=this.opacity;if(!i)return;this._updateAnimationTarget(e);let n={width:this.width,height:this.height},o={x:this.x,y:this.y};i=Math.abs(i)<.001?0:i;let r=nt(e.padding),a=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;e.enabled&&a&&(t.save(),t.globalAlpha=i,this.drawBackground(o,t,n,e),Sn(t,e.textDirection),o.y+=r.top,this.drawTitle(o,t,e),this.drawBody(o,t,e),this.drawFooter(o,t,e),Mn(t,e.textDirection),t.restore())}getActiveElements(){return this._active||[]}setActiveElements(t,e){let i=this._active,n=t.map(({datasetIndex:a,index:l})=>{let c=this.chart.getDatasetMeta(a);if(!c)throw new Error("Cannot find a dataset at index "+a);return{datasetIndex:a,element:c.data[l],index:l}}),o=!gs(i,n),r=this._positionChanged(n,e);(o||r)&&(this._active=n,this._eventPosition=e,this._ignoreReplayEvents=!0,this.update(!0))}handleEvent(t,e,i=!0){if(e&&this._ignoreReplayEvents)return!1;this._ignoreReplayEvents=!1;let n=this.options,o=this._active||[],r=this._getActiveElements(t,o,e,i),a=this._positionChanged(r,t),l=e||!gs(r,o)||a;return l&&(this._active=r,(n.enabled||n.external)&&(this._eventPosition={x:t.x,y:t.y},this.update(!0,e))),l}_getActiveElements(t,e,i,n){let o=this.options;if(t.type==="mouseout")return[];if(!n)return e.filter(a=>this.chart.data.datasets[a.datasetIndex]&&this.chart.getDatasetMeta(a.datasetIndex).controller.getParsed(a.index)!==void 0);let r=this.chart.getElementsAtEventForMode(t,o.mode,o,i);return o.reverse&&r.reverse(),r}_positionChanged(t,e){let{caretX:i,caretY:n,options:o}=this,r=Ms[o.position].call(this,t,e);return r!==!1&&(i!==r.x||n!==r.y)}};v(Ps,"positioners",Ms);var Ef={id:"tooltip",_element:Ps,positioners:Ms,afterInit(s,t,e){e&&(s.tooltip=new Ps({chart:s,options:e}))},beforeUpdate(s,t,e){s.tooltip&&s.tooltip.initialize(e)},reset(s,t,e){s.tooltip&&s.tooltip.initialize(e)},afterDraw(s){let t=s.tooltip;if(t&&t._willRender()){let e={tooltip:t};if(s.notifyPlugins("beforeTooltipDraw",{...e,cancelable:!0})===!1)return;t.draw(s.ctx),s.notifyPlugins("afterTooltipDraw",e)}},afterEvent(s,t){if(s.tooltip){let e=t.replay;s.tooltip.handleEvent(t.event,e,t.inChartArea)&&(t.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(s,t)=>t.bodyFont.size,boxWidth:(s,t)=>t.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,boxPadding:0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:Ya},defaultRoutes:{bodyFont:"font",footerFont:"font",titleFont:"font"},descriptors:{_scriptable:s=>s!=="filter"&&s!=="itemSort"&&s!=="external",_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]},Lf=Object.freeze({__proto__:null,Colors:Ud,Decimation:Gd,Filler:pf,Legend:kf,SubTitle:Mf,Title:Sf,Tooltip:Ef}),Ff=(s,t,e,i)=>(typeof t=="string"?(e=s.push(t)-1,i.unshift({index:e,label:t})):isNaN(t)&&(e=null),e);function Rf(s,t,e,i){let n=s.indexOf(t);if(n===-1)return Ff(s,t,e,i);let o=s.lastIndexOf(t);return n!==o?e:n}var Nf=(s,t)=>s===null?null:K(Math.round(s),0,t);function ya(s){let t=this.getLabels();return s>=0&&se.length-1?null:this.getPixelForValue(e[t].value)}getValueForPixel(t){return Math.round(this._startValue+this.getDecimalForPixel(t)*this._valueRange)}getBasePixel(){return this.bottom}};v(Os,"id","category"),v(Os,"defaults",{ticks:{callback:ya}});function zf(s,t){let e=[],{bounds:n,step:o,min:r,max:a,precision:l,count:c,maxTicks:h,maxDigits:u,includeBounds:d}=s,f=o||1,g=h-1,{min:m,max:p}=t,b=!I(r),y=!I(a),_=!I(c),w=(p-m)/(u+1),x=sn((p-m)/g/f)*f,k,S,M,T;if(x<1e-14&&!b&&!y)return[{value:m},{value:p}];T=Math.ceil(p/x)-Math.floor(m/x),T>g&&(x=sn(T*x/g/f)*f),I(l)||(k=Math.pow(10,l),x=Math.ceil(x*k)/k),n==="ticks"?(S=Math.floor(m/x)*x,M=Math.ceil(p/x)*x):(S=m,M=p),b&&y&&o&&or((a-r)/o,x/1e3)?(T=Math.round(Math.min((a-r)/x,h)),x=(a-r)/T,S=r,M=a):_?(S=b?r:S,M=y?a:M,T=c-1,x=(M-S)/T):(T=(M-S)/x,Le(T,Math.round(T),x/1e3)?T=Math.round(T):T=Math.ceil(T));let C=Math.max(on(x),on(S));k=Math.pow(10,I(l)?C:l),S=Math.round(S*k)/k,M=Math.round(M*k)/k;let A=0;for(b&&(d&&S!==r?(e.push({value:r}),Sa)break;e.push({value:L})}return y&&d&&M!==a?e.length&&Le(e[e.length-1].value,a,xa(a,w,s))?e[e.length-1].value=a:e.push({value:a}):(!y||M===a)&&e.push({value:M}),e}function xa(s,t,{horizontal:e,minRotation:i}){let n=bt(i),o=(e?Math.sin(n):Math.cos(n))||.001,r=.75*t*(""+s).length;return Math.min(t/o,r)}var qe=class extends we{constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._endValue=void 0,this._valueRange=0}parse(t,e){return I(t)||(typeof t=="number"||t instanceof Number)&&!isFinite(+t)?null:+t}handleTickRangeOptions(){let{beginAtZero:t}=this.options,{minDefined:e,maxDefined:i}=this.getUserBounds(),{min:n,max:o}=this,r=l=>n=e?n:l,a=l=>o=i?o:l;if(t){let l=St(n),c=St(o);l<0&&c<0?a(0):l>0&&c>0&&r(0)}if(n===o){let l=o===0?1:Math.abs(o*.05);a(o+l),t||r(n-l)}this.min=n,this.max=o}getTickLimit(){let t=this.options.ticks,{maxTicksLimit:e,stepSize:i}=t,n;return i?(n=Math.ceil(this.max/i)-Math.floor(this.min/i)+1,n>1e3&&(console.warn(`scales.${this.id}.ticks.stepSize: ${i} would result generating up to ${n} ticks. Limiting to 1000.`),n=1e3)):(n=this.computeTickLimit(),e=e||11),e&&(n=Math.min(e,n)),n}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){let t=this.options,e=t.ticks,i=this.getTickLimit();i=Math.max(2,i);let n={maxTicks:i,bounds:t.bounds,min:t.min,max:t.max,precision:e.precision,step:e.stepSize,count:e.count,maxDigits:this._maxDigits(),horizontal:this.isHorizontal(),minRotation:e.minRotation||0,includeBounds:e.includeBounds!==!1},o=this._range||this,r=zf(n,o);return t.bounds==="ticks"&&nn(r,this,"value"),t.reverse?(r.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),r}configure(){let t=this.ticks,e=this.min,i=this.max;if(super.configure(),this.options.offset&&t.length){let n=(i-e)/Math.max(t.length-1,1)/2;e-=n,i+=n}this._startValue=e,this._endValue=i,this._valueRange=i-e}getLabelForValue(t){return Re(t,this.chart.options.locale,this.options.ticks.format)}},Ts=class extends qe{determineDataLimits(){let{min:t,max:e}=this.getMinMax(!0);this.min=Z(t)?t:0,this.max=Z(e)?e:1,this.handleTickRangeOptions()}computeTickLimit(){let t=this.isHorizontal(),e=t?this.width:this.height,i=bt(this.options.ticks.minRotation),n=(t?Math.sin(i):Math.cos(i))||.001,o=this._resolveTickFontOptions(0);return Math.ceil(e/Math.min(40,o.lineHeight/n))}getPixelForValue(t){return t===null?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getValueForPixel(t){return this._startValue+this.getDecimalForPixel(t)*this._valueRange}};v(Ts,"id","linear"),v(Ts,"defaults",{ticks:{callback:ms.formatters.numeric}});var Es=s=>Math.floor(Vt(s)),pe=(s,t)=>Math.pow(10,Es(s)+t);function _a(s){return s/Math.pow(10,Es(s))===1}function wa(s,t,e){let i=Math.pow(10,e),n=Math.floor(s/i);return Math.ceil(t/i)-n}function Vf(s,t){let e=t-s,i=Es(e);for(;wa(s,t,i)>10;)i++;for(;wa(s,t,i)<10;)i--;return Math.min(i,Es(s))}function Wf(s,{min:t,max:e}){t=at(s.min,t);let i=[],n=Es(t),o=Vf(t,e),r=o<0?Math.pow(10,Math.abs(o)):1,a=Math.pow(10,o),l=n>o?Math.pow(10,n):0,c=Math.round((t-l)*r)/r,h=Math.floor((t-l)/a/10)*a*10,u=Math.floor((c-h)/Math.pow(10,o)),d=at(s.min,Math.round((l+h+u*Math.pow(10,o))*r)/r);for(;d=10?u=u<15?15:20:u++,u>=20&&(o++,u=2,r=o>=0?1:r),d=Math.round((l+h+u*Math.pow(10,o))*r)/r;let f=at(s.max,d);return i.push({value:f,major:_a(f),significand:u}),i}var Ds=class extends we{constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._valueRange=0}parse(t,e){let i=qe.prototype.parse.apply(this,[t,e]);if(i===0){this._zero=!0;return}return Z(i)&&i>0?i:null}determineDataLimits(){let{min:t,max:e}=this.getMinMax(!0);this.min=Z(t)?Math.max(0,t):null,this.max=Z(e)?Math.max(0,e):null,this.options.beginAtZero&&(this._zero=!0),this._zero&&this.min!==this._suggestedMin&&!Z(this._userMin)&&(this.min=t===pe(this.min,0)?pe(this.min,-1):pe(this.min,0)),this.handleTickRangeOptions()}handleTickRangeOptions(){let{minDefined:t,maxDefined:e}=this.getUserBounds(),i=this.min,n=this.max,o=a=>i=t?i:a,r=a=>n=e?n:a;i===n&&(i<=0?(o(1),r(10)):(o(pe(i,-1)),r(pe(n,1)))),i<=0&&o(pe(n,-1)),n<=0&&r(pe(i,1)),this.min=i,this.max=n}buildTicks(){let t=this.options,e={min:this._userMin,max:this._userMax},i=Wf(e,this);return t.bounds==="ticks"&&nn(i,this,"value"),t.reverse?(i.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),i}getLabelForValue(t){return t===void 0?"0":Re(t,this.chart.options.locale,this.options.ticks.format)}configure(){let t=this.min;super.configure(),this._startValue=Vt(t),this._valueRange=Vt(this.max)-Vt(t)}getPixelForValue(t){return(t===void 0||t===0)&&(t=this.min),t===null||isNaN(t)?NaN:this.getPixelForDecimal(t===this.min?0:(Vt(t)-this._startValue)/this._valueRange)}getValueForPixel(t){let e=this.getDecimalForPixel(t);return Math.pow(10,this._startValue+e*this._valueRange)}};v(Ds,"id","logarithmic"),v(Ds,"defaults",{ticks:{callback:ms.formatters.logarithmic,major:{enabled:!0}}});function Qn(s){let t=s.ticks;if(t.display&&s.display){let e=nt(t.backdropPadding);return P(t.font&&t.font.size,j.font.size)+e.height}return 0}function Bf(s,t,e){return e=H(e)?e:[e],{w:gr(s,t.string,e),h:e.length*t.lineHeight}}function ka(s,t,e,i,n){return s===i||s===n?{start:t-e/2,end:t+e/2}:sn?{start:t-e,end:t}:{start:t,end:t+e}}function Hf(s){let t={l:s.left+s._padding.left,r:s.right-s._padding.right,t:s.top+s._padding.top,b:s.bottom-s._padding.bottom},e=Object.assign({},t),i=[],n=[],o=s._pointLabels.length,r=s.options.pointLabels,a=r.centerPointLabels?F/o:0;for(let l=0;lt.r&&(a=(i.end-t.r)/o,s.r=Math.max(s.r,t.r+a)),n.startt.b&&(l=(n.end-t.b)/r,s.b=Math.max(s.b,t.b+l))}function jf(s,t,e){let i=s.drawingArea,{extra:n,additionalAngle:o,padding:r,size:a}=e,l=s.getPointPosition(t,i+n+r,o),c=Math.round(si(st(l.angle+q))),h=Gf(l.y,a.h,c),u=Zf(c),d=qf(l.x,a.w,u);return{visible:!0,x:l.x,y:h,textAlign:u,left:d,top:h,right:d+a.w,bottom:h+a.h}}function Uf(s,t){if(!t)return!0;let{left:e,top:i,right:n,bottom:o}=s;return!(Pt({x:e,y:i},t)||Pt({x:e,y:o},t)||Pt({x:n,y:i},t)||Pt({x:n,y:o},t))}function Yf(s,t,e){let i=[],n=s._pointLabels.length,o=s.options,{centerPointLabels:r,display:a}=o.pointLabels,l={extra:Qn(o)/2,additionalAngle:r?F/n:0},c;for(let h=0;h270||e<90)&&(s-=t),s}function Xf(s,t,e){let{left:i,top:n,right:o,bottom:r}=e,{backdropColor:a}=t;if(!I(a)){let l=te(t.borderRadius),c=nt(t.backdropPadding);s.fillStyle=a;let h=i-c.left,u=n-c.top,d=o-i+c.width,f=r-n+c.height;Object.values(l).some(g=>g!==0)?(s.beginPath(),Ne(s,{x:h,y:u,w:d,h:f,radius:l}),s.fill()):s.fillRect(h,u,d,f)}}function Jf(s,t){let{ctx:e,options:{pointLabels:i}}=s;for(let n=t-1;n>=0;n--){let o=s._pointLabelItems[n];if(!o.visible)continue;let r=i.setContext(s.getPointLabelContext(n));Xf(e,r,o);let a=X(r.font),{x:l,y:c,textAlign:h}=o;Qt(e,s._pointLabels[n],l,c+a.lineHeight/2,a,{color:r.color,textAlign:h,textBaseline:"middle"})}}function Za(s,t,e,i){let{ctx:n}=s;if(e)n.arc(s.xCenter,s.yCenter,t,0,$);else{let o=s.getPointPosition(0,t);n.moveTo(o.x,o.y);for(let r=1;r{let n=W(this.options.pointLabels.callback,[e,i],this);return n||n===0?n:""}).filter((e,i)=>this.chart.getDataVisibility(i))}fit(){let t=this.options;t.display&&t.pointLabels.display?Hf(this):this.setCenterPoint(0,0,0,0)}setCenterPoint(t,e,i,n){this.xCenter+=Math.floor((t-e)/2),this.yCenter+=Math.floor((i-n)/2),this.drawingArea-=Math.min(this.drawingArea/2,Math.max(t,e,i,n))}getIndexAngle(t){let e=$/(this._pointLabels.length||1),i=this.options.startAngle||0;return st(t*e+bt(i))}getDistanceFromCenterForValue(t){if(I(t))return NaN;let e=this.drawingArea/(this.max-this.min);return this.options.reverse?(this.max-t)*e:(t-this.min)*e}getValueForDistanceFromCenter(t){if(I(t))return NaN;let e=t/(this.drawingArea/(this.max-this.min));return this.options.reverse?this.max-e:this.min+e}getPointLabelContext(t){let e=this._pointLabels||[];if(t>=0&&t{if(u!==0||u===0&&this.min<0){l=this.getDistanceFromCenterForValue(h.value);let d=this.getContext(u),f=n.setContext(d),g=o.setContext(d);Kf(this,f,l,r,g)}}),i.display){for(t.save(),a=r-1;a>=0;a--){let h=i.setContext(this.getPointLabelContext(a)),{color:u,lineWidth:d}=h;!d||!u||(t.lineWidth=d,t.strokeStyle=u,t.setLineDash(h.borderDash),t.lineDashOffset=h.borderDashOffset,l=this.getDistanceFromCenterForValue(e.reverse?this.min:this.max),c=this.getPointPosition(a,l),t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(c.x,c.y),t.stroke())}t.restore()}}drawBorder(){}drawLabels(){let t=this.ctx,e=this.options,i=e.ticks;if(!i.display)return;let n=this.getIndexAngle(0),o,r;t.save(),t.translate(this.xCenter,this.yCenter),t.rotate(n),t.textAlign="center",t.textBaseline="middle",this.ticks.forEach((a,l)=>{if(l===0&&this.min>=0&&!e.reverse)return;let c=i.setContext(this.getContext(l)),h=X(c.font);if(o=this.getDistanceFromCenterForValue(this.ticks[l].value),c.showLabelBackdrop){t.font=h.string,r=t.measureText(a.label).width,t.fillStyle=c.backdropColor;let u=nt(c.backdropPadding);t.fillRect(-r/2-u.left,-o-h.size/2-u.top,r+u.width,h.size+u.height)}Qt(t,a.label,0,-o,h,{color:c.color,strokeColor:c.textStrokeColor,strokeWidth:c.textStrokeWidth})}),t.restore()}drawTitle(){}};v(ye,"id","radialLinear"),v(ye,"defaults",{display:!0,animate:!0,position:"chartArea",angleLines:{display:!0,lineWidth:1,borderDash:[],borderDashOffset:0},grid:{circular:!1},startAngle:0,ticks:{showLabelBackdrop:!0,callback:ms.formatters.numeric},pointLabels:{backdropColor:void 0,backdropPadding:2,display:!0,font:{size:10},callback(t){return t},padding:5,centerPointLabels:!1}}),v(ye,"defaultRoutes",{"angleLines.color":"borderColor","pointLabels.color":"color","ticks.color":"color"}),v(ye,"descriptors",{angleLines:{_fallback:"grid"}});var Ti={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},ct=Object.keys(Ti);function va(s,t){return s-t}function Sa(s,t){if(I(t))return null;let e=s._adapter,{parser:i,round:n,isoWeekday:o}=s._parseOpts,r=t;return typeof i=="function"&&(r=i(r)),Z(r)||(r=typeof i=="string"?e.parse(r,i):e.parse(r)),r===null?null:(n&&(r=n==="week"&&(fe(o)||o===!0)?e.startOf(r,"isoWeek",o):e.startOf(r,n)),+r)}function Ma(s,t,e,i){let n=ct.length;for(let o=ct.indexOf(s);o=ct.indexOf(e);o--){let r=ct[o];if(Ti[r].common&&s._adapter.diff(n,i,r)>=t-1)return r}return ct[e?ct.indexOf(e):0]}function eg(s){for(let t=ct.indexOf(s)+1,e=ct.length;t=t?e[i]:e[n];s[o]=!0}}function sg(s,t,e,i){let n=s._adapter,o=+n.startOf(t[0].value,i),r=t[t.length-1].value,a,l;for(a=o;a<=r;a=+n.add(a,1,i))l=e[a],l>=0&&(t[l].major=!0);return t}function Ta(s,t,e){let i=[],n={},o=t.length,r,a;for(r=0;r+t.value))}initOffsets(t=[]){let e=0,i=0,n,o;this.options.offset&&t.length&&(n=this.getDecimalForValue(t[0]),t.length===1?e=1-n:e=(this.getDecimalForValue(t[1])-n)/2,o=this.getDecimalForValue(t[t.length-1]),t.length===1?i=o:i=(o-this.getDecimalForValue(t[t.length-2]))/2);let r=t.length<3?.5:.25;e=K(e,0,r),i=K(i,0,r),this._offsets={start:e,end:i,factor:1/(e+1+i)}}_generate(){let t=this._adapter,e=this.min,i=this.max,n=this.options,o=n.time,r=o.unit||Ma(o.minUnit,e,i,this._getLabelCapacity(e)),a=P(n.ticks.stepSize,1),l=r==="week"?o.isoWeekday:!1,c=fe(l)||l===!0,h={},u=e,d,f;if(c&&(u=+t.startOf(u,"isoWeek",l)),u=+t.startOf(u,c?"day":r),t.diff(i,e,r)>1e5*a)throw new Error(e+" and "+i+" are too far apart with stepSize of "+a+" "+r);let g=n.ticks.source==="data"&&this.getDataTimestamps();for(d=u,f=0;d+m)}getLabelForValue(t){let e=this._adapter,i=this.options.time;return i.tooltipFormat?e.format(t,i.tooltipFormat):e.format(t,i.displayFormats.datetime)}format(t,e){let n=this.options.time.displayFormats,o=this._unit,r=e||n[o];return this._adapter.format(t,r)}_tickFormatFunction(t,e,i,n){let o=this.options,r=o.ticks.callback;if(r)return W(r,[t,e,i],this);let a=o.time.displayFormats,l=this._unit,c=this._majorUnit,h=l&&a[l],u=c&&a[c],d=i[e],f=c&&u&&d&&d.major;return this._adapter.format(t,n||(f?u:h))}generateTickLabels(t){let e,i,n;for(e=0,i=t.length;e0?a:1}getDataTimestamps(){let t=this._cache.data||[],e,i;if(t.length)return t;let n=this.getMatchingVisibleMetas();if(this._normalized&&n.length)return this._cache.data=n[0].controller.getAllParsedValues(this);for(e=0,i=n.length;e=s[i].pos&&t<=s[n].pos&&({lo:i,hi:n}=Ct(s,"pos",t)),{pos:o,time:a}=s[i],{pos:r,time:l}=s[n]):(t>=s[i].time&&t<=s[n].time&&({lo:i,hi:n}=Ct(s,"time",t)),{time:o,pos:a}=s[i],{time:r,pos:l}=s[n]);let c=r-o;return c?a+(l-a)*(t-o)/c:a}var Cs=class extends _e{constructor(t){super(t),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){let t=this._getTimestampsForTable(),e=this._table=this.buildLookupTable(t);this._minPos=bi(e,this.min),this._tableRange=bi(e,this.max)-this._minPos,super.initOffsets(t)}buildLookupTable(t){let{min:e,max:i}=this,n=[],o=[],r,a,l,c,h;for(r=0,a=t.length;r=e&&c<=i&&n.push(c);if(n.length<2)return[{time:e,pos:0},{time:i,pos:1}];for(r=0,a=n.length;rn-o)}_getTimestampsForTable(){let t=this._cache.all||[];if(t.length)return t;let e=this.getDataTimestamps(),i=this.getLabelTimestamps();return e.length&&i.length?t=this.normalize(e.concat(i)):t=e.length?e:i,t=this._cache.all=t,t}getDecimalForValue(t){return(bi(this._table,t)-this._minPos)/this._tableRange}getValueForPixel(t){let e=this._offsets,i=this.getDecimalForPixel(t)/e.factor-e.end;return bi(this._table,i*this._tableRange+this._minPos,!0)}};v(Cs,"id","timeseries"),v(Cs,"defaults",_e.defaults);var ig=Object.freeze({__proto__:null,CategoryScale:Os,LinearScale:Ts,LogarithmicScale:Ds,RadialLinearScale:ye,TimeScale:_e,TimeSeriesScale:Cs}),qa=[gu,zd,Lf,ig];yt.register(...qa);var Mt=yt;var Yt=class extends Error{},ho=class extends Yt{constructor(t){super(`Invalid DateTime: ${t.toMessage()}`)}},uo=class extends Yt{constructor(t){super(`Invalid Interval: ${t.toMessage()}`)}},fo=class extends Yt{constructor(t){super(`Invalid Duration: ${t.toMessage()}`)}},oe=class extends Yt{},Fi=class extends Yt{constructor(t){super(`Invalid unit ${t}`)}},Q=class extends Yt{},Rt=class extends Yt{constructor(){super("Zone is an abstract class")}},O="numeric",Dt="short",mt="long",Ri={year:O,month:O,day:O},Ml={year:O,month:Dt,day:O},ng={year:O,month:Dt,day:O,weekday:Dt},Ol={year:O,month:mt,day:O},Tl={year:O,month:mt,day:O,weekday:mt},Dl={hour:O,minute:O},Cl={hour:O,minute:O,second:O},Pl={hour:O,minute:O,second:O,timeZoneName:Dt},Al={hour:O,minute:O,second:O,timeZoneName:mt},Il={hour:O,minute:O,hourCycle:"h23"},El={hour:O,minute:O,second:O,hourCycle:"h23"},Ll={hour:O,minute:O,second:O,hourCycle:"h23",timeZoneName:Dt},Fl={hour:O,minute:O,second:O,hourCycle:"h23",timeZoneName:mt},Rl={year:O,month:O,day:O,hour:O,minute:O},Nl={year:O,month:O,day:O,hour:O,minute:O,second:O},zl={year:O,month:Dt,day:O,hour:O,minute:O},Vl={year:O,month:Dt,day:O,hour:O,minute:O,second:O},og={year:O,month:Dt,day:O,weekday:Dt,hour:O,minute:O},Wl={year:O,month:mt,day:O,hour:O,minute:O,timeZoneName:Dt},Bl={year:O,month:mt,day:O,hour:O,minute:O,second:O,timeZoneName:Dt},Hl={year:O,month:mt,day:O,weekday:mt,hour:O,minute:O,timeZoneName:mt},$l={year:O,month:mt,day:O,weekday:mt,hour:O,minute:O,second:O,timeZoneName:mt},Me=class{get type(){throw new Rt}get name(){throw new Rt}get ianaName(){return this.name}get isUniversal(){throw new Rt}offsetName(t,e){throw new Rt}formatOffset(t,e){throw new Rt}offset(t){throw new Rt}equals(t){throw new Rt}get isValid(){throw new Rt}},eo=null,Ni=class s extends Me{static get instance(){return eo===null&&(eo=new s),eo}get type(){return"system"}get name(){return new Intl.DateTimeFormat().resolvedOptions().timeZone}get isUniversal(){return!1}offsetName(t,{format:e,locale:i}){return tc(t,e,i)}formatOffset(t,e){return Vs(this.offset(t),e)}offset(t){return-new Date(t).getTimezoneOffset()}equals(t){return t.type==="system"}get isValid(){return!0}},go=new Map;function rg(s){let t=go.get(s);return t===void 0&&(t=new Intl.DateTimeFormat("en-US",{hour12:!1,timeZone:s,year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit",second:"2-digit",era:"short"}),go.set(s,t)),t}var ag={year:0,month:1,day:2,era:3,hour:4,minute:5,second:6};function lg(s,t){let e=s.format(t).replace(/\u200E/g,""),i=/(\d+)\/(\d+)\/(\d+) (AD|BC),? (\d+):(\d+):(\d+)/.exec(e),[,n,o,r,a,l,c,h]=i;return[r,n,o,a,l,c,h]}function cg(s,t){let e=s.formatToParts(t),i=[];for(let n=0;n=0?g:1e3+g,(d-f)/(60*1e3)}equals(t){return t.type==="iana"&&t.name===this.name}get isValid(){return this.valid}},Ga={};function hg(s,t={}){let e=JSON.stringify([s,t]),i=Ga[e];return i||(i=new Intl.ListFormat(s,t),Ga[e]=i),i}var mo=new Map;function po(s,t={}){let e=JSON.stringify([s,t]),i=mo.get(e);return i===void 0&&(i=new Intl.DateTimeFormat(s,t),mo.set(e,i)),i}var bo=new Map;function ug(s,t={}){let e=JSON.stringify([s,t]),i=bo.get(e);return i===void 0&&(i=new Intl.NumberFormat(s,t),bo.set(e,i)),i}var yo=new Map;function dg(s,t={}){let{base:e,...i}=t,n=JSON.stringify([s,i]),o=yo.get(n);return o===void 0&&(o=new Intl.RelativeTimeFormat(s,t),yo.set(n,o)),o}var Rs=null;function fg(){return Rs||(Rs=new Intl.DateTimeFormat().resolvedOptions().locale,Rs)}var xo=new Map;function jl(s){let t=xo.get(s);return t===void 0&&(t=new Intl.DateTimeFormat(s).resolvedOptions(),xo.set(s,t)),t}var _o=new Map;function gg(s){let t=_o.get(s);if(!t){let e=new Intl.Locale(s);t="getWeekInfo"in e?e.getWeekInfo():e.weekInfo,"minimalDays"in t||(t={...Ul,...t}),_o.set(s,t)}return t}function mg(s){let t=s.indexOf("-x-");t!==-1&&(s=s.substring(0,t));let e=s.indexOf("-u-");if(e===-1)return[s];{let i,n;try{i=po(s).resolvedOptions(),n=s}catch{let l=s.substring(0,e);i=po(l).resolvedOptions(),n=l}let{numberingSystem:o,calendar:r}=i;return[n,o,r]}}function pg(s,t,e){return(e||t)&&(s.includes("-u-")||(s+="-u"),e&&(s+=`-ca-${e}`),t&&(s+=`-nu-${t}`)),s}function bg(s){let t=[];for(let e=1;e<=12;e++){let i=R.utc(2009,e,1);t.push(s(i))}return t}function yg(s){let t=[];for(let e=1;e<=7;e++){let i=R.utc(2016,11,13+e);t.push(s(i))}return t}function Di(s,t,e,i){let n=s.listingMode();return n==="error"?null:n==="en"?e(t):i(t)}function xg(s){return s.numberingSystem&&s.numberingSystem!=="latn"?!1:s.numberingSystem==="latn"||!s.locale||s.locale.startsWith("en")||jl(s.locale).numberingSystem==="latn"}var wo=class{constructor(t,e,i){this.padTo=i.padTo||0,this.floor=i.floor||!1;let{padTo:n,floor:o,...r}=i;if(!e||Object.keys(r).length>0){let a={useGrouping:!1,...i};i.padTo>0&&(a.minimumIntegerDigits=i.padTo),this.inf=ug(t,a)}}format(t){if(this.inf){let e=this.floor?Math.floor(t):t;return this.inf.format(e)}else{let e=this.floor?Math.floor(t):Eo(t,3);return J(e,this.padTo)}}},ko=class{constructor(t,e,i){this.opts=i,this.originalZone=void 0;let n;if(this.opts.timeZone)this.dt=t;else if(t.zone.type==="fixed"){let r=-1*(t.offset/60),a=r>=0?`Etc/GMT+${r}`:`Etc/GMT${r}`;t.offset!==0&&ae.create(a).valid?(n=a,this.dt=t):(n="UTC",this.dt=t.offset===0?t:t.setZone("UTC").plus({minutes:t.offset}),this.originalZone=t.zone)}else t.zone.type==="system"?this.dt=t:t.zone.type==="iana"?(this.dt=t,n=t.zone.name):(n="UTC",this.dt=t.setZone("UTC").plus({minutes:t.offset}),this.originalZone=t.zone);let o={...this.opts};o.timeZone=o.timeZone||n,this.dtf=po(e,o)}format(){return this.originalZone?this.formatToParts().map(({value:t})=>t).join(""):this.dtf.format(this.dt.toJSDate())}formatToParts(){let t=this.dtf.formatToParts(this.dt.toJSDate());return this.originalZone?t.map(e=>{if(e.type==="timeZoneName"){let i=this.originalZone.offsetName(this.dt.ts,{locale:this.dt.locale,format:this.opts.timeZoneName});return{...e,value:i}}else return e}):t}resolvedOptions(){return this.dtf.resolvedOptions()}},vo=class{constructor(t,e,i){this.opts={style:"long",...i},!e&&Kl()&&(this.rtf=dg(t,i))}format(t,e){return this.rtf?this.rtf.format(t,e):Wg(e,t,this.opts.numeric,this.opts.style!=="long")}formatToParts(t,e){return this.rtf?this.rtf.formatToParts(t,e):[]}},Ul={firstDay:1,minimalDays:4,weekend:[6,7]},B=class s{static fromOpts(t){return s.create(t.locale,t.numberingSystem,t.outputCalendar,t.weekSettings,t.defaultToEN)}static create(t,e,i,n,o=!1){let r=t||Y.defaultLocale,a=r||(o?"en-US":fg()),l=e||Y.defaultNumberingSystem,c=i||Y.defaultOutputCalendar,h=Oo(n)||Y.defaultWeekSettings;return new s(a,l,c,h,r)}static resetCache(){Rs=null,mo.clear(),bo.clear(),yo.clear(),xo.clear(),_o.clear()}static fromObject({locale:t,numberingSystem:e,outputCalendar:i,weekSettings:n}={}){return s.create(t,e,i,n)}constructor(t,e,i,n,o){let[r,a,l]=mg(t);this.locale=r,this.numberingSystem=e||a||null,this.outputCalendar=i||l||null,this.weekSettings=n,this.intl=pg(this.locale,this.numberingSystem,this.outputCalendar),this.weekdaysCache={format:{},standalone:{}},this.monthsCache={format:{},standalone:{}},this.meridiemCache=null,this.eraCache={},this.specifiedLocale=o,this.fastNumbersCached=null}get fastNumbers(){return this.fastNumbersCached==null&&(this.fastNumbersCached=xg(this)),this.fastNumbersCached}listingMode(){let t=this.isEnglish(),e=(this.numberingSystem===null||this.numberingSystem==="latn")&&(this.outputCalendar===null||this.outputCalendar==="gregory");return t&&e?"en":"intl"}clone(t){return!t||Object.getOwnPropertyNames(t).length===0?this:s.create(t.locale||this.specifiedLocale,t.numberingSystem||this.numberingSystem,t.outputCalendar||this.outputCalendar,Oo(t.weekSettings)||this.weekSettings,t.defaultToEN||!1)}redefaultToEN(t={}){return this.clone({...t,defaultToEN:!0})}redefaultToSystem(t={}){return this.clone({...t,defaultToEN:!1})}months(t,e=!1){return Di(this,t,ic,()=>{let i=this.intl==="ja"||this.intl.startsWith("ja-");e&=!i;let n=e?{month:t,day:"numeric"}:{month:t},o=e?"format":"standalone";if(!this.monthsCache[o][t]){let r=i?a=>this.dtFormatter(a,n).format():a=>this.extract(a,n,"month");this.monthsCache[o][t]=bg(r)}return this.monthsCache[o][t]})}weekdays(t,e=!1){return Di(this,t,rc,()=>{let i=e?{weekday:t,year:"numeric",month:"long",day:"numeric"}:{weekday:t},n=e?"format":"standalone";return this.weekdaysCache[n][t]||(this.weekdaysCache[n][t]=yg(o=>this.extract(o,i,"weekday"))),this.weekdaysCache[n][t]})}meridiems(){return Di(this,void 0,()=>ac,()=>{if(!this.meridiemCache){let t={hour:"numeric",hourCycle:"h12"};this.meridiemCache=[R.utc(2016,11,13,9),R.utc(2016,11,13,19)].map(e=>this.extract(e,t,"dayperiod"))}return this.meridiemCache})}eras(t){return Di(this,t,lc,()=>{let e={era:t};return this.eraCache[t]||(this.eraCache[t]=[R.utc(-40,1,1),R.utc(2017,1,1)].map(i=>this.extract(i,e,"era"))),this.eraCache[t]})}extract(t,e,i){let n=this.dtFormatter(t,e),o=n.formatToParts(),r=o.find(a=>a.type.toLowerCase()===i);return r?r.value:null}numberFormatter(t={}){return new wo(this.intl,t.forceSimple||this.fastNumbers,t)}dtFormatter(t,e={}){return new ko(t,this.intl,e)}relFormatter(t={}){return new vo(this.intl,this.isEnglish(),t)}listFormatter(t={}){return hg(this.intl,t)}isEnglish(){return this.locale==="en"||this.locale.toLowerCase()==="en-us"||jl(this.intl).locale.startsWith("en-us")}getWeekSettings(){return this.weekSettings?this.weekSettings:Ql()?gg(this.locale):Ul}getStartOfWeek(){return this.getWeekSettings().firstDay}getMinDaysInFirstWeek(){return this.getWeekSettings().minimalDays}getWeekendDays(){return this.getWeekSettings().weekend}equals(t){return this.locale===t.locale&&this.numberingSystem===t.numberingSystem&&this.outputCalendar===t.outputCalendar}toString(){return`Locale(${this.locale}, ${this.numberingSystem}, ${this.outputCalendar})`}},io=null,kt=class s extends Me{static get utcInstance(){return io===null&&(io=new s(0)),io}static instance(t){return t===0?s.utcInstance:new s(t)}static parseSpecifier(t){if(t){let e=t.match(/^utc(?:([+-]\d{1,2})(?::(\d{2}))?)?$/i);if(e)return new s(ji(e[1],e[2]))}return null}constructor(t){super(),this.fixed=t}get type(){return"fixed"}get name(){return this.fixed===0?"UTC":`UTC${Vs(this.fixed,"narrow")}`}get ianaName(){return this.fixed===0?"Etc/UTC":`Etc/GMT${Vs(-this.fixed,"narrow")}`}offsetName(){return this.name}formatOffset(t,e){return Vs(this.fixed,e)}get isUniversal(){return!0}offset(){return this.fixed}equals(t){return t.type==="fixed"&&t.fixed===this.fixed}get isValid(){return!0}},So=class extends Me{constructor(t){super(),this.zoneName=t}get type(){return"invalid"}get name(){return this.zoneName}get isUniversal(){return!1}offsetName(){return null}formatOffset(){return""}offset(){return NaN}equals(){return!1}get isValid(){return!1}};function ne(s,t){if(D(s)||s===null)return t;if(s instanceof Me)return s;if(Mg(s)){let e=s.toLowerCase();return e==="default"?t:e==="local"||e==="system"?Ni.instance:e==="utc"||e==="gmt"?kt.utcInstance:kt.parseSpecifier(e)||ae.create(s)}else return re(s)?kt.instance(s):typeof s=="object"&&"offset"in s&&typeof s.offset=="function"?s:new So(s)}var Co={arab:"[\u0660-\u0669]",arabext:"[\u06F0-\u06F9]",bali:"[\u1B50-\u1B59]",beng:"[\u09E6-\u09EF]",deva:"[\u0966-\u096F]",fullwide:"[\uFF10-\uFF19]",gujr:"[\u0AE6-\u0AEF]",hanidec:"[\u3007|\u4E00|\u4E8C|\u4E09|\u56DB|\u4E94|\u516D|\u4E03|\u516B|\u4E5D]",khmr:"[\u17E0-\u17E9]",knda:"[\u0CE6-\u0CEF]",laoo:"[\u0ED0-\u0ED9]",limb:"[\u1946-\u194F]",mlym:"[\u0D66-\u0D6F]",mong:"[\u1810-\u1819]",mymr:"[\u1040-\u1049]",orya:"[\u0B66-\u0B6F]",tamldec:"[\u0BE6-\u0BEF]",telu:"[\u0C66-\u0C6F]",thai:"[\u0E50-\u0E59]",tibt:"[\u0F20-\u0F29]",latn:"\\d"},Xa={arab:[1632,1641],arabext:[1776,1785],bali:[6992,7001],beng:[2534,2543],deva:[2406,2415],fullwide:[65296,65303],gujr:[2790,2799],khmr:[6112,6121],knda:[3302,3311],laoo:[3792,3801],limb:[6470,6479],mlym:[3430,3439],mong:[6160,6169],mymr:[4160,4169],orya:[2918,2927],tamldec:[3046,3055],telu:[3174,3183],thai:[3664,3673],tibt:[3872,3881]},_g=Co.hanidec.replace(/[\[|\]]/g,"").split("");function wg(s){let t=parseInt(s,10);if(isNaN(t)){t="";for(let e=0;e=o&&i<=r&&(t+=i-o)}}return parseInt(t,10)}else return t}var Mo=new Map;function kg(){Mo.clear()}function Ot({numberingSystem:s},t=""){let e=s||"latn",i=Mo.get(e);i===void 0&&(i=new Map,Mo.set(e,i));let n=i.get(t);return n===void 0&&(n=new RegExp(`${Co[e]}${t}`),i.set(t,n)),n}var Ja=()=>Date.now(),Ka="system",Qa=null,tl=null,el=null,sl=60,il,nl=null,Y=class{static get now(){return Ja}static set now(t){Ja=t}static set defaultZone(t){Ka=t}static get defaultZone(){return ne(Ka,Ni.instance)}static get defaultLocale(){return Qa}static set defaultLocale(t){Qa=t}static get defaultNumberingSystem(){return tl}static set defaultNumberingSystem(t){tl=t}static get defaultOutputCalendar(){return el}static set defaultOutputCalendar(t){el=t}static get defaultWeekSettings(){return nl}static set defaultWeekSettings(t){nl=Oo(t)}static get twoDigitCutoffYear(){return sl}static set twoDigitCutoffYear(t){sl=t%100}static get throwOnInvalid(){return il}static set throwOnInvalid(t){il=t}static resetCaches(){B.resetCache(),ae.resetCache(),R.resetCache(),kg()}},gt=class{constructor(t,e){this.reason=t,this.explanation=e}toMessage(){return this.explanation?`${this.reason}: ${this.explanation}`:this.reason}},Yl=[0,31,59,90,120,151,181,212,243,273,304,334],Zl=[0,31,60,91,121,152,182,213,244,274,305,335];function _t(s,t){return new gt("unit out of range",`you specified ${t} (of type ${typeof t}) as a ${s}, which is invalid`)}function Po(s,t,e){let i=new Date(Date.UTC(s,t-1,e));s<100&&s>=0&&i.setUTCFullYear(i.getUTCFullYear()-1900);let n=i.getUTCDay();return n===0?7:n}function ql(s,t,e){return e+(Bs(s)?Zl:Yl)[t-1]}function Gl(s,t){let e=Bs(s)?Zl:Yl,i=e.findIndex(o=>oWs(i,t,e)?(c=i+1,l=1):c=i,{weekYear:c,weekNumber:l,weekday:a,...Ui(s)}}function ol(s,t=4,e=1){let{weekYear:i,weekNumber:n,weekday:o}=s,r=Ao(Po(i,1,t),e),a=Qe(i),l=n*7+o-r-7+t,c;l<1?(c=i-1,l+=Qe(c)):l>a?(c=i+1,l-=Qe(i)):c=i;let{month:h,day:u}=Gl(c,l);return{year:c,month:h,day:u,...Ui(s)}}function no(s){let{year:t,month:e,day:i}=s,n=ql(t,e,i);return{year:t,ordinal:n,...Ui(s)}}function rl(s){let{year:t,ordinal:e}=s,{month:i,day:n}=Gl(t,e);return{year:t,month:i,day:n,...Ui(s)}}function al(s,t){if(!D(s.localWeekday)||!D(s.localWeekNumber)||!D(s.localWeekYear)){if(!D(s.weekday)||!D(s.weekNumber)||!D(s.weekYear))throw new oe("Cannot mix locale-based week fields with ISO-based week fields");return D(s.localWeekday)||(s.weekday=s.localWeekday),D(s.localWeekNumber)||(s.weekNumber=s.localWeekNumber),D(s.localWeekYear)||(s.weekYear=s.localWeekYear),delete s.localWeekday,delete s.localWeekNumber,delete s.localWeekYear,{minDaysInFirstWeek:t.getMinDaysInFirstWeek(),startOfWeek:t.getStartOfWeek()}}else return{minDaysInFirstWeek:4,startOfWeek:1}}function vg(s,t=4,e=1){let i=Hi(s.weekYear),n=wt(s.weekNumber,1,Ws(s.weekYear,t,e)),o=wt(s.weekday,1,7);return i?n?o?!1:_t("weekday",s.weekday):_t("week",s.weekNumber):_t("weekYear",s.weekYear)}function Sg(s){let t=Hi(s.year),e=wt(s.ordinal,1,Qe(s.year));return t?e?!1:_t("ordinal",s.ordinal):_t("year",s.year)}function Xl(s){let t=Hi(s.year),e=wt(s.month,1,12),i=wt(s.day,1,Vi(s.year,s.month));return t?e?i?!1:_t("day",s.day):_t("month",s.month):_t("year",s.year)}function Jl(s){let{hour:t,minute:e,second:i,millisecond:n}=s,o=wt(t,0,23)||t===24&&e===0&&i===0&&n===0,r=wt(e,0,59),a=wt(i,0,59),l=wt(n,0,999);return o?r?a?l?!1:_t("millisecond",n):_t("second",i):_t("minute",e):_t("hour",t)}function D(s){return typeof s>"u"}function re(s){return typeof s=="number"}function Hi(s){return typeof s=="number"&&s%1===0}function Mg(s){return typeof s=="string"}function Og(s){return Object.prototype.toString.call(s)==="[object Date]"}function Kl(){try{return typeof Intl<"u"&&!!Intl.RelativeTimeFormat}catch{return!1}}function Ql(){try{return typeof Intl<"u"&&!!Intl.Locale&&("weekInfo"in Intl.Locale.prototype||"getWeekInfo"in Intl.Locale.prototype)}catch{return!1}}function Tg(s){return Array.isArray(s)?s:[s]}function ll(s,t,e){if(s.length!==0)return s.reduce((i,n)=>{let o=[t(n),n];return i&&e(i[0],o[0])===i[0]?i:o},null)[1]}function Dg(s,t){return t.reduce((e,i)=>(e[i]=s[i],e),{})}function ss(s,t){return Object.prototype.hasOwnProperty.call(s,t)}function Oo(s){if(s==null)return null;if(typeof s!="object")throw new Q("Week settings must be an object");if(!wt(s.firstDay,1,7)||!wt(s.minimalDays,1,7)||!Array.isArray(s.weekend)||s.weekend.some(t=>!wt(t,1,7)))throw new Q("Invalid week settings");return{firstDay:s.firstDay,minimalDays:s.minimalDays,weekend:Array.from(s.weekend)}}function wt(s,t,e){return Hi(s)&&s>=t&&s<=e}function Cg(s,t){return s-t*Math.floor(s/t)}function J(s,t=2){let e=s<0,i;return e?i="-"+(""+-s).padStart(t,"0"):i=(""+s).padStart(t,"0"),i}function ie(s){if(!(D(s)||s===null||s===""))return parseInt(s,10)}function ke(s){if(!(D(s)||s===null||s===""))return parseFloat(s)}function Io(s){if(!(D(s)||s===null||s==="")){let t=parseFloat("0."+s)*1e3;return Math.floor(t)}}function Eo(s,t,e="round"){let i=10**t;switch(e){case"expand":return s>0?Math.ceil(s*i)/i:Math.floor(s*i)/i;case"trunc":return Math.trunc(s*i)/i;case"round":return Math.round(s*i)/i;case"floor":return Math.floor(s*i)/i;case"ceil":return Math.ceil(s*i)/i;default:throw new RangeError(`Value rounding ${e} is out of range`)}}function Bs(s){return s%4===0&&(s%100!==0||s%400===0)}function Qe(s){return Bs(s)?366:365}function Vi(s,t){let e=Cg(t-1,12)+1,i=s+(t-e)/12;return e===2?Bs(i)?29:28:[31,null,31,30,31,30,31,31,30,31,30,31][e-1]}function $i(s){let t=Date.UTC(s.year,s.month-1,s.day,s.hour,s.minute,s.second,s.millisecond);return s.year<100&&s.year>=0&&(t=new Date(t),t.setUTCFullYear(s.year,s.month-1,s.day)),+t}function cl(s,t,e){return-Ao(Po(s,1,t),e)+t-1}function Ws(s,t=4,e=1){let i=cl(s,t,e),n=cl(s+1,t,e);return(Qe(s)-i+n)/7}function To(s){return s>99?s:s>Y.twoDigitCutoffYear?1900+s:2e3+s}function tc(s,t,e,i=null){let n=new Date(s),o={hourCycle:"h23",year:"numeric",month:"2-digit",day:"2-digit",hour:"2-digit",minute:"2-digit"};i&&(o.timeZone=i);let r={timeZoneName:t,...o},a=new Intl.DateTimeFormat(e,r).formatToParts(n).find(l=>l.type.toLowerCase()==="timezonename");return a?a.value:null}function ji(s,t){let e=parseInt(s,10);Number.isNaN(e)&&(e=0);let i=parseInt(t,10)||0,n=e<0||Object.is(e,-0)?-i:i;return e*60+n}function ec(s){let t=Number(s);if(typeof s=="boolean"||s===""||!Number.isFinite(t))throw new Q(`Invalid unit value ${s}`);return t}function Wi(s,t){let e={};for(let i in s)if(ss(s,i)){let n=s[i];if(n==null)continue;e[t(i)]=ec(n)}return e}function Vs(s,t){let e=Math.trunc(Math.abs(s/60)),i=Math.trunc(Math.abs(s%60)),n=s>=0?"+":"-";switch(t){case"short":return`${n}${J(e,2)}:${J(i,2)}`;case"narrow":return`${n}${e}${i>0?`:${i}`:""}`;case"techie":return`${n}${J(e,2)}${J(i,2)}`;default:throw new RangeError(`Value format ${t} is out of range for property format`)}}function Ui(s){return Dg(s,["hour","minute","second","millisecond"])}var Pg=["January","February","March","April","May","June","July","August","September","October","November","December"],sc=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],Ag=["J","F","M","A","M","J","J","A","S","O","N","D"];function ic(s){switch(s){case"narrow":return[...Ag];case"short":return[...sc];case"long":return[...Pg];case"numeric":return["1","2","3","4","5","6","7","8","9","10","11","12"];case"2-digit":return["01","02","03","04","05","06","07","08","09","10","11","12"];default:return null}}var nc=["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"],oc=["Mon","Tue","Wed","Thu","Fri","Sat","Sun"],Ig=["M","T","W","T","F","S","S"];function rc(s){switch(s){case"narrow":return[...Ig];case"short":return[...oc];case"long":return[...nc];case"numeric":return["1","2","3","4","5","6","7"];default:return null}}var ac=["AM","PM"],Eg=["Before Christ","Anno Domini"],Lg=["BC","AD"],Fg=["B","A"];function lc(s){switch(s){case"narrow":return[...Fg];case"short":return[...Lg];case"long":return[...Eg];default:return null}}function Rg(s){return ac[s.hour<12?0:1]}function Ng(s,t){return rc(t)[s.weekday-1]}function zg(s,t){return ic(t)[s.month-1]}function Vg(s,t){return lc(t)[s.year<0?0:1]}function Wg(s,t,e="always",i=!1){let n={years:["year","yr."],quarters:["quarter","qtr."],months:["month","mo."],weeks:["week","wk."],days:["day","day","days"],hours:["hour","hr."],minutes:["minute","min."],seconds:["second","sec."]},o=["hours","minutes","seconds"].indexOf(s)===-1;if(e==="auto"&&o){let u=s==="days";switch(t){case 1:return u?"tomorrow":`next ${n[s][0]}`;case-1:return u?"yesterday":`last ${n[s][0]}`;case 0:return u?"today":`this ${n[s][0]}`}}let r=Object.is(t,-0)||t<0,a=Math.abs(t),l=a===1,c=n[s],h=i?l?c[1]:c[2]||c[1]:l?n[s][0]:s;return r?`${a} ${h} ago`:`in ${a} ${h}`}function hl(s,t){let e="";for(let i of s)i.literal?e+=i.val:e+=t(i.val);return e}var Bg={D:Ri,DD:Ml,DDD:Ol,DDDD:Tl,t:Dl,tt:Cl,ttt:Pl,tttt:Al,T:Il,TT:El,TTT:Ll,TTTT:Fl,f:Rl,ff:zl,fff:Wl,ffff:Hl,F:Nl,FF:Vl,FFF:Bl,FFFF:$l},ft=class s{static create(t,e={}){return new s(t,e)}static parseFormat(t){let e=null,i="",n=!1,o=[];for(let r=0;r0||n)&&o.push({literal:n||/^\s+$/.test(i),val:i===""?"'":i}),e=null,i="",n=!n):n||a===e?i+=a:(i.length>0&&o.push({literal:/^\s+$/.test(i),val:i}),i=a,e=a)}return i.length>0&&o.push({literal:n||/^\s+$/.test(i),val:i}),o}static macroTokenToFormatOpts(t){return Bg[t]}constructor(t,e){this.opts=e,this.loc=t,this.systemLoc=null}formatWithSystemDefault(t,e){return this.systemLoc===null&&(this.systemLoc=this.loc.redefaultToSystem()),this.systemLoc.dtFormatter(t,{...this.opts,...e}).format()}dtFormatter(t,e={}){return this.loc.dtFormatter(t,{...this.opts,...e})}formatDateTime(t,e){return this.dtFormatter(t,e).format()}formatDateTimeParts(t,e){return this.dtFormatter(t,e).formatToParts()}formatInterval(t,e){return this.dtFormatter(t.start,e).dtf.formatRange(t.start.toJSDate(),t.end.toJSDate())}resolvedOptions(t,e){return this.dtFormatter(t,e).resolvedOptions()}num(t,e=0,i=void 0){if(this.opts.forceSimple)return J(t,e);let n={...this.opts};return e>0&&(n.padTo=e),i&&(n.signDisplay=i),this.loc.numberFormatter(n).format(t)}formatDateTimeFromString(t,e){let i=this.loc.listingMode()==="en",n=this.loc.outputCalendar&&this.loc.outputCalendar!=="gregory",o=(f,g)=>this.loc.extract(t,f,g),r=f=>t.isOffsetFixed&&t.offset===0&&f.allowZ?"Z":t.isValid?t.zone.formatOffset(t.ts,f.format):"",a=()=>i?Rg(t):o({hour:"numeric",hourCycle:"h12"},"dayperiod"),l=(f,g)=>i?zg(t,f):o(g?{month:f}:{month:f,day:"numeric"},"month"),c=(f,g)=>i?Ng(t,f):o(g?{weekday:f}:{weekday:f,month:"long",day:"numeric"},"weekday"),h=f=>{let g=s.macroTokenToFormatOpts(f);return g?this.formatWithSystemDefault(t,g):f},u=f=>i?Vg(t,f):o({era:f},"era"),d=f=>{switch(f){case"S":return this.num(t.millisecond);case"u":case"SSS":return this.num(t.millisecond,3);case"s":return this.num(t.second);case"ss":return this.num(t.second,2);case"uu":return this.num(Math.floor(t.millisecond/10),2);case"uuu":return this.num(Math.floor(t.millisecond/100));case"m":return this.num(t.minute);case"mm":return this.num(t.minute,2);case"h":return this.num(t.hour%12===0?12:t.hour%12);case"hh":return this.num(t.hour%12===0?12:t.hour%12,2);case"H":return this.num(t.hour);case"HH":return this.num(t.hour,2);case"Z":return r({format:"narrow",allowZ:this.opts.allowZ});case"ZZ":return r({format:"short",allowZ:this.opts.allowZ});case"ZZZ":return r({format:"techie",allowZ:this.opts.allowZ});case"ZZZZ":return t.zone.offsetName(t.ts,{format:"short",locale:this.loc.locale});case"ZZZZZ":return t.zone.offsetName(t.ts,{format:"long",locale:this.loc.locale});case"z":return t.zoneName;case"a":return a();case"d":return n?o({day:"numeric"},"day"):this.num(t.day);case"dd":return n?o({day:"2-digit"},"day"):this.num(t.day,2);case"c":return this.num(t.weekday);case"ccc":return c("short",!0);case"cccc":return c("long",!0);case"ccccc":return c("narrow",!0);case"E":return this.num(t.weekday);case"EEE":return c("short",!1);case"EEEE":return c("long",!1);case"EEEEE":return c("narrow",!1);case"L":return n?o({month:"numeric",day:"numeric"},"month"):this.num(t.month);case"LL":return n?o({month:"2-digit",day:"numeric"},"month"):this.num(t.month,2);case"LLL":return l("short",!0);case"LLLL":return l("long",!0);case"LLLLL":return l("narrow",!0);case"M":return n?o({month:"numeric"},"month"):this.num(t.month);case"MM":return n?o({month:"2-digit"},"month"):this.num(t.month,2);case"MMM":return l("short",!1);case"MMMM":return l("long",!1);case"MMMMM":return l("narrow",!1);case"y":return n?o({year:"numeric"},"year"):this.num(t.year);case"yy":return n?o({year:"2-digit"},"year"):this.num(t.year.toString().slice(-2),2);case"yyyy":return n?o({year:"numeric"},"year"):this.num(t.year,4);case"yyyyyy":return n?o({year:"numeric"},"year"):this.num(t.year,6);case"G":return u("short");case"GG":return u("long");case"GGGGG":return u("narrow");case"kk":return this.num(t.weekYear.toString().slice(-2),2);case"kkkk":return this.num(t.weekYear,4);case"W":return this.num(t.weekNumber);case"WW":return this.num(t.weekNumber,2);case"n":return this.num(t.localWeekNumber);case"nn":return this.num(t.localWeekNumber,2);case"ii":return this.num(t.localWeekYear.toString().slice(-2),2);case"iiii":return this.num(t.localWeekYear,4);case"o":return this.num(t.ordinal);case"ooo":return this.num(t.ordinal,3);case"q":return this.num(t.quarter);case"qq":return this.num(t.quarter,2);case"X":return this.num(Math.floor(t.ts/1e3));case"x":return this.num(t.ts);default:return h(f)}};return hl(s.parseFormat(e),d)}formatDurationFromString(t,e){let i=this.opts.signMode==="negativeLargestOnly"?-1:1,n=h=>{switch(h[0]){case"S":return"milliseconds";case"s":return"seconds";case"m":return"minutes";case"h":return"hours";case"d":return"days";case"w":return"weeks";case"M":return"months";case"y":return"years";default:return null}},o=(h,u)=>d=>{let f=n(d);if(f){let g=u.isNegativeDuration&&f!==u.largestUnit?i:1,m;return this.opts.signMode==="negativeLargestOnly"&&f!==u.largestUnit?m="never":this.opts.signMode==="all"?m="always":m="auto",this.num(h.get(f)*g,d.length,m)}else return d},r=s.parseFormat(e),a=r.reduce((h,{literal:u,val:d})=>u?h:h.concat(d),[]),l=t.shiftTo(...a.map(n).filter(h=>h)),c={isNegativeDuration:l<0,largestUnit:Object.keys(l.values)[0]};return hl(r,o(l,c))}},cc=/[A-Za-z_+-]{1,256}(?::?\/[A-Za-z0-9_+-]{1,256}(?:\/[A-Za-z0-9_+-]{1,256})?)?/;function is(...s){let t=s.reduce((e,i)=>e+i.source,"");return RegExp(`^${t}$`)}function ns(...s){return t=>s.reduce(([e,i,n],o)=>{let[r,a,l]=o(t,n);return[{...e,...r},a||i,l]},[{},null,1]).slice(0,2)}function os(s,...t){if(s==null)return[null,null];for(let[e,i]of t){let n=e.exec(s);if(n)return i(n)}return[null,null]}function hc(...s){return(t,e)=>{let i={},n;for(n=0;nf!==void 0&&(g||f&&h)?-f:f;return[{years:d(ke(e)),months:d(ke(i)),weeks:d(ke(n)),days:d(ke(o)),hours:d(ke(r)),minutes:d(ke(a)),seconds:d(ke(l),l==="-0"),milliseconds:d(Io(c),u)}]}var tm={GMT:0,EDT:-240,EST:-300,CDT:-300,CST:-360,MDT:-360,MST:-420,PDT:-420,PST:-480};function Ro(s,t,e,i,n,o,r){let a={year:t.length===2?To(ie(t)):ie(t),month:sc.indexOf(e)+1,day:ie(i),hour:ie(n),minute:ie(o)};return r&&(a.second=ie(r)),s&&(a.weekday=s.length>3?nc.indexOf(s)+1:oc.indexOf(s)+1),a}var em=/^(?:(Mon|Tue|Wed|Thu|Fri|Sat|Sun),\s)?(\d{1,2})\s(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\s(\d{2,4})\s(\d\d):(\d\d)(?::(\d\d))?\s(?:(UT|GMT|[ECMP][SD]T)|([Zz])|(?:([+-]\d\d)(\d\d)))$/;function sm(s){let[,t,e,i,n,o,r,a,l,c,h,u]=s,d=Ro(t,n,i,e,o,r,a),f;return l?f=tm[l]:c?f=0:f=ji(h,u),[d,new kt(f)]}function im(s){return s.replace(/\([^()]*\)|[\n\t]/g," ").replace(/(\s\s+)/g," ").trim()}var nm=/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun), (\d\d) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) (\d{4}) (\d\d):(\d\d):(\d\d) GMT$/,om=/^(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday), (\d\d)-(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)-(\d\d) (\d\d):(\d\d):(\d\d) GMT$/,rm=/^(Mon|Tue|Wed|Thu|Fri|Sat|Sun) (Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec) ( \d|\d\d) (\d\d):(\d\d):(\d\d) (\d{4})$/;function ul(s){let[,t,e,i,n,o,r,a]=s;return[Ro(t,n,i,e,o,r,a),kt.utcInstance]}function am(s){let[,t,e,i,n,o,r,a]=s;return[Ro(t,a,e,i,n,o,r),kt.utcInstance]}var lm=is($g,Fo),cm=is(jg,Fo),hm=is(Ug,Fo),um=is(dc),gc=ns(Xg,rs,Hs,$s),dm=ns(Yg,rs,Hs,$s),fm=ns(Zg,rs,Hs,$s),gm=ns(rs,Hs,$s);function mm(s){return os(s,[lm,gc],[cm,dm],[hm,fm],[um,gm])}function pm(s){return os(im(s),[em,sm])}function bm(s){return os(s,[nm,ul],[om,ul],[rm,am])}function ym(s){return os(s,[Kg,Qg])}var xm=ns(rs);function _m(s){return os(s,[Jg,xm])}var wm=is(qg,Gg),km=is(fc),vm=ns(rs,Hs,$s);function Sm(s){return os(s,[wm,gc],[km,vm])}var dl="Invalid Duration",mc={weeks:{days:7,hours:168,minutes:10080,seconds:10080*60,milliseconds:10080*60*1e3},days:{hours:24,minutes:1440,seconds:1440*60,milliseconds:1440*60*1e3},hours:{minutes:60,seconds:3600,milliseconds:3600*1e3},minutes:{seconds:60,milliseconds:60*1e3},seconds:{milliseconds:1e3}},Mm={years:{quarters:4,months:12,weeks:52,days:365,hours:365*24,minutes:365*24*60,seconds:365*24*60*60,milliseconds:365*24*60*60*1e3},quarters:{months:3,weeks:13,days:91,hours:2184,minutes:2184*60,seconds:2184*60*60,milliseconds:2184*60*60*1e3},months:{weeks:4,days:30,hours:720,minutes:720*60,seconds:720*60*60,milliseconds:720*60*60*1e3},...mc},xt=146097/400,Ge=146097/4800,Om={years:{quarters:4,months:12,weeks:xt/7,days:xt,hours:xt*24,minutes:xt*24*60,seconds:xt*24*60*60,milliseconds:xt*24*60*60*1e3},quarters:{months:3,weeks:xt/28,days:xt/4,hours:xt*24/4,minutes:xt*24*60/4,seconds:xt*24*60*60/4,milliseconds:xt*24*60*60*1e3/4},months:{weeks:Ge/7,days:Ge,hours:Ge*24,minutes:Ge*24*60,seconds:Ge*24*60*60,milliseconds:Ge*24*60*60*1e3},...mc},Se=["years","quarters","months","weeks","days","hours","minutes","seconds","milliseconds"],Tm=Se.slice(0).reverse();function Ut(s,t,e=!1){let i={values:e?t.values:{...s.values,...t.values||{}},loc:s.loc.clone(t.loc),conversionAccuracy:t.conversionAccuracy||s.conversionAccuracy,matrix:t.matrix||s.matrix};return new tt(i)}function pc(s,t){let e=t.milliseconds??0;for(let i of Tm.slice(1))t[i]&&(e+=t[i]*s[i].milliseconds);return e}function fl(s,t){let e=pc(s,t)<0?-1:1;Se.reduceRight((i,n)=>{if(D(t[n]))return i;if(i){let o=t[i]*e,r=s[n][i],a=Math.floor(o/r);t[n]+=a*e,t[i]-=a*r*e}return n},null),Se.reduce((i,n)=>{if(D(t[n]))return i;if(i){let o=t[i]%1;t[i]-=o,t[n]+=o*s[i][n]}return n},null)}function gl(s){let t={};for(let[e,i]of Object.entries(s))i!==0&&(t[e]=i);return t}var tt=class s{constructor(t){let e=t.conversionAccuracy==="longterm"||!1,i=e?Om:Mm;t.matrix&&(i=t.matrix),this.values=t.values,this.loc=t.loc||B.create(),this.conversionAccuracy=e?"longterm":"casual",this.invalid=t.invalid||null,this.matrix=i,this.isLuxonDuration=!0}static fromMillis(t,e){return s.fromObject({milliseconds:t},e)}static fromObject(t,e={}){if(t==null||typeof t!="object")throw new Q(`Duration.fromObject: argument expected to be an object, got ${t===null?"null":typeof t}`);return new s({values:Wi(t,s.normalizeUnit),loc:B.fromObject(e),conversionAccuracy:e.conversionAccuracy,matrix:e.matrix})}static fromDurationLike(t){if(re(t))return s.fromMillis(t);if(s.isDuration(t))return t;if(typeof t=="object")return s.fromObject(t);throw new Q(`Unknown duration argument ${t} of type ${typeof t}`)}static fromISO(t,e){let[i]=ym(t);return i?s.fromObject(i,e):s.invalid("unparsable",`the input "${t}" can't be parsed as ISO 8601`)}static fromISOTime(t,e){let[i]=_m(t);return i?s.fromObject(i,e):s.invalid("unparsable",`the input "${t}" can't be parsed as ISO 8601`)}static invalid(t,e=null){if(!t)throw new Q("need to specify a reason the Duration is invalid");let i=t instanceof gt?t:new gt(t,e);if(Y.throwOnInvalid)throw new fo(i);return new s({invalid:i})}static normalizeUnit(t){let e={year:"years",years:"years",quarter:"quarters",quarters:"quarters",month:"months",months:"months",week:"weeks",weeks:"weeks",day:"days",days:"days",hour:"hours",hours:"hours",minute:"minutes",minutes:"minutes",second:"seconds",seconds:"seconds",millisecond:"milliseconds",milliseconds:"milliseconds"}[t&&t.toLowerCase()];if(!e)throw new Fi(t);return e}static isDuration(t){return t&&t.isLuxonDuration||!1}get locale(){return this.isValid?this.loc.locale:null}get numberingSystem(){return this.isValid?this.loc.numberingSystem:null}toFormat(t,e={}){let i={...e,floor:e.round!==!1&&e.floor!==!1};return this.isValid?ft.create(this.loc,i).formatDurationFromString(this,t):dl}toHuman(t={}){if(!this.isValid)return dl;let e=t.showZeros!==!1,i=Se.map(n=>{let o=this.values[n];return D(o)||o===0&&!e?null:this.loc.numberFormatter({style:"unit",unitDisplay:"long",...t,unit:n.slice(0,-1)}).format(o)}).filter(n=>n);return this.loc.listFormatter({type:"conjunction",style:t.listStyle||"narrow",...t}).format(i)}toObject(){return this.isValid?{...this.values}:{}}toISO(){if(!this.isValid)return null;let t="P";return this.years!==0&&(t+=this.years+"Y"),(this.months!==0||this.quarters!==0)&&(t+=this.months+this.quarters*3+"M"),this.weeks!==0&&(t+=this.weeks+"W"),this.days!==0&&(t+=this.days+"D"),(this.hours!==0||this.minutes!==0||this.seconds!==0||this.milliseconds!==0)&&(t+="T"),this.hours!==0&&(t+=this.hours+"H"),this.minutes!==0&&(t+=this.minutes+"M"),(this.seconds!==0||this.milliseconds!==0)&&(t+=Eo(this.seconds+this.milliseconds/1e3,3)+"S"),t==="P"&&(t+="T0S"),t}toISOTime(t={}){if(!this.isValid)return null;let e=this.toMillis();return e<0||e>=864e5?null:(t={suppressMilliseconds:!1,suppressSeconds:!1,includePrefix:!1,format:"extended",...t,includeOffset:!1},R.fromMillis(e,{zone:"UTC"}).toISOTime(t))}toJSON(){return this.toISO()}toString(){return this.toISO()}[Symbol.for("nodejs.util.inspect.custom")](){return this.isValid?`Duration { values: ${JSON.stringify(this.values)} }`:`Duration { Invalid, reason: ${this.invalidReason} }`}toMillis(){return this.isValid?pc(this.matrix,this.values):NaN}valueOf(){return this.toMillis()}plus(t){if(!this.isValid)return this;let e=s.fromDurationLike(t),i={};for(let n of Se)(ss(e.values,n)||ss(this.values,n))&&(i[n]=e.get(n)+this.get(n));return Ut(this,{values:i},!0)}minus(t){if(!this.isValid)return this;let e=s.fromDurationLike(t);return this.plus(e.negate())}mapUnits(t){if(!this.isValid)return this;let e={};for(let i of Object.keys(this.values))e[i]=ec(t(this.values[i],i));return Ut(this,{values:e},!0)}get(t){return this[s.normalizeUnit(t)]}set(t){if(!this.isValid)return this;let e={...this.values,...Wi(t,s.normalizeUnit)};return Ut(this,{values:e})}reconfigure({locale:t,numberingSystem:e,conversionAccuracy:i,matrix:n}={}){let r={loc:this.loc.clone({locale:t,numberingSystem:e}),matrix:n,conversionAccuracy:i};return Ut(this,r)}as(t){return this.isValid?this.shiftTo(t).get(t):NaN}normalize(){if(!this.isValid)return this;let t=this.toObject();return fl(this.matrix,t),Ut(this,{values:t},!0)}rescale(){if(!this.isValid)return this;let t=gl(this.normalize().shiftToAll().toObject());return Ut(this,{values:t},!0)}shiftTo(...t){if(!this.isValid)return this;if(t.length===0)return this;t=t.map(r=>s.normalizeUnit(r));let e={},i={},n=this.toObject(),o;for(let r of Se)if(t.indexOf(r)>=0){o=r;let a=0;for(let c in i)a+=this.matrix[c][r]*i[c],i[c]=0;re(n[r])&&(a+=n[r]);let l=Math.trunc(a);e[r]=l,i[r]=(a*1e3-l*1e3)/1e3}else re(n[r])&&(i[r]=n[r]);for(let r in i)i[r]!==0&&(e[o]+=r===o?i[r]:i[r]/this.matrix[o][r]);return fl(this.matrix,e),Ut(this,{values:e},!0)}shiftToAll(){return this.isValid?this.shiftTo("years","months","weeks","days","hours","minutes","seconds","milliseconds"):this}negate(){if(!this.isValid)return this;let t={};for(let e of Object.keys(this.values))t[e]=this.values[e]===0?0:-this.values[e];return Ut(this,{values:t},!0)}removeZeros(){if(!this.isValid)return this;let t=gl(this.values);return Ut(this,{values:t},!0)}get years(){return this.isValid?this.values.years||0:NaN}get quarters(){return this.isValid?this.values.quarters||0:NaN}get months(){return this.isValid?this.values.months||0:NaN}get weeks(){return this.isValid?this.values.weeks||0:NaN}get days(){return this.isValid?this.values.days||0:NaN}get hours(){return this.isValid?this.values.hours||0:NaN}get minutes(){return this.isValid?this.values.minutes||0:NaN}get seconds(){return this.isValid?this.values.seconds||0:NaN}get milliseconds(){return this.isValid?this.values.milliseconds||0:NaN}get isValid(){return this.invalid===null}get invalidReason(){return this.invalid?this.invalid.reason:null}get invalidExplanation(){return this.invalid?this.invalid.explanation:null}equals(t){if(!this.isValid||!t.isValid||!this.loc.equals(t.loc))return!1;function e(i,n){return i===void 0||i===0?n===void 0||n===0:i===n}for(let i of Se)if(!e(this.values[i],t.values[i]))return!1;return!0}},Xe="Invalid Interval";function Dm(s,t){return!s||!s.isValid?es.invalid("missing or invalid start"):!t||!t.isValid?es.invalid("missing or invalid end"):tt:!1}isBefore(t){return this.isValid?this.e<=t:!1}contains(t){return this.isValid?this.s<=t&&this.e>t:!1}set({start:t,end:e}={}){return this.isValid?s.fromDateTimes(t||this.s,e||this.e):this}splitAt(...t){if(!this.isValid)return[];let e=t.map(Fs).filter(r=>this.contains(r)).sort((r,a)=>r.toMillis()-a.toMillis()),i=[],{s:n}=this,o=0;for(;n+this.e?this.e:r;i.push(s.fromDateTimes(n,a)),n=a,o+=1}return i}splitBy(t){let e=tt.fromDurationLike(t);if(!this.isValid||!e.isValid||e.as("milliseconds")===0)return[];let{s:i}=this,n=1,o,r=[];for(;il*n));o=+a>+this.e?this.e:a,r.push(s.fromDateTimes(i,o)),i=o,n+=1}return r}divideEqually(t){return this.isValid?this.splitBy(this.length()/t).slice(0,t):[]}overlaps(t){return this.e>t.s&&this.s=t.e:!1}equals(t){return!this.isValid||!t.isValid?!1:this.s.equals(t.s)&&this.e.equals(t.e)}intersection(t){if(!this.isValid)return this;let e=this.s>t.s?this.s:t.s,i=this.e=i?null:s.fromDateTimes(e,i)}union(t){if(!this.isValid)return this;let e=this.st.e?this.e:t.e;return s.fromDateTimes(e,i)}static merge(t){let[e,i]=t.sort((n,o)=>n.s-o.s).reduce(([n,o],r)=>o?o.overlaps(r)||o.abutsStart(r)?[n,o.union(r)]:[n.concat([o]),r]:[n,r],[[],null]);return i&&e.push(i),e}static xor(t){let e=null,i=0,n=[],o=t.map(l=>[{time:l.s,type:"s"},{time:l.e,type:"e"}]),r=Array.prototype.concat(...o),a=r.sort((l,c)=>l.time-c.time);for(let l of a)i+=l.type==="s"?1:-1,i===1?e=l.time:(e&&+e!=+l.time&&n.push(s.fromDateTimes(e,l.time)),e=null);return s.merge(n)}difference(...t){return s.xor([this].concat(t)).map(e=>this.intersection(e)).filter(e=>e&&!e.isEmpty())}toString(){return this.isValid?`[${this.s.toISO()} \u2013 ${this.e.toISO()})`:Xe}[Symbol.for("nodejs.util.inspect.custom")](){return this.isValid?`Interval { start: ${this.s.toISO()}, end: ${this.e.toISO()} }`:`Interval { Invalid, reason: ${this.invalidReason} }`}toLocaleString(t=Ri,e={}){return this.isValid?ft.create(this.s.loc.clone(e),t).formatInterval(this):Xe}toISO(t){return this.isValid?`${this.s.toISO(t)}/${this.e.toISO(t)}`:Xe}toISODate(){return this.isValid?`${this.s.toISODate()}/${this.e.toISODate()}`:Xe}toISOTime(t){return this.isValid?`${this.s.toISOTime(t)}/${this.e.toISOTime(t)}`:Xe}toFormat(t,{separator:e=" \u2013 "}={}){return this.isValid?`${this.s.toFormat(t)}${e}${this.e.toFormat(t)}`:Xe}toDuration(t,e){return this.isValid?this.e.diff(this.s,t,e):tt.invalid(this.invalidReason)}mapEndpoints(t){return s.fromDateTimes(t(this.s),t(this.e))}},Ke=class{static hasDST(t=Y.defaultZone){let e=R.now().setZone(t).set({month:12});return!t.isUniversal&&e.offset!==e.set({month:6}).offset}static isValidIANAZone(t){return ae.isValidZone(t)}static normalizeZone(t){return ne(t,Y.defaultZone)}static getStartOfWeek({locale:t=null,locObj:e=null}={}){return(e||B.create(t)).getStartOfWeek()}static getMinimumDaysInFirstWeek({locale:t=null,locObj:e=null}={}){return(e||B.create(t)).getMinDaysInFirstWeek()}static getWeekendWeekdays({locale:t=null,locObj:e=null}={}){return(e||B.create(t)).getWeekendDays().slice()}static months(t="long",{locale:e=null,numberingSystem:i=null,locObj:n=null,outputCalendar:o="gregory"}={}){return(n||B.create(e,i,o)).months(t)}static monthsFormat(t="long",{locale:e=null,numberingSystem:i=null,locObj:n=null,outputCalendar:o="gregory"}={}){return(n||B.create(e,i,o)).months(t,!0)}static weekdays(t="long",{locale:e=null,numberingSystem:i=null,locObj:n=null}={}){return(n||B.create(e,i,null)).weekdays(t)}static weekdaysFormat(t="long",{locale:e=null,numberingSystem:i=null,locObj:n=null}={}){return(n||B.create(e,i,null)).weekdays(t,!0)}static meridiems({locale:t=null}={}){return B.create(t).meridiems()}static eras(t="short",{locale:e=null}={}){return B.create(e,null,"gregory").eras(t)}static features(){return{relative:Kl(),localeWeek:Ql()}}};function ml(s,t){let e=n=>n.toUTC(0,{keepLocalTime:!0}).startOf("day").valueOf(),i=e(t)-e(s);return Math.floor(tt.fromMillis(i).as("days"))}function Cm(s,t,e){let i=[["years",(l,c)=>c.year-l.year],["quarters",(l,c)=>c.quarter-l.quarter+(c.year-l.year)*4],["months",(l,c)=>c.month-l.month+(c.year-l.year)*12],["weeks",(l,c)=>{let h=ml(l,c);return(h-h%7)/7}],["days",ml]],n={},o=s,r,a;for(let[l,c]of i)e.indexOf(l)>=0&&(r=l,n[l]=c(s,t),a=o.plus(n),a>t?(n[l]--,s=o.plus(n),s>t&&(a=s,n[l]--,s=o.plus(n))):s=a);return[s,n,a,r]}function Pm(s,t,e,i){let[n,o,r,a]=Cm(s,t,e),l=t-n,c=e.filter(u=>["hours","minutes","seconds","milliseconds"].indexOf(u)>=0);c.length===0&&(r0?tt.fromMillis(l,i).shiftTo(...c).plus(h):h}var Am="missing Intl.DateTimeFormat.formatToParts support";function N(s,t=e=>e){return{regex:s,deser:([e])=>t(wg(e))}}var Im="\xA0",bc=`[ ${Im}]`,yc=new RegExp(bc,"g");function Em(s){return s.replace(/\./g,"\\.?").replace(yc,bc)}function pl(s){return s.replace(/\./g,"").replace(yc," ").toLowerCase()}function Tt(s,t){return s===null?null:{regex:RegExp(s.map(Em).join("|")),deser:([e])=>s.findIndex(i=>pl(e)===pl(i))+t}}function bl(s,t){return{regex:s,deser:([,e,i])=>ji(e,i),groups:t}}function Ci(s){return{regex:s,deser:([t])=>t}}function Lm(s){return s.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g,"\\$&")}function Fm(s,t){let e=Ot(t),i=Ot(t,"{2}"),n=Ot(t,"{3}"),o=Ot(t,"{4}"),r=Ot(t,"{6}"),a=Ot(t,"{1,2}"),l=Ot(t,"{1,3}"),c=Ot(t,"{1,6}"),h=Ot(t,"{1,9}"),u=Ot(t,"{2,4}"),d=Ot(t,"{4,6}"),f=p=>({regex:RegExp(Lm(p.val)),deser:([b])=>b,literal:!0}),m=(p=>{if(s.literal)return f(p);switch(p.val){case"G":return Tt(t.eras("short"),0);case"GG":return Tt(t.eras("long"),0);case"y":return N(c);case"yy":return N(u,To);case"yyyy":return N(o);case"yyyyy":return N(d);case"yyyyyy":return N(r);case"M":return N(a);case"MM":return N(i);case"MMM":return Tt(t.months("short",!0),1);case"MMMM":return Tt(t.months("long",!0),1);case"L":return N(a);case"LL":return N(i);case"LLL":return Tt(t.months("short",!1),1);case"LLLL":return Tt(t.months("long",!1),1);case"d":return N(a);case"dd":return N(i);case"o":return N(l);case"ooo":return N(n);case"HH":return N(i);case"H":return N(a);case"hh":return N(i);case"h":return N(a);case"mm":return N(i);case"m":return N(a);case"q":return N(a);case"qq":return N(i);case"s":return N(a);case"ss":return N(i);case"S":return N(l);case"SSS":return N(n);case"u":return Ci(h);case"uu":return Ci(a);case"uuu":return N(e);case"a":return Tt(t.meridiems(),0);case"kkkk":return N(o);case"kk":return N(u,To);case"W":return N(a);case"WW":return N(i);case"E":case"c":return N(e);case"EEE":return Tt(t.weekdays("short",!1),1);case"EEEE":return Tt(t.weekdays("long",!1),1);case"ccc":return Tt(t.weekdays("short",!0),1);case"cccc":return Tt(t.weekdays("long",!0),1);case"Z":case"ZZ":return bl(new RegExp(`([+-]${a.source})(?::(${i.source}))?`),2);case"ZZZ":return bl(new RegExp(`([+-]${a.source})(${i.source})?`),2);case"z":return Ci(/[a-z_+-/]{1,256}?/i);case" ":return Ci(/[^\S\n\r]/);default:return f(p)}})(s)||{invalidReason:Am};return m.token=s,m}var Rm={year:{"2-digit":"yy",numeric:"yyyyy"},month:{numeric:"M","2-digit":"MM",short:"MMM",long:"MMMM"},day:{numeric:"d","2-digit":"dd"},weekday:{short:"EEE",long:"EEEE"},dayperiod:"a",dayPeriod:"a",hour12:{numeric:"h","2-digit":"hh"},hour24:{numeric:"H","2-digit":"HH"},minute:{numeric:"m","2-digit":"mm"},second:{numeric:"s","2-digit":"ss"},timeZoneName:{long:"ZZZZZ",short:"ZZZ"}};function Nm(s,t,e){let{type:i,value:n}=s;if(i==="literal"){let l=/^\s+$/.test(n);return{literal:!l,val:l?" ":n}}let o=t[i],r=i;i==="hour"&&(t.hour12!=null?r=t.hour12?"hour12":"hour24":t.hourCycle!=null?t.hourCycle==="h11"||t.hourCycle==="h12"?r="hour12":r="hour24":r=e.hour12?"hour12":"hour24");let a=Rm[r];if(typeof a=="object"&&(a=a[o]),a)return{literal:!1,val:a}}function zm(s){return[`^${s.map(e=>e.regex).reduce((e,i)=>`${e}(${i.source})`,"")}$`,s]}function Vm(s,t,e){let i=s.match(t);if(i){let n={},o=1;for(let r in e)if(ss(e,r)){let a=e[r],l=a.groups?a.groups+1:1;!a.literal&&a.token&&(n[a.token.val[0]]=a.deser(i.slice(o,o+l))),o+=l}return[i,n]}else return[i,{}]}function Wm(s){let t=o=>{switch(o){case"S":return"millisecond";case"s":return"second";case"m":return"minute";case"h":case"H":return"hour";case"d":return"day";case"o":return"ordinal";case"L":case"M":return"month";case"y":return"year";case"E":case"c":return"weekday";case"W":return"weekNumber";case"k":return"weekYear";case"q":return"quarter";default:return null}},e=null,i;return D(s.z)||(e=ae.create(s.z)),D(s.Z)||(e||(e=new kt(s.Z)),i=s.Z),D(s.q)||(s.M=(s.q-1)*3+1),D(s.h)||(s.h<12&&s.a===1?s.h+=12:s.h===12&&s.a===0&&(s.h=0)),s.G===0&&s.y&&(s.y=-s.y),D(s.u)||(s.S=Io(s.u)),[Object.keys(s).reduce((o,r)=>{let a=t(r);return a&&(o[a]=s[r]),o},{}),e,i]}var oo=null;function Bm(){return oo||(oo=R.fromMillis(1555555555555)),oo}function Hm(s,t){if(s.literal)return s;let e=ft.macroTokenToFormatOpts(s.val),i=wc(e,t);return i==null||i.includes(void 0)?s:i}function xc(s,t){return Array.prototype.concat(...s.map(e=>Hm(e,t)))}var Bi=class{constructor(t,e){if(this.locale=t,this.format=e,this.tokens=xc(ft.parseFormat(e),t),this.units=this.tokens.map(i=>Fm(i,t)),this.disqualifyingUnit=this.units.find(i=>i.invalidReason),!this.disqualifyingUnit){let[i,n]=zm(this.units);this.regex=RegExp(i,"i"),this.handlers=n}}explainFromTokens(t){if(this.isValid){let[e,i]=Vm(t,this.regex,this.handlers),[n,o,r]=i?Wm(i):[null,null,void 0];if(ss(i,"a")&&ss(i,"H"))throw new oe("Can't include meridiem when specifying 24-hour format");return{input:t,tokens:this.tokens,regex:this.regex,rawMatches:e,matches:i,result:n,zone:o,specificOffset:r}}else return{input:t,tokens:this.tokens,invalidReason:this.invalidReason}}get isValid(){return!this.disqualifyingUnit}get invalidReason(){return this.disqualifyingUnit?this.disqualifyingUnit.invalidReason:null}};function _c(s,t,e){return new Bi(s,e).explainFromTokens(t)}function $m(s,t,e){let{result:i,zone:n,specificOffset:o,invalidReason:r}=_c(s,t,e);return[i,n,o,r]}function wc(s,t){if(!s)return null;let i=ft.create(t,s).dtFormatter(Bm()),n=i.formatToParts(),o=i.resolvedOptions();return n.map(r=>Nm(r,s,o))}var ro="Invalid DateTime",yl=864e13;function Ns(s){return new gt("unsupported zone",`the zone "${s.name}" is not supported`)}function ao(s){return s.weekData===null&&(s.weekData=zi(s.c)),s.weekData}function lo(s){return s.localWeekData===null&&(s.localWeekData=zi(s.c,s.loc.getMinDaysInFirstWeek(),s.loc.getStartOfWeek())),s.localWeekData}function ve(s,t){let e={ts:s.ts,zone:s.zone,c:s.c,o:s.o,loc:s.loc,invalid:s.invalid};return new R({...e,...t,old:e})}function kc(s,t,e){let i=s-t*60*1e3,n=e.offset(i);if(t===n)return[i,t];i-=(n-t)*60*1e3;let o=e.offset(i);return n===o?[i,n]:[s-Math.min(n,o)*60*1e3,Math.max(n,o)]}function Pi(s,t){s+=t*60*1e3;let e=new Date(s);return{year:e.getUTCFullYear(),month:e.getUTCMonth()+1,day:e.getUTCDate(),hour:e.getUTCHours(),minute:e.getUTCMinutes(),second:e.getUTCSeconds(),millisecond:e.getUTCMilliseconds()}}function Ii(s,t,e){return kc($i(s),t,e)}function xl(s,t){let e=s.o,i=s.c.year+Math.trunc(t.years),n=s.c.month+Math.trunc(t.months)+Math.trunc(t.quarters)*3,o={...s.c,year:i,month:n,day:Math.min(s.c.day,Vi(i,n))+Math.trunc(t.days)+Math.trunc(t.weeks)*7},r=tt.fromObject({years:t.years-Math.trunc(t.years),quarters:t.quarters-Math.trunc(t.quarters),months:t.months-Math.trunc(t.months),weeks:t.weeks-Math.trunc(t.weeks),days:t.days-Math.trunc(t.days),hours:t.hours,minutes:t.minutes,seconds:t.seconds,milliseconds:t.milliseconds}).as("milliseconds"),a=$i(o),[l,c]=kc(a,e,s.zone);return r!==0&&(l+=r,c=s.zone.offset(l)),{ts:l,o:c}}function Je(s,t,e,i,n,o){let{setZone:r,zone:a}=e;if(s&&Object.keys(s).length!==0||t){let l=t||a,c=R.fromObject(s,{...e,zone:l,specificOffset:o});return r?c:c.setZone(a)}else return R.invalid(new gt("unparsable",`the input "${n}" can't be parsed as ${i}`))}function Ai(s,t,e=!0){return s.isValid?ft.create(B.create("en-US"),{allowZ:e,forceSimple:!0}).formatDateTimeFromString(s,t):null}function co(s,t,e){let i=s.c.year>9999||s.c.year<0,n="";if(i&&s.c.year>=0&&(n+="+"),n+=J(s.c.year,i?6:4),e==="year")return n;if(t){if(n+="-",n+=J(s.c.month),e==="month")return n;n+="-"}else if(n+=J(s.c.month),e==="month")return n;return n+=J(s.c.day),n}function _l(s,t,e,i,n,o,r){let a=!e||s.c.millisecond!==0||s.c.second!==0,l="";switch(r){case"day":case"month":case"year":break;default:if(l+=J(s.c.hour),r==="hour")break;if(t){if(l+=":",l+=J(s.c.minute),r==="minute")break;a&&(l+=":",l+=J(s.c.second))}else{if(l+=J(s.c.minute),r==="minute")break;a&&(l+=J(s.c.second))}if(r==="second")break;a&&(!i||s.c.millisecond!==0)&&(l+=".",l+=J(s.c.millisecond,3))}return n&&(s.isOffsetFixed&&s.offset===0&&!o?l+="Z":s.o<0?(l+="-",l+=J(Math.trunc(-s.o/60)),l+=":",l+=J(Math.trunc(-s.o%60))):(l+="+",l+=J(Math.trunc(s.o/60)),l+=":",l+=J(Math.trunc(s.o%60)))),o&&(l+="["+s.zone.ianaName+"]"),l}var vc={month:1,day:1,hour:0,minute:0,second:0,millisecond:0},jm={weekNumber:1,weekday:1,hour:0,minute:0,second:0,millisecond:0},Um={ordinal:1,hour:0,minute:0,second:0,millisecond:0},Ei=["year","month","day","hour","minute","second","millisecond"],Ym=["weekYear","weekNumber","weekday","hour","minute","second","millisecond"],Zm=["year","ordinal","hour","minute","second","millisecond"];function Li(s){let t={year:"year",years:"year",month:"month",months:"month",day:"day",days:"day",hour:"hour",hours:"hour",minute:"minute",minutes:"minute",quarter:"quarter",quarters:"quarter",second:"second",seconds:"second",millisecond:"millisecond",milliseconds:"millisecond",weekday:"weekday",weekdays:"weekday",weeknumber:"weekNumber",weeksnumber:"weekNumber",weeknumbers:"weekNumber",weekyear:"weekYear",weekyears:"weekYear",ordinal:"ordinal"}[s.toLowerCase()];if(!t)throw new Fi(s);return t}function wl(s){switch(s.toLowerCase()){case"localweekday":case"localweekdays":return"localWeekday";case"localweeknumber":case"localweeknumbers":return"localWeekNumber";case"localweekyear":case"localweekyears":return"localWeekYear";default:return Li(s)}}function qm(s){if(zs===void 0&&(zs=Y.now()),s.type!=="iana")return s.offset(zs);let t=s.name,e=Do.get(t);return e===void 0&&(e=s.offset(zs),Do.set(t,e)),e}function kl(s,t){let e=ne(t.zone,Y.defaultZone);if(!e.isValid)return R.invalid(Ns(e));let i=B.fromObject(t),n,o;if(D(s.year))n=Y.now();else{for(let l of Ei)D(s[l])&&(s[l]=vc[l]);let r=Xl(s)||Jl(s);if(r)return R.invalid(r);let a=qm(e);[n,o]=Ii(s,a,e)}return new R({ts:n,zone:e,loc:i,o})}function vl(s,t,e){let i=D(e.round)?!0:e.round,n=D(e.rounding)?"trunc":e.rounding,o=(a,l)=>(a=Eo(a,i||e.calendary?0:2,e.calendary?"round":n),t.loc.clone(e).relFormatter(e).format(a,l)),r=a=>e.calendary?t.hasSame(s,a)?0:t.startOf(a).diff(s.startOf(a),a).get(a):t.diff(s,a).get(a);if(e.unit)return o(r(e.unit),e.unit);for(let a of e.units){let l=r(a);if(Math.abs(l)>=1)return o(l,a)}return o(s>t?-0:0,e.units[e.units.length-1])}function Sl(s){let t={},e;return s.length>0&&typeof s[s.length-1]=="object"?(t=s[s.length-1],e=Array.from(s).slice(0,s.length-1)):e=Array.from(s),[t,e]}var zs,Do=new Map,R=class s{constructor(t){let e=t.zone||Y.defaultZone,i=t.invalid||(Number.isNaN(t.ts)?new gt("invalid input"):null)||(e.isValid?null:Ns(e));this.ts=D(t.ts)?Y.now():t.ts;let n=null,o=null;if(!i)if(t.old&&t.old.ts===this.ts&&t.old.zone.equals(e))[n,o]=[t.old.c,t.old.o];else{let a=re(t.o)&&!t.old?t.o:e.offset(this.ts);n=Pi(this.ts,a),i=Number.isNaN(n.year)?new gt("invalid input"):null,n=i?null:n,o=i?null:a}this._zone=e,this.loc=t.loc||B.create(),this.invalid=i,this.weekData=null,this.localWeekData=null,this.c=n,this.o=o,this.isLuxonDateTime=!0}static now(){return new s({})}static local(){let[t,e]=Sl(arguments),[i,n,o,r,a,l,c]=e;return kl({year:i,month:n,day:o,hour:r,minute:a,second:l,millisecond:c},t)}static utc(){let[t,e]=Sl(arguments),[i,n,o,r,a,l,c]=e;return t.zone=kt.utcInstance,kl({year:i,month:n,day:o,hour:r,minute:a,second:l,millisecond:c},t)}static fromJSDate(t,e={}){let i=Og(t)?t.valueOf():NaN;if(Number.isNaN(i))return s.invalid("invalid input");let n=ne(e.zone,Y.defaultZone);return n.isValid?new s({ts:i,zone:n,loc:B.fromObject(e)}):s.invalid(Ns(n))}static fromMillis(t,e={}){if(re(t))return t<-yl||t>yl?s.invalid("Timestamp out of range"):new s({ts:t,zone:ne(e.zone,Y.defaultZone),loc:B.fromObject(e)});throw new Q(`fromMillis requires a numerical input, but received a ${typeof t} with value ${t}`)}static fromSeconds(t,e={}){if(re(t))return new s({ts:t*1e3,zone:ne(e.zone,Y.defaultZone),loc:B.fromObject(e)});throw new Q("fromSeconds requires a numerical input")}static fromObject(t,e={}){t=t||{};let i=ne(e.zone,Y.defaultZone);if(!i.isValid)return s.invalid(Ns(i));let n=B.fromObject(e),o=Wi(t,wl),{minDaysInFirstWeek:r,startOfWeek:a}=al(o,n),l=Y.now(),c=D(e.specificOffset)?i.offset(l):e.specificOffset,h=!D(o.ordinal),u=!D(o.year),d=!D(o.month)||!D(o.day),f=u||d,g=o.weekYear||o.weekNumber;if((f||h)&&g)throw new oe("Can't mix weekYear/weekNumber units with year/month/day or ordinals");if(d&&h)throw new oe("Can't mix ordinal dates with month/day");let m=g||o.weekday&&!f,p,b,y=Pi(l,c);m?(p=Ym,b=jm,y=zi(y,r,a)):h?(p=Zm,b=Um,y=no(y)):(p=Ei,b=vc);let _=!1;for(let C of p){let A=o[C];D(A)?_?o[C]=b[C]:o[C]=y[C]:_=!0}let w=m?vg(o,r,a):h?Sg(o):Xl(o),x=w||Jl(o);if(x)return s.invalid(x);let k=m?ol(o,r,a):h?rl(o):o,[S,M]=Ii(k,c,i),T=new s({ts:S,zone:i,o:M,loc:n});return o.weekday&&f&&t.weekday!==T.weekday?s.invalid("mismatched weekday",`you can't specify both a weekday of ${o.weekday} and a date of ${T.toISO()}`):T.isValid?T:s.invalid(T.invalid)}static fromISO(t,e={}){let[i,n]=mm(t);return Je(i,n,e,"ISO 8601",t)}static fromRFC2822(t,e={}){let[i,n]=pm(t);return Je(i,n,e,"RFC 2822",t)}static fromHTTP(t,e={}){let[i,n]=bm(t);return Je(i,n,e,"HTTP",e)}static fromFormat(t,e,i={}){if(D(t)||D(e))throw new Q("fromFormat requires an input string and a format");let{locale:n=null,numberingSystem:o=null}=i,r=B.fromOpts({locale:n,numberingSystem:o,defaultToEN:!0}),[a,l,c,h]=$m(r,t,e);return h?s.invalid(h):Je(a,l,i,`format ${e}`,t,c)}static fromString(t,e,i={}){return s.fromFormat(t,e,i)}static fromSQL(t,e={}){let[i,n]=Sm(t);return Je(i,n,e,"SQL",t)}static invalid(t,e=null){if(!t)throw new Q("need to specify a reason the DateTime is invalid");let i=t instanceof gt?t:new gt(t,e);if(Y.throwOnInvalid)throw new ho(i);return new s({invalid:i})}static isDateTime(t){return t&&t.isLuxonDateTime||!1}static parseFormatForOpts(t,e={}){let i=wc(t,B.fromObject(e));return i?i.map(n=>n?n.val:null).join(""):null}static expandFormat(t,e={}){return xc(ft.parseFormat(t),B.fromObject(e)).map(n=>n.val).join("")}static resetCache(){zs=void 0,Do.clear()}get(t){return this[t]}get isValid(){return this.invalid===null}get invalidReason(){return this.invalid?this.invalid.reason:null}get invalidExplanation(){return this.invalid?this.invalid.explanation:null}get locale(){return this.isValid?this.loc.locale:null}get numberingSystem(){return this.isValid?this.loc.numberingSystem:null}get outputCalendar(){return this.isValid?this.loc.outputCalendar:null}get zone(){return this._zone}get zoneName(){return this.isValid?this.zone.name:null}get year(){return this.isValid?this.c.year:NaN}get quarter(){return this.isValid?Math.ceil(this.c.month/3):NaN}get month(){return this.isValid?this.c.month:NaN}get day(){return this.isValid?this.c.day:NaN}get hour(){return this.isValid?this.c.hour:NaN}get minute(){return this.isValid?this.c.minute:NaN}get second(){return this.isValid?this.c.second:NaN}get millisecond(){return this.isValid?this.c.millisecond:NaN}get weekYear(){return this.isValid?ao(this).weekYear:NaN}get weekNumber(){return this.isValid?ao(this).weekNumber:NaN}get weekday(){return this.isValid?ao(this).weekday:NaN}get isWeekend(){return this.isValid&&this.loc.getWeekendDays().includes(this.weekday)}get localWeekday(){return this.isValid?lo(this).weekday:NaN}get localWeekNumber(){return this.isValid?lo(this).weekNumber:NaN}get localWeekYear(){return this.isValid?lo(this).weekYear:NaN}get ordinal(){return this.isValid?no(this.c).ordinal:NaN}get monthShort(){return this.isValid?Ke.months("short",{locObj:this.loc})[this.month-1]:null}get monthLong(){return this.isValid?Ke.months("long",{locObj:this.loc})[this.month-1]:null}get weekdayShort(){return this.isValid?Ke.weekdays("short",{locObj:this.loc})[this.weekday-1]:null}get weekdayLong(){return this.isValid?Ke.weekdays("long",{locObj:this.loc})[this.weekday-1]:null}get offset(){return this.isValid?+this.o:NaN}get offsetNameShort(){return this.isValid?this.zone.offsetName(this.ts,{format:"short",locale:this.locale}):null}get offsetNameLong(){return this.isValid?this.zone.offsetName(this.ts,{format:"long",locale:this.locale}):null}get isOffsetFixed(){return this.isValid?this.zone.isUniversal:null}get isInDST(){return this.isOffsetFixed?!1:this.offset>this.set({month:1,day:1}).offset||this.offset>this.set({month:5}).offset}getPossibleOffsets(){if(!this.isValid||this.isOffsetFixed)return[this];let t=864e5,e=6e4,i=$i(this.c),n=this.zone.offset(i-t),o=this.zone.offset(i+t),r=this.zone.offset(i-n*e),a=this.zone.offset(i-o*e);if(r===a)return[this];let l=i-r*e,c=i-a*e,h=Pi(l,r),u=Pi(c,a);return h.hour===u.hour&&h.minute===u.minute&&h.second===u.second&&h.millisecond===u.millisecond?[ve(this,{ts:l}),ve(this,{ts:c})]:[this]}get isInLeapYear(){return Bs(this.year)}get daysInMonth(){return Vi(this.year,this.month)}get daysInYear(){return this.isValid?Qe(this.year):NaN}get weeksInWeekYear(){return this.isValid?Ws(this.weekYear):NaN}get weeksInLocalWeekYear(){return this.isValid?Ws(this.localWeekYear,this.loc.getMinDaysInFirstWeek(),this.loc.getStartOfWeek()):NaN}resolvedLocaleOptions(t={}){let{locale:e,numberingSystem:i,calendar:n}=ft.create(this.loc.clone(t),t).resolvedOptions(this);return{locale:e,numberingSystem:i,outputCalendar:n}}toUTC(t=0,e={}){return this.setZone(kt.instance(t),e)}toLocal(){return this.setZone(Y.defaultZone)}setZone(t,{keepLocalTime:e=!1,keepCalendarTime:i=!1}={}){if(t=ne(t,Y.defaultZone),t.equals(this.zone))return this;if(t.isValid){let n=this.ts;if(e||i){let o=t.offset(this.ts),r=this.toObject();[n]=Ii(r,o,t)}return ve(this,{ts:n,zone:t})}else return s.invalid(Ns(t))}reconfigure({locale:t,numberingSystem:e,outputCalendar:i}={}){let n=this.loc.clone({locale:t,numberingSystem:e,outputCalendar:i});return ve(this,{loc:n})}setLocale(t){return this.reconfigure({locale:t})}set(t){if(!this.isValid)return this;let e=Wi(t,wl),{minDaysInFirstWeek:i,startOfWeek:n}=al(e,this.loc),o=!D(e.weekYear)||!D(e.weekNumber)||!D(e.weekday),r=!D(e.ordinal),a=!D(e.year),l=!D(e.month)||!D(e.day),c=a||l,h=e.weekYear||e.weekNumber;if((c||r)&&h)throw new oe("Can't mix weekYear/weekNumber units with year/month/day or ordinals");if(l&&r)throw new oe("Can't mix ordinal dates with month/day");let u;o?u=ol({...zi(this.c,i,n),...e},i,n):D(e.ordinal)?(u={...this.toObject(),...e},D(e.day)&&(u.day=Math.min(Vi(u.year,u.month),u.day))):u=rl({...no(this.c),...e});let[d,f]=Ii(u,this.o,this.zone);return ve(this,{ts:d,o:f})}plus(t){if(!this.isValid)return this;let e=tt.fromDurationLike(t);return ve(this,xl(this,e))}minus(t){if(!this.isValid)return this;let e=tt.fromDurationLike(t).negate();return ve(this,xl(this,e))}startOf(t,{useLocaleWeeks:e=!1}={}){if(!this.isValid)return this;let i={},n=tt.normalizeUnit(t);switch(n){case"years":i.month=1;case"quarters":case"months":i.day=1;case"weeks":case"days":i.hour=0;case"hours":i.minute=0;case"minutes":i.second=0;case"seconds":i.millisecond=0;break}if(n==="weeks")if(e){let o=this.loc.getStartOfWeek(),{weekday:r}=this;r=3&&(l+="T"),l+=_l(this,a,e,i,n,o,r),l}toISODate({format:t="extended",precision:e="day"}={}){return this.isValid?co(this,t==="extended",Li(e)):null}toISOWeekDate(){return Ai(this,"kkkk-'W'WW-c")}toISOTime({suppressMilliseconds:t=!1,suppressSeconds:e=!1,includeOffset:i=!0,includePrefix:n=!1,extendedZone:o=!1,format:r="extended",precision:a="milliseconds"}={}){return this.isValid?(a=Li(a),(n&&Ei.indexOf(a)>=3?"T":"")+_l(this,r==="extended",e,t,i,o,a)):null}toRFC2822(){return Ai(this,"EEE, dd LLL yyyy HH:mm:ss ZZZ",!1)}toHTTP(){return Ai(this.toUTC(),"EEE, dd LLL yyyy HH:mm:ss 'GMT'")}toSQLDate(){return this.isValid?co(this,!0):null}toSQLTime({includeOffset:t=!0,includeZone:e=!1,includeOffsetSpace:i=!0}={}){let n="HH:mm:ss.SSS";return(e||t)&&(i&&(n+=" "),e?n+="z":t&&(n+="ZZ")),Ai(this,n,!0)}toSQL(t={}){return this.isValid?`${this.toSQLDate()} ${this.toSQLTime(t)}`:null}toString(){return this.isValid?this.toISO():ro}[Symbol.for("nodejs.util.inspect.custom")](){return this.isValid?`DateTime { ts: ${this.toISO()}, zone: ${this.zone.name}, locale: ${this.locale} }`:`DateTime { Invalid, reason: ${this.invalidReason} }`}valueOf(){return this.toMillis()}toMillis(){return this.isValid?this.ts:NaN}toSeconds(){return this.isValid?this.ts/1e3:NaN}toUnixInteger(){return this.isValid?Math.floor(this.ts/1e3):NaN}toJSON(){return this.toISO()}toBSON(){return this.toJSDate()}toObject(t={}){if(!this.isValid)return{};let e={...this.c};return t.includeConfig&&(e.outputCalendar=this.outputCalendar,e.numberingSystem=this.loc.numberingSystem,e.locale=this.loc.locale),e}toJSDate(){return new Date(this.isValid?this.ts:NaN)}diff(t,e="milliseconds",i={}){if(!this.isValid||!t.isValid)return tt.invalid("created by diffing an invalid DateTime");let n={locale:this.locale,numberingSystem:this.numberingSystem,...i},o=Tg(e).map(tt.normalizeUnit),r=t.valueOf()>this.valueOf(),a=r?this:t,l=r?t:this,c=Pm(a,l,o,n);return r?c.negate():c}diffNow(t="milliseconds",e={}){return this.diff(s.now(),t,e)}until(t){return this.isValid?es.fromDateTimes(this,t):this}hasSame(t,e,i){if(!this.isValid)return!1;let n=t.valueOf(),o=this.setZone(t.zone,{keepLocalTime:!0});return o.startOf(e,i)<=n&&n<=o.endOf(e,i)}equals(t){return this.isValid&&t.isValid&&this.valueOf()===t.valueOf()&&this.zone.equals(t.zone)&&this.loc.equals(t.loc)}toRelative(t={}){if(!this.isValid)return null;let e=t.base||s.fromObject({},{zone:this.zone}),i=t.padding?thise.valueOf(),Math.min)}static max(...t){if(!t.every(s.isDateTime))throw new Q("max requires all arguments be DateTimes");return ll(t,e=>e.valueOf(),Math.max)}static fromFormatExplain(t,e,i={}){let{locale:n=null,numberingSystem:o=null}=i,r=B.fromOpts({locale:n,numberingSystem:o,defaultToEN:!0});return _c(r,t,e)}static fromStringExplain(t,e,i={}){return s.fromFormatExplain(t,e,i)}static buildFormatParser(t,e={}){let{locale:i=null,numberingSystem:n=null}=e,o=B.fromOpts({locale:i,numberingSystem:n,defaultToEN:!0});return new Bi(o,t)}static fromFormatParser(t,e,i={}){if(D(t)||D(e))throw new Q("fromFormatParser requires an input string and a format parser");let{locale:n=null,numberingSystem:o=null}=i,r=B.fromOpts({locale:n,numberingSystem:o,defaultToEN:!0});if(!r.equals(e.locale))throw new Q(`fromFormatParser called with a locale of ${r}, but the format parser was created for ${e.locale}`);let{result:a,zone:l,specificOffset:c,invalidReason:h}=e.explainFromTokens(t);return h?s.invalid(h):Je(a,l,i,`format ${e.format}`,t,c)}static get DATE_SHORT(){return Ri}static get DATE_MED(){return Ml}static get DATE_MED_WITH_WEEKDAY(){return ng}static get DATE_FULL(){return Ol}static get DATE_HUGE(){return Tl}static get TIME_SIMPLE(){return Dl}static get TIME_WITH_SECONDS(){return Cl}static get TIME_WITH_SHORT_OFFSET(){return Pl}static get TIME_WITH_LONG_OFFSET(){return Al}static get TIME_24_SIMPLE(){return Il}static get TIME_24_WITH_SECONDS(){return El}static get TIME_24_WITH_SHORT_OFFSET(){return Ll}static get TIME_24_WITH_LONG_OFFSET(){return Fl}static get DATETIME_SHORT(){return Rl}static get DATETIME_SHORT_WITH_SECONDS(){return Nl}static get DATETIME_MED(){return zl}static get DATETIME_MED_WITH_SECONDS(){return Vl}static get DATETIME_MED_WITH_WEEKDAY(){return og}static get DATETIME_FULL(){return Wl}static get DATETIME_FULL_WITH_SECONDS(){return Bl}static get DATETIME_HUGE(){return Hl}static get DATETIME_HUGE_WITH_SECONDS(){return $l}};function Fs(s){if(R.isDateTime(s))return s;if(s&&s.valueOf&&re(s.valueOf()))return R.fromJSDate(s);if(s&&typeof s=="object")return R.fromObject(s);throw new Q(`Unknown datetime argument: ${s}, of type ${typeof s}`)}var Gm={datetime:R.DATETIME_MED_WITH_SECONDS,millisecond:"h:mm:ss.SSS a",second:R.TIME_WITH_SECONDS,minute:R.TIME_SIMPLE,hour:{hour:"numeric"},day:{day:"numeric",month:"short"},week:"DD",month:{month:"short",year:"numeric"},quarter:"'Q'q - yyyy",year:{year:"numeric"}};to._date.override({_id:"luxon",_create:function(s){return R.fromMillis(s,this.options)},init(s){this.options.locale||(this.options.locale=s.locale)},formats:function(){return Gm},parse:function(s,t){let e=this.options,i=typeof s;return s===null||i==="undefined"?null:(i==="number"?s=this._create(s):i==="string"?typeof t=="string"?s=R.fromFormat(s,t,e):s=R.fromISO(s,e):s instanceof Date?s=R.fromJSDate(s,e):i==="object"&&!(s instanceof R)&&(s=R.fromObject(s,e)),s.isValid?s.valueOf():null)},format:function(s,t){let e=this._create(s);return typeof t=="string"?e.toFormat(t):e.toLocaleString(t)},add:function(s,t,e){let i={};return i[e]=t,this._create(s).plus(i).valueOf()},diff:function(s,t,e){return this._create(s).diff(this._create(t)).as(e).valueOf()},startOf:function(s,t,e){if(t==="isoWeek"){e=Math.trunc(Math.min(Math.max(0,e),6));let i=this._create(s);return i.minus({days:(i.weekday-e+7)%7}).startOf("day").valueOf()}return t?this._create(s).startOf(t).valueOf():s},endOf:function(s,t){return this._create(s).endOf(t).valueOf()}});window.filamentChartJsGlobalPlugins&&Array.isArray(window.filamentChartJsGlobalPlugins)&&window.filamentChartJsGlobalPlugins.length>0&&Mt.register(...window.filamentChartJsGlobalPlugins);function Xm({cachedData:s,maxHeight:t,options:e,type:i}){return{userPointBackgroundColor:e?.pointBackgroundColor,userXGridColor:e?.scales?.x?.grid?.color,userYGridColor:e?.scales?.y?.grid?.color,userRadialGridColor:e?.scales?.r?.grid?.color,userRadialTicksColor:e?.scales?.r?.ticks?.color,init(){this.initChart(),this.$wire.$on("updateChartData",({data:n})=>{let o=this.getChart();o&&(s=n,o.data=n,o.update("resize"))}),Alpine.effect(()=>{Alpine.store("theme"),this.$nextTick(()=>{let n=this.getChart();n&&(n.destroy(),this.initChart())})}),window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",()=>{Alpine.store("theme")==="system"&&this.$nextTick(()=>{let n=this.getChart();n&&(n.destroy(),this.initChart())})}),this.resizeObserver=new ResizeObserver(Alpine.debounce(()=>{let n=this.getChart();n&&(n.destroy(),this.initChart())},250)),this.resizeObserver.observe(this.$el)},initChart(n=null){var a,l,c,h,u,d,f,g,m,p,b,y,_,w,x,k;if(!this.$refs.canvas||!this.$refs.backgroundColorElement||!this.$refs.borderColorElement||!this.$refs.textColorElement||!this.$refs.gridColorElement)return;Mt.defaults.animation.duration=0,Mt.defaults.backgroundColor=getComputedStyle(this.$refs.backgroundColorElement).color;let o=getComputedStyle(this.$refs.borderColorElement).color;Mt.defaults.borderColor=o,Mt.defaults.color=getComputedStyle(this.$refs.textColorElement).color,Mt.defaults.font.family=getComputedStyle(this.$el).fontFamily,Mt.defaults.plugins.legend.labels.boxWidth=12,Mt.defaults.plugins.legend.position="bottom";let r=getComputedStyle(this.$refs.gridColorElement).color;if(e??(e={}),e.borderWidth??(e.borderWidth=2),e.maintainAspectRatio??(e.maintainAspectRatio=!!t),e.pointBackgroundColor=this.userPointBackgroundColor??o,e.pointHitRadius??(e.pointHitRadius=4),e.pointRadius??(e.pointRadius=2),e.scales??(e.scales={}),(a=e.scales).x??(a.x={}),(l=e.scales.x).border??(l.border={}),(c=e.scales.x.border).display??(c.display=!1),(h=e.scales.x).grid??(h.grid={}),e.scales.x.grid.color=this.userXGridColor??r,(u=e.scales.x.grid).display??(u.display=!1),(d=e.scales).y??(d.y={}),(f=e.scales.y).border??(f.border={}),(g=e.scales.y.border).display??(g.display=!1),(m=e.scales.y).grid??(m.grid={}),e.scales.y.grid.color=this.userYGridColor??r,["doughnut","pie","polarArea"].includes(i)&&((p=e.scales.x).display??(p.display=!1),(b=e.scales.y).display??(b.display=!1),(y=e.scales.y.grid).display??(y.display=!1)),i==="polarArea"){let S=getComputedStyle(this.$refs.textColorElement).color;(_=e.scales).r??(_.r={}),(w=e.scales.r).grid??(w.grid={}),e.scales.r.grid.color=this.userRadialGridColor??r,(x=e.scales.r).ticks??(x.ticks={}),e.scales.r.ticks.color=this.userRadialTicksColor??S,(k=e.scales.r.ticks).backdropColor??(k.backdropColor="transparent")}return new Mt(this.$refs.canvas,{type:i,data:n??s,options:e,plugins:window.filamentChartJsPlugins??[]})},getChart(){return this.$refs.canvas?Mt.getChart(this.$refs.canvas):null},destroy(){this.resizeObserver&&this.resizeObserver.disconnect(),this.getChart()?.destroy()}}}export{Xm as default}; +/*! Bundled license information: + +@kurkle/color/dist/color.esm.js: + (*! + * @kurkle/color v0.3.4 + * https://github.com/kurkle/color#readme + * (c) 2024 Jukka Kurkela + * Released under the MIT License + *) + +chart.js/dist/chunks/helpers.dataset.js: +chart.js/dist/chart.js: + (*! + * Chart.js v4.5.1 + * https://www.chartjs.org + * (c) 2025 Chart.js Contributors + * Released under the MIT License + *) + +chartjs-adapter-luxon/dist/chartjs-adapter-luxon.esm.js: + (*! + * chartjs-adapter-luxon v1.3.1 + * https://www.chartjs.org + * (c) 2023 chartjs-adapter-luxon Contributors + * Released under the MIT license + *) +*/ diff --git a/public/js/filament/widgets/components/stats-overview/stat/chart.js b/public/js/filament/widgets/components/stats-overview/stat/chart.js new file mode 100644 index 00000000..dd83e710 --- /dev/null +++ b/public/js/filament/widgets/components/stats-overview/stat/chart.js @@ -0,0 +1,22 @@ +var Jo=Object.defineProperty;var Zo=(i,t,e)=>t in i?Jo(i,t,{enumerable:!0,configurable:!0,writable:!0,value:e}):i[t]=e;var M=(i,t,e)=>Zo(i,typeof t!="symbol"?t+"":t,e);function ve(i){return i+.5|0}var wt=(i,t,e)=>Math.max(Math.min(i,e),t);function _e(i){return wt(ve(i*2.55),0,255)}function St(i){return wt(ve(i*255),0,255)}function mt(i){return wt(ve(i/2.55)/100,0,1)}function zs(i){return wt(ve(i*100),0,100)}var nt={0:0,1:1,2:2,3:3,4:4,5:5,6:6,7:7,8:8,9:9,A:10,B:11,C:12,D:13,E:14,F:15,a:10,b:11,c:12,d:13,e:14,f:15},Ai=[..."0123456789ABCDEF"],Qo=i=>Ai[i&15],ta=i=>Ai[(i&240)>>4]+Ai[i&15],Ue=i=>(i&240)>>4===(i&15),ea=i=>Ue(i.r)&&Ue(i.g)&&Ue(i.b)&&Ue(i.a);function ia(i){var t=i.length,e;return i[0]==="#"&&(t===4||t===5?e={r:255&nt[i[1]]*17,g:255&nt[i[2]]*17,b:255&nt[i[3]]*17,a:t===5?nt[i[4]]*17:255}:(t===7||t===9)&&(e={r:nt[i[1]]<<4|nt[i[2]],g:nt[i[3]]<<4|nt[i[4]],b:nt[i[5]]<<4|nt[i[6]],a:t===9?nt[i[7]]<<4|nt[i[8]]:255})),e}var sa=(i,t)=>i<255?t(i):"";function na(i){var t=ea(i)?Qo:ta;return i?"#"+t(i.r)+t(i.g)+t(i.b)+sa(i.a,t):void 0}var oa=/^(hsla?|hwb|hsv)\(\s*([-+.e\d]+)(?:deg)?[\s,]+([-+.e\d]+)%[\s,]+([-+.e\d]+)%(?:[\s,]+([-+.e\d]+)(%)?)?\s*\)$/;function Ns(i,t,e){let s=t*Math.min(e,1-e),n=(o,a=(o+i/30)%12)=>e-s*Math.max(Math.min(a-3,9-a,1),-1);return[n(0),n(8),n(4)]}function aa(i,t,e){let s=(n,o=(n+i/60)%6)=>e-e*t*Math.max(Math.min(o,4-o,1),0);return[s(5),s(3),s(1)]}function ra(i,t,e){let s=Ns(i,1,.5),n;for(t+e>1&&(n=1/(t+e),t*=n,e*=n),n=0;n<3;n++)s[n]*=1-t-e,s[n]+=t;return s}function la(i,t,e,s,n){return i===n?(t-e)/s+(t.5?h/(2-o-a):h/(o+a),l=la(e,s,n,h,o),l=l*60+.5),[l|0,c||0,r]}function Li(i,t,e,s){return(Array.isArray(t)?i(t[0],t[1],t[2]):i(t,e,s)).map(St)}function Ri(i,t,e){return Li(Ns,i,t,e)}function ca(i,t,e){return Li(ra,i,t,e)}function ha(i,t,e){return Li(aa,i,t,e)}function Hs(i){return(i%360+360)%360}function da(i){let t=oa.exec(i),e=255,s;if(!t)return;t[5]!==s&&(e=t[6]?_e(+t[5]):St(+t[5]));let n=Hs(+t[2]),o=+t[3]/100,a=+t[4]/100;return t[1]==="hwb"?s=ca(n,o,a):t[1]==="hsv"?s=ha(n,o,a):s=Ri(n,o,a),{r:s[0],g:s[1],b:s[2],a:e}}function ua(i,t){var e=Ti(i);e[0]=Hs(e[0]+t),e=Ri(e),i.r=e[0],i.g=e[1],i.b=e[2]}function fa(i){if(!i)return;let t=Ti(i),e=t[0],s=zs(t[1]),n=zs(t[2]);return i.a<255?`hsla(${e}, ${s}%, ${n}%, ${mt(i.a)})`:`hsl(${e}, ${s}%, ${n}%)`}var Bs={x:"dark",Z:"light",Y:"re",X:"blu",W:"gr",V:"medium",U:"slate",A:"ee",T:"ol",S:"or",B:"ra",C:"lateg",D:"ights",R:"in",Q:"turquois",E:"hi",P:"ro",O:"al",N:"le",M:"de",L:"yello",F:"en",K:"ch",G:"arks",H:"ea",I:"ightg",J:"wh"},Vs={OiceXe:"f0f8ff",antiquewEte:"faebd7",aqua:"ffff",aquamarRe:"7fffd4",azuY:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"0",blanKedOmond:"ffebcd",Xe:"ff",XeviTet:"8a2be2",bPwn:"a52a2a",burlywood:"deb887",caMtXe:"5f9ea0",KartYuse:"7fff00",KocTate:"d2691e",cSO:"ff7f50",cSnflowerXe:"6495ed",cSnsilk:"fff8dc",crimson:"dc143c",cyan:"ffff",xXe:"8b",xcyan:"8b8b",xgTMnPd:"b8860b",xWay:"a9a9a9",xgYF:"6400",xgYy:"a9a9a9",xkhaki:"bdb76b",xmagFta:"8b008b",xTivegYF:"556b2f",xSange:"ff8c00",xScEd:"9932cc",xYd:"8b0000",xsOmon:"e9967a",xsHgYF:"8fbc8f",xUXe:"483d8b",xUWay:"2f4f4f",xUgYy:"2f4f4f",xQe:"ced1",xviTet:"9400d3",dAppRk:"ff1493",dApskyXe:"bfff",dimWay:"696969",dimgYy:"696969",dodgerXe:"1e90ff",fiYbrick:"b22222",flSOwEte:"fffaf0",foYstWAn:"228b22",fuKsia:"ff00ff",gaRsbSo:"dcdcdc",ghostwEte:"f8f8ff",gTd:"ffd700",gTMnPd:"daa520",Way:"808080",gYF:"8000",gYFLw:"adff2f",gYy:"808080",honeyMw:"f0fff0",hotpRk:"ff69b4",RdianYd:"cd5c5c",Rdigo:"4b0082",ivSy:"fffff0",khaki:"f0e68c",lavFMr:"e6e6fa",lavFMrXsh:"fff0f5",lawngYF:"7cfc00",NmoncEffon:"fffacd",ZXe:"add8e6",ZcSO:"f08080",Zcyan:"e0ffff",ZgTMnPdLw:"fafad2",ZWay:"d3d3d3",ZgYF:"90ee90",ZgYy:"d3d3d3",ZpRk:"ffb6c1",ZsOmon:"ffa07a",ZsHgYF:"20b2aa",ZskyXe:"87cefa",ZUWay:"778899",ZUgYy:"778899",ZstAlXe:"b0c4de",ZLw:"ffffe0",lime:"ff00",limegYF:"32cd32",lRF:"faf0e6",magFta:"ff00ff",maPon:"800000",VaquamarRe:"66cdaa",VXe:"cd",VScEd:"ba55d3",VpurpN:"9370db",VsHgYF:"3cb371",VUXe:"7b68ee",VsprRggYF:"fa9a",VQe:"48d1cc",VviTetYd:"c71585",midnightXe:"191970",mRtcYam:"f5fffa",mistyPse:"ffe4e1",moccasR:"ffe4b5",navajowEte:"ffdead",navy:"80",Tdlace:"fdf5e6",Tive:"808000",TivedBb:"6b8e23",Sange:"ffa500",SangeYd:"ff4500",ScEd:"da70d6",pOegTMnPd:"eee8aa",pOegYF:"98fb98",pOeQe:"afeeee",pOeviTetYd:"db7093",papayawEp:"ffefd5",pHKpuff:"ffdab9",peru:"cd853f",pRk:"ffc0cb",plum:"dda0dd",powMrXe:"b0e0e6",purpN:"800080",YbeccapurpN:"663399",Yd:"ff0000",Psybrown:"bc8f8f",PyOXe:"4169e1",saddNbPwn:"8b4513",sOmon:"fa8072",sandybPwn:"f4a460",sHgYF:"2e8b57",sHshell:"fff5ee",siFna:"a0522d",silver:"c0c0c0",skyXe:"87ceeb",UXe:"6a5acd",UWay:"708090",UgYy:"708090",snow:"fffafa",sprRggYF:"ff7f",stAlXe:"4682b4",tan:"d2b48c",teO:"8080",tEstN:"d8bfd8",tomato:"ff6347",Qe:"40e0d0",viTet:"ee82ee",JHt:"f5deb3",wEte:"ffffff",wEtesmoke:"f5f5f5",Lw:"ffff00",LwgYF:"9acd32"};function ga(){let i={},t=Object.keys(Vs),e=Object.keys(Bs),s,n,o,a,r;for(s=0;s>16&255,o>>8&255,o&255]}return i}var Xe;function pa(i){Xe||(Xe=ga(),Xe.transparent=[0,0,0,0]);let t=Xe[i.toLowerCase()];return t&&{r:t[0],g:t[1],b:t[2],a:t.length===4?t[3]:255}}var ma=/^rgba?\(\s*([-+.\d]+)(%)?[\s,]+([-+.e\d]+)(%)?[\s,]+([-+.e\d]+)(%)?(?:[\s,/]+([-+.e\d]+)(%)?)?\s*\)$/;function ba(i){let t=ma.exec(i),e=255,s,n,o;if(t){if(t[7]!==s){let a=+t[7];e=t[8]?_e(a):wt(a*255,0,255)}return s=+t[1],n=+t[3],o=+t[5],s=255&(t[2]?_e(s):wt(s,0,255)),n=255&(t[4]?_e(n):wt(n,0,255)),o=255&(t[6]?_e(o):wt(o,0,255)),{r:s,g:n,b:o,a:e}}}function xa(i){return i&&(i.a<255?`rgba(${i.r}, ${i.g}, ${i.b}, ${mt(i.a)})`:`rgb(${i.r}, ${i.g}, ${i.b})`)}var Ci=i=>i<=.0031308?i*12.92:Math.pow(i,1/2.4)*1.055-.055,Gt=i=>i<=.04045?i/12.92:Math.pow((i+.055)/1.055,2.4);function _a(i,t,e){let s=Gt(mt(i.r)),n=Gt(mt(i.g)),o=Gt(mt(i.b));return{r:St(Ci(s+e*(Gt(mt(t.r))-s))),g:St(Ci(n+e*(Gt(mt(t.g))-n))),b:St(Ci(o+e*(Gt(mt(t.b))-o))),a:i.a+e*(t.a-i.a)}}function Ke(i,t,e){if(i){let s=Ti(i);s[t]=Math.max(0,Math.min(s[t]+s[t]*e,t===0?360:1)),s=Ri(s),i.r=s[0],i.g=s[1],i.b=s[2]}}function js(i,t){return i&&Object.assign(t||{},i)}function Ws(i){var t={r:0,g:0,b:0,a:255};return Array.isArray(i)?i.length>=3&&(t={r:i[0],g:i[1],b:i[2],a:255},i.length>3&&(t.a=St(i[3]))):(t=js(i,{r:0,g:0,b:0,a:1}),t.a=St(t.a)),t}function ya(i){return i.charAt(0)==="r"?ba(i):da(i)}var ye=class i{constructor(t){if(t instanceof i)return t;let e=typeof t,s;e==="object"?s=Ws(t):e==="string"&&(s=ia(t)||pa(t)||ya(t)),this._rgb=s,this._valid=!!s}get valid(){return this._valid}get rgb(){var t=js(this._rgb);return t&&(t.a=mt(t.a)),t}set rgb(t){this._rgb=Ws(t)}rgbString(){return this._valid?xa(this._rgb):void 0}hexString(){return this._valid?na(this._rgb):void 0}hslString(){return this._valid?fa(this._rgb):void 0}mix(t,e){if(t){let s=this.rgb,n=t.rgb,o,a=e===o?.5:e,r=2*a-1,l=s.a-n.a,c=((r*l===-1?r:(r+l)/(1+r*l))+1)/2;o=1-c,s.r=255&c*s.r+o*n.r+.5,s.g=255&c*s.g+o*n.g+.5,s.b=255&c*s.b+o*n.b+.5,s.a=a*s.a+(1-a)*n.a,this.rgb=s}return this}interpolate(t,e){return t&&(this._rgb=_a(this._rgb,t._rgb,e)),this}clone(){return new i(this.rgb)}alpha(t){return this._rgb.a=St(t),this}clearer(t){let e=this._rgb;return e.a*=1-t,this}greyscale(){let t=this._rgb,e=ve(t.r*.3+t.g*.59+t.b*.11);return t.r=t.g=t.b=e,this}opaquer(t){let e=this._rgb;return e.a*=1+t,this}negate(){let t=this._rgb;return t.r=255-t.r,t.g=255-t.g,t.b=255-t.b,this}lighten(t){return Ke(this._rgb,2,t),this}darken(t){return Ke(this._rgb,2,-t),this}saturate(t){return Ke(this._rgb,1,t),this}desaturate(t){return Ke(this._rgb,1,-t),this}rotate(t){return ua(this._rgb,t),this}};function dt(){}var tn=(()=>{let i=0;return()=>i++})();function A(i){return i==null}function z(i){if(Array.isArray&&Array.isArray(i))return!0;let t=Object.prototype.toString.call(i);return t.slice(0,7)==="[object"&&t.slice(-6)==="Array]"}function T(i){return i!==null&&Object.prototype.toString.call(i)==="[object Object]"}function N(i){return(typeof i=="number"||i instanceof Number)&&isFinite(+i)}function Z(i,t){return N(i)?i:t}function D(i,t){return typeof i>"u"?t:i}var en=(i,t)=>typeof i=="string"&&i.endsWith("%")?parseFloat(i)/100:+i/t,zi=(i,t)=>typeof i=="string"&&i.endsWith("%")?parseFloat(i)/100*t:+i;function F(i,t,e){if(i&&typeof i.call=="function")return i.apply(e,t)}function E(i,t,e,s){let n,o,a;if(z(i))if(o=i.length,s)for(n=o-1;n>=0;n--)t.call(e,i[n],n);else for(n=0;ni,x:i=>i.x,y:i=>i.y};function ka(i){let t=i.split("."),e=[],s="";for(let n of t)s+=n,s.endsWith("\\")?s=s.slice(0,-1)+".":(e.push(s),s="");return e}function wa(i){let t=ka(i);return e=>{for(let s of t){if(s==="")break;e=e&&e[s]}return e}}function _t(i,t){return($s[t]||($s[t]=wa(t)))(i)}function ii(i){return i.charAt(0).toUpperCase()+i.slice(1)}var ee=i=>typeof i<"u",bt=i=>typeof i=="function",Bi=(i,t)=>{if(i.size!==t.size)return!1;for(let e of i)if(!t.has(e))return!1;return!0};function nn(i){return i.type==="mouseup"||i.type==="click"||i.type==="contextmenu"}var R=Math.PI,B=2*R,Sa=B+R,Qe=Number.POSITIVE_INFINITY,Pa=R/180,H=R/2,Ft=R/4,Ys=R*2/3,xt=Math.log10,lt=Math.sign;function ie(i,t,e){return Math.abs(i-t)n-o).pop(),t}function Da(i){return typeof i=="symbol"||typeof i=="object"&&i!==null&&!(Symbol.toPrimitive in i||"toString"in i||"valueOf"in i)}function Vt(i){return!Da(i)&&!isNaN(parseFloat(i))&&isFinite(i)}function an(i,t){let e=Math.round(i);return e-t<=i&&e+t>=i}function Wi(i,t,e){let s,n,o;for(s=0,n=i.length;sl&&c=Math.min(t,e)-s&&i<=Math.max(t,e)+s}function ni(i,t,e){e=e||(a=>i[a]1;)o=n+s>>1,e(o)?n=o:s=o;return{lo:n,hi:s}}var ct=(i,t,e,s)=>ni(i,e,s?n=>{let o=i[n][t];return oi[n][t]ni(i,e,s=>i[s][t]>=e);function cn(i,t,e){let s=0,n=i.length;for(;ss&&i[n-1]>e;)n--;return s>0||n{let s="_onData"+ii(e),n=i[e];Object.defineProperty(i,e,{configurable:!0,enumerable:!1,value(...o){let a=n.apply(this,o);return i._chartjs.listeners.forEach(r=>{typeof r[s]=="function"&&r[s](...o)}),a}})})}function ji(i,t){let e=i._chartjs;if(!e)return;let s=e.listeners,n=s.indexOf(t);n!==-1&&s.splice(n,1),!(s.length>0)&&(hn.forEach(o=>{delete i[o]}),delete i._chartjs)}function $i(i){let t=new Set(i);return t.size===i.length?i:Array.from(t)}var Yi=(function(){return typeof window>"u"?function(i){return i()}:window.requestAnimationFrame})();function Ui(i,t){let e=[],s=!1;return function(...n){e=n,s||(s=!0,Yi.call(window,()=>{s=!1,i.apply(t,e)}))}}function un(i,t){let e;return function(...s){return t?(clearTimeout(e),e=setTimeout(i,t,s)):i.apply(this,s),t}}var oi=i=>i==="start"?"left":i==="end"?"right":"center",K=(i,t,e)=>i==="start"?t:i==="end"?e:(t+e)/2,fn=(i,t,e,s)=>i===(s?"left":"right")?e:i==="center"?(t+e)/2:t;function Xi(i,t,e){let s=t.length,n=0,o=s;if(i._sorted){let{iScale:a,vScale:r,_parsed:l}=i,c=i.dataset&&i.dataset.options?i.dataset.options.spanGaps:null,h=a.axis,{min:d,max:u,minDefined:f,maxDefined:g}=a.getUserBounds();if(f){if(n=Math.min(ct(l,h,d).lo,e?s:ct(t,h,a.getPixelForValue(d)).lo),c){let p=l.slice(0,n+1).reverse().findIndex(m=>!A(m[r.axis]));n-=Math.max(0,p)}n=Y(n,0,s-1)}if(g){let p=Math.max(ct(l,a.axis,u,!0).hi+1,e?0:ct(t,h,a.getPixelForValue(u),!0).hi+1);if(c){let m=l.slice(p-1).findIndex(b=>!A(b[r.axis]));p+=Math.max(0,m)}o=Y(p,n,s)-n}else o=s-n}return{start:n,count:o}}function Ki(i){let{xScale:t,yScale:e,_scaleRanges:s}=i,n={xmin:t.min,xmax:t.max,ymin:e.min,ymax:e.max};if(!s)return i._scaleRanges=n,!0;let o=s.xmin!==t.min||s.xmax!==t.max||s.ymin!==e.min||s.ymax!==e.max;return Object.assign(s,n),o}var qe=i=>i===0||i===1,Us=(i,t,e)=>-(Math.pow(2,10*(i-=1))*Math.sin((i-t)*B/e)),Xs=(i,t,e)=>Math.pow(2,-10*i)*Math.sin((i-t)*B/e)+1,Jt={linear:i=>i,easeInQuad:i=>i*i,easeOutQuad:i=>-i*(i-2),easeInOutQuad:i=>(i/=.5)<1?.5*i*i:-.5*(--i*(i-2)-1),easeInCubic:i=>i*i*i,easeOutCubic:i=>(i-=1)*i*i+1,easeInOutCubic:i=>(i/=.5)<1?.5*i*i*i:.5*((i-=2)*i*i+2),easeInQuart:i=>i*i*i*i,easeOutQuart:i=>-((i-=1)*i*i*i-1),easeInOutQuart:i=>(i/=.5)<1?.5*i*i*i*i:-.5*((i-=2)*i*i*i-2),easeInQuint:i=>i*i*i*i*i,easeOutQuint:i=>(i-=1)*i*i*i*i+1,easeInOutQuint:i=>(i/=.5)<1?.5*i*i*i*i*i:.5*((i-=2)*i*i*i*i+2),easeInSine:i=>-Math.cos(i*H)+1,easeOutSine:i=>Math.sin(i*H),easeInOutSine:i=>-.5*(Math.cos(R*i)-1),easeInExpo:i=>i===0?0:Math.pow(2,10*(i-1)),easeOutExpo:i=>i===1?1:-Math.pow(2,-10*i)+1,easeInOutExpo:i=>qe(i)?i:i<.5?.5*Math.pow(2,10*(i*2-1)):.5*(-Math.pow(2,-10*(i*2-1))+2),easeInCirc:i=>i>=1?i:-(Math.sqrt(1-i*i)-1),easeOutCirc:i=>Math.sqrt(1-(i-=1)*i),easeInOutCirc:i=>(i/=.5)<1?-.5*(Math.sqrt(1-i*i)-1):.5*(Math.sqrt(1-(i-=2)*i)+1),easeInElastic:i=>qe(i)?i:Us(i,.075,.3),easeOutElastic:i=>qe(i)?i:Xs(i,.075,.3),easeInOutElastic(i){return qe(i)?i:i<.5?.5*Us(i*2,.1125,.45):.5+.5*Xs(i*2-1,.1125,.45)},easeInBack(i){return i*i*((1.70158+1)*i-1.70158)},easeOutBack(i){return(i-=1)*i*((1.70158+1)*i+1.70158)+1},easeInOutBack(i){let t=1.70158;return(i/=.5)<1?.5*(i*i*(((t*=1.525)+1)*i-t)):.5*((i-=2)*i*(((t*=1.525)+1)*i+t)+2)},easeInBounce:i=>1-Jt.easeOutBounce(1-i),easeOutBounce(i){return i<1/2.75?7.5625*i*i:i<2/2.75?7.5625*(i-=1.5/2.75)*i+.75:i<2.5/2.75?7.5625*(i-=2.25/2.75)*i+.9375:7.5625*(i-=2.625/2.75)*i+.984375},easeInOutBounce:i=>i<.5?Jt.easeInBounce(i*2)*.5:Jt.easeOutBounce(i*2-1)*.5+.5};function qi(i){if(i&&typeof i=="object"){let t=i.toString();return t==="[object CanvasPattern]"||t==="[object CanvasGradient]"}return!1}function Gi(i){return qi(i)?i:new ye(i)}function Ei(i){return qi(i)?i:new ye(i).saturate(.5).darken(.1).hexString()}var Ca=["x","y","borderWidth","radius","tension"],Aa=["color","borderColor","backgroundColor"];function Ta(i){i.set("animation",{delay:void 0,duration:1e3,easing:"easeOutQuart",fn:void 0,from:void 0,loop:void 0,to:void 0,type:void 0}),i.describe("animation",{_fallback:!1,_indexable:!1,_scriptable:t=>t!=="onProgress"&&t!=="onComplete"&&t!=="fn"}),i.set("animations",{colors:{type:"color",properties:Aa},numbers:{type:"number",properties:Ca}}),i.describe("animations",{_fallback:"animation"}),i.set("transitions",{active:{animation:{duration:400}},resize:{animation:{duration:0}},show:{animations:{colors:{from:"transparent"},visible:{type:"boolean",duration:0}}},hide:{animations:{colors:{to:"transparent"},visible:{type:"boolean",easing:"linear",fn:t=>t|0}}}})}function La(i){i.set("layout",{autoPadding:!0,padding:{top:0,right:0,bottom:0,left:0}})}var Ks=new Map;function Ra(i,t){t=t||{};let e=i+JSON.stringify(t),s=Ks.get(e);return s||(s=new Intl.NumberFormat(i,t),Ks.set(e,s)),s}function ne(i,t,e){return Ra(t,e).format(i)}var gn={values(i){return z(i)?i:""+i},numeric(i,t,e){if(i===0)return"0";let s=this.chart.options.locale,n,o=i;if(e.length>1){let c=Math.max(Math.abs(e[0].value),Math.abs(e[e.length-1].value));(c<1e-4||c>1e15)&&(n="scientific"),o=Ea(i,e)}let a=xt(Math.abs(o)),r=isNaN(a)?1:Math.max(Math.min(-1*Math.floor(a),20),0),l={notation:n,minimumFractionDigits:r,maximumFractionDigits:r};return Object.assign(l,this.options.ticks.format),ne(i,s,l)},logarithmic(i,t,e){if(i===0)return"0";let s=e[t].significand||i/Math.pow(10,Math.floor(xt(i)));return[1,2,3,5,10,15].includes(s)||t>.8*e.length?gn.numeric.call(this,i,t,e):""}};function Ea(i,t){let e=t.length>3?t[2].value-t[1].value:t[1].value-t[0].value;return Math.abs(e)>=1&&i!==Math.floor(i)&&(e=i-Math.floor(i)),e}var Se={formatters:gn};function Ia(i){i.set("scale",{display:!0,offset:!1,reverse:!1,beginAtZero:!1,bounds:"ticks",clip:!0,grace:0,grid:{display:!0,lineWidth:1,drawOnChartArea:!0,drawTicks:!0,tickLength:8,tickWidth:(t,e)=>e.lineWidth,tickColor:(t,e)=>e.color,offset:!1},border:{display:!0,dash:[],dashOffset:0,width:1},title:{display:!1,text:"",padding:{top:4,bottom:4}},ticks:{minRotation:0,maxRotation:50,mirror:!1,textStrokeWidth:0,textStrokeColor:"",padding:3,display:!0,autoSkip:!0,autoSkipPadding:3,labelOffset:0,callback:Se.formatters.values,minor:{},major:{},align:"center",crossAlign:"near",showLabelBackdrop:!1,backdropColor:"rgba(255, 255, 255, 0.75)",backdropPadding:2}}),i.route("scale.ticks","color","","color"),i.route("scale.grid","color","","borderColor"),i.route("scale.border","color","","borderColor"),i.route("scale.title","color","","color"),i.describe("scale",{_fallback:!1,_scriptable:t=>!t.startsWith("before")&&!t.startsWith("after")&&t!=="callback"&&t!=="parser",_indexable:t=>t!=="borderDash"&&t!=="tickBorderDash"&&t!=="dash"}),i.describe("scales",{_fallback:"scale"}),i.describe("scale.ticks",{_scriptable:t=>t!=="backdropPadding"&&t!=="callback",_indexable:t=>t!=="backdropPadding"})}var Ot=Object.create(null),ai=Object.create(null);function Me(i,t){if(!t)return i;let e=t.split(".");for(let s=0,n=e.length;ss.chart.platform.getDevicePixelRatio(),this.elements={},this.events=["mousemove","mouseout","click","touchstart","touchmove"],this.font={family:"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",size:12,style:"normal",lineHeight:1.2,weight:null},this.hover={},this.hoverBackgroundColor=(s,n)=>Ei(n.backgroundColor),this.hoverBorderColor=(s,n)=>Ei(n.borderColor),this.hoverColor=(s,n)=>Ei(n.color),this.indexAxis="x",this.interaction={mode:"nearest",intersect:!0,includeInvisible:!1},this.maintainAspectRatio=!0,this.onHover=null,this.onClick=null,this.parsing=!0,this.plugins={},this.responsive=!0,this.scale=void 0,this.scales={},this.showLine=!0,this.drawActiveElementsOnTop=!0,this.describe(t),this.apply(e)}set(t,e){return Ii(this,t,e)}get(t){return Me(this,t)}describe(t,e){return Ii(ai,t,e)}override(t,e){return Ii(Ot,t,e)}route(t,e,s,n){let o=Me(this,t),a=Me(this,s),r="_"+e;Object.defineProperties(o,{[r]:{value:o[e],writable:!0},[e]:{enumerable:!0,get(){let l=this[r],c=a[n];return T(l)?Object.assign({},c,l):D(l,c)},set(l){this[r]=l}}})}apply(t){t.forEach(e=>e(this))}},V=new Fi({_scriptable:i=>!i.startsWith("on"),_indexable:i=>i!=="events",hover:{_fallback:"interaction"},interaction:{_scriptable:!1,_indexable:!1}},[Ta,La,Ia]);function Fa(i){return!i||A(i.size)||A(i.family)?null:(i.style?i.style+" ":"")+(i.weight?i.weight+" ":"")+i.size+"px "+i.family}function ke(i,t,e,s,n){let o=t[n];return o||(o=t[n]=i.measureText(n).width,e.push(n)),o>s&&(s=o),s}function pn(i,t,e,s){s=s||{};let n=s.data=s.data||{},o=s.garbageCollect=s.garbageCollect||[];s.font!==t&&(n=s.data={},o=s.garbageCollect=[],s.font=t),i.save(),i.font=t;let a=0,r=e.length,l,c,h,d,u;for(l=0;le.length){for(l=0;l0&&i.stroke()}}function ht(i,t,e){return e=e||.5,!t||i&&i.x>t.left-e&&i.xt.top-e&&i.y0&&o.strokeColor!=="",l,c;for(i.save(),i.font=n.string,za(i,o),l=0;l+i||0;function li(i,t){let e={},s=T(t),n=s?Object.keys(t):t,o=T(i)?s?a=>D(i[a],i[t[a]]):a=>i[a]:()=>i;for(let a of n)e[a]=ja(o(a));return e}function Qi(i){return li(i,{top:"y",right:"x",bottom:"y",left:"x"})}function Tt(i){return li(i,["topLeft","topRight","bottomLeft","bottomRight"])}function q(i){let t=Qi(i);return t.width=t.left+t.right,t.height=t.top+t.bottom,t}function $(i,t){i=i||{},t=t||V.font;let e=D(i.size,t.size);typeof e=="string"&&(e=parseInt(e,10));let s=D(i.style,t.style);s&&!(""+s).match(Na)&&(console.warn('Invalid font style specified: "'+s+'"'),s=void 0);let n={family:D(i.family,t.family),lineHeight:Ha(D(i.lineHeight,t.lineHeight),e),size:e,style:s,weight:D(i.weight,t.weight),string:""};return n.string=Fa(n),n}function ae(i,t,e,s){let n=!0,o,a,r;for(o=0,a=i.length;oe&&r===0?0:r+l;return{min:a(s,-Math.abs(o)),max:a(n,o)}}function yt(i,t){return Object.assign(Object.create(i),t)}function ci(i,t=[""],e,s,n=()=>i[0]){let o=e||i;typeof s>"u"&&(s=vn("_fallback",i));let a={[Symbol.toStringTag]:"Object",_cacheable:!0,_scopes:i,_rootScopes:o,_fallback:s,_getTarget:n,override:r=>ci([r,...i],t,o,s)};return new Proxy(a,{deleteProperty(r,l){return delete r[l],delete r._keys,delete i[0][l],!0},get(r,l){return _n(r,l,()=>Ja(l,t,i,r))},getOwnPropertyDescriptor(r,l){return Reflect.getOwnPropertyDescriptor(r._scopes[0],l)},getPrototypeOf(){return Reflect.getPrototypeOf(i[0])},has(r,l){return Gs(r).includes(l)},ownKeys(r){return Gs(r)},set(r,l,c){let h=r._storage||(r._storage=n());return r[l]=h[l]=c,delete r._keys,!0}})}function Bt(i,t,e,s){let n={_cacheable:!1,_proxy:i,_context:t,_subProxy:e,_stack:new Set,_descriptors:ts(i,s),setContext:o=>Bt(i,o,e,s),override:o=>Bt(i.override(o),t,e,s)};return new Proxy(n,{deleteProperty(o,a){return delete o[a],delete i[a],!0},get(o,a,r){return _n(o,a,()=>Ya(o,a,r))},getOwnPropertyDescriptor(o,a){return o._descriptors.allKeys?Reflect.has(i,a)?{enumerable:!0,configurable:!0}:void 0:Reflect.getOwnPropertyDescriptor(i,a)},getPrototypeOf(){return Reflect.getPrototypeOf(i)},has(o,a){return Reflect.has(i,a)},ownKeys(){return Reflect.ownKeys(i)},set(o,a,r){return i[a]=r,delete o[a],!0}})}function ts(i,t={scriptable:!0,indexable:!0}){let{_scriptable:e=t.scriptable,_indexable:s=t.indexable,_allKeys:n=t.allKeys}=i;return{allKeys:n,scriptable:e,indexable:s,isScriptable:bt(e)?e:()=>e,isIndexable:bt(s)?s:()=>s}}var $a=(i,t)=>i?i+ii(t):t,es=(i,t)=>T(t)&&i!=="adapters"&&(Object.getPrototypeOf(t)===null||t.constructor===Object);function _n(i,t,e){if(Object.prototype.hasOwnProperty.call(i,t)||t==="constructor")return i[t];let s=e();return i[t]=s,s}function Ya(i,t,e){let{_proxy:s,_context:n,_subProxy:o,_descriptors:a}=i,r=s[t];return bt(r)&&a.isScriptable(t)&&(r=Ua(t,r,i,e)),z(r)&&r.length&&(r=Xa(t,r,i,a.isIndexable)),es(t,r)&&(r=Bt(r,n,o&&o[t],a)),r}function Ua(i,t,e,s){let{_proxy:n,_context:o,_subProxy:a,_stack:r}=e;if(r.has(i))throw new Error("Recursion detected: "+Array.from(r).join("->")+"->"+i);r.add(i);let l=t(o,a||s);return r.delete(i),es(i,l)&&(l=is(n._scopes,n,i,l)),l}function Xa(i,t,e,s){let{_proxy:n,_context:o,_subProxy:a,_descriptors:r}=e;if(typeof o.index<"u"&&s(i))return t[o.index%t.length];if(T(t[0])){let l=t,c=n._scopes.filter(h=>h!==l);t=[];for(let h of l){let d=is(c,n,i,h);t.push(Bt(d,o,a&&a[i],r))}}return t}function yn(i,t,e){return bt(i)?i(t,e):i}var Ka=(i,t)=>i===!0?t:typeof i=="string"?_t(t,i):void 0;function qa(i,t,e,s,n){for(let o of t){let a=Ka(e,o);if(a){i.add(a);let r=yn(a._fallback,e,n);if(typeof r<"u"&&r!==e&&r!==s)return r}else if(a===!1&&typeof s<"u"&&e!==s)return null}return!1}function is(i,t,e,s){let n=t._rootScopes,o=yn(t._fallback,e,s),a=[...i,...n],r=new Set;r.add(s);let l=qs(r,a,e,o||e,s);return l===null||typeof o<"u"&&o!==e&&(l=qs(r,a,o,l,s),l===null)?!1:ci(Array.from(r),[""],n,o,()=>Ga(t,e,s))}function qs(i,t,e,s,n){for(;e;)e=qa(i,t,e,s,n);return e}function Ga(i,t,e){let s=i._getTarget();t in s||(s[t]={});let n=s[t];return z(n)&&T(e)?e:n||{}}function Ja(i,t,e,s){let n;for(let o of t)if(n=vn($a(o,i),e),typeof n<"u")return es(i,n)?is(e,s,i,n):n}function vn(i,t){for(let e of t){if(!e)continue;let s=e[i];if(typeof s<"u")return s}}function Gs(i){let t=i._keys;return t||(t=i._keys=Za(i._scopes)),t}function Za(i){let t=new Set;for(let e of i)for(let s of Object.keys(e).filter(n=>!n.startsWith("_")))t.add(s);return Array.from(t)}function ss(i,t,e,s){let{iScale:n}=i,{key:o="r"}=this._parsing,a=new Array(s),r,l,c,h;for(r=0,l=s;rti==="x"?"y":"x";function tr(i,t,e,s){let n=i.skip?t:i,o=t,a=e.skip?t:e,r=ti(o,n),l=ti(a,o),c=r/(r+l),h=l/(r+l);c=isNaN(c)?0:c,h=isNaN(h)?0:h;let d=s*c,u=s*h;return{previous:{x:o.x-d*(a.x-n.x),y:o.y-d*(a.y-n.y)},next:{x:o.x+u*(a.x-n.x),y:o.y+u*(a.y-n.y)}}}function er(i,t,e){let s=i.length,n,o,a,r,l,c=Qt(i,0);for(let h=0;h!c.skip)),t.cubicInterpolationMode==="monotone")sr(i,n);else{let c=s?i[i.length-1]:i[0];for(o=0,a=i.length;oi.ownerDocument.defaultView.getComputedStyle(i,null);function or(i,t){return ui(i).getPropertyValue(t)}var ar=["top","right","bottom","left"];function zt(i,t,e){let s={};e=e?"-"+e:"";for(let n=0;n<4;n++){let o=ar[n];s[o]=parseFloat(i[t+"-"+o+e])||0}return s.width=s.left+s.right,s.height=s.top+s.bottom,s}var rr=(i,t,e)=>(i>0||t>0)&&(!e||!e.shadowRoot);function lr(i,t){let e=i.touches,s=e&&e.length?e[0]:i,{offsetX:n,offsetY:o}=s,a=!1,r,l;if(rr(n,o,i.target))r=n,l=o;else{let c=t.getBoundingClientRect();r=s.clientX-c.left,l=s.clientY-c.top,a=!0}return{x:r,y:l,box:a}}function Lt(i,t){if("native"in i)return i;let{canvas:e,currentDevicePixelRatio:s}=t,n=ui(e),o=n.boxSizing==="border-box",a=zt(n,"padding"),r=zt(n,"border","width"),{x:l,y:c,box:h}=lr(i,e),d=a.left+(h&&r.left),u=a.top+(h&&r.top),{width:f,height:g}=t;return o&&(f-=a.width+r.width,g-=a.height+r.height),{x:Math.round((l-d)/f*e.width/s),y:Math.round((c-u)/g*e.height/s)}}function cr(i,t,e){let s,n;if(t===void 0||e===void 0){let o=i&&di(i);if(!o)t=i.clientWidth,e=i.clientHeight;else{let a=o.getBoundingClientRect(),r=ui(o),l=zt(r,"border","width"),c=zt(r,"padding");t=a.width-c.width-l.width,e=a.height-c.height-l.height,s=ei(r.maxWidth,o,"clientWidth"),n=ei(r.maxHeight,o,"clientHeight")}}return{width:t,height:e,maxWidth:s||Qe,maxHeight:n||Qe}}var Dt=i=>Math.round(i*10)/10;function wn(i,t,e,s){let n=ui(i),o=zt(n,"margin"),a=ei(n.maxWidth,i,"clientWidth")||Qe,r=ei(n.maxHeight,i,"clientHeight")||Qe,l=cr(i,t,e),{width:c,height:h}=l;if(n.boxSizing==="content-box"){let u=zt(n,"border","width"),f=zt(n,"padding");c-=f.width+u.width,h-=f.height+u.height}return c=Math.max(0,c-o.width),h=Math.max(0,s?c/s:h-o.height),c=Dt(Math.min(c,a,l.maxWidth)),h=Dt(Math.min(h,r,l.maxHeight)),c&&!h&&(h=Dt(c/2)),(t!==void 0||e!==void 0)&&s&&l.height&&h>l.height&&(h=l.height,c=Dt(Math.floor(h*s))),{width:c,height:h}}function ns(i,t,e){let s=t||1,n=Dt(i.height*s),o=Dt(i.width*s);i.height=Dt(i.height),i.width=Dt(i.width);let a=i.canvas;return a.style&&(e||!a.style.height&&!a.style.width)&&(a.style.height=`${i.height}px`,a.style.width=`${i.width}px`),i.currentDevicePixelRatio!==s||a.height!==n||a.width!==o?(i.currentDevicePixelRatio=s,a.height=n,a.width=o,i.ctx.setTransform(s,0,0,s,0,0),!0):!1}var Sn=(function(){let i=!1;try{let t={get passive(){return i=!0,!1}};hi()&&(window.addEventListener("test",null,t),window.removeEventListener("test",null,t))}catch{}return i})();function os(i,t){let e=or(i,t),s=e&&e.match(/^(\d+)(\.\d+)?px$/);return s?+s[1]:void 0}function Pt(i,t,e,s){return{x:i.x+e*(t.x-i.x),y:i.y+e*(t.y-i.y)}}function Pn(i,t,e,s){return{x:i.x+e*(t.x-i.x),y:s==="middle"?e<.5?i.y:t.y:s==="after"?e<1?i.y:t.y:e>0?t.y:i.y}}function Dn(i,t,e,s){let n={x:i.cp2x,y:i.cp2y},o={x:t.cp1x,y:t.cp1y},a=Pt(i,n,e),r=Pt(n,o,e),l=Pt(o,t,e),c=Pt(a,r,e),h=Pt(r,l,e);return Pt(c,h,e)}var hr=function(i,t){return{x(e){return i+i+t-e},setWidth(e){t=e},textAlign(e){return e==="center"?e:e==="right"?"left":"right"},xPlus(e,s){return e-s},leftForLtr(e,s){return e-s}}},dr=function(){return{x(i){return i},setWidth(i){},textAlign(i){return i},xPlus(i,t){return i+t},leftForLtr(i,t){return i}}};function Wt(i,t,e){return i?hr(t,e):dr()}function as(i,t){let e,s;(t==="ltr"||t==="rtl")&&(e=i.canvas.style,s=[e.getPropertyValue("direction"),e.getPropertyPriority("direction")],e.setProperty("direction",t,"important"),i.prevTextDirection=s)}function rs(i,t){t!==void 0&&(delete i.prevTextDirection,i.canvas.style.setProperty("direction",t[0],t[1]))}function On(i){return i==="angle"?{between:se,compare:Oa,normalize:X}:{between:ut,compare:(t,e)=>t-e,normalize:t=>t}}function Js({start:i,end:t,count:e,loop:s,style:n}){return{start:i%e,end:t%e,loop:s&&(t-i+1)%e===0,style:n}}function ur(i,t,e){let{property:s,start:n,end:o}=e,{between:a,normalize:r}=On(s),l=t.length,{start:c,end:h,loop:d}=i,u,f;if(d){for(c+=l,h+=l,u=0,f=l;ul(n,v,b)&&r(n,v)!==0,_=()=>r(o,b)===0||l(o,v,b),k=()=>p||y(),w=()=>!p||_();for(let S=h,P=h;S<=d;++S)x=t[S%a],!x.skip&&(b=c(x[s]),b!==v&&(p=l(b,n,o),m===null&&k()&&(m=r(b,n)===0?S:P),m!==null&&w()&&(g.push(Js({start:m,end:S,loop:u,count:a,style:f})),m=null),P=S,v=b));return m!==null&&g.push(Js({start:m,end:d,loop:u,count:a,style:f})),g}function cs(i,t){let e=[],s=i.segments;for(let n=0;nn&&i[o%t].skip;)o--;return o%=t,{start:n,end:o}}function gr(i,t,e,s){let n=i.length,o=[],a=t,r=i[t],l;for(l=t+1;l<=e;++l){let c=i[l%n];c.skip||c.stop?r.skip||(s=!1,o.push({start:t%n,end:(l-1)%n,loop:s}),t=a=c.stop?l:null):(a=l,r.skip&&(t=l)),r=c}return a!==null&&o.push({start:t%n,end:a%n,loop:s}),o}function Cn(i,t){let e=i.points,s=i.options.spanGaps,n=e.length;if(!n)return[];let o=!!i._loop,{start:a,end:r}=fr(e,n,o,s);if(s===!0)return Zs(i,[{start:a,end:r,loop:o}],e,t);let l=rr({chart:t,initial:e.initial,numSteps:a,currentStep:Math.min(s-e.start,a)}))}_refresh(){this._request||(this._running=!0,this._request=Yi.call(window,()=>{this._update(),this._request=null,this._running&&this._refresh()}))}_update(t=Date.now()){let e=0;this._charts.forEach((s,n)=>{if(!s.running||!s.items.length)return;let o=s.items,a=o.length-1,r=!1,l;for(;a>=0;--a)l=o[a],l._active?(l._total>s.duration&&(s.duration=l._total),l.tick(t),r=!0):(o[a]=o[o.length-1],o.pop());r&&(n.draw(),this._notify(n,s,t,"progress")),o.length||(s.running=!1,this._notify(n,s,t,"complete"),s.initial=!1),e+=o.length}),this._lastDate=t,e===0&&(this._running=!1)}_getAnims(t){let e=this._charts,s=e.get(t);return s||(s={running:!1,initial:!0,items:[],listeners:{complete:[],progress:[]}},e.set(t,s)),s}listen(t,e,s){this._getAnims(t).listeners[e].push(s)}add(t,e){!e||!e.length||this._getAnims(t).items.push(...e)}has(t){return this._getAnims(t).items.length>0}start(t){let e=this._charts.get(t);e&&(e.running=!0,e.start=Date.now(),e.duration=e.items.reduce((s,n)=>Math.max(s,n._duration),0),this._refresh())}running(t){if(!this._running)return!1;let e=this._charts.get(t);return!(!e||!e.running||!e.items.length)}stop(t){let e=this._charts.get(t);if(!e||!e.items.length)return;let s=e.items,n=s.length-1;for(;n>=0;--n)s[n].cancel();e.items=[],this._notify(t,e,Date.now(),"complete")}remove(t){return this._charts.delete(t)}},vt=new Ms,An="transparent",xr={boolean(i,t,e){return e>.5?t:i},color(i,t,e){let s=Gi(i||An),n=s.valid&&Gi(t||An);return n&&n.valid?n.mix(s,e).hexString():t},number(i,t,e){return i+(t-i)*e}},ks=class{constructor(t,e,s,n){let o=e[s];n=ae([t.to,n,o,t.from]);let a=ae([t.from,o,n]);this._active=!0,this._fn=t.fn||xr[t.type||typeof a],this._easing=Jt[t.easing]||Jt.linear,this._start=Math.floor(Date.now()+(t.delay||0)),this._duration=this._total=Math.floor(t.duration),this._loop=!!t.loop,this._target=e,this._prop=s,this._from=a,this._to=n,this._promises=void 0}active(){return this._active}update(t,e,s){if(this._active){this._notify(!1);let n=this._target[this._prop],o=s-this._start,a=this._duration-o;this._start=s,this._duration=Math.floor(Math.max(a,t.duration)),this._total+=o,this._loop=!!t.loop,this._to=ae([t.to,e,n,t.from]),this._from=ae([t.from,n,e])}}cancel(){this._active&&(this.tick(Date.now()),this._active=!1,this._notify(!1))}tick(t){let e=t-this._start,s=this._duration,n=this._prop,o=this._from,a=this._loop,r=this._to,l;if(this._active=o!==r&&(a||e1?2-l:l,l=this._easing(Math.min(1,Math.max(0,l))),this._target[n]=this._fn(o,r,l)}wait(){let t=this._promises||(this._promises=[]);return new Promise((e,s)=>{t.push({res:e,rej:s})})}_notify(t){let e=t?"res":"rej",s=this._promises||[];for(let n=0;n{let o=t[n];if(!T(o))return;let a={};for(let r of e)a[r]=o[r];(z(o.properties)&&o.properties||[n]).forEach(r=>{(r===n||!s.has(r))&&s.set(r,a)})})}_animateOptions(t,e){let s=e.options,n=yr(t,s);if(!n)return[];let o=this._createAnimations(n,s);return s.$shared&&_r(t.options.$animations,s).then(()=>{t.options=s},()=>{}),o}_createAnimations(t,e){let s=this._properties,n=[],o=t.$animations||(t.$animations={}),a=Object.keys(e),r=Date.now(),l;for(l=a.length-1;l>=0;--l){let c=a[l];if(c.charAt(0)==="$")continue;if(c==="options"){n.push(...this._animateOptions(t,e));continue}let h=e[c],d=o[c],u=s.get(c);if(d)if(u&&d.active()){d.update(u,h,r);continue}else d.cancel();if(!u||!u.duration){t[c]=h;continue}o[c]=d=new ks(u,t,c,h),n.push(d)}return n}update(t,e){if(this._properties.size===0){Object.assign(t,e);return}let s=this._createAnimations(t,e);if(s.length)return vt.add(this._chart,s),!0}};function _r(i,t){let e=[],s=Object.keys(t);for(let n=0;n0||!e&&o<0)return n.index}return null}function En(i,t){let{chart:e,_cachedMeta:s}=i,n=e._stacks||(e._stacks={}),{iScale:o,vScale:a,index:r}=s,l=o.axis,c=a.axis,h=wr(o,a,s),d=t.length,u;for(let f=0;fe[s].axis===t).shift()}function Dr(i,t){return yt(i,{active:!1,dataset:void 0,datasetIndex:t,index:t,mode:"default",type:"dataset"})}function Or(i,t,e){return yt(i,{active:!1,dataIndex:t,parsed:void 0,raw:void 0,element:e,index:t,mode:"default",type:"data"})}function Oe(i,t){let e=i.controller.index,s=i.vScale&&i.vScale.axis;if(s){t=t||i._parsed;for(let n of t){let o=n._stacks;if(!o||o[s]===void 0||o[s][e]===void 0)return;delete o[s][e],o[s]._visualValues!==void 0&&o[s]._visualValues[e]!==void 0&&delete o[s]._visualValues[e]}}}var fs=i=>i==="reset"||i==="none",In=(i,t)=>t?i:Object.assign({},i),Cr=(i,t,e)=>i&&!t.hidden&&t._stacked&&{keys:Ao(e,!0),values:null},it=class{constructor(t,e){this.chart=t,this._ctx=t.ctx,this.index=e,this._cachedDataOpts={},this._cachedMeta=this.getMeta(),this._type=this._cachedMeta.type,this.options=void 0,this._parsing=!1,this._data=void 0,this._objectData=void 0,this._sharedOptions=void 0,this._drawStart=void 0,this._drawCount=void 0,this.enableOptionSharing=!1,this.supportsDecimation=!1,this.$context=void 0,this._syncList=[],this.datasetElementType=new.target.datasetElementType,this.dataElementType=new.target.dataElementType,this.initialize()}initialize(){let t=this._cachedMeta;this.configure(),this.linkScales(),t._stacked=ds(t.vScale,t),this.addElements(),this.options.fill&&!this.chart.isPluginEnabled("filler")&&console.warn("Tried to use the 'fill' option without the 'Filler' plugin enabled. Please import and register the 'Filler' plugin and make sure it is not disabled in the options")}updateIndex(t){this.index!==t&&Oe(this._cachedMeta),this.index=t}linkScales(){let t=this.chart,e=this._cachedMeta,s=this.getDataset(),n=(d,u,f,g)=>d==="x"?u:d==="r"?g:f,o=e.xAxisID=D(s.xAxisID,us(t,"x")),a=e.yAxisID=D(s.yAxisID,us(t,"y")),r=e.rAxisID=D(s.rAxisID,us(t,"r")),l=e.indexAxis,c=e.iAxisID=n(l,o,a,r),h=e.vAxisID=n(l,a,o,r);e.xScale=this.getScaleForId(o),e.yScale=this.getScaleForId(a),e.rScale=this.getScaleForId(r),e.iScale=this.getScaleForId(c),e.vScale=this.getScaleForId(h)}getDataset(){return this.chart.data.datasets[this.index]}getMeta(){return this.chart.getDatasetMeta(this.index)}getScaleForId(t){return this.chart.scales[t]}_getOtherScale(t){let e=this._cachedMeta;return t===e.iScale?e.vScale:e.iScale}reset(){this._update("reset")}_destroy(){let t=this._cachedMeta;this._data&&ji(this._data,this),t._stacked&&Oe(t)}_dataCheck(){let t=this.getDataset(),e=t.data||(t.data=[]),s=this._data;if(T(e)){let n=this._cachedMeta;this._data=kr(e,n)}else if(s!==e){if(s){ji(s,this);let n=this._cachedMeta;Oe(n),n._parsed=[]}e&&Object.isExtensible(e)&&dn(e,this),this._syncList=[],this._data=e}}addElements(){let t=this._cachedMeta;this._dataCheck(),this.datasetElementType&&(t.dataset=new this.datasetElementType)}buildOrUpdateElements(t){let e=this._cachedMeta,s=this.getDataset(),n=!1;this._dataCheck();let o=e._stacked;e._stacked=ds(e.vScale,e),e.stack!==s.stack&&(n=!0,Oe(e),e.stack=s.stack),this._resyncElements(t),(n||o!==e._stacked)&&(En(this,e._parsed),e._stacked=ds(e.vScale,e))}configure(){let t=this.chart.config,e=t.datasetScopeKeys(this._type),s=t.getOptionScopes(this.getDataset(),e,!0);this.options=t.createResolver(s,this.getContext()),this._parsing=this.options.parsing,this._cachedDataOpts={}}parse(t,e){let{_cachedMeta:s,_data:n}=this,{iScale:o,_stacked:a}=s,r=o.axis,l=t===0&&e===n.length?!0:s._sorted,c=t>0&&s._parsed[t-1],h,d,u;if(this._parsing===!1)s._parsed=n,s._sorted=!0,u=n;else{z(n[t])?u=this.parseArrayData(s,n,t,e):T(n[t])?u=this.parseObjectData(s,n,t,e):u=this.parsePrimitiveData(s,n,t,e);let f=()=>d[r]===null||c&&d[r]p||d=0;--u)if(!g()){this.updateRangeFromParsed(c,t,f,l);break}}return c}getAllParsedValues(t){let e=this._cachedMeta._parsed,s=[],n,o,a;for(n=0,o=e.length;n=0&&tthis.getContext(s,n,e),p=c.resolveNamedOptions(u,f,g,d);return p.$shared&&(p.$shared=l,o[a]=Object.freeze(In(p,l))),p}_resolveAnimations(t,e,s){let n=this.chart,o=this._cachedDataOpts,a=`animation-${e}`,r=o[a];if(r)return r;let l;if(n.options.animation!==!1){let h=this.chart.config,d=h.datasetAnimationScopeKeys(this._type,e),u=h.getOptionScopes(this.getDataset(),d);l=h.createResolver(u,this.getContext(t,s,e))}let c=new vi(n,l&&l.animations);return l&&l._cacheable&&(o[a]=Object.freeze(c)),c}getSharedOptions(t){if(t.$shared)return this._sharedOptions||(this._sharedOptions=Object.assign({},t))}includeOptions(t,e){return!e||fs(t)||this.chart._animationsDisabled}_getSharedOptions(t,e){let s=this.resolveDataElementOptions(t,e),n=this._sharedOptions,o=this.getSharedOptions(s),a=this.includeOptions(e,o)||o!==n;return this.updateSharedOptions(o,e,s),{sharedOptions:o,includeOptions:a}}updateElement(t,e,s,n){fs(n)?Object.assign(t,s):this._resolveAnimations(e,n).update(t,s)}updateSharedOptions(t,e,s){t&&!fs(e)&&this._resolveAnimations(void 0,e).update(t,s)}_setStyle(t,e,s,n){t.active=n;let o=this.getStyle(e,n);this._resolveAnimations(e,s,n).update(t,{options:!n&&this.getSharedOptions(o)||o})}removeHoverStyle(t,e,s){this._setStyle(t,s,"active",!1)}setHoverStyle(t,e,s){this._setStyle(t,s,"active",!0)}_removeDatasetHoverStyle(){let t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!1)}_setDatasetHoverStyle(){let t=this._cachedMeta.dataset;t&&this._setStyle(t,void 0,"active",!0)}_resyncElements(t){let e=this._data,s=this._cachedMeta.data;for(let[r,l,c]of this._syncList)this[r](l,c);this._syncList=[];let n=s.length,o=e.length,a=Math.min(o,n);a&&this.parse(0,a),o>n?this._insertElements(n,o-n,t):o{for(c.length+=e,r=c.length-1;r>=a;r--)c[r]=c[r-e]};for(l(o),r=t;rn-o))}return i._cache.$bar}function Tr(i){let t=i.iScale,e=Ar(t,i.type),s=t._length,n,o,a,r,l=()=>{a===32767||a===-32768||(ee(r)&&(s=Math.min(s,Math.abs(a-r)||s)),r=a)};for(n=0,o=e.length;n0?n[i-1]:null,r=iMath.abs(r)&&(l=r,c=a),t[e.axis]=c,t._custom={barStart:l,barEnd:c,start:n,end:o,min:a,max:r}}function To(i,t,e,s){return z(i)?Er(i,t,e,s):t[e.axis]=e.parse(i,s),t}function Fn(i,t,e,s){let n=i.iScale,o=i.vScale,a=n.getLabels(),r=n===o,l=[],c,h,d,u;for(c=e,h=e+s;c=e?1:-1)}function Fr(i){let t,e,s,n,o;return i.horizontal?(t=i.base>i.x,e="left",s="right"):(t=i.baseh.controller.options.grouped),o=s.options.stacked,a=[],r=this._cachedMeta.controller.getParsed(e),l=r&&r[s.axis],c=h=>{let d=h._parsed.find(f=>f[s.axis]===l),u=d&&d[h.vScale.axis];if(A(u)||isNaN(u))return!0};for(let h of n)if(!(e!==void 0&&c(h))&&((o===!1||a.indexOf(h.stack)===-1||o===void 0&&h.stack===void 0)&&a.push(h.stack),h.index===t))break;return a.length||a.push(void 0),a}_getStackCount(t){return this._getStacks(void 0,t).length}_getAxisCount(){return this._getAxis().length}getFirstScaleIdForIndexAxis(){let t=this.chart.scales,e=this.chart.options.indexAxis;return Object.keys(t).filter(s=>t[s].axis===e).shift()}_getAxis(){let t={},e=this.getFirstScaleIdForIndexAxis();for(let s of this.chart.data.datasets)t[D(this.chart.options.indexAxis==="x"?s.xAxisID:s.yAxisID,e)]=!0;return Object.keys(t)}_getStackIndex(t,e,s){let n=this._getStacks(t,s),o=e!==void 0?n.indexOf(e):-1;return o===-1?n.length-1:o}_getRuler(){let t=this.options,e=this._cachedMeta,s=e.iScale,n=[],o,a;for(o=0,a=e.data.length;o=0;--s)e=Math.max(e,t[s].size(this.resolveDataElementOptions(s))/2);return e>0&&e}getLabelAndValue(t){let e=this._cachedMeta,s=this.chart.data.labels||[],{xScale:n,yScale:o}=e,a=this.getParsed(t),r=n.getLabelForValue(a.x),l=o.getLabelForValue(a.y),c=a._custom;return{label:s[t]||"",value:"("+r+", "+l+(c?", "+c:"")+")"}}update(t){let e=this._cachedMeta.data;this.updateElements(e,0,e.length,t)}updateElements(t,e,s,n){let o=n==="reset",{iScale:a,vScale:r}=this._cachedMeta,{sharedOptions:l,includeOptions:c}=this._getSharedOptions(e,n),h=a.axis,d=r.axis;for(let u=e;use(v,r,l,!0)?1:Math.max(y,y*e,_,_*e),g=(v,y,_)=>se(v,r,l,!0)?-1:Math.min(y,y*e,_,_*e),p=f(0,c,d),m=f(H,h,u),b=g(R,c,d),x=g(R+H,h,u);s=(p-b)/2,n=(m-x)/2,o=-(p+b)/2,a=-(m+x)/2}return{ratioX:s,ratioY:n,offsetX:o,offsetY:a}}var kt=class extends it{constructor(t,e){super(t,e),this.enableOptionSharing=!0,this.innerRadius=void 0,this.outerRadius=void 0,this.offsetX=void 0,this.offsetY=void 0}linkScales(){}parse(t,e){let s=this.getDataset().data,n=this._cachedMeta;if(this._parsing===!1)n._parsed=s;else{let o=l=>+s[l];if(T(s[t])){let{key:l="value"}=this._parsing;o=c=>+_t(s[c],l)}let a,r;for(a=t,r=t+e;a0&&!isNaN(t)?B*(Math.abs(t)/e):0}getLabelAndValue(t){let e=this._cachedMeta,s=this.chart,n=s.data.labels||[],o=ne(e._parsed[t],s.options.locale);return{label:n[t]||"",value:o}}getMaxBorderWidth(t){let e=0,s=this.chart,n,o,a,r,l;if(!t){for(n=0,o=s.data.datasets.length;nt!=="spacing",_indexable:t=>t!=="spacing"&&!t.startsWith("borderDash")&&!t.startsWith("hoverBorderDash")}),M(kt,"overrides",{aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){let e=t.data,{labels:{pointStyle:s,textAlign:n,color:o,useBorderRadius:a,borderRadius:r}}=t.legend.options;return e.labels.length&&e.datasets.length?e.labels.map((l,c)=>{let d=t.getDatasetMeta(0).controller.getStyle(c);return{text:l,fillStyle:d.backgroundColor,fontColor:o,hidden:!t.getDataVisibility(c),lineDash:d.borderDash,lineDashOffset:d.borderDashOffset,lineJoin:d.borderJoinStyle,lineWidth:d.borderWidth,strokeStyle:d.borderColor,textAlign:n,pointStyle:s,borderRadius:a&&(r||d.borderRadius),index:c}}):[]}},onClick(t,e,s){s.chart.toggleDataVisibility(e.index),s.chart.update()}}}});var he=class extends it{initialize(){this.enableOptionSharing=!0,this.supportsDecimation=!0,super.initialize()}update(t){let e=this._cachedMeta,{dataset:s,data:n=[],_dataset:o}=e,a=this.chart._animationsDisabled,{start:r,count:l}=Xi(e,n,a);this._drawStart=r,this._drawCount=l,Ki(e)&&(r=0,l=n.length),s._chart=this.chart,s._datasetIndex=this.index,s._decimated=!!o._decimated,s.points=n;let c=this.resolveDatasetElementOptions(t);this.options.showLine||(c.borderWidth=0),c.segment=this.options.segment,this.updateElement(s,void 0,{animated:!a,options:c},t),this.updateElements(n,r,l,t)}updateElements(t,e,s,n){let o=n==="reset",{iScale:a,vScale:r,_stacked:l,_dataset:c}=this._cachedMeta,{sharedOptions:h,includeOptions:d}=this._getSharedOptions(e,n),u=a.axis,f=r.axis,{spanGaps:g,segment:p}=this.options,m=Vt(g)?g:Number.POSITIVE_INFINITY,b=this.chart._animationsDisabled||o||n==="none",x=e+s,v=t.length,y=e>0&&this.getParsed(e-1);for(let _=0;_=x){w.skip=!0;continue}let S=this.getParsed(_),P=A(S[f]),O=w[u]=a.getPixelForValue(S[u],_),C=w[f]=o||P?r.getBasePixel():r.getPixelForValue(l?this.applyStack(r,S,l):S[f],_);w.skip=isNaN(O)||isNaN(C)||P,w.stop=_>0&&Math.abs(S[u]-y[u])>m,p&&(w.parsed=S,w.raw=c.data[_]),d&&(w.options=h||this.resolveDataElementOptions(_,k.active?"active":n)),b||this.updateElement(k,_,w,n),y=S}}getMaxOverflow(){let t=this._cachedMeta,e=t.dataset,s=e.options&&e.options.borderWidth||0,n=t.data||[];if(!n.length)return s;let o=n[0].size(this.resolveDataElementOptions(0)),a=n[n.length-1].size(this.resolveDataElementOptions(n.length-1));return Math.max(s,o,a)/2}draw(){let t=this._cachedMeta;t.dataset.updateControlPoints(this.chart.chartArea,t.iScale.axis),super.draw()}};M(he,"id","line"),M(he,"defaults",{datasetElementType:"line",dataElementType:"point",showLine:!0,spanGaps:!1}),M(he,"overrides",{scales:{_index_:{type:"category"},_value_:{type:"linear"}}});var Yt=class extends it{constructor(t,e){super(t,e),this.innerRadius=void 0,this.outerRadius=void 0}getLabelAndValue(t){let e=this._cachedMeta,s=this.chart,n=s.data.labels||[],o=ne(e._parsed[t].r,s.options.locale);return{label:n[t]||"",value:o}}parseObjectData(t,e,s,n){return ss.bind(this)(t,e,s,n)}update(t){let e=this._cachedMeta.data;this._updateRadius(),this.updateElements(e,0,e.length,t)}getMinMax(){let t=this._cachedMeta,e={min:Number.POSITIVE_INFINITY,max:Number.NEGATIVE_INFINITY};return t.data.forEach((s,n)=>{let o=this.getParsed(n).r;!isNaN(o)&&this.chart.getDataVisibility(n)&&(oe.max&&(e.max=o))}),e}_updateRadius(){let t=this.chart,e=t.chartArea,s=t.options,n=Math.min(e.right-e.left,e.bottom-e.top),o=Math.max(n/2,0),a=Math.max(s.cutoutPercentage?o/100*s.cutoutPercentage:1,0),r=(o-a)/t.getVisibleDatasetCount();this.outerRadius=o-r*this.index,this.innerRadius=this.outerRadius-r}updateElements(t,e,s,n){let o=n==="reset",a=this.chart,l=a.options.animation,c=this._cachedMeta.rScale,h=c.xCenter,d=c.yCenter,u=c.getIndexAngle(0)-.5*R,f=u,g,p=360/this.countVisibleElements();for(g=0;g{!isNaN(this.getParsed(n).r)&&this.chart.getDataVisibility(n)&&e++}),e}_computeAngle(t,e,s){return this.chart.getDataVisibility(t)?ot(this.resolveDataElementOptions(t,e).angle||s):0}};M(Yt,"id","polarArea"),M(Yt,"defaults",{dataElementType:"arc",animation:{animateRotate:!0,animateScale:!0},animations:{numbers:{type:"number",properties:["x","y","startAngle","endAngle","innerRadius","outerRadius"]}},indexAxis:"r",startAngle:0}),M(Yt,"overrides",{aspectRatio:1,plugins:{legend:{labels:{generateLabels(t){let e=t.data;if(e.labels.length&&e.datasets.length){let{labels:{pointStyle:s,color:n}}=t.legend.options;return e.labels.map((o,a)=>{let l=t.getDatasetMeta(0).controller.getStyle(a);return{text:o,fillStyle:l.backgroundColor,strokeStyle:l.borderColor,fontColor:n,lineWidth:l.borderWidth,pointStyle:s,hidden:!t.getDataVisibility(a),index:a}})}return[]}},onClick(t,e,s){s.chart.toggleDataVisibility(e.index),s.chart.update()}}},scales:{r:{type:"radialLinear",angleLines:{display:!1},beginAtZero:!0,grid:{circular:!0},pointLabels:{display:!1},startAngle:0}}});var Re=class extends kt{};M(Re,"id","pie"),M(Re,"defaults",{cutout:0,rotation:0,circumference:360,radius:"100%"});var de=class extends it{getLabelAndValue(t){let e=this._cachedMeta.vScale,s=this.getParsed(t);return{label:e.getLabels()[t],value:""+e.getLabelForValue(s[e.axis])}}parseObjectData(t,e,s,n){return ss.bind(this)(t,e,s,n)}update(t){let e=this._cachedMeta,s=e.dataset,n=e.data||[],o=e.iScale.getLabels();if(s.points=n,t!=="resize"){let a=this.resolveDatasetElementOptions(t);this.options.showLine||(a.borderWidth=0);let r={_loop:!0,_fullLoop:o.length===n.length,options:a};this.updateElement(s,void 0,r,t)}this.updateElements(n,0,n.length,t)}updateElements(t,e,s,n){let o=this._cachedMeta.rScale,a=n==="reset";for(let r=e;r0&&this.getParsed(e-1);for(let y=e;y0&&Math.abs(k[f]-v[f])>b,m&&(w.parsed=k,w.raw=c.data[y]),u&&(w.options=d||this.resolveDataElementOptions(y,_.active?"active":n)),x||this.updateElement(_,y,w,n),v=k}this.updateSharedOptions(d,n,h)}getMaxOverflow(){let t=this._cachedMeta,e=t.data||[];if(!this.options.showLine){let r=0;for(let l=e.length-1;l>=0;--l)r=Math.max(r,e[l].size(this.resolveDataElementOptions(l))/2);return r>0&&r}let s=t.dataset,n=s.options&&s.options.borderWidth||0;if(!e.length)return n;let o=e[0].size(this.resolveDataElementOptions(0)),a=e[e.length-1].size(this.resolveDataElementOptions(e.length-1));return Math.max(n,o,a)/2}};M(ue,"id","scatter"),M(ue,"defaults",{datasetElementType:!1,dataElementType:"point",showLine:!1,fill:!1}),M(ue,"overrides",{interaction:{mode:"point"},scales:{x:{type:"linear"},y:{type:"linear"}}});var Nr=Object.freeze({__proto__:null,BarController:le,BubbleController:ce,DoughnutController:kt,LineController:he,PieController:Re,PolarAreaController:Yt,RadarController:de,ScatterController:ue});function Nt(){throw new Error("This method is not implemented: Check that a complete date adapter is provided.")}var ws=class i{constructor(t){M(this,"options");this.options=t||{}}static override(t){Object.assign(i.prototype,t)}init(){}formats(){return Nt()}parse(){return Nt()}format(){return Nt()}add(){return Nt()}diff(){return Nt()}startOf(){return Nt()}endOf(){return Nt()}},Hr={_date:ws};function jr(i,t,e,s){let{controller:n,data:o,_sorted:a}=i,r=n._cachedMeta.iScale,l=i.dataset&&i.dataset.options?i.dataset.options.spanGaps:null;if(r&&t===r.axis&&t!=="r"&&a&&o.length){let c=r._reversePixels?ln:ct;if(s){if(n._sharedOptions){let h=o[0],d=typeof h.getRange=="function"&&h.getRange(t);if(d){let u=c(o,t,e-d),f=c(o,t,e+d);return{lo:u.lo,hi:f.hi}}}}else{let h=c(o,t,e);if(l){let{vScale:d}=n._cachedMeta,{_parsed:u}=i,f=u.slice(0,h.lo+1).reverse().findIndex(p=>!A(p[d.axis]));h.lo-=Math.max(0,f);let g=u.slice(h.hi).findIndex(p=>!A(p[d.axis]));h.hi+=Math.max(0,g)}return h}}return{lo:0,hi:o.length-1}}function $e(i,t,e,s,n){let o=i.getSortedVisibleDatasetMetas(),a=e[t];for(let r=0,l=o.length;r{l[a]&&l[a](t[e],n)&&(o.push({element:l,datasetIndex:c,index:h}),r=r||l.inRange(t.x,t.y,n))}),s&&!r?[]:o}var Xr={evaluateInteractionItems:$e,modes:{index(i,t,e,s){let n=Lt(t,i),o=e.axis||"x",a=e.includeInvisible||!1,r=e.intersect?ps(i,n,o,s,a):ms(i,n,o,!1,s,a),l=[];return r.length?(i.getSortedVisibleDatasetMetas().forEach(c=>{let h=r[0].index,d=c.data[h];d&&!d.skip&&l.push({element:d,datasetIndex:c.index,index:h})}),l):[]},dataset(i,t,e,s){let n=Lt(t,i),o=e.axis||"xy",a=e.includeInvisible||!1,r=e.intersect?ps(i,n,o,s,a):ms(i,n,o,!1,s,a);if(r.length>0){let l=r[0].datasetIndex,c=i.getDatasetMeta(l).data;r=[];for(let h=0;he.pos===t)}function Wn(i,t){return i.filter(e=>Lo.indexOf(e.pos)===-1&&e.box.axis===t)}function Ae(i,t){return i.sort((e,s)=>{let n=t?s:e,o=t?e:s;return n.weight===o.weight?n.index-o.index:n.weight-o.weight})}function Kr(i){let t=[],e,s,n,o,a,r;for(e=0,s=(i||[]).length;ec.box.fullSize),!0),s=Ae(Ce(t,"left"),!0),n=Ae(Ce(t,"right")),o=Ae(Ce(t,"top"),!0),a=Ae(Ce(t,"bottom")),r=Wn(t,"x"),l=Wn(t,"y");return{fullSize:e,leftAndTop:s.concat(o),rightAndBottom:n.concat(l).concat(a).concat(r),chartArea:Ce(t,"chartArea"),vertical:s.concat(n).concat(l),horizontal:o.concat(a).concat(r)}}function Nn(i,t,e,s){return Math.max(i[e],t[e])+Math.max(i[s],t[s])}function Ro(i,t){i.top=Math.max(i.top,t.top),i.left=Math.max(i.left,t.left),i.bottom=Math.max(i.bottom,t.bottom),i.right=Math.max(i.right,t.right)}function Zr(i,t,e,s){let{pos:n,box:o}=e,a=i.maxPadding;if(!T(n)){e.size&&(i[n]-=e.size);let d=s[e.stack]||{size:0,count:1};d.size=Math.max(d.size,e.horizontal?o.height:o.width),e.size=d.size/d.count,i[n]+=e.size}o.getPadding&&Ro(a,o.getPadding());let r=Math.max(0,t.outerWidth-Nn(a,i,"left","right")),l=Math.max(0,t.outerHeight-Nn(a,i,"top","bottom")),c=r!==i.w,h=l!==i.h;return i.w=r,i.h=l,e.horizontal?{same:c,other:h}:{same:h,other:c}}function Qr(i){let t=i.maxPadding;function e(s){let n=Math.max(t[s]-i[s],0);return i[s]+=n,n}i.y+=e("top"),i.x+=e("left"),e("right"),e("bottom")}function tl(i,t){let e=t.maxPadding;function s(n){let o={left:0,top:0,right:0,bottom:0};return n.forEach(a=>{o[a]=Math.max(t[a],e[a])}),o}return s(i?["left","right"]:["top","bottom"])}function Ee(i,t,e,s){let n=[],o,a,r,l,c,h;for(o=0,a=i.length,c=0;o{typeof p.beforeLayout=="function"&&p.beforeLayout()});let h=l.reduce((p,m)=>m.box.options&&m.box.options.display===!1?p:p+1,0)||1,d=Object.freeze({outerWidth:t,outerHeight:e,padding:n,availableWidth:o,availableHeight:a,vBoxMaxWidth:o/2/h,hBoxMaxHeight:a/2}),u=Object.assign({},n);Ro(u,q(s));let f=Object.assign({maxPadding:u,w:o,h:a,x:n.left,y:n.top},n),g=Gr(l.concat(c),d);Ee(r.fullSize,f,d,g),Ee(l,f,d,g),Ee(c,f,d,g)&&Ee(l,f,d,g),Qr(f),Hn(r.leftAndTop,f,d,g),f.x+=f.w,f.y+=f.h,Hn(r.rightAndBottom,f,d,g),i.chartArea={left:f.left,top:f.top,right:f.left+f.w,bottom:f.top+f.h,height:f.h,width:f.w},E(r.chartArea,p=>{let m=p.box;Object.assign(m,i.chartArea),m.update(f.w,f.h,{left:0,top:0,right:0,bottom:0})})}},Mi=class{acquireContext(t,e){}releaseContext(t){return!1}addEventListener(t,e,s){}removeEventListener(t,e,s){}getDevicePixelRatio(){return 1}getMaximumSize(t,e,s,n){return e=Math.max(0,e||t.width),s=s||t.height,{width:e,height:Math.max(0,n?Math.floor(e/n):s)}}isAttached(t){return!0}updateConfig(t){}},Ss=class extends Mi{acquireContext(t){return t&&t.getContext&&t.getContext("2d")||null}updateConfig(t){t.options.animation=!1}},_i="$chartjs",el={touchstart:"mousedown",touchmove:"mousemove",touchend:"mouseup",pointerenter:"mouseenter",pointerdown:"mousedown",pointermove:"mousemove",pointerup:"mouseup",pointerleave:"mouseout",pointerout:"mouseout"},jn=i=>i===null||i==="";function il(i,t){let e=i.style,s=i.getAttribute("height"),n=i.getAttribute("width");if(i[_i]={initial:{height:s,width:n,style:{display:e.display,height:e.height,width:e.width}}},e.display=e.display||"block",e.boxSizing=e.boxSizing||"border-box",jn(n)){let o=os(i,"width");o!==void 0&&(i.width=o)}if(jn(s))if(i.style.height==="")i.height=i.width/(t||2);else{let o=os(i,"height");o!==void 0&&(i.height=o)}return i}var Eo=Sn?{passive:!0}:!1;function sl(i,t,e){i&&i.addEventListener(t,e,Eo)}function nl(i,t,e){i&&i.canvas&&i.canvas.removeEventListener(t,e,Eo)}function ol(i,t){let e=el[i.type]||i.type,{x:s,y:n}=Lt(i,t);return{type:e,chart:t,native:i,x:s!==void 0?s:null,y:n!==void 0?n:null}}function ki(i,t){for(let e of i)if(e===t||e.contains(t))return!0}function al(i,t,e){let s=i.canvas,n=new MutationObserver(o=>{let a=!1;for(let r of o)a=a||ki(r.addedNodes,s),a=a&&!ki(r.removedNodes,s);a&&e()});return n.observe(document,{childList:!0,subtree:!0}),n}function rl(i,t,e){let s=i.canvas,n=new MutationObserver(o=>{let a=!1;for(let r of o)a=a||ki(r.removedNodes,s),a=a&&!ki(r.addedNodes,s);a&&e()});return n.observe(document,{childList:!0,subtree:!0}),n}var Ne=new Map,$n=0;function Io(){let i=window.devicePixelRatio;i!==$n&&($n=i,Ne.forEach((t,e)=>{e.currentDevicePixelRatio!==i&&t()}))}function ll(i,t){Ne.size||window.addEventListener("resize",Io),Ne.set(i,t)}function cl(i){Ne.delete(i),Ne.size||window.removeEventListener("resize",Io)}function hl(i,t,e){let s=i.canvas,n=s&&di(s);if(!n)return;let o=Ui((r,l)=>{let c=n.clientWidth;e(r,l),c{let l=r[0],c=l.contentRect.width,h=l.contentRect.height;c===0&&h===0||o(c,h)});return a.observe(n),ll(i,o),a}function bs(i,t,e){e&&e.disconnect(),t==="resize"&&cl(i)}function dl(i,t,e){let s=i.canvas,n=Ui(o=>{i.ctx!==null&&e(ol(o,i))},i);return sl(s,t,n),n}var Ps=class extends Mi{acquireContext(t,e){let s=t&&t.getContext&&t.getContext("2d");return s&&s.canvas===t?(il(t,e),s):null}releaseContext(t){let e=t.canvas;if(!e[_i])return!1;let s=e[_i].initial;["height","width"].forEach(o=>{let a=s[o];A(a)?e.removeAttribute(o):e.setAttribute(o,a)});let n=s.style||{};return Object.keys(n).forEach(o=>{e.style[o]=n[o]}),e.width=e.width,delete e[_i],!0}addEventListener(t,e,s){this.removeEventListener(t,e);let n=t.$proxies||(t.$proxies={}),a={attach:al,detach:rl,resize:hl}[e]||dl;n[e]=a(t,e,s)}removeEventListener(t,e){let s=t.$proxies||(t.$proxies={}),n=s[e];if(!n)return;({attach:bs,detach:bs,resize:bs}[e]||nl)(t,e,n),s[e]=void 0}getDevicePixelRatio(){return window.devicePixelRatio}getMaximumSize(t,e,s,n){return wn(t,e,s,n)}isAttached(t){let e=t&&di(t);return!!(e&&e.isConnected)}};function ul(i){return!hi()||typeof OffscreenCanvas<"u"&&i instanceof OffscreenCanvas?Ss:Ps}var st=class{constructor(){M(this,"x");M(this,"y");M(this,"active",!1);M(this,"options");M(this,"$animations")}tooltipPosition(t){let{x:e,y:s}=this.getProps(["x","y"],t);return{x:e,y:s}}hasValue(){return Vt(this.x)&&Vt(this.y)}getProps(t,e){let s=this.$animations;if(!e||!s)return this;let n={};return t.forEach(o=>{n[o]=s[o]&&s[o].active()?s[o]._to:this[o]}),n}};M(st,"defaults",{}),M(st,"defaultRoutes");function fl(i,t){let e=i.options.ticks,s=gl(i),n=Math.min(e.maxTicksLimit||s,s),o=e.major.enabled?ml(t):[],a=o.length,r=o[0],l=o[a-1],c=[];if(a>n)return bl(t,c,o,a/n),c;let h=pl(o,t,n);if(a>0){let d,u,f=a>1?Math.round((l-r)/(a-1)):null;for(gi(t,c,h,A(f)?0:r-f,r),d=0,u=a-1;dn)return l}return Math.max(n,1)}function ml(i){let t=[],e,s;for(e=0,s=i.length;ei==="left"?"right":i==="right"?"left":i,Yn=(i,t,e)=>t==="top"||t==="left"?i[t]+e:i[t]-e,Un=(i,t)=>Math.min(t||i,i);function Xn(i,t){let e=[],s=i.length/t,n=i.length,o=0;for(;oa+r)))return l}function vl(i,t){E(i,e=>{let s=e.gc,n=s.length/2,o;if(n>t){for(o=0;os?s:e,s=n&&e>s?e:s,{min:Z(e,Z(s,e)),max:Z(s,Z(e,s))}}getPadding(){return{left:this.paddingLeft||0,top:this.paddingTop||0,right:this.paddingRight||0,bottom:this.paddingBottom||0}}getTicks(){return this.ticks}getLabels(){let t=this.chart.data;return this.options.labels||(this.isHorizontal()?t.xLabels:t.yLabels)||t.labels||[]}getLabelItems(t=this.chart.chartArea){return this._labelItems||(this._labelItems=this._computeLabelItems(t))}beforeLayout(){this._cache={},this._dataLimitsCached=!1}beforeUpdate(){F(this.options.beforeUpdate,[this])}update(t,e,s){let{beginAtZero:n,grace:o,ticks:a}=this.options,r=a.sampleSize;this.beforeUpdate(),this.maxWidth=t,this.maxHeight=e,this._margins=s=Object.assign({left:0,right:0,top:0,bottom:0},s),this.ticks=null,this._labelSizes=null,this._gridLineItems=null,this._labelItems=null,this.beforeSetDimensions(),this.setDimensions(),this.afterSetDimensions(),this._maxLength=this.isHorizontal()?this.width+s.left+s.right:this.height+s.top+s.bottom,this._dataLimitsCached||(this.beforeDataLimits(),this.determineDataLimits(),this.afterDataLimits(),this._range=xn(this,o,n),this._dataLimitsCached=!0),this.beforeBuildTicks(),this.ticks=this.buildTicks()||[],this.afterBuildTicks();let l=r=o||s<=1||!this.isHorizontal()){this.labelRotation=n;return}let h=this._getLabelSizes(),d=h.widest.width,u=h.highest.height,f=Y(this.chart.width-d,0,this.maxWidth);r=t.offset?this.maxWidth/s:f/(s-1),d+6>r&&(r=f/(s-(t.offset?.5:1)),l=this.maxHeight-Te(t.grid)-e.padding-Kn(t.title,this.chart.options.font),c=Math.sqrt(d*d+u*u),a=si(Math.min(Math.asin(Y((h.highest.height+6)/r,-1,1)),Math.asin(Y(l/c,-1,1))-Math.asin(Y(u/c,-1,1)))),a=Math.max(n,Math.min(o,a))),this.labelRotation=a}afterCalculateLabelRotation(){F(this.options.afterCalculateLabelRotation,[this])}afterAutoSkip(){}beforeFit(){F(this.options.beforeFit,[this])}fit(){let t={width:0,height:0},{chart:e,options:{ticks:s,title:n,grid:o}}=this,a=this._isVisible(),r=this.isHorizontal();if(a){let l=Kn(n,e.options.font);if(r?(t.width=this.maxWidth,t.height=Te(o)+l):(t.height=this.maxHeight,t.width=Te(o)+l),s.display&&this.ticks.length){let{first:c,last:h,widest:d,highest:u}=this._getLabelSizes(),f=s.padding*2,g=ot(this.labelRotation),p=Math.cos(g),m=Math.sin(g);if(r){let b=s.mirror?0:m*d.width+p*u.height;t.height=Math.min(this.maxHeight,t.height+b+f)}else{let b=s.mirror?0:p*d.width+m*u.height;t.width=Math.min(this.maxWidth,t.width+b+f)}this._calculatePadding(c,h,m,p)}}this._handleMargins(),r?(this.width=this._length=e.width-this._margins.left-this._margins.right,this.height=t.height):(this.width=t.width,this.height=this._length=e.height-this._margins.top-this._margins.bottom)}_calculatePadding(t,e,s,n){let{ticks:{align:o,padding:a},position:r}=this.options,l=this.labelRotation!==0,c=r!=="top"&&this.axis==="x";if(this.isHorizontal()){let h=this.getPixelForTick(0)-this.left,d=this.right-this.getPixelForTick(this.ticks.length-1),u=0,f=0;l?c?(u=n*t.width,f=s*e.height):(u=s*t.height,f=n*e.width):o==="start"?f=e.width:o==="end"?u=t.width:o!=="inner"&&(u=t.width/2,f=e.width/2),this.paddingLeft=Math.max((u-h+a)*this.width/(this.width-h),0),this.paddingRight=Math.max((f-d+a)*this.width/(this.width-d),0)}else{let h=e.height/2,d=t.height/2;o==="start"?(h=0,d=t.height):o==="end"&&(h=e.height,d=0),this.paddingTop=h+a,this.paddingBottom=d+a}}_handleMargins(){this._margins&&(this._margins.left=Math.max(this.paddingLeft,this._margins.left),this._margins.top=Math.max(this.paddingTop,this._margins.top),this._margins.right=Math.max(this.paddingRight,this._margins.right),this._margins.bottom=Math.max(this.paddingBottom,this._margins.bottom))}afterFit(){F(this.options.afterFit,[this])}isHorizontal(){let{axis:t,position:e}=this.options;return e==="top"||e==="bottom"||t==="x"}isFullSize(){return this.options.fullSize}_convertTicksToLabels(t){this.beforeTickToLabelConversion(),this.generateTickLabels(t);let e,s;for(e=0,s=t.length;e({width:a[P]||0,height:r[P]||0});return{first:S(0),last:S(e-1),widest:S(k),highest:S(w),widths:a,heights:r}}getLabelForValue(t){return t}getPixelForValue(t,e){return NaN}getValueForPixel(t){}getPixelForTick(t){let e=this.ticks;return t<0||t>e.length-1?null:this.getPixelForValue(e[t].value)}getPixelForDecimal(t){this._reversePixels&&(t=1-t);let e=this._startPixel+t*this._length;return rn(this._alignToPixels?Ct(this.chart,e,0):e)}getDecimalForPixel(t){let e=(t-this._startPixel)/this._length;return this._reversePixels?1-e:e}getBasePixel(){return this.getPixelForValue(this.getBaseValue())}getBaseValue(){let{min:t,max:e}=this;return t<0&&e<0?e:t>0&&e>0?t:0}getContext(t){let e=this.ticks||[];if(t>=0&&tr*n?r/s:l/n:l*n0}_computeGridLineItems(t){let e=this.axis,s=this.chart,n=this.options,{grid:o,position:a,border:r}=n,l=o.offset,c=this.isHorizontal(),d=this.ticks.length+(l?1:0),u=Te(o),f=[],g=r.setContext(this.getContext()),p=g.display?g.width:0,m=p/2,b=function(W){return Ct(s,W,p)},x,v,y,_,k,w,S,P,O,C,L,U;if(a==="top")x=b(this.bottom),w=this.bottom-u,P=x-m,C=b(t.top)+m,U=t.bottom;else if(a==="bottom")x=b(this.top),C=t.top,U=b(t.bottom)-m,w=x+m,P=this.top+u;else if(a==="left")x=b(this.right),k=this.right-u,S=x-m,O=b(t.left)+m,L=t.right;else if(a==="right")x=b(this.left),O=t.left,L=b(t.right)-m,k=x+m,S=this.left+u;else if(e==="x"){if(a==="center")x=b((t.top+t.bottom)/2+.5);else if(T(a)){let W=Object.keys(a)[0],j=a[W];x=b(this.chart.scales[W].getPixelForValue(j))}C=t.top,U=t.bottom,w=x+m,P=w+u}else if(e==="y"){if(a==="center")x=b((t.left+t.right)/2);else if(T(a)){let W=Object.keys(a)[0],j=a[W];x=b(this.chart.scales[W].getPixelForValue(j))}k=x-m,S=k-u,O=t.left,L=t.right}let et=D(n.ticks.maxTicksLimit,d),I=Math.max(1,Math.ceil(d/et));for(v=0;v0&&(It-=Et/2);break}Ye={left:It,top:xe,width:Et+qt.width,height:be+qt.height,color:I.backdropColor}}m.push({label:y,font:P,textOffset:L,options:{rotation:p,color:j,strokeColor:rt,strokeWidth:G,textAlign:Kt,textBaseline:U,translation:[_,k],backdrop:Ye}})}return m}_getXAxisLabelAlignment(){let{position:t,ticks:e}=this.options;if(-ot(this.labelRotation))return t==="top"?"left":"right";let n="center";return e.align==="start"?n="left":e.align==="end"?n="right":e.align==="inner"&&(n="inner"),n}_getYAxisLabelAlignment(t){let{position:e,ticks:{crossAlign:s,mirror:n,padding:o}}=this.options,a=this._getLabelSizes(),r=t+o,l=a.widest.width,c,h;return e==="left"?n?(h=this.right+o,s==="near"?c="left":s==="center"?(c="center",h+=l/2):(c="right",h+=l)):(h=this.right-r,s==="near"?c="right":s==="center"?(c="center",h-=l/2):(c="left",h=this.left)):e==="right"?n?(h=this.left+o,s==="near"?c="right":s==="center"?(c="center",h-=l/2):(c="left",h-=l)):(h=this.left+r,s==="near"?c="left":s==="center"?(c="center",h+=l/2):(c="right",h=this.right)):c="right",{textAlign:c,x:h}}_computeLabelArea(){if(this.options.ticks.mirror)return;let t=this.chart,e=this.options.position;if(e==="left"||e==="right")return{top:0,left:this.left,bottom:t.height,right:this.right};if(e==="top"||e==="bottom")return{top:this.top,left:0,bottom:this.bottom,right:t.width}}drawBackground(){let{ctx:t,options:{backgroundColor:e},left:s,top:n,width:o,height:a}=this;e&&(t.save(),t.fillStyle=e,t.fillRect(s,n,o,a),t.restore())}getLineWidthForValue(t){let e=this.options.grid;if(!this._isVisible()||!e.display)return 0;let n=this.ticks.findIndex(o=>o.value===t);return n>=0?e.setContext(this.getContext(n)).lineWidth:0}drawGrid(t){let e=this.options.grid,s=this.ctx,n=this._gridLineItems||(this._gridLineItems=this._computeGridLineItems(t)),o,a,r=(l,c,h)=>{!h.width||!h.color||(s.save(),s.lineWidth=h.width,s.strokeStyle=h.color,s.setLineDash(h.borderDash||[]),s.lineDashOffset=h.borderDashOffset,s.beginPath(),s.moveTo(l.x,l.y),s.lineTo(c.x,c.y),s.stroke(),s.restore())};if(e.display)for(o=0,a=n.length;o{this.draw(o)}}]:[{z:s,draw:o=>{this.drawBackground(),this.drawGrid(o),this.drawTitle()}},{z:n,draw:()=>{this.drawBorder()}},{z:e,draw:o=>{this.drawLabels(o)}}]}getMatchingVisibleMetas(t){let e=this.chart.getSortedVisibleDatasetMetas(),s=this.axis+"AxisID",n=[],o,a;for(o=0,a=e.length;o{let s=e.split("."),n=s.pop(),o=[i].concat(s).join("."),a=t[e].split("."),r=a.pop(),l=a.join(".");V.route(o,n,l,r)})}function Ol(i){return"id"in i&&"defaults"in i}var Ds=class{constructor(){this.controllers=new pe(it,"datasets",!0),this.elements=new pe(st,"elements"),this.plugins=new pe(Object,"plugins"),this.scales=new pe(Xt,"scales"),this._typedRegistries=[this.controllers,this.scales,this.elements]}add(...t){this._each("register",t)}remove(...t){this._each("unregister",t)}addControllers(...t){this._each("register",t,this.controllers)}addElements(...t){this._each("register",t,this.elements)}addPlugins(...t){this._each("register",t,this.plugins)}addScales(...t){this._each("register",t,this.scales)}getController(t){return this._get(t,this.controllers,"controller")}getElement(t){return this._get(t,this.elements,"element")}getPlugin(t){return this._get(t,this.plugins,"plugin")}getScale(t){return this._get(t,this.scales,"scale")}removeControllers(...t){this._each("unregister",t,this.controllers)}removeElements(...t){this._each("unregister",t,this.elements)}removePlugins(...t){this._each("unregister",t,this.plugins)}removeScales(...t){this._each("unregister",t,this.scales)}_each(t,e,s){[...e].forEach(n=>{let o=s||this._getRegistryForType(n);s||o.isForType(n)||o===this.plugins&&n.id?this._exec(t,o,n):E(n,a=>{let r=s||this._getRegistryForType(a);this._exec(t,r,a)})})}_exec(t,e,s){let n=ii(t);F(s["before"+n],[],s),e[t](s),F(s["after"+n],[],s)}_getRegistryForType(t){for(let e=0;eo.filter(r=>!a.some(l=>r.plugin.id===l.plugin.id));this._notify(n(e,s),t,"stop"),this._notify(n(s,e),t,"start")}};function Cl(i){let t={},e=[],s=Object.keys(gt.plugins.items);for(let o=0;o1&&qn(i[0].toLowerCase());if(s)return s}throw new Error(`Cannot determine type of '${i}' axis. Please provide 'axis' or 'position' option.`)}function Gn(i,t,e){if(e[t+"AxisID"]===i)return{axis:t}}function Fl(i,t){if(t.data&&t.data.datasets){let e=t.data.datasets.filter(s=>s.xAxisID===i||s.yAxisID===i);if(e.length)return Gn(i,"x",e[0])||Gn(i,"y",e[0])}return{}}function zl(i,t){let e=Ot[i.type]||{scales:{}},s=t.scales||{},n=Cs(i.type,t),o=Object.create(null);return Object.keys(s).forEach(a=>{let r=s[a];if(!T(r))return console.error(`Invalid scale configuration for scale: ${a}`);if(r._proxy)return console.warn(`Ignoring resolver passed as options for scale: ${a}`);let l=As(a,r,Fl(a,i),V.scales[r.type]),c=El(l,n),h=e.scales||{};o[a]=te(Object.create(null),[{axis:l},r,h[l],h[c]])}),i.data.datasets.forEach(a=>{let r=a.type||i.type,l=a.indexAxis||Cs(r,t),h=(Ot[r]||{}).scales||{};Object.keys(h).forEach(d=>{let u=Rl(d,l),f=a[u+"AxisID"]||u;o[f]=o[f]||Object.create(null),te(o[f],[{axis:u},s[f],h[d]])})}),Object.keys(o).forEach(a=>{let r=o[a];te(r,[V.scales[r.type],V.scale])}),o}function Fo(i){let t=i.options||(i.options={});t.plugins=D(t.plugins,{}),t.scales=zl(i,t)}function zo(i){return i=i||{},i.datasets=i.datasets||[],i.labels=i.labels||[],i}function Bl(i){return i=i||{},i.data=zo(i.data),Fo(i),i}var Jn=new Map,Bo=new Set;function pi(i,t){let e=Jn.get(i);return e||(e=t(),Jn.set(i,e),Bo.add(e)),e}var Le=(i,t,e)=>{let s=_t(t,e);s!==void 0&&i.add(s)},Ts=class{constructor(t){this._config=Bl(t),this._scopeCache=new Map,this._resolverCache=new Map}get platform(){return this._config.platform}get type(){return this._config.type}set type(t){this._config.type=t}get data(){return this._config.data}set data(t){this._config.data=zo(t)}get options(){return this._config.options}set options(t){this._config.options=t}get plugins(){return this._config.plugins}update(){let t=this._config;this.clearCache(),Fo(t)}clearCache(){this._scopeCache.clear(),this._resolverCache.clear()}datasetScopeKeys(t){return pi(t,()=>[[`datasets.${t}`,""]])}datasetAnimationScopeKeys(t,e){return pi(`${t}.transition.${e}`,()=>[[`datasets.${t}.transitions.${e}`,`transitions.${e}`],[`datasets.${t}`,""]])}datasetElementScopeKeys(t,e){return pi(`${t}-${e}`,()=>[[`datasets.${t}.elements.${e}`,`datasets.${t}`,`elements.${e}`,""]])}pluginScopeKeys(t){let e=t.id,s=this.type;return pi(`${s}-plugin-${e}`,()=>[[`plugins.${e}`,...t.additionalOptionScopes||[]]])}_cachedScopes(t,e){let s=this._scopeCache,n=s.get(t);return(!n||e)&&(n=new Map,s.set(t,n)),n}getOptionScopes(t,e,s){let{options:n,type:o}=this,a=this._cachedScopes(t,s),r=a.get(e);if(r)return r;let l=new Set;e.forEach(h=>{t&&(l.add(t),h.forEach(d=>Le(l,t,d))),h.forEach(d=>Le(l,n,d)),h.forEach(d=>Le(l,Ot[o]||{},d)),h.forEach(d=>Le(l,V,d)),h.forEach(d=>Le(l,ai,d))});let c=Array.from(l);return c.length===0&&c.push(Object.create(null)),Bo.has(e)&&a.set(e,c),c}chartOptionScopes(){let{options:t,type:e}=this;return[t,Ot[e]||{},V.datasets[e]||{},{type:e},V,ai]}resolveNamedOptions(t,e,s,n=[""]){let o={$shared:!0},{resolver:a,subPrefixes:r}=Zn(this._resolverCache,t,n),l=a;if(Wl(a,e)){o.$shared=!1,s=bt(s)?s():s;let c=this.createResolver(t,s,r);l=Bt(a,s,c)}for(let c of e)o[c]=l[c];return o}createResolver(t,e,s=[""],n){let{resolver:o}=Zn(this._resolverCache,t,s);return T(e)?Bt(o,e,void 0,n):o}};function Zn(i,t,e){let s=i.get(t);s||(s=new Map,i.set(t,s));let n=e.join(),o=s.get(n);return o||(o={resolver:ci(t,e),subPrefixes:e.filter(r=>!r.toLowerCase().includes("hover"))},s.set(n,o)),o}var Vl=i=>T(i)&&Object.getOwnPropertyNames(i).some(t=>bt(i[t]));function Wl(i,t){let{isScriptable:e,isIndexable:s}=ts(i);for(let n of t){let o=e(n),a=s(n),r=(a||o)&&i[n];if(o&&(bt(r)||Vl(r))||a&&z(r))return!0}return!1}var Nl="4.5.1",Hl=["top","bottom","left","right","chartArea"];function Qn(i,t){return i==="top"||i==="bottom"||Hl.indexOf(i)===-1&&t==="x"}function to(i,t){return function(e,s){return e[i]===s[i]?e[t]-s[t]:e[i]-s[i]}}function eo(i){let t=i.chart,e=t.options.animation;t.notifyPlugins("afterRender"),F(e&&e.onComplete,[i],t)}function jl(i){let t=i.chart,e=t.options.animation;F(e&&e.onProgress,[i],t)}function Vo(i){return hi()&&typeof i=="string"?i=document.getElementById(i):i&&i.length&&(i=i[0]),i&&i.canvas&&(i=i.canvas),i}var yi={},io=i=>{let t=Vo(i);return Object.values(yi).filter(e=>e.canvas===t).pop()};function $l(i,t,e){let s=Object.keys(i);for(let n of s){let o=+n;if(o>=t){let a=i[n];delete i[n],(e>0||o>t)&&(i[o+e]=a)}}}function Yl(i,t,e,s){return!e||i.type==="mouseout"?null:s?t:i}var at=class{static register(...t){gt.add(...t),so()}static unregister(...t){gt.remove(...t),so()}constructor(t,e){let s=this.config=new Ts(e),n=Vo(t),o=io(n);if(o)throw new Error("Canvas is already in use. Chart with ID '"+o.id+"' must be destroyed before the canvas with ID '"+o.canvas.id+"' can be reused.");let a=s.createResolver(s.chartOptionScopes(),this.getContext());this.platform=new(s.platform||ul(n)),this.platform.updateConfig(s);let r=this.platform.acquireContext(n,a.aspectRatio),l=r&&r.canvas,c=l&&l.height,h=l&&l.width;if(this.id=tn(),this.ctx=r,this.canvas=l,this.width=h,this.height=c,this._options=a,this._aspectRatio=this.aspectRatio,this._layers=[],this._metasets=[],this._stacks=void 0,this.boxes=[],this.currentDevicePixelRatio=void 0,this.chartArea=void 0,this._active=[],this._lastEvent=void 0,this._listeners={},this._responsiveListeners=void 0,this._sortedMetasets=[],this.scales={},this._plugins=new Os,this.$proxies={},this._hiddenIndices={},this.attached=!1,this._animationsDisabled=void 0,this.$context=void 0,this._doResize=un(d=>this.update(d),a.resizeDelay||0),this._dataChanges=[],yi[this.id]=this,!r||!l){console.error("Failed to create chart: can't acquire context from the given item");return}vt.listen(this,"complete",eo),vt.listen(this,"progress",jl),this._initialize(),this.attached&&this.update()}get aspectRatio(){let{options:{aspectRatio:t,maintainAspectRatio:e},width:s,height:n,_aspectRatio:o}=this;return A(t)?e&&o?o:n?s/n:null:t}get data(){return this.config.data}set data(t){this.config.data=t}get options(){return this._options}set options(t){this.config.options=t}get registry(){return gt}_initialize(){return this.notifyPlugins("beforeInit"),this.options.responsive?this.resize():ns(this,this.options.devicePixelRatio),this.bindEvents(),this.notifyPlugins("afterInit"),this}clear(){return Ji(this.canvas,this.ctx),this}stop(){return vt.stop(this),this}resize(t,e){vt.running(this)?this._resizeBeforeDraw={width:t,height:e}:this._resize(t,e)}_resize(t,e){let s=this.options,n=this.canvas,o=s.maintainAspectRatio&&this.aspectRatio,a=this.platform.getMaximumSize(n,t,e,o),r=s.devicePixelRatio||this.platform.getDevicePixelRatio(),l=this.width?"resize":"attach";this.width=a.width,this.height=a.height,this._aspectRatio=this.aspectRatio,ns(this,r,!0)&&(this.notifyPlugins("resize",{size:a}),F(s.onResize,[this,a],this),this.attached&&this._doResize(l)&&this.render())}ensureScalesHaveIDs(){let e=this.options.scales||{};E(e,(s,n)=>{s.id=n})}buildOrUpdateScales(){let t=this.options,e=t.scales,s=this.scales,n=Object.keys(s).reduce((a,r)=>(a[r]=!1,a),{}),o=[];e&&(o=o.concat(Object.keys(e).map(a=>{let r=e[a],l=As(a,r),c=l==="r",h=l==="x";return{options:r,dposition:c?"chartArea":h?"bottom":"left",dtype:c?"radialLinear":h?"category":"linear"}}))),E(o,a=>{let r=a.options,l=r.id,c=As(l,r),h=D(r.type,a.dtype);(r.position===void 0||Qn(r.position,c)!==Qn(a.dposition))&&(r.position=a.dposition),n[l]=!0;let d=null;if(l in s&&s[l].type===h)d=s[l];else{let u=gt.getScale(h);d=new u({id:l,type:h,ctx:this.ctx,chart:this}),s[d.id]=d}d.init(r,t)}),E(n,(a,r)=>{a||delete s[r]}),E(s,a=>{J.configure(this,a,a.options),J.addBox(this,a)})}_updateMetasets(){let t=this._metasets,e=this.data.datasets.length,s=t.length;if(t.sort((n,o)=>n.index-o.index),s>e){for(let n=e;ne.length&&delete this._stacks,t.forEach((s,n)=>{e.filter(o=>o===s._dataset).length===0&&this._destroyDatasetMeta(n)})}buildOrUpdateControllers(){let t=[],e=this.data.datasets,s,n;for(this._removeUnreferencedMetasets(),s=0,n=e.length;s{this.getDatasetMeta(e).controller.reset()},this)}reset(){this._resetElements(),this.notifyPlugins("reset")}update(t){let e=this.config;e.update();let s=this._options=e.createResolver(e.chartOptionScopes(),this.getContext()),n=this._animationsDisabled=!s.animation;if(this._updateScales(),this._checkEventBindings(),this._updateHiddenIndices(),this._plugins.invalidate(),this.notifyPlugins("beforeUpdate",{mode:t,cancelable:!0})===!1)return;let o=this.buildOrUpdateControllers();this.notifyPlugins("beforeElementsUpdate");let a=0;for(let c=0,h=this.data.datasets.length;c{c.reset()}),this._updateDatasets(t),this.notifyPlugins("afterUpdate",{mode:t}),this._layers.sort(to("z","_idx"));let{_active:r,_lastEvent:l}=this;l?this._eventHandler(l,!0):r.length&&this._updateHoverStyles(r,r,!0),this.render()}_updateScales(){E(this.scales,t=>{J.removeBox(this,t)}),this.ensureScalesHaveIDs(),this.buildOrUpdateScales()}_checkEventBindings(){let t=this.options,e=new Set(Object.keys(this._listeners)),s=new Set(t.events);(!Bi(e,s)||!!this._responsiveListeners!==t.responsive)&&(this.unbindEvents(),this.bindEvents())}_updateHiddenIndices(){let{_hiddenIndices:t}=this,e=this._getUniformDataChanges()||[];for(let{method:s,start:n,count:o}of e){let a=s==="_removeElements"?-o:o;$l(t,n,a)}}_getUniformDataChanges(){let t=this._dataChanges;if(!t||!t.length)return;this._dataChanges=[];let e=this.data.datasets.length,s=o=>new Set(t.filter(a=>a[0]===o).map((a,r)=>r+","+a.splice(1).join(","))),n=s(0);for(let o=1;oo.split(",")).map(o=>({method:o[1],start:+o[2],count:+o[3]}))}_updateLayout(t){if(this.notifyPlugins("beforeLayout",{cancelable:!0})===!1)return;J.update(this,this.width,this.height,t);let e=this.chartArea,s=e.width<=0||e.height<=0;this._layers=[],E(this.boxes,n=>{s&&n.position==="chartArea"||(n.configure&&n.configure(),this._layers.push(...n._layers()))},this),this._layers.forEach((n,o)=>{n._idx=o}),this.notifyPlugins("afterLayout")}_updateDatasets(t){if(this.notifyPlugins("beforeDatasetsUpdate",{mode:t,cancelable:!0})!==!1){for(let e=0,s=this.data.datasets.length;e=0;--e)this._drawDataset(t[e]);this.notifyPlugins("afterDatasetsDraw")}_drawDataset(t){let e=this.ctx,s={meta:t,index:t.index,cancelable:!0},n=hs(this,t);this.notifyPlugins("beforeDatasetDraw",s)!==!1&&(n&&Pe(e,n),t.controller.draw(),n&&De(e),s.cancelable=!1,this.notifyPlugins("afterDatasetDraw",s))}isPointInArea(t){return ht(t,this.chartArea,this._minPadding)}getElementsAtEventForMode(t,e,s,n){let o=Xr.modes[e];return typeof o=="function"?o(this,t,s,n):[]}getDatasetMeta(t){let e=this.data.datasets[t],s=this._metasets,n=s.filter(o=>o&&o._dataset===e).pop();return n||(n={type:null,data:[],dataset:null,controller:null,hidden:null,xAxisID:null,yAxisID:null,order:e&&e.order||0,index:t,_dataset:e,_parsed:[],_sorted:!1},s.push(n)),n}getContext(){return this.$context||(this.$context=yt(null,{chart:this,type:"chart"}))}getVisibleDatasetCount(){return this.getSortedVisibleDatasetMetas().length}isDatasetVisible(t){let e=this.data.datasets[t];if(!e)return!1;let s=this.getDatasetMeta(t);return typeof s.hidden=="boolean"?!s.hidden:!e.hidden}setDatasetVisibility(t,e){let s=this.getDatasetMeta(t);s.hidden=!e}toggleDataVisibility(t){this._hiddenIndices[t]=!this._hiddenIndices[t]}getDataVisibility(t){return!this._hiddenIndices[t]}_updateVisibility(t,e,s){let n=s?"show":"hide",o=this.getDatasetMeta(t),a=o.controller._resolveAnimations(void 0,n);ee(e)?(o.data[e].hidden=!s,this.update()):(this.setDatasetVisibility(t,s),a.update(o,{visible:s}),this.update(r=>r.datasetIndex===t?n:void 0))}hide(t,e){this._updateVisibility(t,e,!1)}show(t,e){this._updateVisibility(t,e,!0)}_destroyDatasetMeta(t){let e=this._metasets[t];e&&e.controller&&e.controller._destroy(),delete this._metasets[t]}_stop(){let t,e;for(this.stop(),vt.remove(this),t=0,e=this.data.datasets.length;t{e.addEventListener(this,o,a),t[o]=a},n=(o,a,r)=>{o.offsetX=a,o.offsetY=r,this._eventHandler(o)};E(this.options.events,o=>s(o,n))}bindResponsiveEvents(){this._responsiveListeners||(this._responsiveListeners={});let t=this._responsiveListeners,e=this.platform,s=(l,c)=>{e.addEventListener(this,l,c),t[l]=c},n=(l,c)=>{t[l]&&(e.removeEventListener(this,l,c),delete t[l])},o=(l,c)=>{this.canvas&&this.resize(l,c)},a,r=()=>{n("attach",r),this.attached=!0,this.resize(),s("resize",o),s("detach",a)};a=()=>{this.attached=!1,n("resize",o),this._stop(),this._resize(0,0),s("attach",r)},e.isAttached(this.canvas)?r():a()}unbindEvents(){E(this._listeners,(t,e)=>{this.platform.removeEventListener(this,e,t)}),this._listeners={},E(this._responsiveListeners,(t,e)=>{this.platform.removeEventListener(this,e,t)}),this._responsiveListeners=void 0}updateHoverStyle(t,e,s){let n=s?"set":"remove",o,a,r,l;for(e==="dataset"&&(o=this.getDatasetMeta(t[0].datasetIndex),o.controller["_"+n+"DatasetHoverStyle"]()),r=0,l=t.length;r{let r=this.getDatasetMeta(o);if(!r)throw new Error("No dataset found at index "+o);return{datasetIndex:o,element:r.data[a],index:a}});!we(s,e)&&(this._active=s,this._lastEvent=null,this._updateHoverStyles(s,e))}notifyPlugins(t,e,s){return this._plugins.notify(this,t,e,s)}isPluginEnabled(t){return this._plugins._cache.filter(e=>e.plugin.id===t).length===1}_updateHoverStyles(t,e,s){let n=this.options.hover,o=(l,c)=>l.filter(h=>!c.some(d=>h.datasetIndex===d.datasetIndex&&h.index===d.index)),a=o(e,t),r=s?t:o(t,e);a.length&&this.updateHoverStyle(a,n.mode,!1),r.length&&n.mode&&this.updateHoverStyle(r,n.mode,!0)}_eventHandler(t,e){let s={event:t,replay:e,cancelable:!0,inChartArea:this.isPointInArea(t)},n=a=>(a.options.events||this.options.events).includes(t.native.type);if(this.notifyPlugins("beforeEvent",s,n)===!1)return;let o=this._handleEvent(t,e,s.inChartArea);return s.cancelable=!1,this.notifyPlugins("afterEvent",s,n),(o||s.changed)&&this.render(),this}_handleEvent(t,e,s){let{_active:n=[],options:o}=this,a=e,r=this._getActiveElements(t,n,s,a),l=nn(t),c=Yl(t,this._lastEvent,s,l);s&&(this._lastEvent=null,F(o.onHover,[t,r,this],this),l&&F(o.onClick,[t,r,this],this));let h=!we(r,n);return(h||e)&&(this._active=r,this._updateHoverStyles(r,n,e)),this._lastEvent=c,h}_getActiveElements(t,e,s,n){if(t.type==="mouseout")return[];if(!s)return e;let o=this.options.hover;return this.getElementsAtEventForMode(t,o.mode,o,n)}};M(at,"defaults",V),M(at,"instances",yi),M(at,"overrides",Ot),M(at,"registry",gt),M(at,"version",Nl),M(at,"getChart",io);function so(){return E(at.instances,i=>i._plugins.invalidate())}function Ul(i,t,e){let{startAngle:s,x:n,y:o,outerRadius:a,innerRadius:r,options:l}=t,{borderWidth:c,borderJoinStyle:h}=l,d=Math.min(c/a,X(s-e));if(i.beginPath(),i.arc(n,o,a-c/2,s+d/2,e-d/2),r>0){let u=Math.min(c/r,X(s-e));i.arc(n,o,r+c/2,e-u/2,s+u/2,!0)}else{let u=Math.min(c/2,a*X(s-e));if(h==="round")i.arc(n,o,u,e-R/2,s+R/2,!0);else if(h==="bevel"){let f=2*u*u,g=-f*Math.cos(e+R/2)+n,p=-f*Math.sin(e+R/2)+o,m=f*Math.cos(s+R/2)+n,b=f*Math.sin(s+R/2)+o;i.lineTo(g,p),i.lineTo(m,b)}}i.closePath(),i.moveTo(0,0),i.rect(0,0,i.canvas.width,i.canvas.height),i.clip("evenodd")}function Xl(i,t,e){let{startAngle:s,pixelMargin:n,x:o,y:a,outerRadius:r,innerRadius:l}=t,c=n/r;i.beginPath(),i.arc(o,a,r,s-c,e+c),l>n?(c=n/l,i.arc(o,a,l,e+c,s-c,!0)):i.arc(o,a,n,e+H,s-H),i.closePath(),i.clip()}function Kl(i){return li(i,["outerStart","outerEnd","innerStart","innerEnd"])}function ql(i,t,e,s){let n=Kl(i.options.borderRadius),o=(e-t)/2,a=Math.min(o,s*t/2),r=l=>{let c=(e-Math.min(o,l))*s/2;return Y(l,0,Math.min(o,c))};return{outerStart:r(n.outerStart),outerEnd:r(n.outerEnd),innerStart:Y(n.innerStart,0,a),innerEnd:Y(n.innerEnd,0,a)}}function re(i,t,e,s){return{x:e+i*Math.cos(t),y:s+i*Math.sin(t)}}function wi(i,t,e,s,n,o){let{x:a,y:r,startAngle:l,pixelMargin:c,innerRadius:h}=t,d=Math.max(t.outerRadius+s+e-c,0),u=h>0?h+s+e+c:0,f=0,g=n-l;if(s){let I=h>0?h-s:0,W=d>0?d-s:0,j=(I+W)/2,rt=j!==0?g*j/(j+s):g;f=(g-rt)/2}let p=Math.max(.001,g*d-e/R)/d,m=(g-p)/2,b=l+m+f,x=n-m-f,{outerStart:v,outerEnd:y,innerStart:_,innerEnd:k}=ql(t,u,d,x-b),w=d-v,S=d-y,P=b+v/w,O=x-y/S,C=u+_,L=u+k,U=b+_/C,et=x-k/L;if(i.beginPath(),o){let I=(P+O)/2;if(i.arc(a,r,d,P,I),i.arc(a,r,d,I,O),y>0){let G=re(S,O,a,r);i.arc(G.x,G.y,y,O,x+H)}let W=re(L,x,a,r);if(i.lineTo(W.x,W.y),k>0){let G=re(L,et,a,r);i.arc(G.x,G.y,k,x+H,et+Math.PI)}let j=(x-k/u+(b+_/u))/2;if(i.arc(a,r,u,x-k/u,j,!0),i.arc(a,r,u,j,b+_/u,!0),_>0){let G=re(C,U,a,r);i.arc(G.x,G.y,_,U+Math.PI,b-H)}let rt=re(w,b,a,r);if(i.lineTo(rt.x,rt.y),v>0){let G=re(w,P,a,r);i.arc(G.x,G.y,v,b-H,P)}}else{i.moveTo(a,r);let I=Math.cos(P)*d+a,W=Math.sin(P)*d+r;i.lineTo(I,W);let j=Math.cos(O)*d+a,rt=Math.sin(O)*d+r;i.lineTo(j,rt)}i.closePath()}function Gl(i,t,e,s,n){let{fullCircles:o,startAngle:a,circumference:r}=t,l=t.endAngle;if(o){wi(i,t,e,s,l,n);for(let c=0;c=R&&f===0&&h!=="miter"&&Ul(i,t,p),o||(wi(i,t,e,s,p,n),i.stroke())}var jt=class extends st{constructor(e){super();M(this,"circumference");M(this,"endAngle");M(this,"fullCircles");M(this,"innerRadius");M(this,"outerRadius");M(this,"pixelMargin");M(this,"startAngle");this.options=void 0,this.circumference=void 0,this.startAngle=void 0,this.endAngle=void 0,this.innerRadius=void 0,this.outerRadius=void 0,this.pixelMargin=0,this.fullCircles=0,e&&Object.assign(this,e)}inRange(e,s,n){let o=this.getProps(["x","y"],n),{angle:a,distance:r}=Hi(o,{x:e,y:s}),{startAngle:l,endAngle:c,innerRadius:h,outerRadius:d,circumference:u}=this.getProps(["startAngle","endAngle","innerRadius","outerRadius","circumference"],n),f=(this.options.spacing+this.options.borderWidth)/2,g=D(u,c-l),p=se(a,l,c)&&l!==c,m=g>=B||p,b=ut(r,h+f,d+f);return m&&b}getCenterPoint(e){let{x:s,y:n,startAngle:o,endAngle:a,innerRadius:r,outerRadius:l}=this.getProps(["x","y","startAngle","endAngle","innerRadius","outerRadius"],e),{offset:c,spacing:h}=this.options,d=(o+a)/2,u=(r+l+h+c)/2;return{x:s+Math.cos(d)*u,y:n+Math.sin(d)*u}}tooltipPosition(e){return this.getCenterPoint(e)}draw(e){let{options:s,circumference:n}=this,o=(s.offset||0)/4,a=(s.spacing||0)/2,r=s.circular;if(this.pixelMargin=s.borderAlign==="inner"?.33:0,this.fullCircles=n>B?Math.floor(n/B):0,n===0||this.innerRadius<0||this.outerRadius<0)return;e.save();let l=(this.startAngle+this.endAngle)/2;e.translate(Math.cos(l)*o,Math.sin(l)*o);let c=1-Math.sin(Math.min(R,n||0)),h=o*c;e.fillStyle=s.backgroundColor,e.strokeStyle=s.borderColor,Gl(e,this,h,a,r),Jl(e,this,h,a,r),e.restore()}};M(jt,"id","arc"),M(jt,"defaults",{borderAlign:"center",borderColor:"#fff",borderDash:[],borderDashOffset:0,borderJoinStyle:void 0,borderRadius:0,borderWidth:2,offset:0,spacing:0,angle:void 0,circular:!0,selfJoin:!1}),M(jt,"defaultRoutes",{backgroundColor:"backgroundColor"}),M(jt,"descriptors",{_scriptable:!0,_indexable:e=>e!=="borderDash"});function Wo(i,t,e=t){i.lineCap=D(e.borderCapStyle,t.borderCapStyle),i.setLineDash(D(e.borderDash,t.borderDash)),i.lineDashOffset=D(e.borderDashOffset,t.borderDashOffset),i.lineJoin=D(e.borderJoinStyle,t.borderJoinStyle),i.lineWidth=D(e.borderWidth,t.borderWidth),i.strokeStyle=D(e.borderColor,t.borderColor)}function Zl(i,t,e){i.lineTo(e.x,e.y)}function Ql(i){return i.stepped?mn:i.tension||i.cubicInterpolationMode==="monotone"?bn:Zl}function No(i,t,e={}){let s=i.length,{start:n=0,end:o=s-1}=e,{start:a,end:r}=t,l=Math.max(n,a),c=Math.min(o,r),h=nr&&o>r;return{count:s,start:l,loop:t.loop,ilen:c(a+(c?r-y:y))%o,v=()=>{p!==m&&(i.lineTo(h,m),i.lineTo(h,p),i.lineTo(h,b))};for(l&&(f=n[x(0)],i.moveTo(f.x,f.y)),u=0;u<=r;++u){if(f=n[x(u)],f.skip)continue;let y=f.x,_=f.y,k=y|0;k===g?(_m&&(m=_),h=(d*h+y)/++d):(v(),i.lineTo(y,_),g=k,d=0,p=m=_),b=_}v()}function Ls(i){let t=i.options,e=t.borderDash&&t.borderDash.length;return!i._decimated&&!i._loop&&!t.tension&&t.cubicInterpolationMode!=="monotone"&&!t.stepped&&!e?ec:tc}function ic(i){return i.stepped?Pn:i.tension||i.cubicInterpolationMode==="monotone"?Dn:Pt}function sc(i,t,e,s){let n=t._path;n||(n=t._path=new Path2D,t.path(n,e,s)&&n.closePath()),Wo(i,t.options),i.stroke(n)}function nc(i,t,e,s){let{segments:n,options:o}=t,a=Ls(t);for(let r of n)Wo(i,o,r.style),i.beginPath(),a(i,t,r,{start:e,end:e+s-1})&&i.closePath(),i.stroke()}var oc=typeof Path2D=="function";function ac(i,t,e,s){oc&&!t.options.segment?sc(i,t,e,s):nc(i,t,e,s)}var pt=class extends st{constructor(t){super(),this.animated=!0,this.options=void 0,this._chart=void 0,this._loop=void 0,this._fullLoop=void 0,this._path=void 0,this._points=void 0,this._segments=void 0,this._decimated=!1,this._pointsUpdated=!1,this._datasetIndex=void 0,t&&Object.assign(this,t)}updateControlPoints(t,e){let s=this.options;if((s.tension||s.cubicInterpolationMode==="monotone")&&!s.stepped&&!this._pointsUpdated){let n=s.spanGaps?this._loop:this._fullLoop;kn(this._points,s,t,n,e),this._pointsUpdated=!0}}set points(t){this._points=t,delete this._segments,delete this._path,this._pointsUpdated=!1}get points(){return this._points}get segments(){return this._segments||(this._segments=Cn(this,this.options.segment))}first(){let t=this.segments,e=this.points;return t.length&&e[t[0].start]}last(){let t=this.segments,e=this.points,s=t.length;return s&&e[t[s-1].end]}interpolate(t,e){let s=this.options,n=t[e],o=this.points,a=cs(this,{property:e,start:n,end:n});if(!a.length)return;let r=[],l=ic(s),c,h;for(c=0,h=a.length;ct!=="borderDash"&&t!=="fill"});function no(i,t,e,s){let n=i.options,{[e]:o}=i.getProps([e],s);return Math.abs(t-o)i.replace("rgb(","rgba(").replace(")",", 0.5)"));function jo(i){return Rs[i%Rs.length]}function $o(i){return oo[i%oo.length]}function fc(i,t){return i.borderColor=jo(t),i.backgroundColor=$o(t),++t}function gc(i,t){return i.backgroundColor=i.data.map(()=>jo(t++)),t}function pc(i,t){return i.backgroundColor=i.data.map(()=>$o(t++)),t}function mc(i){let t=0;return(e,s)=>{let n=i.getDatasetMeta(s).controller;n instanceof kt?t=gc(e,t):n instanceof Yt?t=pc(e,t):n&&(t=fc(e,t))}}function ao(i){let t;for(t in i)if(i[t].borderColor||i[t].backgroundColor)return!0;return!1}function bc(i){return i&&(i.borderColor||i.backgroundColor)}function xc(){return V.borderColor!=="rgba(0,0,0,0.1)"||V.backgroundColor!=="rgba(0,0,0,0.1)"}var _c={id:"colors",defaults:{enabled:!0,forceOverride:!1},beforeLayout(i,t,e){if(!e.enabled)return;let{data:{datasets:s},options:n}=i.config,{elements:o}=n,a=ao(s)||bc(n)||o&&ao(o)||xc();if(!e.forceOverride&&a)return;let r=mc(i);s.forEach(r)}};function yc(i,t,e,s,n){let o=n.samples||s;if(o>=e)return i.slice(t,t+e);let a=[],r=(e-2)/(o-2),l=0,c=t+e-1,h=t,d,u,f,g,p;for(a[l++]=i[h],d=0;df&&(f=g,u=i[x],p=x);a[l++]=u,h=p}return a[l++]=i[c],a}function vc(i,t,e,s){let n=0,o=0,a,r,l,c,h,d,u,f,g,p,m=[],b=t+e-1,x=i[t].x,y=i[b].x-x;for(a=t;ap&&(p=c,u=a),n=(o*n+r.x)/++o;else{let k=a-1;if(!A(d)&&!A(u)){let w=Math.min(d,u),S=Math.max(d,u);w!==f&&w!==k&&m.push({...i[w],x:n}),S!==f&&S!==k&&m.push({...i[S],x:n})}a>0&&k!==f&&m.push(i[k]),m.push(r),h=_,o=0,g=p=c,d=u=f=a}}return m}function Yo(i){if(i._decimated){let t=i._data;delete i._decimated,delete i._data,Object.defineProperty(i,"data",{configurable:!0,enumerable:!0,writable:!0,value:t})}}function ro(i){i.data.datasets.forEach(t=>{Yo(t)})}function Mc(i,t){let e=t.length,s=0,n,{iScale:o}=i,{min:a,max:r,minDefined:l,maxDefined:c}=o.getUserBounds();return l&&(s=Y(ct(t,o.axis,a).lo,0,e-1)),c?n=Y(ct(t,o.axis,r).hi+1,s,e)-s:n=e-s,{start:s,count:n}}var kc={id:"decimation",defaults:{algorithm:"min-max",enabled:!1},beforeElementsUpdate:(i,t,e)=>{if(!e.enabled){ro(i);return}let s=i.width;i.data.datasets.forEach((n,o)=>{let{_data:a,indexAxis:r}=n,l=i.getDatasetMeta(o),c=a||n.data;if(ae([r,i.options.indexAxis])==="y"||!l.controller.supportsDecimation)return;let h=i.scales[l.xAxisID];if(h.type!=="linear"&&h.type!=="time"||i.options.parsing)return;let{start:d,count:u}=Mc(l,c),f=e.threshold||4*s;if(u<=f){Yo(n);return}A(a)&&(n._data=c,delete n.data,Object.defineProperty(n,"data",{configurable:!0,enumerable:!0,get:function(){return this._decimated},set:function(p){this._data=p}}));let g;switch(e.algorithm){case"lttb":g=yc(c,d,u,s,e);break;case"min-max":g=vc(c,d,u,s);break;default:throw new Error(`Unsupported decimation algorithm '${e.algorithm}'`)}n._decimated=g})},destroy(i){ro(i)}};function wc(i,t,e){let s=i.segments,n=i.points,o=t.points,a=[];for(let r of s){let{start:l,end:c}=r;c=Di(l,c,n);let h=Es(e,n[l],n[c],r.loop);if(!t.segments){a.push({source:r,target:h,start:n[l],end:n[c]});continue}let d=cs(t,h);for(let u of d){let f=Es(e,o[u.start],o[u.end],u.loop),g=ls(r,n,f);for(let p of g)a.push({source:p,target:u,start:{[e]:lo(h,f,"start",Math.max)},end:{[e]:lo(h,f,"end",Math.min)}})}}return a}function Es(i,t,e,s){if(s)return;let n=t[i],o=e[i];return i==="angle"&&(n=X(n),o=X(o)),{property:i,start:n,end:o}}function Sc(i,t){let{x:e=null,y:s=null}=i||{},n=t.points,o=[];return t.segments.forEach(({start:a,end:r})=>{r=Di(a,r,n);let l=n[a],c=n[r];s!==null?(o.push({x:l.x,y:s}),o.push({x:c.x,y:s})):e!==null&&(o.push({x:e,y:l.y}),o.push({x:e,y:c.y}))}),o}function Di(i,t,e){for(;t>i;t--){let s=e[t];if(!isNaN(s.x)&&!isNaN(s.y))break}return t}function lo(i,t,e,s){return i&&t?s(i[e],t[e]):i?i[e]:t?t[e]:0}function Uo(i,t){let e=[],s=!1;return z(i)?(s=!0,e=i):e=Sc(i,t),e.length?new pt({points:e,options:{tension:0},_loop:s,_fullLoop:s}):null}function co(i){return i&&i.fill!==!1}function Pc(i,t,e){let n=i[t].fill,o=[t],a;if(!e)return n;for(;n!==!1&&o.indexOf(n)===-1;){if(!N(n))return n;if(a=i[n],!a)return!1;if(a.visible)return n;o.push(n),n=a.fill}return!1}function Dc(i,t,e){let s=Tc(i);if(T(s))return isNaN(s.value)?!1:s;let n=parseFloat(s);return N(n)&&Math.floor(n)===n?Oc(s[0],t,n,e):["origin","start","end","stack","shape"].indexOf(s)>=0&&s}function Oc(i,t,e,s){return(i==="-"||i==="+")&&(e=t+e),e===t||e<0||e>=s?!1:e}function Cc(i,t){let e=null;return i==="start"?e=t.bottom:i==="end"?e=t.top:T(i)?e=t.getPixelForValue(i.value):t.getBasePixel&&(e=t.getBasePixel()),e}function Ac(i,t,e){let s;return i==="start"?s=e:i==="end"?s=t.options.reverse?t.min:t.max:T(i)?s=i.value:s=t.getBaseValue(),s}function Tc(i){let t=i.options,e=t.fill,s=D(e&&e.target,e);return s===void 0&&(s=!!t.backgroundColor),s===!1||s===null?!1:s===!0?"origin":s}function Lc(i){let{scale:t,index:e,line:s}=i,n=[],o=s.segments,a=s.points,r=Rc(t,e);r.push(Uo({x:null,y:t.bottom},s));for(let l=0;l=0;--a){let r=n[a].$filler;r&&(r.line.updateControlPoints(o,r.axis),s&&r.fill&&ys(i.ctx,r,o))}},beforeDatasetsDraw(i,t,e){if(e.drawTime!=="beforeDatasetsDraw")return;let s=i.getSortedVisibleDatasetMetas();for(let n=s.length-1;n>=0;--n){let o=s[n].$filler;co(o)&&ys(i.ctx,o,i.chartArea)}},beforeDatasetDraw(i,t,e){let s=t.meta.$filler;!co(s)||e.drawTime!=="beforeDatasetDraw"||ys(i.ctx,s,i.chartArea)},defaults:{propagate:!0,drawTime:"beforeDatasetDraw"}},go=(i,t)=>{let{boxHeight:e=t,boxWidth:s=t}=i;return i.usePointStyle&&(e=Math.min(e,t),s=i.pointStyleWidth||Math.min(s,t)),{boxWidth:s,boxHeight:e,itemHeight:Math.max(t,e)}},$c=(i,t)=>i!==null&&t!==null&&i.datasetIndex===t.datasetIndex&&i.index===t.index,Pi=class extends st{constructor(t){super(),this._added=!1,this.legendHitBoxes=[],this._hoveredItem=null,this.doughnutMode=!1,this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this.legendItems=void 0,this.columnSizes=void 0,this.lineWidths=void 0,this.maxHeight=void 0,this.maxWidth=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.height=void 0,this.width=void 0,this._margins=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e,s){this.maxWidth=t,this.maxHeight=e,this._margins=s,this.setDimensions(),this.buildLabels(),this.fit()}setDimensions(){this.isHorizontal()?(this.width=this.maxWidth,this.left=this._margins.left,this.right=this.width):(this.height=this.maxHeight,this.top=this._margins.top,this.bottom=this.height)}buildLabels(){let t=this.options.labels||{},e=F(t.generateLabels,[this.chart],this)||[];t.filter&&(e=e.filter(s=>t.filter(s,this.chart.data))),t.sort&&(e=e.sort((s,n)=>t.sort(s,n,this.chart.data))),this.options.reverse&&e.reverse(),this.legendItems=e}fit(){let{options:t,ctx:e}=this;if(!t.display){this.width=this.height=0;return}let s=t.labels,n=$(s.font),o=n.size,a=this._computeTitleHeight(),{boxWidth:r,itemHeight:l}=go(s,o),c,h;e.font=n.string,this.isHorizontal()?(c=this.maxWidth,h=this._fitRows(a,o,r,l)+10):(h=this.maxHeight,c=this._fitCols(a,n,r,l)+10),this.width=Math.min(c,t.maxWidth||this.maxWidth),this.height=Math.min(h,t.maxHeight||this.maxHeight)}_fitRows(t,e,s,n){let{ctx:o,maxWidth:a,options:{labels:{padding:r}}}=this,l=this.legendHitBoxes=[],c=this.lineWidths=[0],h=n+r,d=t;o.textAlign="left",o.textBaseline="middle";let u=-1,f=-h;return this.legendItems.forEach((g,p)=>{let m=s+e/2+o.measureText(g.text).width;(p===0||c[c.length-1]+m+2*r>a)&&(d+=h,c[c.length-(p>0?0:1)]=0,f+=h,u++),l[p]={left:0,top:f,row:u,width:m,height:n},c[c.length-1]+=m+r}),d}_fitCols(t,e,s,n){let{ctx:o,maxHeight:a,options:{labels:{padding:r}}}=this,l=this.legendHitBoxes=[],c=this.columnSizes=[],h=a-t,d=r,u=0,f=0,g=0,p=0;return this.legendItems.forEach((m,b)=>{let{itemWidth:x,itemHeight:v}=Yc(s,e,o,m,n);b>0&&f+v+2*r>h&&(d+=u+r,c.push({width:u,height:f}),g+=u+r,p++,u=f=0),l[b]={left:g,top:f,col:p,width:x,height:v},u=Math.max(u,x),f+=v+r}),d+=u,c.push({width:u,height:f}),d}adjustHitBoxes(){if(!this.options.display)return;let t=this._computeTitleHeight(),{legendHitBoxes:e,options:{align:s,labels:{padding:n},rtl:o}}=this,a=Wt(o,this.left,this.width);if(this.isHorizontal()){let r=0,l=K(s,this.left+n,this.right-this.lineWidths[r]);for(let c of e)r!==c.row&&(r=c.row,l=K(s,this.left+n,this.right-this.lineWidths[r])),c.top+=this.top+t+n,c.left=a.leftForLtr(a.x(l),c.width),l+=c.width+n}else{let r=0,l=K(s,this.top+t+n,this.bottom-this.columnSizes[r].height);for(let c of e)c.col!==r&&(r=c.col,l=K(s,this.top+t+n,this.bottom-this.columnSizes[r].height)),c.top=l,c.left+=this.left+n,c.left=a.leftForLtr(a.x(c.left),c.width),l+=c.height+n}}isHorizontal(){return this.options.position==="top"||this.options.position==="bottom"}draw(){if(this.options.display){let t=this.ctx;Pe(t,this),this._draw(),De(t)}}_draw(){let{options:t,columnSizes:e,lineWidths:s,ctx:n}=this,{align:o,labels:a}=t,r=V.color,l=Wt(t.rtl,this.left,this.width),c=$(a.font),{padding:h}=a,d=c.size,u=d/2,f;this.drawTitle(),n.textAlign=l.textAlign("left"),n.textBaseline="middle",n.lineWidth=.5,n.font=c.string;let{boxWidth:g,boxHeight:p,itemHeight:m}=go(a,d),b=function(k,w,S){if(isNaN(g)||g<=0||isNaN(p)||p<0)return;n.save();let P=D(S.lineWidth,1);if(n.fillStyle=D(S.fillStyle,r),n.lineCap=D(S.lineCap,"butt"),n.lineDashOffset=D(S.lineDashOffset,0),n.lineJoin=D(S.lineJoin,"miter"),n.lineWidth=P,n.strokeStyle=D(S.strokeStyle,r),n.setLineDash(D(S.lineDash,[])),a.usePointStyle){let O={radius:p*Math.SQRT2/2,pointStyle:S.pointStyle,rotation:S.rotation,borderWidth:P},C=l.xPlus(k,g/2),L=w+u;Zi(n,O,C,L,a.pointStyleWidth&&g)}else{let O=w+Math.max((d-p)/2,0),C=l.leftForLtr(k,g),L=Tt(S.borderRadius);n.beginPath(),Object.values(L).some(U=>U!==0)?oe(n,{x:C,y:O,w:g,h:p,radius:L}):n.rect(C,O,g,p),n.fill(),P!==0&&n.stroke()}n.restore()},x=function(k,w,S){At(n,S.text,k,w+m/2,c,{strikethrough:S.hidden,textAlign:l.textAlign(S.textAlign)})},v=this.isHorizontal(),y=this._computeTitleHeight();v?f={x:K(o,this.left+h,this.right-s[0]),y:this.top+h+y,line:0}:f={x:this.left+h,y:K(o,this.top+y+h,this.bottom-e[0].height),line:0},as(this.ctx,t.textDirection);let _=m+h;this.legendItems.forEach((k,w)=>{n.strokeStyle=k.fontColor,n.fillStyle=k.fontColor;let S=n.measureText(k.text).width,P=l.textAlign(k.textAlign||(k.textAlign=a.textAlign)),O=g+u+S,C=f.x,L=f.y;l.setWidth(this.width),v?w>0&&C+O+h>this.right&&(L=f.y+=_,f.line++,C=f.x=K(o,this.left+h,this.right-s[f.line])):w>0&&L+_>this.bottom&&(C=f.x=C+e[f.line].width+h,f.line++,L=f.y=K(o,this.top+y+h,this.bottom-e[f.line].height));let U=l.x(C);if(b(U,L,k),C=fn(P,C+g+u,v?C+O:this.right,t.rtl),x(l.x(C),L,k),v)f.x+=O+h;else if(typeof k.text!="string"){let et=c.lineHeight;f.y+=Xo(k,et)+h}else f.y+=_}),rs(this.ctx,t.textDirection)}drawTitle(){let t=this.options,e=t.title,s=$(e.font),n=q(e.padding);if(!e.display)return;let o=Wt(t.rtl,this.left,this.width),a=this.ctx,r=e.position,l=s.size/2,c=n.top+l,h,d=this.left,u=this.width;if(this.isHorizontal())u=Math.max(...this.lineWidths),h=this.top+c,d=K(t.align,d,this.right-u);else{let g=this.columnSizes.reduce((p,m)=>Math.max(p,m.height),0);h=c+K(t.align,this.top,this.bottom-g-t.labels.padding-this._computeTitleHeight())}let f=K(r,d,d+u);a.textAlign=o.textAlign(oi(r)),a.textBaseline="middle",a.strokeStyle=e.color,a.fillStyle=e.color,a.font=s.string,At(a,e.text,f,h,s)}_computeTitleHeight(){let t=this.options.title,e=$(t.font),s=q(t.padding);return t.display?e.lineHeight+s.height:0}_getLegendItemAt(t,e){let s,n,o;if(ut(t,this.left,this.right)&&ut(e,this.top,this.bottom)){for(o=this.legendHitBoxes,s=0;so.length>a.length?o:a)),t+e.size/2+s.measureText(n).width}function Xc(i,t,e){let s=i;return typeof t.text!="string"&&(s=Xo(t,e)),s}function Xo(i,t){let e=i.text?i.text.length:0;return t*e}function Kc(i,t){return!!((i==="mousemove"||i==="mouseout")&&(t.onHover||t.onLeave)||t.onClick&&(i==="click"||i==="mouseup"))}var qc={id:"legend",_element:Pi,start(i,t,e){let s=i.legend=new Pi({ctx:i.ctx,options:e,chart:i});J.configure(i,s,e),J.addBox(i,s)},stop(i){J.removeBox(i,i.legend),delete i.legend},beforeUpdate(i,t,e){let s=i.legend;J.configure(i,s,e),s.options=e},afterUpdate(i){let t=i.legend;t.buildLabels(),t.adjustHitBoxes()},afterEvent(i,t){t.replay||i.legend.handleEvent(t.event)},defaults:{display:!0,position:"top",align:"center",fullSize:!0,reverse:!1,weight:1e3,onClick(i,t,e){let s=t.datasetIndex,n=e.chart;n.isDatasetVisible(s)?(n.hide(s),t.hidden=!0):(n.show(s),t.hidden=!1)},onHover:null,onLeave:null,labels:{color:i=>i.chart.options.color,boxWidth:40,padding:10,generateLabels(i){let t=i.data.datasets,{labels:{usePointStyle:e,pointStyle:s,textAlign:n,color:o,useBorderRadius:a,borderRadius:r}}=i.legend.options;return i._getSortedDatasetMetas().map(l=>{let c=l.controller.getStyle(e?0:void 0),h=q(c.borderWidth);return{text:t[l.index].label,fillStyle:c.backgroundColor,fontColor:o,hidden:!l.visible,lineCap:c.borderCapStyle,lineDash:c.borderDash,lineDashOffset:c.borderDashOffset,lineJoin:c.borderJoinStyle,lineWidth:(h.width+h.height)/4,strokeStyle:c.borderColor,pointStyle:s||c.pointStyle,rotation:c.rotation,textAlign:n||c.textAlign,borderRadius:a&&(r||c.borderRadius),datasetIndex:l.index}},this)}},title:{color:i=>i.chart.options.color,display:!1,position:"center",text:""}},descriptors:{_scriptable:i=>!i.startsWith("on"),labels:{_scriptable:i=>!["generateLabels","filter","sort"].includes(i)}}},He=class extends st{constructor(t){super(),this.chart=t.chart,this.options=t.options,this.ctx=t.ctx,this._padding=void 0,this.top=void 0,this.bottom=void 0,this.left=void 0,this.right=void 0,this.width=void 0,this.height=void 0,this.position=void 0,this.weight=void 0,this.fullSize=void 0}update(t,e){let s=this.options;if(this.left=0,this.top=0,!s.display){this.width=this.height=this.right=this.bottom=0;return}this.width=this.right=t,this.height=this.bottom=e;let n=z(s.text)?s.text.length:1;this._padding=q(s.padding);let o=n*$(s.font).lineHeight+this._padding.height;this.isHorizontal()?this.height=o:this.width=o}isHorizontal(){let t=this.options.position;return t==="top"||t==="bottom"}_drawArgs(t){let{top:e,left:s,bottom:n,right:o,options:a}=this,r=a.align,l=0,c,h,d;return this.isHorizontal()?(h=K(r,s,o),d=e+t,c=o-s):(a.position==="left"?(h=s+t,d=K(r,n,e),l=R*-.5):(h=o-t,d=K(r,e,n),l=R*.5),c=n-e),{titleX:h,titleY:d,maxWidth:c,rotation:l}}draw(){let t=this.ctx,e=this.options;if(!e.display)return;let s=$(e.font),o=s.lineHeight/2+this._padding.top,{titleX:a,titleY:r,maxWidth:l,rotation:c}=this._drawArgs(o);At(t,e.text,0,0,s,{color:e.color,maxWidth:l,rotation:c,textAlign:oi(e.align),textBaseline:"middle",translation:[a,r]})}};function Gc(i,t){let e=new He({ctx:i.ctx,options:t,chart:i});J.configure(i,e,t),J.addBox(i,e),i.titleBlock=e}var Jc={id:"title",_element:He,start(i,t,e){Gc(i,e)},stop(i){let t=i.titleBlock;J.removeBox(i,t),delete i.titleBlock},beforeUpdate(i,t,e){let s=i.titleBlock;J.configure(i,s,e),s.options=e},defaults:{align:"center",display:!1,font:{weight:"bold"},fullSize:!0,padding:10,position:"top",text:"",weight:2e3},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}},mi=new WeakMap,Zc={id:"subtitle",start(i,t,e){let s=new He({ctx:i.ctx,options:e,chart:i});J.configure(i,s,e),J.addBox(i,s),mi.set(i,s)},stop(i){J.removeBox(i,mi.get(i)),mi.delete(i)},beforeUpdate(i,t,e){let s=mi.get(i);J.configure(i,s,e),s.options=e},defaults:{align:"center",display:!1,font:{weight:"normal"},fullSize:!0,padding:0,position:"top",text:"",weight:1500},defaultRoutes:{color:"color"},descriptors:{_scriptable:!0,_indexable:!1}},Ie={average(i){if(!i.length)return!1;let t,e,s=new Set,n=0,o=0;for(t=0,e=i.length;tr+l)/s.size,y:n/o}},nearest(i,t){if(!i.length)return!1;let e=t.x,s=t.y,n=Number.POSITIVE_INFINITY,o,a,r;for(o=0,a=i.length;o-1?i.split(` +`):i}function Qc(i,t){let{element:e,datasetIndex:s,index:n}=t,o=i.getDatasetMeta(s).controller,{label:a,value:r}=o.getLabelAndValue(n);return{chart:i,label:a,parsed:o.getParsed(n),raw:i.data.datasets[s].data[n],formattedValue:r,dataset:o.getDataset(),dataIndex:n,datasetIndex:s,element:e}}function po(i,t){let e=i.chart.ctx,{body:s,footer:n,title:o}=i,{boxWidth:a,boxHeight:r}=t,l=$(t.bodyFont),c=$(t.titleFont),h=$(t.footerFont),d=o.length,u=n.length,f=s.length,g=q(t.padding),p=g.height,m=0,b=s.reduce((y,_)=>y+_.before.length+_.lines.length+_.after.length,0);if(b+=i.beforeBody.length+i.afterBody.length,d&&(p+=d*c.lineHeight+(d-1)*t.titleSpacing+t.titleMarginBottom),b){let y=t.displayColors?Math.max(r,l.lineHeight):l.lineHeight;p+=f*y+(b-f)*l.lineHeight+(b-1)*t.bodySpacing}u&&(p+=t.footerMarginTop+u*h.lineHeight+(u-1)*t.footerSpacing);let x=0,v=function(y){m=Math.max(m,e.measureText(y).width+x)};return e.save(),e.font=c.string,E(i.title,v),e.font=l.string,E(i.beforeBody.concat(i.afterBody),v),x=t.displayColors?a+2+t.boxPadding:0,E(s,y=>{E(y.before,v),E(y.lines,v),E(y.after,v)}),x=0,e.font=h.string,E(i.footer,v),e.restore(),m+=g.width,{width:m,height:p}}function th(i,t){let{y:e,height:s}=t;return ei.height-s/2?"bottom":"center"}function eh(i,t,e,s){let{x:n,width:o}=s,a=e.caretSize+e.caretPadding;if(i==="left"&&n+o+a>t.width||i==="right"&&n-o-a<0)return!0}function ih(i,t,e,s){let{x:n,width:o}=e,{width:a,chartArea:{left:r,right:l}}=i,c="center";return s==="center"?c=n<=(r+l)/2?"left":"right":n<=o/2?c="left":n>=a-o/2&&(c="right"),eh(c,i,t,e)&&(c="center"),c}function mo(i,t,e){let s=e.yAlign||t.yAlign||th(i,e);return{xAlign:e.xAlign||t.xAlign||ih(i,t,e,s),yAlign:s}}function sh(i,t){let{x:e,width:s}=i;return t==="right"?e-=s:t==="center"&&(e-=s/2),e}function nh(i,t,e){let{y:s,height:n}=i;return t==="top"?s+=e:t==="bottom"?s-=n+e:s-=n/2,s}function bo(i,t,e,s){let{caretSize:n,caretPadding:o,cornerRadius:a}=i,{xAlign:r,yAlign:l}=e,c=n+o,{topLeft:h,topRight:d,bottomLeft:u,bottomRight:f}=Tt(a),g=sh(t,r),p=nh(t,l,c);return l==="center"?r==="left"?g+=c:r==="right"&&(g-=c):r==="left"?g-=Math.max(h,u)+n:r==="right"&&(g+=Math.max(d,f)+n),{x:Y(g,0,s.width-t.width),y:Y(p,0,s.height-t.height)}}function bi(i,t,e){let s=q(e.padding);return t==="center"?i.x+i.width/2:t==="right"?i.x+i.width-s.right:i.x+s.left}function xo(i){return ft([],Mt(i))}function oh(i,t,e){return yt(i,{tooltip:t,tooltipItems:e,type:"tooltip"})}function _o(i,t){let e=t&&t.dataset&&t.dataset.tooltip&&t.dataset.tooltip.callbacks;return e?i.override(e):i}var Ko={beforeTitle:dt,title(i){if(i.length>0){let t=i[0],e=t.chart.data.labels,s=e?e.length:0;if(this&&this.options&&this.options.mode==="dataset")return t.dataset.label||"";if(t.label)return t.label;if(s>0&&t.dataIndex"u"?Ko[t].call(e,s):n}var We=class extends st{constructor(t){super(),this.opacity=0,this._active=[],this._eventPosition=void 0,this._size=void 0,this._cachedAnimations=void 0,this._tooltipItems=[],this.$animations=void 0,this.$context=void 0,this.chart=t.chart,this.options=t.options,this.dataPoints=void 0,this.title=void 0,this.beforeBody=void 0,this.body=void 0,this.afterBody=void 0,this.footer=void 0,this.xAlign=void 0,this.yAlign=void 0,this.x=void 0,this.y=void 0,this.height=void 0,this.width=void 0,this.caretX=void 0,this.caretY=void 0,this.labelColors=void 0,this.labelPointStyles=void 0,this.labelTextColors=void 0}initialize(t){this.options=t,this._cachedAnimations=void 0,this.$context=void 0}_resolveAnimations(){let t=this._cachedAnimations;if(t)return t;let e=this.chart,s=this.options.setContext(this.getContext()),n=s.enabled&&e.options.animation&&s.animations,o=new vi(this.chart,n);return n._cacheable&&(this._cachedAnimations=Object.freeze(o)),o}getContext(){return this.$context||(this.$context=oh(this.chart.getContext(),this,this._tooltipItems))}getTitle(t,e){let{callbacks:s}=e,n=Q(s,"beforeTitle",this,t),o=Q(s,"title",this,t),a=Q(s,"afterTitle",this,t),r=[];return r=ft(r,Mt(n)),r=ft(r,Mt(o)),r=ft(r,Mt(a)),r}getBeforeBody(t,e){return xo(Q(e.callbacks,"beforeBody",this,t))}getBody(t,e){let{callbacks:s}=e,n=[];return E(t,o=>{let a={before:[],lines:[],after:[]},r=_o(s,o);ft(a.before,Mt(Q(r,"beforeLabel",this,o))),ft(a.lines,Q(r,"label",this,o)),ft(a.after,Mt(Q(r,"afterLabel",this,o))),n.push(a)}),n}getAfterBody(t,e){return xo(Q(e.callbacks,"afterBody",this,t))}getFooter(t,e){let{callbacks:s}=e,n=Q(s,"beforeFooter",this,t),o=Q(s,"footer",this,t),a=Q(s,"afterFooter",this,t),r=[];return r=ft(r,Mt(n)),r=ft(r,Mt(o)),r=ft(r,Mt(a)),r}_createItems(t){let e=this._active,s=this.chart.data,n=[],o=[],a=[],r=[],l,c;for(l=0,c=e.length;lt.filter(h,d,u,s))),t.itemSort&&(r=r.sort((h,d)=>t.itemSort(h,d,s))),E(r,h=>{let d=_o(t.callbacks,h);n.push(Q(d,"labelColor",this,h)),o.push(Q(d,"labelPointStyle",this,h)),a.push(Q(d,"labelTextColor",this,h))}),this.labelColors=n,this.labelPointStyles=o,this.labelTextColors=a,this.dataPoints=r,r}update(t,e){let s=this.options.setContext(this.getContext()),n=this._active,o,a=[];if(!n.length)this.opacity!==0&&(o={opacity:0});else{let r=Ie[s.position].call(this,n,this._eventPosition);a=this._createItems(s),this.title=this.getTitle(a,s),this.beforeBody=this.getBeforeBody(a,s),this.body=this.getBody(a,s),this.afterBody=this.getAfterBody(a,s),this.footer=this.getFooter(a,s);let l=this._size=po(this,s),c=Object.assign({},r,l),h=mo(this.chart,s,c),d=bo(s,c,h,this.chart);this.xAlign=h.xAlign,this.yAlign=h.yAlign,o={opacity:1,x:d.x,y:d.y,width:l.width,height:l.height,caretX:r.x,caretY:r.y}}this._tooltipItems=a,this.$context=void 0,o&&this._resolveAnimations().update(this,o),t&&s.external&&s.external.call(this,{chart:this.chart,tooltip:this,replay:e})}drawCaret(t,e,s,n){let o=this.getCaretPosition(t,s,n);e.lineTo(o.x1,o.y1),e.lineTo(o.x2,o.y2),e.lineTo(o.x3,o.y3)}getCaretPosition(t,e,s){let{xAlign:n,yAlign:o}=this,{caretSize:a,cornerRadius:r}=s,{topLeft:l,topRight:c,bottomLeft:h,bottomRight:d}=Tt(r),{x:u,y:f}=t,{width:g,height:p}=e,m,b,x,v,y,_;return o==="center"?(y=f+p/2,n==="left"?(m=u,b=m-a,v=y+a,_=y-a):(m=u+g,b=m+a,v=y-a,_=y+a),x=m):(n==="left"?b=u+Math.max(l,h)+a:n==="right"?b=u+g-Math.max(c,d)-a:b=this.caretX,o==="top"?(v=f,y=v-a,m=b-a,x=b+a):(v=f+p,y=v+a,m=b+a,x=b-a),_=v),{x1:m,x2:b,x3:x,y1:v,y2:y,y3:_}}drawTitle(t,e,s){let n=this.title,o=n.length,a,r,l;if(o){let c=Wt(s.rtl,this.x,this.width);for(t.x=bi(this,s.titleAlign,s),e.textAlign=c.textAlign(s.titleAlign),e.textBaseline="middle",a=$(s.titleFont),r=s.titleSpacing,e.fillStyle=s.titleColor,e.font=a.string,l=0;lx!==0)?(t.beginPath(),t.fillStyle=o.multiKeyBackground,oe(t,{x:p,y:g,w:c,h:l,radius:b}),t.fill(),t.stroke(),t.fillStyle=a.backgroundColor,t.beginPath(),oe(t,{x:m,y:g+1,w:c-2,h:l-2,radius:b}),t.fill()):(t.fillStyle=o.multiKeyBackground,t.fillRect(p,g,c,l),t.strokeRect(p,g,c,l),t.fillStyle=a.backgroundColor,t.fillRect(m,g+1,c-2,l-2))}t.fillStyle=this.labelTextColors[s]}drawBody(t,e,s){let{body:n}=this,{bodySpacing:o,bodyAlign:a,displayColors:r,boxHeight:l,boxWidth:c,boxPadding:h}=s,d=$(s.bodyFont),u=d.lineHeight,f=0,g=Wt(s.rtl,this.x,this.width),p=function(S){e.fillText(S,g.x(t.x+f),t.y+u/2),t.y+=u+o},m=g.textAlign(a),b,x,v,y,_,k,w;for(e.textAlign=a,e.textBaseline="middle",e.font=d.string,t.x=bi(this,m,s),e.fillStyle=s.bodyColor,E(this.beforeBody,p),f=r&&m!=="right"?a==="center"?c/2+h:c+2+h:0,y=0,k=n.length;y0&&e.stroke()}_updateAnimationTarget(t){let e=this.chart,s=this.$animations,n=s&&s.x,o=s&&s.y;if(n||o){let a=Ie[t.position].call(this,this._active,this._eventPosition);if(!a)return;let r=this._size=po(this,t),l=Object.assign({},a,this._size),c=mo(e,t,l),h=bo(t,l,c,e);(n._to!==h.x||o._to!==h.y)&&(this.xAlign=c.xAlign,this.yAlign=c.yAlign,this.width=r.width,this.height=r.height,this.caretX=a.x,this.caretY=a.y,this._resolveAnimations().update(this,h))}}_willRender(){return!!this.opacity}draw(t){let e=this.options.setContext(this.getContext()),s=this.opacity;if(!s)return;this._updateAnimationTarget(e);let n={width:this.width,height:this.height},o={x:this.x,y:this.y};s=Math.abs(s)<.001?0:s;let a=q(e.padding),r=this.title.length||this.beforeBody.length||this.body.length||this.afterBody.length||this.footer.length;e.enabled&&r&&(t.save(),t.globalAlpha=s,this.drawBackground(o,t,n,e),as(t,e.textDirection),o.y+=a.top,this.drawTitle(o,t,e),this.drawBody(o,t,e),this.drawFooter(o,t,e),rs(t,e.textDirection),t.restore())}getActiveElements(){return this._active||[]}setActiveElements(t,e){let s=this._active,n=t.map(({datasetIndex:r,index:l})=>{let c=this.chart.getDatasetMeta(r);if(!c)throw new Error("Cannot find a dataset at index "+r);return{datasetIndex:r,element:c.data[l],index:l}}),o=!we(s,n),a=this._positionChanged(n,e);(o||a)&&(this._active=n,this._eventPosition=e,this._ignoreReplayEvents=!0,this.update(!0))}handleEvent(t,e,s=!0){if(e&&this._ignoreReplayEvents)return!1;this._ignoreReplayEvents=!1;let n=this.options,o=this._active||[],a=this._getActiveElements(t,o,e,s),r=this._positionChanged(a,t),l=e||!we(a,o)||r;return l&&(this._active=a,(n.enabled||n.external)&&(this._eventPosition={x:t.x,y:t.y},this.update(!0,e))),l}_getActiveElements(t,e,s,n){let o=this.options;if(t.type==="mouseout")return[];if(!n)return e.filter(r=>this.chart.data.datasets[r.datasetIndex]&&this.chart.getDatasetMeta(r.datasetIndex).controller.getParsed(r.index)!==void 0);let a=this.chart.getElementsAtEventForMode(t,o.mode,o,s);return o.reverse&&a.reverse(),a}_positionChanged(t,e){let{caretX:s,caretY:n,options:o}=this,a=Ie[o.position].call(this,t,e);return a!==!1&&(s!==a.x||n!==a.y)}};M(We,"positioners",Ie);var ah={id:"tooltip",_element:We,positioners:Ie,afterInit(i,t,e){e&&(i.tooltip=new We({chart:i,options:e}))},beforeUpdate(i,t,e){i.tooltip&&i.tooltip.initialize(e)},reset(i,t,e){i.tooltip&&i.tooltip.initialize(e)},afterDraw(i){let t=i.tooltip;if(t&&t._willRender()){let e={tooltip:t};if(i.notifyPlugins("beforeTooltipDraw",{...e,cancelable:!0})===!1)return;t.draw(i.ctx),i.notifyPlugins("afterTooltipDraw",e)}},afterEvent(i,t){if(i.tooltip){let e=t.replay;i.tooltip.handleEvent(t.event,e,t.inChartArea)&&(t.changed=!0)}},defaults:{enabled:!0,external:null,position:"average",backgroundColor:"rgba(0,0,0,0.8)",titleColor:"#fff",titleFont:{weight:"bold"},titleSpacing:2,titleMarginBottom:6,titleAlign:"left",bodyColor:"#fff",bodySpacing:2,bodyFont:{},bodyAlign:"left",footerColor:"#fff",footerSpacing:2,footerMarginTop:6,footerFont:{weight:"bold"},footerAlign:"left",padding:6,caretPadding:2,caretSize:5,cornerRadius:6,boxHeight:(i,t)=>t.bodyFont.size,boxWidth:(i,t)=>t.bodyFont.size,multiKeyBackground:"#fff",displayColors:!0,boxPadding:0,borderColor:"rgba(0,0,0,0)",borderWidth:0,animation:{duration:400,easing:"easeOutQuart"},animations:{numbers:{type:"number",properties:["x","y","width","height","caretX","caretY"]},opacity:{easing:"linear",duration:200}},callbacks:Ko},defaultRoutes:{bodyFont:"font",footerFont:"font",titleFont:"font"},descriptors:{_scriptable:i=>i!=="filter"&&i!=="itemSort"&&i!=="external",_indexable:!1,callbacks:{_scriptable:!1,_indexable:!1},animation:{_fallback:!1},animations:{_fallback:"animation"}},additionalOptionScopes:["interaction"]},rh=Object.freeze({__proto__:null,Colors:_c,Decimation:kc,Filler:jc,Legend:qc,SubTitle:Zc,Title:Jc,Tooltip:ah}),lh=(i,t,e,s)=>(typeof t=="string"?(e=i.push(t)-1,s.unshift({index:e,label:t})):isNaN(t)&&(e=null),e);function ch(i,t,e,s){let n=i.indexOf(t);if(n===-1)return lh(i,t,e,s);let o=i.lastIndexOf(t);return n!==o?e:n}var hh=(i,t)=>i===null?null:Y(Math.round(i),0,t);function yo(i){let t=this.getLabels();return i>=0&&ie.length-1?null:this.getPixelForValue(e[t].value)}getValueForPixel(t){return Math.round(this._startValue+this.getDecimalForPixel(t)*this._valueRange)}getBasePixel(){return this.bottom}};M(Fe,"id","category"),M(Fe,"defaults",{ticks:{callback:yo}});function dh(i,t){let e=[],{bounds:n,step:o,min:a,max:r,precision:l,count:c,maxTicks:h,maxDigits:d,includeBounds:u}=i,f=o||1,g=h-1,{min:p,max:m}=t,b=!A(a),x=!A(r),v=!A(c),y=(m-p)/(d+1),_=Vi((m-p)/g/f)*f,k,w,S,P;if(_<1e-14&&!b&&!x)return[{value:p},{value:m}];P=Math.ceil(m/_)-Math.floor(p/_),P>g&&(_=Vi(P*_/g/f)*f),A(l)||(k=Math.pow(10,l),_=Math.ceil(_*k)/k),n==="ticks"?(w=Math.floor(p/_)*_,S=Math.ceil(m/_)*_):(w=p,S=m),b&&x&&o&&an((r-a)/o,_/1e3)?(P=Math.round(Math.min((r-a)/_,h)),_=(r-a)/P,w=a,S=r):v?(w=b?a:w,S=x?r:S,P=c-1,_=(S-w)/P):(P=(S-w)/_,ie(P,Math.round(P),_/1e3)?P=Math.round(P):P=Math.ceil(P));let O=Math.max(Ni(_),Ni(w));k=Math.pow(10,A(l)?O:l),w=Math.round(w*k)/k,S=Math.round(S*k)/k;let C=0;for(b&&(u&&w!==a?(e.push({value:a}),wr)break;e.push({value:L})}return x&&u&&S!==r?e.length&&ie(e[e.length-1].value,r,vo(r,y,i))?e[e.length-1].value=r:e.push({value:r}):(!x||S===r)&&e.push({value:S}),e}function vo(i,t,{horizontal:e,minRotation:s}){let n=ot(s),o=(e?Math.sin(n):Math.cos(n))||.001,a=.75*t*(""+i).length;return Math.min(t/o,a)}var me=class extends Xt{constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._endValue=void 0,this._valueRange=0}parse(t,e){return A(t)||(typeof t=="number"||t instanceof Number)&&!isFinite(+t)?null:+t}handleTickRangeOptions(){let{beginAtZero:t}=this.options,{minDefined:e,maxDefined:s}=this.getUserBounds(),{min:n,max:o}=this,a=l=>n=e?n:l,r=l=>o=s?o:l;if(t){let l=lt(n),c=lt(o);l<0&&c<0?r(0):l>0&&c>0&&a(0)}if(n===o){let l=o===0?1:Math.abs(o*.05);r(o+l),t||a(n-l)}this.min=n,this.max=o}getTickLimit(){let t=this.options.ticks,{maxTicksLimit:e,stepSize:s}=t,n;return s?(n=Math.ceil(this.max/s)-Math.floor(this.min/s)+1,n>1e3&&(console.warn(`scales.${this.id}.ticks.stepSize: ${s} would result generating up to ${n} ticks. Limiting to 1000.`),n=1e3)):(n=this.computeTickLimit(),e=e||11),e&&(n=Math.min(e,n)),n}computeTickLimit(){return Number.POSITIVE_INFINITY}buildTicks(){let t=this.options,e=t.ticks,s=this.getTickLimit();s=Math.max(2,s);let n={maxTicks:s,bounds:t.bounds,min:t.min,max:t.max,precision:e.precision,step:e.stepSize,count:e.count,maxDigits:this._maxDigits(),horizontal:this.isHorizontal(),minRotation:e.minRotation||0,includeBounds:e.includeBounds!==!1},o=this._range||this,a=dh(n,o);return t.bounds==="ticks"&&Wi(a,this,"value"),t.reverse?(a.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),a}configure(){let t=this.ticks,e=this.min,s=this.max;if(super.configure(),this.options.offset&&t.length){let n=(s-e)/Math.max(t.length-1,1)/2;e-=n,s+=n}this._startValue=e,this._endValue=s,this._valueRange=s-e}getLabelForValue(t){return ne(t,this.chart.options.locale,this.options.ticks.format)}},ze=class extends me{determineDataLimits(){let{min:t,max:e}=this.getMinMax(!0);this.min=N(t)?t:0,this.max=N(e)?e:1,this.handleTickRangeOptions()}computeTickLimit(){let t=this.isHorizontal(),e=t?this.width:this.height,s=ot(this.options.ticks.minRotation),n=(t?Math.sin(s):Math.cos(s))||.001,o=this._resolveTickFontOptions(0);return Math.ceil(e/Math.min(40,o.lineHeight/n))}getPixelForValue(t){return t===null?NaN:this.getPixelForDecimal((t-this._startValue)/this._valueRange)}getValueForPixel(t){return this._startValue+this.getDecimalForPixel(t)*this._valueRange}};M(ze,"id","linear"),M(ze,"defaults",{ticks:{callback:Se.formatters.numeric}});var je=i=>Math.floor(xt(i)),Ht=(i,t)=>Math.pow(10,je(i)+t);function Mo(i){return i/Math.pow(10,je(i))===1}function ko(i,t,e){let s=Math.pow(10,e),n=Math.floor(i/s);return Math.ceil(t/s)-n}function uh(i,t){let e=t-i,s=je(e);for(;ko(i,t,s)>10;)s++;for(;ko(i,t,s)<10;)s--;return Math.min(s,je(i))}function fh(i,{min:t,max:e}){t=Z(i.min,t);let s=[],n=je(t),o=uh(t,e),a=o<0?Math.pow(10,Math.abs(o)):1,r=Math.pow(10,o),l=n>o?Math.pow(10,n):0,c=Math.round((t-l)*a)/a,h=Math.floor((t-l)/r/10)*r*10,d=Math.floor((c-h)/Math.pow(10,o)),u=Z(i.min,Math.round((l+h+d*Math.pow(10,o))*a)/a);for(;u=10?d=d<15?15:20:d++,d>=20&&(o++,d=2,a=o>=0?1:a),u=Math.round((l+h+d*Math.pow(10,o))*a)/a;let f=Z(i.max,u);return s.push({value:f,major:Mo(f),significand:d}),s}var Be=class extends Xt{constructor(t){super(t),this.start=void 0,this.end=void 0,this._startValue=void 0,this._valueRange=0}parse(t,e){let s=me.prototype.parse.apply(this,[t,e]);if(s===0){this._zero=!0;return}return N(s)&&s>0?s:null}determineDataLimits(){let{min:t,max:e}=this.getMinMax(!0);this.min=N(t)?Math.max(0,t):null,this.max=N(e)?Math.max(0,e):null,this.options.beginAtZero&&(this._zero=!0),this._zero&&this.min!==this._suggestedMin&&!N(this._userMin)&&(this.min=t===Ht(this.min,0)?Ht(this.min,-1):Ht(this.min,0)),this.handleTickRangeOptions()}handleTickRangeOptions(){let{minDefined:t,maxDefined:e}=this.getUserBounds(),s=this.min,n=this.max,o=r=>s=t?s:r,a=r=>n=e?n:r;s===n&&(s<=0?(o(1),a(10)):(o(Ht(s,-1)),a(Ht(n,1)))),s<=0&&o(Ht(n,-1)),n<=0&&a(Ht(s,1)),this.min=s,this.max=n}buildTicks(){let t=this.options,e={min:this._userMin,max:this._userMax},s=fh(e,this);return t.bounds==="ticks"&&Wi(s,this,"value"),t.reverse?(s.reverse(),this.start=this.max,this.end=this.min):(this.start=this.min,this.end=this.max),s}getLabelForValue(t){return t===void 0?"0":ne(t,this.chart.options.locale,this.options.ticks.format)}configure(){let t=this.min;super.configure(),this._startValue=xt(t),this._valueRange=xt(this.max)-xt(t)}getPixelForValue(t){return(t===void 0||t===0)&&(t=this.min),t===null||isNaN(t)?NaN:this.getPixelForDecimal(t===this.min?0:(xt(t)-this._startValue)/this._valueRange)}getValueForPixel(t){let e=this.getDecimalForPixel(t);return Math.pow(10,this._startValue+e*this._valueRange)}};M(Be,"id","logarithmic"),M(Be,"defaults",{ticks:{callback:Se.formatters.logarithmic,major:{enabled:!0}}});function Is(i){let t=i.ticks;if(t.display&&i.display){let e=q(t.backdropPadding);return D(t.font&&t.font.size,V.font.size)+e.height}return 0}function gh(i,t,e){return e=z(e)?e:[e],{w:pn(i,t.string,e),h:e.length*t.lineHeight}}function wo(i,t,e,s,n){return i===s||i===n?{start:t-e/2,end:t+e/2}:in?{start:t-e,end:t}:{start:t,end:t+e}}function ph(i){let t={l:i.left+i._padding.left,r:i.right-i._padding.right,t:i.top+i._padding.top,b:i.bottom-i._padding.bottom},e=Object.assign({},t),s=[],n=[],o=i._pointLabels.length,a=i.options.pointLabels,r=a.centerPointLabels?R/o:0;for(let l=0;lt.r&&(r=(s.end-t.r)/o,i.r=Math.max(i.r,t.r+r)),n.startt.b&&(l=(n.end-t.b)/a,i.b=Math.max(i.b,t.b+l))}function bh(i,t,e){let s=i.drawingArea,{extra:n,additionalAngle:o,padding:a,size:r}=e,l=i.getPointPosition(t,s+n+a,o),c=Math.round(si(X(l.angle+H))),h=Mh(l.y,r.h,c),d=yh(c),u=vh(l.x,r.w,d);return{visible:!0,x:l.x,y:h,textAlign:d,left:u,top:h,right:u+r.w,bottom:h+r.h}}function xh(i,t){if(!t)return!0;let{left:e,top:s,right:n,bottom:o}=i;return!(ht({x:e,y:s},t)||ht({x:e,y:o},t)||ht({x:n,y:s},t)||ht({x:n,y:o},t))}function _h(i,t,e){let s=[],n=i._pointLabels.length,o=i.options,{centerPointLabels:a,display:r}=o.pointLabels,l={extra:Is(o)/2,additionalAngle:a?R/n:0},c;for(let h=0;h270||e<90)&&(i-=t),i}function kh(i,t,e){let{left:s,top:n,right:o,bottom:a}=e,{backdropColor:r}=t;if(!A(r)){let l=Tt(t.borderRadius),c=q(t.backdropPadding);i.fillStyle=r;let h=s-c.left,d=n-c.top,u=o-s+c.width,f=a-n+c.height;Object.values(l).some(g=>g!==0)?(i.beginPath(),oe(i,{x:h,y:d,w:u,h:f,radius:l}),i.fill()):i.fillRect(h,d,u,f)}}function wh(i,t){let{ctx:e,options:{pointLabels:s}}=i;for(let n=t-1;n>=0;n--){let o=i._pointLabelItems[n];if(!o.visible)continue;let a=s.setContext(i.getPointLabelContext(n));kh(e,a,o);let r=$(a.font),{x:l,y:c,textAlign:h}=o;At(e,i._pointLabels[n],l,c+r.lineHeight/2,r,{color:a.color,textAlign:h,textBaseline:"middle"})}}function qo(i,t,e,s){let{ctx:n}=i;if(e)n.arc(i.xCenter,i.yCenter,t,0,B);else{let o=i.getPointPosition(0,t);n.moveTo(o.x,o.y);for(let a=1;a{let n=F(this.options.pointLabels.callback,[e,s],this);return n||n===0?n:""}).filter((e,s)=>this.chart.getDataVisibility(s))}fit(){let t=this.options;t.display&&t.pointLabels.display?ph(this):this.setCenterPoint(0,0,0,0)}setCenterPoint(t,e,s,n){this.xCenter+=Math.floor((t-e)/2),this.yCenter+=Math.floor((s-n)/2),this.drawingArea-=Math.min(this.drawingArea/2,Math.max(t,e,s,n))}getIndexAngle(t){let e=B/(this._pointLabels.length||1),s=this.options.startAngle||0;return X(t*e+ot(s))}getDistanceFromCenterForValue(t){if(A(t))return NaN;let e=this.drawingArea/(this.max-this.min);return this.options.reverse?(this.max-t)*e:(t-this.min)*e}getValueForDistanceFromCenter(t){if(A(t))return NaN;let e=t/(this.drawingArea/(this.max-this.min));return this.options.reverse?this.max-e:this.min+e}getPointLabelContext(t){let e=this._pointLabels||[];if(t>=0&&t{if(d!==0||d===0&&this.min<0){l=this.getDistanceFromCenterForValue(h.value);let u=this.getContext(d),f=n.setContext(u),g=o.setContext(u);Sh(this,f,l,a,g)}}),s.display){for(t.save(),r=a-1;r>=0;r--){let h=s.setContext(this.getPointLabelContext(r)),{color:d,lineWidth:u}=h;!u||!d||(t.lineWidth=u,t.strokeStyle=d,t.setLineDash(h.borderDash),t.lineDashOffset=h.borderDashOffset,l=this.getDistanceFromCenterForValue(e.reverse?this.min:this.max),c=this.getPointPosition(r,l),t.beginPath(),t.moveTo(this.xCenter,this.yCenter),t.lineTo(c.x,c.y),t.stroke())}t.restore()}}drawBorder(){}drawLabels(){let t=this.ctx,e=this.options,s=e.ticks;if(!s.display)return;let n=this.getIndexAngle(0),o,a;t.save(),t.translate(this.xCenter,this.yCenter),t.rotate(n),t.textAlign="center",t.textBaseline="middle",this.ticks.forEach((r,l)=>{if(l===0&&this.min>=0&&!e.reverse)return;let c=s.setContext(this.getContext(l)),h=$(c.font);if(o=this.getDistanceFromCenterForValue(this.ticks[l].value),c.showLabelBackdrop){t.font=h.string,a=t.measureText(r.label).width,t.fillStyle=c.backdropColor;let d=q(c.backdropPadding);t.fillRect(-a/2-d.left,-o-h.size/2-d.top,a+d.width,h.size+d.height)}At(t,r.label,0,-o,h,{color:c.color,strokeColor:c.textStrokeColor,strokeWidth:c.textStrokeWidth})}),t.restore()}drawTitle(){}};M($t,"id","radialLinear"),M($t,"defaults",{display:!0,animate:!0,position:"chartArea",angleLines:{display:!0,lineWidth:1,borderDash:[],borderDashOffset:0},grid:{circular:!1},startAngle:0,ticks:{showLabelBackdrop:!0,callback:Se.formatters.numeric},pointLabels:{backdropColor:void 0,backdropPadding:2,display:!0,font:{size:10},callback(t){return t},padding:5,centerPointLabels:!1}}),M($t,"defaultRoutes",{"angleLines.color":"borderColor","pointLabels.color":"color","ticks.color":"color"}),M($t,"descriptors",{angleLines:{_fallback:"grid"}});var Oi={millisecond:{common:!0,size:1,steps:1e3},second:{common:!0,size:1e3,steps:60},minute:{common:!0,size:6e4,steps:60},hour:{common:!0,size:36e5,steps:24},day:{common:!0,size:864e5,steps:30},week:{common:!1,size:6048e5,steps:4},month:{common:!0,size:2628e6,steps:12},quarter:{common:!1,size:7884e6,steps:4},year:{common:!0,size:3154e7}},tt=Object.keys(Oi);function So(i,t){return i-t}function Po(i,t){if(A(t))return null;let e=i._adapter,{parser:s,round:n,isoWeekday:o}=i._parseOpts,a=t;return typeof s=="function"&&(a=s(a)),N(a)||(a=typeof s=="string"?e.parse(a,s):e.parse(a)),a===null?null:(n&&(a=n==="week"&&(Vt(o)||o===!0)?e.startOf(a,"isoWeek",o):e.startOf(a,n)),+a)}function Do(i,t,e,s){let n=tt.length;for(let o=tt.indexOf(i);o=tt.indexOf(e);o--){let a=tt[o];if(Oi[a].common&&i._adapter.diff(n,s,a)>=t-1)return a}return tt[e?tt.indexOf(e):0]}function Oh(i){for(let t=tt.indexOf(i)+1,e=tt.length;t=t?e[s]:e[n];i[o]=!0}}function Ch(i,t,e,s){let n=i._adapter,o=+n.startOf(t[0].value,s),a=t[t.length-1].value,r,l;for(r=o;r<=a;r=+n.add(r,1,s))l=e[r],l>=0&&(t[l].major=!0);return t}function Co(i,t,e){let s=[],n={},o=t.length,a,r;for(a=0;a+t.value))}initOffsets(t=[]){let e=0,s=0,n,o;this.options.offset&&t.length&&(n=this.getDecimalForValue(t[0]),t.length===1?e=1-n:e=(this.getDecimalForValue(t[1])-n)/2,o=this.getDecimalForValue(t[t.length-1]),t.length===1?s=o:s=(o-this.getDecimalForValue(t[t.length-2]))/2);let a=t.length<3?.5:.25;e=Y(e,0,a),s=Y(s,0,a),this._offsets={start:e,end:s,factor:1/(e+1+s)}}_generate(){let t=this._adapter,e=this.min,s=this.max,n=this.options,o=n.time,a=o.unit||Do(o.minUnit,e,s,this._getLabelCapacity(e)),r=D(n.ticks.stepSize,1),l=a==="week"?o.isoWeekday:!1,c=Vt(l)||l===!0,h={},d=e,u,f;if(c&&(d=+t.startOf(d,"isoWeek",l)),d=+t.startOf(d,c?"day":a),t.diff(s,e,a)>1e5*r)throw new Error(e+" and "+s+" are too far apart with stepSize of "+r+" "+a);let g=n.ticks.source==="data"&&this.getDataTimestamps();for(u=d,f=0;u+p)}getLabelForValue(t){let e=this._adapter,s=this.options.time;return s.tooltipFormat?e.format(t,s.tooltipFormat):e.format(t,s.displayFormats.datetime)}format(t,e){let n=this.options.time.displayFormats,o=this._unit,a=e||n[o];return this._adapter.format(t,a)}_tickFormatFunction(t,e,s,n){let o=this.options,a=o.ticks.callback;if(a)return F(a,[t,e,s],this);let r=o.time.displayFormats,l=this._unit,c=this._majorUnit,h=l&&r[l],d=c&&r[c],u=s[e],f=c&&d&&u&&u.major;return this._adapter.format(t,n||(f?d:h))}generateTickLabels(t){let e,s,n;for(e=0,s=t.length;e0?r:1}getDataTimestamps(){let t=this._cache.data||[],e,s;if(t.length)return t;let n=this.getMatchingVisibleMetas();if(this._normalized&&n.length)return this._cache.data=n[0].controller.getAllParsedValues(this);for(e=0,s=n.length;e=i[s].pos&&t<=i[n].pos&&({lo:s,hi:n}=ct(i,"pos",t)),{pos:o,time:r}=i[s],{pos:a,time:l}=i[n]):(t>=i[s].time&&t<=i[n].time&&({lo:s,hi:n}=ct(i,"time",t)),{time:o,pos:r}=i[s],{time:a,pos:l}=i[n]);let c=a-o;return c?r+(l-r)*(t-o)/c:r}var Ve=class extends Ut{constructor(t){super(t),this._table=[],this._minPos=void 0,this._tableRange=void 0}initOffsets(){let t=this._getTimestampsForTable(),e=this._table=this.buildLookupTable(t);this._minPos=xi(e,this.min),this._tableRange=xi(e,this.max)-this._minPos,super.initOffsets(t)}buildLookupTable(t){let{min:e,max:s}=this,n=[],o=[],a,r,l,c,h;for(a=0,r=t.length;a=e&&c<=s&&n.push(c);if(n.length<2)return[{time:e,pos:0},{time:s,pos:1}];for(a=0,r=n.length;an-o)}_getTimestampsForTable(){let t=this._cache.all||[];if(t.length)return t;let e=this.getDataTimestamps(),s=this.getLabelTimestamps();return e.length&&s.length?t=this.normalize(e.concat(s)):t=e.length?e:s,t=this._cache.all=t,t}getDecimalForValue(t){return(xi(this._table,t)-this._minPos)/this._tableRange}getValueForPixel(t){let e=this._offsets,s=this.getDecimalForPixel(t)/e.factor-e.end;return xi(this._table,s*this._tableRange+this._minPos,!0)}};M(Ve,"id","timeseries"),M(Ve,"defaults",Ut.defaults);var Ah=Object.freeze({__proto__:null,CategoryScale:Fe,LinearScale:ze,LogarithmicScale:Be,RadialLinearScale:$t,TimeScale:Ut,TimeSeriesScale:Ve}),Go=[Nr,uc,rh,Ah];at.register(...Go);var Fs=at;function Th({dataChecksum:i,labels:t,values:e}){return{dataChecksum:i,init(){Alpine.effect(()=>{Alpine.store("theme");let s=this.getChart();s&&s.destroy(),this.initChart()}),window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change",()=>{Alpine.store("theme")==="system"&&this.$nextTick(()=>{let s=this.getChart();s&&s.destroy(),this.initChart()})})},initChart(){if(!(!this.$refs.canvas||!this.$refs.backgroundColorElement||!this.$refs.borderColorElement))return new Fs(this.$refs.canvas,{type:"line",data:{labels:t,datasets:[{data:e,borderWidth:2,fill:"start",tension:.5,backgroundColor:getComputedStyle(this.$refs.backgroundColorElement).color,borderColor:getComputedStyle(this.$refs.borderColorElement).color}]},options:{animation:{duration:0},elements:{point:{radius:0}},maintainAspectRatio:!1,plugins:{legend:{display:!1}},scales:{x:{display:!1},y:{display:!1}},tooltips:{enabled:!1}}})},getChart(){return this.$refs.canvas?Fs.getChart(this.$refs.canvas):null}}}export{Th as default}; +/*! Bundled license information: + +@kurkle/color/dist/color.esm.js: + (*! + * @kurkle/color v0.3.4 + * https://github.com/kurkle/color#readme + * (c) 2024 Jukka Kurkela + * Released under the MIT License + *) + +chart.js/dist/chunks/helpers.dataset.js: +chart.js/dist/chart.js: + (*! + * Chart.js v4.5.1 + * https://www.chartjs.org + * (c) 2025 Chart.js Contributors + * Released under the MIT License + *) +*/ diff --git a/public/logo-dark.svg b/public/logo-dark.svg index e0cf79fb..9e1b33e5 100644 --- a/public/logo-dark.svg +++ b/public/logo-dark.svg @@ -1,6 +1,6 @@ - + diff --git a/public/logo.svg b/public/logo.svg index 4feba779..2b60ec2a 100644 --- a/public/logo.svg +++ b/public/logo.svg @@ -1,6 +1,6 @@ - + diff --git a/rector.php b/rector.php new file mode 100644 index 00000000..45d9e6ca --- /dev/null +++ b/rector.php @@ -0,0 +1,33 @@ +withPaths([ + __DIR__.'/app', + __DIR__.'/bootstrap', + __DIR__.'/config', + __DIR__.'/public', + __DIR__.'/resources', + __DIR__.'/routes', + __DIR__.'/tests', + ]) + ->withSkip([ + __DIR__.'/vendor', + ContainerBindConcreteWithClosureOnlyRector::class, + ]) + ->withSets([ + LaravelLevelSetList::UP_TO_LARAVEL_120, + LaravelSetList::LARAVEL_120, + LaravelSetList::LARAVEL_CODE_QUALITY, + ]) + ->withPhpVersion(PhpVersion::PHP_84) + ->withTypeCoverageLevel(0) + ->withDeadCodeLevel(0) + ->withCodeQualityLevel(0); diff --git a/resources/assets/background-pattern-dark.svg b/resources/assets/background-pattern-dark.svg deleted file mode 100644 index 461b3bc4..00000000 --- a/resources/assets/background-pattern-dark.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/resources/assets/background-pattern.svg b/resources/assets/background-pattern.svg deleted file mode 100644 index 9f543b8f..00000000 --- a/resources/assets/background-pattern.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/resources/css/app.css b/resources/css/app.css index d2c3d212..7c59f8d4 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -1,82 +1,223 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import 'tailwindcss'; +@import '../../vendor/livewire/flux/dist/flux.css'; -[x-cloak] { - display: none; +@plugin '@tailwindcss/typography'; +@plugin '@tailwindcss/forms'; + +@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php'; +@source '../../storage/framework/views/*.php'; +@source '../../vendor/filament/**/*.blade.php'; + +/* Safelist classes used in dynamic content (blog posts from database) */ +@source inline("grid-cols-1 grid-cols-2 grid-cols-3 grid-cols-4 grid-cols-5 grid-cols-6"); + +@custom-variant dark (&:where(.dark, .dark *)); + +@theme { + --breakpoint-3xs: 20rem; + --breakpoint-2xs: 25rem; + --breakpoint-xs: 28rem; + + --color-mirage: #141624; + --color-haiti: #16182c; + --color-cloud: #2b2e53; + --color-snow-flurry-50: #e0ffcb; + --color-snow-flurry-100: #d1ffae; + --color-snow-flurry-200: #a2fd00; + --color-snow-flurry-300: #97ed00; + + --font-poppins: 'Poppins', Verdana, sans-serif; } -/* Look cool on safari */ -.blur-background { - backdrop-filter: blur(20px); +@utility container { + margin-inline: auto; } -.background-pattern { - background-image: url('../assets/background-pattern.svg'); +@layer base { + button:not(:disabled), + [role='button']:not(:disabled) { + cursor: pointer; + } } -@media(prefers-color-scheme: dark) { - .background-pattern { - background-image: url('../assets/background-pattern-dark.svg'); +/* + The default border color has changed to `currentcolor` in Tailwind CSS v4, + so we've added these compatibility styles to make sure everything still + looks the same as it did with Tailwind CSS v3. + + If we ever want to remove these styles, we need to add an explicit border + color utility to any element that depends on these defaults. +*/ +@layer base { + *, + ::after, + ::before, + ::backdrop, + ::file-selector-button { + border-color: var(--color-gray-200, currentcolor); } } +@layer base { + html { + -webkit-tap-highlight-color: transparent; + } +} + +@keyframes shine { + 0% { + background-position: 200% 50%; + } + 100% { + background-position: 0% 50%; + } +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +/* Compensate for custom scrollbar width when Flux locks scroll (desktop only) */ +@media (pointer: fine) { + html[data-flux-scroll-unlock] { + padding-right: 8px !important; + } +} + +/* Scrollbar width */ +::-webkit-scrollbar { + height: 8px; + width: 8px; +} + +/* Scrollbar track */ +::-webkit-scrollbar-track { + background: transparent; + border-radius: 4px; +} + +::-webkit-scrollbar-thumb { + background: transparent; +} + +/* Scrollbar thumb (the draggable part) */ +::-webkit-scrollbar-thumb { + border-radius: 999px; +} + +/* Light Theme */ +::-webkit-scrollbar-thumb { + @apply bg-gray-300/80; +} + +/* Scrollbar thumb on hover */ +::-webkit-scrollbar-thumb:hover { + @apply bg-gray-300; +} + +/* Dark Theme */ +.dark ::-webkit-scrollbar-thumb { + @apply bg-cloud/60; +} + +/* Scrollbar thumb on hover */ +.dark ::-webkit-scrollbar-thumb:hover { + @apply bg-cloud; +} /* Whole menu */ -nav ul { - @apply text-xl md:text-sm list-none; +nav.docs-navigation ul { + @apply list-none text-sm; } /* Categories */ -nav > ul > li { +nav.docs-navigation > ul > li { @apply mb-3; & > a { @apply font-semibold; - @apply no-underline px-3 py-1.5 mb-1.5; - @apply border rounded-lg dark:border-white/15; + @apply mb-1.5 px-3 py-2.5 no-underline; + @apply rounded-lg; } } -nav > ul > li.active > a { - @apply text-[#00aaa6]; - /*@apply border-[#00aaa6] dark:border-[#00aaa6]/90;*/ +nav.docs-navigation > ul > li.active > a { + @apply bg-violet-100 text-black dark:bg-violet-950/50 dark:text-white; & > svg { - @apply text-[#00aaa6]; + @apply text-violet-400; } } -nav > ul > li:hover > a { - @apply text-[#00aaa6]; - /*@apply border-[#00aaa6] dark:border-[#00aaa6]/90;*/ +nav.docs-navigation > ul > li > a { + @apply transition duration-200 will-change-transform; +} + +nav.docs-navigation > ul > li:hover > a { + @apply translate-x-0.5 bg-gray-50 dark:bg-gray-900/50; } /* Sub menus */ -nav > ul > li > ul { - @apply mb-6; +nav.docs-navigation > ul > li > ul { + & > li { + @apply font-normal; + + & > a { + @apply ml-3 block w-full border-l-2 py-1.5 pl-3 transition-all duration-200 dark:border-white/10; + } + + &.exact-active > a { + @apply border-violet-400 font-medium text-violet-800/80 dark:border-violet-400/90 dark:text-white; + } + + &:hover > a { + @apply border-violet-400 dark:border-violet-400/90; + } + } +} + +/* Third tier (subsections) */ +nav.docs-navigation ul.third-tier { + @apply mt-1; & > li { @apply font-normal; - & a { - @apply w-full block; - @apply py-1.5; - @apply border-l pl-3 dark:border-white/10; - @apply ml-3; + & > a { + @apply ml-6 block w-full border-l-2 py-1 pl-3 text-xs transition-all duration-200 dark:border-white/10; } - &.exact-active a { - @apply text-[#00aaa6]; - @apply border-[#00aaa6] dark:border-[#00aaa6]/90; - /*@apply font-medium*/ + &.exact-active > a { + @apply border-violet-400 font-medium text-violet-800/80 dark:border-violet-400/90 dark:text-white; } - &:hover a { - /*@apply text-[#00aaa6];*/ - @apply border-[#00aaa6] dark:border-[#00aaa6]/90; + &:hover > a { + @apply border-violet-400 dark:border-violet-400/90; } + } +} +/* Subsection header styling */ +nav.docs-navigation .subsection-header { + @apply ml-3 flex items-center gap-2 border-l-2 py-1.5 pl-3 font-semibold text-gray-700 dark:border-white/10 dark:text-gray-300; +} + +nav.docs-navigation li:has(.third-tier .exact-active) > .subsection-header { + @apply border-violet-400 dark:border-violet-400/90; +} + +/* Prose */ +.prose h1, +.prose h2, +.prose h3 { + & .heading-anchor { + @apply inline-block opacity-0 translate-x-[-4px] transition-all duration-200 ease-out; + } + + &:hover .heading-anchor { + @apply opacity-100 translate-x-0; } } @@ -97,15 +238,20 @@ nav > ul > li > ul { } .prose pre { - @apply p-6 shadow-lg rounded-xl; + @apply max-w-full overflow-x-auto rounded-xl p-6 shadow-lg; } .prose pre code { - @apply text-gray-50; + @apply block bg-transparent p-0 text-sm font-normal text-gray-50; } .prose code { - @apply px-1; + @apply rounded bg-gray-200 px-1.5 py-0.5 text-sm font-medium text-purple-600; +} + +.prose code::before, +.prose code::after { + content: none; } .prose a { @@ -117,7 +263,7 @@ nav > ul > li > ul { } .prose img { - @apply shadow-md rounded-xl; + @apply rounded-xl shadow-md; } .prose img.no-format { @@ -129,7 +275,7 @@ nav > ul > li > ul { overflow-x-auto is recommended. */ .prose pre { - @apply p-0 my-4 overflow-x-auto bg-transparent rounded-md; + @apply my-4 max-w-full overflow-x-auto rounded-md bg-transparent p-0; } /* @@ -139,7 +285,7 @@ nav > ul > li > ul { colors extend edge to edge. */ .prose pre code.torchlight { - @apply block py-4 min-w-max; + @apply block min-w-max py-4; } /* @@ -158,62 +304,138 @@ nav > ul > li > ul { @apply mr-4; } +/* + The wrapper handles the visual container (border radius, + overflow clipping) so code blocks don't break out of their + rounded corners on mobile. The pre inside handles scrolling. +*/ +.torchlight-with-copy { + @apply my-4 overflow-hidden rounded-md; +} +.prose .torchlight-with-copy pre { + @apply my-0; +} +/* + Transparent scrollbar that fades in on hover. +*/ +.torchlight-with-copy pre { + scrollbar-width: thin; + scrollbar-color: transparent transparent; +} +.torchlight-with-copy pre:hover { + scrollbar-color: rgb(255 255 255 / 0.3) transparent; +} -:root { - --docsearch-container-background: rgba(0, 0, 0, 0.5); - --docsearch-primary-color: #00aaa6; +.torchlight-with-copy pre::-webkit-scrollbar { + height: 6px; } -[id=docsearch] { - width: 100%; - @apply md:w-auto; +.torchlight-with-copy pre::-webkit-scrollbar-track, +.torchlight-with-copy pre::-webkit-scrollbar-corner { + background: transparent; } -.DocSearch-Button { - @apply border border-solid border-gray-400/50 dark:border-white/10 dark:bg-black/10 flex items-center w-full rounded; +.torchlight-with-copy pre::-webkit-scrollbar-thumb { + background: transparent; + border-radius: 3px; } -.DocSearch-Button:hover { - box-shadow: none; - @apply text-gray-400 border-gray-400/70 bg-transparent dark:border-white/20; +.torchlight-with-copy pre:hover::-webkit-scrollbar-thumb { + background: rgb(255 255 255 / 0.2); } -.DocSearch-Button .DocSearch-Search-Icon { - height: 0.87rem; - @apply text-gray-500 dark:text-white/60; +/* + Prose dark mode +*/ +.dark .prose strong { + @apply text-gray-300; } -.DocSearch-Button-Placeholder { - @apply dark:text-white/60 pr-44 text-xs; +.dark .prose blockquote { + @apply text-gray-300; } -.DocSearch-Button-Keys { - display: flex; - min-width: auto; +.dark .prose code { + @apply bg-gray-800 text-purple-300; +} - @apply mt-1 text-xs leading-none; +.prose table { + @apply w-full; } -.DocSearch-Button-Key { - background: none; - box-shadow: none; - width: auto; +.prose :where(table):not(:where([class~='not-prose'] *)) { + display: block; + overflow-x: auto; +} - @apply font-sans mr-0.5; +/* + Blur and dim the lines that don't have the `.line-focus` class, + but are within a code block that contains any focus lines. +*/ +.torchlight.has-focus-lines .line:not(.line-focus) { + transition: filter 0.35s, opacity 0.35s; + filter: blur(.095rem); + opacity: .65; +} + +/* + When the code block is hovered, bring all the lines into focus. +*/ +.torchlight.has-focus-lines:hover .line:not(.line-focus) { + filter: blur(0px); + opacity: 1; +} + +.prose aside { + @apply relative z-0 mt-5 overflow-hidden rounded-2xl bg-gradient-to-tl from-transparent to-violet-100/75 px-5 ring-1 ring-black/5 dark:from-slate-900/30 dark:to-indigo-900/35; +} + +.images-two-up { + @apply grid gap-8 mt-0 items-center; + + @variant sm { + @apply grid-cols-2; + } } -.DocSearch-Modal { - @apply text-black; + +/* Responsive embedded content */ +@layer components { + .aspect-video iframe { + @apply absolute inset-0 w-full h-full; + } +} + +/* Snippet component with tabbed code blocks */ +.snippet { + @apply rounded-xl shadow-lg; +} + +.snippet-content { + @apply relative; } -.DocSearch-Screen-Icon { - display: none; +.snippet-tab pre { + @apply m-0 rounded-none bg-transparent p-0; } -.DocSearch-Input { - @apply focus-visible:outline-none; +.snippet-tab pre code { + @apply block min-w-max py-4 text-sm; } +.snippet-tab pre code .line { + @apply px-4; +} + +/* When snippet has a single tab, round the top corners */ +.snippet-content:first-child { + @apply rounded-t-xl; +} + +/* Hide Torchlight's default copy button inside snippets */ +.snippet .torchlight-with-copy > div:first-child { + @apply hidden; +} diff --git a/resources/css/docsearch.css b/resources/css/docsearch.css new file mode 100644 index 00000000..513bc8c3 --- /dev/null +++ b/resources/css/docsearch.css @@ -0,0 +1,161 @@ +@reference "./app.css" + +:root { + --docsearch-container-background: rgba(0, 0, 0, 0.5); + --docsearch-primary-color: #987af1; +} + +[id='docsearch'] { + width: 100%; + @apply md:w-auto; +} + +.DocSearch-Button { + @apply m-0 flex h-10 items-center rounded-full bg-gray-50/50 font-normal ring-1 ring-slate-600/30 transition duration-300 ease-out ring-inset min-[1024px]:h-9 dark:bg-black/30; +} + +.DocSearch-Button:hover { + box-shadow: none; + @apply bg-gray-100 ring-1 ring-transparent dark:bg-slate-950 dark:ring-slate-700/70; +} + +.DocSearch-Button:hover .DocSearch-Button-Placeholder { + @apply text-black dark:text-white; +} + +.DocSearch-Button .DocSearch-Search-Icon { + height: 0.87rem; + @apply text-gray-500 dark:text-white/60; +} + +.DocSearch-Button-Placeholder { + @apply pr-2 pl-1 text-sm text-black/60 transition duration-300 xl:pr-5 dark:text-white/60; +} + +.DocSearch-Button-Keys { + @apply mt-1 ml-1 hidden min-w-[auto] text-sm leading-none sm:flex; +} + +.DocSearch-Button-Placeholder { + @apply inline!; +} + +.DocSearch-Button-Key { + background: none; + box-shadow: none; + width: auto; + + @apply mr-0.5 font-sans; +} + +.DocSearch-Modal { + @apply text-black; +} + +.DocSearch-Modal:where(.dark, .dark *) { + background-color: var(--color-slate-950); + --docsearch-hit-active-color: var(--color-slate-300); +} + +.DocSearch-SearchBar .DocSearch-Form:where(.dark, .dark *) { + border-color: color-mix(in oklab, var(--color-white) 10%, transparent); + background-color: color-mix(in oklab, var(--color-white) 15%, transparent); + color: var(--color-slate-300); +} +.DocSearch-SearchBar .DocSearch-Form:where(.dark, .dark *)::placeholder { + color: var(--color-slate-400); +} +.DocSearch-SearchBar .DocSearch-Form:where(.dark, .dark *) { + --tw-shadow: 0 0 #0000; + box-shadow: + var(--tw-inset-shadow), var(--tw-inset-ring-shadow), + var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow); +} +.DocSearch-SearchBar .DocSearch-Form:where(.dark, .dark *):disabled { + border-color: color-mix(in oklab, var(--color-white) 5%, transparent); + color: var(--color-slate-400); +} +.DocSearch-SearchBar + .DocSearch-Form:where(.dark, .dark *):disabled::placeholder, +.DocSearch-SearchBar .DocSearch-MagnifierLabel { + color: var(--color-slate-500); +} +.DocSearch-SearchBar .DocSearch-MagnifierLabel:where(.dark, .dark *) { + color: var(--color-slate-300); +} +.DocSearch-SearchBar .DocSearch-LoadingIndicator { + color: var(--color-slate-500); +} +.DocSearch-SearchBar .DocSearch-LoadingIndicator:where(.dark, .dark *), +.DocSearch-SearchBar .DocSearch-Input:where(.dark, .dark *), +.DocSearch-SearchBar .DocSearch-Cancel:where(.dark, .dark *) { + color: var(--color-slate-300); +} + +.DocSearch-Dropdown .DocSearch-Hits mark:where(.dark, .dark *) { + color: var(--color-slate-400); +} + +.DocSearch-Hit-source { + @apply py-2.5 text-base; +} + +.DocSearch-Dropdown .DocSearch-Hit-source:where(.dark, .dark *) { + background-color: var(--color-slate-950); + color: var(--color-slate-300); +} + +.DocSearch-Dropdown + .DocSearch-Hit[aria-selected='true'] + a:where(.dark, .dark *) { + background-color: var(--color-indigo-800); +} + +.DocSearch-Dropdown .DocSearch-Hit a:where(.dark, .dark *) { + background-color: var(--color-indigo-900); + box-shadow: none; +} + +.DocSearch-Dropdown .DocSearch-Hit .DocSearch-Hit-title:where(.dark, .dark *) { + color: var(--color-slate-300); +} + +.DocSearch-Dropdown + .DocSearch-NoResults + .DocSearch-Screen-Icon:where(.dark, .dark *) { + color: var(--color-slate-500); +} + +.DocSearch-Dropdown + .DocSearch-NoResults + .DocSearch-Title:where(.dark, .dark *) { + color: var(--color-slate-300); +} + +.DocSearch-Footer { + @apply py-6; +} + +.DocSearch-Footer:where(.dark, .dark *) { + background: var(--color-slate-950); + box-shadow: none; + @apply border-t border-t-slate-700; +} + +.DocSearch-Footer .DocSearch-Logo svg :where(.dark, .dark *) { + fill: var(--color-gray-500); + color: var(--color-white); + background: transparent; +} + +.DocSearch-Screen-Icon { + display: none; +} + +.DocSearch-Input { + @apply ring-0 outline-0 focus-visible:outline-none; +} + +.DocSearch-Form { + @apply rounded-lg; +} diff --git a/resources/images/home/iphone.webp b/resources/images/home/iphone.webp new file mode 100644 index 00000000..f0d0e09e Binary files /dev/null and b/resources/images/home/iphone.webp differ diff --git a/resources/images/home/laravel_welcome_dark.webp b/resources/images/home/laravel_welcome_dark.webp new file mode 100644 index 00000000..5d86ae04 Binary files /dev/null and b/resources/images/home/laravel_welcome_dark.webp differ diff --git a/resources/images/home/laravel_welcome_light.webp b/resources/images/home/laravel_welcome_light.webp new file mode 100644 index 00000000..5c588902 Binary files /dev/null and b/resources/images/home/laravel_welcome_light.webp differ diff --git a/resources/images/home/macbook.jpg b/resources/images/home/macbook.jpg new file mode 100644 index 00000000..0fc3591b Binary files /dev/null and b/resources/images/home/macbook.jpg differ diff --git a/resources/images/home/macbook.webp b/resources/images/home/macbook.webp new file mode 100644 index 00000000..6cf637bc Binary files /dev/null and b/resources/images/home/macbook.webp differ diff --git a/resources/images/home/video_introduction_thumbnail.webp b/resources/images/home/video_introduction_thumbnail.webp new file mode 100644 index 00000000..36229934 Binary files /dev/null and b/resources/images/home/video_introduction_thumbnail.webp differ diff --git a/resources/images/laracon-us-2025/laracon-text.webp b/resources/images/laracon-us-2025/laracon-text.webp new file mode 100644 index 00000000..cdef9ff8 Binary files /dev/null and b/resources/images/laracon-us-2025/laracon-text.webp differ diff --git a/resources/images/laracon-us-2025/speakers/Evan-You.webp b/resources/images/laracon-us-2025/speakers/Evan-You.webp new file mode 100644 index 00000000..c6aba5b7 Binary files /dev/null and b/resources/images/laracon-us-2025/speakers/Evan-You.webp differ diff --git a/resources/images/laracon-us-2025/speakers/Jeffrey-Way.webp b/resources/images/laracon-us-2025/speakers/Jeffrey-Way.webp new file mode 100644 index 00000000..53cd59fc Binary files /dev/null and b/resources/images/laracon-us-2025/speakers/Jeffrey-Way.webp differ diff --git a/resources/images/laracon-us-2025/speakers/Joe-Tannenbaum.webp b/resources/images/laracon-us-2025/speakers/Joe-Tannenbaum.webp new file mode 100644 index 00000000..017f23a3 Binary files /dev/null and b/resources/images/laracon-us-2025/speakers/Joe-Tannenbaum.webp differ diff --git a/resources/images/laracon-us-2025/speakers/Taylor-Otwell.webp b/resources/images/laracon-us-2025/speakers/Taylor-Otwell.webp new file mode 100644 index 00000000..5af62c57 Binary files /dev/null and b/resources/images/laracon-us-2025/speakers/Taylor-Otwell.webp differ diff --git a/resources/images/laracon-us-2025/ticket.webp b/resources/images/laracon-us-2025/ticket.webp new file mode 100644 index 00000000..e5e68949 Binary files /dev/null and b/resources/images/laracon-us-2025/ticket.webp differ diff --git a/resources/images/marcel2023laraconus.webp b/resources/images/marcel2023laraconus.webp new file mode 100644 index 00000000..f4f00ece Binary files /dev/null and b/resources/images/marcel2023laraconus.webp differ diff --git a/resources/images/marcelpaciot_faded.webp b/resources/images/marcelpaciot_faded.webp new file mode 100644 index 00000000..7f5798b6 Binary files /dev/null and b/resources/images/marcelpaciot_faded.webp differ diff --git a/resources/images/mobile/android_phone_mockup.webp b/resources/images/mobile/android_phone_mockup.webp new file mode 100644 index 00000000..f2d766be Binary files /dev/null and b/resources/images/mobile/android_phone_mockup.webp differ diff --git a/resources/images/mobile/developer_holding_phone.webp b/resources/images/mobile/developer_holding_phone.webp new file mode 100644 index 00000000..63e2f7a3 Binary files /dev/null and b/resources/images/mobile/developer_holding_phone.webp differ diff --git a/resources/images/mobile/ios_phone_mockup.webp b/resources/images/mobile/ios_phone_mockup.webp new file mode 100644 index 00000000..9f109a35 Binary files /dev/null and b/resources/images/mobile/ios_phone_mockup.webp differ diff --git a/resources/images/mobile/macos_wallpaper.webp b/resources/images/mobile/macos_wallpaper.webp new file mode 100644 index 00000000..654ec5f7 Binary files /dev/null and b/resources/images/mobile/macos_wallpaper.webp differ diff --git a/resources/images/prizes/3d_license_document.webp b/resources/images/prizes/3d_license_document.webp new file mode 100644 index 00000000..58dbd6ab Binary files /dev/null and b/resources/images/prizes/3d_license_document.webp differ diff --git a/resources/images/prizes/3d_purple_tickets.webp b/resources/images/prizes/3d_purple_tickets.webp new file mode 100644 index 00000000..f4c5b3ef Binary files /dev/null and b/resources/images/prizes/3d_purple_tickets.webp differ diff --git a/resources/images/prizes/bronze_medal.webp b/resources/images/prizes/bronze_medal.webp new file mode 100644 index 00000000..5b27ce9c Binary files /dev/null and b/resources/images/prizes/bronze_medal.webp differ diff --git a/resources/images/prizes/gold_medal.webp b/resources/images/prizes/gold_medal.webp new file mode 100644 index 00000000..2f7b2af1 Binary files /dev/null and b/resources/images/prizes/gold_medal.webp differ diff --git a/resources/images/prizes/nativephp_black_shirt.webp b/resources/images/prizes/nativephp_black_shirt.webp new file mode 100644 index 00000000..ee2e43be Binary files /dev/null and b/resources/images/prizes/nativephp_black_shirt.webp differ diff --git a/resources/images/prizes/silver_medal.webp b/resources/images/prizes/silver_medal.webp new file mode 100644 index 00000000..59c0a67e Binary files /dev/null and b/resources/images/prizes/silver_medal.webp differ diff --git a/resources/images/simon2025laraconeu.webp b/resources/images/simon2025laraconeu.webp new file mode 100644 index 00000000..7ee30aba Binary files /dev/null and b/resources/images/simon2025laraconeu.webp differ diff --git a/resources/images/simonhamp_faded.webp b/resources/images/simonhamp_faded.webp new file mode 100644 index 00000000..90900d0b Binary files /dev/null and b/resources/images/simonhamp_faded.webp differ diff --git a/resources/images/the-vibes/crowd-event.webp b/resources/images/the-vibes/crowd-event.webp new file mode 100644 index 00000000..3119f7aa Binary files /dev/null and b/resources/images/the-vibes/crowd-event.webp differ diff --git a/resources/images/the-vibes/hero-event.webp b/resources/images/the-vibes/hero-event.webp new file mode 100644 index 00000000..e5f79c64 Binary files /dev/null and b/resources/images/the-vibes/hero-event.webp differ diff --git a/resources/images/the-vibes/what-is-vibes.webp b/resources/images/the-vibes/what-is-vibes.webp new file mode 100644 index 00000000..03d3dfcb Binary files /dev/null and b/resources/images/the-vibes/what-is-vibes.webp differ diff --git a/resources/js/alpine/copyMarkdown.js b/resources/js/alpine/copyMarkdown.js new file mode 100644 index 00000000..308dc8e2 --- /dev/null +++ b/resources/js/alpine/copyMarkdown.js @@ -0,0 +1,34 @@ +export default () => ({ + showMessage: false, + + async copyMarkdownToClipboard() { + try { + // Get the current page URL and convert it to .md URL + const currentUrl = window.location.href + const mdUrl = currentUrl.replace( + /\/docs\/([^\/]+\/[^\/]+\/.*)$/, + '/docs/$1.md', + ) + + // Fetch the raw markdown content + const response = await fetch(mdUrl) + if (!response.ok) { + throw new Error('Failed to fetch markdown content') + } + + const markdownContent = await response.text() + + // Copy to clipboard + await navigator.clipboard.writeText(markdownContent) + + // Show success message + this.showMessage = true + setTimeout(() => { + this.showMessage = false + }, 2000) + } catch (error) { + console.error('Failed to copy markdown:', error) + // Could show an error message here if needed + } + }, +}) diff --git a/resources/js/app.js b/resources/js/app.js index 8781af5c..a1a7363b 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,8 +1,153 @@ -import './bootstrap'; -import Alpine from 'alpinejs' -import codeBlock from "./alpine/codeBlock.js"; +import './fonts' +import './bootstrap' +import 'number-flow' +import { gsap } from 'gsap' +import { + Livewire, + Alpine, +} from '../../vendor/livewire/livewire/dist/livewire.esm.js' +import codeBlock from './alpine/codeBlock.js' +import copyMarkdown from './alpine/copyMarkdown.js' +import docsearch from '@docsearch/js' +import Atropos from 'atropos' +import '@docsearch/css' +import 'atropos/css' -window.Alpine = Alpine; +import.meta.glob(['../images/**', '../svg/**']) +import { + animate, + hover, + inView, + easeIn, + easeOut, + easeInOut, + backIn, + backOut, + backInOut, + circIn, + circOut, + circInOut, + anticipate, + spring, + stagger, + cubicBezier, +} from 'motion' +// Motion +window.motion = { + animate: animate, + hover: hover, + inView: inView, + easeIn: easeIn, + easeOut: easeOut, + easeInOut: easeInOut, + backOut: backOut, + backIn: backIn, + backInOut: backInOut, + circIn: circIn, + circOut: circOut, + circInOut: circInOut, + anticipate: anticipate, + spring: spring, + stagger: stagger, + cubicBezier: cubicBezier, +} + +// Atropos +window.Atropos = Atropos + +// GSAP +window.gsap = gsap + +// Alpine Alpine.data('codeBlock', codeBlock) -Alpine.start() +Alpine.data('copyMarkdown', copyMarkdown) +Alpine.magic('refAll', (el) => { + return (refName) => { + return Array.from(el.querySelectorAll(`[x-ref="${refName}"]`)) + } +}) +Alpine.data('countdown', (iso) => ({ + flows: {}, + init() { + // Parse target date from ISO string and ensure it's treated as a specific point in time + this.targetDate = new Date(iso).getTime() + + // refs to the number-flow elements + this.flows = { + dd: this.$refs.dd, // days + hh: this.$refs.hh, // hours + mm: this.$refs.mm, // minutes + ss: this.$refs.ss, // seconds + } + + // limit the rolling wheels so 59 ➜ 00 animates smoothly + this.flows.hh.digits = { 1: { max: 2 }, 0: { max: 9 } } // hours 0-23 + this.flows.mm.digits = { 1: { max: 5 }, 0: { max: 9 } } // minutes 0-59 + this.flows.ss.digits = { 1: { max: 5 }, 0: { max: 9 } } // seconds 0-59 + + this.tick() // draw immediately + this.timer = setInterval(() => this.tick(), 1_000) + }, + tick() { + const now = Date.now() + const diff = Math.max(0, this.targetDate - now) + + if (diff === 0) clearInterval(this.timer) // stop at zero + + // Calculate days, hours, minutes, and seconds properly + const days = Math.floor(diff / (24 * 3600 * 1000)) + const hours = Math.floor((diff % (24 * 3600 * 1000)) / (3600 * 1000)) + const minutes = Math.floor((diff % (3600 * 1000)) / (60 * 1000)) + const seconds = Math.floor((diff % (60 * 1000)) / 1000) + + this.flows.dd.update(days) + this.flows.hh.update(hours) + this.flows.mm.update(minutes) + this.flows.ss.update(seconds) + }, + destroy() { + clearInterval(this.timer) + }, // tidy up +})) + +Livewire.start() + +// Docsearch +const docsPathMatch = window.location.pathname.match(/^\/docs\/(desktop|mobile)\/(\d+)/) +const docsearchOptions = { + appId: 'ZNII9QZ8WI', + apiKey: '9be495a1aaf367b47c873d30a8e7ccf5', + indexName: 'nativephp', + insights: true, + debug: false, + ...(docsPathMatch && { + transformItems(items) { + const prefix = `/docs/${docsPathMatch[1]}/${docsPathMatch[2]}/` + return items.filter((item) => { + try { + return new URL(item.url).pathname.startsWith(prefix) + } catch { + return item.url.includes(prefix) + } + }) + }, + }), +} + +docsearch({ + ...docsearchOptions, + container: '#docsearch-desktop', +}) + +// Mirror the desktop DocSearch button into the mobile container so that +// pressing Cmd+K only registers one handler (avoiding duplicate modals). +const mobileContainer = document.getElementById('docsearch-mobile') +if (mobileContainer) { + const desktopButton = document.querySelector('#docsearch-desktop .DocSearch-Button') + if (desktopButton) { + const mobileButton = desktopButton.cloneNode(true) + mobileContainer.appendChild(mobileButton) + mobileButton.addEventListener('click', () => desktopButton.click()) + } +} diff --git a/resources/js/fonts.js b/resources/js/fonts.js new file mode 100644 index 00000000..6b6da022 --- /dev/null +++ b/resources/js/fonts.js @@ -0,0 +1 @@ +import '@fontsource/poppins/latin.css' diff --git a/resources/views/account/auth/login.blade.php b/resources/views/account/auth/login.blade.php new file mode 100644 index 00000000..aa8ea0fc --- /dev/null +++ b/resources/views/account/auth/login.blade.php @@ -0,0 +1,88 @@ + +
+
+ +
+
+

+ Sign in to your account +

+

+ Or + + create a new account + +

+
+
+ @csrf + + @error('email') +
+
+ + + +

{{ $message }}

+
+
+ @enderror +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+ + +
+ +
+ +
+
+
+
+
diff --git a/resources/views/account/index.blade.php b/resources/views/account/index.blade.php new file mode 100644 index 00000000..bbe18dfe --- /dev/null +++ b/resources/views/account/index.blade.php @@ -0,0 +1,52 @@ + + {{-- Support Grid Section --}} +
+ {{-- Header --}} +
+

Account

+

+ Manage your NativePHP Account.
+ Not {{ auth()->user()->name }}? Logout. +

+
+ + {{-- Support Grid --}} + + + {{-- Additional Support Information --}} +
+

Need more help?

+

+ Check out our documentation for comprehensive guides and tutorials to help you get the most out of NativePHP. +

+
+
+
diff --git a/resources/views/article.blade.php b/resources/views/article.blade.php new file mode 100644 index 00000000..071f8798 --- /dev/null +++ b/resources/views/article.blade.php @@ -0,0 +1,114 @@ + + {{-- Hero --}} +
+
+ {{-- Blurred circle - Decorative --}} + + + {{-- Back button --}} + + + {{-- Primary Heading --}} +

+ {{ $article->title }} +

+ + {{-- Date --}} +
+
+
+ + {{-- Divider --}} + + +
+ {{-- Content --}} +
+ {!! App\Support\CommonMark\CommonMark::convertToHtml($article->content) !!} +
+ + {{-- Sidebar --}} + +
+ + {{-- Mobile ad & partner card --}} +
+ + +
+
+
diff --git a/resources/views/auth/forgot-password.blade.php b/resources/views/auth/forgot-password.blade.php new file mode 100644 index 00000000..71b93f5e --- /dev/null +++ b/resources/views/auth/forgot-password.blade.php @@ -0,0 +1,28 @@ + + Reset your password + + Enter your email address and we'll send you a link to reset your password. + + + @if (session('status')) + + {{ session('status') }} + + @endif + +
+ @csrf + + + Email address + + + + + Send Password Reset Link + +
+ Back to login +
+
+
diff --git a/resources/views/auth/login.blade.php b/resources/views/auth/login.blade.php new file mode 100644 index 00000000..b7da3cad --- /dev/null +++ b/resources/views/auth/login.blade.php @@ -0,0 +1,53 @@ + + Sign in to your account + + Or create a new account + + + @if (session('status')) + + {{ session('status') }} + + @endif + + @if (session('message')) + + {{ session('message') }} + + @endif + +
+ @csrf + +
+ + Email address + + + + + + Password + + + + +
+ + + Forgot your password? +
+
+ + Sign in +
+ + + + + + Sign in with GitHub + +
diff --git a/resources/views/auth/register.blade.php b/resources/views/auth/register.blade.php new file mode 100644 index 00000000..6f77e866 --- /dev/null +++ b/resources/views/auth/register.blade.php @@ -0,0 +1,60 @@ + + Create your account + + Already have an account? Sign in + + +
+ @csrf + +
+ + Name + + + + + + Email address + + + + + + Password + + + + + + Confirm Password + + +
+ + Create account + + + By creating an account, you agree to our + Terms of Service + and + Privacy Policy. + +
+ + + + + + Sign up with GitHub + + + + By signing up with GitHub, you agree to our + Terms of Service + and + Privacy Policy. + +
diff --git a/resources/views/auth/reset-password.blade.php b/resources/views/auth/reset-password.blade.php new file mode 100644 index 00000000..fa37f6a9 --- /dev/null +++ b/resources/views/auth/reset-password.blade.php @@ -0,0 +1,30 @@ + + Set new password + +
+ @csrf + + + +
+ + Email address + + + + + + New Password + + + + + + Confirm New Password + + +
+ + Reset Password +
+
diff --git a/resources/views/blog.blade.php b/resources/views/blog.blade.php new file mode 100644 index 00000000..4bc25240 --- /dev/null +++ b/resources/views/blog.blade.php @@ -0,0 +1,200 @@ + +
+ {{-- Hero --}} +
+ {{-- Primary Heading --}} +

+ Blog +

+ {{-- Introduction Description --}} +

+ Welcome to our blog! Here, we share insights, updates, and + stories from our community. Stay tuned for the latest news and + articles. +

+
+ + {{-- Divider --}} + + + {{-- Articles --}} +
+ {{-- Semantic heading for section (visually hidden) --}} +

+ Blog Articles +

+ {{-- Main --}} +
+ {{-- List --}} +
+ @foreach ($articles as $article) + + {{ $article->excerpt }} + + @endforeach +
+ + {{-- Sidebar --}} + +
+ + {{-- Mobile ad & partner card --}} +
+ + +
+ {{-- Pagination --}} + +
+
+
diff --git a/resources/views/brand.blade.php b/resources/views/brand.blade.php new file mode 100644 index 00000000..86088974 --- /dev/null +++ b/resources/views/brand.blade.php @@ -0,0 +1,204 @@ + + {{-- Hero Section --}} +
+ {{-- Decorative dashed grid background (pure CSS) --}} + + + {{-- Grid --}} + + + {{-- Header --}} +
+ {{-- Primary Heading --}} +

+ NativePHP brand assets +

+ + {{-- Introduction Description --}} +

+ This page provides assets and rules for using + + NativePHP + + visuals in articles, videos, open source projects, and community + content. + +
+
+ + Our name and logo are trademarks of + + + Bifrost Technology. + + Please use them respectfully and only in ways that reflect + + NativePHP + + accurately. +

+
+ + {{-- List --}} +
+ @php + $assets = [ + [ + 'src' => '/brand-assets/logo/nativephp-for-light-background.svg', + 'alt' => 'NativePHP logo', + 'download' => '/brand-assets/logo/nativephp-for-light-background.svg', + 'height' => 'h-8', + 'isDarkSurface' => false, + ], + [ + 'src' => '/brand-assets/logo/nativephp-for-dark-background.svg', + 'alt' => 'NativePHP logo', + 'download' => '/brand-assets/logo/nativephp-for-dark-background.svg', + 'height' => 'h-8', + 'isDarkSurface' => true, + ], + [ + 'src' => '/brand-assets/mobile/nativephp-mobile-for-light-background.svg', + 'alt' => 'NativePHP For Mobile logo', + 'download' => '/brand-assets/mobile/nativephp-mobile-for-light-background.svg', + 'height' => 'h-16', + 'isDarkSurface' => false, + ], + [ + 'src' => '/brand-assets/mobile/nativephp-mobile-for-dark-background.svg', + 'alt' => 'NativePHP For Mobile logo', + 'download' => '/brand-assets/mobile/nativephp-mobile-for-dark-background.svg', + 'height' => 'h-16', + 'isDarkSurface' => true, + ], + [ + 'src' => '/brand-assets/mobile/nativephp-mobile-in-grayscale.svg', + 'alt' => 'NativePHP For Mobile logo', + 'download' => '/brand-assets/mobile/nativephp-mobile-in-grayscale.svg', + 'height' => 'h-16', + 'isDarkSurface' => true, + ], + [ + 'src' => '/brand-assets/desktop/nativephp-desktop-for-light-background.svg', + 'alt' => 'NativePHP For Desktop logo', + 'download' => '/brand-assets/desktop/nativephp-desktop-for-light-background.svg', + 'height' => 'h-16', + 'isDarkSurface' => false, + ], + [ + 'src' => '/brand-assets/desktop/nativephp-desktop-for-dark-background.svg', + 'alt' => 'NativePHP For Desktop logo', + 'download' => '/brand-assets/desktop/nativephp-desktop-for-dark-background.svg', + 'height' => 'h-16', + 'isDarkSurface' => true, + ], + [ + 'src' => '/brand-assets/desktop/nativephp-desktop-in-grayscale.svg', + 'alt' => 'NativePHP For Desktop logo', + 'download' => '/brand-assets/desktop/nativephp-desktop-in-grayscale.svg', + 'height' => 'h-16', + 'isDarkSurface' => true, + ], + ]; + @endphp + + @foreach ($assets as $asset) + + @endforeach +
+
+
diff --git a/resources/views/build-my-app.blade.php b/resources/views/build-my-app.blade.php new file mode 100644 index 00000000..8f050af4 --- /dev/null +++ b/resources/views/build-my-app.blade.php @@ -0,0 +1,30 @@ + + @push('head') + @if (config('services.turnstile.site_key')) + + @endif + @endpush + +
+
+
+

+ { + Build My App + } +

+ +

+ Need help bringing your app idea to life?
+ Tell us about your project! We'd love to help. +

+
+
+ +
+
+ +
+
+
+
diff --git a/resources/views/bundle-show.blade.php b/resources/views/bundle-show.blade.php new file mode 100644 index 00000000..8d17d0c2 --- /dev/null +++ b/resources/views/bundle-show.blade.php @@ -0,0 +1,216 @@ + +
+
+ {{-- Blurred circle - Decorative --}} + + + {{-- Back button --}} + + + {{-- Bundle icon and title --}} +
+ @if ($bundle->hasLogo()) + {{ $bundle->name }} logo + @else +
+ + + +
+ @endif +
+ + Bundle + +

+ {{ $bundle->name }} +

+

+ {{ $bundle->plugins->count() }} plugins included +

+
+
+
+ + {{-- Divider --}} + + +
+ {{-- Main content - Description and Plugins --}} +
+ {{-- Description --}} + @if ($bundle->description) +
+

About this Bundle

+

{{ $bundle->description }}

+
+ @endif + + {{-- Included Plugins --}} +
+

+ Included Plugins +

+

+ Purchase this bundle to get access to all {{ $bundle->plugins->count() }} plugins. +

+
+ @foreach ($bundle->plugins as $plugin) + + @endforeach +
+
+
+ + {{-- Sidebar - Purchase Box --}} + +
+
+
diff --git a/resources/views/cart/show.blade.php b/resources/views/cart/show.blade.php new file mode 100644 index 00000000..b76e116b --- /dev/null +++ b/resources/views/cart/show.blade.php @@ -0,0 +1,378 @@ + +
+ {{-- Header --}} +
+ + + + + Back to Plugin Marketplace + +

Your Cart

+
+ + {{-- Flash Messages --}} + @if (session('success')) +
+

{!! session('success') !!}

+
+ @endif + + @if (session('error')) +
+

{{ session('error') }}

+
+ @endif + + @if (session('info')) +
+

{{ session('info') }}

+
+ @endif + + {{-- Price Change Notifications --}} + @if (count($priceChanges) > 0) +
+

Price Updates

+
    + @foreach ($priceChanges as $change) + @if ($change['type'] === 'price_changed') +
  • {{ $change['name'] }}: Price changed from ${{ number_format($change['old_price'] / 100) }} to ${{ number_format($change['new_price'] / 100) }}
  • + @else +
  • {{ $change['name'] }}: No longer available and was removed from your cart
  • + @endif + @endforeach +
+
+ @endif + + @if ($cart->isEmpty()) + {{-- Empty Cart --}} +
+ + + +

Your cart is empty

+

Browse our plugin marketplace to find plugins for your app.

+ + Browse Plugins + +
+ @else +
+ {{-- Cart Items --}} +
+
+
    + @foreach ($cart->items as $item) + @if ($item->isProduct()) + {{-- Product Item --}} +
  • product_id) + x-data="{ highlight: true }" + x-init="setTimeout(() => highlight = false, 3000)" + :class="highlight ? 'bg-purple-50 dark:bg-purple-900/20 ring-2 ring-purple-500 ring-inset' : ''" + class="flex gap-4 p-6 transition-all duration-1000" + @else + class="flex gap-4 p-6" + @endif + > + {{-- Product Logo --}} +
    + @if ($item->product->logo_path) + {{ $item->product->name }} + @else +
    + +
    + @endif +
    + + {{-- Product Details --}} +
    +
    +
    +
    +

    + + {{ $item->product->name }} + +

    + + Product + +
    + @if ($item->product->github_repo) +

    + Includes nativephp/{{ $item->product->github_repo }} access +

    + @endif +
    +

    + {{ $item->getFormattedPrice() }} +

    +
    + +
    +
    + @csrf + @method('DELETE') + +
    +
    +
    +
  • + @elseif ($item->isBundle()) + {{-- Bundle Item --}} +
  • plugin_bundle_id) + x-data="{ highlight: true }" + x-init="setTimeout(() => highlight = false, 3000)" + :class="highlight ? 'bg-amber-50 dark:bg-amber-900/20 ring-2 ring-amber-500 ring-inset' : ''" + class="flex gap-4 p-6 transition-all duration-1000" + @else + class="flex gap-4 p-6" + @endif + > + {{-- Bundle Logo --}} +
    + @if ($item->pluginBundle->hasLogo()) + {{ $item->pluginBundle->name }} + @else +
    + + + +
    + @endif +
    + + {{-- Bundle Details --}} +
    +
    +
    + +

    + {{ $item->pluginBundle->plugins->count() }} plugins included +

    + @php + $cartBundleDiscount = $item->pluginBundle->getDiscountPercentForUser(auth()->user()); + $cartBundleSavings = $item->pluginBundle->getFormattedSavingsForUser(auth()->user()); + @endphp + @if ($cartBundleDiscount > 0) +

    + Save {{ $cartBundleDiscount }}% ({{ $cartBundleSavings }}) +

    + @endif +
    +

    + {{ $item->getFormattedPrice() }} +

    +
    + +
    +
    + @csrf + @method('DELETE') + +
    +
    +
    +
  • + @else + {{-- Plugin Item --}} +
  • plugin_id) + x-data="{ highlight: true }" + x-init="setTimeout(() => highlight = false, 3000)" + :class="highlight ? 'bg-green-50 dark:bg-green-900/20 ring-2 ring-green-500 ring-inset' : ''" + class="flex gap-4 p-6 transition-all duration-1000" + @else + class="flex gap-4 p-6" + @endif + > + {{-- Plugin Logo --}} +
    + @if ($item->plugin->hasLogo()) + {{ $item->plugin->name }} + @elseif ($item->plugin->hasGradientIcon()) +
    + +
    + @else +
    + +
    + @endif +
    + + {{-- Plugin Details --}} +
    +
    +
    +

    + + {{ $item->plugin->name }} + +

    +

    + by {{ $item->plugin->user->display_name }} +

    +
    +
    +

    + {{ $item->getFormattedPrice() }} +

    + @if ($item->plugin->isOfficial() && auth()->user()?->hasUltraAccess()) + Included with Ultra + @endif +
    +
    + +
    +
    + @csrf + @method('DELETE') + +
    +
    +
    +
  • + @endif + @endforeach +
+
+ + {{-- Clear Cart --}} +
+
+ @csrf + @method('DELETE') + +
+
+
+ + {{-- Order Summary --}} +
+
+

Order Summary

+ +
+
+
Subtotal ({{ $cart->itemCount() }} {{ Str::plural('item', $cart->itemCount()) }})
+
{{ $cart->getFormattedSubtotal() }}
+
+
+ +
+
+
Total
+
{{ $cart->getFormattedSubtotal() }}
+
+
+ +
+ @csrf + +
+ + @guest +

+ You'll need to log in or create an account to complete your purchase. +

+ @endguest + +

+ Secure checkout powered by Stripe +

+
+
+
+ + @endif + + {{-- Available Bundles --}} + @if ($bundleUpgrades->isNotEmpty()) +
+

+ @if ($showingRandomBundles) + Check Out Our Bundles + @else + Available Bundles + @endif +

+
+
+ @foreach ($bundleUpgrades as $bundle) +
+
+ @if ($bundle->hasLogo()) + {{ $bundle->name }} + @else +
+ + + +
+ @endif +
+

+ {{ $bundle->name }} +

+

+ {{ $bundle->plugins->count() }} plugins +

+ @php + $upgradeBundleDiscount = $bundle->getDiscountPercentForUser(auth()->user()); + $upgradeBundlePrice = $bundle->getFormattedPriceForUser(auth()->user()); + @endphp + @if ($upgradeBundleDiscount > 0) +

+ Save {{ $upgradeBundleDiscount }}% +

+ @endif +
+
+
+ + {{ $upgradeBundlePrice }} + + + View bundle → + +
+
+ @endforeach +
+ @if ($bundleUpgrades->count() > 1) +
+ @endif +
+
+ @endif +
+
diff --git a/resources/views/cart/success.blade.php b/resources/views/cart/success.blade.php new file mode 100644 index 00000000..9dc35fdf --- /dev/null +++ b/resources/views/cart/success.blade.php @@ -0,0 +1,331 @@ + +
+ @if ($isFreeCheckout ?? false) + {{-- Free Checkout: Immediate success, no polling --}} +
+
+ + + +
+ +

Plugins Added!

+

+ Your plugins are ready to use. +

+ + @if ($cart && $cart->items->isNotEmpty()) +
+

Plugins

+
+ @foreach ($cart->items as $item) + @if ($item->plugin) +
+
+
+ +
+
+ {{ $item->plugin->name }} + @if ($item->plugin->isOfficial() && auth()->user()?->hasUltraAccess()) +

Included with Ultra

+ @endif +
+
+ + View Plugin + +
+ @endif + + @if ($item->isBundle() && $item->pluginBundle) + @foreach ($item->pluginBundle->plugins as $plugin) +
+
+
+ +
+ {{ $plugin->name }} +
+
+ @endforeach + @endif + @endforeach +
+
+ @endif + + {{-- Plugin Installation --}} +
+

Plugin Installation

+
    +
  • + + + + Configure your Composer credentials in your project +
  • +
  • + + + + Run composer require [package-name] +
  • +
  • + + + + Follow the plugin's installation instructions +
  • +
+
+ + {{-- Actions --}} + +
+ @else + {{-- Paid Checkout: Alpine.js polling for Stripe webhook --}} +
+ {{-- Loading State --}} + + + {{-- Success State --}} + + + {{-- Timeout/Error State --}} + +
+ @endif +
+
diff --git a/resources/views/components/alert-beta.blade.php b/resources/views/components/alert-beta.blade.php deleted file mode 100644 index 99d91a9a..00000000 --- a/resources/views/components/alert-beta.blade.php +++ /dev/null @@ -1,26 +0,0 @@ -
class(['rounded-lg flex items-center p-3 mt-8 space-x-6 border - text-orange-800 border-orange-300 bg-orange-50 - dark:text-orange-100 dark:bg-orange-900/20 dark:border-orange-900']) }}> - -
-

- NativePHP is currently in - - beta - -

- - - Let's get to v1! - - -
-
diff --git a/resources/views/components/alert.blade.php b/resources/views/components/alert.blade.php deleted file mode 100644 index 91ff09be..00000000 --- a/resources/views/components/alert.blade.php +++ /dev/null @@ -1,6 +0,0 @@ - diff --git a/resources/views/components/benefit-card.blade.php b/resources/views/components/benefit-card.blade.php new file mode 100644 index 00000000..85fe2b68 --- /dev/null +++ b/resources/views/components/benefit-card.blade.php @@ -0,0 +1,17 @@ +
+
+
+ {{ $icon }} +
+
+
+

{{ $title }}

+

+ {{ $description }} +

+
+
diff --git a/resources/views/components/bifrost-banner.blade.php b/resources/views/components/bifrost-banner.blade.php new file mode 100644 index 00000000..d809f74c --- /dev/null +++ b/resources/views/components/bifrost-banner.blade.php @@ -0,0 +1,119 @@ + + {{-- Left side decorations --}} + + + {{-- Right side decorations --}} + + + {{-- Bifrost --}} +
+ +
+ + {{-- Label --}} +
+ {{-- Text --}} +
+ +
+ Build your NativePHP apps in the cloud. Plans from $19/month + a FREE Mobile Mini license +
+
+ + {{-- Arrow --}} + +
+ + {{-- Left blur --}} +
+
+
+ + {{-- Right blur --}} +
+
+
+
diff --git a/resources/views/components/bifrost-button.blade.php b/resources/views/components/bifrost-button.blade.php new file mode 100644 index 00000000..86cb001a --- /dev/null +++ b/resources/views/components/bifrost-button.blade.php @@ -0,0 +1,89 @@ +@props([ + 'small' => false, +]) + + $small, + + 'px-6 py-3' => ! $small, + ]) +> + + + + + + Try Bifrost! + + + Build + + + Distribute + + + {{ $small ? 'Ship' : 'Ship it!' }} + + + diff --git a/resources/views/components/bifrost-product-hunt-banner.blade.php b/resources/views/components/bifrost-product-hunt-banner.blade.php new file mode 100644 index 00000000..52e6e4b4 --- /dev/null +++ b/resources/views/components/bifrost-product-hunt-banner.blade.php @@ -0,0 +1,176 @@ + + {{-- Decorative left arrows --}} + + + {{-- Text: part 1 --}} + + Bifrost is on + + + {{-- Product Hunt badge --}} + + + + Product Hunt + + + + {{-- Text: part 2 --}} + + Please give us an upvote! + + + {{-- Decorative right arrows --}} + + + {{-- Decorative blurs --}} + + + diff --git a/resources/views/components/black-friday-banner.blade.php b/resources/views/components/black-friday-banner.blade.php new file mode 100644 index 00000000..7f41cb1a --- /dev/null +++ b/resources/views/components/black-friday-banner.blade.php @@ -0,0 +1,95 @@ + + {{-- Label Section --}} +
+ {{-- Icon --}} + + + {{-- Text --}} +
+ +
+ Black Friday: 40% off Bifrost Hela & Thor plans • Code: BLACKFRIDAY40 +
+
+
+ + {{-- Countdown Timer --}} +
+ Ends in: +
+ + d +
+
+ + h +
+
+ + m +
+
+ + s +
+
+ + {{-- Icon --}} + +
\ No newline at end of file diff --git a/resources/views/components/blog/ad-rotation.blade.php b/resources/views/components/blog/ad-rotation.blade.php new file mode 100644 index 00000000..84ea4e49 --- /dev/null +++ b/resources/views/components/blog/ad-rotation.blade.php @@ -0,0 +1,259 @@ +@props(['ads' => ['mobile', 'devkit', 'ultra', 'vibes', 'masterclass']]) + +@php + $adsJson = json_encode($ads); +@endphp + +
+ {{-- NativePHP Mobile Ad --}} + @if (in_array('mobile', $ads)) + + {{-- Logo --}} +
+ + NativePHP +
+ + {{-- Tagline --}} +
+ Bring your + Laravel + skills to + mobile apps. +
+ + {{-- Iphone --}} +
+ +
+ + {{-- Star 1 --}} + + {{-- Star 2 --}} + + {{-- Star 3 --}} + + {{-- White blur --}} +
+
+
+ {{-- Sky blur --}} +
+
+
+ {{-- Violet blur --}} +
+
+
+
+ @endif + + {{-- Plugin Dev Kit Ad --}} + @if (in_array('devkit', $ads)) + + {{-- Icon --}} +
+ +
+ + {{-- Title --}} +
+ Plugin Dev Kit +
+ + {{-- Tagline --}} +
+ Build native plugins with + Claude Code +
+ + {{-- CTA --}} +
+ Learn More +
+ + {{-- Decorative stars --}} + + + +
+ @endif + + {{-- Ultra Ad --}} + @if (in_array('ultra', $ads)) + + {{-- Icon --}} +
+ +
+ + {{-- Title --}} +
+ NativePHP Ultra +
+ + {{-- Tagline --}} +
+ All NativePHP plugins, teams & priority support from + ${{ config('subscriptions.plans.max.price_monthly') }}/mo +
+ + {{-- CTA --}} +
+ Learn More +
+ + {{-- Decorative stars --}} + + + +
+ @endif + + {{-- The Vibes Ad --}} + @if (in_array('vibes', $ads)) + + {{-- Background image --}} + + + {{-- Title --}} +
+ The Vibes +
+ + {{-- Tagline --}} +
+ The unofficial Laracon US + Day 3 +
+ + {{-- CTA --}} +
+ Grab Your Spot +
+ + {{-- Scarcity Label --}} +
+ Only 100 tickets! +
+ + {{-- Decorative stars --}} + + +
+ @endif + + {{-- Masterclass Ad --}} + @if (in_array('masterclass', $ads)) + + {{-- Icon --}} +
+ +
+ + {{-- Title --}} +
+ The Masterclass +
+ + {{-- Tagline --}} +
+ Go from zero to + published app +
+ in no time +
+ + {{-- CTA --}} +
+ Learn More +
+ + {{-- Early Bird Label --}} +
+ Early Bird Pricing +
+ + {{-- Decorative stars --}} + + + +
+ @endif +
diff --git a/resources/views/components/blog/article-card.blade.php b/resources/views/components/blog/article-card.blade.php new file mode 100644 index 00000000..555a2396 --- /dev/null +++ b/resources/views/components/blog/article-card.blade.php @@ -0,0 +1,60 @@ +@props([ + 'title' => '', + 'url' => '#', + 'date' => null, +]) + + +
+ {{-- Header --}} +
+
+ {{-- Title --}} +

+ {{ $title }} +

+ {{-- Date --}} + @if ($date) + @php + $dateObject = \Carbon\Carbon::parse($date); + $formattedDate = $dateObject->format('F j, Y'); + @endphp + + + @endif +
+ + {{-- Arrow --}} +
+ +
+ {{-- Content --}} +

+ {{ $slot }} +

+
+ + {{-- Blur decoration --}} +
+
+
diff --git a/resources/views/components/blog/sidebar.blade.php b/resources/views/components/blog/sidebar.blade.php new file mode 100644 index 00000000..9fc18be5 --- /dev/null +++ b/resources/views/components/blog/sidebar.blade.php @@ -0,0 +1,22 @@ + diff --git a/resources/views/components/brand/asset-card.blade.php b/resources/views/components/brand/asset-card.blade.php new file mode 100644 index 00000000..f95400b4 --- /dev/null +++ b/resources/views/components/brand/asset-card.blade.php @@ -0,0 +1,43 @@ +@props([ + 'src' => '', + 'alt' => '', + 'downloadHref' => null, + 'height' => 'h-8', + 'isDarkSurface' => false, +]) + +@php + $downloadHref = $downloadHref ?? $src; + $containerClasses = [ + 'grid h-50 w-full place-items-center rounded-xl p-5 ring-1', + $isDarkSurface ? 'bg-gray-900 ring-gray-800' : 'bg-gray-100 ring-gray-300', + ]; +@endphp + +
+ {{-- Asset --}} +
+ {{ $alt }} +
+ + {{-- Download button --}} + + +
Download
+
+
diff --git a/resources/views/components/bundle-card.blade.php b/resources/views/components/bundle-card.blade.php new file mode 100644 index 00000000..2a121ada --- /dev/null +++ b/resources/views/components/bundle-card.blade.php @@ -0,0 +1,66 @@ +@props(['bundle']) + +@php + $user = auth()->user(); + $formattedPrice = $bundle->getFormattedPriceForUser($user); + $discountPercent = $bundle->getDiscountPercentForUser($user); +@endphp + + +
+ @if ($bundle->hasLogo()) + {{ $bundle->name }} logo + @else +
+ + + +
+ @endif + + + Bundle + +
+ +
+

+ {{ $bundle->name }} +

+ @if ($bundle->description) +

+ {{ $bundle->description }} +

+ @endif + +

+ {{ $bundle->plugins->count() }} plugins included +

+
+ +
+
+ + {{ $formattedPrice }} + + @if ($discountPercent > 0) + + {{ $bundle->formatted_retail_value }} + + @endif +
+ + @if ($discountPercent > 0) + + Save {{ $discountPercent }}% + + @endif +
+
diff --git a/resources/views/components/comparison/bar-chart.blade.php b/resources/views/components/comparison/bar-chart.blade.php new file mode 100644 index 00000000..9d122425 --- /dev/null +++ b/resources/views/components/comparison/bar-chart.blade.php @@ -0,0 +1,65 @@ +@props([ + 'items' => [], + 'label' => '', + 'unit' => '', +]) + +
+ @if ($label) +
+ {{ $label }} +
+ @endif + +
+ @foreach ($items as $item) +
+
+ {{ $item['name'] }} +
+
+
+
+ {{ $item['value'] }}{{ $unit }} +
+
+
+ @endforeach +
+
diff --git a/resources/views/components/comparison/native-features.blade.php b/resources/views/components/comparison/native-features.blade.php new file mode 100644 index 00000000..a5e0bd51 --- /dev/null +++ b/resources/views/components/comparison/native-features.blade.php @@ -0,0 +1,227 @@ +{{-- Native Features Grid Component --}} +
+
+

+ Native Features Built In +

+

+ Access powerful device capabilities with simple PHP facades +

+
+ + + +

+ All accessible via simple PHP facades like + Biometrics::prompt() +

+
diff --git a/resources/views/components/comparison/video-placeholder.blade.php b/resources/views/components/comparison/video-placeholder.blade.php new file mode 100644 index 00000000..ec8c45ad --- /dev/null +++ b/resources/views/components/comparison/video-placeholder.blade.php @@ -0,0 +1,42 @@ +@props([ + 'title' => 'App Boot Demo', + 'description' => 'Video coming soon', +]) + +
merge(['class' => 'relative overflow-hidden rounded-2xl bg-gray-100 dark:bg-mirage']) }} +> +
+ {{-- Video slot for when videos are ready --}} + @if (isset($video)) + {{ $video }} + @else + {{-- Placeholder state --}} +
+ {{-- Play button icon --}} +
+ + + +
+
+
+ {{ $title }} +
+
+ {{ $description }} +
+
+
+ @endif +
+
diff --git a/resources/views/components/customer/empty-state.blade.php b/resources/views/components/customer/empty-state.blade.php new file mode 100644 index 00000000..5ad1eb6d --- /dev/null +++ b/resources/views/components/customer/empty-state.blade.php @@ -0,0 +1,14 @@ +@props(['icon', 'title', 'description']) + +
+ + + {{ $title }} + {{ $description }} + @if($slot->isNotEmpty()) +
+ {{ $slot }} +
+ @endif +
+
diff --git a/resources/views/components/customer/masked-key.blade.php b/resources/views/components/customer/masked-key.blade.php new file mode 100644 index 00000000..72c35a75 --- /dev/null +++ b/resources/views/components/customer/masked-key.blade.php @@ -0,0 +1,16 @@ +@props(['key-value']) + + + {{ Str::substr($keyValue, 0, 4) }}****{{ Str::substr($keyValue, -4) }} + + Copy + Copied! + + diff --git a/resources/views/components/customer/plugin-credentials.blade.php b/resources/views/components/customer/plugin-credentials.blade.php new file mode 100644 index 00000000..2a913656 --- /dev/null +++ b/resources/views/components/customer/plugin-credentials.blade.php @@ -0,0 +1,82 @@ +@props(['pluginLicenseKey']) + + +
+
+ Your Plugin Credentials + + Use these credentials with Composer to install plugins from the NativePHP Plugin Marketplace. + +
+ + + +
+ +
+ + How to configure Composer + +
+ 1. Add the NativePHP plugins repository: +
+
+ composer config repositories.nativephp-plugins composer https://plugins.nativephp.com +
+ +
+ 2. Configure your credentials: +
+
+ composer config http-basic.plugins.nativephp.com {{ auth()->user()->email }} {{ $pluginLicenseKey }} +
+ +
+
+
+
+ +{{-- Rotate Key Confirmation Modal --}} + +
+
+ Rotate Plugin License Key + + Are you sure you want to rotate your plugin license key? This action cannot be undone. + +
+ + + After rotating your key, you will need to: + +
    +
  • Update your auth.json file in all projects
  • +
  • Reconfigure Composer credentials on any CI/CD systems
  • +
  • Update any deployment scripts that reference the old key
  • +
+
+
+ +
+ + + Cancel + + Rotate Key +
+
+
diff --git a/resources/views/components/customer/status-badge.blade.php b/resources/views/components/customer/status-badge.blade.php new file mode 100644 index 00000000..b1b64467 --- /dev/null +++ b/resources/views/components/customer/status-badge.blade.php @@ -0,0 +1,15 @@ +@props(['status']) + +@php +$color = match($status) { + 'Active', 'Approved', 'Licensed' => 'green', + 'Expired', 'Pending', 'Pending Review', 'Open' => 'yellow', + 'Needs Renewal', 'In Progress' => 'blue', + 'Suspended', 'Rejected', 'Closed' => 'red', + 'Responded' => 'green', + 'On Hold' => 'zinc', + default => 'zinc', +}; +@endphp + +{{ $status }} diff --git a/resources/views/components/dashboard-card.blade.php b/resources/views/components/dashboard-card.blade.php new file mode 100644 index 00000000..b6319092 --- /dev/null +++ b/resources/views/components/dashboard-card.blade.php @@ -0,0 +1,70 @@ +@props([ + 'title', + 'href' => null, + 'linkText' => 'View', + 'icon' => null, + 'color' => 'blue', + 'count' => null, + 'value' => null, + 'description' => null, + 'badge' => null, + 'badgeColor' => 'green', + 'secondBadge' => null, + 'secondBadgeColor' => 'yellow', +]) + +@php + $colorClasses = [ + 'blue' => 'bg-blue-100 text-blue-600 dark:bg-blue-900/30 dark:text-blue-400', + 'green' => 'bg-green-100 text-green-600 dark:bg-green-900/30 dark:text-green-400', + 'yellow' => 'bg-yellow-100 text-yellow-600 dark:bg-yellow-900/30 dark:text-yellow-400', + 'purple' => 'bg-purple-100 text-purple-600 dark:bg-purple-900/30 dark:text-purple-400', + 'indigo' => 'bg-indigo-100 text-indigo-600 dark:bg-indigo-900/30 dark:text-indigo-400', + 'gray' => 'bg-gray-100 text-gray-600 dark:bg-gray-700 dark:text-gray-400', + ]; +@endphp + + +
+
+ @if($icon) +
+
+ +
+
+ @endif +
+ {{ $title }} +
+ @if($count !== null) + {{ $count }} + @elseif($value !== null) + {{ $value }} + @endif + @if($badge || $secondBadge) + + @if($badge) + {{ $badge }} + @endif + @if($secondBadge) + {{ $secondBadge }} + @endif + + @endif +
+ @if($description) + {{ $description }} + @endif +
+
+
+ @if($href) + + @endif +
diff --git a/resources/views/components/discounts-banner.blade.php b/resources/views/components/discounts-banner.blade.php new file mode 100644 index 00000000..0837b561 --- /dev/null +++ b/resources/views/components/discounts-banner.blade.php @@ -0,0 +1,27 @@ +@props(['inline' => false]) + +
!$inline])> +
+
+
+ + + +
+
+

+ Discounted Licenses! +

+

+ As a thank you for your support of NativePHP, you can get a Pro or Max license at a + significant discount. +

+ +
+
+
+
diff --git a/resources/views/components/divider.blade.php b/resources/views/components/divider.blade.php new file mode 100644 index 00000000..643648d6 --- /dev/null +++ b/resources/views/components/divider.blade.php @@ -0,0 +1,19 @@ + diff --git a/resources/views/components/docs-layout.blade.php b/resources/views/components/docs-layout.blade.php new file mode 100644 index 00000000..bac40dbe --- /dev/null +++ b/resources/views/components/docs-layout.blade.php @@ -0,0 +1,37 @@ + + {{-- Main container --}} +
+ {{-- Left sidebar --}} + @if (! empty($sidebarLeft)) + + {{ $sidebarLeft }} + + @endif + +
+ {{-- Content --}} +
+ {{-- Docs mobile menu --}} + + + + + {{-- Main content --}} +
{{ $slot }}
+ + {{-- Mobile partner card --}} + +
+ + {{-- Right sidebar --}} + @if (! empty($sidebarRight)) + + {{ $sidebarRight }} + + @endif +
+
+
diff --git a/resources/views/components/docs/alert-v1-announcement.blade.php b/resources/views/components/docs/alert-v1-announcement.blade.php new file mode 100644 index 00000000..523f5968 --- /dev/null +++ b/resources/views/components/docs/alert-v1-announcement.blade.php @@ -0,0 +1,271 @@ + diff --git a/resources/views/components/docs/copy-markdown-button.blade.php b/resources/views/components/docs/copy-markdown-button.blade.php new file mode 100644 index 00000000..80d30188 --- /dev/null +++ b/resources/views/components/docs/copy-markdown-button.blade.php @@ -0,0 +1,16 @@ +{{-- Copy as Markdown Button --}} +
+ +
diff --git a/resources/views/components/flex-list-of-links.blade.php b/resources/views/components/docs/flex-list-of-links.blade.php similarity index 100% rename from resources/views/components/flex-list-of-links.blade.php rename to resources/views/components/docs/flex-list-of-links.blade.php diff --git a/resources/views/components/docs/link-button.blade.php b/resources/views/components/docs/link-button.blade.php new file mode 100644 index 00000000..ac4f0d1c --- /dev/null +++ b/resources/views/components/docs/link-button.blade.php @@ -0,0 +1,6 @@ + + {{ $slot }} + diff --git a/resources/views/components/docs/link-subtle.blade.php b/resources/views/components/docs/link-subtle.blade.php new file mode 100644 index 00000000..2bf2a5c4 --- /dev/null +++ b/resources/views/components/docs/link-subtle.blade.php @@ -0,0 +1,6 @@ + + {{ $slot }} + diff --git a/resources/views/components/docs/menu.blade.php b/resources/views/components/docs/menu.blade.php new file mode 100644 index 00000000..085db611 --- /dev/null +++ b/resources/views/components/docs/menu.blade.php @@ -0,0 +1,39 @@ +@php + $isMobile = request()->is('docs/mobile/*'); +@endphp + +
+ {{-- Docs menu button --}} + + + {{-- Docs mobile menu --}} + +
diff --git a/resources/views/components/docs/old-version-notice.blade.php b/resources/views/components/docs/old-version-notice.blade.php new file mode 100644 index 00000000..818f164f --- /dev/null +++ b/resources/views/components/docs/old-version-notice.blade.php @@ -0,0 +1,59 @@ +@props(['platform', 'version', 'page']) + +@php + $latestVersion = config("docs.latest_versions.{$platform}"); + $isOldVersion = $latestVersion && (int) $version < (int) $latestVersion; + + if ($isOldVersion) { + $latestPagePath = resource_path("views/docs/{$platform}/{$latestVersion}/{$page}.md"); + + // Handle renamed paths (e.g., apis/* moved to plugins/core/*) + $remappedPage = $page; + if (str_starts_with($page, 'apis/')) { + $remappedPage = 'plugins/core/' . substr($page, 5); + $remappedPath = resource_path("views/docs/{$platform}/{$latestVersion}/{$remappedPage}.md"); + if (file_exists($remappedPath)) { + $latestPagePath = $remappedPath; + $page = $remappedPage; + } + } + + $targetPage = file_exists($latestPagePath) ? $page : 'getting-started/introduction'; + + $latestUrl = route('docs.show', [ + 'platform' => $platform, + 'version' => $latestVersion, + 'page' => $targetPage, + ]); + } +@endphp + +@if ($isOldVersion) + +@endif diff --git a/resources/views/components/docs/platform-switcher.blade.php b/resources/views/components/docs/platform-switcher.blade.php new file mode 100644 index 00000000..8e4f3d5d --- /dev/null +++ b/resources/views/components/docs/platform-switcher.blade.php @@ -0,0 +1,36 @@ +@php + $isMobile = request()->is('docs/mobile/*'); + $mobileHref = '/docs/mobile/3'; + $desktopHref = '/docs/desktop/2'; +@endphp + + +
+ @if ($isMobile) + + @else + + @endif +
+
You're reading the
+
+ {{ $isMobile ? 'Mobile' : 'Desktop' }} Documentation +
+
+
+ +
+
+ {{ $isMobile ? 'Mobile' : 'Desktop' }} + + {{ $isMobile ? 'Desktop' : 'Mobile' }} +
+
+
diff --git a/resources/views/components/separator.blade.php b/resources/views/components/docs/separator.blade.php similarity index 100% rename from resources/views/components/separator.blade.php rename to resources/views/components/docs/separator.blade.php diff --git a/resources/views/components/docs/sidebar-left-navigation.blade.php b/resources/views/components/docs/sidebar-left-navigation.blade.php new file mode 100644 index 00000000..e6724bd1 --- /dev/null +++ b/resources/views/components/docs/sidebar-left-navigation.blade.php @@ -0,0 +1,9 @@ + diff --git a/resources/views/components/docs/sidebar-right.blade.php b/resources/views/components/docs/sidebar-right.blade.php new file mode 100644 index 00000000..11c0b116 --- /dev/null +++ b/resources/views/components/docs/sidebar-right.blade.php @@ -0,0 +1,5 @@ + diff --git a/resources/views/components/docs/toc-and-sponsors.blade.php b/resources/views/components/docs/toc-and-sponsors.blade.php new file mode 100644 index 00000000..c71f896b --- /dev/null +++ b/resources/views/components/docs/toc-and-sponsors.blade.php @@ -0,0 +1,15 @@ +{{-- Copy as Markdown Button --}} + + +
+ {{ $slot }} + + {{-- Sponsors --}} +
+ +
+ + Become a Partner +
diff --git a/resources/views/components/early-access-button.blade.php b/resources/views/components/early-access-button.blade.php deleted file mode 100644 index 602f981f..00000000 --- a/resources/views/components/early-access-button.blade.php +++ /dev/null @@ -1,23 +0,0 @@ -class(['group -inline-block - text-sm bg-purple-500 border-purple-600 -dark:border-purple-500 dark:bg-purple-700 -hover:bg-purple-600 dark:hover:bg-purple-800 - px-4 py-1.5 - border rounded-md - font-medium - text-white - ']) }} href="{{route('early-adopter')}}"> -
- - - {{-- --}} - {{-- --}} - Soon on iOS! -
-
diff --git a/resources/views/components/faq-card.blade.php b/resources/views/components/faq-card.blade.php new file mode 100644 index 00000000..4d997084 --- /dev/null +++ b/resources/views/components/faq-card.blade.php @@ -0,0 +1,75 @@ +@props([ + 'question', +]) + +@php + global $count; +@endphp + +
+
+ {{-- Number --}} +
+ {{ str_pad(++$count, 2, 0, STR_PAD_LEFT) }} +
+ + {{-- Question --}} +
+ {{ $question }} +
+ + {{-- Arrow --}} +
+
+
+ + + + + + + + + + +
+
+
+ + {{-- Answer --}} +
+ {!! $slot !!} +
+
+
diff --git a/resources/views/components/footer.blade.php b/resources/views/components/footer.blade.php index d4fd7a5d..77d3c86b 100644 --- a/resources/views/components/footer.blade.php +++ b/resources/views/components/footer.blade.php @@ -1,18 +1,520 @@ -
-
-

- © {{ date('Y') }} NativePHP. -

-

- NativePHP is a copyright of and maintained by
Marcel Pociot - and Simon Hamp. -

-

- Logo by Caneco. -

-
+ diff --git a/resources/views/components/free-plugins-offer-banner.blade.php b/resources/views/components/free-plugins-offer-banner.blade.php new file mode 100644 index 00000000..7fed1199 --- /dev/null +++ b/resources/views/components/free-plugins-offer-banner.blade.php @@ -0,0 +1,37 @@ +@props(['inline' => false]) + +
!$inline])> +
+
+
+ + + +
+
+

+ Claim Your Free Plugins! +

+

+ As a thank you for your continued support, you can claim 5 premium plugins for free: + Biometrics, + Geolocation, + Firebase, + Secure Storage, and + Scanner. +

+
+
+ @csrf + +
+
+
+
+
+
diff --git a/resources/views/components/header-banner.blade.php b/resources/views/components/header-banner.blade.php deleted file mode 100644 index 50a1c4d3..00000000 --- a/resources/views/components/header-banner.blade.php +++ /dev/null @@ -1,75 +0,0 @@ -@props(['hasMenu' => false]) -
- - - -
- - - - NativePHP - - -
-
- -
-
- - -
- @if($hasMenu) - - @else - - - - - @endif -
-
-
-
- -
diff --git a/resources/views/components/home/announcements.blade.php b/resources/views/components/home/announcements.blade.php new file mode 100644 index 00000000..63cc4da6 --- /dev/null +++ b/resources/views/components/home/announcements.blade.php @@ -0,0 +1,22 @@ +{{-- Announcements Section: Plugins (full width) + Bifrost (50%) & Course/Jump (50% stacked) --}} +
+
+ {{-- Plugins Announcement (Full Width) --}} + + + {{-- Course & Jump + Bifrost Row --}} +
+ {{-- Left Column - Course & Jump --}} +
+ {{-- Course Card --}} + + + {{-- Jump Card --}} + +
+ + {{-- Bifrost Banner (Right - 50%) --}} + +
+
+
\ No newline at end of file diff --git a/resources/views/components/home/bifrost-card.blade.php b/resources/views/components/home/bifrost-card.blade.php new file mode 100644 index 00000000..4c039ca1 --- /dev/null +++ b/resources/views/components/home/bifrost-card.blade.php @@ -0,0 +1,103 @@ +{{-- Bifrost Card - Cloud development platform --}} + +
+ {{-- Animated glow --}} + + + + {{-- Badge --}} +
+ + + + Cloud Platform +
+ + {{-- Logo --}} +
+ +
+ + {{-- Tagline --}} +

+ Build in the cloud. Deploy anywhere. +

+ + {{-- Description --}} +

+ And when you've built your app, get it to the stores and into the hands of users as fast as humanly possible. +

+ + {{-- Bifrost Diagram --}} +
+ +
+ + {{-- CTA --}} +
+ Ship it! + + + +
+
+
diff --git a/resources/views/components/home/category-pill.blade.php b/resources/views/components/home/category-pill.blade.php new file mode 100644 index 00000000..c2c887ee --- /dev/null +++ b/resources/views/components/home/category-pill.blade.php @@ -0,0 +1,15 @@ +@props([ + 'name' => '', +]) +
+ + +
{{ $name }}
+
diff --git a/resources/views/components/home/course-card.blade.php b/resources/views/components/home/course-card.blade.php new file mode 100644 index 00000000..79a2e8ba --- /dev/null +++ b/resources/views/components/home/course-card.blade.php @@ -0,0 +1,102 @@ +{{-- Course Card - NativePHP Masterclass --}} + +
+ {{-- Animated glow --}} + + + {{-- Badge --}} +
+ + + + + Early Bird +
+ + {{-- Title --}} +

+ The Masterclass +

+ + {{-- Tagline --}} +

+ Zero to published app. +

+ + {{-- Description --}} +

+ Learn to build native mobile and desktop apps using PHP and Laravel. +

+ + {{-- Features list --}} +
    +
  • + + + + Mobile & Desktop +
  • +
  • + + + + Use your existing PHP skills +
  • +
  • + + + + Zero to published app +
  • +
+ + {{-- CTA --}} +
+ Start learning + + + +
+
+
diff --git a/resources/views/components/home/explainer.blade.php b/resources/views/components/home/explainer.blade.php new file mode 100644 index 00000000..77fffd51 --- /dev/null +++ b/resources/views/components/home/explainer.blade.php @@ -0,0 +1,574 @@ +
+ {{-- Part 1 --}} +
+ {{-- How does it work --}} +
+ {{-- Left side --}} +
+ {{-- Header --}} +
+

+ Under the hood +

+

+ How does it work? +

+
+ {{-- Description --}} +

+ + NativePHP + + bundles PHP with your app and lets it run inside a + + Swift, + + + + Kotlin + + (mobile) or + + Electron + + (desktop) shell. It uses special + + bridges + + to talk directly to the device and show your app in a + + native web view. + + +
+
+ You still write PHP like you’re used to—just with a few + extra tools that connect it to the device's native features. +
+
+ That’s it. It feels like + + magic, + + but it’s just PHP... on your user's device! +

+
+ + {{-- Right side --}} +
+
+ {{-- Phone wireframe --}} +
+
+ {{-- Grid illustration --}} + + {{-- Dashed vertical line --}} + + {{-- Solid vertical line --}} + + {{-- Dashed horizontal line --}} + +
+ + {{-- Right side --}} +
+ {{-- Performance --}} +
+
+ +
+

+ Fast apps +

+

+ Laravel running at native speed +

+
+
+
+ +
+

+ Tiny apps +

+

+ Mobile apps under 50MB +

+
+
+
+ + {{-- Tools --}} +
+
+

+ Bring your favorite tools +

+

+ Use any Composer packages and front-end frameworks. +

+
+ +
+ @php + $skills = [ + ['name' => 'Laravel', 'link' => 'https://laravel.com/', 'icon' => 'icons.skills.laravel'], + ['name' => 'React', 'link' => 'https://reactjs.org/', 'icon' => 'icons.skills.reactjs'], + ['name' => 'Vue.js', 'link' => 'https://vuejs.org/', 'icon' => 'icons.skills.vuejs'], + ['name' => 'Nuxt', 'link' => 'https://nuxtjs.org/', 'icon' => 'icons.skills.nuxtjs'], + ['name' => 'Next.js', 'link' => 'https://nextjs.org/', 'icon' => 'icons.skills.nextjs'], + ['name' => 'Livewire', 'link' => 'https://livewire.laravel.com', 'icon' => 'icons.skills.livewire'], + ['name' => 'FilamentPHP', 'link' => 'https://filamentphp.com/', 'icon' => 'icons.skills.filamentphp'], + ['name' => 'Alpine.js', 'link' => 'https://alpinejs.dev/', 'icon' => 'icons.skills.alpinejs'], + ['name' => 'Inertia.js', 'link' => 'https://inertiajs.com/', 'icon' => 'icons.skills.inertiajs'], + ['name' => 'TailwindCSS', 'link' => 'https://tailwindcss.com/', 'icon' => 'icons.skills.tailwind-css'], + ['name' => 'TypeScript', 'link' => 'https://www.typescriptlang.org/', 'icon' => 'icons.skills.typescript'], + ['name' => 'JavaScript', 'link' => 'https://www.javascript.com/', 'icon' => 'icons.skills.javascript'], + ['name' => 'Pest', 'link' => 'https://pestphp.com/', 'icon' => 'icons.skills.pest'], + ['name' => 'PHPUnit', 'link' => 'https://phpunit.de/', 'icon' => 'icons.skills.phpunit'], + ]; + @endphp + + @foreach ($skills as $skill) + + + + @endforeach +
+
+
+
+ + {{-- Part 2 --}} +
+ {{-- Left side --}} +
+ {{-- Header --}} +
+

+ Step by step +

+

+ How do I get it? +

+
+ + {{-- Steps --}} +
    +
  1. +
    +
    +
    +
    + 1. + + Read the docs + +
  2. +
  3. +
    +
    +
    +
    + 2. + + Install the package. + +
  4. +
  5. +
    +
    +
    +
    + 3. + + Build your app. + +
  6. +
+
+ + {{-- Right side --}} +
+
+

+ Your next app starts here +

+

+ What can I build? +

+
+ + {{-- Description --}} +

+ Whether you're building tools for your team, apps for your + customers, or your next big idea — + + NativePHP + + gives you the flexibility and performance to bring it to life. +

+ +
+ @php + $categories = [ + ['name' => 'SaaS clients', 'icon' => 'icons.home.web'], + ['name' => 'Games', 'icon' => 'icons.home.game'], + ['name' => 'eCommerce', 'icon' => 'icons.home.shop'], + ['name' => 'Social apps', 'icon' => 'icons.home.social'], + ['name' => 'Field services', 'icon' => 'icons.home.wrench'], + ['name' => 'Health', 'icon' => 'icons.home.health'], + ]; + @endphp + + @foreach ($categories as $category) + + + + @endforeach +
+ + {{-- Decorative circle --}} +
+
+
+ +
diff --git a/resources/views/components/home/featured-partner-card.blade.php b/resources/views/components/home/featured-partner-card.blade.php new file mode 100644 index 00000000..4c52b8e9 --- /dev/null +++ b/resources/views/components/home/featured-partner-card.blade.php @@ -0,0 +1,35 @@ +@props([ + 'href' => '', + 'partnerName' => '', + 'tagline' => '', + 'description' => '', + 'title' => null, +]) + +@php + $computedTitle = $title ?? "Learn more about {$partnerName}"; +@endphp + + +
{{ $logo }}
+ +

+ {{ $partnerName }} +

+ +
+
+ {{ $tagline }} +
+

+ {{ $description }} +

+
+
diff --git a/resources/views/components/home/feedback.blade.php b/resources/views/components/home/feedback.blade.php new file mode 100644 index 00000000..8dee2ea2 --- /dev/null +++ b/resources/views/components/home/feedback.blade.php @@ -0,0 +1,115 @@ +@php + $submissions = \App\Models\WallOfLoveSubmission::query() + ->approved() + ->promoted() + ->latest() + ->get(); +@endphp + +@if ($submissions->count() > 0) +
+
+

+ What developers are saying +

+

+ From the Wall of Love +

+
+ + @php + $columns = 4; + $itemsPerColumn = (int) ceil($submissions->count() / $columns); + @endphp +
+ @foreach ($submissions as $submission) + @php + $row = $loop->index % $itemsPerColumn; + @endphp +
+ {{-- Quote --}} +
+ "{{ $submission->promoted_testimonial ?? $submission->testimonial }}" +
+ + {{-- Author --}} +
+ @if ($submission->photo_path) + {{ $submission->name }} + @else +
+ {{ substr($submission->name, 0, 1) }} +
+ @endif +
+
+ @if ($submission->url) + + {{ $submission->name }} + + @else + {{ $submission->name }} + @endif +
+ @if ($submission->company) +
+ {{ $submission->company }} +
+ @endif +
+
+
+ @endforeach +
+
+@endif diff --git a/resources/views/components/home/hero.blade.php b/resources/views/components/home/hero.blade.php new file mode 100644 index 00000000..d876841a --- /dev/null +++ b/resources/views/components/home/hero.blade.php @@ -0,0 +1,778 @@ +
+
+ {{-- Demo app --}} +
+
+

+ Try our + Mobile + app: +

+ + + +
+
+ + {{-- Mockups --}} +
+
+
+ {{-- Macbook --}} + + {{-- Window --}} +
+ {{-- Header --}} +
+ {{-- Traffic lights --}} +
+
+
+
+
+ {{-- Label --}} +
+ NativePHP App +
+
+ {{-- Page --}} +
+ + +
+
+
+ + {{-- Iphone --}} + +
+ + {{-- Feature list (infinite vertical marquee) --}} + @php + $features = [ + ['icon' => 'icons.home.share-link', 'label' => 'Native sharing'], + ['icon' => 'icons.home.gallery', 'label' => 'Gallery'], + ['icon' => 'icons.home.camera', 'label' => 'Camera'], + ['icon' => 'icons.home.fingerprint', 'label' => 'Biometrics'], + ['icon' => 'icons.home.bell', 'label' => 'Push notifications'], + ['icon' => 'icons.home.phone-message', 'label' => 'Native dialogs'], + ['icon' => 'icons.home.external-link', 'label' => 'Deep links'], + ['icon' => 'icons.home.phone-vibrate', 'label' => 'Haptic feedback'], + ['icon' => 'icons.home.flashlight', 'label' => 'Flashlight'], + ['icon' => 'icons.home.database-shield', 'label' => 'Secure storage'], + ['icon' => 'icons.home.location-pin', 'label' => 'Location services'], + ]; + @endphp + + {{-- Local CSS for marquee (kept tiny and scoped) --}} + + + + + {{-- Feature list (horizontal marquee on small screens) --}} +
+ {{-- Track (two sets for seamless loop) --}} +
+ {{-- Set A --}} +
+ @foreach ($features as $feature) +
+
+ @endforeach +
+ + {{-- Set B (clone) --}} + +
+
+
+ + {{-- Main --}} +
+ {{-- Headline --}} +

+ Build + + + Native PHP + Apps + + {{-- Star --}} +
+
+ + {{-- Video --}} + +

+ + {{-- Description --}} +

+ Bring your + + PHP + + & + + Laravel + + skills to the world of + + desktop & mobile apps + + . + + Build cross-platform applications effortlessly—no extra tools, + just the stack you love. +

+ + {{-- Call to action --}} + + + {{-- Introduction video for mobile viewport --}} + +
+ + {{-- Top left line --}} + + + {{-- Top right vertical lines --}} + + + {{-- Bottom left vertical lines --}} + + + {{-- Green blur --}} + + + {{-- Cyan blur --}} + +
+
diff --git a/resources/views/components/home/jump-card.blade.php b/resources/views/components/home/jump-card.blade.php new file mode 100644 index 00000000..fc1f8c23 --- /dev/null +++ b/resources/views/components/home/jump-card.blade.php @@ -0,0 +1,113 @@ +{{-- Jump Card - Preview on real devices --}} + +
+ {{-- Animated glow --}} + + + {{-- Platform badges (top-right) --}} +
+ + iOS + + + Android + +
+ + {{-- Badge --}} +
+ + + + Preview Tool +
+ + {{-- Title --}} +

+ Jump +

+ + {{-- Tagline --}} +

+ Code here. Jump there. +

+ + {{-- Description --}} +

+ Preview your NativePHP app on real devices instantly. +

+ + {{-- Features list --}} +
    +
  • + + + + Works offline after download +
  • +
  • + + + + No Xcode or Android Studio +
  • +
  • + + + + Free for local development +
  • +
+ + {{-- CTA --}} +
+ Jump in + + + +
+
+
diff --git a/resources/views/components/home/marcel-talk.blade.php b/resources/views/components/home/marcel-talk.blade.php new file mode 100644 index 00000000..fbb315bc --- /dev/null +++ b/resources/views/components/home/marcel-talk.blade.php @@ -0,0 +1,132 @@ +
+
+ {{-- Left side --}} +
+

+ Laracon US Talk +

+

+ Where did this come from? +

+ +

+ Watch Marcel's original NativePHP talk from Laracon US 2023 in + Nashville. Minds were blown as he demonstrated how to use + Laravel to build cross-platform desktop applications. +

+
+ + {{-- Right side --}} + +
+
diff --git a/resources/views/components/home/mimi-card.blade.php b/resources/views/components/home/mimi-card.blade.php new file mode 100644 index 00000000..ff1ef5b7 --- /dev/null +++ b/resources/views/components/home/mimi-card.blade.php @@ -0,0 +1,118 @@ +{{-- Mimi Card - AI-powered app creation --}} + +
+ {{-- Animated glow --}} + + + {{-- Badge --}} +
+ + + + + + AI-Powered +
+ + {{-- Title --}} +

+ Mimi +

+ + {{-- Tagline --}} +

+ Describe it. Build it. +

+ + {{-- Description --}} +

+ Turn your ideas into native mobile apps with AI. +

+ + {{-- Features list --}} +
    +
  • + + + + Real-time preview +
  • +
  • + + + + Running SotA models +
  • +
  • + + + + Voice powered +
  • +
+ + {{-- CTA --}} +
+ Vibe away + + + +
+
+
diff --git a/resources/views/components/home/partner-card.blade.php b/resources/views/components/home/partner-card.blade.php new file mode 100644 index 00000000..e58d1846 --- /dev/null +++ b/resources/views/components/home/partner-card.blade.php @@ -0,0 +1,18 @@ +@props([ + 'href' => '', + 'partnerName' => '', +]) + + +
{{ $slot }}
+ +

+ {{ $partnerName }} +

+
diff --git a/resources/views/components/home/partners.blade.php b/resources/views/components/home/partners.blade.php new file mode 100644 index 00000000..0065de14 --- /dev/null +++ b/resources/views/components/home/partners.blade.php @@ -0,0 +1,233 @@ +
+
+
+

+ Partners get more +

+

+ Our Partner Program enables the best development teams and technology vendors to excel in delivering + world class native apps. +

+
+ + {{-- Featured partners (two-column) --}} +
+ + + + + + + + + + + Synergi Tech logo + + + + + Synergi Tech are an established bespoke software development agency in the UK, specialising in business management and high-growth, complex infrastructure. Proud to partner with NativePHP. + + + + + + + + + + BeyondCode logo - PHP development tools and packages + + + + + From local full stack development to cutting-edge AI + platforms, we provide the tools for building your next + great app. + + + + {{-- Partner CTA --}} +
+
+
+ Get more from NativePHP as a partner! +
+

+ Our Partners are helping us bring NativePHP to + everyone and getting some incredible benefits to + boot. +

+
+ + +
Join
+ + +
+
+
+ + {{-- Sponsor logos --}} +
+ + + + + + + + + +
+
+
diff --git a/resources/views/components/home/plugins-announcement.blade.php b/resources/views/components/home/plugins-announcement.blade.php new file mode 100644 index 00000000..4ce706a4 --- /dev/null +++ b/resources/views/components/home/plugins-announcement.blade.php @@ -0,0 +1,229 @@ +
+ {{-- Inner container --}} +
+ {{-- Animated background grid --}} + + + {{-- Glowing orbs --}} + + + + + {{-- NEW badge --}} +
+ + + + NEW +
+ + {{-- Main headline --}} +

+ Build anything with + + Plugins + +

+ + {{-- Subtitle --}} +

+ Extend your mobile apps with powerful plugins. + Unlimited possibilities. +

+ + {{-- CTA Button --}} + + + {{-- Decorative corner elements --}} + + + + +
+
diff --git a/resources/views/components/home/skill-pill.blade.php b/resources/views/components/home/skill-pill.blade.php new file mode 100644 index 00000000..5a6fbb77 --- /dev/null +++ b/resources/views/components/home/skill-pill.blade.php @@ -0,0 +1,19 @@ +@props([ + 'link' => '#', + 'name' => '', +]) + + + +
{{ $name }}
+
diff --git a/resources/views/components/home/testimonials.blade.php b/resources/views/components/home/testimonials.blade.php new file mode 100644 index 00000000..bb4bd297 --- /dev/null +++ b/resources/views/components/home/testimonials.blade.php @@ -0,0 +1,103 @@ +@php + $aaronQuotes = [ + [ + 'text' => "It does sound insane... that's why I like it!", + 'url' => 'https://youtu.be/GuelLKsWwlc?t=1725', + ], + [ + 'text' => "It's WILD!", + 'url' => null, + ], + [ + 'text' => "Did everyone tell them it was crazy? Yes. Why? Because it's crazy! It's a crazy idea. Did they do it anyway? Yes. Did it work? Yes!", + 'url' => 'https://youtu.be/dgr-WAUgELw?t=565', + ], + ]; + + $aaronQuote = $aaronQuotes[array_rand($aaronQuotes)]; + + $testimonials = [ + [ + 'name' => 'Aaron Francis', + 'role' => 'Developer & Educator', + 'image' => 'https://unavatar.io/twitter/aarondfrancis', + 'quote' => $aaronQuote['text'], + 'url' => $aaronQuote['url'], + ], + [ + 'name' => 'Taylor Otwell', + 'role' => 'Creator of Laravel', + 'image' => 'https://unavatar.io/twitter/taylorotwell', + 'quote' => "I think it's super cool... it's just wild", + 'url' => 'https://youtu.be/JElNFR_efnM?t=3830', + ], + [ + 'name' => 'Nuno Maduro', + 'role' => 'Creator of Pest & Laravel Core', + 'image' => 'https://unavatar.io/twitter/enunomaduro', + 'quote' => 'NativePHP FTW!', + 'url' => 'https://youtu.be/XkreP6Amwq0?t=384', + ], + ]; +@endphp + +
+

What people are saying

+ +
+ @foreach ($testimonials as $testimonial) +
+ {{-- Quote --}} +
+

+ "{{ $testimonial['quote'] }}" +

+
+ + {{-- Author --}} +
+ {{ $testimonial['name'] }} +
+
+ {{ $testimonial['name'] }} +
+
+ {{ $testimonial['role'] }} +
+
+ @if ($testimonial['url']) + + + + + + @endif +
+
+ @endforeach +
+
diff --git a/resources/views/components/home/vertical-lines.blade.php b/resources/views/components/home/vertical-lines.blade.php new file mode 100644 index 00000000..3a20ff0f --- /dev/null +++ b/resources/views/components/home/vertical-lines.blade.php @@ -0,0 +1,7 @@ +
+ @foreach (range(1, 50) as $_) +
+ @endforeach +
diff --git a/resources/views/components/icons/alert-diamond.blade.php b/resources/views/components/icons/alert-diamond.blade.php new file mode 100644 index 00000000..e4d0eebc --- /dev/null +++ b/resources/views/components/icons/alert-diamond.blade.php @@ -0,0 +1,47 @@ + + + + + + + + + + diff --git a/resources/views/components/icons/app-store.blade.php b/resources/views/components/icons/app-store.blade.php new file mode 100644 index 00000000..88c4d21a --- /dev/null +++ b/resources/views/components/icons/app-store.blade.php @@ -0,0 +1,32 @@ + + + + + + + + + + diff --git a/resources/views/components/icons/banner/dollar-decrease.blade.php b/resources/views/components/icons/banner/dollar-decrease.blade.php new file mode 100644 index 00000000..facb07c0 --- /dev/null +++ b/resources/views/components/icons/banner/dollar-decrease.blade.php @@ -0,0 +1,14 @@ + + + diff --git a/resources/views/components/icons/checkmark.blade.php b/resources/views/components/icons/checkmark.blade.php new file mode 100644 index 00000000..e17d5d81 --- /dev/null +++ b/resources/views/components/icons/checkmark.blade.php @@ -0,0 +1,12 @@ + + + diff --git a/resources/views/components/icons/chevron-down.blade.php b/resources/views/components/icons/chevron-down.blade.php new file mode 100644 index 00000000..e09e182b --- /dev/null +++ b/resources/views/components/icons/chevron-down.blade.php @@ -0,0 +1,14 @@ + + + diff --git a/resources/views/components/icons/colored-confetti.blade.php b/resources/views/components/icons/colored-confetti.blade.php new file mode 100644 index 00000000..92f22525 --- /dev/null +++ b/resources/views/components/icons/colored-confetti.blade.php @@ -0,0 +1,187 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/views/components/icons/confetti.blade.php b/resources/views/components/icons/confetti.blade.php new file mode 100644 index 00000000..456c7822 --- /dev/null +++ b/resources/views/components/icons/confetti.blade.php @@ -0,0 +1,46 @@ + + + + + + + + + + diff --git a/resources/views/components/icons/copy.blade.php b/resources/views/components/icons/copy.blade.php new file mode 100644 index 00000000..58247a11 --- /dev/null +++ b/resources/views/components/icons/copy.blade.php @@ -0,0 +1,3 @@ +merge(['fill' => 'none', 'stroke' => 'currentColor', 'viewBox' => '0 0 24 24']) }}> + + \ No newline at end of file diff --git a/resources/views/components/icons/date.blade.php b/resources/views/components/icons/date.blade.php new file mode 100644 index 00000000..c2fe28ee --- /dev/null +++ b/resources/views/components/icons/date.blade.php @@ -0,0 +1,73 @@ + + + + + + + + + + + + diff --git a/resources/views/components/icons/desktop-computer.blade.php b/resources/views/components/icons/desktop-computer.blade.php new file mode 100644 index 00000000..6e351b4c --- /dev/null +++ b/resources/views/components/icons/desktop-computer.blade.php @@ -0,0 +1,39 @@ + + + + + + + + + diff --git a/resources/views/components/icons/device-mobile-phone.blade.php b/resources/views/components/icons/device-mobile-phone.blade.php index 0c894c76..7472aed3 100644 --- a/resources/views/components/icons/device-mobile-phone.blade.php +++ b/resources/views/components/icons/device-mobile-phone.blade.php @@ -1,3 +1,14 @@ - - + + diff --git a/resources/views/components/icons/docs.blade.php b/resources/views/components/icons/docs.blade.php new file mode 100644 index 00000000..29db34d5 --- /dev/null +++ b/resources/views/components/icons/docs.blade.php @@ -0,0 +1,18 @@ + + + + + diff --git a/resources/views/components/icons/dollar-circle.blade.php b/resources/views/components/icons/dollar-circle.blade.php new file mode 100644 index 00000000..e7cf56bd --- /dev/null +++ b/resources/views/components/icons/dollar-circle.blade.php @@ -0,0 +1,22 @@ + + + + + + diff --git a/resources/views/components/icons/download.blade.php b/resources/views/components/icons/download.blade.php new file mode 100644 index 00000000..e2aadfec --- /dev/null +++ b/resources/views/components/icons/download.blade.php @@ -0,0 +1,14 @@ + + + diff --git a/resources/views/components/icons/email-document.blade.php b/resources/views/components/icons/email-document.blade.php new file mode 100644 index 00000000..3b8d98c7 --- /dev/null +++ b/resources/views/components/icons/email-document.blade.php @@ -0,0 +1,103 @@ + + + + + + + + + + + + + + + + diff --git a/resources/views/components/icons/git-branch.blade.php b/resources/views/components/icons/git-branch.blade.php new file mode 100644 index 00000000..36c4574b --- /dev/null +++ b/resources/views/components/icons/git-branch.blade.php @@ -0,0 +1,41 @@ + + + + + + diff --git a/resources/views/components/icons/github.blade.php b/resources/views/components/icons/github.blade.php index eb46b515..351f580d 100644 --- a/resources/views/components/icons/github.blade.php +++ b/resources/views/components/icons/github.blade.php @@ -1,3 +1,11 @@ GitHub + xmlns="http://www.w3.org/2000/svg" + viewBox="0 0 16 16" +> + + diff --git a/resources/views/components/icons/heart.blade.php b/resources/views/components/icons/heart.blade.php new file mode 100644 index 00000000..82995c06 --- /dev/null +++ b/resources/views/components/icons/heart.blade.php @@ -0,0 +1,14 @@ + + + diff --git a/resources/views/components/icons/home/android.blade.php b/resources/views/components/icons/home/android.blade.php new file mode 100644 index 00000000..9ef7c142 --- /dev/null +++ b/resources/views/components/icons/home/android.blade.php @@ -0,0 +1,12 @@ + + + diff --git a/resources/views/components/icons/home/apple.blade.php b/resources/views/components/icons/home/apple.blade.php new file mode 100644 index 00000000..d0b854c3 --- /dev/null +++ b/resources/views/components/icons/home/apple.blade.php @@ -0,0 +1,11 @@ + + + diff --git a/resources/views/components/icons/home/arc-connector.blade.php b/resources/views/components/icons/home/arc-connector.blade.php new file mode 100644 index 00000000..8e01312d --- /dev/null +++ b/resources/views/components/icons/home/arc-connector.blade.php @@ -0,0 +1,31 @@ + + + + + + + + + diff --git a/resources/views/components/icons/home/bell.blade.php b/resources/views/components/icons/home/bell.blade.php new file mode 100644 index 00000000..bc85c031 --- /dev/null +++ b/resources/views/components/icons/home/bell.blade.php @@ -0,0 +1,28 @@ + + + + + diff --git a/resources/views/components/icons/home/browser.blade.php b/resources/views/components/icons/home/browser.blade.php new file mode 100644 index 00000000..fce23844 --- /dev/null +++ b/resources/views/components/icons/home/browser.blade.php @@ -0,0 +1,49 @@ + + + + + + + + diff --git a/resources/views/components/icons/home/camera.blade.php b/resources/views/components/icons/home/camera.blade.php new file mode 100644 index 00000000..63e042fa --- /dev/null +++ b/resources/views/components/icons/home/camera.blade.php @@ -0,0 +1,33 @@ + + + + + + diff --git a/resources/views/components/icons/home/charging-thunder.blade.php b/resources/views/components/icons/home/charging-thunder.blade.php new file mode 100644 index 00000000..eb1ccec2 --- /dev/null +++ b/resources/views/components/icons/home/charging-thunder.blade.php @@ -0,0 +1,28 @@ + + + + + diff --git a/resources/views/components/icons/home/database-shield.blade.php b/resources/views/components/icons/home/database-shield.blade.php new file mode 100644 index 00000000..9255e514 --- /dev/null +++ b/resources/views/components/icons/home/database-shield.blade.php @@ -0,0 +1,56 @@ + + + + + + + + + diff --git a/resources/views/components/icons/home/document.blade.php b/resources/views/components/icons/home/document.blade.php new file mode 100644 index 00000000..91cebb51 --- /dev/null +++ b/resources/views/components/icons/home/document.blade.php @@ -0,0 +1,51 @@ + + + + + + + + diff --git a/resources/views/components/icons/home/external-link.blade.php b/resources/views/components/icons/home/external-link.blade.php new file mode 100644 index 00000000..258f6d39 --- /dev/null +++ b/resources/views/components/icons/home/external-link.blade.php @@ -0,0 +1,38 @@ + + + + + + diff --git a/resources/views/components/icons/home/fingerprint.blade.php b/resources/views/components/icons/home/fingerprint.blade.php new file mode 100644 index 00000000..c3c49c63 --- /dev/null +++ b/resources/views/components/icons/home/fingerprint.blade.php @@ -0,0 +1,36 @@ + + + + + + diff --git a/resources/views/components/icons/home/flashlight.blade.php b/resources/views/components/icons/home/flashlight.blade.php new file mode 100644 index 00000000..d0040a8f --- /dev/null +++ b/resources/views/components/icons/home/flashlight.blade.php @@ -0,0 +1,43 @@ + + + + + + + diff --git a/resources/views/components/icons/home/gallery.blade.php b/resources/views/components/icons/home/gallery.blade.php new file mode 100644 index 00000000..b94c87e2 --- /dev/null +++ b/resources/views/components/icons/home/gallery.blade.php @@ -0,0 +1,59 @@ + + + + + + + + + + diff --git a/resources/views/components/icons/home/game.blade.php b/resources/views/components/icons/home/game.blade.php new file mode 100644 index 00000000..18984514 --- /dev/null +++ b/resources/views/components/icons/home/game.blade.php @@ -0,0 +1,51 @@ + + + + + + + + diff --git a/resources/views/components/icons/home/health.blade.php b/resources/views/components/icons/home/health.blade.php new file mode 100644 index 00000000..9c0f4e64 --- /dev/null +++ b/resources/views/components/icons/home/health.blade.php @@ -0,0 +1,29 @@ + + + + + diff --git a/resources/views/components/icons/home/location-pin.blade.php b/resources/views/components/icons/home/location-pin.blade.php new file mode 100644 index 00000000..592a809d --- /dev/null +++ b/resources/views/components/icons/home/location-pin.blade.php @@ -0,0 +1,46 @@ + + + + + + + + diff --git a/resources/views/components/icons/home/phone-message.blade.php b/resources/views/components/icons/home/phone-message.blade.php new file mode 100644 index 00000000..59214ef0 --- /dev/null +++ b/resources/views/components/icons/home/phone-message.blade.php @@ -0,0 +1,43 @@ + + + + + + + diff --git a/resources/views/components/icons/home/phone-vibrate.blade.php b/resources/views/components/icons/home/phone-vibrate.blade.php new file mode 100644 index 00000000..685dd27e --- /dev/null +++ b/resources/views/components/icons/home/phone-vibrate.blade.php @@ -0,0 +1,46 @@ + + + + + + + diff --git a/resources/views/components/icons/home/rocket.blade.php b/resources/views/components/icons/home/rocket.blade.php new file mode 100644 index 00000000..835e1551 --- /dev/null +++ b/resources/views/components/icons/home/rocket.blade.php @@ -0,0 +1,19 @@ + + + + + diff --git a/resources/views/components/icons/home/share-link.blade.php b/resources/views/components/icons/home/share-link.blade.php new file mode 100644 index 00000000..dcdcb34e --- /dev/null +++ b/resources/views/components/icons/home/share-link.blade.php @@ -0,0 +1,62 @@ + + + + + + + + + + diff --git a/resources/views/components/icons/home/shop.blade.php b/resources/views/components/icons/home/shop.blade.php new file mode 100644 index 00000000..a09f8604 --- /dev/null +++ b/resources/views/components/icons/home/shop.blade.php @@ -0,0 +1,62 @@ + + + + + + + + + + diff --git a/resources/views/components/icons/home/social.blade.php b/resources/views/components/icons/home/social.blade.php new file mode 100644 index 00000000..6250df45 --- /dev/null +++ b/resources/views/components/icons/home/social.blade.php @@ -0,0 +1,49 @@ + + + + + + + + diff --git a/resources/views/components/icons/home/startup.blade.php b/resources/views/components/icons/home/startup.blade.php new file mode 100644 index 00000000..7fd46f54 --- /dev/null +++ b/resources/views/components/icons/home/startup.blade.php @@ -0,0 +1,67 @@ + + + + + + + + + + + diff --git a/resources/views/components/icons/home/web.blade.php b/resources/views/components/icons/home/web.blade.php new file mode 100644 index 00000000..d134131b --- /dev/null +++ b/resources/views/components/icons/home/web.blade.php @@ -0,0 +1,49 @@ + + + + + + + + diff --git a/resources/views/components/icons/home/windows.blade.php b/resources/views/components/icons/home/windows.blade.php new file mode 100644 index 00000000..108006eb --- /dev/null +++ b/resources/views/components/icons/home/windows.blade.php @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/resources/views/components/icons/home/wrench.blade.php b/resources/views/components/icons/home/wrench.blade.php new file mode 100644 index 00000000..5d6d849c --- /dev/null +++ b/resources/views/components/icons/home/wrench.blade.php @@ -0,0 +1,36 @@ + + + + + + diff --git a/resources/views/components/icons/laracon-us.blade.php b/resources/views/components/icons/laracon-us.blade.php new file mode 100644 index 00000000..b9846f27 --- /dev/null +++ b/resources/views/components/icons/laracon-us.blade.php @@ -0,0 +1,47 @@ + + + + + + + + + + + + diff --git a/resources/views/components/icons/list-down.blade.php b/resources/views/components/icons/list-down.blade.php new file mode 100644 index 00000000..c89467ba --- /dev/null +++ b/resources/views/components/icons/list-down.blade.php @@ -0,0 +1,32 @@ + + + + + + diff --git a/resources/views/components/icons/modern-arrow.blade.php b/resources/views/components/icons/modern-arrow.blade.php new file mode 100644 index 00000000..79ccc8a6 --- /dev/null +++ b/resources/views/components/icons/modern-arrow.blade.php @@ -0,0 +1,13 @@ + + + diff --git a/resources/views/components/icons/monitor-smartphone.blade.php b/resources/views/components/icons/monitor-smartphone.blade.php new file mode 100644 index 00000000..57c11860 --- /dev/null +++ b/resources/views/components/icons/monitor-smartphone.blade.php @@ -0,0 +1 @@ + diff --git a/resources/views/components/icons/pc.blade.php b/resources/views/components/icons/pc.blade.php new file mode 100644 index 00000000..a71220a0 --- /dev/null +++ b/resources/views/components/icons/pc.blade.php @@ -0,0 +1,14 @@ + + + diff --git a/resources/views/components/icons/pinkary.blade.php b/resources/views/components/icons/pinkary.blade.php index 4a810c2e..c36dcdf6 100644 --- a/resources/views/components/icons/pinkary.blade.php +++ b/resources/views/components/icons/pinkary.blade.php @@ -1,7 +1,16 @@ - - - + viewBox="0 0 1024 1024" + fill="none" +> + + diff --git a/resources/views/components/icons/play-button.blade.php b/resources/views/components/icons/play-button.blade.php new file mode 100644 index 00000000..760cf936 --- /dev/null +++ b/resources/views/components/icons/play-button.blade.php @@ -0,0 +1,13 @@ + + + diff --git a/resources/views/components/icons/play-store.blade.php b/resources/views/components/icons/play-store.blade.php new file mode 100644 index 00000000..fb1379f8 --- /dev/null +++ b/resources/views/components/icons/play-store.blade.php @@ -0,0 +1,22 @@ + + + + + + diff --git a/resources/views/components/icons/plug.blade.php b/resources/views/components/icons/plug.blade.php new file mode 100644 index 00000000..351d207d --- /dev/null +++ b/resources/views/components/icons/plug.blade.php @@ -0,0 +1,3 @@ +@props(['class' => 'size-5']) + +merge(['class' => $class]) }} /> diff --git a/resources/views/components/icons/puzzle.blade.php b/resources/views/components/icons/puzzle.blade.php new file mode 100644 index 00000000..2ece32d5 --- /dev/null +++ b/resources/views/components/icons/puzzle.blade.php @@ -0,0 +1,12 @@ + + + diff --git a/resources/views/components/icons/right-arrow.blade.php b/resources/views/components/icons/right-arrow.blade.php new file mode 100644 index 00000000..57e1d87a --- /dev/null +++ b/resources/views/components/icons/right-arrow.blade.php @@ -0,0 +1,22 @@ + + + + + diff --git a/resources/views/components/icons/rocket.blade.php b/resources/views/components/icons/rocket.blade.php new file mode 100644 index 00000000..7484f2dd --- /dev/null +++ b/resources/views/components/icons/rocket.blade.php @@ -0,0 +1,15 @@ + + + + + + diff --git a/resources/views/components/icons/skills/alpinejs.blade.php b/resources/views/components/icons/skills/alpinejs.blade.php new file mode 100644 index 00000000..cbcfc85c --- /dev/null +++ b/resources/views/components/icons/skills/alpinejs.blade.php @@ -0,0 +1,18 @@ + + + + diff --git a/resources/views/components/icons/skills/filamentphp.blade.php b/resources/views/components/icons/skills/filamentphp.blade.php new file mode 100644 index 00000000..ce2a1fd2 --- /dev/null +++ b/resources/views/components/icons/skills/filamentphp.blade.php @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/views/components/icons/skills/inertiajs.blade.php b/resources/views/components/icons/skills/inertiajs.blade.php new file mode 100644 index 00000000..e35be103 --- /dev/null +++ b/resources/views/components/icons/skills/inertiajs.blade.php @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + diff --git a/resources/views/components/icons/skills/javascript.blade.php b/resources/views/components/icons/skills/javascript.blade.php new file mode 100644 index 00000000..cc7949e6 --- /dev/null +++ b/resources/views/components/icons/skills/javascript.blade.php @@ -0,0 +1,18 @@ + + + + + + diff --git a/resources/views/components/icons/skills/laravel.blade.php b/resources/views/components/icons/skills/laravel.blade.php new file mode 100644 index 00000000..52b15f41 --- /dev/null +++ b/resources/views/components/icons/skills/laravel.blade.php @@ -0,0 +1,13 @@ + + + diff --git a/resources/views/components/icons/skills/livewire.blade.php b/resources/views/components/icons/skills/livewire.blade.php new file mode 100644 index 00000000..244fb8c7 --- /dev/null +++ b/resources/views/components/icons/skills/livewire.blade.php @@ -0,0 +1,52 @@ + + + + + + + + + + diff --git a/resources/views/components/icons/skills/nextjs.blade.php b/resources/views/components/icons/skills/nextjs.blade.php new file mode 100644 index 00000000..c5d721ad --- /dev/null +++ b/resources/views/components/icons/skills/nextjs.blade.php @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + diff --git a/resources/views/components/icons/skills/nuxtjs.blade.php b/resources/views/components/icons/skills/nuxtjs.blade.php new file mode 100644 index 00000000..427b6ea4 --- /dev/null +++ b/resources/views/components/icons/skills/nuxtjs.blade.php @@ -0,0 +1,10 @@ + + + diff --git a/resources/views/components/icons/skills/pest.blade.php b/resources/views/components/icons/skills/pest.blade.php new file mode 100644 index 00000000..9cf8411a --- /dev/null +++ b/resources/views/components/icons/skills/pest.blade.php @@ -0,0 +1,28 @@ + + + + + + + diff --git a/resources/views/components/icons/skills/phpunit.blade.php b/resources/views/components/icons/skills/phpunit.blade.php new file mode 100644 index 00000000..16789ace --- /dev/null +++ b/resources/views/components/icons/skills/phpunit.blade.php @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + diff --git a/resources/views/components/icons/skills/reactjs.blade.php b/resources/views/components/icons/skills/reactjs.blade.php new file mode 100644 index 00000000..d167c046 --- /dev/null +++ b/resources/views/components/icons/skills/reactjs.blade.php @@ -0,0 +1,22 @@ + + + + + + + + + + diff --git a/resources/views/components/icons/skills/tailwind-css.blade.php b/resources/views/components/icons/skills/tailwind-css.blade.php new file mode 100644 index 00000000..371b1b11 --- /dev/null +++ b/resources/views/components/icons/skills/tailwind-css.blade.php @@ -0,0 +1,10 @@ + + + diff --git a/resources/views/components/icons/skills/typescript.blade.php b/resources/views/components/icons/skills/typescript.blade.php new file mode 100644 index 00000000..cebbd9d3 --- /dev/null +++ b/resources/views/components/icons/skills/typescript.blade.php @@ -0,0 +1,18 @@ + + + + + + diff --git a/resources/views/components/icons/skills/vuejs.blade.php b/resources/views/components/icons/skills/vuejs.blade.php new file mode 100644 index 00000000..dc6f7468 --- /dev/null +++ b/resources/views/components/icons/skills/vuejs.blade.php @@ -0,0 +1,18 @@ + + + + + diff --git a/resources/views/components/icons/stacked-lines.blade.php b/resources/views/components/icons/stacked-lines.blade.php new file mode 100644 index 00000000..33d1ae6c --- /dev/null +++ b/resources/views/components/icons/stacked-lines.blade.php @@ -0,0 +1,20 @@ + + + + + + diff --git a/resources/views/components/icons/star-circle.blade.php b/resources/views/components/icons/star-circle.blade.php new file mode 100644 index 00000000..2e59ddf5 --- /dev/null +++ b/resources/views/components/icons/star-circle.blade.php @@ -0,0 +1,14 @@ + + + diff --git a/resources/views/components/icons/star.blade.php b/resources/views/components/icons/star.blade.php new file mode 100644 index 00000000..88384f86 --- /dev/null +++ b/resources/views/components/icons/star.blade.php @@ -0,0 +1,12 @@ + diff --git a/resources/views/components/icons/tablet-smartphone.blade.php b/resources/views/components/icons/tablet-smartphone.blade.php new file mode 100644 index 00000000..6856b5a7 --- /dev/null +++ b/resources/views/components/icons/tablet-smartphone.blade.php @@ -0,0 +1 @@ + diff --git a/resources/views/components/icons/twitter.blade.php b/resources/views/components/icons/twitter.blade.php new file mode 100644 index 00000000..824e2697 --- /dev/null +++ b/resources/views/components/icons/twitter.blade.php @@ -0,0 +1,10 @@ + + + diff --git a/resources/views/components/icons/upload-box.blade.php b/resources/views/components/icons/upload-box.blade.php new file mode 100644 index 00000000..ba8446a8 --- /dev/null +++ b/resources/views/components/icons/upload-box.blade.php @@ -0,0 +1,43 @@ + + + + + + + + diff --git a/resources/views/components/icons/user-single.blade.php b/resources/views/components/icons/user-single.blade.php new file mode 100644 index 00000000..07276637 --- /dev/null +++ b/resources/views/components/icons/user-single.blade.php @@ -0,0 +1,21 @@ + + + + + + diff --git a/resources/views/components/icons/warning.blade.php b/resources/views/components/icons/warning.blade.php new file mode 100644 index 00000000..1f6cb4fd --- /dev/null +++ b/resources/views/components/icons/warning.blade.php @@ -0,0 +1,15 @@ + + + + + diff --git a/resources/views/components/icons/xmark.blade.php b/resources/views/components/icons/xmark.blade.php new file mode 100644 index 00000000..5f9d162c --- /dev/null +++ b/resources/views/components/icons/xmark.blade.php @@ -0,0 +1,14 @@ + + + diff --git a/resources/views/components/icons/youtube.blade.php b/resources/views/components/icons/youtube.blade.php new file mode 100644 index 00000000..1b50cb80 --- /dev/null +++ b/resources/views/components/icons/youtube.blade.php @@ -0,0 +1,14 @@ + + + + diff --git a/resources/views/components/illustrations/bifrost-diagram.blade.php b/resources/views/components/illustrations/bifrost-diagram.blade.php new file mode 100644 index 00000000..1030adc3 --- /dev/null +++ b/resources/views/components/illustrations/bifrost-diagram.blade.php @@ -0,0 +1,146 @@ +
+ {{-- Source (NativePHP app) --}} + + + {{-- Left line (decorative) --}} + + + {{-- Center (Bifrost) --}} +
+ {{-- Decorative blank box (top) --}} + + +
+
+ + Bifrost build system +
+
+ + {{-- Decorative blank box (bottom) --}} + +
+ + {{-- Right (Build outputs to platforms) --}} +
+ + + {{-- Platforms list --}} +
    + {{-- Decorative (top) --}} + + +
  • + + Apple (macOS) +
  • +
  • + + Android +
  • +
  • + + Windows +
  • + + {{-- Decorative (bottom) --}} + +
+
+ +
+ Diagram: NativePHP app passes through Bifrost build system to produce + Apple (macOS), Android, and Windows platform outputs. +
+
diff --git a/resources/views/components/illustrations/partnership.blade.php b/resources/views/components/illustrations/partnership.blade.php new file mode 100644 index 00000000..5f4e28fd --- /dev/null +++ b/resources/views/components/illustrations/partnership.blade.php @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + diff --git a/resources/views/components/illustrations/phone-wireframe.blade.php b/resources/views/components/illustrations/phone-wireframe.blade.php new file mode 100644 index 00000000..ecffffb3 --- /dev/null +++ b/resources/views/components/illustrations/phone-wireframe.blade.php @@ -0,0 +1,61 @@ + + + + + + + + + + + + diff --git a/resources/views/components/illustrations/shushing.blade.php b/resources/views/components/illustrations/shushing.blade.php new file mode 100644 index 00000000..1d03ada7 --- /dev/null +++ b/resources/views/components/illustrations/shushing.blade.php @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/views/components/layout-three-columns.blade.php b/resources/views/components/layout-three-columns.blade.php deleted file mode 100644 index 496b7447..00000000 --- a/resources/views/components/layout-three-columns.blade.php +++ /dev/null @@ -1,31 +0,0 @@ - -
- -
-
- - @if(!empty($sidebarLeft)) - - {{ $sidebarLeft }} - - @endif - - -
-
- - @if(!empty($sidebarRight)) - - {{ $sidebarRight }} - - @endif - - {{ $slot }} - - -
-
-
-
-
-
diff --git a/resources/views/components/layout.blade.php b/resources/views/components/layout.blade.php index 9a6fd679..f5360382 100644 --- a/resources/views/components/layout.blade.php +++ b/resources/views/components/layout.blade.php @@ -1,47 +1,132 @@ - - - - - - - - - {!! SEOMeta::generate() !!} - {!! OpenGraph::generate() !!} - {!! Twitter::generate() !!} - - - - - - @vite(["resources/css/app.css", "resources/js/app.js"]) - - - - - - + + + + + + + + @php + $seoTitle = SEOMeta::getTitle(); + $defaultSeoTitle = config('seotools.meta.defaults.title'); + @endphp + + @if ($seoTitle === $defaultSeoTitle) + {{ isset($title) ? $title . ' - ' : '' }}NativePHP + @endif + + {{-- Favicon --}} + + + {!! SEOMeta::generate() !!} + {!! OpenGraph::generate() !!} + {!! Twitter::generate() !!} + + + @production + + @endproduction + + + + {{-- Styles --}} + + @livewireStyles + @vite('resources/css/app.css') + @stack('head') + + + + + + +
+ {{ $slot }} +
+ + + + - -{{ $slot }} - - - + @livewireScriptConfig + @fluxScripts + @vite('resources/js/app.js') + @vite('resources/css/docsearch.css') + diff --git a/resources/views/components/layouts/auth.blade.php b/resources/views/components/layouts/auth.blade.php new file mode 100644 index 00000000..eb8c5f21 --- /dev/null +++ b/resources/views/components/layouts/auth.blade.php @@ -0,0 +1,60 @@ + + + + + + + + + + {{ isset($title) ? $title . ' - ' : '' }}NativePHP + + + + + @livewireStyles + @vite('resources/css/app.css') + + +
+
+ + NativePHP + +
+ + {{ $slot }} +
+ + @livewireScriptConfig + @fluxScripts + @vite('resources/js/app.js') + + diff --git a/resources/views/components/layouts/dashboard.blade.php b/resources/views/components/layouts/dashboard.blade.php new file mode 100644 index 00000000..12c7fd5b --- /dev/null +++ b/resources/views/components/layouts/dashboard.blade.php @@ -0,0 +1,259 @@ + + + + + + + + + + {{ isset($title) ? $title . ' - ' : '' }}NativePHP + + {{-- Favicon --}} + + + + @production + + @endproduction + + {{-- Styles --}} + + @livewireStyles + @vite('resources/css/app.css') + @stack('head') + + + + + + + + + + + Dashboard + + + @if(auth()->user()->licenses()->exists()) + + Licenses + + @endif + + @feature(App\Features\ShowPlugins::class) + + Purchased Plugins + + @endfeature + + + Purchase History + + + @if(auth()->user()->hasActiveUltraSubscription()) + + Ultra + + @endif + + @if(auth()->user()->hasUltraAccess()) + + Support Tickets + + @endif + + + + Showcase + + @if(auth()->user()->licenses()->where('created_at', '<', '2025-06-01')->exists()) + @php + $wallOfLoveSubmission = auth()->user()->wallOfLoveSubmissions()->first(); + $wallOfLoveUrl = $wallOfLoveSubmission + ? route('customer.wall-of-love.edit', $wallOfLoveSubmission) + : route('customer.wall-of-love.create'); + @endphp + + Wall of Love + + @endif + + Discord + + + + @if(auth()->user()->hasActiveUltraSubscription() || auth()->user()->isUltraTeamMember()) + @php + $ownedTeam = auth()->user()->ownedTeam; + $teamMemberships = auth()->user()->activeTeamMemberships(); + @endphp + + @if($ownedTeam) + + {{ $ownedTeam->name }} + + @endif + + @foreach($teamMemberships as $membership) + + {{ $membership->team->name }} + + @endforeach + + @if(! $ownedTeam && $teamMemberships->isEmpty() && auth()->user()->hasActiveUltraSubscription()) + + Create Team + + @endif + + @endif + + @feature(App\Features\ShowPlugins::class) + + + Hub + + + My Plugins + + + Settings + + + @endfeature + + + + + @php $unreadCount = auth()->user()->unreadNotifications()->count(); @endphp + + + + Notifications + + + + Integrations + + + + Manage Subscription + + + + + + + + + Settings + + Log out + + + + + + + + {{-- Mobile header with sidebar toggle --}} + + + + + + + + @if ($unreadCount > 0) + + @endif + + + + + + + Settings + + Log out + + + + + + + {{ $slot }} + + + + + @livewireScriptConfig + @fluxScripts + @vite('resources/js/app.js') + + diff --git a/resources/views/components/link-button.blade.php b/resources/views/components/link-button.blade.php deleted file mode 100644 index 987b617a..00000000 --- a/resources/views/components/link-button.blade.php +++ /dev/null @@ -1,16 +0,0 @@ -class([' -inline-block -w-fit -font-medium -shadow-sm dark:shadow-white/10 -border border-gray-300 dark:border-transparent -bg-white dark:bg-gray-700 -rounded-lg -px-4 py-2 -text-black dark:text-gray-100 -hover:text-[#00aaa6] hover:border-[#00aaa6]/25 dark:hover:border-transparent -flex items-center gap-2 -text-sm -']) }}> - {{ $slot }} - diff --git a/resources/views/components/link-subtle.blade.php b/resources/views/components/link-subtle.blade.php deleted file mode 100644 index 417ec8fb..00000000 --- a/resources/views/components/link-subtle.blade.php +++ /dev/null @@ -1,3 +0,0 @@ -class('tracking-wide underline underline-offset-4 text-sm text-gray-500 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-100')}}> - {{$slot}} - diff --git a/resources/views/components/logo.blade.php b/resources/views/components/logo.blade.php new file mode 100644 index 00000000..25b6837d --- /dev/null +++ b/resources/views/components/logo.blade.php @@ -0,0 +1,26 @@ + + + + + + diff --git a/resources/views/components/logos/bifrost.blade.php b/resources/views/components/logos/bifrost.blade.php new file mode 100644 index 00000000..06aeb78b --- /dev/null +++ b/resources/views/components/logos/bifrost.blade.php @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/views/components/mini-logo.blade.php b/resources/views/components/mini-logo.blade.php new file mode 100644 index 00000000..ca59e167 --- /dev/null +++ b/resources/views/components/mini-logo.blade.php @@ -0,0 +1,22 @@ + + + + + diff --git a/resources/views/components/mobile-free-banner.blade.php b/resources/views/components/mobile-free-banner.blade.php new file mode 100644 index 00000000..8e0ffb67 --- /dev/null +++ b/resources/views/components/mobile-free-banner.blade.php @@ -0,0 +1,65 @@ + + {{-- Label --}} +
+ {{-- Icon --}} + + + + + {{-- Text --}} +
+ +
+ NativePHP for Mobile is now completely free and open source! +
+
+
+ + {{-- Arrow --}} +
+ +
+
diff --git a/resources/views/components/navbar/device-dropdown-item.blade.php b/resources/views/components/navbar/device-dropdown-item.blade.php new file mode 100644 index 00000000..d1851ca7 --- /dev/null +++ b/resources/views/components/navbar/device-dropdown-item.blade.php @@ -0,0 +1,47 @@ +@props([ + 'href' => '#', + 'title' => '', + 'subtitle' => null, + 'icon' => null, + 'github' => null, + 'tooltip' => null, + 'iconClass' => 'size-5', +]) + + +
+ @if ($icon) + + @endif +
+ +
+ @if ($title) +
{{ $title }}
+ @endif + + @if ($subtitle) +
+ {{ $subtitle }} +
+ @endif + + +
+
diff --git a/resources/views/components/navbar/device-dropdown.blade.php b/resources/views/components/navbar/device-dropdown.blade.php new file mode 100644 index 00000000..7fd6918c --- /dev/null +++ b/resources/views/components/navbar/device-dropdown.blade.php @@ -0,0 +1,159 @@ +@php + /** + * Props: + * - string $label The dropdown button label (e.g., "Desktop", "Mobile"). + * - string $icon The icon component name from x-icons.* (e.g., 'pc', 'tablet-smartphone'). + * - ?string $id Optional unique id base to avoid collisions across multiple dropdowns. + */ + $label = $label ?? 'Devices'; + $icon = $icon ?? 'pc'; + $base = $id ?? \Illuminate\Support\Str::slug($label) . '-' . \Illuminate\Support\Str::random(6); + $buttonId = $base . '-btn'; + $menuId = $base . '-menu'; + $align = $align ?? 'left'; // 'left', 'center' +@endphp + +
+ + + +
diff --git a/resources/views/components/navbar/device-dropdowns.blade.php b/resources/views/components/navbar/device-dropdowns.blade.php new file mode 100644 index 00000000..37f2591a --- /dev/null +++ b/resources/views/components/navbar/device-dropdowns.blade.php @@ -0,0 +1,73 @@ +@php + $showShowcase = \App\Models\Showcase::approved()->count() >= 4; +@endphp + + diff --git a/resources/views/components/navbar/mobile-menu.blade.php b/resources/views/components/navbar/mobile-menu.blade.php new file mode 100644 index 00000000..e2a6d95d --- /dev/null +++ b/resources/views/components/navbar/mobile-menu.blade.php @@ -0,0 +1,473 @@ +
+ + + +
+
diff --git a/resources/views/components/navbar/theme-toggle.blade.php b/resources/views/components/navbar/theme-toggle.blade.php new file mode 100644 index 00000000..3661b171 --- /dev/null +++ b/resources/views/components/navbar/theme-toggle.blade.php @@ -0,0 +1,273 @@ +
+ + Toggle theme + + + {{-- System icon --}} + + {{-- Animated moon/sun group (hidden in system mode) --}} + + + {{-- Hover hint: Current -> Next preference --}} + +
diff --git a/resources/views/components/navigation-bar.blade.php b/resources/views/components/navigation-bar.blade.php new file mode 100644 index 00000000..9f04ad33 --- /dev/null +++ b/resources/views/components/navigation-bar.blade.php @@ -0,0 +1,106 @@ + diff --git a/resources/views/components/plugin-card.blade.php b/resources/views/components/plugin-card.blade.php new file mode 100644 index 00000000..27911fb4 --- /dev/null +++ b/resources/views/components/plugin-card.blade.php @@ -0,0 +1,55 @@ +@props(['plugin']) + + +
+ @if ($plugin->hasLogo()) + {{ $plugin->name }} logo + @elseif ($plugin->hasGradientIcon()) +
+ +
+ @else +
+ +
+ @endif + @if ($plugin->isPaid() && $plugin->isOfficial() && auth()->user()?->hasUltraAccess()) + + Included with Ultra + + @elseif ($plugin->isPaid()) + + Paid + + @else + + Free + + @endif +
+ +
+

+ {{ $plugin->name }} +

+ @if ($plugin->description) +

+ {{ $plugin->description }} +

+ @endif +
+ +
+ View details + + + +
+
diff --git a/resources/views/components/plugin-toc.blade.php b/resources/views/components/plugin-toc.blade.php new file mode 100644 index 00000000..de095caa --- /dev/null +++ b/resources/views/components/plugin-toc.blade.php @@ -0,0 +1,43 @@ +
+ + + + On this page + + + + + + +
diff --git a/resources/views/components/price-drop-banner.blade.php b/resources/views/components/price-drop-banner.blade.php new file mode 100644 index 00000000..7ee6a121 --- /dev/null +++ b/resources/views/components/price-drop-banner.blade.php @@ -0,0 +1,59 @@ + + {{-- Label --}} +
+ {{-- Icon --}} + + + {{-- Text --}} +
+ +
+ Mobile Pro and Max licenses now up to 65% cheaper! +
+
+
+ + {{-- Icon --}} +
+ +
+
diff --git a/resources/views/components/pricing-plan-features.blade.php b/resources/views/components/pricing-plan-features.blade.php new file mode 100644 index 00000000..36d03343 --- /dev/null +++ b/resources/views/components/pricing-plan-features.blade.php @@ -0,0 +1,148 @@ +@props([ + 'features' => [ + 'apps' => 1, + 'keys' => 1, + ], +]) + +{{-- Features --}} +
+
+
+
+
+
+
+ @if($features['teams'] ?? false) +
+
+ @endif +
+ +{{-- Divider - Decorative --}} + + +{{-- Perks --}} +
+
+ +
One year of package updates
+
+
+ +
Community support via Discord
+
+
+ +
Access Private Discord channels
+
+
+ +
Direct repo access on GitHub
+
+
+ +
Help decide feature priority
+
+
+ +
Business hours email support (GMT)
+
+
diff --git a/resources/views/components/pricing-plan.blade.php b/resources/views/components/pricing-plan.blade.php new file mode 100644 index 00000000..353b1a57 --- /dev/null +++ b/resources/views/components/pricing-plan.blade.php @@ -0,0 +1,69 @@ +
+ @if($popular ?? false) + {{-- Popular badge --}} +
+ Most Popular +
+ @endif + + {{-- Plan Name --}} +

+ + {{ $name }} + +

+ + {{-- Price --}} +
+ +
+ @if($discounted) + + Was ${{ number_format($price) }} + + @endif +
+ ${{ number_format($discounted ? $discountedPrice : $price) }} +
+
+
per year
+
+ + @auth + + @else + + @endauth + + +
diff --git a/resources/views/components/showcase-card.blade.php b/resources/views/components/showcase-card.blade.php new file mode 100644 index 00000000..27490e9d --- /dev/null +++ b/resources/views/components/showcase-card.blade.php @@ -0,0 +1,314 @@ +@props(['showcase']) + +
+ {{-- NEW Badge --}} + @if($showcase->isNew()) +
+ + NEW + +
+ @endif + + {{-- Screenshot Carousel --}} +
+ @if($showcase->screenshots && count($showcase->screenshots) > 0) +
+ @foreach($showcase->screenshots as $index => $screenshot) + + @endforeach + + {{-- Navigation Arrows --}} + +
+ @elseif($showcase->image) + {{ $showcase->title }} + @else +
+ + + +
+ @endif +
+ + {{-- Content --}} +
+ {{-- Header with icon and title --}} +
+ @if($showcase->image) + {{ $showcase->title }} icon + @else +
+ {{ substr($showcase->title, 0, 1) }} +
+ @endif + +
+

+ {{ $showcase->title }} +

+ + {{-- Platform badges --}} +
+ @if($showcase->has_mobile) + + + + + Mobile + + @endif + @if($showcase->has_desktop) + + + + + Desktop + + @endif +
+
+
+ + {{-- Description --}} +

+ {{ $showcase->description }} +

+ + {{-- Download/Store Links --}} +
+ @if($showcase->has_mobile) + @if($showcase->app_store_url) + + + App Store + + @endif + @if($showcase->play_store_url) + + + Play Store + + @endif + @endif + + @if($showcase->has_desktop) + @if($showcase->macos_download_url) + + + macOS + + @endif + @if($showcase->windows_download_url) + + + Windows + + @endif + @if($showcase->linux_download_url) + + + Linux + + @endif + @endif +
+
+ + {{-- Lightbox Modal --}} + +
diff --git a/resources/views/components/sidebar-left-navigation.blade.php b/resources/views/components/sidebar-left-navigation.blade.php deleted file mode 100644 index 4e06eb34..00000000 --- a/resources/views/components/sidebar-left-navigation.blade.php +++ /dev/null @@ -1,36 +0,0 @@ - - - - diff --git a/resources/views/components/sidebar-right.blade.php b/resources/views/components/sidebar-right.blade.php deleted file mode 100644 index 0e9e0909..00000000 --- a/resources/views/components/sidebar-right.blade.php +++ /dev/null @@ -1,15 +0,0 @@ - diff --git a/resources/views/components/sidebar-title.blade.php b/resources/views/components/sidebar-title.blade.php deleted file mode 100644 index d8d4389e..00000000 --- a/resources/views/components/sidebar-title.blade.php +++ /dev/null @@ -1,3 +0,0 @@ -
class(['uppercase text-sm font-semibold pb-1 mb-2 text-gray-400 dark:text-gray-500 border-b dark:border-gray-800']) }}> - {{ $slot }} -
diff --git a/resources/views/components/snippet.blade.php b/resources/views/components/snippet.blade.php new file mode 100644 index 00000000..67519d6f --- /dev/null +++ b/resources/views/components/snippet.blade.php @@ -0,0 +1,32 @@ +@props([ + 'title' => null, +]) +
merge(['class' => 'snippet not-prose my-6']) }} x-data="{ + activeTab: null, + tabs: [], + init() { + const container = this.$el.querySelector('.snippet-content'); + if (container) { + this.tabs = Array.from(container.querySelectorAll('[data-snippet-tab]')); + if (this.tabs.length > 0) { + this.activeTab = this.tabs[0].dataset.snippetTab; + } + } + }, + setActiveTab(tab) { + this.activeTab = tab; + }, + isActive(tab) { + return this.activeTab === tab; + } +}"> +
+ +
+@if ($title)
{{ $title }}
@endif +
+{{ $slot }} +
+
diff --git a/resources/views/components/snippet/tab.blade.php b/resources/views/components/snippet/tab.blade.php new file mode 100644 index 00000000..db8fa152 --- /dev/null +++ b/resources/views/components/snippet/tab.blade.php @@ -0,0 +1,16 @@ +@props([ + 'name', +]) +
merge(['class' => 'snippet-tab']) }} data-snippet-tab="{{ $name }}" x-show="isActive('{{ $name }}')" x-cloak> +
+
+Copied! + +
+
+{{ $slot }} +
+
+
diff --git a/resources/views/components/social-networks-all.blade.php b/resources/views/components/social-networks-all.blade.php index 5fea9273..4c6421f9 100644 --- a/resources/views/components/social-networks-all.blade.php +++ b/resources/views/components/social-networks-all.blade.php @@ -1,24 +1,95 @@ + - - - + - - - + - - - + - - - + - - - + - - - + + + diff --git a/resources/views/components/sponsors-corporate.blade.php b/resources/views/components/sponsors-corporate.blade.php deleted file mode 100644 index 67b3770e..00000000 --- a/resources/views/components/sponsors-corporate.blade.php +++ /dev/null @@ -1,23 +0,0 @@ -@props(['height' => 'h-12']) - - - - - - - - - -{{-- - - - ---}} - - - - - - - - diff --git a/resources/views/components/sponsors-featured.blade.php b/resources/views/components/sponsors-featured.blade.php deleted file mode 100644 index 132fa6b2..00000000 --- a/resources/views/components/sponsors-featured.blade.php +++ /dev/null @@ -1,9 +0,0 @@ -@props(['height' => 'h-24', 'sameHeight' => true]) - - - - - - - - diff --git a/resources/views/components/sponsors/borah.blade.php b/resources/views/components/sponsors/borah.blade.php deleted file mode 100644 index 37298c7e..00000000 --- a/resources/views/components/sponsors/borah.blade.php +++ /dev/null @@ -1 +0,0 @@ - diff --git a/resources/views/components/sponsors/kaashosting.blade.php b/resources/views/components/sponsors/kaashosting.blade.php deleted file mode 100644 index 70c89fe9..00000000 --- a/resources/views/components/sponsors/kaashosting.blade.php +++ /dev/null @@ -1 +0,0 @@ - diff --git a/resources/views/components/sponsors/laradir.blade.php b/resources/views/components/sponsors/laradir.blade.php deleted file mode 100644 index 9f239e45..00000000 --- a/resources/views/components/sponsors/laradir.blade.php +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/resources/views/components/sponsors/lists/docs/corporate-sponsors.blade.php b/resources/views/components/sponsors/lists/docs/corporate-sponsors.blade.php new file mode 100644 index 00000000..ae2b7218 --- /dev/null +++ b/resources/views/components/sponsors/lists/docs/corporate-sponsors.blade.php @@ -0,0 +1,48 @@ +@php + $sponsors = [ + [ + 'url' => 'https://sevalla.com/?utm_source=nativephp&utm_medium=Referral&utm_campaign=homepage', + 'title' => 'Learn more about Sevalla', + 'label' => 'Visit Sevalla website', + 'component' => 'sponsors.logos.sevalla', + 'class' => 'h-auto max-h-8 max-w-full text-black dark:text-white', + 'alt' => 'Sevalla logo', + 'name' => 'Sevalla', + ], + [ + 'url' => 'https://www.kaashosting.nl/?lang=en', + 'title' => 'Learn more about KaasHosting', + 'label' => 'Visit KaasHosting website', + 'component' => 'sponsors.logos.kaashosting', + 'class' => 'block h-auto max-h-8 max-w-full fill-[#042340] dark:fill-white', + 'alt' => 'KaasHosting logo', + 'name' => 'KaasHosting', + ], + [ + 'url' => 'https://www.quantumweb.co/', + 'title' => 'Learn more about Quantumweb', + 'label' => 'Visit Quantumweb website', + 'component' => 'sponsors.logos.quantumweb', + 'class' => 'block h-auto max-h-8 max-w-full fill-[#042340] dark:fill-white', + 'alt' => 'Quantumweb logo', + 'name' => 'Quantumweb', + ], + ]; + + $randomSponsor = $sponsors[array_rand($sponsors)]; +@endphp + + + + {{ $randomSponsor['name'] }} + diff --git a/resources/views/components/sponsors/lists/docs/featured-sponsors.blade.php b/resources/views/components/sponsors/lists/docs/featured-sponsors.blade.php new file mode 100644 index 00000000..927f2774 --- /dev/null +++ b/resources/views/components/sponsors/lists/docs/featured-sponsors.blade.php @@ -0,0 +1,75 @@ +@php + $partners = [ + [ + 'url' => 'https://nexcalia.com/?ref=nativephp', + 'name' => 'Nexcalia', + 'tagline' => 'Smart tools for scheduling & visitor management', + 'component' => 'sponsors.logos.nexcalia', + 'class' => 'w-full text-black dark:text-white', + ], + [ + 'url' => 'https://www.webmavens.com/?ref=nativephp', + 'name' => 'Web Mavens', + 'tagline' => 'Build Secure, Scalable Web Apps', + 'component' => 'sponsors.logos.webmavens', + 'class' => 'w-full dark:fill-white', + ], + [ + 'url' => 'https://synergitech.co.uk/partners/nativephp/', + 'name' => 'Synergi Tech', + 'tagline' => 'Bespoke software for complex infrastructure', + 'image' => '/img/sponsors/synergi.svg', + 'imageDark' => '/img/sponsors/synergi-dark.svg', + 'class' => 'w-full', + ], + [ + 'url' => 'https://laradevs.com/?ref=nativephp', + 'name' => 'Laradevs', + 'tagline' => 'Hire the best Laravel developers anywhere', + 'component' => 'sponsors.logos.laradevs', + 'class' => 'w-full text-black dark:text-white', + ], + [ + 'url' => 'https://beyondco.de/?utm_source=nativephp&utm_medium=logo&utm_campaign=nativephp', + 'name' => 'BeyondCode', + 'tagline' => 'Essential tools for web developers', + 'image' => '/img/sponsors/beyondcode.webp', + 'imageDark' => '/img/sponsors/beyondcode-dark.webp', + 'class' => 'w-full', + ], + ]; + + $partner = $partners[array_rand($partners)]; +@endphp + + +
+ @if (isset($partner['component'])) +
+ {{ $partner['tagline'] }} +
diff --git a/resources/views/components/sponsors/lists/home/corporate-sponsors.blade.php b/resources/views/components/sponsors/lists/home/corporate-sponsors.blade.php new file mode 100644 index 00000000..d686504e --- /dev/null +++ b/resources/views/components/sponsors/lists/home/corporate-sponsors.blade.php @@ -0,0 +1,119 @@ +{{-- +--}} + + + +{{-- + +--}} + + + + + +{{-- + +--}} diff --git a/resources/views/components/sponsors/logos/borah.blade.php b/resources/views/components/sponsors/logos/borah.blade.php new file mode 100644 index 00000000..d05b874b --- /dev/null +++ b/resources/views/components/sponsors/logos/borah.blade.php @@ -0,0 +1,6 @@ + diff --git a/resources/views/components/sponsors/logos/kaashosting.blade.php b/resources/views/components/sponsors/logos/kaashosting.blade.php new file mode 100644 index 00000000..f8d3653a --- /dev/null +++ b/resources/views/components/sponsors/logos/kaashosting.blade.php @@ -0,0 +1,50 @@ + + + + + + + + + diff --git a/resources/views/components/sponsors/logos/laradevs.blade.php b/resources/views/components/sponsors/logos/laradevs.blade.php new file mode 100644 index 00000000..7d5b4034 --- /dev/null +++ b/resources/views/components/sponsors/logos/laradevs.blade.php @@ -0,0 +1,22 @@ + + + + + diff --git a/resources/views/components/sponsors/logos/nexcalia.blade.php b/resources/views/components/sponsors/logos/nexcalia.blade.php new file mode 100644 index 00000000..d46d724a --- /dev/null +++ b/resources/views/components/sponsors/logos/nexcalia.blade.php @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/views/components/sponsors/logos/quantumweb.blade.php b/resources/views/components/sponsors/logos/quantumweb.blade.php new file mode 100644 index 00000000..7735e6c6 --- /dev/null +++ b/resources/views/components/sponsors/logos/quantumweb.blade.php @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + diff --git a/resources/views/components/sponsors/logos/redgalaxy.blade.php b/resources/views/components/sponsors/logos/redgalaxy.blade.php new file mode 100644 index 00000000..bf428e68 --- /dev/null +++ b/resources/views/components/sponsors/logos/redgalaxy.blade.php @@ -0,0 +1,38 @@ + + + + + + + diff --git a/resources/views/components/sponsors/serverauth.blade.php b/resources/views/components/sponsors/logos/serverauth.blade.php similarity index 100% rename from resources/views/components/sponsors/serverauth.blade.php rename to resources/views/components/sponsors/logos/serverauth.blade.php diff --git a/resources/views/components/sponsors/sevalla.blade.php b/resources/views/components/sponsors/logos/sevalla.blade.php similarity index 100% rename from resources/views/components/sponsors/sevalla.blade.php rename to resources/views/components/sponsors/logos/sevalla.blade.php diff --git a/resources/views/components/sponsors/logos/webmavens.blade.php b/resources/views/components/sponsors/logos/webmavens.blade.php new file mode 100644 index 00000000..f5338e39 --- /dev/null +++ b/resources/views/components/sponsors/logos/webmavens.blade.php @@ -0,0 +1,3 @@ + + + diff --git a/resources/views/components/sponsors/redgalaxy.blade.php b/resources/views/components/sponsors/redgalaxy.blade.php deleted file mode 100644 index 16f03fd1..00000000 --- a/resources/views/components/sponsors/redgalaxy.blade.php +++ /dev/null @@ -1 +0,0 @@ - diff --git a/resources/views/components/testimonial.blade.php b/resources/views/components/testimonial.blade.php new file mode 100644 index 00000000..56b0300e --- /dev/null +++ b/resources/views/components/testimonial.blade.php @@ -0,0 +1,73 @@ +@props([ + 'quote', + 'author', + 'handle', + 'content', + 'avatar', +]) + +@php + // Define color palettes for light mode only + $lightColorPalettes = [ + ['bg' => 'bg-cyan-100', 'text' => 'text-cyan-800'], + ['bg' => 'bg-amber-100', 'text' => 'text-amber-800'], + ['bg' => 'bg-red-100', 'text' => 'text-red-800'], + ['bg' => 'bg-emerald-100', 'text' => 'text-emerald-800'], + ['bg' => 'bg-blue-100', 'text' => 'text-blue-800'], + ['bg' => 'bg-purple-100', 'text' => 'text-purple-800'], + ['bg' => 'bg-orange-100', 'text' => 'text-orange-800'], + ['bg' => 'bg-teal-100', 'text' => 'text-teal-800'], + ['bg' => 'bg-slate-100', 'text' => 'text-slate-800'], + ['bg' => 'bg-violet-100', 'text' => 'text-violet-800'], + ]; + + // Select a random light color palette + $randomLightPalette = $lightColorPalettes[array_rand($lightColorPalettes)]; + + // Define a single consistent dark mode color palette + $darkBg = 'dark:bg-indigo-900/20'; + $darkText = 'dark:text-indigo-200'; +@endphp + +
+ {{-- Highlight --}} +
+ "{{ $quote }}" +
+ + {{-- Author --}} +
+ {{-- Image --}} + {{ $author }} + + {{-- Information --}} +
+ {{-- Name --}} +
{{ $author }}
+ + @if ($handle) + {{-- Handle --}} +
+ {{ $handle }} +
+ @endif +
+
+ + {{-- Content --}} + @if ($content) +

+ {{ $content }} +

+ @endif +
diff --git a/resources/views/components/testimonials.blade.php b/resources/views/components/testimonials.blade.php new file mode 100644 index 00000000..e867c70e --- /dev/null +++ b/resources/views/components/testimonials.blade.php @@ -0,0 +1,139 @@ +
+
+ {{-- Section Heading --}} +

+ +
Testimonials
+

+ + {{-- Section Description --}} +

+ Here's what folks are saying about NativePHP for Mobile +

+
+ + {{-- Testimonial List --}} +
+ + + + + + + + + + + +
+
diff --git a/resources/views/components/the-vibes-banner.blade.php b/resources/views/components/the-vibes-banner.blade.php new file mode 100644 index 00000000..a258503d --- /dev/null +++ b/resources/views/components/the-vibes-banner.blade.php @@ -0,0 +1,65 @@ + + {{-- Label --}} +
+ {{-- Icon --}} + + + + + {{-- Text --}} +
+ +
+ July 30, 2026 — The unofficial Laracon US Day 3. Get your ticket to The Vibes +
+
+
+ + {{-- Arrow --}} +
+ +
+
diff --git a/resources/views/components/toc-and-sponsors.blade.php b/resources/views/components/toc-and-sponsors.blade.php deleted file mode 100644 index 5282d18e..00000000 --- a/resources/views/components/toc-and-sponsors.blade.php +++ /dev/null @@ -1,26 +0,0 @@ - - On this page - @if (count($tableOfContents) > 0) - - @endif - - Featured sponsors -
- -
- - Corporate sponsors -
- -
diff --git a/resources/views/components/ultra-plan.blade.php b/resources/views/components/ultra-plan.blade.php new file mode 100644 index 00000000..f81c99b3 --- /dev/null +++ b/resources/views/components/ultra-plan.blade.php @@ -0,0 +1,51 @@ +
+
+ {{-- Plan Name --}} +

+ + Ultra + + Partners get even more! +

+ +

+ Ultra is our Partnership Program, offering + dedicated support, feature development, + training, marketing opportunities + and other enterprise-oriented services for teams of any size. +

+ +
+
diff --git a/resources/views/components/wall-of-love/early-adopter-card.blade.php b/resources/views/components/wall-of-love/early-adopter-card.blade.php new file mode 100644 index 00000000..ffb7a731 --- /dev/null +++ b/resources/views/components/wall-of-love/early-adopter-card.blade.php @@ -0,0 +1,90 @@ +@props([ + 'name' => '', + 'title' => '', + 'url' => '', + 'image' => '', + 'featured' => false, + 'hasUserImage' => false, +]) + +
+
+ {{-- Name & Title --}} +
$featured, + 'from-black/70 to-transparent' => ! $featured, + ]) + > +
+

$featured, + ]) + itemprop="name" + > + {{ $name }} +

+

$featured, + 'text-xs' => ! $featured, + ]) + @if($title) itemprop="jobTitle" @endif + > + {{ $title }} +

+
+
+ + {{-- Image --}} + {{ $title ? $name . ', ' . $title : $name }} $featured, + 'aspect-square max-h-50 xl:max-h-none' => ! $featured, + 'grayscale-50 dark:brightness-50' => ! $hasUserImage, + ]) + /> + + {{-- Loading placeholder --}} + + + {{-- External link --}} + @if ($url) + + @endif +
+
diff --git a/resources/views/course.blade.php b/resources/views/course.blade.php new file mode 100644 index 00000000..6c69d5c0 --- /dev/null +++ b/resources/views/course.blade.php @@ -0,0 +1,789 @@ + +
+ {{-- Hero Section --}} +
+
+ {{-- Badge --}} +
+ + + + + Early Bird Pricing Available +
+ + {{-- Title --}} +

+ The + + NativePHP + +
+ Masterclass +

+ + {{-- Subtitle --}} +

+ Go from zero to published app. Learn to build native mobile + and desktop applications using the PHP and Laravel skills you + already have. +

+ + {{-- CTA --}} +
+ @if ($alreadyOwned) +
+ + + + You Own This Course +
+ @else + + Get Early Bird Access — $101 + + + + + + or join the waitlist + + @endif +
+
+
+ + {{-- What You'll Learn --}} +
+
+

+ What You'll Learn +

+

+ A complete curriculum taking you from setup to the app stores +

+
+ +
+ {{-- Module 1 --}} +
+
+ + + +
+

+ Getting Started +

+

+ Install NativePHP, configure your environment for mobile + and desktop, and run your first native app in minutes. +

+
+ + {{-- Module 2 --}} +
+
+ + + + +
+

+ Building for Mobile +

+

+ Build iOS and Android apps with Livewire and Blade. + Learn navigation patterns, gestures, and mobile-first UI. +

+
+ + {{-- Module 3 --}} +
+
+ + + +
+

+ Building for Desktop +

+

+ Create macOS, Windows, and Linux desktop apps. Window + management, menus, system tray, and file system access. +

+
+ + {{-- Module 4 --}} +
+
+ + + + +
+

+ Native APIs +

+

+ Access the camera, push notifications, biometrics, + haptics, sharing, and more — all from PHP. +

+
+ + {{-- Module 5 --}} +
+
+ + + +
+

+ Plugins & Extensibility +

+

+ Extend your app with the NativePHP plugin ecosystem. + Learn to use and build plugins for custom native features. +

+
+ + {{-- Module 6 --}} +
+
+ + + + +
+

+ Deploy & Publish +

+

+ Ship to the App Store, Google Play, and desktop platforms. + Use Bifrost for cloud builds and continuous deployment. +

+
+
+
+ + {{-- Who It's For --}} +
+
+

+ Who Is This For? +

+

+ This masterclass is built for developers who want to go + native without starting from scratch +

+
+ +
+ {{-- Persona 1 --}} +
+
+ + + +
+

+ Laravel Developers +

+

+ You already build web apps with Laravel. Now you want to + ship real native mobile and desktop apps — without + learning Swift, Kotlin, or Dart. +

+
+ + {{-- Persona 2 --}} +
+
+ + + +
+

+ PHP Developers +

+

+ You know PHP inside and out. This course shows you how + to leverage that expertise to build apps that run natively + on any device. +

+
+ + {{-- Persona 3 --}} +
+
+ + + + +
+

+ Web Developers +

+

+ Tired of being told you need to learn a completely new + stack for native apps? This course proves you don't. +

+
+
+
+ + {{-- Pricing --}} +
+
+

+ Simple Pricing +

+

+ One price. Full access. No subscriptions. +

+
+ +
+
+ {{-- Badge --}} +
+ EARLY BIRD +
+ +

+ The NativePHP Masterclass +

+ + @if ($alreadyOwned) +
+
+ + + +
+

You Own This Course

+

+ You'll be notified when the course launches. +

+
+ @else +
+ + $101 + + + one-time payment + +
+ @endif + +
    +
  • + + Full mobile + desktop curriculum +
  • +
  • + + Lifetime access to all content +
  • +
  • + + Future updates included +
  • +
  • + + Source code for all example projects +
  • +
  • + + Access to private community +
  • +
+ + @unless ($alreadyOwned) +
+ @csrf + +
+ +

+ Early bird pricing won't last forever. Lock in the lowest price today. +

+ @endunless +
+
+
+ + {{-- Timeline / Availability --}} +
+
+

+ When Can I Start? +

+

+ The NativePHP Masterclass is coming Summer/Fall 2026. + We're putting the finishing touches on the content to make sure it's the best learning experience possible. +

+

+ Grab the early bird price now and you'll be first in line when the doors open. + Sign up below to stay in the loop. +

+
+
+ + {{-- Email Signup --}} +
+
+

+ Not Ready to Buy? +

+

+ Join the waitlist and we'll let you know when the masterclass + launches, plus get exclusive early access content. +

+ +
+ + + {{-- Honeypot --}} +
+ + +
+ + +
+
+
+
+ + @auth + @if (request('checkout') && ! $alreadyOwned) + + @endif + @endauth +
diff --git a/resources/views/customer/team/index.blade.php b/resources/views/customer/team/index.blade.php new file mode 100644 index 00000000..2a94c4d2 --- /dev/null +++ b/resources/views/customer/team/index.blade.php @@ -0,0 +1,94 @@ + +
+
+ @if($team) +
+ {{ $team->name }} + +
+ Manage your team and share your Ultra benefits + + {{-- Inline Team Name Edit --}} +
+
+ @csrf + @method('PATCH') +
+ + @error('name') + {{ $message }} + @enderror +
+ Save + Cancel +
+
+ @else + Team + Manage your team and share your Ultra benefits + @endif +
+ + {{-- Flash Messages --}} + @if(session('success')) + + {{ session('success') }} + + @endif + + @if(session('error')) + + {{ session('error') }} + + @endif + +
+ @if($team) + {{-- User owns a team --}} + + @elseif($membership) + {{-- User is a member of another team --}} + + Team Membership + + You are a member of {{ $membership->team->name }}. + +
+ Your benefits include: +
    +
  • Free access to all first-party NativePHP plugins
  • +
  • Access to the Plugin Dev Kit GitHub repository
  • +
+
+
+ @elseif(auth()->user()->hasActiveUltraSubscription()) + {{-- User has Ultra but no team --}} + + Create a Team + As an Ultra subscriber, you can create a team and invite up to {{ config('subscriptions.plans.max.included_seats') - 1 }} members who will share your benefits ({{ config('subscriptions.plans.max.included_seats') }} seats total, including you). + +
+ @csrf +
+
+ +
+ Create Team +
+ @error('name') + {{ $message }} + @enderror +
+
+ @else + {{-- User doesn't have Ultra --}} + + Teams + Teams are available to Ultra subscribers. Upgrade to Ultra to create a team and share benefits with up to {{ config('subscriptions.plans.max.included_seats') - 1 }} members ({{ config('subscriptions.plans.max.included_seats') }} seats total). + + View Plans + + @endif +
+
+
diff --git a/resources/views/customer/team/show.blade.php b/resources/views/customer/team/show.blade.php new file mode 100644 index 00000000..ab3c7b92 --- /dev/null +++ b/resources/views/customer/team/show.blade.php @@ -0,0 +1,77 @@ + +
+
+ {{ $team->name }} + Your team membership benefits +
+ +
+ {{-- Membership Info --}} + + Team Membership + + You are a member of {{ $team->name }}, managed by {{ $team->owner->display_name }}. + + @if($membership->accepted_at) + + Joined {{ $membership->accepted_at->diffForHumans() }} + + @endif + + + {{-- Benefits --}} + + Your Benefits +
    +
  • Free access to all first-party NativePHP plugins
  • +
  • Access to the Plugin Dev Kit GitHub repository — Set up access via Integrations
  • +
  • Access to plugins purchased by your team owner
  • +
+
+ + {{-- Accessible Plugins --}} + @if($plugins->isNotEmpty()) + Accessible Plugins + + + @foreach($plugins as $plugin) + + +
+
+ @if($plugin->hasLogo()) + {{ $plugin->name }} + @elseif($plugin->hasGradientIcon()) +
+ +
+ @else +
+ +
+ @endif +
+
+ + {{ $plugin->name }} + + @if($plugin->description) + {{ $plugin->description }} + @endif +
+
+
+
+ @endforeach +
+
+ @else + + @endif +
+
+
diff --git a/resources/views/customer/ultra/index.blade.php b/resources/views/customer/ultra/index.blade.php new file mode 100644 index 00000000..bfae9a1c --- /dev/null +++ b/resources/views/customer/ultra/index.blade.php @@ -0,0 +1,170 @@ + +
+
+
+ +
+
+ Ultra + Your premium subscription benefits +
+
+ +
+ {{-- Subscription Status --}} + +
+
+ Subscription + + @if($subscription->stripe_price === config('subscriptions.plans.max.stripe_price_id_monthly')) + ${{ config('subscriptions.plans.max.price_monthly') }}/month + @elseif($subscription->stripe_price === config('subscriptions.plans.max.stripe_price_id_eap')) + ${{ config('subscriptions.plans.max.eap_price_yearly') }}/year (Early Access) + @else + ${{ config('subscriptions.plans.max.price_yearly') }}/year + @endif + +
+ + Manage + +
+
+ + {{-- Benefits --}} + + Your Benefits + Everything included with your Ultra subscription. + +
+
+
+ +
+
+
All first-party plugins
+ Every NativePHP-published plugin is included at no extra cost. New plugins are added automatically. +
+
+ +
+
+ +
+
+
Claude Code Plugin Dev Kit
+ Tools and resources to build NativePHP plugins using Claude Code. + Set up access via Integrations +
+
+ +
+
+ +
+
+
Teams
+ + Invite up to {{ config('subscriptions.plans.max.included_seats') - 1 }} members to share your Ultra benefits ({{ config('subscriptions.plans.max.included_seats') }} seats included). + Extra seats available at ${{ config('subscriptions.plans.max.extra_seat_price_monthly') }}/mo or ${{ config('subscriptions.plans.max.extra_seat_price_yearly') }}/mo on annual plans. + + Manage team +
+
+ +
+
+ +
+
+
Premium support
+ Private support channels with expedited turnaround on your issues. + Support tickets +
+
+ +
+
+ +
+
+
Up to 90% Marketplace revenue
+ Keep up to 90% of earnings on paid plugins you publish to the Marketplace. +
+
+ +
+
+ +
+
+
Exclusive discounts
+ Discounted pricing on NativePHP courses, apps, and future products. +
+
+ +
+
+ +
+
+
Direct repo access on GitHub
+ Access the NativePHP source code repositories directly. +
+
+ +
+
+ +
+
+
Shape the roadmap
+ Help decide feature priority and influence what gets built next. +
+
+
+
+ + {{-- Included Plugins --}} + @if($plugins->isNotEmpty()) +
+ Included Plugins + + + @foreach($plugins as $plugin) + + +
+
+ @if($plugin->hasLogo()) + {{ $plugin->name }} + @elseif($plugin->hasGradientIcon()) +
+ +
+ @else +
+ +
+ @endif +
+
+ + {{ $plugin->name }} + + @if($plugin->description) + {{ $plugin->description }} + @endif +
+
+
+
+ @endforeach +
+
+
+ @endif +
+
+
diff --git a/resources/views/developer-terms.blade.php b/resources/views/developer-terms.blade.php new file mode 100644 index 00000000..b45910f9 --- /dev/null +++ b/resources/views/developer-terms.blade.php @@ -0,0 +1,494 @@ + + {{-- Hero --}} +
+
+ {{-- Blurred circle - Decorative --}} + + + {{-- Primary Heading --}} +

+ Plugin Developer Terms and Conditions +

+ + {{-- Date --}} +
+ Last updated +
+
+ + {{-- Divider --}} + + + {{-- Content --}} +
+

+ These Plugin Developer Terms and Conditions ("Developer Terms") + govern your participation as a plugin developer ("Developer", + "you", "your") on the NativePHP Plugin Marketplace (the + "Marketplace") operated by Bifrost Technology, LLC ("NativePHP", + "we", "us", "our"). These Developer Terms are in addition to + and supplement our general + Terms of Service. +

+ +

+ By submitting a plugin to the Marketplace, you acknowledge that + you have read, understood, and agree to be bound by these + Developer Terms. If you do not agree, you may not submit plugins + for sale on the Marketplace. +

+ +

1. Definitions

+ +

For the purposes of these Developer Terms:

+ +
    +
  • + "Plugin" means any software package, + extension, or add-on developed by you and submitted for + listing on the Marketplace. +
  • +
  • + "Customer" means any end user who purchases + or otherwise acquires a license to use a Plugin through the + Marketplace. +
  • +
  • + "Gross Sale Amount" means the total price + paid by a Customer for a Plugin, inclusive of any applicable + taxes but before the deduction of any fees. +
  • +
  • + "Platform Fee" means the commission retained + by NativePHP from the Gross Sale Amount of each Plugin sale. +
  • +
+ +

2. Revenue Share and Platform Fee

+ +

+ NativePHP shall retain a Platform Fee of thirty percent (30%) of + the Gross Sale Amount for each Plugin sale made through the + Marketplace. The remaining seventy percent (70%) shall be paid + to the Developer ("Developer Share"), subject to the payment + terms set out in Section 3. +

+ +

+ The Platform Fee covers the costs of payment processing, + hosting, distribution, platform maintenance, and related + services provided by NativePHP. +

+ +

+ NativePHP reserves the right to modify the Platform Fee + percentage upon thirty (30) days' written notice to the + Developer. Continued participation in the Marketplace after such + notice constitutes acceptance of the revised Platform Fee. +

+ +

3. Payment Terms

+ +

+ Developer payouts shall be processed through Stripe Connect. + As a condition of selling Plugins on the Marketplace, the + Developer must complete the Stripe Connect onboarding process + and maintain an active Stripe Connect account in good standing. +

+ +

+ Developer payouts are subject to a holding period of fifteen + (15) days from the date of each sale. During this period, the + sale proceeds are held by NativePHP to allow for the processing + of any Customer refund requests. Following the holding period, + the Developer Share will be transferred to the Developer's + Stripe Connect account, subject to the Developer's account + being in good standing. +

+ +

+ In the event that a refund is issued to a Customer during the + holding period, the corresponding Developer Share for that sale + will not be paid out. If a refund is issued after the Developer + Share has been transferred, NativePHP reserves the right to + deduct the refunded Developer Share amount from future payouts + or to request repayment from the Developer. +

+ +

+ Payouts of the Developer Share are subject to the processing + timelines and requirements of Stripe. NativePHP shall not be + liable for any delays in payment caused by Stripe or the + Developer's failure to maintain a valid payment account. +

+ +

+ The Developer is solely responsible for all tax obligations + arising from their receipt of the Developer Share, including + income taxes, value added taxes, goods and services taxes, or + any other applicable levies in their jurisdiction. +

+ +

4. Plugin Pricing and Discounts

+ +

+ NativePHP shall have the sole and absolute discretion to set, + adjust, and determine the retail price of each Plugin listed on + the Marketplace. The Developer acknowledges and agrees that + NativePHP may: +

+ +
    +
  • + Set the initial listing price of the Plugin; +
  • +
  • + Modify the retail price of the Plugin at any time without + prior notice to the Developer; +
  • +
  • + Offer discounts, promotional pricing, bundled pricing, or + other price reductions on the Plugin at NativePHP's sole + discretion; +
  • +
  • + Include the Plugin in platform-wide promotions, seasonal + sales, or other marketing campaigns. +
  • +
+ +

+ The Platform Fee and Developer Share shall be calculated based + on the actual price paid by the Customer, which may differ from + the standard listing price due to discounts or promotions + applied by NativePHP. +

+ +

5. Developer Responsibilities and Liability

+ +

+ The Developer is solely and entirely responsible for: +

+ +
    +
  • + The development, quality, performance, maintenance, and + ongoing support of their Plugin; +
  • +
  • + Providing customer support to end users who purchase or use + the Plugin, including responding to bug reports, feature + requests, and technical issues; +
  • +
  • + Ensuring the Plugin does not infringe upon the intellectual + property rights of any third party; +
  • +
  • + Ensuring the Plugin complies with all applicable laws, + regulations, and industry standards; +
  • +
  • + Maintaining accurate and up-to-date documentation for + the Plugin; +
  • +
  • + Ensuring the Plugin does not contain malicious code, + vulnerabilities, or any functionality that could harm + Customers or their systems. +
  • +
+ +

+ NativePHP does not provide any support to Customers in relation + to third-party Plugins. NativePHP shall have no obligation to + assist Customers with installation, configuration, bug + resolution, or any other matter relating to the Developer's + Plugin. +

+ +

+ NativePHP accepts no liability whatsoever for the performance, + reliability, security, compatibility, or fitness for purpose of + any Plugin submitted by a Developer. The Developer shall + indemnify and hold harmless NativePHP, its officers, directors, + employees, and agents from and against any claims, damages, + losses, liabilities, costs, or expenses (including reasonable + legal fees) arising from or in connection with the Developer's + Plugin or any breach of these Developer Terms. +

+ +

6. Listing Criteria and Marketplace Standards

+ +

+ NativePHP reserves the right, in its sole and absolute + discretion, to: +

+ +
    +
  • + Establish, modify, and enforce criteria for the listing of + Plugins on the Marketplace, including but not limited to + technical standards, quality requirements, documentation + standards, and code review processes; +
  • +
  • + Change such listing criteria at any time, with or without + notice to the Developer; +
  • +
  • + Approve or reject any Plugin submitted for listing on the + Marketplace, for any reason or no reason; +
  • +
  • + Remove, suspend, or discontinue the listing of any Plugin + from the Marketplace at any time, for any reason or no + reason, including but not limited to quality concerns, policy + violations, inactivity, Customer complaints, or changes in + platform strategy; +
  • +
  • + Require the Developer to make modifications to their Plugin + as a condition of continued listing. +
  • +
+ +

+ NativePHP shall not be liable to the Developer for any losses, + damages, or lost revenue resulting from the rejection, removal, + suspension, or discontinuation of a Plugin listing. +

+ +

7. Intellectual Property

+ +

+ The Developer retains all ownership rights in and to their + Plugin, subject to the license granted herein. By submitting a + Plugin to the Marketplace, the Developer grants NativePHP a + non-exclusive, worldwide, royalty-free license to host, + distribute, display, market, and promote the Plugin on the + Marketplace and through NativePHP's marketing channels. +

+ +

+ The Developer represents and warrants that they have all + necessary rights, licenses, and permissions to submit the Plugin + to the Marketplace and to grant the license described above. +

+ +

8. Data and Privacy

+ +

+ In the course of processing Plugin sales, certain Customer data + (such as name, email address, and license information) may be + shared with the Developer to facilitate the transaction and + enable the Developer to provide support. The Developer agrees to + handle all Customer data in compliance with applicable data + protection laws, including but not limited to the General Data + Protection Regulation (GDPR) and the California Consumer + Privacy Act (CCPA). +

+ +

+ The Developer shall not use Customer data for any purpose other + than providing support for and delivering updates to their + Plugin, unless the Customer has provided separate, explicit + consent. +

+ +

9. Representations and Warranties

+ +

The Developer represents and warrants that:

+ +
    +
  • + They have the legal capacity and authority to enter into + these Developer Terms; +
  • +
  • + The Plugin is their original work or they have obtained all + necessary licenses and permissions; +
  • +
  • + The Plugin does not violate any applicable law or regulation; +
  • +
  • + The Plugin does not infringe upon any third party's + intellectual property rights; +
  • +
  • + All information provided to NativePHP in connection with + these Developer Terms is accurate and complete. +
  • +
+ +

10. Termination

+ +

+ Either party may terminate these Developer Terms at any time + by providing written notice to the other party. Upon + termination: +

+ +
    +
  • + The Developer's Plugins shall be removed from the + Marketplace within a reasonable time; +
  • +
  • + Existing Customer licenses for the Developer's Plugins shall + remain valid and enforceable; +
  • +
  • + NativePHP shall pay any outstanding Developer Share amounts + for sales completed prior to termination; +
  • +
  • + The Developer's obligations regarding indemnification, + intellectual property, and data protection shall survive + termination. +
  • +
+ +

+ NativePHP may terminate these Developer Terms immediately and + without notice in the event of a material breach by the + Developer. +

+ +

11. Limitation of Liability

+ +

+ To the maximum extent permitted by law, NativePHP shall not be + liable to the Developer for any indirect, incidental, special, + consequential, or punitive damages, including but not limited to + loss of profits, revenue, data, or business opportunities, + arising from or related to these Developer Terms or the + Developer's participation in the Marketplace. +

+ +

+ NativePHP's total aggregate liability under these Developer + Terms shall not exceed the total Developer Share amounts paid to + the Developer in the twelve (12) months preceding the event + giving rise to the claim. +

+ +

12. Amendments

+ +

+ NativePHP reserves the right to modify these Developer Terms at + any time. We will provide notice of material changes by + updating the "Last updated" date at the top of this page and, + where practicable, by notifying the Developer via email. The + Developer's continued participation in the Marketplace after + such changes constitutes acceptance of the amended Developer + Terms. +

+ +

13. Waivers

+ +

+ No delay or failure to exercise any right or remedy provided for + in these Developer Terms shall be deemed to be a waiver. +

+ +

14. Severability

+ +

+ If any provision of these Developer Terms is held invalid or + unenforceable, for any reason, by any arbitrator, court or + governmental agency, department, body or tribunal, the remaining + provisions shall remain in full force and effect. +

+ +

15. Governing Law

+ +

+ These Developer Terms shall be governed by and construed in + accordance with the laws of the State of Delaware, United States + of America, without regard to its conflict of law provisions. +

+ +

16. Entire Agreement

+ +

+ These Developer Terms, together with the + Terms of Service and + Privacy Policy, constitute the + entire agreement between the Developer and NativePHP with + respect to the Developer's participation in the Marketplace and + supersede all prior or contemporaneous communications, + agreements, and understandings, whether written or oral, + relating to the subject matter herein. +

+ +

Contact

+ +

+ If you have any questions regarding these Developer Terms, + please contact us at + support@nativephp.com. +

+
+
+
diff --git a/resources/views/docs/1/getting-started/sponsoring.md b/resources/views/docs/1/getting-started/sponsoring.md deleted file mode 100644 index 0162a5f9..00000000 --- a/resources/views/docs/1/getting-started/sponsoring.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -title: Sponsoring -order: 002 ---- -## Support NativePHP - -NativePHP is wholly dependent on the dedication of its maintainers and contributors, who volunteer their free time to -ensure its continued development and improvement. As we prioritize our paid client work to sustain ourselves, your -support through donations and sponsorships helps us devote more time to the project. - -We realize that not everyone is able to contribute of their time to support with responding to tickets or contributing -features and bugfixes. We are open to contributions of financial support and provide the following ways you can -contribute: - -- [GitHub Sponsors](https://github.com/nativephp/laravel?sponsor=1) -- [OpenCollective](https://opencollective.com/nativephp) - -All contributions are welcome, at any amount, as a one-off payment or on a recurring schedule. - -All monthly sponsors above $10/month will be bestowed the `Sponsor` role on the NativePHP -[Discord](https://discord.gg/X62tWNStZK), granting access to private channels, early access to new releases, and -discounts on future premium services. - -Your contributions help cover the costs of development, infrastructure, and community initiatives. Even a small donation -goes a long way in defraying the expenses of working for free to keep this project alive and thriving. - -Together, we can continue to grow NativePHP and ensure it remains a valuable tool for the community. - -## Early Access Program - -If you're interested in NativePHP for mobile, you can get access right now via the [Early Access Program](/ios). We are -already working on NativePHP for iOS and your support will help us speed up development. - -## Corporate Partners - -If your organization is using NativePHP, we strongly encourage you to consider a Corporate Sponsorship. This level of -support will provide your team with the added benefits of increased levels of support, hands-on help directly from the -maintainers of NativePHP and promotion of your brand as a supporter of cutting-edge open source work. - -For more details, please email [nativephp@simonhamp.me](mailto:nativephp@simonhamp.me?subject=Corporate%20Sponsorship). diff --git a/resources/views/docs/1/getting-started/status.md b/resources/views/docs/1/getting-started/status.md deleted file mode 100644 index 4c055bcf..00000000 --- a/resources/views/docs/1/getting-started/status.md +++ /dev/null @@ -1,41 +0,0 @@ ---- -title: Roadmap -order: 099 ---- - -## Roadmap -We're currently focused on reaching v1. - -We've put together a basic [roadmap](https://github.com/orgs/NativePHP/projects/2/views/1) to help visualize our -current _estimated_ development timeline. - -## Current Status -NativePHP is currently in the `beta` stage. - -We need your help with testing and bug-hunting, so we want you to _build_ apps with NativePHP. - -As we're at the `beta` stage, NativePHP is almost ready for prime-time. This means we think it's ready for -you to distribute your apps to users. But you should be aware of a couple of issues: - -- The PHP source code inside your app can be seen _and modified_. We're actively working on a solution for this in time - for the `v1` release. -- Documentation around signing apps for Microsoft Windows and macOS are not yet available. - -**If you're planning to release an app, it may be useful to get in touch on [Discord](https://discord.gg/X62tWNStZK) so -we can run through any last minute checks to ensure your app is in the best shape.** - -The more parts of the whole process we can exercise to find bugs and fix them, the better. - -Be sure to share your findings through the [forum](https://github.com/orgs/nativephp/discussions), by -[raising issues](https://github.com/nativephp/laravel/issues/new/choose)/reporting bugs, and on -[Discord](https://discord.gg/X62tWNStZK). - - diff --git a/resources/views/docs/1/the-basics/notifications.md b/resources/views/docs/1/the-basics/notifications.md deleted file mode 100644 index f2406603..00000000 --- a/resources/views/docs/1/the-basics/notifications.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -title: Notifications -order: 500 ---- - -## Native Notifications - -NativePHP allows you to send system notifications using an elegant PHP API. These notifications are, unlike Laravel's built-in notifications, actual UI notifications displayed by your operating system. - -When used sparingly, notifications can be a great way to inform the user about events that are occurring in your application and to bring their attention back to it, especially if further input from them is required. - -Notifications are sent using the `Notification` facade. -```php -use Native\Laravel\Facades\Notification; -``` - -### Sending Notifications - -You may send a notification using the `Notification` facade. - -```php -Notification::title('Hello from NativePHP') - ->message('This is a detail message coming from your Laravel app.') - ->show(); -``` - -This will show a system-wide notification to the user with the given title and message. - -### Handling clicks on notifications - -You may register a custom event along with your NativePHP notification. -This event will be fired when a user clicks on the notification, so that you may add some custom logic within your application in this scenario. - -To attach an event to your notification, you may use the `event` method. The argument passed to this method is the class name of the event that should get dispatched upon clicking on the notification. - -```php -Notification::title('Hello from NativePHP') - ->message('This is a detail message coming from your Laravel app.') - ->event(\App\Events\MyNotificationEvent::class) - ->show(); -``` - -## Events - -### `NotificationClicked` -The `Native\Laravel\Events\Notifications\NotificationClicked` event is dispatched when a user clicks on a notification. - diff --git a/resources/views/docs/chooser.blade.php b/resources/views/docs/chooser.blade.php new file mode 100644 index 00000000..f5cbf46a --- /dev/null +++ b/resources/views/docs/chooser.blade.php @@ -0,0 +1,32 @@ + +
+
+

Documentation

+

+ Choose your platform to get started. +

+
+ + +
+
diff --git a/resources/views/docs/1/_index.md b/resources/views/docs/desktop/1/_index.md similarity index 100% rename from resources/views/docs/1/_index.md rename to resources/views/docs/desktop/1/_index.md diff --git a/resources/views/docs/1/digging-deeper/_index.md b/resources/views/docs/desktop/1/digging-deeper/_index.md similarity index 100% rename from resources/views/docs/1/digging-deeper/_index.md rename to resources/views/docs/desktop/1/digging-deeper/_index.md diff --git a/resources/views/docs/1/digging-deeper/broadcasting.md b/resources/views/docs/desktop/1/digging-deeper/broadcasting.md similarity index 94% rename from resources/views/docs/1/digging-deeper/broadcasting.md rename to resources/views/docs/desktop/1/digging-deeper/broadcasting.md index af1bc63c..e357c699 100644 --- a/resources/views/docs/1/digging-deeper/broadcasting.md +++ b/resources/views/docs/desktop/1/digging-deeper/broadcasting.md @@ -5,9 +5,9 @@ order: 100 # Broadcasting -NativePHP facilitates event broadcasting of both [native events](#native-events) (emitted by Electron/Tauri) and +NativePHP facilitates event broadcasting of both [native events](#native-events) (emitted by Electron) and [custom events](#custom-events) dispatched by your Laravel app. You can listen to all of these events in your -Laravel application as you normally would or in the [JavaSscript](#listening-with-javascript) on your pages. +Laravel application as you normally would or in the [JavaScript](#listening-with-javascript) on your pages. ## Native events diff --git a/resources/views/docs/1/digging-deeper/child-processes.md b/resources/views/docs/desktop/1/digging-deeper/child-processes.md similarity index 96% rename from resources/views/docs/1/digging-deeper/child-processes.md rename to resources/views/docs/desktop/1/digging-deeper/child-processes.md index c23a4c5b..ae877000 100644 --- a/resources/views/docs/1/digging-deeper/child-processes.md +++ b/resources/views/docs/desktop/1/digging-deeper/child-processes.md @@ -29,7 +29,7 @@ application is running on (Mac/Linux vs Windows).** **Where possible, you should explicitly reference binaries by their full path name, unless you can reliably assume that the executable you're trying to spawn is available in the user's `PATH`.** -Child Processes are managed by the runtime (Electron/Tauri) but are fully accessible to the Laravel side of your +Child Processes are managed by the runtime (Electron) but are fully accessible to the Laravel side of your application. --- @@ -189,7 +189,7 @@ ChildProcess::stop('tail'); This will attempt to stop the process gracefully. The [`ProcessExited`](#codeprocessexitedcode) event will be dispatched if the process exits. -Note that [persistent processes](/docs/1/digging-deeper/child-process#persistent-processes) will be permanently stopped and will only be restarted when the `start` method is called again. If you want to restart a persistent process, use the `restart` method instead. +Note that [persistent processes](/docs/digging-deeper/child-processes#persistent-processes) will be permanently stopped and will only be restarted when the `start` method is called again. If you want to restart a persistent process, use the `restart` method instead. ## Restarting a Child Process @@ -271,11 +271,11 @@ A Child Process may send output via any of the following interfaces: - A custom interface, e.g. a network socket. - Broadcasting a Custom Event -`STDOUT`, `STDERR` & [Custom Events](/docs/1/digging-deeper/broadcasting#custom-events) are dispatched using +`STDOUT`, `STDERR` & [Custom Events](/docs/digging-deeper/broadcasting#custom-events) are dispatched using Laravel's event system. You may listen to these events by registering a listener in your app service provider, or on the front end -using the [Native helper](/docs/1/digging-deeper/broadcasting#listening-with-javascript). +using the [Native helper](/docs/digging-deeper/broadcasting#listening-with-javascript). Please see the [Events](#events) section for a full list of events. diff --git a/resources/views/docs/1/digging-deeper/databases.md b/resources/views/docs/desktop/1/digging-deeper/databases.md similarity index 86% rename from resources/views/docs/1/digging-deeper/databases.md rename to resources/views/docs/desktop/1/digging-deeper/databases.md index 215dca6b..41ad3064 100644 --- a/resources/views/docs/1/digging-deeper/databases.md +++ b/resources/views/docs/desktop/1/digging-deeper/databases.md @@ -30,18 +30,18 @@ keeping download & install size small. ### Configuration You do not need to do anything special to configure your application to use SQLite. NativePHP will automatically: -- Switch to using SQLite when building your application -- Create a database file for you in the `appdata` directory on the user's system -- Configure your application to use that database file -- Run your migrations each time your app starts, as needed +- Switch to using SQLite when building your application. +- Create a database file for you in the `appdata` directory on the user's system. +- Configure your application to use that database file. +- Run your migrations each time your app starts, as needed. ## Development -Remember that in [development](/docs/getting-started/development) your application's database is always going to be -the SQLite database created in the [`appdata`](/docs/getting-started/debugging#appdata) folder for your application. +In [development](/docs/getting-started/development), your application uses a database called `nativephp.sqlite` +which is created in the build directory. -This means that even if you've got different config in your `.env` file, your application will not be connecting to any -other database when it is running within the Electron/Tauri environment. +NativePHP forces your application to use this database when it is running within the Electron environment so that +it doesn't modify any other SQLite databases you may already be using. ## Migrations @@ -53,7 +53,7 @@ upon foreign key constraints, [you need to enable SQLite support for them](https **It's important to test your migrations on prod builds before releasing updates!** You don't want to accidentally delete your user's data when they update your app. -### Running migrations +### In production In production builds of your app, NativePHP will check to see if the app version has changed and attempt to migrate the user's copy of your database in their `appdata` folder. diff --git a/resources/views/docs/1/digging-deeper/files.md b/resources/views/docs/desktop/1/digging-deeper/files.md similarity index 99% rename from resources/views/docs/1/digging-deeper/files.md rename to resources/views/docs/desktop/1/digging-deeper/files.md index 8c5fbc70..e5b76671 100644 --- a/resources/views/docs/1/digging-deeper/files.md +++ b/resources/views/docs/desktop/1/digging-deeper/files.md @@ -92,4 +92,4 @@ However, in a NativePHP app, this approach is less applicable. The entire applic We recommend avoiding symlinks within your NativePHP app. Instead, consider either: - Placing files directly in their intended locations -- Using Laravel's Storage facade to mount directories outside your application +- Using Laravel's Storage facade to mount directories outside your application \ No newline at end of file diff --git a/resources/views/docs/1/digging-deeper/php-binaries.md b/resources/views/docs/desktop/1/digging-deeper/php-binaries.md similarity index 100% rename from resources/views/docs/1/digging-deeper/php-binaries.md rename to resources/views/docs/desktop/1/digging-deeper/php-binaries.md diff --git a/resources/views/docs/1/digging-deeper/queues.md b/resources/views/docs/desktop/1/digging-deeper/queues.md similarity index 89% rename from resources/views/docs/1/digging-deeper/queues.md rename to resources/views/docs/desktop/1/digging-deeper/queues.md index 7ec8bde8..77d9e5ca 100644 --- a/resources/views/docs/1/digging-deeper/queues.md +++ b/resources/views/docs/desktop/1/digging-deeper/queues.md @@ -31,6 +31,7 @@ Once you publish the NativePHP config file using `php artisan vendor:publish`, y 'queues' => ['high'], 'memory_limit' => 1024, 'timeout' => 600, + 'sleep' => 3, ], 'four' => [ 'queues' => ['high'], @@ -52,8 +53,11 @@ If you do not provide values for any of these settings, the following sensible d 'queues' => ['default'], 'memory_limit' => 128, 'timeout' => 60, +'sleep' => 3, ``` +The `sleep` parameter defines the number of seconds the worker will wait (sleep) when there are no new jobs available. A lower value means the worker polls for new jobs more frequently, which might be more responsive but uses more CPU. A higher value reduces CPU usage but may introduce a slight delay in processing newly added jobs. + ### Managing workers The handy `QueueWorker::up()` and `QueueWorker::down()` methods available on `Facades\QueueWorker` can be used to start diff --git a/resources/views/docs/1/digging-deeper/security.md b/resources/views/docs/desktop/1/digging-deeper/security.md similarity index 90% rename from resources/views/docs/1/digging-deeper/security.md rename to resources/views/docs/desktop/1/digging-deeper/security.md index 8fa2ff13..8a3780e7 100644 --- a/resources/views/docs/1/digging-deeper/security.md +++ b/resources/views/docs/desktop/1/digging-deeper/security.md @@ -2,6 +2,7 @@ title: Security order: 400 --- + # Security When building desktop applications it's essential to take your application's security to the next level, both to @@ -16,6 +17,7 @@ application that allows users to manipulate data on their filesystem or other so A major consideration for NativePHP is how it can protect _your_ application. ### Secrets and .env + As your application is being installed on systems outside of your/your organisation's control, it is important to think of the environment that it's in as _potentially_ hostile, which is to say that any secrets, passwords or keys could fall into the hands of someone who might try to abuse them. @@ -28,6 +30,8 @@ application and any API use a robust and secure authentication protocol, such as distribute unique and expiring tokens (an expiration date less than 48 hours in the future is recommended) with a high level of entropy, as this makes them hard to guess and hard to abuse. +Always use HTTPS. + If your application allows users to connect _their own_ API keys for a service, you should treat these keys with great care. If you choose to store them anywhere (either in a [File](/docs/digging-deeper/files) or [Database](/docs/digging-deeper/databases)), make sure you store them @@ -69,27 +73,11 @@ but you should do all you can to prevent unwanted attack vectors from being made #### Prevent regular browser access -When you're ready, you should add the `PreventRegularBrowserAccess` middleware to your application's global middleware -stack, **especially before building your application for production release**. - -**This is NOT done for you.** +When you are running a build, the global `PreventRegularBrowserAccess` middleware will be applied to all your routes automatically. This ensures that only requests coming from the web view shell that booted your application can make requests into your application. -Append the middleware in your `bootstrap/app.php` file: - -```php -use Native\Laravel\Http\Middleware\PreventRegularBrowserAccess; - -return Application::configure(basePath: dirname(__DIR__)) - // ... - ->withMiddleware(function (Middleware $middleware) { - $middleware->append(PreventRegularBrowserAccess::class); - }) - // ... -``` - ## Protecting your users and their data Equally important is how your app protects users. NativePHP is a complex combination of powerful software and so there diff --git a/resources/views/docs/1/getting-started/_index.md b/resources/views/docs/desktop/1/getting-started/_index.md similarity index 100% rename from resources/views/docs/1/getting-started/_index.md rename to resources/views/docs/desktop/1/getting-started/_index.md diff --git a/resources/views/docs/1/getting-started/configuration.md b/resources/views/docs/desktop/1/getting-started/configuration.md similarity index 92% rename from resources/views/docs/1/getting-started/configuration.md rename to resources/views/docs/desktop/1/getting-started/configuration.md index 40bd67d2..98935738 100644 --- a/resources/views/docs/1/getting-started/configuration.md +++ b/resources/views/docs/desktop/1/getting-started/configuration.md @@ -40,6 +40,21 @@ return [ * The author of your application. */ 'author' => env('NATIVEPHP_APP_AUTHOR'), + + /** + * The copyright notice for your application. + */ + 'copyright' => env('NATIVEPHP_APP_COPYRIGHT'), + + /** + * The description of your application. + */ + 'description' => env('NATIVEPHP_APP_DESCRIPTION', 'An awesome app built with NativePHP'), + + /** + * The Website of your application. + */ + 'website' => env('NATIVEPHP_APP_WEBSITE', 'https://nativephp.com'), /** * The default service provider for your application. This provider diff --git a/resources/views/docs/1/getting-started/debugging.md b/resources/views/docs/desktop/1/getting-started/debugging.md similarity index 94% rename from resources/views/docs/1/getting-started/debugging.md rename to resources/views/docs/desktop/1/getting-started/debugging.md index 0090270e..9a5ac101 100644 --- a/resources/views/docs/1/getting-started/debugging.md +++ b/resources/views/docs/desktop/1/getting-started/debugging.md @@ -19,13 +19,13 @@ This means that while some issues can be solved within NativePHP it's also very ### The layers - Your application, built on Laravel, using your local installations of PHP & Node. -- NativePHP's development tools (`native:serve` and `native:build`) manage the Electron/Tauri build processes - this is +- NativePHP's development tools (`native:serve` and `native:build`) manage the Electron build processes - this is what creates your Application Bundle. - NativePHP moves the appropriate version of a statically-compiled binary of PHP into your application's bundle - when your app boots, it's _this_ version of PHP that is being used to execute your PHP code, not your system's version of PHP. -- Electron & Tauri use suites of platform-specific build tools and dependencies - Electron's ecosystem is mostly - Javascript based, Tauri's is mostly Rust based. Much of this will be hidden away in your `vendor` directory. +- Electron uses a suite of platform-specific build tools and dependencies - Electron's ecosystem is mostly + Javascript based. Much of this will be hidden away in your `vendor` directory. - The operating system (OS) and its architecture (arch) - you can't build an application for one architecture and distribute it to a different OS/arch. It won't work. You must build your application to match the OS+arch combination where you want it to run. @@ -138,6 +138,6 @@ C:\path\to\your\app\dist\win-unpacked\resources\app.asar.unpacked\resources\php\ If you've found a bug, please [open an issue](https://github.com/nativephp/laravel/issues/new) on GitHub. There's also [Discussions](https://github.com/orgs/NativePHP/discussions) and -[Discord](https://discord.gg/X62tWNStZK) for live chat. +[Discord]({{ $discordLink }}) for live chat. Come join us! We want you to succeed. diff --git a/resources/views/docs/1/getting-started/development.md b/resources/views/docs/desktop/1/getting-started/development.md similarity index 93% rename from resources/views/docs/1/getting-started/development.md rename to resources/views/docs/desktop/1/getting-started/development.md index 2db33e67..e94aebc6 100644 --- a/resources/views/docs/1/getting-started/development.md +++ b/resources/views/docs/desktop/1/getting-started/development.md @@ -6,7 +6,7 @@ order: 300 ## Development ```shell -php artisan native:serve +php artisan native:run ``` NativePHP isn't prescriptive about how you develop your application. You can build it in the way you're most comfortable @@ -20,7 +20,7 @@ This is known as 'running a dev build'. ### What does the `native:serve` command do? -The `native:serve` command runs the Electron/Tauri 'debug build' commands, which build your application with various +The `native:serve` command runs the Electron 'debug build' commands, which build your application with various debug options set to help make debugging easier, such as allowing you to show the Dev Tools in the embedded web view. It also keeps the connection to the terminal open so you can see and inspect useful output from your app, such as logs, @@ -102,6 +102,8 @@ For more details, see the [Databases](/docs/digging-deeper/databases) section. The `native:serve` and `native:build` commands look for the following icon files when building your application: - `public/icon.png` - your main icon, used on the Desktop, Dock and app switcher. +- `public/icon.ico` - if it exists, it is used as an icon file for Windows (optional). +- `public/icon.icns` - if it exists, it is used as an icon file for macOS (optional). - `public/IconTemplate.png` - used in the Menu Bar on non-retina displays. - `public/IconTemplate@2x.png` - used in the Menu Bar on retina displays. diff --git a/resources/views/docs/1/getting-started/env-files.md b/resources/views/docs/desktop/1/getting-started/env-files.md similarity index 100% rename from resources/views/docs/1/getting-started/env-files.md rename to resources/views/docs/desktop/1/getting-started/env-files.md diff --git a/resources/views/docs/1/getting-started/installation.md b/resources/views/docs/desktop/1/getting-started/installation.md similarity index 89% rename from resources/views/docs/1/getting-started/installation.md rename to resources/views/docs/desktop/1/getting-started/installation.md index d03a1e3e..3295b405 100644 --- a/resources/views/docs/1/getting-started/installation.md +++ b/resources/views/docs/desktop/1/getting-started/installation.md @@ -5,9 +5,9 @@ order: 100 ## Requirements -1. PHP 8.1+ -2. Laravel 10 or higher -3. Node 20+ +1. PHP 8.3+ +2. Laravel 11 or higher +3. Node 22+ 4. Windows 10+ / macOS 12+ / Linux ### PHP & Node @@ -31,7 +31,8 @@ NativePHP is built to work best with Laravel. You can install it into an existin composer require nativephp/electron ``` -The Tauri runtime is coming soon. +This package contains all the classes, commands, and interfaces that your application will need to work with the +Electron runtime. ## Run the NativePHP installer @@ -40,7 +41,7 @@ php artisan native:install ``` The NativePHP installer takes care of publishing the NativePHP service provider, which bootstraps the necessary -dependencies for your application to work with the runtime you're using: Electron or Tauri. +dependencies for your application to work with Electron. It also publishes the NativePHP configuration file to `config/nativephp.php`. diff --git a/resources/views/docs/1/getting-started/introduction.md b/resources/views/docs/desktop/1/getting-started/introduction.md similarity index 61% rename from resources/views/docs/1/getting-started/introduction.md rename to resources/views/docs/desktop/1/getting-started/introduction.md index 2ac6b5eb..293f562f 100644 --- a/resources/views/docs/1/getting-started/introduction.md +++ b/resources/views/docs/desktop/1/getting-started/introduction.md @@ -14,33 +14,41 @@ Whatever your path, we think you're going to be productive quickly. NativePHP is taking the world by storm, enabling PHP developers to create true cross-platform, native apps using the tools and technologies they already know: HTML, CSS, Javascript, and, of course, PHP. -And they said PHP was dead. +## Why PHP? + +**PHP is great.** It's a mature language that has been honed for 30 years in one of the most ruthless environments: +the web. + +Despite the odds, it remains one of the most used languages worldwide and continues to grow in usage every day. + +Its shared-nothing approach to memory safety makes it an unexpectedly productive candidate for building native +applications. + +Its focus on HTTP as a paradigm for building applications lends itself towards using the incredibly +accessible web technologies to build rich UIs that can feel at home on any platform. ## What exactly is NativePHP? Strictly speaking, NativePHP is a combination of elements: -1. A collection of easy-to-use classes - abstractions - to enable you to interact with a variety of host operating -system features. -2. A set of tools to enable building and bundling your native application using either the Electron or Tauri browser -environment. -3. A static PHP runtime that allows your app to run on any user's system with zero effort on their part. +1. A collection of easy-to-use classes to enable you to interact with a variety of host operating system features. +2. A set of tools to enable building and bundling your native application. +3. A static PHP runtime that allows your app to run on any user's device with zero effort on their part. ## What NativePHP isn't -NativePHP is **not an especially opinionated way to build native apps**. Right now, **we only support a Laravel driver**, -but we'd love for it to work whatever PHP framework you're using - and even if you're not using a framework at all. -Can you help? Please consider [contributing](https://github.com/NativePHP/laravel/blob/main/CONTRIBUTING.md). +NativePHP is **not a completely new framework that you need to learn**. It builds on top of the incredible affordances +and ecosystem that Laravel provides. Before using NativePHP, you'll want to be familiar with building web applications +using Laravel. NativePHP is **not a GUI framework**. We don't want to tell you how to build your app. You can choose whatever UI toolset -makes you and your team feel most productive. - -Building a React front-end? No problem. Vue? Sure. Livewire or Inertia? Doesn't matter! Plain old HTML and CSS? -You got it. Tailwind? Bootstrap? Material UI? Whatever you want. +makes you and your team feel most productive. Building a React front-end? No problem. Vue? Sure. Livewire or Inertia? +Doesn't matter! Plain old HTML and CSS? You got it. Tailwind, Bootstrap, Material UI: whatever you want. NativePHP is **not some new, custom fork of PHP**. This is the good new PHP you know and love. -It's also not an extension that you need to figure out and install into PHP. +It's also not an extension that you need to figure out and install into PHP. You're just a `composer require` away from +awesome. ## What's in the box? @@ -64,12 +72,14 @@ that puts cowboy hats on every smiley-face emoji it sees. (You should totally build that last one.) +Need some inspiration? [Check out our repository of awesome projects](https://github.com/NativePHP/awesome-nativephp) created by people like you! + ## What's next? Go read the docs! We've tried to make them as comprehensive as possible, but if you find something missing, please feel free to [contribute](https://github.com/nativephp/nativephp.com). -This site and all the NativePHP repositories are open source and available on [GitHub](https://github.com/nativephp). +This site and all the NativePHP for Desktop repositories are open source and available on [GitHub](https://github.com/nativephp). Ready to jump in? [Let's get started](installation). @@ -79,7 +89,6 @@ NativePHP wouldn't be possible without the following projects and the hard work - [PHP](https://php.net) - [Electron](https://electronjs.org) -- [Tauri](https://tauri.app) - [Laravel](https://laravel.com) - [Symfony](https://symfony.com) - [Static PHP CLI](https://github.com/crazywhalecc/static-php-cli/) diff --git a/resources/views/docs/desktop/1/getting-started/releasenotes.md b/resources/views/docs/desktop/1/getting-started/releasenotes.md new file mode 100644 index 00000000..3c9d6436 --- /dev/null +++ b/resources/views/docs/desktop/1/getting-started/releasenotes.md @@ -0,0 +1,40 @@ +--- +title: Release Notes +order: 1100 +--- + +## NativePHP/electron +@forelse (\App\Support\GitHub::electron()->releases()->take(10) as $release) +### {{ $release->name }} +**Released: {{ \Carbon\Carbon::parse($release->published_at)->format('F j, Y') }}** + +{{ $release->getBodyForMarkdown() }} +--- +@empty +## We couldn't show you the latest release notes at this time. +Not to worry, you can head over to GitHub to see the [latest release notes](https://github.com/NativePHP/electron/releases). +@endforelse + +## NativePHP/laravel +@forelse (\App\Support\GitHub::laravel()->releases()->take(10) as $release) +### {{ $release->name }} +**Released: {{ \Carbon\Carbon::parse($release->published_at)->format('F j, Y') }}** + +{{ $release->getBodyForMarkdown() }} +--- +@empty +## We couldn't show you the latest release notes at this time. +Not to worry, you can head over to GitHub to see the [latest release notes](https://github.com/NativePHP/electron/releases). +@endforelse + +## NativePHP/php-bin +@forelse (\App\Support\GitHub::phpBin()->releases()->take(10) as $release) +### {{ $release->name }} +**Released: {{ \Carbon\Carbon::parse($release->published_at)->format('F j, Y') }}** + +{{ $release->getBodyForMarkdown() }} +--- +@empty +## We couldn't show you the latest release notes at this time. +Not to worry, you can head over to GitHub to see the [latest release notes](https://github.com/NativePHP/electron/releases). +@endforelse diff --git a/resources/views/docs/desktop/1/getting-started/status.md b/resources/views/docs/desktop/1/getting-started/status.md new file mode 100644 index 00000000..15bfd319 --- /dev/null +++ b/resources/views/docs/desktop/1/getting-started/status.md @@ -0,0 +1,28 @@ +--- +title: Roadmap +order: 099 +--- + +## Roadmap + +We're currently focused on reaching the next minor release. + +## Current Status + +NativePHP for Desktop is **production-ready**. So you should feel completely ready to build and distribute apps +with NativePHP. + +But we always need your help! If you spot any bugs or feel that there are any features missing, be sure to share +your ideas and questions through the [forum](https://github.com/orgs/nativephp/discussions), by +[raising issues](https://github.com/nativephp/laravel/issues/new/choose)/reporting bugs, and discussing on +[Discord]({{ $discordLink }}). + + diff --git a/resources/views/docs/desktop/1/getting-started/support-policy.md b/resources/views/docs/desktop/1/getting-started/support-policy.md new file mode 100644 index 00000000..d5ed95e3 --- /dev/null +++ b/resources/views/docs/desktop/1/getting-started/support-policy.md @@ -0,0 +1,37 @@ +--- +title: Support Policy +order: 500 +--- + +## Support Policy + +NativePHP for Desktop is an open-source project dedicated to providing robust and reliable releases. + +We are committed to supporting the two latest PHP versions, ensuring that our users benefit from the latest features, security updates, and performance improvements. + +__We do not remove support for a PHP version without a major version release.__ + +Additionally, we support each Laravel version until it reaches its official end of life (EOL), ensuring that your applications can remain up-to-date. + +Our support policy reflects our commitment to maintaining high standards of quality and security, providing our users with the confidence they need to build and deploy their applications using NativePHP for Desktop. + +### PHP Versions +| NativePHP Version | Supported PHP Versions | +|-------------------|------------------------| +| ^1.0 | 8.3, 8.4 | + +[PHP: Supported Versions](https://www.php.net/supported-versions.php) + +NativePHP provides methods of bundling your own static PHP binaries. Support is not provided for these. + +### Laravel Versions +| NativePHP Version | Supported Laravel Versions | +|-------------------|----------------------------| +| ^1.0 | 11.x, 12.x | + +[Laravel: Support Policy](https://laravel.com/docs/master/releases#support-policy) + +## Requesting Support +Support can be obtained by opening an issue on the [NativePHP/laravel]({{ $githubLink }}/laravel/issues) repository or by joining the [Discord]({{ $discordLink }}). + +When requesting support, it is requested that you are using a supported version. If you are not, you will be asked to upgrade to a supported version before any support is provided. diff --git a/resources/views/docs/1/publishing/_index.md b/resources/views/docs/desktop/1/publishing/_index.md similarity index 100% rename from resources/views/docs/1/publishing/_index.md rename to resources/views/docs/desktop/1/publishing/_index.md diff --git a/resources/views/docs/1/publishing/building.md b/resources/views/docs/desktop/1/publishing/building.md similarity index 63% rename from resources/views/docs/1/publishing/building.md rename to resources/views/docs/desktop/1/publishing/building.md index 18af3bdc..4b5de7a2 100644 --- a/resources/views/docs/1/publishing/building.md +++ b/resources/views/docs/desktop/1/publishing/building.md @@ -16,7 +16,7 @@ Before you prepare a distributable build, please make sure you've been through t ## Building The build process compiles your app for one platform at a time. It compiles your application along with the -Electron/Tauri runtime into a single executable. +Electron runtime into a single executable. Once built, you can distribute your app however you prefer, but NativePHP also provides a [publish command](publishing) that will automatically upload your build artifacts to your chosen [provider](/docs/publishing/updating) - this allows @@ -25,6 +25,25 @@ your app to provide automatic updates. You should build your application for each platform you intend to support and test it on each platform _before_ publishing to make sure that everything works as expected. +### Running commands before and after builds +Many applications rely on a tool such as [Vite](https://vitejs.dev/) or [Webpack](https://webpack.js.org/) to compile their CSS and JS assets before a production build. + +To facilitate this, NativePHP provides two hooks that you can use to run commands before and after the build process. + +To utilise these hooks, add the following to your `config/nativephp.php` file: + +```php +'prebuild' => [ + 'npm run build', // Run a command before the build + 'php artisan optimize', // Run another command before the build +], +'postbuild' => [ + 'npm run release', // Run a command after the build +], +``` + +These commands will be run in the root of your project directory and you can specify as many as required. + ## Versioning For every build you create, you should change the version of your application in your app's `config/nativephp.php` file. @@ -77,7 +96,45 @@ NativePHP makes this as easy for you as it can, but each platform does have slig ### Windows -[See the Electron documentation](https://www.electronforge.io/guides/code-signing/code-signing-windows) for more details. +NativePHP supports two methods for Windows code signing: traditional certificate-based signing and Azure Trusted Signing. + +#### Azure Trusted Signing (Recommended) + +Azure Trusted Signing is a cloud-based code signing service that eliminates the need to manage local certificates. + +When building your application, you can identify which signing method is being used: +- **Azure Trusted Signing**: The build output will show "Signing with Azure Trusted Signing (beta)" +- **Traditional Certificate**: The build output will show "Signing with signtool.exe" + +To use Azure Trusted Signing, add the following environment variables to your `.env` file: + +```dotenv +# Azure AD authentication +AZURE_TENANT_ID=your-tenant-id +AZURE_CLIENT_ID=your-client-id +AZURE_CLIENT_SECRET=your-client-secret + +# Azure Trusted Signing configuration +# This is the CommonName (CN) value - your full name or company name +# as entered in the Identity Validation Request form +NATIVEPHP_AZURE_PUBLISHER_NAME=your-publisher-name + +# The endpoint URL for the Azure region where your certificate is stored +NATIVEPHP_AZURE_ENDPOINT=https://eus.codesigning.azure.net/ + +# The name of your certificate profile (NOT the Trusted Signing Account) +NATIVEPHP_AZURE_CERTIFICATE_PROFILE_NAME=your-certificate-profile + +# Your Trusted Signing Account name (NOT the app registration display name) +# This is the account name shown in Azure Trusted Signing, not your login name +NATIVEPHP_AZURE_CODE_SIGNING_ACCOUNT_NAME=your-code-signing-account +``` + +These credentials will be automatically stripped from your built application for security. + +#### Traditional Certificate Signing + +For traditional certificate-based signing, [see the Electron documentation](https://www.electronforge.io/guides/code-signing/code-signing-windows) for more details. ### macOS diff --git a/resources/views/docs/1/publishing/publishing.md b/resources/views/docs/desktop/1/publishing/publishing.md similarity index 55% rename from resources/views/docs/1/publishing/publishing.md rename to resources/views/docs/desktop/1/publishing/publishing.md index 27e6eea9..1a66662f 100644 --- a/resources/views/docs/1/publishing/publishing.md +++ b/resources/views/docs/desktop/1/publishing/publishing.md @@ -2,6 +2,7 @@ title: Publishing order: 200 --- + ## Publishing Your App Publishing your app is similar to building, but in addition NativePHP will upload the build artifacts to your chosen @@ -15,6 +16,8 @@ php artisan native:publish This will build for the platform and architecture where you are running the build. +**Make sure you've bumped your app version in your .env file before building** + ### Cross-compilation You can also specify a platform to build for by passing the `os` argument, so for example you could build for Windows @@ -27,3 +30,11 @@ php artisan native:publish win Possible options are: `mac`, `win`, `linux`. **Cross-compilation is not supported on all platforms.** + +### GitHub Releases + +If you use the GitHub [updater provider](/docs/publishing/updating), you'll need to create a draft release first. + +Set the "Tag version" to the value of `version` in your application `.env` file, and prefix it with v. "Release title" can be anything you want. + +Whenever you run `native:publish`, your build artifacts will be attached to your draft release. If you decide to rebuild before tagging the release, it will update the artifacts attached to your draft. diff --git a/resources/views/docs/1/publishing/updating.md b/resources/views/docs/desktop/1/publishing/updating.md similarity index 53% rename from resources/views/docs/1/publishing/updating.md rename to resources/views/docs/desktop/1/publishing/updating.md index 9923c558..3051fc88 100644 --- a/resources/views/docs/1/publishing/updating.md +++ b/resources/views/docs/desktop/1/publishing/updating.md @@ -2,7 +2,9 @@ title: Updating order: 300 --- + ## The Updater + NativePHP ships with a built-in auto-update tool, which allows your users to update your application without needing to manually download and install new releases. @@ -10,9 +12,10 @@ This leaves you to focus on building and releasing new versions of your applicat distributing those updates to your users. **macOS: Automatic updating is only supported for [signed](/docs/publishing/building#signing-and-notarizing) -applications.** +applications.** ## How it works + The updater works by checking a remote URL for a new version of your application. If a new version is found, the updater will download the new version and replace the existing application files with the new ones. @@ -27,9 +30,10 @@ The updater supports three providers: You can configure all settings for the updater in your `config/nativephp.php` file or via your `.env` file. -**The updater will only run when your app is running in production mode.** +**The updater will only run when your app is running in production mode.** ## Configuration + The default updater configuration looks like this: ```php @@ -73,12 +77,14 @@ The default updater configuration looks like this: ``` How to setup your storage and generate the relevant API credentials: + - [DigitalOcean](https://docs.digitalocean.com/products/spaces/how-to/manage-access/) -- Amazon S3 - See [this video](https://www.youtube.com/watch?v=FLIp6BLtwjk&ab_channel=CloudCasts) by Chris Fidao or - this [Step 2](https://www.twilio.com/docs/video/tutorials/storing-aws-s3#step-2) of this article by Twilio +- Amazon S3 - See [this video](https://www.youtube.com/watch?v=FLIp6BLtwjk&ab_channel=CloudCasts) by Chris Fidao or + this [Step 2](https://www.twilio.com/docs/video/tutorials/storing-aws-s3#step-2) of this article by Twilio - If you got the error message "The bucket does not allow ACLs" you can follow this guide from [Learn AWS](https://www.learnaws.org/2023/08/26/aws-s3-bucket-does-not-allow-acls) - on how to setup your bucket correctly. + If you got the error message "The bucket does not allow ACLs" you can follow this guide + from [Learn AWS](https://www.learnaws.org/2023/08/26/aws-s3-bucket-does-not-allow-acls) + on how to setup your bucket correctly. ## Disabling the updater @@ -88,3 +94,79 @@ If you don't want your application to check for updates, you can disable the upd ```dotenv NATIVEPHP_UPDATER_ENABLED=false ``` + +## Manually checking for updates + +You can manually check for updates by calling the `checkForUpdates` method on the `AutoUpdater` facade: + +```php +use Native\Laravel\Facades\AutoUpdater; + +AutoUpdater::checkForUpdates(); +``` + +**Note:** If an update is available, it will be downloaded automatically. Calling `AutoUpdater::checkForUpdates() twice +will download the update two times. + +## Quit and Install + +You can quit the application and install the update by calling the `quitAndInstall` method on the `AutoUpdater` facade: + +```php +use Native\Laravel\Facades\AutoUpdater; + +AutoUpdater::quitAndInstall(); +``` + +This will quit the application and install the update. The application will then relaunch automatically. + +**Note:** Calling this method is optional — any successfully downloaded update will be applied the next time the +application starts. + +## Events + +### `CheckingForUpdate` + +The `Native\Laravel\Events\AutoUpdater\CheckingForUpdate` event is dispatched when checking for an available update has +started. + +### `UpdateAvailable` + +The `Native\Laravel\Events\AutoUpdater\UpdateAvailable` event is dispatched when there is an available update. The +update is downloaded automatically. + +### `UpdateNotAvailable` + +The `Native\Laravel\Events\AutoUpdater\UpdateNotAvailable` event is dispatched when there is no available update. + +### `DownloadProgress` + +The `Native\Laravel\Events\AutoUpdater\DownloadProgress` event is dispatched when the update is being downloaded. + +The event contains the following properties: + +- `total`: The total size of the update in bytes. +- `delta`: The size of the update that has been downloaded since the last event. +- `transferred`: The total size of the update that has been downloaded. +- `percent`: The percentage of the update that has been downloaded (0-100). +- `bytesPerSecond`: The download speed in bytes per second. + +### `UpdateDownloaded` + +The `Native\Laravel\Events\AutoUpdater\UpdateDownloaded` event is dispatched when the update has been downloaded. + +The event contains the following properties: + +- `version`: The version of the update. +- `downloadedFile`: The local path to the downloaded update file. +- `releaseDate`: The release date of the update in ISO 8601 format. +- `releaseNotes`: The release notes of the update. +- `releaseName`: The name of the update. + +### `Error` + +The `Native\Laravel\Events\AutoUpdater\Error` event is dispatched when there is an error while updating. + +The event contains the following properties: + +- `error`: The error message. diff --git a/resources/views/docs/1/testing/_index.md b/resources/views/docs/desktop/1/testing/_index.md similarity index 100% rename from resources/views/docs/1/testing/_index.md rename to resources/views/docs/desktop/1/testing/_index.md diff --git a/resources/views/docs/1/testing/basics.md b/resources/views/docs/desktop/1/testing/basics.md similarity index 89% rename from resources/views/docs/1/testing/basics.md rename to resources/views/docs/desktop/1/testing/basics.md index ead13b18..36ce58cc 100644 --- a/resources/views/docs/1/testing/basics.md +++ b/resources/views/docs/desktop/1/testing/basics.md @@ -4,7 +4,7 @@ order: 99 --- # Understanding fake test doubles When working with a NativePHP application, you may encounter an elevated level of difficulty when writing tests for your code. -This is because NativePHP relies on an Electron/Tauri application to be open at all times, listening to HTTP requests. Obviously, +This is because NativePHP relies on an Electron application to be open at all times, listening to HTTP requests. Obviously, emulating this in a test environment can be cumbersome. You will often hit an HTTP error, and this is normal. This is where NativePHP's fake test doubles come in. diff --git a/resources/views/docs/1/testing/child-process.md b/resources/views/docs/desktop/1/testing/child-process.md similarity index 100% rename from resources/views/docs/1/testing/child-process.md rename to resources/views/docs/desktop/1/testing/child-process.md diff --git a/resources/views/docs/1/testing/global-shortcut.md b/resources/views/docs/desktop/1/testing/global-shortcut.md similarity index 100% rename from resources/views/docs/1/testing/global-shortcut.md rename to resources/views/docs/desktop/1/testing/global-shortcut.md diff --git a/resources/views/docs/1/testing/power-monitor.md b/resources/views/docs/desktop/1/testing/power-monitor.md similarity index 100% rename from resources/views/docs/1/testing/power-monitor.md rename to resources/views/docs/desktop/1/testing/power-monitor.md diff --git a/resources/views/docs/1/testing/queue-worker.md b/resources/views/docs/desktop/1/testing/queue-worker.md similarity index 100% rename from resources/views/docs/1/testing/queue-worker.md rename to resources/views/docs/desktop/1/testing/queue-worker.md diff --git a/resources/views/docs/1/testing/windows.md b/resources/views/docs/desktop/1/testing/windows.md similarity index 100% rename from resources/views/docs/1/testing/windows.md rename to resources/views/docs/desktop/1/testing/windows.md diff --git a/resources/views/docs/1/the-basics/_index.md b/resources/views/docs/desktop/1/the-basics/_index.md similarity index 100% rename from resources/views/docs/1/the-basics/_index.md rename to resources/views/docs/desktop/1/the-basics/_index.md diff --git a/resources/views/docs/desktop/1/the-basics/alerts.md b/resources/views/docs/desktop/1/the-basics/alerts.md new file mode 100644 index 00000000..fa7d4b61 --- /dev/null +++ b/resources/views/docs/desktop/1/the-basics/alerts.md @@ -0,0 +1,118 @@ +--- +title: Alerts +order: 410 +--- + +## Native Alerts + +NativePHP allows you to show native alerts to the user. They can be used to display messages, ask for confirmation, or +report an error. + +Alerts are created using the `Alert` facade. + +```php +use Native\Laravel\Facades\Alert; +``` + +### Showing Alerts + +To show an alert, you may use the `Alert` class and its `show()` method. + +```php +Alert::new() + ->show('This is a simple alert'); +``` + +## Configuring Alerts + +### Alert Title + +You may set the title of the alert using the `title()` method. + +```php +Alert::new() + ->title('Pizza Order') + ->show('Your pizza has been ordered'); +``` + +### Alert Buttons + +You may configure the buttons of the alert using the `buttons()` method. +This method takes an array of button labels. + +The return value of the `show()` method is the index of the button that the user clicked. +Example: If the user clicks the "Yes" button, the `show()` method will return `0`. If the user clicks the "Maybe" +button, the `show()` method will return `2`. + +If no buttons are defined, the alert will only have an "OK" button. + +```php +Alert::new() + ->buttons(['Yes', 'No', 'Maybe']) + ->show('Do you like pizza?'); +``` + +### Alert Detail + +You may set the detail of the alert using the `detail()` method. +The detail is displayed below the message and provides additional information about the alert. + +```php +Alert::new() + ->detail('Fun facts: Pizza was first made in Naples in 1889') + ->show('Do you like pizza?'); +``` + +### Alert Type + +You may set the type of the alert using the `type()` method. +The type can be one of the following values: `none`, `info`, `warning`, `error`, `question`. On Windows, `question` +displays the same icon as `info`. On macOS, both `warning` and `error` display the same warning icon. + +```php +Alert::new() + ->type('error') + ->show('An error occurred'); +``` + +### Alert Default Button + +You may set the default button of the alert using the `defaultId()` method. +The default button is preselected when the alert appears. + +The default button can be set to the index of the button in the `buttons()` array. + +```php +Alert::new() + ->defaultId(0) + ->buttons(['Yes', 'No', 'Maybe']) + ->show('Do you like pizza?'); +``` + +### Alert Cancel Button + +You may set the cancel button of the alert using the `cancelId()` method. +The cancel button is the button that is selected when the user presses the "Escape" key. + +The cancel button can be set to the index of the button in the `buttons()` array. + +By default, this is assigned to the first button labeled 'Cancel' or 'No'. If no such buttons exist and this option is +not set, the return value will be `0`. + +```php +Alert::new() + ->cancelId(1) + ->buttons(['Yes', 'No', 'Maybe']) + ->show('Do you like pizza?'); +``` + +### Error Alerts + +You may use the `error()` method to display an error alert. + +The `error()` method takes two required parameters: the title of the error alert and the message of the error alert. + +```php +Alert::new() + ->error('An error occurred', 'The pizza oven is broken'); +``` diff --git a/resources/views/docs/1/the-basics/app-lifecycle.md b/resources/views/docs/desktop/1/the-basics/app-lifecycle.md similarity index 96% rename from resources/views/docs/1/the-basics/app-lifecycle.md rename to resources/views/docs/desktop/1/the-basics/app-lifecycle.md index 7994fd04..7383fb5a 100644 --- a/resources/views/docs/1/the-basics/app-lifecycle.md +++ b/resources/views/docs/desktop/1/the-basics/app-lifecycle.md @@ -7,7 +7,7 @@ order: 1 When your NativePHP application starts - whether it's in development or production - it performs a series of steps to get your application up and running. -1. The native shell (Electron or Tauri) is started. +1. The native shell (Electron) is started. 2. NativePHP runs `php artisan migrate` to ensure your database is up-to-date. 3. NativePHP runs `php artisan serve` to start the PHP development server. 4. NativePHP boots your application by running the `boot()` method on your `NativeAppServiceProvider`. diff --git a/resources/views/docs/1/the-basics/application-menu.md b/resources/views/docs/desktop/1/the-basics/application-menu.md similarity index 89% rename from resources/views/docs/1/the-basics/application-menu.md rename to resources/views/docs/desktop/1/the-basics/application-menu.md index bc2fa7fa..346963b0 100644 --- a/resources/views/docs/1/the-basics/application-menu.md +++ b/resources/views/docs/desktop/1/the-basics/application-menu.md @@ -425,3 +425,60 @@ Menu::make() Menu::quit(), ); ``` + +## Context Menu + +You may wish to add a custom native context menu to the elements in the views of your application and override the default one. + +You can use the `Native` JavaScript helper provided by NativePHP's preload script. + +This object exposes the `contextMenu()` method which takes an array of objects that matches the +[MenuItem](https://www.electronjs.org/docs/latest/api/menu-item) constructor's `options` argument. + +```js +Native.contextMenu([ + { + label: 'Edit', + accelerator: 'e', + click(menuItem, window, event) { + // Code to execute when the menu item is clicked + }, + }, + // Other options +]); +``` + +You can listen for the `contextmenu` event to show your custom context menu: + +```js +const element = document.getElementById('your-element'); + +element.addEventListener('contextmenu', (event) => { + event.preventDefault(); + + Native.contextMenu([ + { + label: 'Duplicate', + accelerator: 'd', + click() { + duplicateEntry(element.dataset.id); + }, + }, + { + label: 'Edit', + accelerator: 'e', + click() { + showEditForm(element.dataset.id); + }, + }, + { + label: 'Delete', + click() { + if (confirm('Are you sure you want to delete this entry?')) { + deleteEntry(element.dataset.id); + } + }, + }, + ]); +}); +``` diff --git a/resources/views/docs/desktop/1/the-basics/application.md b/resources/views/docs/desktop/1/the-basics/application.md new file mode 100644 index 00000000..f48cceb9 --- /dev/null +++ b/resources/views/docs/desktop/1/the-basics/application.md @@ -0,0 +1,157 @@ +--- +title: Application +order: 250 +--- + +## Application + +The `App` facade allows you to perform basic operations with the Electron app. + +Note: Some methods are only available on specific operating systems and are labeled as such. + +To use the `App` facade, add the following to the top of your file: + +```php +use Native\Laravel\Facades\App; +``` + +### Quit the app + +To quit the app, use the `quit` method: + +```php +App::quit(); +``` + +### Relaunch the app +To relaunch the app, use the `relaunch` method. This will quit the app and relaunch it. + +```php +App::relaunch(); +``` + +### Focus the app + +To focus the app, use the `focus` method. + +On Linux, it focuses on the first visible window. On macOS, it makes the application the active one. On Windows, it +focuses on the application's first window. + +```php +App::focus(); +``` + +### Hide the app +_Only available on macOS_ + +The `hide` method will hide all application windows without minimizing them. This method is only available on macOS. + +```php +App::hide(); +``` + +### Check if the app is hidden +_Only available on macOS_ + +To check if the app is hidden, use the `isHidden` method. This method is only available on macOS. + +Returns a boolean: `true` if the application—including all its windows—is hidden (e.g., with Command-H), `false` +otherwise. + +```php +$isHidden = App::isHidden(); +``` + +### Current Version + +To get the current app version, use the `version` method. The version is defined in the `config/nativephp.php` file. + +```php +$version = App::version(); +``` + +### Locale information + +The facade offers several methods for accessing some of the system's localisation information. +This data can be helpful for localising your application, e.g. if you want to suggest the corresponding language to the user on first launch. + +```php +App::getLocale(); // e.g. "de", "fr-FR" +App::getLocaleCountryCode(); // e.g. "US", "DE" +App::getSystemLocale(); // e.g. "it-IT", "de-DE" +``` + +The `getLocale` method will return the locale used by the app. +Dependening on the user's settings, this might include both the language and the country / region or the language only. +It is based on Chromiums `l10n_util` library; see [this page](https://source.chromium.org/chromium/chromium/src/+/main:ui/base/l10n/l10n_util.cc) to see possible values. + +`getLocaleCountryCode` returns the user's system country code (using the [ISO 3166 code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)). +This information is pulled from native OS APIs. If it is not possible to detect this information, an empty string will be returned. + +With `getSystemLocale` you can access the system-wide locale setting. This is the locale set at the operating system level, not necessarily what the app is using. +Under Windows and Linux, Chromium's `i18n` library is used to evaluate this information. macOS will use `[NSLocale currentLocale]`. + + +### App Badge Count +_Only available on macOS and Linux_ + +You can set the app's badge count. +On macOS, it shows on the dock icon. On Linux, it only works for Unity launcher. + +To set the badge count, use the `badgeCount` method: + +```php +App::badgeCount(5); +``` + +To remove the badge count, use the `badgeCount` method with `0` as the argument: + +```php +App::badgeCount(0); +``` + +To get the badge count, use the `badgeCount` method without any arguments: + +```php +$badgeCount = App::badgeCount(); +``` + +### Recent documents list +_Only available on macOS and Windows_ + +The recent documents list is a list of files that the user has recently opened. This list is available on macOS and +Windows. + +To add a document to the recent documents list, use the `addRecentDocument` method: + +```php +App::addRecentDocument('/path/to/document'); +``` + +To clear the recent documents list, use the `clearRecentDocuments` method: + +```php +App::clearRecentDocuments(); +``` + +### Open at login +_Only available on macOS and Windows_ + +To enable 'open at login', use the `openAtLogin` method: + +```php +App::openAtLogin(true); +``` + +To disable open at login, use the `openAtLogin` method with `false` as the argument: + +```php +App::openAtLogin(false); +``` + +To check if the app is set to open at login, use the `openAtLogin` method without any arguments: + +```php +$isOpenAtLogin = App::openAtLogin(); +``` + diff --git a/resources/views/docs/1/the-basics/clipboard.md b/resources/views/docs/desktop/1/the-basics/clipboard.md similarity index 100% rename from resources/views/docs/1/the-basics/clipboard.md rename to resources/views/docs/desktop/1/the-basics/clipboard.md diff --git a/resources/views/docs/1/the-basics/dialogs.md b/resources/views/docs/desktop/1/the-basics/dialogs.md similarity index 100% rename from resources/views/docs/1/the-basics/dialogs.md rename to resources/views/docs/desktop/1/the-basics/dialogs.md diff --git a/resources/views/docs/1/the-basics/global-hotkeys.md b/resources/views/docs/desktop/1/the-basics/global-hotkeys.md similarity index 100% rename from resources/views/docs/1/the-basics/global-hotkeys.md rename to resources/views/docs/desktop/1/the-basics/global-hotkeys.md diff --git a/resources/views/docs/1/the-basics/menu-bar.md b/resources/views/docs/desktop/1/the-basics/menu-bar.md similarity index 99% rename from resources/views/docs/1/the-basics/menu-bar.md rename to resources/views/docs/desktop/1/the-basics/menu-bar.md index 6f1ec98d..ca9df764 100644 --- a/resources/views/docs/1/the-basics/menu-bar.md +++ b/resources/views/docs/desktop/1/the-basics/menu-bar.md @@ -298,4 +298,4 @@ The `Native\Laravel\Events\MenuBar\MenuBarContextMenuOpened` event will be dispa Show only the context menu without opening a window when the menu bar icon is clicked: ```php MenuBar::onlyShowContextMenu(); -``` +``` \ No newline at end of file diff --git a/resources/views/docs/desktop/1/the-basics/notifications.md b/resources/views/docs/desktop/1/the-basics/notifications.md new file mode 100644 index 00000000..f3ecf303 --- /dev/null +++ b/resources/views/docs/desktop/1/the-basics/notifications.md @@ -0,0 +1,182 @@ +--- +title: Notifications +order: 500 +--- + +## Native Notifications + +NativePHP allows you to send system notifications using an elegant PHP API. These notifications are, unlike Laravel's built-in notifications, actual UI notifications displayed by your operating system. + +When used sparingly, notifications can be a great way to inform the user about events that are occurring in your application and to bring their attention back to it, especially if further input from them is required. + +Notifications are sent using the `Notification` facade. + +```php +use Native\Laravel\Facades\Notification; +``` + +### Sending Notifications + +You may send a notification using the `Notification` facade. + +```php +Notification::title('Hello from NativePHP') + ->message('This is a detail message coming from your Laravel app.') + ->show(); +``` + +This will show a system-wide notification to the user with the given title and message. + +### Handling clicks on notifications + +You may register a custom event along with your NativePHP notification. +This event will be fired when a user clicks on the notification, so that you may add some custom logic within your application in this scenario. + +To attach an event to your notification, you may use the `event` method. The argument passed to this method is the class name of the event that should get dispatched upon clicking on the notification. + +```php +Notification::title('Hello from NativePHP') + ->message('This is a detail message coming from your Laravel app.') + ->event(\App\Events\MyNotificationEvent::class) + ->show(); +``` + +### Notification References + +To keep track of different notifications, you may use the notification's `$reference` property. + +By default, a unique reference is generated for you, but you may manually set a reference by [chaining the `reference()`](#notification-reference) method when creating +the notification. + +## Configuring Notifications + +### Notification Title + +You may set the notification's title using the `title()` method. + +```php +Notification::title('Hello from NativePHP') + ->show(); +``` + +### Notification Reference + +You can access the `$reference` property of a notification after it has been created: + +```php +$notification = Notification::title('Hello from NativePHP')->show(); + +$notification->reference; +``` + +You may chain the `reference()` method to set a custom reference when creating a notification: + +```php +Notification::title('Hello from NativePHP') + ->reference(Str::uuid()) + ->show(); +``` + +The reference will be sent along with any event triggered by the notification and can be used to track which specific notification was clicked: + +```php +use App\Events\PostNotificationClicked; +use App\Models\Post; + +Post::recentlyCreated() + ->get() + ->each(function(Post $post) { + Notification::title('New post: ' . $post->title) + ->reference($post->id) + ->event(PostNotificationClicked::class) + ->show(); + }); + +Event::listen(PostNotificationClicked::class, function (PostNotificationClicked $event) { + $post = Post::findOrFail($event->reference); + + Window::open()->url($post->url); +}); +``` + +### Notification Message + +You may set the notification's message using the `message()` method. + +```php +Notification::title('Hello from NativePHP') + ->message('This is a detail message coming from your Laravel app.') + ->show(); +``` + +### Notification Reply + +On macOS, you can allow the user to reply to a notification using the `hasReply()` method. + +```php +Notification::title('Hello from NativePHP') + ->hasReply() + ->show(); +``` + +The `hasReply()` method accepts a placeholder reply message as an argument. + +```php +Notification::title('Hello from NativePHP') + ->hasReply('This is a placeholder') + ->show(); +``` + +### Notification Actions + +On macOS, you can add action buttons to a notification using the `addAction()` method. + +```php +Notification::title('Hello from NativePHP') + ->addAction('Click here') + ->show(); +``` + +You can call the `addAction()` method multiple times if you need to add multiple buttons. + +```php +Notification::title('Hello from NativePHP') + ->addAction('Button One') + ->addAction('Button Two') + ->show(); +``` + +When an action button is clicked, it will trigger the [`NotificationActionClicked`](#codenotificationactionclickedcode) event. + +This event contains an `$index` property, which refers to the index of the action button that was clicked. Action button indexes start at `0`: + +```php +use Native\Laravel\Events\Notifications\NotificationActionClicked; + +Notification::title('Do you accept?') + ->addAction('Accept') // This action will be $index = 0 + ->addAction('Decline') // This action will be $index = 1 + ->show(); + +Event::listen(NotificationActionClicked::class, function (NotificationActionClicked $event) { + if ($event->index === 0) { + // 'Accept' clicked + } elseif ($event->index === 1) { + // 'Decline' clicked + } +}); +``` + +## Events + +### `NotificationClicked` +The `Native\Laravel\Events\Notifications\NotificationClicked` event is dispatched when a user clicks on a notification. + +### `NotificationClosed` +The `Native\Laravel\Events\Notifications\NotificationClosed` event is dispatched when a user closes a notification. + +### `NotificationReply` +The `Native\Laravel\Events\Notifications\NotificationReply` event is dispatched when a user replies to a notification. + +### `NotificationActionClicked` +The `Native\Laravel\Events\Notifications\NotificationActionClicked` event is dispatched when a user clicks an action button on a notification. diff --git a/resources/views/docs/1/the-basics/power-monitor.md b/resources/views/docs/desktop/1/the-basics/power-monitor.md similarity index 100% rename from resources/views/docs/1/the-basics/power-monitor.md rename to resources/views/docs/desktop/1/the-basics/power-monitor.md diff --git a/resources/views/docs/1/the-basics/screens.md b/resources/views/docs/desktop/1/the-basics/screens.md similarity index 100% rename from resources/views/docs/1/the-basics/screens.md rename to resources/views/docs/desktop/1/the-basics/screens.md diff --git a/resources/views/docs/1/the-basics/settings.md b/resources/views/docs/desktop/1/the-basics/settings.md similarity index 87% rename from resources/views/docs/1/the-basics/settings.md rename to resources/views/docs/desktop/1/the-basics/settings.md index f738b01e..01bde91d 100644 --- a/resources/views/docs/1/the-basics/settings.md +++ b/resources/views/docs/desktop/1/the-basics/settings.md @@ -27,11 +27,16 @@ To retrieve a setting, use the `get` method. $value = Settings::get('key'); ``` -You may also provide a default value to return if the setting does not exist. +You may also provide a default value to return if the setting does not exist. You can provide either a static default value, or a closure: ```php $value = Settings::get('key', 'default'); ``` -If the setting does not exist, `default` will be returned. +```php +$value = Settings::get('key', function () { + return 'default'; + }); +``` +If the setting does not exist, and no default value is provided, `null` will be returned. ### Forgetting a value If you want to remove a setting altogether, use the `forget` method. diff --git a/resources/views/docs/1/the-basics/shell.md b/resources/views/docs/desktop/1/the-basics/shell.md similarity index 100% rename from resources/views/docs/1/the-basics/shell.md rename to resources/views/docs/desktop/1/the-basics/shell.md diff --git a/resources/views/docs/1/the-basics/system.md b/resources/views/docs/desktop/1/the-basics/system.md similarity index 72% rename from resources/views/docs/1/the-basics/system.md rename to resources/views/docs/desktop/1/the-basics/system.md index b1cbe022..2717146c 100644 --- a/resources/views/docs/1/the-basics/system.md +++ b/resources/views/docs/desktop/1/the-basics/system.md @@ -2,10 +2,11 @@ title: System order: 800 --- + ## The System One of the main advantages of building a native application is having more direct access to system resources, such as -peripherals connected to the physical device and APIs that aren't typically accessible inside a browser's sandbox. +peripherals connected to the physical device and APIs that aren't typically accessible inside a browser's sandbox. NativePHP makes it trivial to access these resources and APIs. @@ -19,6 +20,7 @@ While some features are platform-specific, NativePHP gracefully handles this for about whether something is Linux-, Mac-, or Windows-only. Most of the system-related features are available through the `System` facade. + ```php use Native\Laravel\Facades\System; ``` @@ -109,6 +111,25 @@ System::print('...', $printer); If no `$printer` object is provided, the default printer and settings will be used. +You can also print directly to PDF: + +```php +System::printToPDF('...'); +``` + +This returns the PDF data in a `base64_encoded` binary string. So be sure to `base64_decode` it before storing it to +disk: + +```php +use Illuminate\Support\Facades\Storage; + +$pdf = System::printToPDF('...'); + +Storage::disk('desktop')->put('My Awesome File.pdf', base64_decode($pdf)); +``` + +### Print Settings + You can change the configuration before sending something to be printed, for example if you want multiple copies: ```php @@ -117,22 +138,39 @@ $printer->options['copies'] = 5; System::print('...', $printer); ``` -You can also print directly to PDF: +Additionally, both the `print()` and `printToPDF()` methods accept an optional `$settings` parameter that allows you to customize the print behavior: ```php -System::printToPDF('...'); +System::print('...', $printer, $settings); ``` -This returns the PDF data in a `base64_encoded` binary string. So be sure to `base64_decode` it before storing it to disk: +#### Print Settings Examples + +You can customize print behavior using the settings array. Here are some common examples: ```php -use Illuminate\Support\Facades\Storage; +// Print with custom page size and orientation +$settings = [ + 'pageSize' => 'A4', + 'landscape' => true, +]; -$pdf = System::printToPDF('...'); +System::print('...', $printer, $settings); +``` -Storage::disk('desktop')->put('My Awesome File.pdf', base64_decode($pdf)); +```php +// Print multiple copies with duplex +$settings = [ + 'copies' => 3, + 'duplexMode' => 'longEdge', // 'simplex', 'shortEdge', 'longEdge' + 'color' => false, // true for color, false for monochrome +]; + +System::print('...', $printer, $settings); ``` +For a complete list of available print settings, refer to the [Electron webContents.print()](https://www.electronjs.org/docs/latest/api/web-contents#contentsprintoptions-callback) and [webContents.printToPDF()](https://www.electronjs.org/docs/latest/api/web-contents#contentsprinttopdfoptions) documentation. + ## Time Zones PHP and your Laravel application will generally be configured to work with a specific time zone. This could be UTC, for @@ -160,3 +198,25 @@ $timezone = System::timezone(); // $timezone => 'Europe/London' ``` + +## Theme + +NativePHP allows you to detect the current theme of the user's operating system. This is useful for applications that +want to adapt their UI to match the user's preferences. +You can use the `System::theme()` method to get the current theme of the user's operating system. + +```php +$theme = System::theme(); +// $theme => SystemThemesEnum::LIGHT, SystemThemesEnum::DARK or SystemThemesEnum::SYSTEM +``` + +You can also set the theme of your application using the `System::theme()` method. This will change the theme of your +application to the specified value. The available options are `SystemThemesEnum::LIGHT`, `SystemThemesEnum::DARK` and +`SystemThemesEnum::SYSTEM`. + +```php +System::theme(SystemThemesEnum::DARK); +``` + +Setting the theme to `SystemThemesEnum::SYSTEM` will remove the override and everything will be reset to the OS default. +By default themeSource is `SystemThemesEnum::SYSTEM`. diff --git a/resources/views/docs/1/the-basics/windows.md b/resources/views/docs/desktop/1/the-basics/windows.md similarity index 80% rename from resources/views/docs/1/the-basics/windows.md rename to resources/views/docs/desktop/1/the-basics/windows.md index 8d8edec1..9809bddf 100644 --- a/resources/views/docs/1/the-basics/windows.md +++ b/resources/views/docs/desktop/1/the-basics/windows.md @@ -366,6 +366,83 @@ Window::open() ->hideMenu(); ``` +### Taskbar and Mission Control Visibility + +You may control whether a window appears in the taskbar and Mission Control. + +#### Skip Taskbar + +By default, all windows created with the `Window` facade will appear in the taskbar on Windows and macOS. +You may use the `skipTaskbar()` method to prevent a window from appearing in the taskbar. + +```php +Window::open() + ->skipTaskbar(); +``` + +This is useful for utility windows, floating toolboxes, or background windows that should not clutter the taskbar. + +#### Hidden in Mission Control + +On macOS, all windows will appear in Mission Control by default. +You may use the `hiddenInMissionControl()` method to prevent a window from appearing when the user toggles into Mission Control. + +```php +Window::open() + ->hiddenInMissionControl(); +``` + +This is particularly useful for always-on-top utility windows or menubar applications that should not be visible in Mission Control. + +### Restrict navigation within a window + +When opening windows that display content that is not under your control (such as external websites), you may want to +restrict the user's navigation options. NativePHP provides two handy methods for this on the `Window` facade: + +```php +Window::open() + ->url('https://nativephp.com/') + ->preventLeaveDomain(); + +Window::open() + ->url('https://laravel-news.com/bifrost') + ->preventLeavePage(); +``` + +The `preventLeaveDomain()` method allows navigation within the same domain but blocks any attempt to navigate away to a +different domain, scheme or port. + +With `preventLeavePage()` you can strictly confine the user to the initially rendered page. Any attempt to navigate to a +different path (even within the same domain) will be blocked. However, in-page navigation via anchors (e.g. "#section") +and updates to the query string remain permitted. + +#### Preventing new windows from popping up + +By default, Electron allows additional windows to be opened from a window that was previously opened programmatically. +This is the case, for example, with `a` elements that have the target attribute set to `_blank` or when the user clicks on a link with the middle mouse button. +This behaviour is potentially undesirable in a desktop application, as it enables the user to "break out" of a window. + +To prevent additional windows from opening, you can apply the `suppressNewWindows()` method when opening a new window. + +```php +Window::open() + ->suppressNewWindows(); +``` + +### Zoom factor + +In certain cases, you may want to set a zoom factor for a window. +This can be particularly helpful in cases where you have no control over the content displayed (e.g. when showing external websites). +You may use the `zoomFactor` method to define a zoom factor. + +```php +Window::open() + ->zoomFactor(1.25); +``` + +The zoom factor is the zoom percent divided by 100. +This means that you need to pass the value `1.25` if you want the window to be displayed at 125% size. + ## Window Title Styles ### Default Title Style diff --git a/resources/views/docs/desktop/2/_index.md b/resources/views/docs/desktop/2/_index.md new file mode 100644 index 00000000..e69de29b diff --git a/resources/views/docs/desktop/2/digging-deeper/_index.md b/resources/views/docs/desktop/2/digging-deeper/_index.md new file mode 100644 index 00000000..7d67168d --- /dev/null +++ b/resources/views/docs/desktop/2/digging-deeper/_index.md @@ -0,0 +1,4 @@ +--- +title: Digging Deeper +order: 3 +--- diff --git a/resources/views/docs/desktop/2/digging-deeper/broadcasting.md b/resources/views/docs/desktop/2/digging-deeper/broadcasting.md new file mode 100644 index 00000000..83d41bb3 --- /dev/null +++ b/resources/views/docs/desktop/2/digging-deeper/broadcasting.md @@ -0,0 +1,110 @@ +--- +title: Broadcasting +order: 100 +--- + +# Broadcasting + +NativePHP facilitates event broadcasting of both [native events](#native-events) (emitted by Electron) and +[custom events](#custom-events) dispatched by your Laravel app. You can listen to all of these events in your +Laravel application as you normally would or in the [JavaScript](#listening-with-javascript) on your pages. + +## Native events + +NativePHP fires various events during its operations, such as `WindowBlurred` & `NotificationClicked`. A full list +of all events fired and broadcast by NativePHP can be found in the +[`src/Events`](https://github.com/nativephp/desktop/tree/main/src/Events) folder. + +## Custom events + +You can also broadcast your own events. Simply implement the `ShouldBroadcastNow` contract in your event class and +define the `broadcastOn` method, returning `nativephp` as one of the channels it broadcasts to: + +```php +use Illuminate\Broadcasting\Channel; +use Illuminate\Contracts\Broadcasting\ShouldBroadcastNow; + +class JobFinished implements ShouldBroadcastNow +{ + public function broadcastOn(): array + { + return [ + new Channel('nativephp'), + ]; + } +} +``` + +This is particularly useful for scenarios where you want to offload an intensive task to a background queue and await +its completion without constantly polling your application for its status. + +## Listening with JavaScript + +You can listen to all native and custom events emitted by your application in real-time using JavaScript. + +NativePHP injects a `window.Native` object into every window. The `on()` method allows you to register a callback as +the second parameter that will run when the event specified in the first parameter is fired: + +```js +window.addEventListener('native:init', () => { + + Native.on('Native\\Desktop\\Events\\Windows\\WindowBlurred', (payload, event) => { + // + }) + + // +}) +``` + +Make sure you declare the listener inside a `native:init` handler, otherwise there is a possibility the `Native` object is not injected inside your window yet. + +## Listening with Livewire + +To make this process even easier when using [Livewire](https://livewire.laravel.com), you may use the `native:` prefix when +listening to events. This is similar to +[listening to Laravel Echo events using Livewire](https://livewire.laravel.com/docs/events#real-time-events-using-laravel-echo). + +You may use a string name: + +```php +class AppSettings extends Component +{ + public $windowFocused = true; + + #[On('native:\\Native\\Desktop\\Events\\Windows\\WindowFocused')] + public function windowFocused() + { + $this->windowFocused = true; + } + + #[On('native:\\Native\\Desktop\\Events\\Windows\\WindowBlurred')] + public function windowBlurred() + { + $this->windowFocused = false; + } +} +``` + +You may find it more convenient to use PHP's class name resolution keyword, `::class`: + +```php +use Native\Desktop\Events\Windows\WindowBlurred; +use Native\Desktop\Events\Windows\WindowFocused; + +class AppSettings extends Component +{ + public $windowFocused = true; + + #[On('native:'.WindowFocused::class)] + public function windowFocused() + { + $this->windowFocused = true; + } + + #[On('native:'.WindowBlurred::class)] + public function windowBlurred() + { + $this->windowFocused = false; + } +} +``` diff --git a/resources/views/docs/desktop/2/digging-deeper/child-processes.md b/resources/views/docs/desktop/2/digging-deeper/child-processes.md new file mode 100644 index 00000000..dbb65769 --- /dev/null +++ b/resources/views/docs/desktop/2/digging-deeper/child-processes.md @@ -0,0 +1,376 @@ +--- +title: Child Processes +order: 700 +--- + +# Child Processes + +Child Processes allow your application to spin up managed processes, forked from your app's main process. This is great +for long-running processes that you want to interact with repeatedly during the life of your application. + +Child Processes can be managed from your application using a straightforward API. When your app quits, these processes +get shut down gracefully. + +"Spawning" a Child Process is like running a command from the CLI. Any command you can run in the terminal can be a +Child Process. + +```php +ChildProcess::start( + cmd: 'tail -f storage/logs/laravel.log', + alias: 'tail' +); +``` + +Any process invoked using the ChildProcess facade will be non-blocking and keep running in the background. Even if the request that triggered it has finished. + +**Bear in mind that your Child Process ("command line") arguments may need to differ depending on which platform your +application is running on (Mac/Linux vs Windows).** + +**Where possible, you should explicitly reference binaries by their full path name, unless you can reliably assume that +the executable you're trying to spawn is available in the user's `PATH`.** + +Child Processes are managed by the runtime (Electron) but are fully accessible to the Laravel side of your +application. + +--- + +## Alternatives + +Before deciding to use a Child Process, consider the alternatives available to you. You should pick the most +appropriate for the problem you're trying to solve: + +### Queues + +The [queue runner](queues) is useful for very simply offloading _Laravel_ tasks to the background. Each task must be a +Laravel queued [Job](https://laravel.com/docs/queues#creating-jobs). + +Any queued jobs that don't get processed before your app is quit, will get processed when your application (and the +queue runner) starts again. + +### Scheduler + +The Laravel scheduler runs as normal (every minute) inside a NativePHP application. You can add +[scheduled tasks](https://laravel.com/docs/scheduling) to your application just as you normally would, to have them run +on a regular schedule. + +Any scheduled tasks that would have run while your application isn't running will be skipped. + +**The queue runner and the scheduler are tied to your _application_, not the operating system, so they will +only be able to run while your application is running.** + +### `shell_exec`, `proc_open` etc + +PHP has good built-in support for running arbitrary programs in separate processes. For example: + +- [`shell_exec`](https://www.php.net/manual/en/function.shell-exec.php) allows you to run commands and return their + output to your application. +- [`proc_open`](https://www.php.net/manual/en/function.proc-open.php) allows you to spin up a command with more control + over how its input and output streams are handled. + +While these can be used in your NativePHP application, consider that they: + +- May block the script that is executing them until the sub-process has finished. +- May become orphaned from your application, allowing them to continue running after your app has quit. + +Runaway orphaned processes could negatively impact your user's system and can become tricky to manage without user +intervention. You should be cautious about starting processes this way. + +--- + +## Starting a Child Process + +Each Child Process must have a unique alias. This is the name you will use to reference and interact with this process +throughout your application. + +You may start a process using the `ChildProcess` facade: + +```php +use Native\Desktop\Facades\ChildProcess; + +ChildProcess::start( + cmd: 'tail -f storage/logs/laravel.log', + alias: 'tail' +); +``` + +The `start` method will return a `Native\Desktop\ChildProcess` instance, which represents the process. You may interact +directly with this instance to make changes to that process, but this does not necessarily mean that the +process was started. + +The timing of process initilization is controlled by the user's operating system and spawning +may fail for a number of reasons. + +**To determine if the process has started successfully, you should listen for the +[`ProcessSpawned` event](#codeprocessspawnedcode).** + +### Current Working Directory + +By default, the child process will use the working directory of your application as it's "current working directory" +(`cwd`). However, you can explicitly change this if needed by passing a string path to the `$cwd` parameter of the +`start` method: + +```php +ChildProcess::start( + cmd: ['tail', '-f', 'logs/laravel.log'], + alias: 'tail', + cwd: storage_path() +); +``` + +### Persistent Processes + +You may mark a process as `persistent` to indicate that the runtime should make sure that once it has been started it +is always running. This works similarly to tools like [`supervisord`](http://supervisord.org/), ensuring that the +process gets booted up again in case it crashes. + +```php +ChildProcess::start( + cmd: ['tail', '-f', 'logs/laravel.log'], + alias: 'tail', + persistent: true +); +``` + +### PHP scripts + +For your convenience, NativePHP provides a simple method to execute PHP scripts in the background using NativePHP's packaged PHP binary: + +```php +ChildProcess::php('path/to/script.php', alias: 'script'); +``` + +### Artisan commands + +NativePHP provides a similar method convenience for Artisan commands: + +```php +ChildProcess::artisan('smtp:serve', alias: 'smtp-server'); +``` + +### Node scripts + +NativePHP provides a convenient method to execute JavaScript files using the bundled Node.js runtime: + +```php +ChildProcess::node( + cmd: 'resources/js/filesystem-watcher.js', + alias: 'filesystem-watcher' +); +``` + +This method automatically uses the Node.js runtime that ships with your NativePHP application, ensuring consistency across different environments. + +**Key benefits:** +- No need to compile JavaScript files beforehand +- Dependencies can be used directly without bundling for browser compatibility +- Leverages the same Node.js version across all platforms + +## Getting running processes + +### Getting a single process + +You can use the `ChildProcess` facade's `get` method to get a running process with a given alias: + +```php +$tail = ChildProcess::get('tail'); +``` + +This will return a `Native\Desktop\ChildProcess` instance. + +### Getting all processes + +You can use the `ChildProcess` facade's `all` method to get all running processes: + +```php +$processes = ChildProcess::all(); +``` + +This will return an array of `Native\Desktop\ChildProcess` instances. + +## Stopping a Child Process + +Your child processes will shut down when your application exits. However, you may also choose to stop them manually or +provide this control to your user. + +If you have a `Native\Desktop\ChildProcess` instance, you may call the `stop` method on it: + +```php +$tail->stop(); +``` + +Alternatively, you may use the `ChildProcess` facade to stop a process via its alias: + +```php +ChildProcess::stop('tail'); +``` + +This will attempt to stop the process gracefully. The [`ProcessExited`](#codeprocessexitedcode) event will be +dispatched if the process exits. + +Note that [persistent processes](/docs/digging-deeper/child-processes#persistent-processes) will be permanently stopped and will only be restarted when the `start` method is called again. If you want to restart a persistent process, use the `restart` method instead. + +## Restarting a Child Process + +As a convenience, you may simply restart a Child Process using the `restart` method. This may be useful in cases where +the program has become unresponsive and you simply need to "reboot" it. + +If you have a `Native\Desktop\ChildProcess` instance, you may call the `restart` method on it: + +```php +$tail->restart(); +``` + +Alternatively, you may use the `ChildProcess` facade to restart a process via its alias: + +```php +ChildProcess::restart('tail'); +``` + +## Sending input + +There are multiple ways to provide input to your Child Process: + +- The environment. +- Arguments to the command. +- Its standard input stream (`STDIN`). +- A custom interface, e.g. a network socket. + +Which you use will depend on what the program is capable of handling. + +### Environment + +Child Processes will inherit the environment available to your application by default. If needed, you can provide extra +environment variables when starting the process via the `$env` parameter of the `start` method: + +```php +ChildProcess::start( + cmd: 'tail ...', + alias: 'tail', + env: [ + 'CUSTOM_ENV_VAR' => 'custom value', + ] +); +``` + +### Command line arguments + +You can pass arguments to the program via the `$cmd` parameter of the `start` method. This accepts a `string` or an +`array`, whichever you prefer to use: + +```php +ChildProcess::start( + cmd: ['tail', '-f', 'storage/logs/laravel.log'], + alias: 'tail' +); +``` + +### Messaging a Child Process + +You may send messages to a running child process's standard input stream (`STDIN`) using the `message` method: + +```php +$tail->message('Hello, world!'); +``` + +Alternatively, you may use the `ChildProcess` facade to message a process via its alias: + +```php +ChildProcess::message('Hello, world!', 'tail'); +``` + +The message format and how they are handled will be determined by the program you're running. + +## Handling output + +A Child Process may send output via any of the following interfaces: + +- Its standard output stream (`STDOUT`). +- Its standard error stream (`STDERR`). +- A custom interface, e.g. a network socket. +- Broadcasting a Custom Event + +`STDOUT`, `STDERR` & [Custom Events](/docs/digging-deeper/broadcasting#custom-events) are dispatched using +Laravel's event system. + +You may listen to these events by registering a listener in your app service provider, or on the front end +using the [Native helper](/docs/digging-deeper/broadcasting#listening-with-javascript). + +Please see the [Events](#events) section for a full list of events. + +### Listening for Output (`STDOUT`) + +You may receive standard output for a process by registering an event listener for the +[`MessageReceived`](#codemessagereceivedcode) event: + +### Listening for Errors (`STDERR`) + +You may receive standard errors for a process by registering an event listener for the +[`ErrorReceived`](#codeerrorreceivedcode) event: + +## Events + +NativePHP provides a simple way to listen for Child Process events. + +All events get dispatched as regular Laravel events, so you may use your `AppServiceProvider` to register listeners. + +```php +use Illuminate\Support\Facades\Event; +use Native\Desktop\Events\ChildProcess\MessageReceived; + +/** + * Bootstrap any application services. + */ +public function boot(): void +{ + Event::listen(MessageReceived::class, function(MessageReceived $event) { + if ($event->alias === 'tail') { + // + } + }); +} + +``` + +Sometimes you may want to listen and react to these events in real-time, which is why NativePHP also broadcasts all +Child Process events to the `nativephp` broadcast channel. Any events broadcasted this way also get dispatched over IPC, enabling you to react to them on the front-end without using websockets. + +```js +window.addEventListener('native:init', () => { + + Native.on('Native\\Desktop\\Events\\ChildProcess\\MessageReceived', (event) => { + if (event.alias === 'tail') { + container.append(event.data) + } + }) + + // +}) +``` + +To learn more about NativePHP's broadcasting capabilities, please refer to the +[Broadcasting](/docs/digging-deeper/broadcasting) section. + +### `ProcessSpawned` + +This `Native\Desktop\Events\ChildProcess\ProcessSpawned` event will be dispatched when a Child Process has successfully +been spawned. The payload of the event contains the `$alias` and the `$pid` of the process. + +**In Electron, the `$pid` here will be the Process ID of an Electron Helper process which spawns the underlying +process.** + +### `ProcessExited` + +This `Native\Desktop\Events\ChildProcess\ProcessExited` event will be dispatched when a Child Process exits. The +payload of the event contains the `$alias` of the process and its exit `$code`. + +### `MessageReceived` + +This `Native\Desktop\Events\ChildProcess\MessageReceived` event will be dispatched when the Child Process emits some +output via its standard output stream (`STDOUT`). The payload of the event contains the `$alias` of the process and the +message `$data`. + +### `ErrorReceived` + +This `Native\Desktop\Events\ChildProcess\ErrorReceived` event will be dispatched when the Child Process emits an error +via its standard error stream (`STDERR`). The payload of the event contains the `$alias` of the process and the +error `$data`. diff --git a/resources/views/docs/desktop/2/digging-deeper/databases.md b/resources/views/docs/desktop/2/digging-deeper/databases.md new file mode 100644 index 00000000..cb64e252 --- /dev/null +++ b/resources/views/docs/desktop/2/digging-deeper/databases.md @@ -0,0 +1,103 @@ +--- +title: Databases +order: 200 +--- + +# Working with Databases + +Almost every application needs a database, especially if your app is working with complex user data or communicating +with an API. A database is an efficient and reliable way to persist structured data across multiple versions of +your application. + +When building a _server-side_ application, you are free to choose the database engine you prefer. But in the context of +a self-contained native application, your choices are limited to: +- what you can reasonably bundle with your app; or +- what you can expect the user's system to have installed. + +**To keep the footprint of your application small, NativePHP currently only supports SQLite out of the box.** + +You can interact with SQLite via PDO or an ORM, such as Eloquent, in exactly the way you're used to. + +## SQLite + +[SQLite](https://sqlite.org/) is a feature-rich, portable, lightweight, file-based database. It's perfect for native +applications that need persistent storage of complex data structures with the speed and tooling of SQL. + +Its small footprint and minimal dependencies make it ideal for cross-platform, native applications. Your users +don't need to install anything else besides your app, and it doesn't add hundreds of MBs to your bundle, +keeping download & install size small. + +### Configuration + +You do not need to do anything special to configure your application to use SQLite. NativePHP will automatically: +- Switch to using SQLite when building your application. +- Create a database file for you in the `appdata` directory on the user's system. +- Configure your application to use that database file. +- Run your migrations each time your app starts, as needed. + +## Development + +In [development](/docs/getting-started/development), your application uses a database called `nativephp.sqlite` +which is created in the build directory. + +NativePHP forces your application to use this database when it is running within the Electron environment so that +it doesn't modify any other SQLite databases you may already be using. + +## Migrations + +When writing migrations, you need to consider any special recommendations for working with SQLite. + +For example, prior to Laravel 11, SQLite foreign key constraints are turned off by default. If your application relies +upon foreign key constraints, [you need to enable SQLite support for them](https://laravel.com/docs/database#configuration) before running your migrations. + +**It's important to test your migrations on prod builds before releasing updates!** You don't want to accidentally +delete your user's data when they update your app. + +### In production + +In production builds of your app, NativePHP will check to see if the app version has changed and attempt to migrate +the user's copy of your database in their `appdata` folder. + +During development, you will need to migrate your development database manually: + +```shell +php artisan native:migrate +``` + +This command uses the exact same signature as the Laravel `migrate` command, so everything you're used to there can be +used here. + +You can do this whether the application is running or not, but depending on how your app behaves, it may be better to +do it _before_ running your app. + +### Refreshing your app database + +You can completely refresh your app database using the `native:migrate:fresh` command: + +```shell +php artisan native:migrate:fresh +``` + +**This is a destructive action that will delete all data in your database.** + +## Seeding + +When developing, it's especially useful to seed your database with sample data. If you've set up +[Database Seeders](https://laravel.com/docs/seeding), you can run these using the `native:seed` command: + +```shell +php artisan native:seed +``` + +## When not to use a database + +If you're only storing small amounts of very simple metadata or working files, you may not need a database at all. +Consider [storing files](/docs/digging-deeper/files) instead. These could be JSON, CSV, plain text or any other format +that makes sense for your application. + +Consider also using file storage for very critical metadata about the state of your application on a user's device. +If you rely on the same database you store the user's data to store this information, if the database becomes +corrupted for any reason, your application may not be able to start at all. + +If you store this information in a file, you can at least instruct your users to delete the file and restart the +application lowering the risk of deleting their data. diff --git a/resources/views/docs/desktop/2/digging-deeper/files.md b/resources/views/docs/desktop/2/digging-deeper/files.md new file mode 100644 index 00000000..19878278 --- /dev/null +++ b/resources/views/docs/desktop/2/digging-deeper/files.md @@ -0,0 +1,143 @@ +--- +title: Files +order: 300 +--- + +## Files & Paths + +Working with files in NativePHP is just like working with files in a regular Laravel application. +To achieve this, NativePHP rewrites the `Application::$storagePath()` (and thus `app()->storagePath()` and the `storage_path()` helper) +to the [Electron `app.getPath('appData')` path](https://www.electronjs.org/docs/latest/api/app#appgetpathname), +which is different for each operating system. + +This means that you can continue to use Laravel's `Storage` facade to store and retrieve files on your user's file +system just as you would on your server. + +If you use the default Storage configuration for the `local` filesystem, your `local` disk will also point to this +`appdata` directory, followed by `storage/app`. + +![](/img/appdata.png) + +Here you may see some folders you recognise, namely `database` and `storage`. The other folders are managed by Electron. +The `storage` folder is exactly the same `storage` directory you are used to seeing in your Laravel application. It +stores various caches and also application logs. + +You should use this `Application::storagePath()` when storing files on your user's computer that need to remain +available even when your application is updated or removed from the system, e.g. your application's configuration, +settings and any user data that the user doesn't need direct access to. + +It's also the location where your SQLite database will be stored. + +### Storing files elsewhere + +NativePHP doesn't interfere with any of your _existing_ filesystem configuration, so you may continue to configure +[Filesystem](https://laravel.com/docs/filesystem) as you normally would, however you should be aware that it does +_add_ some new default filesystems for your convenience. + +Consider that your users want to store their files in locations other than the obscure `appdata` directories on their +preferred OS. To that end, NativePHP provides a variety of convenient `filesystems` which are configured at runtime to +point to the respective, platform-specific directories for the current user. + +[warning] +If your application also defines any of these filesystems, NativePHP will override their configuration with its own. +[/warning] + +You can use these filesystem simply using the `Storage` facade like this: + +```php +Storage::disk('user_home')->get('file.txt'); +Storage::disk('desktop')->get('file.txt'); +Storage::disk('documents')->get('file.txt'); +Storage::disk('downloads')->get('file.txt'); +Storage::disk('music')->get('file.txt'); +Storage::disk('pictures')->get('file.txt'); +Storage::disk('videos')->get('file.txt'); +Storage::disk('recent')->get('file.txt'); +Storage::disk('extras')->get('file.txt'); +``` + +Note that the PHP process which runs your application operates with the same privileges as the logged-in user, this +means your application is able to read and write files wherever your user is authorised to. + +Generally, you should only read and write files to the user's `home` directory or your app's `appdata` directory. Be +aware that some operating systems now actively prompt the user to grant permissions to apps when they first attempt to +access directories in the user's home directory. + +See [Security](/docs/digging-deeper/security) for more considerations. + +[aside] +You can also continue to use cloud storage providers if you wish. + +However, be mindful that an application installed on a user's device is even more likely to experience network +disruption than one operating on a server in the cloud, as your users may be without an internet connection at any +time. + +You should prepare more carefully for such scenarios when interacting with any APIs that require network connectivity +by checking for a connection _before_ making a request and/or handling exceptions gracefully should a request fail. + +This will help maintain a smooth user experience +[/aside] + +NativePHP uses the `local` disk by default. If you would like to use a different disk, you may configure this in your +`config/filesystems.php` file. + +Remember, you can set the filesystem disk your application uses by default in your `config/filesystems.php` file or by +adding a `FILESYSTEM_DISK` variable to your `.env` file. + +### Symlinks + +In traditional web servers, symlinking directories is a common practice - for example, it's a convenient way to expose parts of your filesystem through the public directory. + +However, in a NativePHP app, this approach is less applicable. The entire application is already accessible to the end user, and symlinks can cause unexpected issues during the packaging process due to differences in how Unix-like and Windows systems handle symbolic links. + +We recommend avoiding symlinks within your NativePHP app. Instead, consider either: + +- Placing files directly in their intended locations +- Using Laravel's Storage facade to mount directories outside your application + +## Bundling Application Resources + +NativePHP allows you to bundle additional files with your application that can be accessed at runtime. This is useful for including pre-compiled executables or other resources that need to be distributed with your app. + +### Adding Files + +To include extra files with your application, place them in a `extras/` directory at the root of your Laravel project: + +``` +your-project/ +├── extras/ +│ ├── my-tool.sh +│ ├── my-tool.exe +│ └── sample.csv +├── app/ +└── ... +``` + +These files will be automatically bundled with your application during the build process. + +The `extras` disk is read-only. Any files you write to this directory will be overwritten when your application is updated. + +### Accessing Files + +You can access bundled extra files using Laravel's Storage facade with the `extras` disk: + +```php +use Illuminate\Support\Facades\Storage; + +$toolPath = Storage::disk('extras')->path('my-tool.exe'); +``` + +### Using Bundled Tools + +```php +use Illuminate\Support\Facades\Storage; +use Native\Desktop\Facades\ChildProcess; + +// Get the path to a bundled executable +$toolPath = Storage::disk('extras')->path('my-tool.sh'); + +ChildProcess::start( + cmd: $toolPath, + alias: 'my-tool' +); +``` diff --git a/resources/views/docs/desktop/2/digging-deeper/php-binaries.md b/resources/views/docs/desktop/2/digging-deeper/php-binaries.md new file mode 100644 index 00000000..11c04289 --- /dev/null +++ b/resources/views/docs/desktop/2/digging-deeper/php-binaries.md @@ -0,0 +1,80 @@ +--- +title: PHP Binaries +order: 600 +--- +# Static PHP + +At the heart of NativePHP are the platform-specific, single-file PHP binaries, which are portable, statically-compiled +versions of PHP. + +These allow us to ship PHP to a user's device without forcing them to compile from source or manage a sprawling set of +dynamic libraries and configuration files. + +It also means that your applications can each use an isolated version of PHP without depending on or interfering with +the version of PHP the user may already have installed on their machine or in another NativePHP app. + +The binaries that ship with NativePHP are built to have a _minimal_ set of the most common PHP extensions required to +run almost any web application you can build with Laravel. + +One key goal of NativePHP is to maintain feature parity across platforms so that you can reliably distribute your apps +to users on any device. This means that we will only ship PHP with extensions that can be supported across Windows, +macOS and Linux. + +On top of this, fewer PHP extensions means a smaller application size and attack surface. Beware that installing more +extensions has both performance & [security](security) implications for your apps. + +The extensions that are included in the default binaries are defined in the +[`php-extensions.txt`](https://github.com/NativePHP/php-bin/blob/main/php-extensions.txt) in the `php-bin` repo. + +If you think an extension is missing that would make sense as a default extension, feel free to +[make a feature request](https://github.com/nativephp/desktop/issues/new/choose) for it. + +## Building custom binaries + +NativePHP uses the awesome [`static-php-cli`](https://static-php.dev/) library to build distributable PHP binaries. + +You may use this too to build your own binaries. Of course, you may build static binaries however you prefer. + +Whichever method you use, you should aim to create a single-file executable that has statically linked all of its +dependencies for each platform and architecture that you wish your app to run on. + +### Building apps with custom binaries + +In order to use your custom binaries, you will need to instruct NativePHP where to find them. + +To do this, you may use the `NATIVEPHP_PHP_BINARY_PATH` environment variable. You can set this in your `.env` file. +For example, if you store the binaries in a `bin` folder in the root of your application: + +```dotenv +NATIVEPHP_PHP_BINARY_PATH=/path/to/your-nativephp-app/bin/ +``` + +The binaries you are using need to be stored in a structure that mirrors the folder structure found in the `php-bin` +package: + +![PHP binary folder structure](/img/docs/php-binaries.png) + +Note how the platform is the first folder (`linux`, `mac`, `win`) and the architecture is provided as a subfolder +(`x64`, `arm64`, `x86`). + +You do not need to build binaries for every PHP version or every platform; You only need binaries for the platforms you +wish to support and for the version of PHP that your application requires. + +Make sure the binaries are named `php` (macOS/Linux) or `php.exe` (Windows) and zipped and named like so: + +```shell +// macoS / Linux +zip php-[PHP_MAJOR_VERSION].[PHP_MINOR_VERSION].zip php + +// Windows +powershell Compress-Archive -Path "php.exe" -DestinationPath "php-[PHP_MAJOR_VERSION].[PHP_MINOR_VERSION].zip" +``` + +NativePHP will then build your application using the relevant binaries found in this custom location. + +## A note on safety & support + +When using custom binaries, you should make every reasonable effort to secure your build pipeline so as not to allow an +attacker to introduce vulnerabilities into your PHP executables. + +Further, any apps that use custom binaries will not be eligible for support via GitHub Issues. diff --git a/resources/views/docs/desktop/2/digging-deeper/queues.md b/resources/views/docs/desktop/2/digging-deeper/queues.md new file mode 100644 index 00000000..f6014217 --- /dev/null +++ b/resources/views/docs/desktop/2/digging-deeper/queues.md @@ -0,0 +1,95 @@ +--- +title: Queues +order: 500 +--- + +# Queues + +Queueing tasks to be run in the background is a critical part of creating a great user experience. + +NativePHP has built-in support for Laravel's [Queues](https://laravel.com/docs/queues). + +## Queueing a job + +If you're familiar with queueing jobs in Laravel, you should feel right at home. There's nothing special you need to do. + +Jobs live in the SQLite [database](/docs/digging-deeper/databases) that your app uses by default and the `jobs` table +migration will have been created and migrated for you. + +## Processing Jobs / Working the Queue + +By default, NativePHP will boot up a single queue worker which will consume jobs from the `default` queue. + +If you wish to modify the configuration of this worker or run more workers, see [Configuring workers](#configuring-workers). + +### Configuring workers + +Once you publish the NativePHP config file using `php artisan vendor:publish`, you will find a `queue_workers` key in +`config/nativephp.php`. Here are some acceptable values to get you started: + +```php +'queue_workers' => [ + 'one' => [], + 'two' => [], + 'three' => [ + 'queues' => ['high'], + 'memory_limit' => 1024, + 'timeout' => 600, + 'sleep' => 3, + ], + 'four' => [ + 'queues' => ['high'], + ], + 'five' => [ + 'memory_limit' => 1024, + ], +], +``` + +Each item in the array will be spun up as a persistent [Child Process](/docs/digging-deeper/child-processes), with the key +name you provide being used as both the process's and the worker's alias. + +You may configure which queues a worker is able to process jobs from, its memory limit and its timeout. + +If you do not provide values for any of these settings, the following sensible defaults will be used: + +```php +'queues' => ['default'], +'memory_limit' => 128, +'timeout' => 60, +'sleep' => 3, +``` + +The `sleep` parameter defines the number of seconds the worker will wait (sleep) when there are no new jobs available. A lower value means the worker polls for new jobs more frequently, which might be more responsive but uses more CPU. A higher value reduces CPU usage but may introduce a slight delay in processing newly added jobs. + +### Managing workers + +The handy `QueueWorker::up()` and `QueueWorker::down()` methods available on `Facades\QueueWorker` can be used to start +and stop workers, should you need to. + +```php +use Native\DTOs\QueueConfig; +use Native\Desktop\Facades\QueueWorker; + +$queueConfig = new QueueConfig(alias: 'manual', queuesToConsume: ['default'], memoryLimit: 1024, timeout: 600, sleep: 5); + +QueueWorker::up($queueConfig); + +// Alternatively, if you already have the worker config in your config/nativephp.php file, you may simply use its alias: +QueueWorker::up(config: 'manual'); + +// Later... +QueueWorker::down(alias: 'manual'); +``` + +## When to Queue + +Given that your database and application typically exist on the same machine (i.e. there's no network involved), +queueing background tasks can mostly be left for very intense operations and when making API calls over the network. + +Even so, you may find it more user-friendly to have slow tasks complete in the main application thread. You may simply +choose to have your UI indicate that something is occurring (e.g. with a loading spinner) while the user waits for the +process to finish. + +This may be clearer for the user and easier to handle in case issues arise, as you can provide visual feedback to the +user and they can try again more easily. diff --git a/resources/views/docs/desktop/2/digging-deeper/security.md b/resources/views/docs/desktop/2/digging-deeper/security.md new file mode 100644 index 00000000..8a3780e7 --- /dev/null +++ b/resources/views/docs/desktop/2/digging-deeper/security.md @@ -0,0 +1,105 @@ +--- +title: Security +order: 400 +--- + +# Security + +When building desktop applications it's essential to take your application's security to the next level, both to +protect your application and infrastructure, but also to protect your users, their system and their data. This is a +complex and wide-reaching topic. Please take time to thoroughly understand everything discussed in this chapter. + +Remember that we can't cover everything here either, so please use good judgement when implementing features of your +application that allows users to manipulate data on their filesystem or other sources. + +## Protecting your application and infrastructure + +A major consideration for NativePHP is how it can protect _your_ application. + +### Secrets and .env + +As your application is being installed on systems outside of your/your organisation's control, it is important to think +of the environment that it's in as _potentially_ hostile, which is to say that any secrets, passwords or keys +could fall into the hands of someone who might try to abuse them. + +This means you should, where possible, use unique keys for each installation, preferring to generate these at first-run +or on every run rather than sharing the same key for every user across many installations. + +Especially if your application is communicating with any private APIs over the network, we highly recommend that your +application and any API use a robust and secure authentication protocol, such as OAuth2, that enables you to create and +distribute unique and expiring tokens (an expiration date less than 48 hours in the future is recommended) with a high +level of entropy, as this makes them hard to guess and hard to abuse. + +Always use HTTPS. + +If your application allows users to connect _their own_ API keys for a service, you should treat these keys with great +care. If you choose to store them anywhere (either in a [File](/docs/digging-deeper/files) or +[Database](/docs/digging-deeper/databases)), make sure you store them +[encrypted](/docs/the-basics/system#encryption-decryption) and decrypt them only when needed. + +See [Environment Files](/docs/getting-started/env-files#removing-sensitive-data-from-your-environment-files) for details +on how to redact your `.env` files at build-time. + +### Files and privileges + +Your application runs in a privileged state thanks to the PHP runtime being executed as the user who is currently +operating the system. This is convenient, but it also comes with risks. Your application has access to everything that +the user is authorized to access on the system. + +You should limit where you are reading and writing files to the locations your user expects. These are the `appdata` +folder for the combination of your application and this user and the user's `home` directory (and the other user +subdirectories). + +All of these can be done simply by using the provided Storage filesystems detailed in +[Files](/docs/digging-deeper/files). + +### The web servers + +NativePHP works by spinning up web servers on each side of the runtime environment: one on the PHP side to execute your +application and another on the runtime side, to interact with the runtime's native environment hooks for the operating +system. It then bridges the gap between the two by making _authenticated_ HTTP calls between the two using a pre-shared, +dynamic key that is regenerated every time your application starts. + +This goes some way to preventing third-party software from snooping/sniffing the connection and 'tinkering' with either +your application or the runtime environment. It also ensures that the runtime APIs built _for your application_ will +respond **only** to your application. + +**You MUST NOT bypass this security measure!** If you do, your application will be open to attack from very basic HTTP +calls. It is trivial for any installed application to make such calls, or even for your user to be coerced into making +them via a web browser (e.g. from a phishing attack). + +By default, Laravel's built-in CSRF and CORS protections will go some way to preventing many of these kinds of attacks +but you should do all you can to prevent unwanted attack vectors from being made available. + +#### Prevent regular browser access + +When you are running a build, the global `PreventRegularBrowserAccess` middleware will be applied to all your routes automatically. + +This ensures that only requests coming from the web view shell that booted your application can make requests into your +application. + +## Protecting your users and their data + +Equally important is how your app protects users. NativePHP is a complex combination of powerful software and so there +are a number of risks associated with its use. + +### When sending data over the network + +**Always use HTTPS to communicate with web services.** This ensures that any data sent between your user's device and +the service is encrypted in transit. + +### The PHP executable + +Currently, the bundled PHP executable can be used by any user or application that knows where to find it and has +privileges to execute binaries in that location. + +This is a potential attack vector that your users ought to be aware of when they are installing other applications. If +a user installs an application that they don't trust, it may attempt to use the PHP binary bundled with your application +to execute arbitrary code on your user's device. This is known as a Remote Code Execution attack (or RCE). + +While this may not directly affect your application (unless it's the target of such an attack), you can still help users +to secure their device by reminding them of their responsibility to only install trusted software from reputable +vendors. + +There's very little that can be done to mitigate this kind of attack in practice, just the same as any application you +install now on your device could use any other application installed. diff --git a/resources/views/docs/desktop/2/getting-started/_index.md b/resources/views/docs/desktop/2/getting-started/_index.md new file mode 100644 index 00000000..7f67f577 --- /dev/null +++ b/resources/views/docs/desktop/2/getting-started/_index.md @@ -0,0 +1,4 @@ +--- +title: Getting Started +order: 1 +--- diff --git a/resources/views/docs/desktop/2/getting-started/configuration.md b/resources/views/docs/desktop/2/getting-started/configuration.md new file mode 100644 index 00000000..625940bb --- /dev/null +++ b/resources/views/docs/desktop/2/getting-started/configuration.md @@ -0,0 +1,180 @@ +--- +title: Configuration +order: 200 +--- + +The `native:install` command publishes a configuration file to `config/nativephp.php`. +This file contains all the configuration options for NativePHP. + +## Default Configuration File + +```php +return [ + /** + * The version of your app. + * It is used to determine if the app needs to be updated. + * Increment this value every time you release a new version of your app. + */ + 'version' => env('NATIVEPHP_APP_VERSION', '1.0.0'), + + /** + * The ID of your application. This should be a unique identifier + * usually in the form of a reverse domain name. + * For example: com.nativephp.app + */ + 'app_id' => env('NATIVEPHP_APP_ID'), + + /** + * If your application allows deep linking, you can specify the scheme + * to use here. This is the scheme that will be used to open your + * application from within other applications. + * For example: "nativephp" + * + * This would allow you to open your application using a URL like: + * nativephp://some/path + */ + 'deeplink_scheme' => env('NATIVEPHP_DEEPLINK_SCHEME'), + + /** + * The author of your application. + */ + 'author' => env('NATIVEPHP_APP_AUTHOR'), + + /** + * The copyright notice for your application. + */ + 'copyright' => env('NATIVEPHP_APP_COPYRIGHT'), + + /** + * The description of your application. + */ + 'description' => env('NATIVEPHP_APP_DESCRIPTION', 'An awesome app built with NativePHP'), + + /** + * The Website of your application. + */ + 'website' => env('NATIVEPHP_APP_WEBSITE', 'https://nativephp.com'), + + /** + * The default service provider for your application. This provider + * takes care of bootstrapping your application and configuring + * any global hotkeys, menus, windows, etc. + */ + 'provider' => \App\Providers\NativeAppServiceProvider::class, + + /** + * A list of environment keys that should be removed from the + * .env file when the application is bundled for production. + * You may use wildcards to match multiple keys. + */ + 'cleanup_env_keys' => [ + 'AWS_*', + 'GITHUB_*', + 'DO_SPACES_*', + '*_SECRET', + 'NATIVEPHP_UPDATER_PATH', + 'NATIVEPHP_APPLE_ID', + 'NATIVEPHP_APPLE_ID_PASS', + 'NATIVEPHP_APPLE_TEAM_ID', + ], + + /** + * A list of files and folders that should be removed from the + * final app before it is bundled for production. + * You may use glob / wildcard patterns here. + */ + 'cleanup_exclude_files' => [ + 'content', + 'storage/app/framework/{sessions,testing,cache}', + 'storage/logs/laravel.log', + ], + + /** + * The NativePHP updater configuration. + */ + 'updater' => [ + /** + * Whether or not the updater is enabled. Please note that the + * updater will only work when your application is bundled + * for production. + */ + 'enabled' => env('NATIVEPHP_UPDATER_ENABLED', true), + + /** + * The updater provider to use. + * Supported: "github", "s3", "spaces" + */ + 'default' => env('NATIVEPHP_UPDATER_PROVIDER', 'spaces'), + + 'providers' => [ + 'github' => [ + 'driver' => 'github', + 'repo' => env('GITHUB_REPO'), + 'owner' => env('GITHUB_OWNER'), + 'token' => env('GITHUB_TOKEN'), + 'vPrefixedTagName' => env('GITHUB_V_PREFIXED_TAG_NAME', true), + 'private' => env('GITHUB_PRIVATE', false), + 'channel' => env('GITHUB_CHANNEL', 'latest'), + 'releaseType' => env('GITHUB_RELEASE_TYPE', 'draft'), + ], + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'endpoint' => env('AWS_ENDPOINT'), + 'path' => env('NATIVEPHP_UPDATER_PATH', null), + ], + + 'spaces' => [ + 'driver' => 'spaces', + 'key' => env('DO_SPACES_KEY_ID'), + 'secret' => env('DO_SPACES_SECRET_ACCESS_KEY'), + 'name' => env('DO_SPACES_NAME'), + 'region' => env('DO_SPACES_REGION'), + 'path' => env('NATIVEPHP_UPDATER_PATH', null), + ], + ], + ], +]; +``` + +## Customize php.ini + +When your NativePHP application starts, you may want to customize php.ini directives that will be used for your application. + +You may configure these directives via the `phpIni()` method on your `NativeAppServiceProvider`. +This method should return an array of php.ini directives to be set. + +```php +namespace App\Providers; + +use Native\Desktop\Facades\Window; +use Native\Desktop\Contracts\ProvidesPhpIni; + +class NativeAppServiceProvider implements ProvidesPhpIni +{ + /** + * Executed once the native application has been booted. + * Use this method to open windows, register global shortcuts, etc. + */ + public function boot(): void + { + Window::open(); + } + + + public function phpIni(): array + { + return [ + 'memory_limit' => '512M', + 'display_errors' => '1', + 'error_reporting' => 'E_ALL', + 'max_execution_time' => '0', + 'max_input_time' => '0', + ]; + } +} +``` diff --git a/resources/views/docs/desktop/2/getting-started/debugging.md b/resources/views/docs/desktop/2/getting-started/debugging.md new file mode 100644 index 00000000..ca5e88d5 --- /dev/null +++ b/resources/views/docs/desktop/2/getting-started/debugging.md @@ -0,0 +1,142 @@ +--- +title: Debugging +order: 350 +--- + +## When things go wrong + +Building native applications is a complex task with many moving parts. There will be errors, crashes and lots of +head-scratching. + +NativePHP works to hide much of the complexity, but sometimes you will need go under the hood to find out what's really +going on. + +**Remember that NativePHP is a relatively thin layer above a whole ocean of dependencies and tools that are built and +maintained by many developers outside the NativePHP team.** + +This means that while some issues can be solved within NativePHP it's also very likely that the problem lies elsewhere. + +### The layers + +- Your application, built on Laravel, using your local installations of PHP & Node. +- NativePHP's development tools (`native:run` and `native:build`) manage the Electron build processes - this is + what creates your Application Bundle. +- NativePHP moves the appropriate version of a statically-compiled binary of PHP into your application's bundle - when + your app boots, it's _this_ version of PHP that is being used to execute your PHP code, not your system's version of + PHP. +- Electron uses suites of platform-specific (mostly Javascript based) build tools and dependencies. Much of this will be hidden away in your `vendor` directory. +- The operating system (OS) and its architecture (arch) - you can't build an application for one architecture and + distribute it to a different OS/arch. It won't work. You must build your application to match the OS+arch combination + where you want it to run. + +While you are not expected to know in-depth how all of these layers work and fit together, some familiarity with what's +going on will help you find the root cause of issues and be able to raise meaningful tickets with the right people. + +## Doing some digging + +Here are some tips for debugging: + +### Two or three copies +Remember that when a build is generated (dev or prod), your whole Laravel application is _copied into_ the build folder. + +The dev build copy is stored in `vendor` (holy inception, Batman!). + +Prod builds get packed in the `dist` folder. + +This means there are at least 2 versions of your code that could be running depending on what you're doing: the hot code +that you edit in your IDE (your 'development environment') and the bundle code that actually gets executed when your app +runs in either a dev build or a prod build. + +Having a clear understanding about what context you're in when issues occur will help you to solve the problem faster. + +### Verbose output +Use `-v`, `-vv` or `-vvv` when running `native:run` or `native:build` as this will provide more detail as to what's +happening at each stage of a process. + +### Check the logs +Logs generated by running builds of your application are stored in `{appdata}/storage/logs/`. + +Logs generated when running Artisan commands in your development environment are in `storage/logs/` (default Laravel). + +### Step out +Try running the step that's failing _outside_ of the runtime environment. Reduce the layers of abstraction to +identify or rule out environment-specific complications. + +### Start from scratch + +#### `dist` +Don't be afraid to delete builds and start again! The `dist` folder in your application's root may sometimes get into +an unusual state and just needs wiping out. + +#### AppData +The appdata directory is where your application's database, logs, and other application-specific items are stored. +This is a reliable place to store data and files that your application needs to function _outside_ of the app +bundle and without cluttering your user's home directory or other personal folders. + +When testing prod builds, the appdata directory will be created on your machine, allowing you to fully mimic an end-user +experience. + +In some cases, you may need to wipe this folder and then re-run your app. + +| Platform | Location | +|----------|--------------------------------| +| macOS | ~/Library/Application Support | +| Linux | $XDG_CONFIG_HOME or ~/.config | +| Windows | %APPDATA% | + +#### Database +Try [completely refreshing](/docs/digging-deeper/databases#refreshing-your-app-database) your app's prod database: + +```shell +php artisan native:migrate:fresh +``` + +#### Processes +Make sure there are no lingering processes. Check your Activity Monitor/Task Manager to find stray processes from your +app that may hang around after a build has failed, and force them to quit. + +### Check your app and PHP +Errors that occur in PHP execution during the application's boot-up sequence can cause the app to crash before it even +starts. + +A 500 error in your application code, for example, may prevent the main window from showing, but would leave the runtime's +shell process running. + +Try booting your application in a standard browser to see if there are any errors when hitting its entrypoint URL. If +you're using Laravel Herd, for example, move your app development environment into your Herd root folder and go to +`http://{your-app-folder-name}.test/` in your favorite browser. + +Also make sure that the PHP version in the bundle is the same as the one you have installed on your machine, i.e. +if you're running PHP8.2 on your machine, the PHP binary that is moved into the `dist` folder should be PHP8.2 for your +current OS+arch. + +Checking this will also prove that the executable itself is stable: + +#### For dev builds: +macOS & Linux: +```shell +/path/to/your/app/vendor/nativephp/electron/resources/js/resources/php/php -v +``` +Windows: +``` +C:\path\to\your\app\vendor\nativephp\electron\resources\js\resources\php\php.exe -v +``` + +#### For production builds: +macOS: +```shell +/path/to/your/app/dist/{os+arch}/AppName/Contents/Resources/app.asar.unpacked/resources/php/php -v +``` + +Windows: +``` +C:\path\to\your\app\dist\win-unpacked\resources\app.asar.unpacked\resources\php\php.exe -v +``` + +## Still stuck? +If you've found a bug, please [open an issue](https://github.com/nativephp/desktop/issues/new) on GitHub. + +There's also [Discussions](https://github.com/orgs/NativePHP/discussions) and +[Discord]({{ $discordLink }}) for live chat. + +Come join us! We want you to succeed. diff --git a/resources/views/docs/desktop/2/getting-started/development.md b/resources/views/docs/desktop/2/getting-started/development.md new file mode 100644 index 00000000..06047e1c --- /dev/null +++ b/resources/views/docs/desktop/2/getting-started/development.md @@ -0,0 +1,113 @@ +--- +title: Development +order: 300 +--- + +## Development + +```shell +php artisan native:run +``` + +NativePHP isn't prescriptive about how you develop your application. You can build it in the way you're most comfortable +and familiar with, just as if you were building a traditional web application. + +The only difference comes in the feedback cycle. Instead of switching to and refreshing your browser, you'll need to +be serving your application using `php artisan native:run` and refreshing (and in some cases restarting) your +application to see changes. + +This is known as 'running a dev build'. + +### What does the `native:run` command do? + +The `native:run` command runs the Electron 'debug build' commands, which build your application with various +debug options set to help make debugging easier, such as allowing you to show the Dev Tools in the embedded web view. + +It also keeps the connection to the terminal open so you can see and inspect useful output from your app, such as logs, +in real time. + +These builds are unsigned and not meant for distribution. They do not go through various optimizations typically done +when [building your application for production](/docs/publishing) and so they expose more about the inner workings of +the code than you would typically want to share with your users. + +A major part of the build process, even for debug builds, involves _copying_ your application code into the runtime's +build environment. This means that changes you make to your application code _will not_ be reflected in your running +application until you restart it. + +You can stop the `native:run` command by pressing `Ctrl-C` on your keyboard in the terminal window it's running in. +It will also terminate when you quit your application. + +## Hot Reloading + +Hot reloading is an awesome feature for automatically seeing changes to your application during development. NativePHP +supports hot reloading of certain files within its core and your application, but it does _not_ watch all of your +source code for changes. It is left to you to determine how you want to approach this. + +If you're using Vite, hot reloading will just work inside your app as long as you've booted your Vite dev server and +[included the Vite script tag](https://laravel.com/docs/vite#loading-your-scripts-and-styles) in your views +(ideally in your app's main layout file). + +You can do this easily in Blade using the `@@vite` directive. + +Then, in a separate terminal session to your `php artisan native:run`, from the root folder of your application, run: + +```shell +npm run dev +``` + +Now changes you make to files in your source code will cause a hot reload in your running application. + +Which files trigger reloads will depend on your Vite configuration. + +### `composer native:dev` + +You may find the `native:dev` script convenient. By default, it is setup to run both `native:run` and `npm run dev` +concurrently in a single command: + +```shell +composer native:dev +``` + +You may modify this script to suit your needs. Simply edit the command in your `composer.json` scripts section. + +## First run + +When your application runs for the first time, a number of things occur. + +NativePHP will: + +1. Create the `appdata` folder - where this is created depends which platform you're developing on. In development, it + is named according to your `APP_NAME`. +2. Create a `nativephp.sqlite` SQLite database in your `database` folder. +3. Migrate this database. + +The `appdata` structure is identical to that created by _production_ builds of your app, but when running in +development, the database created there is _not_ migrated. + +**If you change your `APP_NAME`, a new `appdata` folder will be created. No previous files will be deleted.** + +## Subsequent runs + +In development, your application will not run migrations of the `nativephp.sqlite` database for you. You must do this +manually: + +```shell +php artisan native:migrate +``` + +For more details, see the [Databases](/docs/digging-deeper/databases) section. + +## App Icon + +The `native:run` and `native:build` commands look for the following icon files when building your application: + +- `public/icon.png` - your main icon, used on the Desktop, Dock and app switcher. +- `public/icon.ico` - if it exists, it is used as an icon file for Windows (optional). +- `public/icon.icns` - if it exists, it is used as an icon file for macOS (optional). +- `public/IconTemplate.png` - used in the Menu Bar on non-retina displays. +- `public/IconTemplate@2x.png` - used in the Menu Bar on retina displays. + +If any of these files exist, they will be moved into the relevant location to be used as your application's icons. +You simply need to follow the naming convention. + +Your main icon should be at least 512x512 pixels. diff --git a/resources/views/docs/desktop/2/getting-started/env-files.md b/resources/views/docs/desktop/2/getting-started/env-files.md new file mode 100644 index 00000000..28c889fd --- /dev/null +++ b/resources/views/docs/desktop/2/getting-started/env-files.md @@ -0,0 +1,48 @@ +--- +title: Environment Files +order: 400 +--- + +## Environment Files + +When NativePHP bundles your application, it will copy your entire application directory into the bundle, including your +`.env` file. + +**This means that your `.env` file will be accessible to anyone who has access to your application bundle.** + +So you should be careful to not include any sensitive information in your `.env` file, such as API keys or passwords. +This is quite unlike a traditional web application deployed to a server you control. + +If you need to perform any sensitive operations, such as accessing an API or database, you should do so using a +separate API that you create specifically for your application. You can then call _this_ API from your application and +have it perform the sensitive operations on your behalf. + +See [Security](/docs/digging-deeper/security) for more tips. + +## Removing sensitive data from your environment files + +There are certain environment variables that NativePHP uses internally, for example to configure your application's +updater, or Apple's notarization service. + +These environment variables are automatically removed from your `.env` file when your application is bundled, so you +don't need to worry about them being exposed. + +If you want to remove other environment variables from your `.env` file, you can do so by adding them to the +`cleanup_env_keys` configuration option in your `nativephp.php` config file: + +```php + /** + * A list of environment keys that should be removed from the + * .env file when the application is bundled for production. + * You may use wildcards to match multiple keys. + */ + 'cleanup_env_keys' => [ + 'AWS_*', + 'DO_SPACES_*', + '*_SECRET', + 'NATIVEPHP_UPDATER_PATH', + 'NATIVEPHP_APPLE_ID', + 'NATIVEPHP_APPLE_ID_PASS', + 'NATIVEPHP_APPLE_TEAM_ID', + ], +``` diff --git a/resources/views/docs/desktop/2/getting-started/installation.md b/resources/views/docs/desktop/2/getting-started/installation.md new file mode 100644 index 00000000..ebd09c86 --- /dev/null +++ b/resources/views/docs/desktop/2/getting-started/installation.md @@ -0,0 +1,72 @@ +--- +title: Installation +order: 100 +--- + +## Requirements + +1. PHP 8.3+ +2. Laravel 11 or higher +3. Node 22+ +4. Windows 10+ / macOS 12+ / Linux + +### PHP & Node + +The best development experience for NativePHP is to have PHP and Node running on your development machine directly. + +If you're using Mac or Windows, the most painless way to get PHP and Node up and running on your system is with Laravel Herd. It's fast and free! + +Please note that, while it's possible to develop and run your application from a virtualized environment or container, +you may encounter more unexpected issues and have more manual steps to create working builds. + +### Laravel + +NativePHP is built to work with Laravel. You can install it into an existing Laravel application, or start a new one. + +## Install NativePHP + +```shell +composer require nativephp/desktop +``` + +This package contains all the classes, commands, and interfaces that your application will need to work with the +Electron runtime. + +## Run the NativePHP installer + +```shell +php artisan native:install +``` + +The NativePHP installer takes care of publishing the NativePHP service provider, which bootstraps the necessary +dependencies for your application to work with the Electron runtime. + +It also publishes the NativePHP configuration file to `config/nativephp.php`. + +It adds the `native:dev` script to your `composer.json`, which you are free to modify to suit your needs. + +Then it registers the `php artisan native:install` command as a `post-update-cmd` so your environment is always up to date after a `composer update`. + +Finally, it installs any other dependencies needed to run Electron. + +**Whenever you set up NativePHP on a new machine or in CI, you should run the installer to make sure all the +necessary dependencies are in place to build your application.** + +### Publishing the Electron project + +If you need to make any specific adjustments to the underlying Electron app, you can publish it using `php artisan native:install --publish`. This will export the Electron project to `{project-root}/nativephp/electron` and allow you to fully control all of NativePHP's inner workings. + +Additionally, this will modify your `post-update-cmd` script to keep your project up to date, but note that you may need to cherry-pick any adjustments you've made after a `composer update`. + +## Start the development server + +**Heads up!** Before starting your app in a native context, try running it in the browser. You may bump into exceptions +which need addressing before you can run your app natively, and may be trickier to spot when doing so. + +Once you're ready: + +```shell +php artisan native:run +``` + +And that's it! You should now see your Laravel application running in a native desktop window. 🎉 diff --git a/resources/views/docs/desktop/2/getting-started/introduction.md b/resources/views/docs/desktop/2/getting-started/introduction.md new file mode 100644 index 00000000..293f562f --- /dev/null +++ b/resources/views/docs/desktop/2/getting-started/introduction.md @@ -0,0 +1,94 @@ +--- +title: Introduction +order: 001 +--- + +## Hello, NativePHP! + +NativePHP is a new framework for rapidly building rich, native desktop applications using PHP. If you're already a PHP +developer, you'll feel right at home. If you're new to PHP, we think you'll find NativePHP easy to pick up and use. +Whatever your path, we think you're going to be productive quickly. + +**NativePHP makes distributing PHP apps to users on any platform a cinch.** + +NativePHP is taking the world by storm, enabling PHP developers to create true cross-platform, native apps +using the tools and technologies they already know: HTML, CSS, Javascript, and, of course, PHP. + +## Why PHP? + +**PHP is great.** It's a mature language that has been honed for 30 years in one of the most ruthless environments: +the web. + +Despite the odds, it remains one of the most used languages worldwide and continues to grow in usage every day. + +Its shared-nothing approach to memory safety makes it an unexpectedly productive candidate for building native +applications. + +Its focus on HTTP as a paradigm for building applications lends itself towards using the incredibly +accessible web technologies to build rich UIs that can feel at home on any platform. + +## What exactly is NativePHP? + +Strictly speaking, NativePHP is a combination of elements: + +1. A collection of easy-to-use classes to enable you to interact with a variety of host operating system features. +2. A set of tools to enable building and bundling your native application. +3. A static PHP runtime that allows your app to run on any user's device with zero effort on their part. + +## What NativePHP isn't + +NativePHP is **not a completely new framework that you need to learn**. It builds on top of the incredible affordances +and ecosystem that Laravel provides. Before using NativePHP, you'll want to be familiar with building web applications +using Laravel. + +NativePHP is **not a GUI framework**. We don't want to tell you how to build your app. You can choose whatever UI toolset +makes you and your team feel most productive. Building a React front-end? No problem. Vue? Sure. Livewire or Inertia? +Doesn't matter! Plain old HTML and CSS? You got it. Tailwind, Bootstrap, Material UI: whatever you want. + +NativePHP is **not some new, custom fork of PHP**. This is the good new PHP you know and love. + +It's also not an extension that you need to figure out and install into PHP. You're just a `composer require` away from +awesome. + +## What's in the box? + +NativePHP comes with a bunch of useful features out of the box, including: + +- Window management +- Menu management +- File management +- Database support (SQLite) +- Native notifications + +All of this and more is explored in the rest of these docs. + +## What can I build with NativePHP? + +Honestly, anything you want. We believe NativePHP is going to empower thousands of developers to build all kinds of +applications. The only limit is your imagination. + +You could build a menubar app that lets you manage your cron jobs, or a cool new launcher app, or a screen recorder +that puts cowboy hats on every smiley-face emoji it sees. + +(You should totally build that last one.) + +Need some inspiration? [Check out our repository of awesome projects](https://github.com/NativePHP/awesome-nativephp) created by people like you! + +## What's next? + +Go read the docs! We've tried to make them as comprehensive as possible, but if you find something missing, please +feel free to [contribute](https://github.com/nativephp/nativephp.com). + +This site and all the NativePHP for Desktop repositories are open source and available on [GitHub](https://github.com/nativephp). + +Ready to jump in? [Let's get started](installation). + +## Credits + +NativePHP wouldn't be possible without the following projects and the hard work of all of their wonderful contributors: + +- [PHP](https://php.net) +- [Electron](https://electronjs.org) +- [Laravel](https://laravel.com) +- [Symfony](https://symfony.com) +- [Static PHP CLI](https://github.com/crazywhalecc/static-php-cli/) diff --git a/resources/views/docs/desktop/2/getting-started/releasenotes.md b/resources/views/docs/desktop/2/getting-started/releasenotes.md new file mode 100644 index 00000000..f3df1365 --- /dev/null +++ b/resources/views/docs/desktop/2/getting-started/releasenotes.md @@ -0,0 +1,53 @@ +--- +title: Release Notes +order: 1100 +--- + +## NativePHP/desktop +@forelse (\App\Support\GitHub::desktop()->releases()->take(10) as $release) +### {{ $release->name }} +**Released: {{ \Carbon\Carbon::parse($release->published_at)->format('F j, Y') }}** + +{{ $release->getBodyForMarkdown() }} +--- +@empty +## We couldn't show you the latest release notes at this time. +Not to worry, you can head over to GitHub to see the [latest release notes](https://github.com/NativePHP/electron/releases). +@endforelse + +## NativePHP/php-bin +@forelse (\App\Support\GitHub::phpBin()->releases()->take(10) as $release) +### {{ $release->name }} +**Released: {{ \Carbon\Carbon::parse($release->published_at)->format('F j, Y') }}** + +{{ $release->getBodyForMarkdown() }} +--- +@empty +## We couldn't show you the latest release notes at this time. +Not to worry, you can head over to GitHub to see the [latest release notes](https://github.com/NativePHP/electron/releases). +@endforelse + +## NativePHP/electron (v1) +@forelse (\App\Support\GitHub::electron()->releases()->take(10) as $release) +### {{ $release->name }} +**Released: {{ \Carbon\Carbon::parse($release->published_at)->format('F j, Y') }}** + +{{ $release->getBodyForMarkdown() }} +--- +@empty +## We couldn't show you the latest release notes at this time. +Not to worry, you can head over to GitHub to see the [latest release notes](https://github.com/NativePHP/electron/releases). +@endforelse + +## NativePHP/laravel (v1) +@forelse (\App\Support\GitHub::laravel()->releases()->take(10) as $release) +### {{ $release->name }} +**Released: {{ \Carbon\Carbon::parse($release->published_at)->format('F j, Y') }}** + +{{ $release->getBodyForMarkdown() }} +--- +@empty +## We couldn't show you the latest release notes at this time. +Not to worry, you can head over to GitHub to see the [latest release notes](https://github.com/NativePHP/electron/releases). +@endforelse + diff --git a/resources/views/docs/desktop/2/getting-started/status.md b/resources/views/docs/desktop/2/getting-started/status.md new file mode 100644 index 00000000..574b1e45 --- /dev/null +++ b/resources/views/docs/desktop/2/getting-started/status.md @@ -0,0 +1,28 @@ +--- +title: Roadmap +order: 099 +--- + +## Roadmap + +We're currently focused on reaching the next minor release. + +## Current Status + +NativePHP for Desktop is **production-ready**. So you should feel completely ready to build and distribute apps +with NativePHP. + +But we always need your help! If you spot any bugs or feel that there are any features missing, be sure to share +your ideas and questions through the [forum](https://github.com/orgs/nativephp/discussions), by +[raising issues](https://github.com/nativephp/desktop/issues/new/choose)/reporting bugs, and discussing on +[Discord]({{ $discordLink }}). + + diff --git a/resources/views/docs/desktop/2/getting-started/support-policy.md b/resources/views/docs/desktop/2/getting-started/support-policy.md new file mode 100644 index 00000000..a18b032b --- /dev/null +++ b/resources/views/docs/desktop/2/getting-started/support-policy.md @@ -0,0 +1,39 @@ +--- +title: Support Policy +order: 500 +--- + +## Support Policy + +NativePHP for Desktop is an open-source project dedicated to providing robust and reliable releases. + +We are committed to supporting the two latest PHP versions, ensuring that our users benefit from the latest features, security updates, and performance improvements. + +__We do not remove support for a PHP version without a major version release.__ + +Additionally, we support each Laravel version until it reaches its official end of life (EOL), ensuring that your applications can remain up-to-date. + +Our support policy reflects our commitment to maintaining high standards of quality and security, providing our users with the confidence they need to build and deploy their applications using NativePHP for Desktop. + +### PHP Versions +| NativePHP Version | Supported PHP Versions | +|-------------------|------------------------| +| ^2.0 | 8.3, 8.4 | +| ^1.0 | 8.3, 8.4 | + +[PHP: Supported Versions](https://www.php.net/supported-versions.php) + +NativePHP provides methods of bundling your own static PHP binaries. **Support is not provided for these.** + +### Laravel Versions +| NativePHP Version | Supported Laravel Versions | +|-------------------|----------------------------| +| ^2.0 | 11.x, 12.x | +| ^1.0 | 11.x, 12.x | + +[Laravel: Support Policy](https://laravel.com/docs/master/releases#support-policy) + +## Requesting Support +Support can be obtained by opening an issue on the [NativePHP/desktop]({{ $githubLink }}/desktop/issues) repository or by joining the [Discord]({{ $discordLink }}). + +When requesting support, it is requested that you are using a supported version. If you are not, you will be asked to upgrade to a supported version before any support is provided. diff --git a/resources/views/docs/desktop/2/getting-started/upgrade-guide.md b/resources/views/docs/desktop/2/getting-started/upgrade-guide.md new file mode 100644 index 00000000..b76d0bb8 --- /dev/null +++ b/resources/views/docs/desktop/2/getting-started/upgrade-guide.md @@ -0,0 +1,79 @@ +--- +title: Upgrade Guide +order: 1200 +--- + +### High Impact Changes + +- [New package name `nativephp/desktop`](#upgrading-to-20-from-1x) +- [Root namespace updated to `Native\Desktop`](#update-class-imports) +- [Dropped macOS **_Catalina_** and **_Big Sur_** support](#macos-support) + +### Medium Impact Changes + +- [Serve command renamed](#renamed-codenativeservecode-command) +- [Node integration disabled by default](#security-defaults) + +### Low Impact Changes + +- [Modifying the Electron backend](#modifying-the-electron-backend) +- [New build output location](#new-codedistcode-location) + +## Upgrading To 2.0 From 1.x + +NativePHP for Desktop v2 is a significant architecture overhaul and security release. The package has moved to a new repository with a new name: `nativephp/desktop`. +Please replace `nativephp/electron` in your `composer.json` with the new package. + +```json +"require": { + "nativephp/electron": "^1.3", // [tl! remove] + "nativephp/laravel": "^1.3", // [tl! remove] + "nativephp/desktop": "^2.0" // [tl! add] +} +``` + +If you're requiring `nativephp/laravel` as well, please remove that too. + +Then update the package: + +```sh +composer update +php artisan native:install +``` + +After installation, the `native:install` script will be automatically registered as a `post-update-cmd`, so you won't have to manually run it after a composer update. + +## Update class imports + +With the package rename, the root namespace has also changed. Please update all occurrences of `Native\Laravel` to `Native\Desktop`. + +```php +use Native\Laravel\Facades\Window; // [tl! remove] +use Native\Desktop\Facades\Window; // [tl! add] +``` + +## macOS support + +v2 drops support for macOS **_Catalina_** and **_Big Sur_**. This change comes from the Electron v38 upgrade and aligns with Apple's supported OS versions. Most users should be unaffected, but please check your deployment targets before upgrading. + +- Electron Catalina support dropped +- Electron Big Sur support dropped + +## Renamed `native:serve` command + +The `artisan native:serve` command has been deprecated and renamed to `artisan native:run` for better symmetry with the mobile package. +Please update the `composer native:dev` script to reference the new run command. + +## New `dist` location + +The build output has moved to `nativephp/electron/dist` + +## Security defaults + +`nodeIntegration` is now disabled by default. While this improves security, it may affect applications that rely on this functionality. You can easily re-enable it using `Window::webPreferences()` where needed. + +## Modifying the Electron backend + +If you need to make any specific adjustments to the underlying Electron app, you can publish it using `php artisan native:install --publish`. This will export the Electron project to `{project-root}/nativephp/electron` and allow you to fully control all of NativePHP's inner workings. + +Additionally, this will modify your `post-update-cmd` script to keep your project up to date, but note that you may need to cherry-pick any adjustments you've made after a `composer update`. diff --git a/resources/views/docs/desktop/2/publishing/_index.md b/resources/views/docs/desktop/2/publishing/_index.md new file mode 100644 index 00000000..ad0c31b5 --- /dev/null +++ b/resources/views/docs/desktop/2/publishing/_index.md @@ -0,0 +1,4 @@ +--- +title: Publishing Your App +order: 4 +--- diff --git a/resources/views/docs/desktop/2/publishing/building.md b/resources/views/docs/desktop/2/publishing/building.md new file mode 100644 index 00000000..4b5de7a2 --- /dev/null +++ b/resources/views/docs/desktop/2/publishing/building.md @@ -0,0 +1,188 @@ +--- +title: Building +order: 100 +--- + +## Building Your App + +Building your app is the process of compiling your application into a production-ready state. When building, NativePHP +attempts to sign and notarize your application. Once signed, your app is ready to be distributed. + +## Securing + +Before you prepare a distributable build, please make sure you've been through the +[Security guide](/docs/digging-deeper/security). + +## Building + +The build process compiles your app for one platform at a time. It compiles your application along with the +Electron runtime into a single executable. + +Once built, you can distribute your app however you prefer, but NativePHP also provides a [publish command](publishing) +that will automatically upload your build artifacts to your chosen [provider](/docs/publishing/updating) - this allows +your app to provide automatic updates. + +You should build your application for each platform you intend to support and test it on each platform _before_ +publishing to make sure that everything works as expected. + +### Running commands before and after builds +Many applications rely on a tool such as [Vite](https://vitejs.dev/) or [Webpack](https://webpack.js.org/) to compile their CSS and JS assets before a production build. + +To facilitate this, NativePHP provides two hooks that you can use to run commands before and after the build process. + +To utilise these hooks, add the following to your `config/nativephp.php` file: + +```php +'prebuild' => [ + 'npm run build', // Run a command before the build + 'php artisan optimize', // Run another command before the build +], +'postbuild' => [ + 'npm run release', // Run a command after the build +], +``` + +These commands will be run in the root of your project directory and you can specify as many as required. + +## Versioning + +For every build you create, you should change the version of your application in your app's `config/nativephp.php` file. + +This can be any format you choose, but you may find that a simple incrementing build number is the easiest to manage. + +**Migrations will only run on the user's machine if the version reference is _different_ to the currently-installed version.** + +You may choose to have a different version number that uses a different scheme (e.g. SemVer) that you use for user-facing +releases. + +## Running a build + +```shell +php artisan native:build +``` + +This will build for the platform and architecture where you are running the build. + +### Cross-compilation + +You can also specify a platform to build for by passing the `os` argument, so for example you could build for Windows +whilst on a Mac: + +```shell +php artisan native:build win +``` + +Possible options are: `mac`, `win`, `linux`. + +**Cross-compilation is not supported on all platforms.** + +#### Cross-compilation on Linux + +Compiling Windows binaries is possible with [wine](https://www.winehq.org/). +NSIS requires 32-bit wine when building x64 applications. + +```bash +# Example installation of wine for Debian based distributions (Ubuntu) +dpkg --add-architecture i386 +apt-get -y update +apt-get -y install wine32 +``` + +## Code signing + +Both macOS and Windows require your app to be signed before it can be distributed to your users. + +NativePHP makes this as easy for you as it can, but each platform does have slightly different requirements. + +### Windows + +NativePHP supports two methods for Windows code signing: traditional certificate-based signing and Azure Trusted Signing. + +#### Azure Trusted Signing (Recommended) + +Azure Trusted Signing is a cloud-based code signing service that eliminates the need to manage local certificates. + +When building your application, you can identify which signing method is being used: +- **Azure Trusted Signing**: The build output will show "Signing with Azure Trusted Signing (beta)" +- **Traditional Certificate**: The build output will show "Signing with signtool.exe" + +To use Azure Trusted Signing, add the following environment variables to your `.env` file: + +```dotenv +# Azure AD authentication +AZURE_TENANT_ID=your-tenant-id +AZURE_CLIENT_ID=your-client-id +AZURE_CLIENT_SECRET=your-client-secret + +# Azure Trusted Signing configuration +# This is the CommonName (CN) value - your full name or company name +# as entered in the Identity Validation Request form +NATIVEPHP_AZURE_PUBLISHER_NAME=your-publisher-name + +# The endpoint URL for the Azure region where your certificate is stored +NATIVEPHP_AZURE_ENDPOINT=https://eus.codesigning.azure.net/ + +# The name of your certificate profile (NOT the Trusted Signing Account) +NATIVEPHP_AZURE_CERTIFICATE_PROFILE_NAME=your-certificate-profile + +# Your Trusted Signing Account name (NOT the app registration display name) +# This is the account name shown in Azure Trusted Signing, not your login name +NATIVEPHP_AZURE_CODE_SIGNING_ACCOUNT_NAME=your-code-signing-account +``` + +These credentials will be automatically stripped from your built application for security. + +#### Traditional Certificate Signing + +For traditional certificate-based signing, [see the Electron documentation](https://www.electronforge.io/guides/code-signing/code-signing-windows) for more details. + +### macOS + +[See the Electron documentation](https://www.electronforge.io/guides/code-signing/code-signing-macos) for more details. + +To prepare for signing and notarizing, please provide the following environment variables when running +`php artisan native:build`: + +```dotenv +NATIVEPHP_APPLE_ID=developer@abcwidgets.com +NATIVEPHP_APPLE_ID_PASS=app-specific-password +NATIVEPHP_APPLE_TEAM_ID=8XCUU22SN2 +``` + +These can be added to your `.env` file as they will be stripped out when your app is built. + +Without proper notarization your app will only run on the development machine. Other Macs will show a "app is damaged and can't be opened" warning. +This is a security feature in macOS that prevents running unsigned or improperly notarized applications. Make sure to complete the notarization process to avoid this issue. + +## First run + +When your application runs for the first time, a number of things occur. + +NativePHP will: + +1. Create the `appdata` folder - where this is created depends which platform you're developing on. It is named + according to your `nativephp.app_id` [config](/docs/getting-started/configuration) value (which is based on the + `NATIVEPHP_APP_ID` env variable). +2. Creating the `{appdata}/database/database.sqlite` SQLite database - your user's copy of your app's database. +3. Migrate this database. + +If you wish to seed the user's database, you should run this somewhere that runs +[every time your app boots](/docs/the-basics/app-lifecycle#codeApplicationBootedcode). + +Check if the database was already seeded and, if not, run the appropriate `db:seed` command. For example: + +```php +use App\Models\Config; +use Illuminate\Support\Facades\Artisan; + +if (Config::where('seeded', true)->count() === 1) { + Artisan::call('db:seed'); +} +``` + +## Subsequent runs + +Each time a user opens your app, NativePHP will check to see if the [app version](#versioning) has changed and attempt +to migrate the user's copy of your database in their `appdata` folder. + +This is why you should change the version identifier for each release. diff --git a/resources/views/docs/desktop/2/publishing/publishing.md b/resources/views/docs/desktop/2/publishing/publishing.md new file mode 100644 index 00000000..1a66662f --- /dev/null +++ b/resources/views/docs/desktop/2/publishing/publishing.md @@ -0,0 +1,40 @@ +--- +title: Publishing +order: 200 +--- + +## Publishing Your App + +Publishing your app is similar to building, but in addition NativePHP will upload the build artifacts to your chosen +[updater provider](/docs/publishing/updating) automatically. + +## Running a build + +```shell +php artisan native:publish +``` + +This will build for the platform and architecture where you are running the build. + +**Make sure you've bumped your app version in your .env file before building** + +### Cross-compilation + +You can also specify a platform to build for by passing the `os` argument, so for example you could build for Windows +whilst on a Mac: + +```shell +php artisan native:publish win +``` + +Possible options are: `mac`, `win`, `linux`. + +**Cross-compilation is not supported on all platforms.** + +### GitHub Releases + +If you use the GitHub [updater provider](/docs/publishing/updating), you'll need to create a draft release first. + +Set the "Tag version" to the value of `version` in your application `.env` file, and prefix it with v. "Release title" can be anything you want. + +Whenever you run `native:publish`, your build artifacts will be attached to your draft release. If you decide to rebuild before tagging the release, it will update the artifacts attached to your draft. diff --git a/resources/views/docs/desktop/2/publishing/updating.md b/resources/views/docs/desktop/2/publishing/updating.md new file mode 100644 index 00000000..66795ec3 --- /dev/null +++ b/resources/views/docs/desktop/2/publishing/updating.md @@ -0,0 +1,172 @@ +--- +title: Updating +order: 300 +--- + +## The Updater + +NativePHP ships with a built-in auto-update tool, which allows your users to update your application without needing to +manually download and install new releases. + +This leaves you to focus on building and releasing new versions of your application, without needing to worry about +distributing those updates to your users. + +**macOS: Automatic updating is only supported for [signed](/docs/publishing/building#signing-and-notarizing) +applications.** + +## How it works + +The updater works by checking a remote URL for a new version of your application. If a new version is found, the updater +will download the new version and replace the existing application files with the new ones. + +This means your application's builds need to be hosted online. NativePHP will automatically upload your application for +you. After configuring the updater, simply use the [`php artisan native:publish`](/docs/publishing/publishing) command. + +The updater supports three providers: + +- GitHub Releases (`github`) +- Amazon S3 (`s3`) +- DigitalOcean Spaces (`spaces`) + +You can configure all settings for the updater in your `config/nativephp.php` file or via your `.env` file. + +**The updater will only run when your app is running in production mode.** + +## Configuration + +The default updater configuration looks like this: + +```php + 'updater' => [ + 'enabled' => env('NATIVEPHP_UPDATER_ENABLED', true), + + 'default' => env('NATIVEPHP_UPDATER_PROVIDER', 'spaces'), + + 'providers' => [ + 'github' => [ + 'driver' => 'github', + 'repo' => env('GITHUB_REPO'), + 'owner' => env('GITHUB_OWNER'), + 'token' => env('GITHUB_TOKEN'), + 'vPrefixedTagName' => env('GITHUB_V_PREFIXED_TAG_NAME', true), + 'private' => env('GITHUB_PRIVATE', false), + 'channel' => env('GITHUB_CHANNEL', 'latest'), + 'releaseType' => env('GITHUB_RELEASE_TYPE', 'draft'), + ], + + 's3' => [ + 'driver' => 's3', + 'key' => env('AWS_ACCESS_KEY_ID'), + 'secret' => env('AWS_SECRET_ACCESS_KEY'), + 'region' => env('AWS_DEFAULT_REGION'), + 'bucket' => env('AWS_BUCKET'), + 'endpoint' => env('AWS_ENDPOINT'), + 'path' => env('NATIVEPHP_UPDATER_PATH', null), + ], + + 'spaces' => [ + 'driver' => 'spaces', + 'key' => env('DO_SPACES_KEY_ID'), + 'secret' => env('DO_SPACES_SECRET_ACCESS_KEY'), + 'name' => env('DO_SPACES_NAME'), + 'region' => env('DO_SPACES_REGION'), + 'path' => env('NATIVEPHP_UPDATER_PATH', null), + ], + ], + ], +``` + +How to setup your storage and generate the relevant API credentials: + +- [DigitalOcean](https://docs.digitalocean.com/products/spaces/how-to/manage-access/) +- Amazon S3 - See [this video](https://www.youtube.com/watch?v=FLIp6BLtwjk&ab_channel=CloudCasts) by Chris Fidao or + this [Step 2](https://www.twilio.com/docs/video/tutorials/storing-aws-s3#step-2) of this article by Twilio + + If you got the error message "The bucket does not allow ACLs" you can follow this guide + from [Learn AWS](https://www.learnaws.org/2023/08/26/aws-s3-bucket-does-not-allow-acls) + on how to setup your bucket correctly. + +## Disabling the updater + +If you don't want your application to check for updates, you can disable the updater by setting the +`updater.enabled` option to `false` in your `config/nativephp.php` file or via your `.env` file: + +```dotenv +NATIVEPHP_UPDATER_ENABLED=false +``` + +## Manually checking for updates + +You can manually check for updates by calling the `checkForUpdates` method on the `AutoUpdater` facade: + +```php +use Native\Desktop\Facades\AutoUpdater; + +AutoUpdater::checkForUpdates(); +``` + +**Note:** If an update is available, it will be downloaded automatically. Calling `AutoUpdater::checkForUpdates() twice +will download the update two times. + +## Quit and Install + +You can quit the application and install the update by calling the `quitAndInstall` method on the `AutoUpdater` facade: + +```php +use Native\Desktop\Facades\AutoUpdater; + +AutoUpdater::quitAndInstall(); +``` + +This will quit the application and install the update. The application will then relaunch automatically. + +**Note:** Calling this method is optional — any successfully downloaded update will be applied the next time the +application starts. + +## Events + +### `CheckingForUpdate` + +The `Native\Desktop\Events\AutoUpdater\CheckingForUpdate` event is dispatched when checking for an available update has +started. + +### `UpdateAvailable` + +The `Native\Desktop\Events\AutoUpdater\UpdateAvailable` event is dispatched when there is an available update. The +update is downloaded automatically. + +### `UpdateNotAvailable` + +The `Native\Desktop\Events\AutoUpdater\UpdateNotAvailable` event is dispatched when there is no available update. + +### `DownloadProgress` + +The `Native\Desktop\Events\AutoUpdater\DownloadProgress` event is dispatched when the update is being downloaded. + +The event contains the following properties: + +- `total`: The total size of the update in bytes. +- `delta`: The size of the update that has been downloaded since the last event. +- `transferred`: The total size of the update that has been downloaded. +- `percent`: The percentage of the update that has been downloaded (0-100). +- `bytesPerSecond`: The download speed in bytes per second. + +### `UpdateDownloaded` + +The `Native\Desktop\Events\AutoUpdater\UpdateDownloaded` event is dispatched when the update has been downloaded. + +The event contains the following properties: + +- `version`: The version of the update. +- `downloadedFile`: The local path to the downloaded update file. +- `releaseDate`: The release date of the update in ISO 8601 format. +- `releaseNotes`: The release notes of the update. +- `releaseName`: The name of the update. + +### `Error` + +The `Native\Desktop\Events\AutoUpdater\Error` event is dispatched when there is an error while updating. + +The event contains the following properties: + +- `error`: The error message. diff --git a/resources/views/docs/desktop/2/testing/_index.md b/resources/views/docs/desktop/2/testing/_index.md new file mode 100644 index 00000000..72d59da1 --- /dev/null +++ b/resources/views/docs/desktop/2/testing/_index.md @@ -0,0 +1,4 @@ +--- +title: Testing +order: 5 +--- diff --git a/resources/views/docs/desktop/2/testing/basics.md b/resources/views/docs/desktop/2/testing/basics.md new file mode 100644 index 00000000..2e4b613d --- /dev/null +++ b/resources/views/docs/desktop/2/testing/basics.md @@ -0,0 +1,32 @@ +--- +title: Basics +order: 99 +--- + +# Understanding fake test doubles + +When working with a NativePHP application, you may encounter an elevated level of difficulty when writing tests for your code. +This is because NativePHP relies on an Electron application to be open at all times, listening to HTTP requests. Obviously, +emulating this in a test environment can be cumbersome. You will often hit an HTTP error, and this is normal. This is where +NativePHP's fake test doubles come in. + +```php +use Native\Desktop\Facades\Window; + +#[\PHPUnit\Framework\Attributes\Test] +public function example(): void +{ + Window::fake(); + + $this->get('/whatever-action'); + + Window::assertOpened('window-name'); +} +``` + +## Where have I seen this before? + +If you've ever written tests for a Laravel application, you may have seen the `*::fake()` method available on +all sorts of facades. Under the hood, these methods are swapping the real implementation and behavior – in NativePHP's case, +an HTTP call that forces us to keep the server up and running, in turn degrading the ability to write expressive tests – with a fake one +that follows the same API. This means you do not have to change any of your code to write great tests. diff --git a/resources/views/docs/desktop/2/testing/child-process.md b/resources/views/docs/desktop/2/testing/child-process.md new file mode 100644 index 00000000..9f3741db --- /dev/null +++ b/resources/views/docs/desktop/2/testing/child-process.md @@ -0,0 +1,34 @@ +--- +title: Child Process +order: 100 +--- + +# Fake Child Processes + +## Example test case + +```php +use Native\Desktop\Facades\ChildProcess; + +#[\PHPUnit\Framework\Attributes\Test] +public function example(): void +{ + ChildProcess::fake(); + + $this->get('/whatever-action'); + + ChildProcess::assertGet('background-worker'); + ChildProcess::assertMessage(fn (string $message, string|null $alias) => $message === '{"some-payload":"for-the-worker"}' && $alias === null); +} +``` + +## Available assertions + +- `assertGet` +- `assertStarted` +- `assertPhp` +- `assertArtisan` +- `assertNode` +- `assertStop` +- `assertRestart` +- `assertMessage` diff --git a/resources/views/docs/desktop/2/testing/global-shortcut.md b/resources/views/docs/desktop/2/testing/global-shortcut.md new file mode 100644 index 00000000..63d0c333 --- /dev/null +++ b/resources/views/docs/desktop/2/testing/global-shortcut.md @@ -0,0 +1,31 @@ +--- +title: Global Shortcut +order: 100 +--- + +# Fake Global Shortcuts + +## Example test case + +```php +use Native\Desktop\Facades\GlobalShortcut; + +#[\PHPUnit\Framework\Attributes\Test] +public function example(): void +{ + GlobalShortcut::fake(); + + $this->get('/whatever-action'); + + GlobalShortcut::assertKey('CmdOrCtrl+,'); + GlobalShortcut::assertRegisteredCount(1); + GlobalShortcut::assertEvent(OpenPreferencesEvent::class); +} +``` + +## Available assertions + +- `assertKey` +- `assertRegisteredCount` +- `assertUnregisteredCount` +- `assertEvent` diff --git a/resources/views/docs/desktop/2/testing/power-monitor.md b/resources/views/docs/desktop/2/testing/power-monitor.md new file mode 100644 index 00000000..8597c362 --- /dev/null +++ b/resources/views/docs/desktop/2/testing/power-monitor.md @@ -0,0 +1,30 @@ +--- +title: Power Monitor +order: 100 +--- + +# Fake Power Monitor + +## Example test case + +```php +use Native\Desktop\Facades\PowerMonitor; + +#[\PHPUnit\Framework\Attributes\Test] +public function example(): void +{ + PowerMonitor::fake(); + + $this->get('/whatever-action'); + + PowerMonitor::assertGetSystemIdleState('...'); +} +``` + +## Available assertions + +- `assertGetSystemIdleState` +- `assertGetSystemIdleStateCount` +- `assertGetSystemIdleTimeCount` +- `assertGetCurrentThermalStateCount` +- `assertIsOnBatteryPowerCount` diff --git a/resources/views/docs/desktop/2/testing/queue-worker.md b/resources/views/docs/desktop/2/testing/queue-worker.md new file mode 100644 index 00000000..f9c8c950 --- /dev/null +++ b/resources/views/docs/desktop/2/testing/queue-worker.md @@ -0,0 +1,28 @@ +--- +title: Queue Worker +order: 100 +--- + +# Fake Queue Worker + +## Example test case + +```php +use Native\Desktop\Facades\QueueWorker; +use Native\Desktop\DTOs\QueueConfig; + +#[\PHPUnit\Framework\Attributes\Test] +public function example(): void +{ + QueueWorker::fake(); + + $this->get('/whatever-action'); + + QueueWorker::assertUp(fn (QueueConfig $config) => $config->alias === 'custom'); +} +``` + +## Available assertions + +- `assertUp` +- `assertDown` diff --git a/resources/views/docs/desktop/2/testing/shell.md b/resources/views/docs/desktop/2/testing/shell.md new file mode 100644 index 00000000..fd18d342 --- /dev/null +++ b/resources/views/docs/desktop/2/testing/shell.md @@ -0,0 +1,29 @@ +--- +title: Shell +order: 100 +--- + +# Fake Shell + +## Example test case + +```php +use Native\Desktop\Facades\Shell; + +#[\PHPUnit\Framework\Attributes\Test] +public function example(): void +{ + Shell::fake(); + + $this->get('/whatever-action'); + + Shell::assertOpenedExternal('https://some-url.test'); +} +``` + +## Available assertions + +- `assertShowInFolder` +- `assertOpenedFile` +- `assertTrashedFile` +- `assertOpenedExternal` diff --git a/resources/views/docs/desktop/2/testing/windows.md b/resources/views/docs/desktop/2/testing/windows.md new file mode 100644 index 00000000..12b70b99 --- /dev/null +++ b/resources/views/docs/desktop/2/testing/windows.md @@ -0,0 +1,60 @@ +--- +title: Windows +order: 100 +--- + +# Fake Windows + +## Example test case + +```php +use Native\Desktop\Facades\Window; +use Illuminate\Support\Facades\Http; + + #[\PHPUnit\Framework\Attributes\Test] +public function example(): void +{ + Http::fake(); + Window::fake(); + + $this->get('/whatever-action'); + + Window::assertOpened(fn (string $windowId) => Str::startsWith($windowId, ['window-name'])); + Window::assertClosed('window-name'); + Window::assertHidden('window-name'); +} +``` + +## Available assertions + +- `assertOpened` +- `assertClosed` +- `assertHidden` + +## Asserting against a window instance (advanced) + +```php +use Illuminate\Support\Facades\Http; +use Native\Desktop\Facades\Window; +use Native\Desktop\Windows\Window as WindowImplementation; +use Mockery; + +#[\PHPUnit\Framework\Attributes\Test] +public function example(): void +{ + Http::fake(); + Window::fake(); + Window::alwaysReturnWindows([ + $mockWindow = Mockery::mock(WindowImplementation::class)->makePartial(), + ]); + + $mockWindow->shouldReceive('route')->once()->with('action')->andReturnSelf(); + $mockWindow->shouldReceive('transparent')->once()->andReturnSelf(); + $mockWindow->shouldReceive('height')->once()->with(500)->andReturnSelf(); + $mockWindow->shouldReceive('width')->once()->with(775)->andReturnSelf(); + $mockWindow->shouldReceive('minHeight')->once()->with(500)->andReturnSelf(); + $mockWindow->shouldReceive('minWidth')->once()->with(775)->andReturnSelf(); + + $this->get(route('action')); +} +``` diff --git a/resources/views/docs/desktop/2/the-basics/_index.md b/resources/views/docs/desktop/2/the-basics/_index.md new file mode 100644 index 00000000..e8a6458a --- /dev/null +++ b/resources/views/docs/desktop/2/the-basics/_index.md @@ -0,0 +1,4 @@ +--- +title: The Basics +order: 2 +--- diff --git a/resources/views/docs/desktop/2/the-basics/alerts.md b/resources/views/docs/desktop/2/the-basics/alerts.md new file mode 100644 index 00000000..ed614ebb --- /dev/null +++ b/resources/views/docs/desktop/2/the-basics/alerts.md @@ -0,0 +1,118 @@ +--- +title: Alerts +order: 410 +--- + +## Native Alerts + +NativePHP allows you to show native alerts to the user. They can be used to display messages, ask for confirmation, or +report an error. + +Alerts are created using the `Alert` facade. + +```php +use Native\Desktop\Facades\Alert; +``` + +### Showing Alerts + +To show an alert, you may use the `Alert` class and its `show()` method. + +```php +Alert::new() + ->show('This is a simple alert'); +``` + +## Configuring Alerts + +### Alert Title + +You may set the title of the alert using the `title()` method. + +```php +Alert::new() + ->title('Pizza Order') + ->show('Your pizza has been ordered'); +``` + +### Alert Buttons + +You may configure the buttons of the alert using the `buttons()` method. +This method takes an array of button labels. + +The return value of the `show()` method is the index of the button that the user clicked. +Example: If the user clicks the "Yes" button, the `show()` method will return `0`. If the user clicks the "Maybe" +button, the `show()` method will return `2`. + +If no buttons are defined, the alert will only have an "OK" button. + +```php +Alert::new() + ->buttons(['Yes', 'No', 'Maybe']) + ->show('Do you like pizza?'); +``` + +### Alert Detail + +You may set the detail of the alert using the `detail()` method. +The detail is displayed below the message and provides additional information about the alert. + +```php +Alert::new() + ->detail('Fun facts: Pizza was first made in Naples in 1889') + ->show('Do you like pizza?'); +``` + +### Alert Type + +You may set the type of the alert using the `type()` method. +The type can be one of the following values: `none`, `info`, `warning`, `error`, `question`. On Windows, `question` +displays the same icon as `info`. On macOS, both `warning` and `error` display the same warning icon. + +```php +Alert::new() + ->type('error') + ->show('An error occurred'); +``` + +### Alert Default Button + +You may set the default button of the alert using the `defaultId()` method. +The default button is preselected when the alert appears. + +The default button can be set to the index of the button in the `buttons()` array. + +```php +Alert::new() + ->defaultId(0) + ->buttons(['Yes', 'No', 'Maybe']) + ->show('Do you like pizza?'); +``` + +### Alert Cancel Button + +You may set the cancel button of the alert using the `cancelId()` method. +The cancel button is the button that is selected when the user presses the "Escape" key. + +The cancel button can be set to the index of the button in the `buttons()` array. + +By default, this is assigned to the first button labeled 'Cancel' or 'No'. If no such buttons exist and this option is +not set, the return value will be `0`. + +```php +Alert::new() + ->cancelId(1) + ->buttons(['Yes', 'No', 'Maybe']) + ->show('Do you like pizza?'); +``` + +### Error Alerts + +You may use the `error()` method to display an error alert. + +The `error()` method takes two required parameters: the title of the error alert and the message of the error alert. + +```php +Alert::new() + ->error('An error occurred', 'The pizza oven is broken'); +``` diff --git a/resources/views/docs/desktop/2/the-basics/app-lifecycle.md b/resources/views/docs/desktop/2/the-basics/app-lifecycle.md new file mode 100644 index 00000000..d0e55001 --- /dev/null +++ b/resources/views/docs/desktop/2/the-basics/app-lifecycle.md @@ -0,0 +1,57 @@ +--- +title: Application Lifecycle +order: 1 +--- + +## NativePHP Application Lifecycle + +When your NativePHP application starts - whether it's in development or production - it performs a series of steps to get your application up and running. + +1. The native shell (Electron) is started. +2. NativePHP runs `php artisan migrate` to ensure your database is up-to-date. +3. NativePHP runs `php artisan serve` to start the PHP development server. +4. NativePHP boots your application by running the `boot()` method on your `NativeAppServiceProvider`. +5. NativePHP also dispatches a `ApplicationBooted` event. + +## The NativeAppServiceProvider + +When running `php artisan native:install`, NativePHP publishes a `NativeAppServiceProvider` to `app/Providers/NativeAppServiceProvider.php`. + +You may use this service provider to boostrap your application. +For example, you may want to open a window, register global shortcuts, or configure your application menu. + +The default `NativeAppServiceProvider` looks like this: + +```php +namespace App\Providers; + +use Native\Desktop\Facades\Window; +use Native\Desktop\Contracts\ProvidesPhpIni; + +class NativeAppServiceProvider implements ProvidesPhpIni +{ + /** + * Executed once the native application has been booted. + * Use this method to open windows, register global shortcuts, etc. + */ + public function boot(): void + { + Window::open(); + } + + /** + * Return an array of php.ini directives to be set. + */ + public function phpIni(): array + { + return [ + ]; + } +} +``` + +## Events + +### `ApplicationBooted` + +As mentioned above, the `Native\Desktop\Events\App\ApplicationBooted` event is dispatched when your application has been booted. diff --git a/resources/views/docs/desktop/2/the-basics/application-menu.md b/resources/views/docs/desktop/2/the-basics/application-menu.md new file mode 100644 index 00000000..860f3a49 --- /dev/null +++ b/resources/views/docs/desktop/2/the-basics/application-menu.md @@ -0,0 +1,489 @@ +--- +title: Application Menus +order: 300 +--- + +## Configuring the Application Menu + +NativePHP allows you to configure the native menu of your application, as well as context menus, MenuBar and Dock menus, using a +single, unified and expressive Menu API, available through the `Menu` facade. Use this for building all of your app's menus. + +```php +use Native\Desktop\Facades\Menu; +``` + +The configuration of your application menu should happen in the `boot` method of your `NativeAppServiceProvider`. + +### Creating the menu + +To create a new application menu, you may use the `Menu::create()` method. This method creates _and registers_ your +menu in one step. + +You can customize the items that appear in the menu by passing them as parameters to the `create` method: + +```php +namespace App\Providers; + +use Native\Desktop\Facades\Menu; +use Native\Desktop\Facades\Window; + +class NativeAppServiceProvider +{ + public function boot(): void + { + Menu::create( + Menu::app(), // Only on macOS + Menu::file(), + Menu::edit(), + Menu::view(), + Menu::window(), + ); + + Window::open(); + } +} +``` + +### The Default menu + +You may use the `Menu::default()` method to create the default application menu. This menu contains all the items that +you would expect in a typical application menu (File, Edit, View, Window): + +```php +// Instead of... +Menu::create( + Menu::app(), + Menu::file(), + Menu::edit(), + Menu::view(), + Menu::window(), +); + +// You can just write... +Menu::default(); +``` + +### Recreating the menu + +It's sometimes desirable to update the main application menu with a brand new configuration in response to changes in your +application, making it contextually sensitive, perhaps based on which window currently has focus. + +You can update your application menu at any time simply by calling `Menu::create()` again with your desired menu structure. +This might be in an event listener, a controller action or even a Livewire action method. + +## Predefined menus + +NativePHP comes with a few predefined menus that you can use out of the box. These are a convenience and, for the most +part, the only thing that can be changed about them is their label. + +You may change this by passing a string as the first parameter to each method, for example: + +```php +Menu::edit('My Edit Menu') +``` + +**The default menus enable a number of common keyboard shortcuts ("hotkeys"), such as those typically used for cut, +copy, and paste. If you decide to build custom versions of these menus, you will need to explicitly define these +shortcuts yourself.** + +**On macOS, the first item in your application menu will _always_ use the name of your application as its label, +overriding any custom label you set, regardless of which type of submenu you place first.** + +### The App menu + +You may use the `Menu::app()` method to create the default application menu. This menu contains all the items that +you would expect in an application menu (e.g. About, Services, Quit, etc.). + +```php +Menu::create( + Menu::app(), +); +``` + +**The app menu is only available for macOS. It is typically used as the first menu in your application's menu.** + +### The File menu + +You may use the `Menu::file()` method to create the default file menu. This menu contains items and functionality that +you would expect in a file menu (e.g. Close/Quit). + +```php +Menu::create( + Menu::app(), + Menu::file(), +); +``` + +The file menu uses "File" as its label by default. + +### The Edit menu + +You may use the `Menu::edit()` method to create the default edit menu. This menu contains all the items and +functionality that you would expect in an edit menu (e.g. Undo, Redo, Cut, Copy, Paste, etc.). + +```php +Menu::create( + Menu::app(), + Menu::edit(), +); +``` + +The edit menu uses "Edit" as its label by default. + +### The View menu + +You may use the `Menu::view()` method to create the default view menu. This menu contains all the default items and +functionality that you would expect in a view menu (e.g. Toggle Fullscreen, Toggle Developer Tools, etc.). + +```php +Menu::create( + Menu::app(), + Menu::view(), +); +``` + +The view menu uses "View" as its label by default. + +### The Window menu + +You may use the `Menu::window()` method to create the default window menu. This menu contains all the default items and +functionality that you would expect in a window menu (e.g. Minimize, Zoom, etc.). + +```php +Menu::create( + Menu::app(), + Menu::window() +); +``` + +The window menu uses "Window" as its label by default. + +## Custom Submenus + +You may use the `Menu::make()` method to build a custom menu. Rather than registering this menu as the main application +menu, the `make()` method returns an instance of the `Native\Desktop\Menu\Menu` object, which you can pass into places +where `Menu` instances are accepted. + +`Menu` instances are also a `MenuItem`, so they can be nested within other menus to create submenus: + +```php +Menu::create( + Menu::app(), + Menu::make( + Menu::link('https://nativephp.com', 'Documentation'), + )->label('My Submenu') +); +``` + +## Menu Items + +![Menu items](/img/docs/custom-menus.png) + +NativePHP provides a range of menu items that you can use in your menus, all accessible from the `Menu` facade: + +```php +Menu::make( + Menu::checkbox(string $label, bool $checked = false, ?string $hotkey = null), + Menu::label(string $label, ?string $hotkey = null), + Menu::link(string $url, ?string $label = null, ?string $hotkey = null), + Menu::radio(string $label, bool $checked = false, ?string $hotkey = null), + Menu::route(string $route, ?string $label = null, ?string $hotkey = null), +); +``` + +Each is a subclass of the `Native\Desktop\Menu\Items\MenuItem` class which provides many useful methods to help you +build the perfect menu: + +```php +$item = Menu::route('welcome') + ->label('Home') + ->id('my-item') + ->icon(public_path('path/to/icon.png')) + ->visible(false) + ->tooltip('Hover text FTW!') // macOS only + ->hotkey('Cmd+F') + ->disabled(); +``` + +Other methods are available depending on the type of menu item. + +### Handling clicks + +Almost all menu items will fire an event when clicked or by pressing their hotkey combo. You may decide which event is +fired by chaining the `event()` method to the menu item: + +```php +Menu::label('Click me!') + ->event(MyCustomMenuItemEvent::class) +``` + +Your custom event class should extend the default `Native\Desktop\Events\Menu\MenuItemClicked` class. + +If you do not provide a custom event to fire, the default event will be used. By default, this event is +[broadcast](/docs/digging-deeper/broadcasting) across your app so you can listen for it either in your Laravel back-end, +via Javascript in your windows, or both. + +The click event receives details of the menu item that was clicked, as well as an array of combo keys that may have +been pressed at the time the item was clicked. + +### Hotkeys + +Hotkeys can be defined for all menu items, either via the `hotkey` parameter of the respective `Menu` facade method or +by using the `hotkey()` chainable method: + +```php +Menu::label('Quick search', hotkey: 'Ctrl+K'); + +// Or + +Menu::label('Quick search')->hotkey('Ctrl+K'); +``` + +You can find a list of available hotkey modifiers in the +[global hotkey documentation section](/docs/the-basics/global-hotkeys#available-modifiers). + +Unlike global hotkeys, hotkeys registered to menu items will only be fired when one of your application's windows are +focused or the relevant context menu is open. + +When a menu item is fired from a hotkey combo press, the event's `$combo` parameter will have its `triggeredByAccelerator` +value set to `true`. + +### Label items + +The simplest menu item is just a label. You may use the `Menu::label()` method to add a label item to your menu: + +```php +Menu::make( + Menu::label('Support'), +); +``` + +These are great when you want your app to do something in response to the user clicking the menu item or pressing +a hotkey combo. + +### Link items + +Link items allow you to define navigational elements within your menus. These can either navigate users to another URL +within your application or to an external page hosted on the internet. + +You may add a link to your menu by using the `Menu::link()` method: + +```php +Menu::link('/login', 'Login'); +``` + +This will navigate the currently-focused window to the URL provided. + +You may use the `Menu::route()` method as a convenience to map to a +[named route](https://laravel.com/docs/routing#named-routes): + +```php +Menu::route('login', 'Login'); +``` + +When combined with the `openInBrowser()` method, Link items are great for creating links to external websites that you +would like to open in the user's default web browser: + +```php +Menu::link('https://nativephp.com/', 'Documentation') + ->openInBrowser(); +``` + +**You should never open untrusted external websites within your application's windows. If you're not very careful, you +may introduce serious vulnerabilities onto your user's device.** + +### Checkbox and Radio items + +In some cases, your app may not require a preferences panel, and a few interactive menu items may suffice to allow +your user to configure some settings. Or you may wish to make certain commonly-used settings more readily accessible. + +Checkbox and Radio items enable you to create menu items for just these purposes. They operate in a very similar way +to checkboxes and radio buttons in a web form. Their default state is 'unchecked'. + +You may use the `Menu::checkbox()` and `Menu::radio()` methods to create such items, passing the initial state of the +item to the `checked` parameter or using the `checked()` chainable method: + +```php +Menu::checkbox('Word wrap', checked: true); + +// Or + +Menu::checkbox('Word wrap')->checked(); +``` + +When Checkbox and Radio items are triggered, the click event data will indicate whether or not the item is currently +checked via the `$item['checked']` value. + +#### Radio groups + +Unlike radio buttons in HTML forms, Radio menu items are not grouped by their name; they are grouped logically with +all other radio items in the same menu. + +However, you _can_ have separate groups of radio buttons within the _same_ menu if you separate them with a +[separator](#separators): + +```php +Menu::make( + Menu::radio('Option 1'), + Menu::radio('Option 2'), + Menu::separator(), + Menu::radio('Option 1'), + Menu::radio('Option 2'), +); +``` + +These two radio groups will operate independently of each other. + +## Special Menu Items + +NativePHP also ships with a number of "special" menu items that provide specific behavior for you to use in your menus. + +These items usually have default labels and hotkeys associated with them and provide the basic, default functionality +commonly associated with them in any web browser. Therefore, they do not fire any click events. + +You may only override their labels. + +### Separators + +You may add separators to your menu by using the `Menu::separator()` method. + +A separator is a horizontal line that visually separates menu items. + +```php +Menu::make( + Menu::link('https://nativephp.com', 'Learn more'), + Menu::separator(), + Menu::link('https://nativephp.com/docs/', 'Documentation'), +); +``` + +As already noted, they also aid in logically grouping radio items. + +### Undo and Redo + +If you have chosen not to include the [default Edit menu](#the-edit-menu) in your application menu, +you may add the default undo and redo functionality to your app by using the `Menu::undo()` and +`Menu::redo()` methods. + +```php +Menu::make() + Menu::undo(), + Menu::redo(), +); +``` + +**These standard actions work well with text input from the user provided via standard `input` or `textarea` elements, +but for more complex undo/redo workflows, you may wish to implement your own logic. In which case, you should not use +these items.** + +### Cut, Copy, and Paste + +If you have chosen not to include the [default Edit menu](#the-edit-menu) in your application menu, +you may add the default cut, copy and paste functionality to your app by using the `Menu::cut()`, `Menu::copy()` and +`Menu::paste()` methods. + +```php +Menu::make() + Menu::cut(), + Menu::copy(), + Menu::paste(), +); +``` + +**These standard actions work well with text input from the user provided via standard `input` or `textarea` elements, +but for more complex cut, copy and paste workflows, you may wish to implement your own logic. In which case, you should +not use these items.** + +### Fullscreen + +You may add a fullscreen item to your menu by using the `Menu::fullscreen()` method. + +When the user clicks on the fullscreen item, the application will attempt to enter fullscreen mode. This will only work +if your currently-focused window is [fullscreen-able](/docs/the-basics/windows#full-screen-windows). + +```php +Menu::make() + Menu::fullscreen('Supersize me!'), +); +``` + +### Minimize + +You may add a minimize item to your menu by using the `Menu::minimize()` method. + +When the user clicks on the minimize item, the currently-focused window will be minimized. + +```php +Menu::make() + Menu::minimize(), +); +``` + +### Quit + +You may add a quit item to your menu by using the `Menu::quit()` method. + +When the user clicks on the quit item, the application will attempt to quit. + +```php +Menu::make() + Menu::quit(), +); +``` + +## Context Menu + +You may wish to add a custom native context menu to the elements in the views of your application and override the default one. + +You can use the `Native` JavaScript helper provided by NativePHP's preload script. + +This object exposes the `contextMenu()` method which takes an array of objects that matches the +[MenuItem](https://www.electronjs.org/docs/latest/api/menu-item) constructor's `options` argument. + +```js +Native.contextMenu([ + { + label: 'Edit', + accelerator: 'e', + click(menuItem, window, event) { + // Code to execute when the menu item is clicked + }, + }, + // Other options +]) +``` + +You can listen for the `contextmenu` event to show your custom context menu: + +```js +const element = document.getElementById('your-element') + +element.addEventListener('contextmenu', (event) => { + event.preventDefault() + + Native.contextMenu([ + { + label: 'Duplicate', + accelerator: 'd', + click() { + duplicateEntry(element.dataset.id) + }, + }, + { + label: 'Edit', + accelerator: 'e', + click() { + showEditForm(element.dataset.id) + }, + }, + { + label: 'Delete', + click() { + if (confirm('Are you sure you want to delete this entry?')) { + deleteEntry(element.dataset.id) + } + }, + }, + ]) +}) +``` diff --git a/resources/views/docs/desktop/2/the-basics/application.md b/resources/views/docs/desktop/2/the-basics/application.md new file mode 100644 index 00000000..b56a246e --- /dev/null +++ b/resources/views/docs/desktop/2/the-basics/application.md @@ -0,0 +1,161 @@ +--- +title: Application +order: 250 +--- + +## Application + +The `App` facade allows you to perform basic operations with the Electron app. + +Note: Some methods are only available on specific operating systems and are labeled as such. + +To use the `App` facade, add the following to the top of your file: + +```php +use Native\Desktop\Facades\App; +``` + +### Quit the app + +To quit the app, use the `quit` method: + +```php +App::quit(); +``` + +### Relaunch the app + +To relaunch the app, use the `relaunch` method. This will quit the app and relaunch it. + +```php +App::relaunch(); +``` + +### Focus the app + +To focus the app, use the `focus` method. + +On Linux, it focuses on the first visible window. On macOS, it makes the application the active one. On Windows, it +focuses on the application's first window. + +```php +App::focus(); +``` + +### Hide the app + +_Only available on macOS_ + +The `hide` method will hide all application windows without minimizing them. This method is only available on macOS. + +```php +App::hide(); +``` + +### Check if the app is hidden + +_Only available on macOS_ + +To check if the app is hidden, use the `isHidden` method. This method is only available on macOS. + +Returns a boolean: `true` if the application—including all its windows—is hidden (e.g., with Command-H), `false` +otherwise. + +```php +$isHidden = App::isHidden(); +``` + +### Current Version + +To get the current app version, use the `version` method. The version is defined in the `config/nativephp.php` file. + +```php +$version = App::version(); +``` + +### Locale information + +The facade offers several methods for accessing some of the system's localisation information. +This data can be helpful for localising your application, e.g. if you want to suggest the corresponding language to the user on first launch. + +```php +App::getLocale(); // e.g. "de", "fr-FR" +App::getLocaleCountryCode(); // e.g. "US", "DE" +App::getSystemLocale(); // e.g. "it-IT", "de-DE" +``` + +The `getLocale` method will return the locale used by the app. +Dependening on the user's settings, this might include both the language and the country / region or the language only. +It is based on Chromiums `l10n_util` library; see [this page](https://source.chromium.org/chromium/chromium/src/+/main:ui/base/l10n/l10n_util.cc) to see possible values. + +`getLocaleCountryCode` returns the user's system country code (using the [ISO 3166 code](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2)). +This information is pulled from native OS APIs. If it is not possible to detect this information, an empty string will be returned. + +With `getSystemLocale` you can access the system-wide locale setting. This is the locale set at the operating system level, not necessarily what the app is using. +Under Windows and Linux, Chromium's `i18n` library is used to evaluate this information. macOS will use `[NSLocale currentLocale]`. + +### App Badge Count + +_Only available on macOS and Linux_ + +You can set the app's badge count. +On macOS, it shows on the dock icon. On Linux, it only works for Unity launcher. + +To set the badge count, use the `badgeCount` method: + +```php +App::badgeCount(5); +``` + +To remove the badge count, use the `badgeCount` method with `0` as the argument: + +```php +App::badgeCount(0); +``` + +To get the badge count, use the `badgeCount` method without any arguments: + +```php +$badgeCount = App::badgeCount(); +``` + +### Recent documents list + +_Only available on macOS and Windows_ + +The recent documents list is a list of files that the user has recently opened. This list is available on macOS and +Windows. + +To add a document to the recent documents list, use the `addRecentDocument` method: + +```php +App::addRecentDocument('/path/to/document'); +``` + +To clear the recent documents list, use the `clearRecentDocuments` method: + +```php +App::clearRecentDocuments(); +``` + +### Open at login + +_Only available on macOS and Windows_ + +To enable 'open at login', use the `openAtLogin` method: + +```php +App::openAtLogin(true); +``` + +To disable open at login, use the `openAtLogin` method with `false` as the argument: + +```php +App::openAtLogin(false); +``` + +To check if the app is set to open at login, use the `openAtLogin` method without any arguments: + +```php +$isOpenAtLogin = App::openAtLogin(); +``` diff --git a/resources/views/docs/desktop/2/the-basics/clipboard.md b/resources/views/docs/desktop/2/the-basics/clipboard.md new file mode 100644 index 00000000..83301379 --- /dev/null +++ b/resources/views/docs/desktop/2/the-basics/clipboard.md @@ -0,0 +1,46 @@ +--- +title: Clipboard +order: 700 +--- + +## Working with the Clipboard + +NativePHP allows you to easily read from and write to the system clipboard using just PHP, thanks to the `Clipboard` +facade. + +```php +use Native\Desktop\Facades\Clipboard; +``` + +### Reading from the Clipboard + +You can read `text`, `html` or `image` data from the clipboard using the appropriate method: + +```php +Clipboard::text(); +Clipboard::html(); +Clipboard::image(); +``` + +### Writing to the Clipboard + +You can write `text`, `html` or `image` data to the clipboard using the appropriate method: + +```php +Clipboard::text('Some copied text'); +Clipboard::html('
Some copied HTML
'); +Clipboard::image('path/to/image.png'); +``` + +Note that the `image()` method expects a path to an image, not the image data itself. NativePHP will take care of +serializing the image data for you. + +### Clearing the Clipboard + +You may also programmatically clear the clipboard using the `clear()` method. + +```php +Clipboard::clear(); +``` + +This is useful if you need the contents of the clipboard to expire after a certain amount of time. diff --git a/resources/views/docs/desktop/2/the-basics/dialogs.md b/resources/views/docs/desktop/2/the-basics/dialogs.md new file mode 100644 index 00000000..81129a19 --- /dev/null +++ b/resources/views/docs/desktop/2/the-basics/dialogs.md @@ -0,0 +1,152 @@ +--- +title: Dialogs +order: 400 +--- + +## Native Dialogs + +NativePHP allows you to open native file dialogs. They can be used to give the user the ability to select a file or folder, or to save a file. + +Dialogs are created using the `Dialog` facade. + +```php +use Native\Desktop\Dialog; +``` + +### Opening File Dialogs + +To open a file dialog, you may use the `Dialog` class and its `open()` method. + +The return value of the `open()` method is the path to the file or folder that the user selected. +This could be null, a file path (string), or an array of file paths, depending on the type of dialog you open. + +```php +Dialog::new() + ->title('Select a file') + ->open(); +``` + +### Opening Save Dialogs + +The `open()` dialog allows users to select existing files or folders, but not to create new files or folders. +For this, you may use the `save()` method. +This method will return the path to the file that the user wants to save. + +Please note that the `save()` method will not actually save the file for you, it will only return the path to the file that the user wants to save. + +```php +Dialog::new() + ->title('Save a file') + ->save(); +``` + +## Configuring File Dialogs + +### Dialog Title + +You may set the title of the dialog using the `title()` method. + +```php +Dialog::new() + ->title('Select a file') + ->open(); +``` + +### Dialog Button Label + +You may configure the label of the dialog button using the `button()` method. +This is the button that the user clicks to confirm their selection. + +```php +Dialog::new() + ->button('Select') + ->open(); +``` + +### Dialog Default Path + +You may configure the default path of the dialog using the `defaultPath()` method. +This is the path that the dialog will open in by default, if it exists. + +```php +Dialog::new() + ->defaultPath('/Users/username/Desktop') + ->open(); +``` + +### Dialog File Filters + +By default, the file dialog will allow the user to select any file. +You may constrain the file types that the user can select using the `filter()` method. +One dialog can have multiple filters. + +The first argument of the `filter()` method is the name of the filter, and the second argument is an array of file extensions. + +```php +Dialog::new() + ->filter('Images', ['jpg', 'png', 'gif']) + ->filter('Documents', ['pdf', 'docx']) + ->open(); +``` + +### Allowing Multiple Selections + +By default, the file dialog will only allow the user to select one file. +You may change this behavior using the `multiple()` method. +This will result in the `open()` method returning an array of file paths, instead of a single file path string. + +```php +$files = Dialog::new() + ->multiple() + ->open(); +``` + +### Showing Hidden Files + +By default, the file dialog will not show hidden files (files that start with a dot). +You may change this behavior using the `showHiddenFiles()` method. + +```php +Dialog::new() + ->withHiddenFiles() + ->open(); +``` + +### Resolving Symbolic Links + +By default, the file dialog will always resolve symbolic links. +This means that if you select a symbolic link, the dialog will return the path to the file or folder that the symbolic link points to. + +You may change this behavior using the `dontResolveSymlinks()` method. + +```php +Dialog::new() + ->dontResolveSymlinks() + ->open(); +``` + +### Opening Dialogs as Sheets + +By default, all NativePHP dialogs will open as separate windows that can be moved around independently. + +If you would like to open a dialog as a "sheet" (a dialog that is attached to a window), you may use the `asSheet()` method. +The first argument of the `asSheet()` method is the ID of the window to attach the dialog to. +If you do not specify a window ID, NativePHP will use the ID of the currently focused window. + +```php +Dialog::new() + ->asSheet() + ->open(); +``` + +### Opening Folders + +By default, the dialog opens a file or group of files. + +If you would like to open a folder instead, you may use the `folders()` method. + +```php +Dialog::new() + ->folders() + ->open(); +``` diff --git a/resources/views/docs/desktop/2/the-basics/global-hotkeys.md b/resources/views/docs/desktop/2/the-basics/global-hotkeys.md new file mode 100644 index 00000000..dab968cf --- /dev/null +++ b/resources/views/docs/desktop/2/the-basics/global-hotkeys.md @@ -0,0 +1,95 @@ +--- +title: Global Hotkeys +order: 600 +--- + +## Global Hotkeys + +In your NativePHP application, you may define multiple global hotkeys. +Unlike hotkeys that you may define in your application via JavaScript, these hotkeys are globally registered. +This means that your application may be aware of these hotkeys being triggered even when it is running in the +background and not focused. + +As these global hotkeys are usually used in your entire application, a common approach to registering them is inside +the `NativeAppServiceProvider` class. + +```php +namespace App\Providers; + +use Native\Desktop\Facades\GlobalShortcut; + +class NativeAppServiceProvider +{ + public function boot(): void + { + GlobalShortcut::key('CmdOrCtrl+Shift+A') + ->event(\App\Events\MyShortcutEvent::class) + ->register(); + + // Additional code, such as registering a menu, opening windows, etc. + } +} +``` + +## Registering Hotkeys + +You may register a global shortcut using the `GlobalShortcut` facade. +Using the `key` method, you may specify the hotkey to listen for. The hotkey must be a string that contains the +modifiers and the key separated by a `+` sign. + +For example, if you want to register a hotkey that triggers the `MyEvent` event when the user presses `Cmd+Shift+D`, +you may do the following: + +```php +GlobalShortcut::key('Cmd+Shift+D') + ->event(\App\Events\MyEvent::class) + ->register(); +``` + +You can find a list of all available modifiers [here](#available-modifiers). + +## Removing registered hotkeys + +Sometimes you may want to remove an already registered global hotkey. +To do this, specify the hotkey that you used to register and call the `unregister` method on the `GlobalShortcut` facade. +You do not need to provide an event class in this case, as every hotkey can only be registered once. + +For example, in order to remove the `Cmd+Shift+D` global hotkey, you may do the following: + +```php +GlobalShortcut::key('Cmd+Shift+D') + ->unregister(); +``` + +### Available modifiers + +- `Command` or `Cmd` +- `Control` or `Ctrl` +- `CommandOrControl` or `CmdOrCtrl` +- `Alt` +- `Option` +- `AltGr` +- `Shift` +- `Super` +- `Meta` + +### Available key codes + +- `0` to `9` +- `A` to `Z` +- `F1` to `F24` +- `Backspace` +- `Delete` +- `Insert` +- `Return` or `Enter` +- `Up`, `Down`, `Left` and `Right` +- `Home` and `End` +- `PageUp` and `PageDown` +- `Escape` or `Esc` +- `VolumeUp`, `VolumeDown` and `VolumeMute` +- `MediaNextTrack`, `MediaPreviousTrack`, `MediaStop` and `MediaPlayPause` +- `PrintScreen` +- `Numlock` +- `Scrolllock` +- `Space` +- `Plus` diff --git a/resources/views/docs/desktop/2/the-basics/menu-bar.md b/resources/views/docs/desktop/2/the-basics/menu-bar.md new file mode 100644 index 00000000..98a0d01f --- /dev/null +++ b/resources/views/docs/desktop/2/the-basics/menu-bar.md @@ -0,0 +1,328 @@ +--- +title: Menu Bar +order: 200 +--- + +## Working with the Menu Bar + +![Menu Bar Example on macOS](/img/docs/menubar.png) + +NativePHP allows you to create a native application menu bar for your application. This can be used as an addition to +your existing application that already uses windows, or as a standalone (menu-bar only) application. + +When the user clicks on the menu bar icon, the menu bar window will open and show the given URL or route. + +The configuration of your MenuBar should happen in the `boot` method of your `NativeAppServiceProvider`. + +### Creating a stand-alone Menu Bar application + +To create a menu bar for your application, you may use the `MenuBar` facade. +When creating the menu bar, NativePHP will automatically open the root URL of your application. +By default, adding a menu bar will automatically hide the dock icon of your application. + +```php +namespace App\Providers; + +use Native\Desktop\Facades\MenuBar; + +class NativeAppServiceProvider +{ + public function boot(): void + { + MenuBar::create(); + } +} +``` + +### Creating a Menu Bar for an application that already uses windows + +You may also create a menu bar for an application that already uses windows. Usually you will want to show the +dock icon of your application in this case. +To do so, you may use the `MenuBar::create()` method, but this time call the `showDockIcon()` method. + +```php +namespace App\Providers; + +use Native\Desktop\Facades\MenuBar; + +class NativeAppServiceProvider +{ + public function boot(): void + { + MenuBar::create() + ->showDockIcon(); + } +} +``` + +### Opening the Menu Bar + +You may use the `MenuBar::show()` method to manually open the menu bar window. + +```php +MenuBar::show(); +``` + +### Hiding the Menu Bar + +You may use the `MenuBar::hide()` method to manually close the menu bar window. + +```php +MenuBar::hide(); +``` + +### Menu Bar Labels + +![Menu Bar Labels](/img/docs/menubar-labels.png) + +By default, the menu bar will only show the configured [menu bar icon](#menu-bar-icon). +Additionally, you may add a label to the menu bar that will be shown next to the icon. + +This label can be changed at any time by using the `label()` method. + +```php +MenuBar::label('Status: Online'); +``` + +You may also use the `label()` method while creating the menu bar to set the initial label. + +```php +MenuBar::create() + ->label('Status: Online'); +``` + +To remove the label, you may pass an empty string to the `label()` method. + +```php +MenuBar::label(''); +``` + +### Tooltip + +Add a tooltip to the menu bar icon: + +```php +MenuBar::tooltip('Click to open'); +``` + +## Configuring the Menu Bar + +### Menu Bar URL + +![Menu Bar Window](/img/docs/menubar-window.png) + +By default, the `MenuBar::create()` method will configure your menu bar to show the root URL of your application when clicked. +If you would like to open a different URL, you may use the `route()` method to specify the route name to open. + +```php +MenuBar::create() + ->route('home'); +``` + +You may also pass an absolute URL to the `url()` method: + +```php +MenuBar::create() + ->url('https://google.com'); +``` + +### Menu Bar Icon + +The default menu bar icon is the NativePHP logo. You may change this icon by using the `icon()` method. +This method accepts an absolute path to an image file. + +When providing an icon, you should make sure that the image is a PNG file with a transparent background. +The recommended size for the icon is **22x22 pixels**, as well as **44x44 pixels** for retina displays. + +The file name for the retina display icon should be the same as the regular icon, but with `@2x` appended to the file name. + +Example: + +```text +menuBarIcon.png +menuBarIcon@2x.png +``` + +On macOS, it is recommended to use a so-called "Template Image". +This is an image that is rendered as a white or black image with a transparent background. + +![Menu Bar Icon Light Mode](/img/docs/menubar-icon-light.png) +![Menu Bar Icon Dark Mode](/img/docs/menubar-icon-dark.png) + +NativePHP can automatically convert your image to a template image. To do so, you may name your image file with `Template` appended to the file name. + +Example: + +```text +menuBarIconTemplate.png +menuBarIconTemplate@2x.png +``` + +You do not need to manually append `@2x` to the file name, as NativePHP will automatically detect the retina display icon and use it when available. + +```php +MenuBar::create() + ->icon(storage_path('app/menuBarIconTemplate.png')); +``` + +### Vibrancy and Background Color + +For macOS, you may use the `vibrancy` method to apply window vibrancy effects: + +```php +MenuBar::create()->vibrancy('light'); +``` + +To create a solid background color instead: + +```php +MenuBar::create()->backgroundColor('#ffffff'); +``` + +### Menu Bar Window Sizes + +![Menu Bar Window Sizes](/img/docs/menubar-window-size.png) + +The default size of the menu bar window is **400x400 pixels**. +You may use the `width()` and `height()` methods to specify the size of the window that will be opened when the user clicks on the menu bar icon. + +```php +MenuBar::create() + ->width(800) + ->height(600); +``` + +### Resizable Window + +Allow or prevent resizing of the menu bar window: + +```php +MenuBar::resizable(false); +``` + +### Positioning + +You may manually set the position of the menu bar window: + +```php +MenuBar::create() + ->x(100) + ->y(200); +``` + +### Menu Bar on Top + +When developing a menu bar application, you may want to make sure that the menu bar window is always open and on top of all other windows. +This makes it easier to develop your application, as you do not have to click on the menu bar icon every time you want to see the window. + +To do so, you may use the `alwaysOnTop()` method on the `MenuBar`. + +```php +MenuBar::create() + ->alwaysOnTop(); +``` + +## Menu Bar Context Menu + +You may add a context menu to your menu bar icon. This context menu will be shown when the user right-clicks on the menu bar icon. + +### Adding a Context Menu + +![Menu Bar Context Menu](/img/docs/menubar-context-menu.png) + +To add a context menu to your Menu Bar app, you may use the `withContextMenu()` method on the `MenuBar`. + +This method accepts a `Native\Desktop\Menu\Menu` instance, which can be created using the `Menu::make()` method of the `Menu` facade. + +```php +use Native\Desktop\Facades\Menu; + +MenuBar::create() + ->withContextMenu( + Menu::make( + Menu::label('My Application'), + Menu::separator(), + Menu::link('https://nativephp.com', 'Learn more…') + ->openInBrowser(), + Menu::separator(), + Menu::quit() + ) + ); +``` + +To learn more about the `Menu` facade, please refer to the [Application Menu](/docs/the-basics/application-menu) documentation. + +### Opening a Context Menu + +You can programmatically display the context menu that has been configured for your Menu Bar app using the `showContextMenu()` method. This method will show the same context menu that appears when a user clicks on the Menu Bar app. + +```php +MenuBar::showContextMenu(); +``` + +This is useful when you want to trigger the context menu from within your application logic, such as in response to a keyboard shortcut, button click, or other application events. + +### Setting webpage features + +You may control various web page features by passing an optional `webPreferences` configuration array, similar to how it works with [Windows](/docs/the-basics/windows#setting-webpage-features). This allows you to customize how the menu bar window behaves and what features are available to the web content. + +```php +MenuBar::create() + ->webPreferences([ + 'nodeIntegration' => true, + 'spellcheck' => true, + 'backgroundThrottling' => true, + ]); +``` + +The same defaults and restrictions apply as with Windows. For more details about available options and default settings, see the [Windows documentation](/docs/the-basics/windows#setting-webpage-features). + +## Events + +NativePHP provides a simple way to listen for menu bar events. +All events get dispatched as regular Laravel events, so you may use your `EventServiceProvider` to register listeners. + +Sometimes you may want to listen and react to window events in real-time, which is why NativePHP also broadcasts all +window events to the `nativephp` broadcast channel. + +To learn more about NativePHP's broadcasting capabilities, please refer to the [Broadcasting](/docs/digging-deeper/broadcasting) section. + +### Listening for Custom Events + +Attach a custom event that should be fired when the menu bar icon is clicked. This only works when combined with [`onlyShowContextMenu()`](#context-menu-only): + +```php +MenuBar::create()->event(MenuBarClicked::class); + +class MenuBarClicked +{ + public function __construct(public array $combo, public array $bounds, public array $position) + { + // $combo - details of any combo keys pressed when the click occurred + // $bounds - the current absolute bounds of the menu bar icon at the time of the event + // $position - the absolute cursor position at the time of the event + } +} +``` + +### `MenuBarShown` + +The `Native\Desktop\Events\MenuBar\MenuBarShown` event will be dispatched when the user clicks on the menu bar icon and the menu bar window opens, or when +the menu bar gets shown by using the `MenuBar::show()` method. + +### `MenuBarHidden` + +The `Native\Desktop\Events\MenuBar\MenuBarHidden` event will be dispatched when the user clicks out of the menu bar window and the menu bar window closes, or when +the menu bar gets hidden by using the `MenuBar::hide()` method. + +### `MenuBarContextMenuOpened` + +The `Native\Desktop\Events\MenuBar\MenuBarContextMenuOpened` event will be dispatched when the user right-clicks on the menu bar icon and the context menu opens. + +### Context Menu Only + +Show only the context menu without opening a window when the menu bar icon is clicked: + +```php +MenuBar::onlyShowContextMenu(); +``` diff --git a/resources/views/docs/desktop/2/the-basics/notifications.md b/resources/views/docs/desktop/2/the-basics/notifications.md new file mode 100644 index 00000000..d0d145d7 --- /dev/null +++ b/resources/views/docs/desktop/2/the-basics/notifications.md @@ -0,0 +1,218 @@ +--- +title: Notifications +order: 500 +--- + +## Native Notifications + +NativePHP allows you to send system notifications using an elegant PHP API. These notifications are, unlike Laravel's built-in notifications, actual UI notifications displayed by your operating system. + +When used sparingly, notifications can be a great way to inform the user about events that are occurring in your application and to bring their attention back to it, especially if further input from them is required. + +Notifications are sent using the `Notification` facade. + +```php +use Native\Desktop\Facades\Notification; +``` + +### Sending Notifications + +You may send a notification using the `Notification` facade. + +```php +Notification::title('Hello from NativePHP') + ->message('This is a detail message coming from your Laravel app.') + ->show(); +``` + +This will show a system-wide notification to the user with the given title and message. + +### Handling clicks on notifications + +You may register a custom event along with your NativePHP notification. +This event will be fired when a user clicks on the notification, so that you may add some custom logic within your application in this scenario. + +To attach an event to your notification, you may use the `event` method. The argument passed to this method is the class name of the event that should get dispatched upon clicking on the notification. + +```php +Notification::title('Hello from NativePHP') + ->message('This is a detail message coming from your Laravel app.') + ->event(\App\Events\MyNotificationEvent::class) + ->show(); +``` + +### Notification References + +To keep track of different notifications, you may use the notification's `$reference` property. + +By default, a unique reference is generated for you, but you may manually set a reference by [chaining the `reference()`](#notification-reference) method when creating +the notification. + +## Configuring Notifications + +### Notification Title + +You may set the notification's title using the `title()` method. + +```php +Notification::title('Hello from NativePHP') + ->show(); +``` + +### Notification Reference + +You can access the `$reference` property of a notification after it has been created: + +```php +$notification = Notification::title('Hello from NativePHP')->show(); + +$notification->reference; +``` + +You may chain the `reference()` method to set a custom reference when creating a notification: + +```php +Notification::title('Hello from NativePHP') + ->reference(Str::uuid()) + ->show(); +``` + +The reference will be sent along with any event triggered by the notification and can be used to track which specific notification was clicked: + +```php +use App\Events\PostNotificationClicked; +use App\Models\Post; + +Post::recentlyCreated() + ->get() + ->each(function(Post $post) { + Notification::title('New post: ' . $post->title) + ->reference($post->id) + ->event(PostNotificationClicked::class) + ->show(); + }); + +Event::listen(PostNotificationClicked::class, function (PostNotificationClicked $event) { + $post = Post::findOrFail($event->reference); + + Window::open()->url($post->url); +}); +``` + +### Notification Message + +You may set the notification's message using the `message()` method. + +```php +Notification::title('Hello from NativePHP') + ->message('This is a detail message coming from your Laravel app.') + ->show(); +``` + +### Notification Reply + +On macOS, you can allow the user to reply to a notification using the `hasReply()` method. + +```php +Notification::title('Hello from NativePHP') + ->hasReply() + ->show(); +``` + +The `hasReply()` method accepts a placeholder reply message as an argument. + +```php +Notification::title('Hello from NativePHP') + ->hasReply('This is a placeholder') + ->show(); +``` + +### Notification Sounds + +You may set a custom audio by using the `sound()` method. + +Setting a sound overrides the system's default notification sound for that notification. + +You can use the system's default notification sounds. On macOS, for example, set the sound to `'Ping'`. + +```php +Notification::title('Hello from NativePHP') + ->sound('Ping') + ->show(); +``` + +You can also provide a custom audio file. + +```php +Notification::title('Hello from NativePHP') + ->sound(resource_path('example.mp3')) + ->show(); +``` + +### Silent Notifications + +Make a notification silent with the `silent()` method: + +```php +Notification::title('Hello from NativePHP') + ->silent() + ->show(); +``` + +### Notification Actions + +On macOS, you can add action buttons to a notification using the `addAction()` method. + +```php +Notification::title('Hello from NativePHP') + ->addAction('Click here') + ->show(); +``` + +You can call the `addAction()` method multiple times if you need to add multiple buttons. + +```php +Notification::title('Hello from NativePHP') + ->addAction('Button One') + ->addAction('Button Two') + ->show(); +``` + +When an action button is clicked, it will trigger the [`NotificationActionClicked`](#codenotificationactionclickedcode) event. + +This event contains an `$index` property, which refers to the index of the action button that was clicked. Action button indexes start at `0`: + +```php +use Native\Desktop\Events\Notifications\NotificationActionClicked; + +Notification::title('Do you accept?') + ->addAction('Accept') // This action will be $index = 0 + ->addAction('Decline') // This action will be $index = 1 + ->show(); + +Event::listen(NotificationActionClicked::class, function (NotificationActionClicked $event) { + if ($event->index === 0) { + // 'Accept' clicked + } elseif ($event->index === 1) { + // 'Decline' clicked + } +}); +``` + +## Events + +### `NotificationClicked` + +The `Native\Desktop\Events\Notifications\NotificationClicked` event is dispatched when a user clicks on a notification. + +### `NotificationClosed` + +The `Native\Desktop\Events\Notifications\NotificationClosed` event is dispatched when a user closes a notification. + +### `NotificationReply` + +The `Native\Desktop\Events\Notifications\NotificationReply` event is dispatched when a user replies to a notification. + +### `NotificationActionClicked` + +The `Native\Desktop\Events\Notifications\NotificationActionClicked` event is dispatched when a user clicks an action button on a notification. diff --git a/resources/views/docs/desktop/2/the-basics/power-monitor.md b/resources/views/docs/desktop/2/the-basics/power-monitor.md new file mode 100644 index 00000000..e4ef6ac3 --- /dev/null +++ b/resources/views/docs/desktop/2/the-basics/power-monitor.md @@ -0,0 +1,99 @@ +--- +title: Power Monitor +order: 900 +--- + +The power monitor allows you to gather information about the power state of the device. + +## System Idle State + +You can check if the system is idle with the `getSystemIdleState` method. + +It expects a `int $threshold` argument, which is the number of seconds the system must be idle before it is considered idle. And it'll return an enum value of `SystemIdleStatesEnum`. + +```php +use Native\Desktop\Enums\SystemIdleStatesEnum; +use Native\Desktop\Facades\PowerMonitor; + +$state = PowerMonitor::getSystemIdleState(60); + +if ($state === SystemIdleStatesEnum::IDLE) { + // The system is idle! +} +``` + +The possible values for the `SystemIdleStatesEnum` enum are: + +- `SystemIdleStatesEnum::ACTIVE` +- `SystemIdleStatesEnum::IDLE` +- `SystemIdleStatesEnum::LOCKED` +- `SystemIdleStatesEnum::UNKNOWN` + +## System Idle Time + +You can get the number of seconds the system has been idle with the `getSystemIdleTime` method. + +```php +use Native\Desktop\Facades\PowerMonitor; + +$seconds = PowerMonitor::getSystemIdleTime(); +``` + +## Current Thermal State + +You can get the current thermal state of the system with the `getCurrentThermalState` method. It'll return an enum value of `ThermalStatesEnum`. + +```php +use Native\Desktop\Enums\ThermalStatesEnum; +use Native\Desktop\Facades\PowerMonitor; + +$thermalState = PowerMonitor::getCurrentThermalState(); + +if ($state === ThermalStatesEnum::CRITICAL) { + // Wow, the CPU is running hot! +} +``` + +The possible values for the `ThermalStatesEnum` enum are: + +- `ThermalStatesEnum::UNKNOWN` +- `ThermalStatesEnum::NOMINAL` +- `ThermalStatesEnum::FAIR` +- `ThermalStatesEnum::SERIOUS` +- `ThermalStatesEnum::CRITICAL` + +## Battery Information + +You can determine if the device is running on battery power or AC power with the `isOnBatteryPower` method. + +```php +use Native\Desktop\Facades\PowerMonitor; + +if (PowerMonitor::isOnBatteryPower()) { + // The device is running on battery power. +} else { + // The device is running on AC power. +} +``` + +## Events + +You can listen to the following events to get handle when the system's power state changes: + +### `PowerStateChanged` + +This `Native\Desktop\Events\PowerStateChanged` event is fired whenever the power state of the system changes. For example, when the system goes from battery power to AC power, or vice versa. + +The event contains a public `$state` property which is an enum value of `Native\Desktop\Enums\PowerStatesEnum`. + +### `SpeedLimitChanged` + +This `Native\Desktop\Events\SpeedLimitChanged` event is fired whenever the CPU speed limit changes, usually due to thermal throttling or low battery. + +The event contains a public `$limit` property which is the percentage of the maximum CPU speed that is currently allowed. + +### `ThermalStateChanged` + +The `Native\Desktop\Events\ThermalStateChanged` event is fired whenever the thermal state of the system changes. + +The event contains a public `$state` property which is an enum value of `Native\Desktop\Enums\ThermalStatesEnum`. diff --git a/resources/views/docs/desktop/2/the-basics/screens.md b/resources/views/docs/desktop/2/the-basics/screens.md new file mode 100644 index 00000000..7de5b373 --- /dev/null +++ b/resources/views/docs/desktop/2/the-basics/screens.md @@ -0,0 +1,70 @@ +--- +title: Screens +order: 850 +--- + +The `Screen` facade lets you get information about the screens currently connected to the computer. + +```php +use Native\Desktop\Facades\Screen; +``` + +## Displays + +The `displays` method gives you an array of information about all the displays actually used. + +The Display object represents a physical display connected to the system. A fake display may exist on a headless system, or a display may correspond to a remote, virtual display. If you use an external display with your laptop screen closed, the internal screen of your laptop will not be part of the array. + +See [Display object](https://www.electronjs.org/docs/latest/api/structures/display) documentation. + +```php +$screens = Screen::displays(); + +[ + 0 => [ + 'bounds' => [ + 'x' => 0, + 'y' => 0, + 'width' => 2560, + 'height' => 1440, + ], + 'detected' => true, + 'id' => 2026675401, + 'internal' => false, + 'label' => 'U3277WB', + 'size' => [ + 'width' => 2560, + 'height' => 1440, + ], + 'workArea' => [ + 'x' => 0, + 'y' => 25, + 'width' => 2560, + 'height' => 1345, + ], + // ... + ], + // ... +] +``` + +The screen bounds are the desktop area that the screen covers. The `x` and `y` values are the top-left corner of the screen relative to the primary display, and the `width` and `height` values are the width and height of the screen. + +## Cursor position + +The `cursorPosition` method gives you the coordinates of the current absolute position of the mouse cursor. + +```php +$position = Screen::cursorPosition(); + +(object) [ + 'x' => 627, + 'y' => 168, +] +``` + +The position of the cursor is relative to the top-left corner of the primary display. These values can be +negative as well as positive. + +For example, a secondary display may be oriented by your system to the right of your primary display. +If your mouse cursor is on the secondary display when calling `Screen::cursorPosition()`, the `x` value will be a negative integer. diff --git a/resources/views/docs/desktop/2/the-basics/settings.md b/resources/views/docs/desktop/2/the-basics/settings.md new file mode 100644 index 00000000..72434759 --- /dev/null +++ b/resources/views/docs/desktop/2/the-basics/settings.md @@ -0,0 +1,93 @@ +--- +title: Settings +order: 450 +--- + +## Storing Settings + +NativePHP offers an easy method to store and retrieve settings in your application. This is helpful for saving application-wide +settings that persist even after closing and reopening the application. + +Settings are managed using the `Settings` facade and are stored in a file named `config.json` in the +[`appdata`](/docs/getting-started/debugging#start-from-scratch) directory of your application. + +```php +use Native\Desktop\Facades\Settings; +``` + +### Setting a value + +It's as simple as calling the `set` method. The key must be a string. + +```php +Settings::set('key', 'value'); +``` + +### Getting a value + +To retrieve a setting, use the `get` method. + +```php +$value = Settings::get('key'); +``` + +You may also provide a default value to return if the setting does not exist. You can provide either a static default value, or a closure: + +```php +$value = Settings::get('key', 'default'); +``` + +```php +$value = Settings::get('key', function () { + return 'default'; + }); +``` + +If the setting does not exist, and no default value is provided, `null` will be returned. + +### Forgetting a value + +If you want to remove a setting altogether, use the `forget` method. + +```php +Settings::forget('key'); +``` + +### Clearing all settings + +To remove all settings, use the `clear` method. + +```php +Settings::clear(); +``` + +This will remove all settings from the `config.json` file. + +## Events + +### `SettingChanged` + +The `Native\Desktop\Events\Notifications\SettingChanged` event is dispatched when a setting is changed. + +Example usage: + +```php +Event::listen(SettingChanged::class, function (SettingChanged $event) { + $key = $event->key; // Key of the setting that was changed + $value = $event->value; // New value of the setting +}); +``` + +This event can also be listened with Livewire to refresh your settings page: + +```php +use Livewire\Component; +use Native\Desktop\Events\Notifications\SettingChanged; + +class Settings extends Component +{ + protected $listeners = [ + 'native:'.SettingChanged::class => '$refresh', + ]; +} +``` diff --git a/resources/views/docs/desktop/2/the-basics/shell.md b/resources/views/docs/desktop/2/the-basics/shell.md new file mode 100644 index 00000000..2a3c6ee0 --- /dev/null +++ b/resources/views/docs/desktop/2/the-basics/shell.md @@ -0,0 +1,51 @@ +--- +title: Shell +order: 850 +--- + +## Shell operations + +The `Shell` facade lets you perform some basic operations with files on the user's system in the context of the system's +default behaviour. + +To use the `Shell` facade, add the following to the top of your file: + +```php +use Native\Desktop\Facades\Shell; +``` + +### Showing a file + +The `showInFolder` method will attempt to open the given `$path` in the user's default file manager, e.g. File Explorer, +Finder etc. + +```php +Shell::showInFolder($path); +``` + +### Opening a file + +The `openFile` method will attempt to open the given `$path` using the default application associated with that file +type. If it was successful, this method will return an empty string (`""`); if unsuccessful, the returned string may +contain an error message. + +```php +$result = Shell::openFile($path); +``` + +### Trashing a file + +The `trashFile` method will attempt to send the given `$path` to the system's trash. + +```php +Shell::trashFile($path); +``` + +### Open a URL + +The `openExternal` method will attempt to open the given `$url` using the default handler registered for that URL's +scheme on the system's, e.g. the `http` and `https` schemes will most likely open the user's default web browser. + +```php +Shell::openExternal($url); +``` diff --git a/resources/views/docs/desktop/2/the-basics/system.md b/resources/views/docs/desktop/2/the-basics/system.md new file mode 100644 index 00000000..dd6e5e21 --- /dev/null +++ b/resources/views/docs/desktop/2/the-basics/system.md @@ -0,0 +1,219 @@ +--- +title: System +order: 800 +--- + +## The System + +One of the main advantages of building a native application is having more direct access to system resources, such as +peripherals connected to the physical device and APIs that aren't typically accessible inside a browser's sandbox. + +NativePHP makes it trivial to access these resources and APIs. + +One of the main challenges - particularly when writing cross-platform apps - is that each operating system has +its own set of available APIs, along with their own idiosyncrasies. + +NativePHP smooths over as much of this as possible, to offer a simple and consistent set of interfaces regardless of +the platform on which your app is running. + +While some features are platform-specific, NativePHP gracefully handles this for you so that you don't have to think +about whether something is Linux-, Mac-, or Windows-only. + +Most of the system-related features are available through the `System` facade. + +```php +use Native\Desktop\Facades\System; +``` + +## Encryption / Decryption + +Almost every non-trivial application will require some concept of secure data storage and retrieval. For example, if +you want to generate and store an API key to access a third-party service on behalf of your user. + +You shouldn't ship these sorts of secrets _with_ your app, but rather generate them or ask your user for them at +runtime. + +But when your app is running on a user's device, you have +[far less control and fewer guarantees](/docs/digging-deeper/security) over the safety of any secrets stored. + +On a traditional server-rendered application, this is a relatively simple problem to solve using server-side encryption +with keys which are hidden from end users. + +For this to work on the user's device, you need to be able to generate and store an encryption key securely. + +NativePHP takes care of the key generation and storage for you, all that's left for you to do is encrypt, store and +decrypt the secrets that you need to store on behalf of your user. + +NativePHP allows you to encrypt and decrypt data in your application easily: + +```php +if (System::canEncrypt()) { + $encrypted = System::encrypt('secret_key_a79hiunfw86...'); + + // $encrypted => 'djEwJo+Huv+aeBgUoav5nIJWRQ==' +} +``` + +You can then safely store the encrypted string in a database or the filesystem. + +When you need to get the original value, you can decrypt it: + +```php +if (System::canEncrypt()) { + $decrypted = System::decrypt('djEwJo+Huv+aeBgUoav5nIJWRQ=='); + + // $decrypted = 'secret_key_a79hiunfw86...' +} +``` + +## TouchID + +For Mac systems that support TouchID, you can use TouchID to protect and unlock various parts of your application. + +```php +if (System::canPromptTouchID() && System::promptTouchID('access your Contacts')) { + // Do your super secret activity here +} +``` + +You must pass a `string $reason` as the only argument to `System::promptTouchID`. This will show up in the dialog that +TouchID users are familiar with: + +![TouchID Prompt Example on macOS](/img/docs/touchid.png) + +Using this, you can gate certain parts of your app, or your _entire_ application, allowing you to offer an extra layer +of protection for your user's data. + +**Note: Despite the name, TouchID only gives you greater _confidence_ that the person using your app is the same as the +person who has unlocked the device your app is installed on. It does not allow you to _identify_ that user, nor does +it give you any special privileges to their system.** + +## Printing + +You can list all available printers: + +```blade +@@use(Native\Desktop\Facades\System) @@foreach(System::printers() as $printer) +@{{ $printer->displayName }} @@endforeach +``` + +Each item in the printers array is a `\Native\Desktop\DataObjects\Printer` which contains various device details and +default configuration. + +You can send some HTML to be printed like this: + +```php +System::print('...', $printer); +``` + +If no `$printer` object is provided, the default printer and settings will be used. + +You can also print directly to PDF: + +```php +System::printToPDF('...'); +``` + +This returns the PDF data in a `base64_encoded` binary string. So be sure to `base64_decode` it before storing it to +disk: + +```php +use Illuminate\Support\Facades\Storage; + +$pdf = System::printToPDF('...'); + +Storage::disk('desktop')->put('My Awesome File.pdf', base64_decode($pdf)); +``` + +### Print Settings + +You can change the configuration before sending something to be printed, for example if you want multiple copies: + +```php +$printer->options['copies'] = 5; + +System::print('...', $printer); +``` + +Additionally, both the `print()` and `printToPDF()` methods accept an optional `$settings` parameter that allows you to customize the print behavior: + +```php +System::print('...', $printer, $settings); +``` + +#### Print Settings Examples + +You can customize print behavior using the settings array. Here are some common examples: + +```php +// Print with custom page size and orientation +$settings = [ + 'pageSize' => 'A4', + 'landscape' => true, +]; + +System::print('...', $printer, $settings); +``` + +```php +// Print multiple copies with duplex +$settings = [ + 'copies' => 3, + 'duplexMode' => 'longEdge', // 'simplex', 'shortEdge', 'longEdge' + 'color' => false, // true for color, false for monochrome +]; + +System::print('...', $printer, $settings); +``` + +For a complete list of available print settings, refer to the [Electron webContents.print()](https://www.electronjs.org/docs/latest/api/web-contents#contentsprintoptions-callback) and [webContents.printToPDF()](https://www.electronjs.org/docs/latest/api/web-contents#contentsprinttopdfoptions) documentation. + +## Time Zones + +PHP and your Laravel application will generally be configured to work with a specific time zone. This could be UTC, for +example. + +But users of your application will think about time differently. Normally, the user's perspective of time is reflected +in their operating system's time zone setting. + +NativePHP includes a mechanism to translate cross-platform time zone identifiers to consistent identifiers that PHP +expects to use. + +You can use this to show dates and times in the appropriate time zone without having to ask your users to manually +select their current time zone. + +**Note: In some cases, this mechanism may not select the _exact_ time zone that the user is in. It uses an approximation +to simplify things, as there are many overlapping time zones and methods of naming them.** + +Using this approach, your app will be responsive to changes in the system's time zone settings, e.g. in case the +user moves between time zones. + +Get the current system time zone: + +```php +$timezone = System::timezone(); + +// $timezone => 'Europe/London' +``` + +## Theme + +NativePHP allows you to detect the current theme of the user's operating system. This is useful for applications that +want to adapt their UI to match the user's preferences. +You can use the `System::theme()` method to get the current theme of the user's operating system. + +```php +$theme = System::theme(); +// $theme => SystemThemesEnum::LIGHT, SystemThemesEnum::DARK or SystemThemesEnum::SYSTEM +``` + +You can also set the theme of your application using the `System::theme()` method. This will change the theme of your +application to the specified value. The available options are `SystemThemesEnum::LIGHT`, `SystemThemesEnum::DARK` and +`SystemThemesEnum::SYSTEM`. + +```php +System::theme(SystemThemesEnum::DARK); +``` + +Setting the theme to `SystemThemesEnum::SYSTEM` will remove the override and everything will be reset to the OS default. +By default themeSource is `SystemThemesEnum::SYSTEM`. diff --git a/resources/views/docs/desktop/2/the-basics/windows.md b/resources/views/docs/desktop/2/the-basics/windows.md new file mode 100644 index 00000000..05ffc03a --- /dev/null +++ b/resources/views/docs/desktop/2/the-basics/windows.md @@ -0,0 +1,565 @@ +--- +title: Windows +order: 100 +--- + +## Working with Windows + +NativePHP allows you to open native application Windows. +While this usually happens in your `NativeAppServiceProvider`, you are free to open a window anywhere in your application. + +### Opening Windows + +To open a window, you may use the `Window` facade. + +```php +namespace App\Providers; + +use Native\Desktop\Facades\Window; + +class NativeAppServiceProvider +{ + public function boot(): void + { + Window::open() + ->width(800) + ->height(800); + } +} +``` + +When opening a window, NativePHP will automatically open the root URL of your application. +You may pass a unique identifier to the `open()` method to distinguish between multiple windows. + +The default ID, if none is specified, is `main`. + +You can use the ID to reference the window in other methods, such as `Window::close()` or `Window::resize()`. + +### Closing Windows + +To close a window, you may use the `Window::close()` method. + +You may pass a unique identifier to the `close()` method to specify which window to close. + +If you do not specify a window ID, NativePHP will try to detect the window ID automatically based on the current route. + +```php +Window::close(); +Window::close('settings'); +``` + +### Resizing Windows + +You may use the `Window::resize()` method to resize a window. This method accepts a width and height as its first and second arguments, respectively. + +You may pass a unique identifier to the `resize()` method to specify which window to resize. + +If you do not specify a window ID, NativePHP will try to detect the window ID automatically based on the current route. + +```php +Window::resize(400, 300); + +Window::resize(400, 300, 'settings'); +``` + +### Minimizing and Maximizing + +There are convenience methods that allow you to minimize and maximize windows. + +#### Minimize a Window + +To minimize a window, you may use the `Window::minimize()` method. + +You may pass the window ID to the `minimize()` method to specify which window to minimize. + +If you do not specify a window ID, NativePHP will try to detect the window ID automatically based on the current route. + +```php +Window::open('secondary'); + +// Later... + +Window::minimize('secondary'); +``` + +#### Maximize a Window + +To maximize a window, you may use the `Window::maximize()` method. + +You may pass the window ID to the `maximize()` method to specify which window to maximize. + +If you do not specify a window ID, NativePHP will try to detect the window ID automatically based on the current route. + +```php +Window::open('secondary'); + +// Later... + +Window::maximize('secondary'); +``` + +Of course, you may also wish to open windows in a minimized or maximized state. You can achieve this simply by chaining the +`minimized()` and `maximized()` methods to your `Window::open()` call: + +```php +Window::open() + ->maximized(); +``` + +### Changing the URL + +While the URL in a window will change based on user activity, your Laravel routes and the flow of your application, +sometimes it may be useful to change the URL of a window outside of the usual typical request-response cycle of PHP. + +For this, you can use the `url()` method: + +```php +Window::open('secondary'); + +// Later... + +Window::get('secondary')->url(route('home')); +``` + +A case where this may be useful is when handling [Event-based menu item](/docs/the-basics/application-menu#event-based-menu-items) +or in response to some output from a queued job, scheduled task or [Child Process](/docs/digging-deeper/child-process). + +### Changing the Window Title + +To change a window's Title, you may use the `title()` method: + +```php +Window::open('secondary'); + +// Later... + +Window::get('secondary')->title('Mmmm... delicious!'); +``` + +### Retrieving the Current Window + +You may use the `Window::current()` method to retrieve the currently focused window. +This method returns an object with the following properties: + +- `id`: The ID of the window. +- `title`: The title of the window. +- `width`: The width of the window. +- `height`: The height of the window. +- `x`: The x position of the window. +- `y`: The y position of the window. +- `alwaysOnTop`: Whether the window is always on top. + +```php +$currentWindow = Window::current(); +``` + +### Retrieving all Windows + +You may use the `Window::all()` method to retrieve all open windows. + +```php +foreach (Window::all() as $window) { + $window->url(route('home')); +} +``` + +## Managing Multiple Windows + +If you would like to open multiple windows, you may use the `Window::open()` method multiple times. +In order to distinguish between the individual windows, you may pass a unique identifier to the `open()` method. + +If you do not specify an ID, NativePHP will automatically use `main` as the ID. + +This ID can be used to reference the window in other methods, such as `Window::close()` or `Window::resize()`. + +```php +Window::open('home') + ->width(800) + ->height(800); + +Window::open('settings') + ->route('settings') + ->width(800) + ->height(800); +``` + +## Configuring Windows + +### Window URLs + +By default, all calls to `Window::open()` will open up the root URL of your application. +If you would like to open a different URL, you may use the `route()` method to specify the route name to open. + +```php +Window::open() + ->route('home'); +``` + +You may also pass an absolute URL to the `url()` method: + +```php +Window::open() + ->url('https://google.com'); +``` + +### Window Titles + +By default, all calls to `Window::open()` will use the application name as the window title. +If you would like to use a different title, you may use the `title()` method to specify the window title to use. + +```php +Window::open() + ->title('My Window'); +``` + +### Window Sizes + +You may use the `width()` and `height()` methods to specify the size of the window. + +```php +Window::open() + ->width(800) + ->height(800); +``` + +If you want to constrain the window to a specific size, you may make use of the `minWidth()`, `minHeight()`, +`maxWidth()`, and `maxHeight()` methods. + +```php +Window::open() + ->minWidth(400) + ->minHeight(400) + ->maxWidth(800) + ->maxHeight(800); +``` + +### Window Position + +To specify the position of the window, you may use the `position($x, $y)` method. + +```php +Window::open() + ->position(100, 100); +``` + +### Remembering Window State + +The users of your application may resize or move the window and expect it to be in the same position and size the next +time they open it. NativePHP provides a simple way to manage the state of your window. You may use the `rememberState()` +method to instruct NativePHP to remember the state of the window. + +```php +Window::open() + ->rememberState(); +``` + +Please note that NativePHP only allows you to remember the state of one window at a time. + +### Resizable Windows + +By default, all windows created with the `Window` facade are resizable. +If you would like to disable resizing, you may use the `resizable()` method and pass `false` as the first argument. + +```php +Window::open() + ->resizable(false); +``` + +### Focusable Windows + +By default, all windows created with the `Window` facade are focusable by clicking on them. +You may use the `focusable()` method to disable focusing. + +```php +Window::open() + ->focusable(false); +``` + +### Movable Windows + +By default, all windows created with the `Window` facade are movable. + +You may use the `movable()` method to disable moving. + +```php +Window::open() + ->movable(false); +``` + +### Minimizable, Maximizable, and Closable Windows + +By default, all windows created with the `Window` facade are minimizable, maximizable, and closable. + +You may use the `minimizable()`, `maximizable()`, and `closable()` methods to disable these features. + +```php +Window::open() + ->minimizable(false) + ->maximizable(false) + ->closable(false); +``` + +### Full Screen Windows + +By default, all windows created with the `Window` facade are fullscreen-able, meaning that they can enter Full Screen Mode. + +You may use the `fullscreenable()` method to disable this feature. + +```php +Window::open()->fullscreenable(false); +``` + +If you wish, you may open a window in full screen mode using the `fullscreen()` method. + +```php +Window::open()->fullscreen(); +``` + +### Window Shadow + +By default, all windows created with the `Window` facade have a shadow. You may use the `hasShadow()` method to disable the shadow. + +```php +Window::open() + ->hasShadow(false); +``` + +### Windows on Top + +In some cases, you may want to make a window always on top of other windows. +When opening a window, you may use the `alwaysOnTop()` method to make the window always on top. + +```php +Window::open() + ->alwaysOnTop(); +``` + +If you would like to toggle the always on top state of a window, you may use the `alwaysOnTop()` method on the `Window` facade +directly and pass the window ID as the second argument. + +If you do not specify a window ID, NativePHP will try to detect the window ID automatically based on the current route. + +```php +Window::alwaysOnTop(true, 'settings'); +``` + +### Window Background Color + +By default, all windows created with the `Window` facade have a white background color. +This color is visible when resizing the window, right before the content is rendered. + +You may use the `backgroundColor()` method to change the background color of the window. +This method accepts a hex color code as its first argument. +You may also pass a hex color code with an alpha channel to make the background color semi-transparent. + +```php +Window::open() + ->backgroundColor('#00000050'); // Semi-transparent black +``` + +### Hiding the menu + +By default on Windows and Linux the application menu will be visible. +This method will hide the menu and have it reveal when the user presses ALT. + +```php +Window::open() + ->hideMenu(); +``` + +### Taskbar and Mission Control Visibility + +You may control whether a window appears in the taskbar and Mission Control. + +#### Skip Taskbar + +By default, all windows created with the `Window` facade will appear in the taskbar on Windows and macOS. +You may use the `skipTaskbar()` method to prevent a window from appearing in the taskbar. + +```php +Window::open() + ->skipTaskbar(); +``` + +This is useful for utility windows, floating toolboxes, or background windows that should not clutter the taskbar. + +#### Hidden in Mission Control + +On macOS, all windows will appear in Mission Control by default. +You may use the `hiddenInMissionControl()` method to prevent a window from appearing when the user toggles into Mission Control. + +```php +Window::open() + ->hiddenInMissionControl(); +``` + +This is particularly useful for always-on-top utility windows or menubar applications that should not be visible in Mission Control. + +### Restrict navigation within a window + +When opening windows that display content that is not under your control (such as external websites), you may want to +restrict the user's navigation options. NativePHP provides two handy methods for this on the `Window` facade: + +```php +Window::open() + ->url('https://nativephp.com/') + ->preventLeaveDomain(); + +Window::open() + ->url('https://laravel-news.com/bifrost') + ->preventLeavePage(); +``` + +The `preventLeaveDomain()` method allows navigation within the same domain but blocks any attempt to navigate away to a +different domain, scheme or port. + +With `preventLeavePage()` you can strictly confine the user to the initially rendered page. Any attempt to navigate to a +different path (even within the same domain) will be blocked. However, in-page navigation via anchors (e.g. "#section") +and updates to the query string remain permitted. + +#### Preventing new windows from popping up + +By default, Electron allows additional windows to be opened from a window that was previously opened programmatically. +This is the case, for example, with `a` elements that have the target attribute set to `_blank` or when the user clicks on a link with the middle mouse button. +This behaviour is potentially undesirable in a desktop application, as it enables the user to "break out" of a window. + +To prevent additional windows from opening, you can apply the `suppressNewWindows()` method when opening a new window. + +```php +Window::open() + ->suppressNewWindows(); +``` + +### Zoom factor + +In certain cases, you may want to set a zoom factor for a window. +This can be particularly helpful in cases where you have no control over the content displayed (e.g. when showing external websites). +You may use the `zoomFactor` method to define a zoom factor. + +```php +Window::open() + ->zoomFactor(1.25); +``` + +The zoom factor is the zoom percent divided by 100. +This means that you need to pass the value `1.25` if you want the window to be displayed at 125% size. + +## Window Title Styles + +### Default Title Style + +By default, all windows created with the `Window` facade show their title in the center of the title bar. + +### Hidden Title Style + +You may use the `titleBarHidden()` method to hide the title bar of a window. + +```php +Window::open() + ->titleBarHidden(); +``` + +When using this style, you may want to add a custom title bar to the window yourself via HTML/JS. + +In order to keep the window draggable, you should add an HTML element with the following CSS attributes: + +```html +
+ +
+``` + +### Hidden Traffic Lights (macOS only) + +On macOS, you may use the `trafficLightsHidden()` method to hide the window control buttons (red, yellow, green) while keeping the title bar visible. + +```php +Window::open() + ->trafficLightsHidden(); +``` + +This is useful when you want to create a custom window appearance on macOS while maintaining the title bar for dragging. Unlike `titleBarHidden()` which hides the entire title bar, this method only hides the control buttons. + +### Setting webpage features + +You may control various web page features by passing an optional `webPreferences` configuration array. This allows you to customize how the Electron window behaves and what features are available to the web content. + +```php +Window::open() + ->webPreferences([ + 'nodeIntegration' => true, + 'spellcheck' => true, + 'backgroundThrottling' => true, + ]); +``` + +#### Default Settings + +NativePHP sets the following defaults for security and compatibility: + +```php +[ + 'sandbox' => false, // locked + 'preload' => '{preload-path}', // locked + 'contextIsolation' => true, // locked + 'spellcheck' => false, + 'nodeIntegration' => false, + 'backgroundThrottling' => false, +] +``` + +The `preload`, `contextIsolation`, and `sandbox` options are locked and cannot be changed for security reasons. All other options from the [Electron WebPreferences documentation](https://www.electronjs.org/docs/latest/api/structures/web-preferences) may be passed and will override the defaults. + +## Events + +NativePHP provides a simple way to listen for native window events. +All events get dispatched as regular Laravel events, so you may use your `EventServiceProvider` to register listeners. + +```php +protected $listen = [ + 'Native\Desktop\Events\Windows\WindowShown' => [ + 'App\Listeners\WindowWasShownListener', + ], + // ... +]; +``` + +Sometimes you may want to listen and react to window events in real-time, which is why NativePHP also broadcasts all +window events to the `nativephp` broadcast channel. + +To learn more about NativePHP's broadcasting capabilities, please refer to the [Broadcasting](/docs/digging-deeper/broadcasting) section. + +### `WindowShown` + +The `Native\Desktop\Events\Windows\WindowShown` event will be dispatched when a window is shown to the user. +The payload of this event contains the window ID. + +### `WindowClosed` + +The `Native\Desktop\Events\Windows\WindowClosed` event will be dispatched when a window is closed. +The payload of this event contains the window ID. + +### `WindowFocused` + +The `Native\Desktop\Events\Windows\WindowFocused` event will be dispatched when a window is focused. +The payload of this event contains the window ID. + +### `WindowBlurred` + +The `Native\Desktop\Events\Windows\WindowBlurred` event will be dispatched when a window is blurred. +The payload of this event contains the window ID. + +### `WindowMinimized` + +The `Native\Desktop\Events\Windows\WindowMinimized` event will be dispatched when a window is minimized. +The payload of this event contains the window ID. + +### `WindowMaximized` + +The `Native\Desktop\Events\Windows\WindowMaximized` event will be dispatched when a window is maximized. +The payload of this event contains the window ID. + +### `WindowResized` + +The `Native\Desktop\Events\Windows\WindowResized` event will be dispatched after a window has been resized. +The payload of this event contains the window ID and the new window `$width` and `$height`. diff --git a/resources/views/docs/index.blade.php b/resources/views/docs/index.blade.php index 674ca1a2..286513ff 100644 --- a/resources/views/docs/index.blade.php +++ b/resources/views/docs/index.blade.php @@ -1,60 +1,161 @@ - +@push('head') + + +@endpush + + {!! $navigation !!} - + {{-- Version switcher --}} + @if($platform === 'desktop') + + @elseif($platform === 'mobile') + + @endif + + + {{-- Ad rotation --}} + + -

- {{$title}} + + +

+ {{ $title }}

- + + + {{-- Table of contents --}} +
+ + {{-- Version switcher --}} + @if($platform === 'desktop') + + @elseif($platform === 'mobile') + + @endif - + {{-- Copy as Markdown Button --}} + + +
@if (count($tableOfContents) > 0) - +
+
+ + + + On this page + + + + + + +
+
@endif -
+
{!! $content !!}
- + - @php $linkAlign = $previousPage === null ? 'right' : 'between'; @endphp - - @if($previousPage !== null) - - Previous page: - - {{ $previousPage['title'] }} - - @endif - @if($nextPage !== null) - - Next page: - {{ $nextPage['title'] }} - - + @php + $linkAlign = $previousPage === null ? 'right' : 'between'; + @endphp + + + @if ($previousPage !== null) + +
+
+ +
Previous
+
+
{{ $previousPage['title'] }}
+
+
@endif -
- + @if ($nextPage !== null) + +
+
+
Next
+ +
+
{{ $nextPage['title'] }}
+
+
+ @endif + -
- +
+ Edit this page on GitHub - +
- + {{-- Mobile ad rotation --}} + + diff --git a/resources/views/docs/mobile/1/_index.md b/resources/views/docs/mobile/1/_index.md new file mode 100644 index 00000000..c719fbe2 --- /dev/null +++ b/resources/views/docs/mobile/1/_index.md @@ -0,0 +1,4 @@ +--- +title: Mobile +order: 1 +--- diff --git a/resources/views/docs/mobile/1/apis/_index.md b/resources/views/docs/mobile/1/apis/_index.md new file mode 100644 index 00000000..31293365 --- /dev/null +++ b/resources/views/docs/mobile/1/apis/_index.md @@ -0,0 +1,4 @@ +--- +title: APIs +order: 4 +--- diff --git a/resources/views/docs/mobile/1/apis/biometrics.md b/resources/views/docs/mobile/1/apis/biometrics.md new file mode 100644 index 00000000..9a9e72b6 --- /dev/null +++ b/resources/views/docs/mobile/1/apis/biometrics.md @@ -0,0 +1,61 @@ +--- +title: Biometrics +order: 100 +--- + +## Overview + +The Biometrics API allows you to authenticate users using their device's biometric sensors like Face ID, Touch ID, or +fingerprint scanners. + +```php +use Native\Mobile\Facades\Biometrics; +``` + +## Methods + +### `prompt()` + +Prompts the user for biometric authentication. + +```php +use Native\Mobile\Facades\Biometrics; + +Biometrics::prompt(); +``` + +## Events + +### `Completed` + +Fired when biometric authentication completes (success or failure). + +```php +use Livewire\Attributes\On; +use Native\Mobile\Events\Biometric\Completed; + +#[On('native:'.Completed::class)] +public function handle(bool $success) +{ + if ($success) { + // User authenticated successfully + $this->unlockSecureFeature(); + } else { + // Authentication failed + $this->showErrorMessage(); + } +} +``` + +## Platform Support + +- **iOS:** Face ID, Touch ID +- **Android:** Fingerprint, Face unlock, other biometric methods +- **Fallback:** System authentication (PIN, password, pattern) + +## Security Notes + +- Biometric authentication provides **convenience**, not absolute security +- Always combine with other authentication factors for sensitive operations +- Consider implementing session timeouts for unlocked states +- Users can potentially bypass biometrics if their device is compromised diff --git a/resources/views/docs/mobile/1/apis/browser.md b/resources/views/docs/mobile/1/apis/browser.md new file mode 100644 index 00000000..246ef73d --- /dev/null +++ b/resources/views/docs/mobile/1/apis/browser.md @@ -0,0 +1,58 @@ +--- +title: Browser +order: 200 +--- + +## Overview + +The Browser API provides three methods for opening URLs, each designed for specific use cases: +in-app browsing, system browser navigation, and web authentication flows. + +```php +use Native\Mobile\Facades\Browser; +``` + +## Methods + +### `inApp()` + +Opens a URL in an embedded browser within your app using Custom Tabs (Android) or SFSafariViewController (iOS). + +```php +Browser::inApp('https://nativephp.com/mobile'); +``` + +### `open()` + +Opens a URL in the device's default browser app, leaving your application entirely. + +```php +Browser::open('https://nativephp.com/mobile'); +``` + +### `auth()` + +Opens a URL in a specialized authentication browser designed for OAuth flows with automatic `nativephp://` redirect handling. + +```php +Browser::auth('https://provider.com/oauth/authorize?client_id=123&redirect_uri=nativephp://127.0.0.1/auth/callback'); +``` + +## Use Cases + +### When to Use Each Method + +**`inApp()`** - Keep users within your app experience: +- Documentation, help pages, terms of service +- External content that relates to your app +- When you want users to easily return to your app + +**`open()`** - Full browser experience needed: +- Complex web applications +- Content requiring specific browser features +- When users need bookmarking or sharing capabilities + +**`auth()`** - OAuth authentication flows: +- Login with WorkOS, Auth0, Google, Facebook, etc. +- Secure authentication with automatic redirects +- Isolated browser session for security diff --git a/resources/views/docs/mobile/1/apis/camera.md b/resources/views/docs/mobile/1/apis/camera.md new file mode 100644 index 00000000..179a94fc --- /dev/null +++ b/resources/views/docs/mobile/1/apis/camera.md @@ -0,0 +1,90 @@ +--- +title: Camera +order: 300 +--- + +## Overview + +The Camera API provides access to the device's camera for taking photos and selecting images from the gallery. + +```php +use Native\Mobile\Facades\Camera; +``` + +## Methods + +### `getPhoto()` + +Opens the camera interface to take a photo. + +```php +Camera::getPhoto(); +``` + +### `pickImages()` + +Opens the gallery/photo picker to select existing images. + +**Parameters:** +- `string $media_type` - Type of media to pick: `'all'`, `'images'`, `'videos'` (default: `'all'`) +- `bool $multiple` - Allow multiple selection (default: `false`) + +**Returns:** `bool` - `true` if picker opened successfully + +```php +// Pick a single image +Camera::pickImages('images', false); + +// Pick multiple images +Camera::pickImages('images', true); + +// Pick any media type +Camera::pickImages('all', true); +``` + +## Events + +### `PhotoTaken` + +Fired when a photo is taken with the camera. + +**Payload:** `string $path` - File path to the captured photo + +```php +use Livewire\Attributes\On; +use Native\Mobile\Events\Camera\PhotoTaken; + +#[On('native:'.PhotoTaken::class)] +public function handlePhotoTaken(string $path) +{ + // Process the captured photo + $this->processPhoto($path); +} +``` + +### `MediaSelected` + +Fired when media is selected from the gallery. + +**Payload:** `array $media` - Array of selected media items + +```php +use Livewire\Attributes\On; +use Native\Mobile\Events\Gallery\MediaSelected; + +#[On('native:'.MediaSelected::class)] +public function handleMediaSelected($success, $files, $count) +{ + foreach ($files as $file) { + // Process each selected media item + $this->processMedia($file); + } +} +``` + +## Notes + +- The first time your app requests camera access, users will be prompted for permission +- If permission is denied, camera functions will fail silently +- Captured photos are stored in the app's temporary directory +- File formats are platform-dependent (typically JPEG) diff --git a/resources/views/docs/mobile/1/apis/device.md b/resources/views/docs/mobile/1/apis/device.md new file mode 100644 index 00000000..a9931d3a --- /dev/null +++ b/resources/views/docs/mobile/1/apis/device.md @@ -0,0 +1,55 @@ +--- +title: Device +order: 400 +--- + +## Overview + +The Device API exposes internal information about the device, such as the model and operating system version, along with user information such as unique ids. +```php +use Native\Mobile\Facades\Device; +``` + +## Methods + +### `getId()` + +Return a unique identifier for the device. + +Returns: `string` + +### `getInfo()` + +Return information about the underlying device/os/platform. + +Returns JSON encoded: `string` + + +### `getBatteryInfo()` + +Return information about the battery. + +Returns JSON encoded: `string` + +## Device Info + +| Prop | Type | Description +|---|---|---|---| +| name | string | The name of the device. For example, "John's iPhone". On iOS 16+ this will return a generic device name without the appropriate entitlements. +| model | string | The device model. For example, "iPhone13,4". +| platform | 'ios' \| 'android' | The device platform (lowercase). +| operatingSystem | string | The operating system of the device. +| osVersion | string | The version of the device OS. +| iOSVersion | number | The iOS version number. Only available on iOS. Multi-part version numbers are crushed down into an integer padded to two-digits, e.g., "16.3.1" → `160301`. | 5.0.0 | +| androidSDKVersion | number | The Android SDK version number. Only available on Android. | 5.0.0 | +| manufacturer | string | The manufacturer of the device. +| isVirtual | boolean | Whether the app is running in a simulator/emulator. +| memUsed | number | Approximate memory used by the current app, in bytes. Divide by 1,048,576 to get MBs used. +| webViewVersion | string | The web view browser version. + +## Battery Info + +| Prop | Type | Description +|---|---|---|---| +| batteryLevel | number | A percentage (0 to 1) indicating how much the battery is charged. +| isCharging | boolean | Whether the device is charging. diff --git a/resources/views/docs/mobile/1/apis/dialog.md b/resources/views/docs/mobile/1/apis/dialog.md new file mode 100644 index 00000000..5002f16a --- /dev/null +++ b/resources/views/docs/mobile/1/apis/dialog.md @@ -0,0 +1,103 @@ +--- +title: Dialog +order: 500 +--- + +## Overview + +The Dialog API provides access to native UI elements like alerts, toasts, and sharing interfaces. + +```php +use Native\Mobile\Facades\Dialog; +``` + +## Methods + +### `alert()` + +Displays a native alert dialog with customizable buttons. + +**Parameters:** +- `string $title` - The alert title +- `string $message` - The alert message +- `array $buttons` - Array of button labels (max 3 buttons) + +**Button Positioning:** +- **1 button** - Positive (OK/Confirm) +- **2 buttons** - Negative (Cancel) + Positive (OK/Confirm) +- **3 buttons** - Negative (Cancel) + Neutral (Maybe) + Positive (OK/Confirm) + +```php +Dialog::alert( + 'Confirm Action', + 'Are you sure you want to delete this item?', + ['Cancel', 'Delete'] +); +``` + +### `toast()` + +Displays a brief toast notification message. + + +**Parameters:** +- `string $message` - The message to display + +```php +Dialog::toast('Item saved successfully!'); +``` + +#### Good toast messages + +- Short and clear +- Great for confirmations and status updates +- Don't rely on them for critical information +- Avoid showing multiple toasts in quick succession + +### `share()` + +Opens the native sharing interface. + +**Parameters:** +- `string $title` - The share dialog title +- `string $text` - Text content to share +- `string $url` - URL to share + +```php +Dialog::share( + 'Check this out!', + 'I found this amazing Laravel package for mobile development', + 'https://nativephp.com' +); +``` + +## Events + +### `ButtonPressed` + +Fired when a button is pressed in an alert dialog. + +**Payload:** +- `int $index` - Index of the pressed button (0-based) +- `string $label` - Label/text of the pressed button + +```php +use Livewire\Attributes\On; +use Native\Mobile\Events\Alert\ButtonPressed; + +#[On('native:'.ButtonPressed::class)] +public function handleAlertButton($index, $label) +{ + switch ($index) { + case 0: + // First button (usually Cancel) + Dialog::toast("You pressed '{$label}'"); + break; + case 1: + // Second button (usually OK/Confirm) + $this->performAction(); + Dialog::toast("You pressed '{$label}'"); + break; + } +} +``` diff --git a/resources/views/docs/mobile/1/apis/geolocation.md b/resources/views/docs/mobile/1/apis/geolocation.md new file mode 100644 index 00000000..18bdbae7 --- /dev/null +++ b/resources/views/docs/mobile/1/apis/geolocation.md @@ -0,0 +1,155 @@ +--- +title: Geolocation +order: 600 +--- + +## Overview + +The Geolocation API provides access to the device's GPS and location services to determine the user's current position. + +```php +use Native\Mobile\Facades\Geolocation; +``` + +## Methods + +### `getCurrentPosition()` + +Gets the current GPS location of the device. + +**Parameters:** +- `bool $fineAccuracy` - Whether to use high accuracy mode (GPS vs network) (default: `false`) + +**Returns:** Location data via events + +```php +// Get location using network positioning (faster, less accurate) +Geolocation::getCurrentPosition(); + +// Get location using GPS (slower, more accurate) +Geolocation::getCurrentPosition(true); +``` + +### `checkPermissions()` + +Checks the current location permissions status. + +**Returns:** Permission status via events + +```php +Geolocation::checkPermissions(); +``` + +### `requestPermissions()` + +Requests location permissions from the user. + +**Returns:** Permission status after request via events + +```php +Geolocation::requestPermissions(); +``` + +## Events + +### `LocationReceived` + +Fired when location data is requested (success or failure). + +**Event Parameters:** +- `bool $success` - Whether location was successfully retrieved +- `float $latitude` - Latitude coordinate (when successful) +- `float $longitude` - Longitude coordinate (when successful) +- `float $accuracy` - Accuracy in meters (when successful) +- `int $timestamp` - Unix timestamp of location fix +- `string $provider` - Location provider used (GPS, network, etc.) +- `string $error` - Error message (when unsuccessful) + +```php +use Livewire\Attributes\On; +use Native\Mobile\Events\Geolocation\LocationReceived; + +#[On('native:'.LocationReceived::class)] +public function handleLocationReceived( + $success = null, + $latitude = null, + $longitude = null, + $accuracy = null, + $timestamp = null, + $provider = null, + $error = null +) { + // ... +} +``` + +### `PermissionStatusReceived` + +Fired when permission status is checked. + +**Event Parameters:** +- `string $location` - Overall location permission status +- `string $coarseLocation` - Coarse location permission status +- `string $fineLocation` - Fine location permission status + +**Permission Values:** +- `'granted'` - Permission is granted +- `'denied'` - Permission is denied +- `'not_determined'` - Permission not yet requested + +```php +use Livewire\Attributes\On; +use Native\Mobile\Events\Geolocation\PermissionStatusReceived; + +#[On('native:'.PermissionStatusReceived::class)] +public function handlePermissionStatus($location, $coarseLocation, $fineLocation) +{ + // ... +} +``` + +### `PermissionRequestResult` + +Fired when a permission request completes. + +**Event Parameters:** +- `string $location` - Overall location permission result +- `string $coarseLocation` - Coarse location permission result +- `string $fineLocation` - Fine location permission result +- `string $message` - Optional message (for permanently denied) +- `bool $needsSettings` - Whether user needs to go to Settings + +**Special Values:** +- `'permanently_denied'` - User has permanently denied permission + +```php +use Livewire\Attributes\On; +use Native\Mobile\Events\Geolocation\PermissionRequestResult; + +#[On('native:' . PermissionRequestResult::class)] +public function handlePermissionRequest($location, $coarseLocation, $fineLocation, $message = null, $needsSettings = null) +{ + if ($location === 'permanently_denied') { + $this->error = 'Location permission permanently denied. Please enable in Settings.'; + } elseif ($coarseLocation === 'granted' || $fineLocation === 'granted') { + $this->getCurrentLocation(); + } else { + $this->error = 'Location permission is required for this feature.'; + } +} +``` + +## Privacy Considerations + +- **Explain why** you need location access before requesting +- **Request at the right time** - when the feature is actually needed +- **Respect denials** - provide alternative functionality when possible +- **Use appropriate accuracy** - don't request fine location if coarse is sufficient +- **Limit frequency** - don't request location updates constantly + +### Performance Considerations +- **Battery Usage** - GPS uses more battery than network location +- **Time to Fix** - GPS takes longer for initial position +- **Indoor Accuracy** - GPS may not work well indoors +- **Caching** - Consider caching recent locations for better UX + diff --git a/resources/views/docs/mobile/1/apis/haptics.md b/resources/views/docs/mobile/1/apis/haptics.md new file mode 100644 index 00000000..3ddfdc2c --- /dev/null +++ b/resources/views/docs/mobile/1/apis/haptics.md @@ -0,0 +1,29 @@ +--- +title: Haptics +order: 700 +--- + +## Overview + +The Haptics API provides access to the device's vibration and haptic feedback system for tactile user interactions. + +```php +use Native\Mobile\Facades\Haptics; +``` + +## Methods + +### `vibrate()` + +Triggers device vibration for tactile feedback. + +**Returns:** `void` + +```php +Haptics::vibrate(); +``` + +**Use haptics for:** Button presses, form validation, important notifications, game events. + +**Avoid haptics for:** Frequent events, background processes, minor updates. + diff --git a/resources/views/docs/mobile/1/apis/push-notifications.md b/resources/views/docs/mobile/1/apis/push-notifications.md new file mode 100644 index 00000000..087abc2f --- /dev/null +++ b/resources/views/docs/mobile/1/apis/push-notifications.md @@ -0,0 +1,63 @@ +--- +title: PushNotifications +order: 800 +--- + +## Overview + +The PushNotifications API handles device registration for Firebase Cloud Messaging to receive push notifications. + +```php +use Native\Mobile\Facades\PushNotifications; +``` + +## Methods + +### `enroll()` + +Requests permission and enrolls the device for push notifications. + +**Returns:** `void` + +### `getToken()` + +Retrieves the current push notification token for this device. + +**Returns:** `string|null` - The FCM token, or `null` if not available + +## Events + +### `TokenGenerated` + +Fired when a push notification token is successfully generated. + +**Payload:** `string $token` - The FCM token for this device + +```php +use Livewire\Attributes\On; +use Native\Mobile\Events\PushNotification\TokenGenerated; + +#[On('native:'.TokenGenerated::class)] +public function handlePushToken(string $token) +{ + // Send token to your backend + $this->sendTokenToServer($token); +} +``` + +## Permission Flow + +1. User taps "Enable Notifications" +2. App calls `enroll()` +3. System shows permission dialog +4. If granted, FCM generates token +5. `TokenGenerated` event fires with token +6. App sends token to backend +7. Backend stores token for user +8. Server can now send notifications to this device + +## Best Practices + +- Request permission at the right time (not immediately on app launch) +- Explain the value of notifications to users +- Handle permission denial gracefully diff --git a/resources/views/docs/mobile/1/apis/secure-storage.md b/resources/views/docs/mobile/1/apis/secure-storage.md new file mode 100644 index 00000000..0c1a5217 --- /dev/null +++ b/resources/views/docs/mobile/1/apis/secure-storage.md @@ -0,0 +1,87 @@ +--- +title: SecureStorage +order: 900 +--- + +## Overview + +The SecureStorage API provides secure storage using the device's native keychain (iOS) or keystore (Android). It's +ideal for storing sensitive data like tokens, passwords, and user credentials. + +```php +use Native\Mobile\Facades\SecureStorage; +``` + +## Methods + +### `set()` + +Stores a secure value in the native keychain or keystore. + +**Parameters:** +- `string $key` - The key to store the value under +- `string|null $value` - The value to store securely + +**Returns:** `bool` - `true` if successfully stored, `false` otherwise + +```php +SecureStorage::set('api_token', 'abc123xyz'); +``` + +### `get()` + +Retrieves a secure value from the native keychain or keystore. + +**Parameters:** +- `string $key` - The key to retrieve the value for + +**Returns:** `string|null` - The stored value or `null` if not found + +```php +$token = SecureStorage::get('api_token'); +``` + +### `delete()` + +Deletes a secure value from the native keychain or keystore. + +**Parameters:** +- `string $key` - The key to delete the value for + +**Returns:** `bool` - `true` if successfully deleted, `false` otherwise + +## Platform Implementation + +### iOS - Keychain Services +- Uses the iOS Keychain Services API +- Data is encrypted and tied to your app's bundle ID +- Survives app deletion and reinstallation if iCloud Keychain is enabled +- Protected by device passcode/biometrics + +### Android - Keystore +- Uses Android Keystore system +- Hardware-backed encryption when available +- Data is automatically deleted when app is uninstalled +- Protected by device lock screen + +## Security Features + +- **Encryption:** All data is automatically encrypted +- **App Isolation:** Data is only accessible by your app +- **System Protection:** Protected by device authentication +- **Tamper Resistance:** Hardware-backed security when available + +## What to Store +- API tokens and refresh tokens +- User credentials (if necessary) +- Encryption keys +- Sensitive user preferences +- Two-factor authentication secrets + +## What NOT to Store +- Large amounts of data (use encrypted database instead) +- Non-sensitive data +- Temporary data +- Cached content + + diff --git a/resources/views/docs/mobile/1/apis/system.md b/resources/views/docs/mobile/1/apis/system.md new file mode 100644 index 00000000..c9f2f99d --- /dev/null +++ b/resources/views/docs/mobile/1/apis/system.md @@ -0,0 +1,36 @@ +--- +title: System +order: 1000 +--- + +## Overview + +The System API provides access to basic system functions like flashlight control and platform detection. + +```php +use Native\Mobile\Facades\System; +``` + +## Methods + +### `flashlight()` + +Toggles the device flashlight (camera flash LED) on and off. + +**Returns:** `void` + +```php +System::flashlight(); // Toggle flashlight state +``` + +### `isIos()` + +Determines if the current device is running iOS. + +**Returns:** `true` if iOS, `false` otherwise + +### `isAndroid()` + +Determines if the current device is running Android. + +**Returns:** `true` if Android, `false` otherwise diff --git a/resources/views/docs/mobile/1/concepts/_index.md b/resources/views/docs/mobile/1/concepts/_index.md new file mode 100644 index 00000000..97e3b3a0 --- /dev/null +++ b/resources/views/docs/mobile/1/concepts/_index.md @@ -0,0 +1,4 @@ +--- +title: Concepts +order: 3 +--- diff --git a/resources/views/docs/mobile/1/concepts/ci-cd.md b/resources/views/docs/mobile/1/concepts/ci-cd.md new file mode 100644 index 00000000..0e05bd2c --- /dev/null +++ b/resources/views/docs/mobile/1/concepts/ci-cd.md @@ -0,0 +1,159 @@ +--- +title: CI/CD Integration +order: 500 +--- + +## Overview + +NativePHP for Mobile provides robust CLI commands designed for automated CI/CD environments. With proper configuration, +you can build, package, and deploy mobile apps without manual intervention. + +## Key Commands for CI/CD + +### Installation Command + +Install NativePHP dependencies in automated environments: + +```shell +# Install Android platform, overwriting existing files +php artisan native:install android --force --no-tty + +# Install with ICU support for Filament/intl features +php artisan native:install android --force --with-icu + +# Install both platforms +php artisan native:install both --force +``` + +### Build Commands + +Build your app for different environments: + +```shell +# Build debug version (development) +php artisan native:run android --build=debug --no-tty + +# Build release version (production) +php artisan native:run android --build=release --no-tty + +# Build app bundle for Play Store +php artisan native:run android --build=bundle --no-tty +``` + +### Packaging Command + +Package signed releases for distribution: + +```bash +# Package signed APK using environment variables +php artisan native:package android --build-type=release --output=/artifacts --no-tty + +# Package signed App Bundle for Play Store +php artisan native:package android --build-type=bundle --output=/artifacts --no-tty +``` + +## Environment Variables + +Store sensitive signing information in environment variables: + +```bash +# Android Signing +ANDROID_KEYSTORE_FILE="/path/to/keystore.jks" +ANDROID_KEYSTORE_PASSWORD="your-keystore-password" +ANDROID_KEY_ALIAS="your-key-alias" +ANDROID_KEY_PASSWORD="your-key-password" +``` + +## Command Line Options + +### `--no-tty` Flag +Essential for CI/CD environments where TTY is not available: +- Disables interactive prompts +- Provides non-interactive output +- Shows build progress without real-time updates +- Required for most automated environments + +### `--force` Flag +Overwrites existing files and directories: +- Useful for clean builds in CI +- Ensures fresh installation of NativePHP scaffolding +- Prevents build failures from existing files +- Do this whenever you are updating the `nativephp/mobile` package. + +### Build Types +- `--build=debug`: Development builds with debugging enabled +- `--build=release`: Production builds optimized for distribution +- `--build=bundle`: App bundles for Play Store distribution + +## Signing Configuration + +### Using Command Line Options +```bash +php artisan native:package android \ + --build-type=release \ + --keystore=/path/to/keystore.jks \ + --keystore-password=your-password \ + --key-alias=your-alias \ + --key-password=your-key-password \ + --output=./artifacts \ + --no-tty +``` + +### Using Environment Variables (Recommended) +```bash +# Set environment variables in CI +export ANDROID_KEYSTORE_FILE="/path/to/keystore.jks" +export ANDROID_KEYSTORE_PASSWORD="your-password" +export ANDROID_KEY_ALIAS="your-alias" +export ANDROID_KEY_PASSWORD="your-key-password" + +# Run packaging command +php artisan native:package android --build-type=release --output=./artifacts --no-tty +``` + +## Common CI Workflows + +### Development Pipeline +1. Install dependencies: `composer install` +2. Setup environment: copy `.env`, generate key +3. Install NativePHP: `native:install android --force` +4. Build debug: `native:run android --build=debug --no-tty` + +### Release Pipeline +1. Install dependencies: `composer install --no-dev --optimize-autoloader` +2. Setup environment with production settings +3. Install NativePHP: `native:install android --force --with-icu` +4. Package release: `native:package android --build-type=release --no-tty` + +### Play Store Pipeline +1. Same as release pipeline through step 3 +2. Package bundle: `native:package android --build-type=bundle --no-tty` +3. Upload to Play Console + +## Error Handling + +NativePHP commands provide proper exit codes for CI/CD: +- `0`: Success +- `1`: General error +- Build errors are logged and reported + +Monitor build logs for: +- Compilation errors +- Signing failures +- Missing dependencies +- Permission issues + +## Performance Tips + +### Caching +Cache these directories in CI for faster builds: +- `vendor/` (Composer dependencies) +- `nativephp/android/` (Android project) +- Android SDK components + +### Optimization +- Use `--no-dev` for production Composer installs +- Enable Composer autoloader optimization +- Minimize included files with cleanup configuration + +The `--no-tty` flag and environment variable support make NativePHP Mobile well-suited for modern CI/CD pipelines, enabling fully automated mobile app builds and deployments. diff --git a/resources/views/docs/mobile/1/concepts/databases.md b/resources/views/docs/mobile/1/concepts/databases.md new file mode 100644 index 00000000..c25122ef --- /dev/null +++ b/resources/views/docs/mobile/1/concepts/databases.md @@ -0,0 +1,149 @@ +--- +title: Databases +order: 200 +--- + +## Working with Databases + +You'll almost certainly want your application to persist structured data. For this, NativePHP supports +[SQLite](https://sqlite.org/), which works on both iOS and Android devices. + +You can interact with SQLite from PHP in whichever way you're used to. + +## Configuration + +You do not need to do anything special to configure your application to use SQLite. NativePHP will automatically: +- Switch to using SQLite when building your application. +- Create the database for you in the app container. +- Run your migrations each time your app starts, as needed. + +## Migrations + +When writing migrations, you need to consider any special recommendations for working with SQLite. + +For example, prior to Laravel 11, SQLite foreign key constraints are turned off by default. If your application relies +upon foreign key constraints, [you need to enable SQLite support for them](https://laravel.com/docs/database#configuration) before running your migrations. + +**It's important to test your migrations on [prod builds](/docs/mobile/1/getting-started/development#releasing) +before releasing updates!** You don't want to accidentally delete your user's data when they update your app. + +## Seeding data with migrations + +Migrations are the perfect mechanism for seeding data in mobile applications. They provide the natural behavior you +want for data seeding: + +- **Run once**: Each migration runs exactly once per installation. +- **Tracked**: Laravel tracks which migrations have been executed. +- **Versioned**: New app versions can include new data seeding migrations. +- **Reversible**: You can create migrations to remove or update seed data. + +### Creating seed migrations + +Create dedicated migrations for seeding data: + +```shell +php artisan make:migration seed_app_settings +``` + +```php +use Illuminate\Database\Migrations\Migration; +use Illuminate\Support\Facades\DB; + +return new class extends Migration +{ + public function up() + { + DB::table('categories')->insert([ + ['name' => 'Work', 'color' => '#3B82F6'], + ['name' => 'Personal', 'color' => '#10B981'], + ]); + } +}; +``` + +### Test thoroughly + +This is the most important step when releasing new versions of your app, especially with new migrations. + +Your migrations should work both for users who are installing your app for the first time (or re-installing) _and_ +users who have updated your app to a new release. + +Make sure you test your migrations under the different scenarios that your users' databases are likely to be in. + +## Things to note + +- As your app is installed on a separate device, you do not have remote access to the database. +- If a user deletes your application from their device, any databases are also deleted. + +## Can I get MySQL/Postgres/other support? + +No. + +SQLite being the only supported database driver is a deliberate security decision to prevent developers from +accidentally embedding production database credentials directly in mobile applications. Why? + +- Mobile apps are distributed to user devices and can be reverse-engineered. +- Database credentials embedded in apps may be accessible to anyone with the app binary. +- Direct database connections bypass important security layers like rate limiting and access controls. +- Network connectivity issues make direct database connections unreliable from mobile devices and can be troublesome + for your database to handle. + +## API-first + +If a key part of your application relies on syncing data between a central database and your client apps, we strongly +recommend that you do so via a secure API backend that your mobile app can communicate with. + +This provides multiple security and architectural benefits: + +**Security Benefits:** +- Database credentials never leave your server +- Implement proper authentication and authorization +- Rate limiting and request validation +- Audit logs for all data access +- Ability to revoke access instantly + +**Technical Benefits:** +- Better error handling and offline support +- Easier to scale and maintain +- Version your API for backward compatibility +- Transform data specifically for mobile consumption + +### Securing your API + +For the same reasons that you shouldn't share database credentials in your `.env` file or elsewhere in your app code, +you shouldn't store API keys or tokens either. + +If anything, you should provide a client key that **only** allows client apps to request tokens. Once you have +authenticated your user, you can pass an access token back to your mobile app and use this for communicating with your +API. + +Store these tokens on your users' devices securely with the [`SecureStorage`](/docs/mobile/1/apis/secure-storage) API. + +It's a good practice to ensure these tokens have high entropy so that they are very hard to guess and a short lifespan. +Generating tokens is cheap; leaking personal customer data can get _very_ expensive! + +Use industry-standard tools like OAuth-2.0-based providers, Laravel Passport, or Laravel Sanctum. + + + +#### Considerations + +In your mobile apps: + +- Always store API tokens using `SecureStorage` +- Use HTTPS for all API communications +- Cache data locally using SQLite for offline functionality +- Check for connectivity before making API calls + +And on the API side: + +- Use token-based authentication +- Implement rate limiting to prevent abuse +- Validate and sanitize all input data +- Use HTTPS with proper SSL certificates +- Log all authentication attempts and API access diff --git a/resources/views/docs/mobile/1/concepts/deep-links.md b/resources/views/docs/mobile/1/concepts/deep-links.md new file mode 100644 index 00000000..3bb8a305 --- /dev/null +++ b/resources/views/docs/mobile/1/concepts/deep-links.md @@ -0,0 +1,92 @@ +--- +title: Deep Links +order: 300 +--- + +## Overview + +NativePHP for Mobile supports **deep linking** into your app via Custom URL Schemes and Associated Domains: + +- **Custom URL Scheme** + ``` + myapp://some/path + ``` +- **Associated Domains** (a.k.a. Universal Links on iOS, App Links on Android) + ``` + https://example.net/some/path + ``` + +In each case, your app can be opened directly at the route matching `/some/path`. + +Each method has its use cases, and NativePHP handles all the platform-specific configuration automatically when you +provide the proper environment variables. + +You can even use both approaches at the same time in a single app! + +## Custom URL Scheme + +Custom URL schemes are a great way to allow apps to pass data between themselves. If your app is installed when a user +uses a deep link that incorporates your custom scheme, your app will open immediately to the desired route. + +But note that custom URL schemes can only work when your app has been installed and cannot aid in app discovery. If a +user interacts with URL with a custom scheme for an app they don't have installed, there will be no prompt to install +an app that can load that URL. + +To enable your app's custom URL scheme, define it in your `.env`: + +```dotenv +NATIVEPHP_DEEPLINK_SCHEME=myapp +``` + +You should choose a scheme that is unique to your app to avoid confusion with other apps. Note that some schemes are +reserved by the system and cannot be used (e.g. `https`). + +## Associated domains + +Universal Links/App Links allow real HTTPS URLs to open your app instead of in a web browser, if the app is installed. +If the app is not installed, the URL will load as normal in the browser. + +This flow increases the opportunity for app discovery dramatically and provides a much better overall user experience. + +### How it works + +1. You must prove to the operating system on the user's device that your app is legitimately associated with the domain + you are trying to redirect by hosting special files on your server: + - `.well-known/apple-app-site-association` (for iOS) + - `.well-known/assetlinks.json` (for Android) +2. The mobile OS reads these files to verify the link association +3. Once verified, tapping a real URL will open your app instead of opening it in the user's browser + +**NativePHP handles all the technical setup automatically** - you just need to host the verification files and +configure your domain correctly. + +To enable an app-associated domain, define it in your `.env`: + +```dotenv +NATIVEPHP_DEEPLINK_HOST=example.net +``` + +## Testing & troubleshooting + +Associated Domains do not usually work in simulators. Testing on a real device that connects to a publicly-accessible +server for verification is often the best way to ensure these are operating correctly. + +If you are experiencing issues getting your associated domain to open your app, try: +- Completely deleting and reinstalling the app. Registration verifications (including failures) are often cached + against the app. +- Validating that your associated domain verification files are formatted correctly and contain the correct data. + +There is usually no such limitation for Custom URL Schemes. + +## Use cases + +Deep linking is great for bringing users from another context directly to a key place in your app. Universal/App Links +are usually the more appropriate choice for this because of their flexibility in falling back to simple loading a URL +in the browser. + +They're also more likely to behave the same across both platforms. + +Then you could use Universal/App Links in: +- NFC tags +- QR codes +- Email/SMS marketing diff --git a/resources/views/docs/mobile/1/concepts/push-notifications.md b/resources/views/docs/mobile/1/concepts/push-notifications.md new file mode 100644 index 00000000..8680ce05 --- /dev/null +++ b/resources/views/docs/mobile/1/concepts/push-notifications.md @@ -0,0 +1,93 @@ +--- +title: Push Notifications +order: 400 +--- + +## Overview + +NativePHP for Mobile uses Firebase Cloud Messaging (FCM) to send push notifications to your users on both iOS and +Android devices. + +To send a push notification to a user, your app must request a token. That token must then be stored securely (ideally +on a server application via a secure API) and associated with that user/device. + +Requesting push notification will trigger an alert for the user to either approve or deny your request. If they approve, +your app will receive the token. + +When you want to send a notification to that user, you pass this token along with a request to the FCM service and +Firebase handles sending the message to the right device. + + + +## Firebase + +1. Create a [Firebase](https://firebase.google.com/) account +2. Create a project +3. Download the `google-services.json` file (for Android) and `GoogleService-Info.plist` file (for iOS) +4. These files contain the configuration for your app and is used by the Firebase SDK to retrieve tokens for each device + +Place these files in the root of your application and NativePHP will automatically handle setting them up appropriately +for each platform. + +You can ignore Firebase's further setup instructions as this is already taken care of by NativePHP. + +### Service account + +For sending push notifications from your server-side application, you'll also need a Firebase service account: + +1. Go to your Firebase Console → Project Settings → Service Accounts +2. Click "Generate New Private Key" to download the service account JSON file +3. Save this file as `fcm-service-account.json` somewhere safe in your server application + +## Getting push tokens + +It's common practice to request push notification permissions during app bootup as tokens can change when: +- The app is restored on a new device +- The app data is restored from backup +- The app is updated +- Other internal FCM operations + +To request a token, use the `PushNotifications::getToken()` method: + +```php +use Native\Mobile\Facades\PushNotifications; + +PushNotifications::getToken(); +``` + +If the user has approved your app to use push notifications and the request to FCM succeeded, a `TokenGenerated` event +will fire. + +Listen for this event to receive the token. Here's an example in a Livewire component: + +```php +use App\Services\APIService; +use Livewire\Attributes\On; +use Native\Mobile\Facades\PushNotifications; +use Native\Mobile\Events\PushNotification\TokenGenerated; + +class PushNotifications extends Component +{ + #[On('native:'.TokenGenerated::class)] + public function storePushToken(APIService $api, string $token) + { + $api->storePushToken($token); + } +} +``` + +## Sending push notifications + +Once you have a token, you may use it from your server-side applications to trigger Push Notifications directly to your +user's device. + + diff --git a/resources/views/docs/mobile/1/concepts/security.md b/resources/views/docs/mobile/1/concepts/security.md new file mode 100644 index 00000000..fcb2e777 --- /dev/null +++ b/resources/views/docs/mobile/1/concepts/security.md @@ -0,0 +1,104 @@ +--- +title: Security +order: 100 +--- + +## Security + +Although NativePHP tries to make it as easy as possible to make your application secure, it is your responsibility to +protect your users. + +### Secrets and .env + +As your application is being installed on systems outside your/your organisation's control, it is important to think +of the environment that it's in as _potentially_ hostile, which is to say that any secrets, passwords or keys +could fall into the hands of someone who might try to abuse them. + +This means you should, where possible, use unique keys for each installation, preferring to generate these at first-run +or on every run rather than sharing the same key for every user across many installations. + +Especially if your application is communicating with any private APIs over the network, we highly recommend that your +application and any API use a robust and secure authentication protocol, such as OAuth2, that enables you to create and +distribute unique and expiring tokens (an expiration date less than 48 hours in the future is recommended) with a high +level of entropy, as this makes them hard to guess and hard to abuse. + +**Always use HTTPS.** + +If your application allows users to connect _their own_ API keys for a service, you should treat these keys with great +care. If you choose to store them anywhere (either in a file or +[Database](databases)), make sure you store them +[encrypted](../the-basics/system#encryption-decryption) and decrypt them only when needed. + +## Secure Storage + +NativePHP provides access to your users' device's native Keystore/Keychain through the +[`SecureStorage`](/docs/apis/secure-storage) facade, which +allow you to store small amounts of data in a secure way. + +The device's secure storage encrypts and decrypts data on the fly and that means you can safely rely on it to store +critical things like API tokens, keeping your users and your systems safe. + +This data is only accessible by your app and is persisted beyond the lifetime of your app, so it will still be available +the next time your app is open. + +### Why not use the Laravel `Crypt` facade? + +By default, the `Crypt` facade - and by extension the `encrypt` and `decrypt` helper functions - all rely on the +`APP_KEY` value set in your `.env` file. + +We _will_ use Laravel's underlying `Encryption` class, but you should avoid using these helpers directly. + +In the context of distributed apps, the `APP_KEY` is shipped _with_ your app and therefore isn't secure. Anyone who +knows where to look for it will be able to find it. Then any data encrypted with it is no better off than if it was +stored in plain text. + +Also, it will be the same key for every user, and this presents a considerable risk. + +What you really want is a **unique key for each user**, and for that you really need to generate your encryption key +once your app is installed on your user's device. + +You could do this and update the `.env` file, but it would still be stored in a way that an attacker may be able to +exploit. + +A better approach is to generate a secure key the first time your app opens, place that key in Secure Storage, and +then use that key to encrypt your other data before storage: + +```php +use Illuminate\Encryption\Encrypter; +use Illuminate\Support\Facades\Storage; +use Native\Mobile\Facades\SecureStorage; + +function generateRandomKey() +{ + return base64_encode( + Encrypter::generateKey(config('app.cipher')) + ); +} + +$encryptionKey = SecureStorage::get('encryption_key'); + +if (! $encryptionKey) { + SecureStorage::set('encryption_key', $encryptionKey = generateRandomKey()); +} + +$mobileEncrypter = new Encrypter($encryptionKey); + +$encryptedContents = $mobileEncrypter->encrypt( + $request->file('super_private_file') +); + +Storage::put('my_secure_file.pdf', $encryptedContents); +``` + +And then decrypt it later: + +```php +$decryptedContents = $mobileEncrypter->decrypt( + Storage::get('my_secure_file.pdf') +); +``` + +### Secure Storage vs Database/Files + +Secure Storage is only meant for small amounts of text data, usually no more than a few KBs. If you need to store +larger amounts of data or files, you should store this in a database or as a file. diff --git a/resources/views/docs/mobile/1/getting-started/_index.md b/resources/views/docs/mobile/1/getting-started/_index.md new file mode 100644 index 00000000..5c684744 --- /dev/null +++ b/resources/views/docs/mobile/1/getting-started/_index.md @@ -0,0 +1,4 @@ +--- +title: Getting Started +order: 1 +--- \ No newline at end of file diff --git a/resources/views/docs/mobile/1/getting-started/configuration.md b/resources/views/docs/mobile/1/getting-started/configuration.md new file mode 100644 index 00000000..ec4c727d --- /dev/null +++ b/resources/views/docs/mobile/1/getting-started/configuration.md @@ -0,0 +1,139 @@ +--- +title: Configuration +order: 200 +--- + +## Overview + +NativePHP for Mobile is designed so that most configuration happens **inside your Laravel application**, without +requiring you to manually open Xcode or Android Studio. + +This page explains the key configuration points you can control through Laravel. + +## The `nativephp.php` Config File + +The `config/nativephp.php` config file contains a number of useful options. + +NativePHP uses sensible defaults and makes several assumptions based on default installations for tools required to +build and run apps from your computer. + +You can override these defaults by editing the `nativephp.php` config file in your Laravel project, and in many case +simply by changing environment variables. + +```dotenv +NATIVEPHP_APP_VERSION +NATIVEPHP_APP_VERSION_CODE +NATIVEPHP_APP_ID +NATIVEPHP_DEEPLINK_SCHEME +NATIVEPHP_DEEPLINK_HOST +NATIVEPHP_APP_AUTHOR +NATIVEPHP_GRADLE_PATH +NATIVEPHP_ANDROID_SDK_LOCATION +``` + +## `NATIVEPHP_APP_ID` + +You must set your app ID to something unique. A common practice is to use a reverse-DNS-style name, e.g. +`com.yourcompany.yourapp`. + +Your app ID (also known as a *Bundle Identifier*) is a critical piece of identification across both Android and iOS +platforms. Different app IDs are treated as separate apps. + +And it is often referenced across multiple services, such as Apple Developer Center and the Google Play Console. + +So it's not something you want to be changing very often. + +## `NATIVEPHP_APP_VERSION` + +The `NATIVEPHP_APP_VERSION` environment variable controls your app's versioning behavior. + +When your app is compiling, NativePHP first copies the relevant Laravel files into a temporary directory, zips them up, +and embeds the archive into the native application. + +When your app boots, it checks the embedded version against the previously installed version to see if it needs to +extract the bundled Laravel application. + +If the versions match, the app uses the existing files without re-extracting the archive. + +To force your application to always install the latest version of your code - especially useful during development - +set this to `DEBUG`: + +```dotenv +NATIVEPHP_APP_VERSION=DEBUG +``` + +Note that this will make your application's boot up slightly slower as it must unpack the zip every time it loads. + +But this ensures that you can iterate quickly during development, while providing a faster, more stable experience for +end users once an app is published. + +## Cleanup `env` keys + +The `cleanup_env_keys` array in the config file allows you to specify keys that should be removed from the `.env` file before bundling. +This is useful for removing sensitive information like API keys or other secrets. + +## Cleanup `exclude_files` + +The `cleanup_exclude_files` array in the config file allows you to specify files and folders that should be removed before bundling. +This is useful for removing files like logs or other temporary files. + +## Permissions +In general, the app stores don't want your app to have permissions (a.k.a entitlements) it doesn't need. + +By default, all optional permissions are disabled. + +You may enable the features you intend to use simply by changing the value of the appropriate permission to `true`: + +```php + + 'permissions' => [ + + 'biometric' => true, + + ], +``` + +### Available permissions + +- `biometric` - Allows your application to use fingerprint or face-recognition hardware (with a fallback to PIN code) + to secure parts of your application. +- `camera` - Allows your application to request access to the device's camera, if present. Note that the user may deny + access and any camera functions will then result in a no-op. +- `nfc` - Allows your application to request access to the device's NFC reader, if present. +- `push_notifications` - Allows your application to request permissions to send push notifications. Note that the user + may deny this and any push notification functions will then result in a no-op. +- `location` - Allows your application to request access to the device's GPS receiver, if present. Note that the user + may deny this and any location functions will then result in a no-op. +- `vibrate` - In modern Android devices this is a requirement for most haptic feedback. +- `storage_read` - Grants your app access to read from device storage locations. +- `storage_write` - Allows your app to write to device storage. + +## Orientation + +NativePHP (as of v1.10.3) allows users to custom specific orientations per device through the config file. The config allows for granularity for iPad, iPhone and Android devices. Options for each device can be seen below. + +NOTE: if you want to disable iPad support completely simply apply `false` for each option. + +```php +'orientation' => [ + 'iPhone' => [ + 'portrait' => true, + 'upside_down' => false, + 'landscape_left' => false, + 'landscape_right' => false, + ], + 'iPad' => [ + 'portrait' => true, + 'upside_down' => false, + 'landscape_left' => false, + 'landscape_right' => false, + ], + 'android' => [ + 'portrait' => true, + 'upside_down' => false, + 'landscape_left' => false, + 'landscape_right' => false, + ], +], +``` + diff --git a/resources/views/docs/mobile/1/getting-started/development.md b/resources/views/docs/mobile/1/getting-started/development.md new file mode 100644 index 00000000..1ecc8b76 --- /dev/null +++ b/resources/views/docs/mobile/1/getting-started/development.md @@ -0,0 +1,201 @@ +--- +title: Development +order: 300 +--- + +Developing your NativePHP apps can be done in the browser, using workflows with which you're already familiar. + +This allows you to iterate rapidly on parts like the UI and major functionality, even using your favorite tools for +testing etc. + +But when you want to test _native_ features, then you must run your app on a real/emulated device. + +Whether you run your native app on an emulated or real device, it will always require compilation after changes have +been made. + + + +## Build your frontend + +If you're using Vite or similar tooling to build any part of your UI (e.g. for React/Vue, Tailwind etc), you'll need +to run your asset build command _before_ compiling your app. + +### Inertia on iOS + +Due to the way your apps are configured to work on iOS, we need to patch the Axios package to make Inertia work. + +We've tried to make this as straightforward as possible. Simply run: + +```shell +php artisan native:patch-inertia +``` + +This will backup your current `vite.config.js` and replace it with one that 'fixes' Axios. + +You will just need to copy over any specific config (plugins etc) from your old Vite config to this new one. + +Once that's done, you'll need to adjust your Vite build command for when you're creating iOS builds. _Only_ for iOS +builds. (If you try to run these builds on Android they probably won't work.) + +Add the `--mode=ios` to your build command. Run it before compiling your app for iOS. Here's an example using `npm`: + +```shell +npm run build -- --mode=ios +``` + +## Compile your app + +To compile and run your app, simply run: + +```shell +php artisan native:run --build=debug +``` + +This single command takes care of everything and allows you to run new builds of your application without having to +learn any new editors or platform-specific tools. + + + +## Working with Xcode or Android Studio + +On occasion, it is useful to compile your app from inside the target platform's dedicated development tools, Android +Studio and Xcode. + +If you're familiar with these tools, you can easily open the projects using the following Artisan command: + +```shell +php artisan native:open +``` + +## Hot Reloading + +We've tried to make compiling your apps as fast as possible, but when coming from the 'make a change; hit refresh'-world +of PHP development that we all love, compiling apps can feel like a slow and time-consuming process. + +So we've released hot reloading, which aims to make your development experience feel just like home. + +You can enable hot reloading by running the following command: + +```shell +php artisan native:watch {platform:ios|android} +``` + +This is useful during development for quickly testing changes without re-compiling your entire app. When you make +changes to any files in your Laravel app, the web view will be reloaded and your changes should show almost immediately. + +### Implementation + +The proper way to implement this is to first `run` your app on your device/emulator, then start HMR with `npm run dev` then in a separate terminal run the `native:watch` command. This will reload any Blade/Livewire files as well as any recompiled assets (css/js etc). + + + + +## Releasing + +To prepare your app for release, you should set the version number to a new version number that you have not used +before and increment the build number: + +```dotenv +NATIVEPHP_APP_VERSION=1.2.3 +NATIVEPHP_APP_VERSION_CODE=48 +``` + +### Versioning + +You have complete freedom in how you version your applications. You may use semantic versioning, codenames, +date-based versions, or any scheme that works for your project, team or business. + +Remember that your app versions are usually public-facing (e.g. in store listings and on-device settings and update +screens) and can be useful for customers to reference if they need to contact you for help and support. + +The build number is managed via the `NATIVEPHP_APP_VERSION` key in your `.env`. + +### Build numbers + +Both the Google Play Store and Apple App Store require your app's build number to increase for each release you submit. + +The build number is managed via the `NATIVEPHP_APP_VERSION_CODE` key in your `.env`. + +### Run a `release` build + +Then run a release build: + +```shell +php artisan native:run --build=release +``` + +This builds your application with various optimizations that reduce its overall size and improve its performance, such +as removing debugging code and unnecessary features (i.e. Composer dev dependencies). + +**You should test this build on a real device.** Once you're happy that everything is working as intended you can then +submit it to the stores for approval and distribution. + +- [Google Play Store submission guidelines](https://support.google.com/googleplay/android-developer/answer/9859152?hl=en-GB#zippy=%2Cmaximum-size-limit) +- [Apple App Store submission guidelines](https://developer.apple.com/ios/submit/) + + diff --git a/resources/views/docs/mobile/1/getting-started/environment-setup.md b/resources/views/docs/mobile/1/getting-started/environment-setup.md new file mode 100644 index 00000000..35235763 --- /dev/null +++ b/resources/views/docs/mobile/1/getting-started/environment-setup.md @@ -0,0 +1,144 @@ +--- +title: Environment Setup +order: 100 +--- + +## Requirements + +1. PHP 8.3+ +2. Laravel 11+ +3. [A NativePHP for Mobile license](https://nativephp.com/mobile) + +If you don't already have PHP installed on your machine, the most painless way to get PHP up and running on Mac and +Windows is with [Laravel Herd](https://herd.laravel.com). It's fast and free! + +## iOS Requirements + + + +1. macOS (required - iOS development is only possible on a Mac) +2. [Xcode 16.0 or later](https://apps.apple.com/app/xcode/id497799835) +3. Xcode Command Line Tools +4. Homebrew & CocoaPods +5. _Optional_ iOS device for testing + +### Setting up iOS Development Environment + +1. **Install Xcode** + - Download from the [Mac App Store](https://apps.apple.com/app/xcode/id497799835) + - Minimum version: Xcode 16.0 + +2. **Install Xcode Command Line Tools** + ```shell + xcode-select --install + ``` + Verify installation: + ```shell + xcode-select -p + ``` + +3. **Install Homebrew** (if not already installed) + ```shell + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + ``` + +4. **Install CocoaPods** + ```shell + brew install cocoapods + ``` + Verify installation: + ```shell + pod --version + ``` + +### Apple Developer Account +You **do not** need to enroll in the [Apple Developer Program](https://developer.apple.com/programs/enroll/) ($99/year) +to develop and test your apps on a Simulator. However, you will need to enroll when you want to: +- Test your apps on real devices +- Distribute your apps via the App Store + +## Android Requirements + +1. [Android Studio 2024.2.1 or later](https://developer.android.com/studio) +2. Android SDK with API 33 or higher +3. **Windows only**: You must have [7zip](https://www.7-zip.org/) installed. + + + +### Setting up Android Studio and SDK + +1. **Download and Install Android Studio** + - Download from the [Android Studio download page](https://developer.android.com/studio) + - Minimum version required: Android Studio 2024.2.1 + +2. **Install Android SDK** + - Open Android Studio + - Navigate to **Tools → SDK Manager** + - In the **SDK Platforms** tab, install at least one Android SDK platform for API 33 or higher + - Latest stable version: Android 15 (API 35) + - You only need to install one API version to get started + - In the **SDK Tools** tab, ensure **Android SDK Build-Tools** and **Android SDK Platform-Tools** are installed + +That's it! Android Studio handles all the necessary configuration automatically. + +### Preparing for NativePHP + +1. Check that you can run `java -version` and `adb devices` from the terminal. +2. The following environment variables set: + +#### On macOS +```shell +# This isn't required if JAVA_HOME is already set in your environment variables (check using `printenv | grep JAVA_HOME`) +export JAVA_HOME=$(/usr/libexec/java_home -v 17) + +export ANDROID_HOME=$HOME/Library/Android/sdk +export PATH=$PATH:$JAVA_HOME/bin:$ANDROID_HOME/emulator:$ANDROID_HOME/tools:$ANDROID_HOME/tools/bin:$ANDROID_HOME/platform-tools +``` + +#### On Windows +The example below assumes default installation paths for the Android SDK and JDK: + +```shell +set ANDROID_HOME=C:\Users\yourname\AppData\Local\Android\Sdk +set PATH=%PATH%;%JAVA_HOME%\bin;%ANDROID_HOME%\platform-tools + +# This isn't required if JAVA_HOME is already set in the Windows Env Variables +set JAVA_HOME=C:\Program Files\Microsoft\jdk-17.0.8.7-hotspot +``` + +### "No AVDs found" error +If you encounter this error, it means no Virtual Devices are configured in Android Studio. +To resolve it, open Android Studio, navigate to Virtual Devices, and create at least one device. + +## Testing on Real Devices + +You don't _need_ a physical iOS/Android device to compile and test your application, as NativePHP for Mobile supports +the iOS Simulator and Android emulators. However, we highly recommend that you test your application on a real device +before submitting to the Apple App Store and Google Play Store. + +### On iOS +If you want to run your app on a real iOS device, you need to make sure it is in +[Developer Mode](https://developer.apple.com/documentation/xcode/enabling-developer-mode-on-a-device) +and that it's been added to your Apple Developer account as +[a registered device](https://developer.apple.com/account/resources/devices/list). + +### On Android +On Android you need to [enable developer options](https://developer.android.com/studio/debug/dev-options#enable) +and have USB debugging (ADB) enabled. diff --git a/resources/views/docs/mobile/1/getting-started/installation.md b/resources/views/docs/mobile/1/getting-started/installation.md new file mode 100644 index 00000000..23bd1fb5 --- /dev/null +++ b/resources/views/docs/mobile/1/getting-started/installation.md @@ -0,0 +1,159 @@ +--- +title: Installation +order: 100 +--- + +## Get a license + +Before you begin, you will need to [purchase a license](/mobile). + +To make NativePHP for Mobile a reality has taken a lot of work and will continue to require even more. For this reason, +it's not open source, and you are not free to distribute or modify its source code. + +Your license fee goes straight back into the NativePHP project and community, enabling us to: +- Develop premium features for everyone. +- Provide first-class support. +- Sponsor our dependencies. +- Donate to our contributors. +- Support community events. +- Ensure that the whole NativePHP project remains viable for a long time to come. + +Thank you for supporting the project in this way! 🙏 + +## Install the Composer package + + + +Once you have your license, you will need to add the following to your `composer.json`: + +```json +"repositories": [ + { + "type": "composer", + "url": "https://nativephp.composer.sh" + } +], +``` + +Then run: +```shell +composer require nativephp/mobile +``` + + + + +If this is the first time you're installing the package, you will be prompted to authenticate. Your username is the +email address you used when purchasing your license. Your password is your license key. + +This package contains all the libraries, classes, commands, and interfaces that your application will need to work with +iOS and Android. + +## Run the NativePHP installer + +**Before** running the `install` command, it is important to set the following variables in your `.env`: + +```dotenv +NATIVEPHP_APP_ID=com.yourcompany.yourapp +NATIVEPHP_APP_VERSION="DEBUG" +NATIVEPHP_APP_VERSION_CODE="1" +``` + +Find out more about these options in +[Configuration](/docs/getting-started/configuration#codenativephp-app-idcode). + + + + +```shell +php artisan native:install +``` + +The NativePHP installer takes care of setting up and configuring your Laravel application to work with iOS and Android. + +You may be prompted about whether you would like to install the ICU-enabled PHP binaries. You should install these if +your application relies on the `intl` PHP extension. + +If you don't need `intl` or are not sure, choose the default, non-ICU builds. + + + +### The `nativephp` Directory + +After running: `php artisan native:install` you’ll see a new `nativephp` directory at the root of your Laravel project +as well as a `config/nativephp.php` config file. + +The `nativephp` folder contains the native application project files needed to build your app for the desired platforms. + +You should not need to manually open or edit any native project files under normal circumstances. NativePHP handles +the heavy lifting for you. + +**You should treat this directory as ephemeral.** When upgrading the NativePHP package, it will be necessary to run +`php artisan native:install --force`, which completely rebuilds this directory, deleting all files within. + +For this reason, we also recommend you add the `nativephp` folder to your `.gitignore`. + +## Start your app + +**Heads up!** Before starting your app in a native context, try running it in the browser. You may bump into exceptions +which need addressing before you can run your app natively, and may be trickier to spot when doing so. + +Once you're ready: + +```shell +php artisan native:run +``` + +Just follow the prompts! This will start compiling your application and boot it on whichever device you select. + +### Running on a real device + +#### On iOS +If you want to run your app on a real iOS device, you need to make sure it is in +[Developer Mode](https://developer.apple.com/documentation/xcode/enabling-developer-mode-on-a-device) +and that it's been added to your Apple Developer account as +[a registered device](https://developer.apple.com/account/resources/devices/list). + +#### On Android +On Android you need to [enable developer options](https://developer.android.com/studio/debug/dev-options#enable) +and have USB debugging (ADB) enabled. + +And that's it! You should now see your Laravel application running as a native app! 🎉 diff --git a/resources/views/docs/mobile/1/getting-started/introduction.md b/resources/views/docs/mobile/1/getting-started/introduction.md new file mode 100644 index 00000000..26506d7f --- /dev/null +++ b/resources/views/docs/mobile/1/getting-started/introduction.md @@ -0,0 +1,74 @@ +--- +title: Introduction +order: 1 +--- + +## Welcome to the revolution! + +NativePHP for Mobile is the first library of its kind that lets you run full PHP applications natively on mobile +devices — no web server required. + +By embedding a statically compiled PHP runtime alongside Laravel, and bridging directly into each platform’s native +APIs, NativePHP brings the power of modern PHP to truly native mobile apps. Build performant, offline-capable +experiences using the tools you already know. + +**It's never been this easy to build beautiful, local-first apps for iOS and Android.** + +## What makes NativePHP for Mobile special? + +- 📱 **Native performance** + Your app runs natively through an embedded PHP runtime optimized for each platform. +- 🔥 **True mobile APIs** + Access camera, biometrics, push notifications, and more. One cohesive library that does it all. +- ⚡ **Laravel powered** + Leverage the entire Laravel ecosystem and your existing skillset. +- 🚫 **No web server required** + Your app runs entirely on-device and can operate completely offline-first. +- 🔄 **Cross platform** + Build apps for both iOS and Android from a single codebase. + +## Old tools, new tricks + +With NativePHP for Mobile, you don’t need to learn Swift, Kotlin, or anything new. +No new languages. No unfamiliar build tools. No fighting with Gradle or Xcode. + +Just PHP. + +Developers around the world are using the skills they already have to build and ship real mobile apps — faster than +ever. In just a few hours, you can go from code to app store submission. + +## How does it work? + +On the simplest level: + +1. A statically-compiled version of PHP is bundled with your code into a Swift/Kotlin shell application. +2. NativePHP's custom Swift/Kotlin bridges manage the PHP environment, running your PHP code directly. +3. A custom PHP extension is compiled into PHP, that exposes PHP interfaces to native functions. +4. Your app renders in a native web view, so you can continue developing your UI the way you're used to. + +You simply interact with an easy-to-use set of functions from PHP and everything just works! + +## Batteries included + +NativePHP for Mobile is way more than just a web view wrapper for your server-based application. Your application lives +_on device_ and is shipped with each installation. + +Thanks to our custom PHP extension, you can interact with many native APIs today, with more coming all the time, +including: + +- 📷 Camera & Gallery access +- 🔐 Biometric authentication (Face ID, Touch ID, Fingerprint) +- 🔔 Push notifications via APNs (for iOS) or Firebase (both) +- 💬 Native dialogs & toasts +- 🔗 Deep links & universal links +- 📳 Haptic feedback & vibration +- 🔦 Flashlight control +- 📤 Native sharing +- 🔒 Secure storage (Keychain/Keystore) +- 📍 Location services + +You have the full power of PHP and Laravel at your fingertips... literally! And you're not sandboxed into the web view; +this goes way beyond what's possible with PWAs and WASM without any of the complexity... we've got full-cream PHP at +the ready! + +**What are you waiting for!? [Let's go!](quick-start)** diff --git a/resources/views/docs/mobile/1/getting-started/quick-start.md b/resources/views/docs/mobile/1/getting-started/quick-start.md new file mode 100644 index 00000000..1e7a1fff --- /dev/null +++ b/resources/views/docs/mobile/1/getting-started/quick-start.md @@ -0,0 +1,56 @@ +--- +title: Quick Start +order: 2 +--- + +## Let's go! + + + +If you've already got your [environment set up](environment-setup) to build mobile apps using Xcode and/or Android +Studio, then you can get building your first mobile app with NativePHP in minutes: + +### 1. Update your `composer.json` +Add the NativePHP Composer repository: + +```json +"repositories": [ + { + "type": "composer", + "url": "https://nativephp.composer.sh" + } +] +``` + +### 2. Set your app's identifier +You must set a `NATIVEPHP_APP_ID` in your `.env` file: + +```dotenv +NATIVEPHP_APP_ID=com.cocacola.cokezero +``` + +### 3. Install & run + +```bash +# Install NativePHP for Mobile into a new Laravel app +composer require nativephp/mobile + +# Ready your app to go native +php artisan native:install + +# Run your app on a mobile device +php artisan native:run +``` + +## Need help? + +- **Community** - Join our [Discord](/discord) for support and discussions. +- **Examples** - Check out the Kitchen Sink demo app + on [Android](https://play.google.com/store/apps/details?id=com.nativephp.kitchensinkapp) and + [iOS](https://testflight.apple.com/join/vm9Qtshy)! diff --git a/resources/views/docs/mobile/1/getting-started/roadmap.md b/resources/views/docs/mobile/1/getting-started/roadmap.md new file mode 100644 index 00000000..3c65c174 --- /dev/null +++ b/resources/views/docs/mobile/1/getting-started/roadmap.md @@ -0,0 +1,45 @@ +--- +title: Roadmap +order: 400 +--- + +NativePHP for Mobile is stable and already deployed in production apps released on the app stores. But it's still early +days. We haven't yet built interfaces to all of the available mobile APIs. + +We're working on adding more and more features, including (in no particular order): + - Microphone access + - Bluetooth + - SMS (Android only) + - File picker + - Video camera access + - Document scanner + - Background tasks + - Geofencing + - Calendar access + - Local notifications, scheduled notifications + - Clipboard API + - Contacts access + - App badges + - OTA Updates + - App review prompt + - Proximity sensor + - Gyroscope + - Accelerometer + - Screen brightness + - More Haptic feedback + - Network info access + - Battery status + - CPU/Device information + - Ads + - In-app billing + + diff --git a/resources/views/docs/mobile/1/getting-started/support-policy.md b/resources/views/docs/mobile/1/getting-started/support-policy.md new file mode 100644 index 00000000..396f4fc6 --- /dev/null +++ b/resources/views/docs/mobile/1/getting-started/support-policy.md @@ -0,0 +1,22 @@ +--- +title: Support Policy +order: 600 +--- + +NativePHP for Mobile is still very new. We aim to make it workable with as many versions of iOS and Android as is +reasonable. Considering that we have a very small team and a lot of work, our current stance on version support is this: + +**We aim (but do not guarantee) to support all the current and upcoming major, currently vendor-supported versions of +the platforms, with a focus on the current major release as a priority.** + +In practical terms, as of September 2025, this means we intend for NativePHP to be compatible — in part or in whole — +with: + +- iOS 18+ +- Android 13+ + +We do not guarantee support of all features across these versions, and whilst NativePHP may work in part on even older +versions than the currently-supported ones, we do not provide support for these under this standard policy. + +If you require explicit backwards compatibility with older or unsupported versions, we will be happy to have you join +our [partner](/partners) program, where a custom support policy can be arranged. diff --git a/resources/views/docs/mobile/1/getting-started/versioning.md b/resources/views/docs/mobile/1/getting-started/versioning.md new file mode 100644 index 00000000..192b5787 --- /dev/null +++ b/resources/views/docs/mobile/1/getting-started/versioning.md @@ -0,0 +1,69 @@ +--- +title: Versioning Policy +order: 500 +--- + +NativePHP for Mobile follows [semantic versioning](https://semver.org) with a mobile-specific approach that distinguishes between +Laravel-only changes and native code changes. This ensures predictable updates and optimal compatibility. + +Our aim is to limit the amount of work you need to do to get the latest updates and ensure everything works. + +We will aim to post update instructions with each release. + +## Release types + +### Patch releases + +Patch releases of `nativephp/mobile` should have **no breaking changes** and **only change Laravel/PHP code**. +This will typically include bug fixes and dependency updates that don't affect native code. + +These releases should be completely compatible with the existing version of your native applications. + +This means that you can: + +- Safely update via `composer update`. +- Avoid a complete rebuild (no need to `native:install --force`). +- Allow for easier app updates avoiding the app stores. + +### Minor releases + +Minor releases may contain **native code changes**. Respecting semantic versioning, these still should not contain +breaking changes, but there may be new native APIs, Kotlin/Swift updates, platform-specific features, or native +dependency changes. + +Minor releases will: + +- Require a complete rebuild (`php artisan native:install --force`) to work with the latest APIs. +- Need app store submission for distribution. +- Include advance notice and migration guides where necessary. + +### Major releases + +Major releases are reserved for breaking changes. This will usually follow a period of deprecations so that you have +time to make the necessary changes to your application code. + +## Version constraints + +We recommend using the [tilde range operator](https://getcomposer.org/doc/articles/versions.md#tilde-version-range-) +with a full minimum patch release defined in your `composer.json`: + +```json +{ + "require": { + "nativephp/mobile": "~1.1.0" + } +} +``` + +This automatically receives patch updates while giving you control over minor releases. + +## Your application versioning + +Just because we're using semantic versioning for the `nativephp/mobile` package, doesn't mean your app must follow that +same scheme. + +You have complete freedom in versioning your own applications! You may use semantic versioning, codenames, +date-based versions, or any scheme that works for your project, team or business. + +Remember that your app versions are usually public-facing (e.g. in store listings and on-device settings and update +screens) and can be useful for customers to reference if they need to contact you for help and support. diff --git a/resources/views/docs/mobile/1/the-basics/_index.md b/resources/views/docs/mobile/1/the-basics/_index.md new file mode 100644 index 00000000..373920fa --- /dev/null +++ b/resources/views/docs/mobile/1/the-basics/_index.md @@ -0,0 +1,4 @@ +--- +title: The Basics +order: 2 +--- \ No newline at end of file diff --git a/resources/views/docs/mobile/1/the-basics/app-icon.md b/resources/views/docs/mobile/1/the-basics/app-icon.md new file mode 100644 index 00000000..2e8aa135 --- /dev/null +++ b/resources/views/docs/mobile/1/the-basics/app-icon.md @@ -0,0 +1,25 @@ +--- +title: App Icons +order: 300 +--- + +NativePHP makes it easy to apply a custom app icon to your iOS and Android apps. + +## Supply your icon + +Place a single high-resolution icon file at: `public/icon.png`. + +### Requirements +- Format: PNG +- Size: 1024 × 1024 pixels +- Background: Transparent or solid — your choice +- GD PHP extension must be enabled, ensure it has enough memory (~2GB should be enough) + +This image will be automatically resized for all Android densities and used as the base iOS app icon. +You must have the GD extension installed in your development machine's PHP environment for this to work. + + diff --git a/resources/views/docs/mobile/1/the-basics/assets.md b/resources/views/docs/mobile/1/the-basics/assets.md new file mode 100644 index 00000000..b09352eb --- /dev/null +++ b/resources/views/docs/mobile/1/the-basics/assets.md @@ -0,0 +1,28 @@ +--- +title: Assets +order: 500 +--- + +## Compiling CSS and JavaScript + +If you are using React, Vue or another JavaScript library, or Tailwind CSS, tools that requires your frontend to be +built by build tooling like Vite, you will need to run your build process _before_ compiling the native application. + +For example, if you're using Vite with NPM to build a React application that is using Tailwind, to ensure that your +latest styles and JavaScript are included, always run `npm run build` before running `php artisan native:run`. + +## Other files + +NativePHP will include all files from the root of your Laravel application. So you can store any files that you wish to +make available to your application wherever makes the most sense for you. + + diff --git a/resources/views/docs/mobile/1/the-basics/events.md b/resources/views/docs/mobile/1/the-basics/events.md new file mode 100644 index 00000000..07b7b7ae --- /dev/null +++ b/resources/views/docs/mobile/1/the-basics/events.md @@ -0,0 +1,112 @@ +--- +title: Events +order: 200 +--- + +## Overview + +Many native mobile operations take time to complete and await user interaction. PHP isn't really set up to handle this +sort of asynchronous behaviour; it is built to do its work, send a response and move on as quickly as possible. + +NativePHP for Mobile smooths over this disparity between the different paradigms using a simple event system that +handles completion of asynchronous methods using a webhook-/websocket-style approach to notify your Laravel app. + +## Understanding Async vs Sync + +Not all actions are async. Some methods run immediately, and in some cases return a result straight away. + +Here are a few of the **synchronous** APIs: + +```php +Haptics::vibrate(); +System::flashlight(); +Dialog::toast('Hello!'); +``` +Asynchronous actions trigger operations that may complete later. These return immediately, usually with a `bool` or +`void`, allowing PHP's execution to finish. In many of these cases, the user interacts directly with a native component. +When the user has completed their task and the native UI is dismissed, the native app + +```php +// These trigger operations and fire events when complete +Camera::getPhoto(); // → PhotoTaken event +Biometrics::promptForBiometricID(); // → Completed event +PushNotifications::enrollForPushNotifications(); // → TokenGenerated event +``` + +## Basic Event Structure + +All events are standard [Laravel Event classes](https://laravel.com/docs/12.x/events#defining-events). The public +properties of the events contain the pertinent data coming from the native app side. + +## Event Handling + +All asynchronous methods follow the same pattern: + +1. **Call the method** to trigger the operation. +2. **Listen for the appropriate events** to handle the result. +3. **Update your UI** based on the outcome. + +All events get sent directly to JavaScript in the web view _and_ to your PHP application via a special route. This +allows you to listen for these events in the context that best suits your application. + +### On the frontend + +Events are 'broadcast' to the frontend of your application via the web view through a custom `Native` helper. You can +easily listen for these events in JavaScript in two ways: + +- The `Native.on()` helper +- Livewire's `#[On()]` attribute + +#### The `Native.on()` helper + +Register the event listener directly in JavaScript: + +```blade +@@use(Native\Mobile\Events\Alert\ButtonPressed) + + +``` + +#### Livewire's `#[On()]` attribute + +Livewire makes listening to 'broadcast' events simple. Just add the event name, prefixed by `native:` to the `#[On()]` +attribute attached to the method you want to use as its handler: + +```php +use Native\Mobile\Events\Camera\PhotoTaken; + +#[On('native:'.PhotoTaken::class)] +public function handlePhoto(string $path) +{ + // Handle captured photo +} +``` + +### On the backend + +You can also listen for these events on the PHP side as they are simultaneously passed to your Laravel application. + +Simply [add a listener](https://laravel.com/docs/12.x/events#registering-events-and-listeners) as you normally would: + +```php +use App\Services\APIService; +use Native\Mobile\Events\Camera\PhotoTaken; + +class UpdateAvatar +{ + public function __construct(private APIService $api) {} + + public function handle(PhotoTaken $event): void + { + $imageData = base64_encode( + file_get_contents($event->path) + ); + + $this->api->updateAvatar($imageData); + } +} +``` diff --git a/resources/views/docs/mobile/1/the-basics/native-functions.md b/resources/views/docs/mobile/1/the-basics/native-functions.md new file mode 100644 index 00000000..34b88cf6 --- /dev/null +++ b/resources/views/docs/mobile/1/the-basics/native-functions.md @@ -0,0 +1,31 @@ +--- +title: Native Functions +order: 100 +--- + +Our custom PHP extension enables tight integration with each platform, providing a consistent and performant abstraction +that lets you focus on building your app. Build for both platforms while you develop on one. + +Native device functions are called directly from your PHP code, giving you access to platform-specific features while +maintaining the productivity and familiarity of Laravel development. + +These functions are called from your PHP code using an ever-growing list of classes. These classes are also wrapped in +Laravel Facades for ease of access and testing: + +- `Native\Mobile\Facades\Biometrics` +- `Native\Mobile\Facades\Camera` +- `Native\Mobile\Facades\Dialog` +- `Native\Mobile\Facades\Geolocation` +- `Native\Mobile\Facades\Haptics` +- `Native\Mobile\Facades\PushNotifications` +- `Native\Mobile\Facades\SecureStorage` +- `Native\Mobile\Facades\System` + + diff --git a/resources/views/docs/mobile/1/the-basics/overview.md b/resources/views/docs/mobile/1/the-basics/overview.md new file mode 100644 index 00000000..3ef606ce --- /dev/null +++ b/resources/views/docs/mobile/1/the-basics/overview.md @@ -0,0 +1,58 @@ +--- +title: Overview +order: 50 +--- + +NativePHP for Mobile is made up of multiple parts: + +- A Laravel application (PHP) +- The `nativephp/mobile` Composer package +- A custom build of PHP with custom NativePHP extension +- Native applications (Swift & Kotlin) + +## Your Laravel app + +You can build your Laravel application just as you normally would, for the most part, sprinkling native functionality +in where desired by using NativePHP's built-in APIs. + +## `nativephp/mobile` + +The package is a pretty normal Composer package. It contains the PHP code needed to interface with the NativePHP +extension, the tools to install and run your applications, and all the code for each native application - iOS and +Android. + +## The PHP builds + +When you run the `native:install` Artisan command, the package will fetch the appropriate versions of the custom-built +PHP binaries. + +NativePHP for Mobile currently bundles **PHP 8.4**. You should ensure that your application is built to work with this +version of PHP. + +These custom PHP builds have been compiled specifically to target the mobile platforms and cannot be used in other +contexts. + +They are compiled as embeddable C libraries and embedded _into_ the native application. In this way, PHP doesn't run as +a separate process/service under a typical web server environment; essentially, the native application itself is +extended with the capability to execute your PHP code. + +Your Laravel application is then executed directly by the native app, using the embedded PHP engine to run the code. +This runs PHP as close to natively as it can get. It is very fast and efficient on modern hardware. + +## The native apps + +NativePHP ships one app for iOS and one for Android. When you run the `native:run` Artisan command, your Laravel app is +packaged up and copied into one of these apps. + +To build for both platforms, you must run the `native:run` command twice, targeting each platform. + +Each native app "shell" runs a number of steps to prepare the environment each time your application is booted, +including: + +- Checking to see if the bundled version of your Laravel app is newer than the installed version + - Installing the newer version if necessary +- Running migrations +- Clearing caches + +Normally, this process takes just a couple of seconds in normal use. After your app has been updated, it will take a +few seconds longer. diff --git a/resources/views/docs/mobile/1/the-basics/splash-screens.md b/resources/views/docs/mobile/1/the-basics/splash-screens.md new file mode 100644 index 00000000..1c803e4e --- /dev/null +++ b/resources/views/docs/mobile/1/the-basics/splash-screens.md @@ -0,0 +1,18 @@ +--- +title: Splash Screens +order: 400 +--- + +NativePHP makes it easy to add custom splash screens to your iOS and Android apps. + +## Supply your Splash Screens + +Place the relevant files in the locations specified: + +- `public/splash.png` - for the Light Mode splash screen +- `public/splash-dark.png` - for the Dark Mode splash screen + +### Requirements +- Format: PNG +- Minimum Size/Ratio: 1080 × 1920 pixels +- GD PHP extension must be enabled, ensure it has enough memory (~2GB should be enough) diff --git a/resources/views/docs/mobile/1/the-basics/web-view.md b/resources/views/docs/mobile/1/the-basics/web-view.md new file mode 100644 index 00000000..2f4183e3 --- /dev/null +++ b/resources/views/docs/mobile/1/the-basics/web-view.md @@ -0,0 +1,132 @@ +--- +title: Web View +order: 60 +--- + +Every mobile app built with NativePHP centers around a single native web view. The web view allows you to use whichever +web technologies you are most comfortable with to build your app's user interface (UI). + +You're not limited to any one tool or framework — you can use Livewire, Vue, React, Svelte, HTMX... even jQuery! +Whatever you're most comfortable with for building a web UI, you can use to build a mobile app with NativePHP. + +The web view is rendered to fill the entire view of your application and is intended to remain visible to your users at +all times — except when another full-screen action takes place, such as accessing the camera or an in-app browser. + +## The Viewport + +Just like a normal browser, the web view has the concept of a **viewport** which represents the viewable area of the +page. The viewport can be controlled with the `viewport` meta tag, just as you would in a traditional web application: + +```html + +``` + +### Disable Zoom +When building mobile apps, you may want to have a little more control over the experience. For example, you may +want to disable user-controlled zoom, allowing your app to behave similarly to a traditional native app. + +To achieve this, you can set `user-scalable=no`: + +```html + +``` + +## Edge-to-Edge + +To give you the most flexibility in how you design your app's UI, the web view occupies the entire screen, allowing you +to render anything anywhere on the display whilst your app is in the foreground using just HTML, CSS and JavaScript. + +But you should bear in mind that not all parts of the display are visible to the user. Many devices have camera +notches, rounded corners and curved displays. These areas may still be considered part of the `viewport`, but they may +be invisible and/or non-interactive. + +To account for this in your UI, you should set the `viewport-fit=cover` option in your `viewport` meta tag and use the +safe area insets. + +### Safe Areas + +Safe areas are the sections of the display which are not obscured by either a physical interruption (a rounded corner +or camera), or some persistent UI, such as the Home Indicator (a.k.a. the bottom bar) or notch. + +Safe areas are calculated for your app by the device at runtime and adjust according to its orientation, allowing your +UI to be responsive to the various device configurations with a simple and predictable set of CSS rules. + +The fundamental building blocks are a set of four values known as `insets`. These are injected into your pages as the +following CSS variables: + +- `--inset-top` +- `--inset-bottom` +- `--inset-left` +- `--inset-right` + +You can apply these insets in whichever way you need to build a usable interface. + +There is also a handy `nativephp-safe-area` CSS class that can be applied to most elements to ensure they sit within +the safe areas of the display. + +Say you want a `fixed`-position header bar like this: + +![](/img/docs/viewport-fit-cover.png) + +If you're using Tailwind, you might try something like this: + +```html +
+ ... +
+``` + +If you tried to do this without `viewport-fit=cover` and use of the safe areas, here's what you'd end up with in +portrait view: + +![](/img/docs/viewport-default.png) + +And it may be even worse in landscape view: + +![](/img/docs/viewport-default-landscape.png) + +But by adding a few simple adjustments to our page, we can make it beautiful again (Well, maybe we should lose the +red...): + +```html + +
+ ... +
+``` + +![](/img/docs/viewport-fit-cover-landscape.png) + +### Status Bar Style + +On Android, the icons in the Status Bar do not change color automatically based on the background color in your app. +By default, they change based on whether the device is in Light/Dark Mode. + +If you have a consistent background color in both light and dark mode, you may use the `nativephp.status_bar_style` +config key to set the appropriate status bar style for your app to give users the best experience. + +The possible options are: + +- `auto` - the default, which changes based on the device's Dark Mode setting +- `light` - ideal if your app's background is dark-colored +- `dark` - better if your app's background is light-colored + + + +With just a few small changes, we've been able to define a layout that will work well on a multitude of devices +without having to add complex calculations or lots of device-specific CSS rules to our code. diff --git a/resources/views/docs/mobile/2/_index.md b/resources/views/docs/mobile/2/_index.md new file mode 100644 index 00000000..c719fbe2 --- /dev/null +++ b/resources/views/docs/mobile/2/_index.md @@ -0,0 +1,4 @@ +--- +title: Mobile +order: 1 +--- diff --git a/resources/views/docs/mobile/2/apis/_index.md b/resources/views/docs/mobile/2/apis/_index.md new file mode 100644 index 00000000..6b4bb8c5 --- /dev/null +++ b/resources/views/docs/mobile/2/apis/_index.md @@ -0,0 +1,4 @@ +--- +title: APIs +order: 50 +--- diff --git a/resources/views/docs/mobile/2/apis/biometrics.md b/resources/views/docs/mobile/2/apis/biometrics.md new file mode 100644 index 00000000..2a189cef --- /dev/null +++ b/resources/views/docs/mobile/2/apis/biometrics.md @@ -0,0 +1,163 @@ +--- +title: Biometrics +order: 100 +--- + +## Overview + +The Biometrics API allows you to authenticate users using their device's biometric sensors like Face ID, Touch ID, or +fingerprint scanners. + + + + + +```php +use Native\Mobile\Facades\Biometrics; +``` + + + + +```js +import { biometric, on, off, Events } from '#nativephp'; +``` + + + + +## Methods + +### `prompt()` + +Prompts the user for biometric authentication. + + + + + +```php +use Native\Mobile\Facades\Biometrics; + +Biometrics::prompt(); +``` + + + + +```js +// Basic usage +await biometric.prompt(); + +// With an identifier for tracking +await biometric.prompt() + .id('secure-action-auth'); +``` + + + + +## Events + +### `Completed` + +Fired when biometric authentication completes (success or failure). + + + + + +```php +use Native\Mobile\Attributes\OnNative; +use Native\Mobile\Events\Biometric\Completed; + +#[OnNative(Completed::class)] +public function handle(bool $success) +{ + if ($success) { + // User authenticated successfully + $this->unlockSecureFeature(); + } else { + // Authentication failed + $this->showErrorMessage(); + } +} +``` + + + + +```js +import { biometric, on, off, Events } from '#nativephp'; +import { ref, onMounted, onUnmounted } from 'vue'; + +const isAuthenticated = ref(false); + +const handleBiometricComplete = (payload) => { + if (payload.success) { + isAuthenticated.value = true; + unlockSecureFeature(); + } else { + showErrorMessage(); + } +}; + +const authenticate = async () => { + await biometric.prompt(); +}; + +onMounted(() => { + on(Events.Biometric.Completed, handleBiometricComplete); +}); + +onUnmounted(() => { + off(Events.Biometric.Completed, handleBiometricComplete); +}); +``` + + + + +```jsx +import { biometric, on, off, Events } from '#nativephp'; +import { useState, useEffect } from 'react'; + +const [isAuthenticated, setIsAuthenticated] = useState(false); + +const handleBiometricComplete = (payload) => { + if (payload.success) { + setIsAuthenticated(true); + unlockSecureFeature(); + } else { + showErrorMessage(); + } +}; + +const authenticate = async () => { + await biometric.prompt(); +}; + +useEffect(() => { + on(Events.Biometric.Completed, handleBiometricComplete); + + return () => { + off(Events.Biometric.Completed, handleBiometricComplete); + }; +}, []); +``` + + + + +## Platform Support + +- **iOS:** Face ID, Touch ID +- **Android:** Fingerprint, Face unlock, other biometric methods +- **Fallback:** System authentication (PIN, password, pattern) + +## Security Notes + +- Biometric authentication provides **convenience**, not absolute security +- Always combine with other authentication factors for sensitive operations +- Consider implementing session timeouts for unlocked states +- Users can potentially bypass biometrics if their device is compromised diff --git a/resources/views/docs/mobile/2/apis/browser.md b/resources/views/docs/mobile/2/apis/browser.md new file mode 100644 index 00000000..56c158aa --- /dev/null +++ b/resources/views/docs/mobile/2/apis/browser.md @@ -0,0 +1,114 @@ +--- +title: Browser +order: 200 +--- + +## Overview + +The Browser API provides three methods for opening URLs, each designed for specific use cases: +in-app browsing, system browser navigation, and web authentication flows. + + + + + +```php +use Native\Mobile\Facades\Browser; +``` + + + + +```js +import { browser } from '#nativephp'; +``` + + + + +## Methods + +### `inApp()` + +Opens a URL in an embedded browser within your app using Custom Tabs (Android) or SFSafariViewController (iOS). + + + + + +```php +Browser::inApp('https://nativephp.com/mobile'); +``` + + + + +```js +await browser.inApp('https://nativephp.com/mobile'); +``` + + + + +### `open()` + +Opens a URL in the device's default browser app, leaving your application entirely. + + + + + +```php +Browser::open('https://nativephp.com/mobile'); +``` + + + + +```js +await browser.open('https://nativephp.com/mobile'); +``` + + + + +### `auth()` + +Opens a URL in a specialized authentication browser designed for OAuth flows with automatic `nativephp://` redirect handling. + + + + + +```php +Browser::auth('https://provider.com/oauth/authorize?client_id=123&redirect_uri=nativephp://127.0.0.1/auth/callback'); +``` + + + + +```js +await browser.auth('https://provider.com/oauth/authorize?client_id=123&redirect_uri=nativephp://127.0.0.1/auth/callback'); +``` + + + + +## Use Cases + +### When to Use Each Method + +**`inApp()`** - Keep users within your app experience: +- Documentation, help pages, terms of service +- External content that relates to your app +- When you want users to easily return to your app + +**`open()`** - Full browser experience needed: +- Complex web applications +- Content requiring specific browser features +- When users need bookmarking or sharing capabilities + +**`auth()`** - OAuth authentication flows: +- Login with WorkOS, Auth0, Google, Facebook, etc. +- Secure authentication with automatic redirects +- Isolated browser session for security diff --git a/resources/views/docs/mobile/2/apis/camera.md b/resources/views/docs/mobile/2/apis/camera.md new file mode 100644 index 00000000..aeeb7a91 --- /dev/null +++ b/resources/views/docs/mobile/2/apis/camera.md @@ -0,0 +1,558 @@ +--- +title: Camera +order: 300 +--- + +## Overview + +The Camera API provides access to the device's camera for taking photos, recording videos, and selecting media from the gallery. + + + + + +```php +use Native\Mobile\Facades\Camera; +``` + + + + +```js +import { camera, on, off, Events } from '#nativephp'; +``` + + + + +## Methods + +### `getPhoto()` + +Opens the camera interface to take a photo. + + + + + +```php +Camera::getPhoto(); +``` + + + + +```js +// Basic usage +await camera.getPhoto(); + +// With identifier for tracking +await camera.getPhoto() + .id('profile-pic'); +``` + + + + +### `recordVideo()` + +Opens the camera interface to record a video with optional configuration. + +**Parameters:** +- `array $options` - Optional recording options (default: `[]`) + +**Returns:** `PendingVideoRecorder` - Fluent interface for configuring video recording + + + + + +```php +// Basic video recording +Camera::recordVideo(); + +// With maximum duration (30 seconds) +Camera::recordVideo(['maxDuration' => 30]); + +// Using fluent API +Camera::recordVideo() + ->maxDuration(60) + ->id('my-video-123') + ->start(); +``` + + + + +```js +// Basic video recording +await camera.recordVideo(); + +// With maximum duration +await camera.recordVideo() + .maxDuration(60); + +// With identifier for tracking +await camera.recordVideo() + .maxDuration(30) + .id('my-video-123'); +``` + + + + +### `pickImages()` + +Opens the gallery/photo picker to select existing images. + +**Parameters:** +- `string $media_type` - Type of media to pick: `'all'`, `'images'`, `'videos'` (default: `'all'`) +- `bool $multiple` - Allow multiple selection (default: `false`) + +**Returns:** `bool` - `true` if picker opened successfully + + + + + +```php +// Pick a single image +Camera::pickImages('images', false); + +// Pick multiple images +Camera::pickImages('images', true); + +// Pick any media type +Camera::pickImages('all', true); +``` + + + + +```js +// Pick images using fluent API +await camera.pickImages() + .images() + .multiple() + .maxItems(5); + +// Pick only videos +await camera.pickImages() + .videos() + .multiple(); + +// Pick any media type +await camera.pickImages() + .all() + .multiple() + .maxItems(10); + +// Single image selection +await camera.pickImages() + .images(); +``` + + + + +## PendingVideoRecorder + +The fluent API returned by `recordVideo()` provides several methods for configuring video recording: + +### `maxDuration(int $seconds)` + +Set the maximum recording duration in seconds. + +```php +Camera::recordVideo() + ->maxDuration(30) + ->start(); +``` + +### `id(string $id)` + +Set a unique identifier for this recording to correlate with events. + +```php +Camera::recordVideo() + ->id('user-upload-video') + ->start(); +``` + +### `getId()` + +Get the recorder's unique identifier (auto-generates UUID if not set). + +```php +$recorder = Camera::recordVideo(); +$id = $recorder->getId(); // Returns UUID +``` + +### `event(string $eventClass)` + +Set a custom event class to dispatch when recording completes. + +```php +Camera::recordVideo() + ->event(MyCustomVideoEvent::class) + ->start(); +``` + +### `remember()` + +Store the recorder's ID in the session for later retrieval. + +```php +Camera::recordVideo() + ->remember() + ->start(); + +// Later, in your event handler +$recorderId = PendingVideoRecorder::lastId(); +``` + +### `start()` + +Explicitly start the video recording. If not called, recording starts automatically. + +```php +Camera::recordVideo() + ->maxDuration(60) + ->start(); +``` + +## Events + +### `PhotoTaken` + +Fired when a photo is taken with the camera. + +**Payload:** `string $path` - File path to the captured photo + + + + + +```php +use Native\Mobile\Attributes\OnNative; +use Native\Mobile\Events\Camera\PhotoTaken; + +#[OnNative(PhotoTaken::class)] +public function handlePhotoTaken(string $path) +{ + // Process the captured photo + $this->processPhoto($path); +} +``` + + + + +```js +import { on, off, Events } from '#nativephp'; +import { ref, onMounted, onUnmounted } from 'vue'; + +const photoPath = ref(''); + +const handlePhotoTaken = (payload) => { + photoPath.value = payload.path; + processPhoto(payload.path); +}; + +onMounted(() => { + on(Events.Camera.PhotoTaken, handlePhotoTaken); +}); + +onUnmounted(() => { + off(Events.Camera.PhotoTaken, handlePhotoTaken); +}); +``` + + + + +```jsx +import { on, off, Events } from '#nativephp'; +import { useState, useEffect } from 'react'; + +const [photoPath, setPhotoPath] = useState(''); + +const handlePhotoTaken = (payload) => { + setPhotoPath(payload.path); + processPhoto(payload.path); +}; + +useEffect(() => { + on(Events.Camera.PhotoTaken, handlePhotoTaken); + + return () => { + off(Events.Camera.PhotoTaken, handlePhotoTaken); + }; +}, []); +``` + + + + +### `VideoRecorded` + +Fired when a video is successfully recorded. + +**Payload:** +- `string $path` - File path to the recorded video +- `string $mimeType` - Video MIME type (default: `'video/mp4'`) +- `?string $id` - Optional identifier if set via `id()` method + + + + + +```php +use Native\Mobile\Attributes\OnNative; +use Native\Mobile\Events\Camera\VideoRecorded; + +#[OnNative(VideoRecorded::class)] +public function handleVideoRecorded(string $path, string $mimeType, ?string $id = null) +{ + // Process the recorded video + $this->processVideo($path); + + // Check if this is the video we're expecting + if ($id === 'my-upload-video') { + $this->uploadVideo($path); + } +} +``` + + + + +```js +import { on, off, Events } from '#nativephp'; +import { ref, onMounted, onUnmounted } from 'vue'; + +const videoPath = ref(''); + +const handleVideoRecorded = (payload) => { + const { path, mimeType, id } = payload; + videoPath.value = path; + processVideo(path); + + if (id === 'my-upload-video') { + uploadVideo(path); + } +}; + +onMounted(() => { + on(Events.Camera.VideoRecorded, handleVideoRecorded); +}); + +onUnmounted(() => { + off(Events.Camera.VideoRecorded, handleVideoRecorded); +}); +``` + + + + +```jsx +import { on, off, Events } from '#nativephp'; +import { useState, useEffect } from 'react'; + +const [videoPath, setVideoPath] = useState(''); + +const handleVideoRecorded = (payload) => { + const { path, mimeType, id } = payload; + setVideoPath(path); + processVideo(path); + + if (id === 'my-upload-video') { + uploadVideo(path); + } +}; + +useEffect(() => { + on(Events.Camera.VideoRecorded, handleVideoRecorded); + + return () => { + off(Events.Camera.VideoRecorded, handleVideoRecorded); + }; +}, []); +``` + + + + +### `VideoCancelled` + +Fired when video recording is cancelled by the user. + +**Payload:** +- `bool $cancelled` - Always `true` +- `?string $id` - Optional identifier if set via `id()` method + + + + + +```php +use Native\Mobile\Attributes\OnNative; +use Native\Mobile\Events\Camera\VideoCancelled; + +#[OnNative(VideoCancelled::class)] +public function handleVideoCancelled(bool $cancelled, ?string $id = null) +{ + // Handle cancellation + $this->notifyUser('Video recording was cancelled'); +} +``` + + + + +```js +import { on, off, Events } from '#nativephp'; +import { onMounted, onUnmounted } from 'vue'; + +const handleVideoCancelled = (payload) => { + notifyUser('Video recording was cancelled'); +}; + +onMounted(() => { + on(Events.Camera.VideoCancelled, handleVideoCancelled); +}); + +onUnmounted(() => { + off(Events.Camera.VideoCancelled, handleVideoCancelled); +}); +``` + + + + +```jsx +import { on, off, Events } from '#nativephp'; +import { useEffect } from 'react'; + +const handleVideoCancelled = (payload) => { + notifyUser('Video recording was cancelled'); +}; + +useEffect(() => { + on(Events.Camera.VideoCancelled, handleVideoCancelled); + + return () => { + off(Events.Camera.VideoCancelled, handleVideoCancelled); + }; +}, []); +``` + + + + +### `MediaSelected` + +Fired when media is selected from the gallery. + +**Payload:** `array $media` - Array of selected media items + + + + + +```php +use Native\Mobile\Attributes\OnNative; +use Native\Mobile\Events\Gallery\MediaSelected; + +#[OnNative(MediaSelected::class)] +public function handleMediaSelected($success, $files, $count) +{ + foreach ($files as $file) { + // Process each selected media item + $this->processMedia($file); + } +} +``` + + + + +```js +import { on, off, Events } from '#nativephp'; +import { ref, onMounted, onUnmounted } from 'vue'; + +const selectedFiles = ref([]); + +const handleMediaSelected = (payload) => { + const { success, files, count } = payload; + + if (success) { + selectedFiles.value = files; + files.forEach(file => processMedia(file)); + } +}; + +onMounted(() => { + on(Events.Gallery.MediaSelected, handleMediaSelected); +}); + +onUnmounted(() => { + off(Events.Gallery.MediaSelected, handleMediaSelected); +}); +``` + + + + +```jsx +import { on, off, Events } from '#nativephp'; +import { useState, useEffect } from 'react'; + +const [selectedFiles, setSelectedFiles] = useState([]); + +const handleMediaSelected = (payload) => { + const { success, files, count } = payload; + + if (success) { + setSelectedFiles(files); + files.forEach(file => processMedia(file)); + } +}; + +useEffect(() => { + on(Events.Gallery.MediaSelected, handleMediaSelected); + + return () => { + off(Events.Gallery.MediaSelected, handleMediaSelected); + }; +}, []); +``` + + + + +## Storage Locations + +Media files are stored in different locations depending on the platform: + +**Photos:** +- **Android:** App cache directory at `{cache}/captured.jpg` +- **iOS:** Application Support at `~/Library/Application Support/Photos/captured.jpg` + +**Videos:** +- **Android:** App cache directory at `{cache}/video_{timestamp}.mp4` +- **iOS:** Application Support at `~/Library/Application Support/Videos/captured_video_{timestamp}.mp4` + +**Important Notes:** +- Android stores media in the cache directory (temporary, can be cleared by system) +- iOS stores media in Application Support (persistent, excluded from backups) +- iOS photo captures use a fixed filename `captured.jpg` (overwrites previous) +- iOS/Android videos use timestamped filenames (don't overwrite) + +## Notes + +- **Permissions:** You must enable the `camera` permission in `config/nativephp.php` to use camera features. Once enabled, camera permissions are handled automatically on both platforms, and users will be prompted for permission the first time your app requests camera access. +- If permission is denied, camera functions will fail silently +- Camera permission is required for photos, videos, AND QR/barcode scanning +- File formats: JPEG for photos, MP4 for videos (platform-dependent) +- Video quality and camera selection are controlled by the native camera app diff --git a/resources/views/docs/mobile/2/apis/device.md b/resources/views/docs/mobile/2/apis/device.md new file mode 100644 index 00000000..ccf79314 --- /dev/null +++ b/resources/views/docs/mobile/2/apis/device.md @@ -0,0 +1,184 @@ +--- +title: Device +order: 400 +--- + +## Overview + +The Device API exposes internal information about the device, such as the model and operating system version, along with user information such as unique ids. + + + + + +```php +use Native\Mobile\Facades\Device; +``` + + + + +```js +import { device } from '#nativephp'; +``` + + + + +## Methods + +### `getId()` + +Return a unique identifier for the device. + +Returns: `string` + + + + + +```php +$id = Device::getId(); +``` + + + + +```js +const result = await device.getId(); +const deviceId = result.id; +``` + + + + +### `getInfo()` + +Return information about the underlying device/os/platform. + +Returns JSON encoded: `string` + + + + + +```php +$info = Device::getInfo(); +$deviceInfo = json_decode($info); +``` + + + + +```js +const result = await device.getInfo(); +const deviceInfo = JSON.parse(result.info); + +console.log(deviceInfo.platform); // 'ios' or 'android' +console.log(deviceInfo.model); // e.g., 'iPhone13,4' +console.log(deviceInfo.osVersion); // e.g., '17.0' +``` + + + + +### `vibrate()` + +Triggers device vibration for tactile feedback. + +**Returns:** `void` + + + + + +```php +Device::vibrate(); +``` + + + + +```js +await device.vibrate(); +``` + + + + +### `flashlight()` + +Toggles the device flashlight (camera flash LED) on and off. + +**Returns:** `void` + + + + + +```php +Device::flashlight(); // Toggle flashlight state +``` + + + + +```js +const result = await device.flashlight(); +console.log(result.state); // true = on, false = off +``` + + + + +### `getBatteryInfo()` + +Return information about the battery. + +Returns JSON encoded: `string` + + + + + +```php +$info = Device::getBatteryInfo(); +$batteryInfo = json_decode($info); +``` + + + + +```js +const result = await device.getBatteryInfo(); +const batteryInfo = JSON.parse(result.info); + +console.log(batteryInfo.batteryLevel); // 0-1 (e.g., 0.85 = 85%) +console.log(batteryInfo.isCharging); // true/false +``` + + + + +## Device Info + +| Prop | Type | Description +|---|---|---|---| +| name | string | The name of the device. For example, "John's iPhone". On iOS 16+ this will return a generic device name without the appropriate entitlements. +| model | string | The device model. For example, "iPhone13,4". +| platform | 'ios' \| 'android' | The device platform (lowercase). +| operatingSystem | string | The operating system of the device. +| osVersion | string | The version of the device OS. +| iOSVersion | number | The iOS version number. Only available on iOS. Multi-part version numbers are crushed down into an integer padded to two-digits, e.g., "16.3.1" → `160301`. | 5.0.0 | +| androidSDKVersion | number | The Android SDK version number. Only available on Android. | 5.0.0 | +| manufacturer | string | The manufacturer of the device. +| isVirtual | boolean | Whether the app is running in a simulator/emulator. +| memUsed | number | Approximate memory used by the current app, in bytes. Divide by 1,048,576 to get MBs used. +| webViewVersion | string | The web view browser version. + +## Battery Info + +| Prop | Type | Description +|---|---|---|---| +| batteryLevel | number | A percentage (0 to 1) indicating how much the battery is charged. +| isCharging | boolean | Whether the device is charging. diff --git a/resources/views/docs/mobile/2/apis/dialog.md b/resources/views/docs/mobile/2/apis/dialog.md new file mode 100644 index 00000000..1a72f6be --- /dev/null +++ b/resources/views/docs/mobile/2/apis/dialog.md @@ -0,0 +1,232 @@ +--- +title: Dialog +order: 500 +--- + +## Overview + +The Dialog API provides access to native UI elements like alerts, toasts, and sharing interfaces. + + + + + +```php +use Native\Mobile\Facades\Dialog; +``` + + + + +```js +import { dialog, on, off, Events } from '#nativephp'; +``` + + + + +## Methods + +### `alert()` + +Displays a native alert dialog with customizable buttons. + +**Parameters:** +- `string $title` - The alert title +- `string $message` - The alert message +- `array $buttons` - Array of button labels (max 3 buttons) + +**Button Positioning:** +- **1 button** - Positive (OK/Confirm) +- **2 buttons** - Negative (Cancel) + Positive (OK/Confirm) +- **3 buttons** - Negative (Cancel) + Neutral (Maybe) + Positive (OK/Confirm) + + + + + +```php +Dialog::alert( + 'Confirm Action', + 'Are you sure you want to delete this item?', + ['Cancel', 'Delete'] +); +``` + + + + +```js +// Simple usage +await dialog.alert('Confirm Action', 'Are you sure you want to delete this item?', ['Cancel', 'Delete']); + +// Fluent builder API +await dialog.alert() + .title('Confirm Action') + .message('Are you sure you want to delete this item?') + .buttons(['Cancel', 'Delete']); + +// Quick confirm dialog (OK/Cancel) +await dialog.alert() + .confirm('Confirm Action', 'Are you sure?'); + +// Quick destructive confirm (Cancel/Delete) +await dialog.alert() + .confirmDelete('Delete Item', 'This action cannot be undone.'); +``` + + + + +### `toast()` + +Displays a brief toast notification message. + +**Parameters:** +- `string $message` - The message to display + + + + + +```php +Dialog::toast('Item saved successfully!'); +``` + + + + +```js +await dialog.toast('Item saved successfully!'); +``` + + + + +#### Good toast messages + +- Short and clear +- Great for confirmations and status updates +- Don't rely on them for critical information +- Avoid showing multiple toasts in quick succession + +### `share()` + + + +Opens the native sharing interface. + +**Parameters:** +- `string $title` - The share dialog title +- `string $text` - Text content to share +- `string $url` - URL to share + +```php +Dialog::share( + 'Check this out!', + 'I found this amazing Laravel package for mobile development', + 'https://nativephp.com' +); +``` + +## Events + +### `ButtonPressed` + +Fired when a button is pressed in an alert dialog. + +**Payload:** +- `int $index` - Index of the pressed button (0-based) +- `string $label` - Label/text of the pressed button + + + + + +```php +use Native\Mobile\Attributes\OnNative; +use Native\Mobile\Events\Alert\ButtonPressed; + +#[OnNative(ButtonPressed::class)] +public function handleAlertButton($index, $label) +{ + switch ($index) { + case 0: + // First button (usually Cancel) + Dialog::toast("You pressed '{$label}'"); + break; + case 1: + // Second button (usually OK/Confirm) + $this->performAction(); + Dialog::toast("You pressed '{$label}'"); + break; + } +} +``` + + + + +```js +import { dialog, on, off, Events } from '#nativephp'; +import { ref, onMounted, onUnmounted } from 'vue'; + +const buttonLabel = ref(''); + +const handleButtonPressed = (payload) => { + const { index, label } = payload; + buttonLabel.value = label; + + if (index === 0) { + dialog.toast(`You pressed '${label}'`); + } else if (index === 1) { + performAction(); + dialog.toast(`You pressed '${label}'`); + } +}; + +onMounted(() => { + on(Events.Alert.ButtonPressed, handleButtonPressed); +}); + +onUnmounted(() => { + off(Events.Alert.ButtonPressed, handleButtonPressed); +}); +``` + + + + +```jsx +import { dialog, on, off, Events } from '#nativephp'; +import { useState, useEffect } from 'react'; + +const [buttonLabel, setButtonLabel] = useState(''); + +const handleButtonPressed = (payload) => { + const { index, label } = payload; + setButtonLabel(label); + + if (index === 0) { + dialog.toast(`You pressed '${label}'`); + } else if (index === 1) { + performAction(); + dialog.toast(`You pressed '${label}'`); + } +}; + +useEffect(() => { + on(Events.Alert.ButtonPressed, handleButtonPressed); + + return () => { + off(Events.Alert.ButtonPressed, handleButtonPressed); + }; +}, []); +``` + + + diff --git a/resources/views/docs/mobile/2/apis/file.md b/resources/views/docs/mobile/2/apis/file.md new file mode 100644 index 00000000..a2c266d0 --- /dev/null +++ b/resources/views/docs/mobile/2/apis/file.md @@ -0,0 +1,187 @@ +--- +title: File +order: 600 +--- + +## Overview + +The File API provides utilities for managing files on the device. You can move files between directories or copy files to new locations. These operations execute synchronously and return a boolean indicating success or failure. + + + + + +```php +use Native\Mobile\Facades\File; +``` + + + + +```js +import { file } from '#nativephp'; +``` + + + + +## Methods + +### `move(string $from, string $to)` + +Moves a file from one location to another. The source file is removed from its original location after being moved successfully. + +**Parameters:** +- `string $from` - Absolute path to the source file +- `string $to` - Absolute path to the destination file + +**Returns:** `bool` - `true` on success, `false` on failure + + + + + +```php +// Move a captured photo to the app's storage directory +$success = File::move( + '/var/mobile/Containers/Data/tmp/photo.jpg', + '/var/mobile/Containers/Data/Documents/photos/photo.jpg' +); + +if ($success) { + // File moved successfully +} else { + // Move operation failed +} +``` + + + + +```js +// Move a captured photo to the app's storage directory +const result = await file.move( + '/var/mobile/Containers/Data/tmp/photo.jpg', + '/var/mobile/Containers/Data/Documents/photos/photo.jpg' +); + +if (result.success) { + // File moved successfully +} else { + // Move operation failed +} +``` + + + + +### `copy(string $from, string $to)` + +Copies a file to a new location. The source file remains in its original location. + +**Parameters:** +- `string $from` - Absolute path to the source file +- `string $to` - Absolute path to the destination file + +**Returns:** `bool` - `true` on success, `false` on failure + + + + + +```php +// Copy a file to create a backup +$success = File::copy( + '/var/mobile/Containers/Data/Documents/document.pdf', + '/var/mobile/Containers/Data/Documents/backups/document.pdf' +); + +if ($success) { + // File copied successfully +} else { + // Copy operation failed +} +``` + + + + +```js +// Copy a file to create a backup +const result = await file.copy( + '/var/mobile/Containers/Data/Documents/document.pdf', + '/var/mobile/Containers/Data/Documents/backups/document.pdf' +); + +if (result.success) { + // File copied successfully +} else { + // Copy operation failed +} +``` + + + + +## Examples + +### Moving Captured Media + +After capturing media with the camera or audio API, move it from the temporary directory to permanent storage: + +```php +use Native\Mobile\Attributes\OnNative; +use Native\Mobile\Events\Camera\PhotoTaken; +use Native\Mobile\Facades\File; + +#[OnNative(PhotoTaken::class)] +public function handlePhotoTaken(string $path) +{ + $destination = storage_path('app/photos/'.basename($path)); + + if (File::move($path, $destination)) { + $this->photo_path = $destination; + } else { + $this->error = 'Failed to save photo'; + } +} +``` + +### Copying Files for Backup + +Create backups of important files: + +```php +$original = storage_path('app/documents/contract.pdf'); +$backup = storage_path('app/backups/contract_backup.pdf'); + +if (File::copy($original, $backup)) { + // Backup created successfully +} else { + // Handle backup failure +} +``` + +### Organizing Files by Date + +Move files into date-based directories: + +```php +use Native\Mobile\Facades\File; + +$today = now()->format('Y-m-d'); +$source = storage_path('app/uploads/file.jpg'); +$destination = storage_path("app/uploads/{$today}/file.jpg"); + +File::move($source, $destination); +``` + +## Notes + +- File paths must be absolute paths. Use Laravel's `storage_path()` helper to construct paths +- Both the source file and destination directory must exist and be accessible +- If the source file does not exist, the operation fails and returns `false` +- If the destination file already exists, the operation fails and returns `false` +- These operations are synchronous and block execution until completion +- Ensure your app has the necessary file system permissions to read from the source and write to the destination +- No events are dispatched by these operations; they return results directly diff --git a/resources/views/docs/mobile/2/apis/geolocation.md b/resources/views/docs/mobile/2/apis/geolocation.md new file mode 100644 index 00000000..6e0d361d --- /dev/null +++ b/resources/views/docs/mobile/2/apis/geolocation.md @@ -0,0 +1,412 @@ +--- +title: Geolocation +order: 700 +--- + +## Overview + +The Geolocation API provides access to the device's GPS and location services to determine the user's current position. + + + + + +```php +use Native\Mobile\Facades\Geolocation; +``` + + + + +```js +import { geolocation, on, off, Events } from '#nativephp'; +``` + + + + +## Methods + +### `getCurrentPosition()` + +Gets the current GPS location of the device. + +**Parameters:** +- `bool $fineAccuracy` - Whether to use high accuracy mode (GPS vs network) (default: `false`) + +**Returns:** Location data via events + + + + + +```php +// Get location using network positioning (faster, less accurate) +Geolocation::getCurrentPosition(); + +// Get location using GPS (slower, more accurate) +Geolocation::getCurrentPosition(true); +``` + + + + +```js +// Get location using network positioning (faster, less accurate) +await geolocation.getCurrentPosition(); + +// Get location using GPS (slower, more accurate) +await geolocation.getCurrentPosition() + .fineAccuracy(true); + +// With identifier for tracking +await geolocation.getCurrentPosition() + .fineAccuracy(true) + .id('current-loc'); +``` + + + + +### `checkPermissions()` + +Checks the current location permissions status. + +**Returns:** Permission status via events + + + + + +```php +Geolocation::checkPermissions(); +``` + + + + +```js +await geolocation.checkPermissions(); +``` + + + + +### `requestPermissions()` + +Requests location permissions from the user. + +**Returns:** Permission status after request via events + + + + + +```php +Geolocation::requestPermissions(); +``` + + + + +```js +await geolocation.requestPermissions(); + +// With remember flag +await geolocation.requestPermissions() + .remember(); +``` + + + + +## Events + +### `LocationReceived` + +Fired when location data is requested (success or failure). + +**Event Parameters:** +- `bool $success` - Whether location was successfully retrieved +- `float $latitude` - Latitude coordinate (when successful) +- `float $longitude` - Longitude coordinate (when successful) +- `float $accuracy` - Accuracy in meters (when successful) +- `int $timestamp` - Unix timestamp of location fix +- `string $provider` - Location provider used (GPS, network, etc.) +- `string $error` - Error message (when unsuccessful) + + + + + +```php +use Native\Mobile\Attributes\OnNative; +use Native\Mobile\Events\Geolocation\LocationReceived; + +#[OnNative(LocationReceived::class)] +public function handleLocationReceived( + $success = null, + $latitude = null, + $longitude = null, + $accuracy = null, + $timestamp = null, + $provider = null, + $error = null +) { + // ... +} +``` + + + + +```js +import { on, off, Events } from '#nativephp'; +import { ref, onMounted, onUnmounted } from 'vue'; + +const location = ref({ latitude: null, longitude: null }); +const error = ref(''); + +const handleLocationReceived = (payload) => { + if (payload.success) { + location.value = { + latitude: payload.latitude, + longitude: payload.longitude + }; + } else { + error.value = payload.error; + } +}; + +onMounted(() => { + on(Events.Geolocation.LocationReceived, handleLocationReceived); +}); + +onUnmounted(() => { + off(Events.Geolocation.LocationReceived, handleLocationReceived); +}); +``` + + + + +```jsx +import { on, off, Events } from '#nativephp'; +import { useState, useEffect } from 'react'; + +const [location, setLocation] = useState({ latitude: null, longitude: null }); +const [error, setError] = useState(''); + +const handleLocationReceived = (payload) => { + if (payload.success) { + setLocation({ + latitude: payload.latitude, + longitude: payload.longitude + }); + } else { + setError(payload.error); + } +}; + +useEffect(() => { + on(Events.Geolocation.LocationReceived, handleLocationReceived); + + return () => { + off(Events.Geolocation.LocationReceived, handleLocationReceived); + }; +}, []); +``` + + + + +### `PermissionStatusReceived` + +Fired when permission status is checked. + +**Event Parameters:** +- `string $location` - Overall location permission status +- `string $coarseLocation` - Coarse location permission status +- `string $fineLocation` - Fine location permission status + +**Permission Values:** +- `'granted'` - Permission is granted +- `'denied'` - Permission is denied +- `'not_determined'` - Permission not yet requested + + + + + +```php +use Native\Mobile\Attributes\OnNative; +use Native\Mobile\Events\Geolocation\PermissionStatusReceived; + +#[OnNative(PermissionStatusReceived::class)] +public function handlePermissionStatus($location, $coarseLocation, $fineLocation) +{ + // ... +} +``` + + + + +```js +import { on, off, Events } from '#nativephp'; +import { ref, onMounted, onUnmounted } from 'vue'; + +const permissionStatus = ref(''); + +const handlePermissionStatus = (payload) => { + const { location } = payload; + permissionStatus.value = location; +}; + +onMounted(() => { + on(Events.Geolocation.PermissionStatusReceived, handlePermissionStatus); +}); + +onUnmounted(() => { + off(Events.Geolocation.PermissionStatusReceived, handlePermissionStatus); +}); +``` + + + + +```jsx +import { on, off, Events } from '#nativephp'; +import { useState, useEffect } from 'react'; + +const [permissionStatus, setPermissionStatus] = useState(''); + +const handlePermissionStatus = (payload) => { + const { location } = payload; + setPermissionStatus(location); +}; + +useEffect(() => { + on(Events.Geolocation.PermissionStatusReceived, handlePermissionStatus); + + return () => { + off(Events.Geolocation.PermissionStatusReceived, handlePermissionStatus); + }; +}, []); +``` + + + + +### `PermissionRequestResult` + +Fired when a permission request completes. + +**Event Parameters:** +- `string $location` - Overall location permission result +- `string $coarseLocation` - Coarse location permission result +- `string $fineLocation` - Fine location permission result +- `string $message` - Optional message (for permanently denied) +- `bool $needsSettings` - Whether user needs to go to Settings + +**Special Values:** +- `'permanently_denied'` - User has permanently denied permission + + + + + +```php +use Native\Mobile\Attributes\OnNative; +use Native\Mobile\Events\Geolocation\PermissionRequestResult; + +#[On('native:' . PermissionRequestResult::class)] +public function handlePermissionRequest($location, $coarseLocation, $fineLocation, $message = null, $needsSettings = null) +{ + if ($location === 'permanently_denied') { + $this->error = 'Location permission permanently denied. Please enable in Settings.'; + } elseif ($coarseLocation === 'granted' || $fineLocation === 'granted') { + $this->getCurrentLocation(); + } else { + $this->error = 'Location permission is required for this feature.'; + } +} +``` + + + + +```js +import { on, off, Events } from '#nativephp'; +import { ref, onMounted, onUnmounted } from 'vue'; + +const error = ref(''); + +const handlePermissionRequest = (payload) => { + const { location, coarseLocation, fineLocation } = payload; + + if (location === 'permanently_denied') { + error.value = 'Please enable location in Settings.'; + } else if (coarseLocation === 'granted' || fineLocation === 'granted') { + getCurrentLocation(); + } else { + error.value = 'Location permission is required.'; + } +}; + +onMounted(() => { + on(Events.Geolocation.PermissionRequestResult, handlePermissionRequest); +}); + +onUnmounted(() => { + off(Events.Geolocation.PermissionRequestResult, handlePermissionRequest); +}); +``` + + + + +```jsx +import { on, off, Events } from '#nativephp'; +import { useState, useEffect } from 'react'; + +const [error, setError] = useState(''); + +const handlePermissionRequest = (payload) => { + const { location, coarseLocation, fineLocation } = payload; + + if (location === 'permanently_denied') { + setError('Please enable location in Settings.'); + } else if (coarseLocation === 'granted' || fineLocation === 'granted') { + getCurrentLocation(); + } else { + setError('Location permission is required.'); + } +}; + +useEffect(() => { + on(Events.Geolocation.PermissionRequestResult, handlePermissionRequest); + + return () => { + off(Events.Geolocation.PermissionRequestResult, handlePermissionRequest); + }; +}, []); +``` + + + + +## Privacy Considerations + +- **Explain why** you need location access before requesting +- **Request at the right time** - when the feature is actually needed +- **Respect denials** - provide alternative functionality when possible +- **Use appropriate accuracy** - don't request fine location if coarse is sufficient +- **Limit frequency** - don't request location updates constantly + +### Performance Considerations +- **Battery Usage** - GPS uses more battery than network location +- **Time to Fix** - GPS takes longer for initial position +- **Indoor Accuracy** - GPS may not work well indoors +- **Caching** - Consider caching recent locations for better UX + diff --git a/resources/views/docs/mobile/2/apis/haptics.md b/resources/views/docs/mobile/2/apis/haptics.md new file mode 100644 index 00000000..84e8ad2f --- /dev/null +++ b/resources/views/docs/mobile/2/apis/haptics.md @@ -0,0 +1,57 @@ +--- +title: Haptics +order: 800 +--- + +## Overview + +The Haptics API provides access to the device's vibration and haptic feedback system for tactile user interactions. + + + + + +```php +use Native\Mobile\Facades\Haptics; +``` + + + + +```js +import { device } from '#nativephp'; +``` + + + + +## Methods + +### `vibrate()` + +Triggers device vibration for tactile feedback. + +**Returns:** `void` + + + + + +```php +Haptics::vibrate(); +``` + + + + +```js +await device.vibrate(); +``` + + + + +**Use haptics for:** Button presses, form validation, important notifications, game events. + +**Avoid haptics for:** Frequent events, background processes, minor updates. + diff --git a/resources/views/docs/mobile/2/apis/microphone.md b/resources/views/docs/mobile/2/apis/microphone.md new file mode 100644 index 00000000..067d0d63 --- /dev/null +++ b/resources/views/docs/mobile/2/apis/microphone.md @@ -0,0 +1,364 @@ +--- +title: Microphone +order: 900 +--- + +## Overview + +The Microphone API provides access to the device's microphone for recording audio. It offers a fluent interface for +starting and managing recordings, tracking them with unique identifiers, and responding to completion events. + + + + + +```php +use Native\Mobile\Facades\Microphone; +``` + + + + +```js +import { microphone, on, off, Events } from '#nativephp'; +``` + + + + +## Methods + +### `record()` + +Start an audio recording. Returns a `PendingMicrophone` instance that controls the recording lifecycle. + + + + + +```php +Microphone::record()->start(); +``` + + + + +```js +// Basic recording +await microphone.record(); + +// With identifier for tracking +await microphone.record() + .id('voice-memo'); +``` + + + + +### `stop()` + +Stop the current audio recording. If this results in a saved file, this dispatches the `AudioRecorded` event with the +recording file path. + + + + + +```php +Microphone::stop(); +``` + + + + +```js +await microphone.stop(); +``` + + + + +### `pause()` + +Pause the current audio recording without ending it. + + + + + +```php +Microphone::pause(); +``` + + + + +```js +await microphone.pause(); +``` + + + + +### `resume()` + +Resume a paused audio recording. + + + + + +```php +Microphone::resume(); +``` + + + + +```js +await microphone.resume(); +``` + + + + +### `getStatus()` + +Get the current recording status. + +**Returns:** `string` - One of: `"idle"`, `"recording"`, or `"paused"` + + + + + +```php +$status = Microphone::getStatus(); + +if ($status === 'recording') { + // A recording is in progress +} +``` + + + + +```js +const result = await microphone.getStatus(); + +if (result.status === 'recording') { + // A recording is in progress +} +``` + + + + +### `getRecording()` + +Get the file path to the last recorded audio file. + +**Returns:** `string|null` - Path to the last recording, or `null` if none exists + + + + + +```php +$path = Microphone::getRecording(); + +if ($path) { + // Process the recording file +} +``` + + + + +```js +const result = await microphone.getRecording(); + +if (result.path) { + // Process the recording file +} +``` + + + + +## PendingMicrophone + +The `PendingMicrophone` provides a fluent interface for configuring and starting audio recordings. Most methods return +`$this` for method chaining. + +### `id(string $id)` + +Set a unique identifier for this recording. This ID will be included in the `AudioRecorded` event, allowing you to +correlate recordings with completion events. + +```php +$recorderId = 'message-recording-' . $this->id; + +Microphone::record() + ->id($recorderId) + ->start(); +``` + +### `getId()` + +Get the recorder's unique identifier. If no ID was set, one is automatically generated (UUID v4). + +```php +$recorder = Microphone::record() + ->id('my-recording'); + +$id = $recorder->getId(); // 'my-recording' +``` + +### `event(string $eventClass)` + +Set a custom event class to dispatch when recording completes. By default, `AudioRecorded` is used. + +**Throws:** `InvalidArgumentException` if the event class does not exist + +```php +use App\Events\VoiceMessageRecorded; + +Microphone::record() + ->event(VoiceMessageRecorded::class) + ->start(); +``` + +### `remember()` + +Store the recorder's ID in the session for later retrieval. This is useful when the recording completes on the next request. + +```php +Microphone::record() + ->id('voice-note') + ->remember() + ->start(); +``` + +### `lastId()` + +Retrieve the last remembered audio recorder ID from the session. Use this in event listeners to correlate recordings. + +```php +use Native\Mobile\Attributes\OnNative; +use Native\Mobile\Events\Microphone\MicrophoneRecorded; + +#[OnNative(MicrophoneRecorded::class)] +public function handleAudioRecorded(string $path, string $mimeType, ?string $id) +{ + // For comparing with remembered IDs + if ($id === Audio::record()->lastId()) { + $this->saveRecording($path); + } +} +``` + +### `start()` + +Explicitly start the audio recording. This is optional - recordings auto-start if you don't call this method. + +**Returns:** `bool` - `true` if recording started successfully, `false` if it failed or was already started + +```php +$recorder = Microphone::record()->id('my-recording'); + +if ($recorder->start()) { + // Recording started +} else { + // Recording failed - likely due to permission denial +} +``` + +## Events + +### `MicrophoneRecorded` + +Dispatched when an audio recording completes. The event includes the file path and recording ID. + +**Payload:** +- `string $path` - File path to the recorded audio +- `string $mimeType` - MIME type of the audio (default: `'audio/m4a'`) +- `?string $id` - The recorder's ID, if one was set + + + + + +```php +#[OnNative(MicrophoneRecorded::class)] +public function handleAudioRecorded(string $path, string $mimeType, ?string $id) +{ + // Store or process the recording + $this->recordings[] = [ + 'path' => $path, + 'mimeType' => $mimeType, + 'id' => $id, + ]; +} +``` + + + + +```js +import { on, off, Events } from '#nativephp'; +import { ref, onMounted, onUnmounted } from 'vue'; + +const recordings = ref([]); + +const handleAudioRecorded = (payload) => { + const { path, mimeType, id } = payload; + recordings.value.push({ path, mimeType, id }); +}; + +onMounted(() => { + on(Events.Microphone.MicrophoneRecorded, handleAudioRecorded); +}); + +onUnmounted(() => { + off(Events.Microphone.MicrophoneRecorded, handleAudioRecorded); +}); +``` + + + + +```jsx +import { on, off, Events } from '#nativephp'; +import { useState, useEffect } from 'react'; + +const [recordings, setRecordings] = useState([]); + +const handleAudioRecorded = (payload) => { + const { path, mimeType, id } = payload; + setRecordings(prev => [...prev, { path, mimeType, id }]); +}; + +useEffect(() => { + on(Events.Microphone.MicrophoneRecorded, handleAudioRecorded); + + return () => { + off(Events.Microphone.MicrophoneRecorded, handleAudioRecorded); + }; +}, []); +``` + + + + +## Notes + +- **Microphone Permission:** The first time your app requests microphone access, users will be prompted for permission. If denied, recording functions will fail silently. + +- **Microphone Background Permission:** You can allow your app to record audio while the device is locked by toggling `microphone_background` to true in [the config](../getting-started/configuration) + +- **File Format:** Recordings are stored as M4A/AAC audio files (`.m4a`). This format is optimized for small file sizes while maintaining quality. + +- **Recording State:** Only one recording can be active at a time. Calling `start()` while a recording is in progress will return `false`. + +- **Auto-Start Behavior:** If you don't explicitly call `start()`, the recording will automatically start when the `PendingMicrophone is destroyed. This maintains backward compatibility with earlier versions. diff --git a/resources/views/docs/mobile/2/apis/network.md b/resources/views/docs/mobile/2/apis/network.md new file mode 100644 index 00000000..b206dc8c --- /dev/null +++ b/resources/views/docs/mobile/2/apis/network.md @@ -0,0 +1,87 @@ +--- +title: Network +order: 1000 +--- + +## Overview + +The Network API provides access to the device's current network status and connection information. You can check whether the device is connected, determine the connection type, and detect metered or low-bandwidth conditions. + + + + + +```php +use Native\Mobile\Facades\Network; +``` + + + + +```js +import { network } from '#nativephp'; +``` + + + + +## Methods + +### `status()` + +Gets the current network status of the device. + +**Returns:** `object|null` - Network status object or `null` if unavailable + +The returned object contains the following properties: + +- `connected` (bool) - Whether the device is connected to a network +- `type` (string) - The type of connection: `"wifi"`, `"cellular"`, `"ethernet"`, or `"unknown"` +- `isExpensive` (bool) - Whether the connection is metered/cellular (iOS only, always `false` on Android) +- `isConstrained` (bool) - Whether Low Data Mode is enabled (iOS only, always `false` on Android) + + + + + +```php +$status = Network::status(); + +if ($status) { + echo $status->connected; // true/false + echo $status->type; // "wifi", "cellular", "ethernet", or "unknown" + echo $status->isExpensive; // true/false (iOS only) + echo $status->isConstrained; // true/false (iOS only) +} +``` + + + + +```js +const status = await network.status(); + +if (status) { + console.log(status.connected); // true/false + console.log(status.type); // "wifi", "cellular", "ethernet", or "unknown" + console.log(status.isExpensive); // true/false (iOS only) + console.log(status.isConstrained); // true/false (iOS only) +} +``` + + + + +## Notes + +- **iOS-specific properties:** The `isExpensive` and `isConstrained` properties are only meaningful on iOS. On Android, these values will always be `false`. + +- **Android behavior:** Android reports the basic connection state (connected/type). For Android 10+, the connection type detection is more accurate due to API improvements. + +- **Permissions:** Network status monitoring requires the `network_state` permission, which is enabled by default in your NativePHP configuration (`config/nativephp.php`). + +- **Snapshot, not stream:** The `status()` method returns a snapshot of the current network state. It's not a real-time stream. Call it whenever you need the current status. + +- **No events:** Unlike other APIs, Network doesn't provide events. Call `status()` directly when you need to check the connection or perform periodic checks from your component lifecycle. + +- **Real-time monitoring:** For monitoring changes, consider calling `status()` periodically or in response to user actions. diff --git a/resources/views/docs/mobile/2/apis/push-notifications.md b/resources/views/docs/mobile/2/apis/push-notifications.md new file mode 100644 index 00000000..a919d5f7 --- /dev/null +++ b/resources/views/docs/mobile/2/apis/push-notifications.md @@ -0,0 +1,171 @@ +--- +title: PushNotifications +order: 1100 +--- + +## Overview + +The PushNotifications API handles device registration for Firebase Cloud Messaging to receive push notifications. + + + + + +```php +use Native\Mobile\Facades\PushNotifications; +``` + + + + +```js +import { pushNotifications, on, off, Events } from '#nativephp'; +``` + + + + +## Methods + +### `enroll()` + +Requests permission and enrolls the device for push notifications. + +**Returns:** `void` + + + + + +```php +PushNotifications::enroll(); +``` + + + + +```js +// Basic enrollment +await pushNotifications.enroll(); + +// With identifier for tracking +await pushNotifications.enroll() + .id('main-enrollment') + .remember(); +``` + + + + +### `getToken()` + +Retrieves the current push notification token for this device. + +**Returns:** `string|null` - The FCM token, or `null` if not available + + + + + +```php +$token = PushNotifications::getToken(); +``` + + + + +```js +const result = await pushNotifications.getToken(); +const token = result.token; // APNS token on iOS, FCM token on Android +``` + + + + +## Events + +### `TokenGenerated` + +Fired when a push notification token is successfully generated. + +**Payload:** `string $token` - The FCM token for this device + + + + + +```php +use Native\Mobile\Attributes\OnNative; +use Native\Mobile\Events\PushNotification\TokenGenerated; + +#[OnNative(TokenGenerated::class)] +public function handlePushToken(string $token) +{ + // Send token to your backend + $this->sendTokenToServer($token); +} +``` + + + + +```js +import { on, off, Events } from '#nativephp'; +import { onMounted, onUnmounted } from 'vue'; + +const handleTokenGenerated = (payload) => { + const { token } = payload; + // Send token to your backend + sendTokenToServer(token); +}; + +onMounted(() => { + on(Events.PushNotification.TokenGenerated, handleTokenGenerated); +}); + +onUnmounted(() => { + off(Events.PushNotification.TokenGenerated, handleTokenGenerated); +}); +``` + + + + +```jsx +import { on, off, Events } from '#nativephp'; +import { useEffect } from 'react'; + +const handleTokenGenerated = (payload) => { + const { token } = payload; + // Send token to your backend + sendTokenToServer(token); +}; + +useEffect(() => { + on(Events.PushNotification.TokenGenerated, handleTokenGenerated); + + return () => { + off(Events.PushNotification.TokenGenerated, handleTokenGenerated); + }; +}, []); +``` + + + + +## Permission Flow + +1. User taps "Enable Notifications" +2. App calls `enroll()` +3. System shows permission dialog +4. If granted, FCM generates token +5. `TokenGenerated` event fires with token +6. App sends token to backend +7. Backend stores token for user +8. Server can now send notifications to this device + +## Best Practices + +- Request permission at the right time (not immediately on app launch) +- Explain the value of notifications to users +- Handle permission denial gracefully diff --git a/resources/views/docs/mobile/2/apis/scanner.md b/resources/views/docs/mobile/2/apis/scanner.md new file mode 100644 index 00000000..4702d8ca --- /dev/null +++ b/resources/views/docs/mobile/2/apis/scanner.md @@ -0,0 +1,307 @@ +--- +title: Scanner +order: 1200 +--- + +## Overview + +The Scanner API provides cross-platform barcode and QR code scanning capabilities through a native camera interface. + + + + + +```php +use Native\Mobile\Facades\Scanner; +use Native\Mobile\Events\Scanner\CodeScanned; +``` + + + + +```js +import { scanner, on, off, Events } from '#nativephp'; +``` + + + + +## Basic Usage + + + + + +```php +// Open scanner +Scanner::scan(); + +// Listen for scan results +#[OnNative(CodeScanned::class)] +public function handleScan($data, $format, $id = null) +{ + Dialog::toast("Scanned: {$data}"); +} +``` + + + + +```js +import { scanner, dialog, on, off, Events } from '#nativephp'; +import { onMounted, onUnmounted } from 'vue'; + +// Open scanner +await scanner.scan(); + +// Listen for scan results +const handleScan = (payload) => { + const { data, format, id } = payload; + dialog.toast(`Scanned: ${data}`); +}; + +onMounted(() => { + on(Events.Scanner.CodeScanned, handleScan); +}); + +onUnmounted(() => { + off(Events.Scanner.CodeScanned, handleScan); +}); +``` + + + + +```jsx +import { scanner, dialog, on, off, Events } from '#nativephp'; +import { useEffect } from 'react'; + +// Open scanner +await scanner.scan(); + +// Listen for scan results +const handleScan = (payload) => { + const { data, format, id } = payload; + dialog.toast(`Scanned: ${data}`); +}; + +useEffect(() => { + on(Events.Scanner.CodeScanned, handleScan); + + return () => { + off(Events.Scanner.CodeScanned, handleScan); + }; +}, []); +``` + + + + +## Configuration Methods + +### `prompt(string $prompt)` + +Set custom prompt text displayed on the scanner screen. + + + + + +```php +Scanner::scan()->prompt('Scan product barcode'); +``` + + + + +```js +await scanner.scan() + .prompt('Scan product barcode'); +``` + + + + +### `continuous(bool $continuous = true)` + +Keep scanner open to scan multiple codes. Default is `false` (closes after first scan). + + + + + +```php +Scanner::scan()->continuous(true); +``` + + + + +```js +await scanner.scan() + .continuous(true); +``` + + + + +### `formats(array $formats)` + +Specify which barcode formats to scan. Default is `['qr']`. + +**Available formats:** `qr`, `ean13`, `ean8`, `code128`, `code39`, `upca`, `upce`, `all` + + + + + +```php +Scanner::scan()->formats(['qr', 'ean13', 'code128']); +``` + + + + +```js +await scanner.scan() + .formats(['qr', 'ean13', 'code128']); +``` + + + + +### `id(string $id)` + +Set a unique identifier for the scan session. Useful for handling different scan contexts. + + + + + +```php +Scanner::scan()->id('checkout-scanner'); +``` + + + + +```js +await scanner.scan() + .id('checkout-scanner'); +``` + + + + +### Combined Example + + + + + +```php +Scanner::scan() + ->prompt('Scan your ticket') + ->continuous(true) + ->formats(['qr', 'ean13']) + ->id('ticket-scanner'); +``` + + + + +```js +await scanner.scan() + .prompt('Scan your ticket') + .continuous(true) + .formats(['qr', 'ean13']) + .id('ticket-scanner'); +``` + + + + +## Events + +### `CodeScanned` + +Fired when a barcode is successfully scanned. + +**Properties:** +- `string $data` - The decoded barcode data +- `string $format` - The barcode format +- `string|null $id` - The scan session ID (if set) + + + + + +```php +#[OnNative(CodeScanned::class)] +public function handleScan($data, $format, $id = null) +{ + if ($id === 'product-scanner') { + $this->addProduct($data); + } +} +``` + + + + +```js +import { on, off, Events } from '#nativephp'; +import { onMounted, onUnmounted } from 'vue'; + +const handleScan = (payload) => { + const { data, format, id } = payload; + + if (id === 'product-scanner') { + addProduct(data); + } +}; + +onMounted(() => { + on(Events.Scanner.CodeScanned, handleScan); +}); + +onUnmounted(() => { + off(Events.Scanner.CodeScanned, handleScan); +}); +``` + + + + +```jsx +import { on, off, Events } from '#nativephp'; +import { useEffect } from 'react'; + +const handleScan = (payload) => { + const { data, format, id } = payload; + + if (id === 'product-scanner') { + addProduct(data); + } +}; + +useEffect(() => { + on(Events.Scanner.CodeScanned, handleScan); + + return () => { + off(Events.Scanner.CodeScanned, handleScan); + }; +}, []); +``` + + + + +## Notes + +- **Platform Support:** + - **Android:** ML Kit Barcode Scanning (API 21+) + - **iOS:** AVFoundation (iOS 13.0+) +- **Permissions:** You must enable the `scanner` permission in `config/nativephp.php` to use the scanner. Camera + permissions are then handled automatically, and users will be prompted for permission the first time the scanner is + used. diff --git a/resources/views/docs/mobile/2/apis/secure-storage.md b/resources/views/docs/mobile/2/apis/secure-storage.md new file mode 100644 index 00000000..7dac7fb4 --- /dev/null +++ b/resources/views/docs/mobile/2/apis/secure-storage.md @@ -0,0 +1,156 @@ +--- +title: SecureStorage +order: 1300 +--- + +## Overview + +The SecureStorage API provides secure storage using the device's native keychain (iOS) or keystore (Android). It's +ideal for storing sensitive data like tokens, passwords, and user credentials. + + + + + +```php +use Native\Mobile\Facades\SecureStorage; +``` + + + + +```js +import { secureStorage } from '#nativephp'; +``` + + + + +## Methods + +### `set()` + +Stores a secure value in the native keychain or keystore. + +**Parameters:** +- `string $key` - The key to store the value under +- `string|null $value` - The value to store securely + +**Returns:** `bool` - `true` if successfully stored, `false` otherwise + + + + + +```php +SecureStorage::set('api_token', 'abc123xyz'); +``` + + + + +```js +const result = await secureStorage.set('api_token', 'abc123xyz'); + +if (result.success) { + // Value stored securely +} +``` + + + + +### `get()` + +Retrieves a secure value from the native keychain or keystore. + +**Parameters:** +- `string $key` - The key to retrieve the value for + +**Returns:** `string|null` - The stored value or `null` if not found + + + + + +```php +$token = SecureStorage::get('api_token'); +``` + + + + +```js +const result = await secureStorage.get('api_token'); +const token = result.value; // or null if not found +``` + + + + +### `delete()` + +Deletes a secure value from the native keychain or keystore. + +**Parameters:** +- `string $key` - The key to delete the value for + +**Returns:** `bool` - `true` if successfully deleted, `false` otherwise + + + + + +```php +SecureStorage::delete('api_token'); +``` + + + + +```js +const result = await secureStorage.delete('api_token'); + +if (result.success) { + // Value deleted +} +``` + + + + +## Platform Implementation + +### iOS - Keychain Services +- Uses the iOS Keychain Services API +- Data is encrypted and tied to your app's bundle ID +- Survives app deletion and reinstallation if iCloud Keychain is enabled +- Protected by device passcode/biometrics + +### Android - Keystore +- Uses Android Keystore system +- Hardware-backed encryption when available +- Data is automatically deleted when app is uninstalled +- Protected by device lock screen + +## Security Features + +- **Encryption:** All data is automatically encrypted +- **App Isolation:** Data is only accessible by your app +- **System Protection:** Protected by device authentication +- **Tamper Resistance:** Hardware-backed security when available + +## What to Store +- API tokens and refresh tokens +- User credentials (if necessary) +- Encryption keys +- Sensitive user preferences +- Two-factor authentication secrets + +## What NOT to Store +- Large amounts of data (use encrypted database instead) +- Non-sensitive data +- Temporary data +- Cached content + + diff --git a/resources/views/docs/mobile/2/apis/share.md b/resources/views/docs/mobile/2/apis/share.md new file mode 100644 index 00000000..ddfd28ef --- /dev/null +++ b/resources/views/docs/mobile/2/apis/share.md @@ -0,0 +1,218 @@ +--- +title: Share +order: 1400 +--- + +## Overview + +The Share API enables users to share content from your app using the native share sheet. On iOS, this opens the native share menu with options like Messages, Mail, and social media apps. On Android, it launches the system share intent with available apps. + + + + + +```php +use Native\Mobile\Facades\Share; +``` + + + + +```js +import { share } from '#nativephp'; +``` + + + + +## Methods + +### `url()` + +Share a URL using the native share dialog. + +**Parameters:** +- `string $title` - Title/subject for the share +- `string $text` - Text content or message to share +- `string $url` - URL to share + +**Returns:** `void` + + + + + +```php +Share::url( + title: 'Check this out', + text: 'I found something interesting', + url: 'https://example.com/article' +); +``` + + + + +```js +await share.url( + 'Check this out', + 'I found something interesting', + 'https://example.com/article' +); +``` + + + + +### `file()` + +Share a file using the native share dialog. + +**Parameters:** +- `string $title` - Title/subject for the share +- `string $text` - Text content or message to share +- `string $filePath` - Absolute path to the file to share + +**Returns:** `void` + + + + + +```php +Share::file( + title: 'Share Document', + text: 'Check out this PDF', + filePath: '/path/to/document.pdf' +); +``` + + + + +```js +await share.file( + 'Share Document', + 'Check out this PDF', + '/path/to/document.pdf' +); +``` + + + + +## Examples + +### Sharing a Website Link + +Share a link to your app's website or external content. + + + + + +```php +Share::url( + title: 'My Awesome App', + text: 'Download my app today!', + url: 'https://myapp.com' +); +``` + + + + +```js +await share.url( + 'My Awesome App', + 'Download my app today!', + 'https://myapp.com' +); +``` + + + + +### Sharing Captured Photos + +Share a photo that was captured with the camera. + + + + + +```php +use Native\Mobile\Attributes\OnNative; +use Native\Mobile\Events\Camera\PhotoTaken; + +#[OnNative(PhotoTaken::class)] +public function handlePhotoTaken(string $path) +{ + Share::file( + title: 'My Photo', + text: 'Check out this photo I just took!', + filePath: $path + ); +} +``` + + + + +```js +import { share, on, off, Events } from '#nativephp'; +import { onMounted, onUnmounted } from 'vue'; + +const handlePhotoTaken = (payload) => { + share.file( + 'My Photo', + 'Check out this photo I just took!', + payload.path + ); +}; + +onMounted(() => { + on(Events.Camera.PhotoTaken, handlePhotoTaken); +}); + +onUnmounted(() => { + off(Events.Camera.PhotoTaken, handlePhotoTaken); +}); +``` + + + + +```jsx +import { share, on, off, Events } from '#nativephp'; +import { useEffect } from 'react'; + +const handlePhotoTaken = (payload) => { + share.file( + 'My Photo', + 'Check out this photo I just took!', + payload.path + ); +}; + +useEffect(() => { + on(Events.Camera.PhotoTaken, handlePhotoTaken); + + return () => { + off(Events.Camera.PhotoTaken, handlePhotoTaken); + }; +}, []); +``` + + + + +## Notes + +- The native share sheet opens, allowing users to choose which app to share with (Messages, Email, social media, etc.) +- The file path must be absolute and the file must exist before calling the share method +- File paths should be verified to exist before attempting to share to avoid errors +- The Share API works with any file type (PDF, images, videos, documents, etc.) +- There is no way to determine which app the user selected or whether they cancelled the share +- No events are dispatched by the Share API +- The `url()` method works with any URL format (http, https, deep links, etc.) diff --git a/resources/views/docs/mobile/2/apis/system.md b/resources/views/docs/mobile/2/apis/system.md new file mode 100644 index 00000000..78c165af --- /dev/null +++ b/resources/views/docs/mobile/2/apis/system.md @@ -0,0 +1,147 @@ +--- +title: System +order: 1500 +--- + +## Overview + +The System API provides access to basic system functions like flashlight control and platform detection. + + + + + +```php +use Native\Mobile\Facades\System; +``` + + + + +```js +import { system } from '#nativephp'; +// or import individual functions +import { isIos, isAndroid, isMobile } from '#nativephp'; +``` + + + + +## Methods + +### `flashlight()` - Deprecated, see [Device](device) + +Toggles the device flashlight (camera flash LED) on and off. + +**Returns:** `void` + + + + + +```php +System::flashlight(); // Toggle flashlight state +``` + + + + +```js +// Use device.flashlight() instead +import { device } from '#nativephp'; + +await device.flashlight(); +``` + + + + +### `isIos()` + +Determines if the current device is running iOS. + +**Returns:** `true` if iOS, `false` otherwise + + + + + +```php +if (System::isIos()) { + // iOS-specific code +} +``` + + + + +```js +const ios = await system.isIos(); + +if (ios) { + // iOS-specific code +} +``` + + + + +### `isAndroid()` + +Determines if the current device is running Android. + +**Returns:** `true` if Android, `false` otherwise + + + + + +```php +if (System::isAndroid()) { + // Android-specific code +} +``` + + + + +```js +const android = await system.isAndroid(); + +if (android) { + // Android-specific code +} +``` + + + + +### `isMobile()` + +Determines if the current device is running Android or iOS. + +**Returns:** `true` if Android or iOS, `false` otherwise + + + + + +```php +if (System::isMobile()) { + // Mobile-specific code +} +``` + + + + +```js +const mobile = await system.isMobile(); + +if (mobile) { + // Mobile-specific code +} +``` + + + diff --git a/resources/views/docs/mobile/2/concepts/_index.md b/resources/views/docs/mobile/2/concepts/_index.md new file mode 100644 index 00000000..6e3f1cfd --- /dev/null +++ b/resources/views/docs/mobile/2/concepts/_index.md @@ -0,0 +1,4 @@ +--- +title: Concepts +order: 40 +--- diff --git a/resources/views/docs/mobile/2/concepts/authentication.md b/resources/views/docs/mobile/2/concepts/authentication.md new file mode 100644 index 00000000..e0c54a7c --- /dev/null +++ b/resources/views/docs/mobile/2/concepts/authentication.md @@ -0,0 +1,107 @@ +--- +title: Authentication +order: 150 +--- + +## Authenticating Your Users + +Most apps will want to have some concept of who the user is so that they can use your app to connect securely with +external services, such as your API or cloud service. + +In a mobile app, you will always need to call an external service (external to the user's device) that acts as the +source of truth about your users credentials. It could be a service you manage or a third-party service like WorkOS, +Auth0 or Amazon Cognito. + +Authenticating the user serves two purposes: + +1. It allows you to prove who they are when they connect to your API. +2. It gives you an opportunity to grant or deny access to certain features of your app based on the authenticated state + of the user. + +Whilst some user data may be stored on the user's device for convenience, you should not rely on this data to +authenticate the user. This is because **the data is outside of your control**. So you will not be using the typical +Laravel authentication mechanisms to check for an authenticated user. + + + +## Tokens FTW! + +Most mobile apps opt for some form of "auth token" (e.g. a JWT or an expiring API key) that is generated by your auth +service and stored securely on the user's device. + +These tokens should only live for a short period, usually no more than a few days. It's useful to have a single-use +"refresh token" that lives for a longer time (e.g. 30 days) also shared with your user when they have successfully +authenticated. This can be exchanged for a new auth token when the user's current auth token has expired. + +You should store both auth and refresh tokens in secure storage. **Checking for an auth token's existence to validate +that the user is authenticated is _not sufficient_.** If the token has expired or been revoked, you should force the +user to re-authenticate. The only way to know for certain is to exercise the token. + + + +### Laravel Sanctum + +[Laravel Sanctum](https://laravel.com/docs/12.x/sanctum) is a very convenient and easy-to-use mechanism for generating +auth tokens for your users. They simply provide their login credentials and if authenticated, receive a token. Using a +simple login form, you can collect their username and password in your app and `POST` it securely to your auth service +via an API call. + +Note that, by default, Sanctum tokens don't expire. +[You should enable token expiration](https://laravel.com/docs/12.x/sanctum#token-expiration) for increased +security. You may only find out that a token has expired when your app attempts to use it unsuccessfully. + +### OAuth + +OAuth is a robust and battle-tested solution to the mobile app auth problem. If you're running +[Laravel Passport](https://laravel.com/docs/12.x/passport) or your authentication service support OAuth, you should use +it! + +You will likely want to use an OAuth client library in your app to make interacting with your OAuth service easier. + +When initiating the auth flow for the user, you should use the `Native\Mobile\Facades\Browser::auth()` API, as this is +purpose-built for securely passing authorization codes back from the OAuth service to your app. + +For this to work, you must set a `NATIVEPHP_DEEPLINK_SCHEME` that will be unique for your application on users' devices. + +```dotenv +NATIVEPHP_DEEPLINK_SCHEME=myapp +``` + +Then you must define your redirect URL. It should match your scheme and the route in your app that will handle the callback +data. + +```php +Browser::auth('https://workos.com/my-company/auth?redirect=myapp://auth/handle') +``` + +Most services will expect you to pre-define your redirect URLs as a security feature. You should be able to provide your +exact URL, as this will be the most secure method. + +How you handle the response in your app depends on how that particular API operates and the needs of your application. + + diff --git a/resources/views/docs/mobile/2/concepts/databases.md b/resources/views/docs/mobile/2/concepts/databases.md new file mode 100644 index 00000000..c25122ef --- /dev/null +++ b/resources/views/docs/mobile/2/concepts/databases.md @@ -0,0 +1,149 @@ +--- +title: Databases +order: 200 +--- + +## Working with Databases + +You'll almost certainly want your application to persist structured data. For this, NativePHP supports +[SQLite](https://sqlite.org/), which works on both iOS and Android devices. + +You can interact with SQLite from PHP in whichever way you're used to. + +## Configuration + +You do not need to do anything special to configure your application to use SQLite. NativePHP will automatically: +- Switch to using SQLite when building your application. +- Create the database for you in the app container. +- Run your migrations each time your app starts, as needed. + +## Migrations + +When writing migrations, you need to consider any special recommendations for working with SQLite. + +For example, prior to Laravel 11, SQLite foreign key constraints are turned off by default. If your application relies +upon foreign key constraints, [you need to enable SQLite support for them](https://laravel.com/docs/database#configuration) before running your migrations. + +**It's important to test your migrations on [prod builds](/docs/mobile/1/getting-started/development#releasing) +before releasing updates!** You don't want to accidentally delete your user's data when they update your app. + +## Seeding data with migrations + +Migrations are the perfect mechanism for seeding data in mobile applications. They provide the natural behavior you +want for data seeding: + +- **Run once**: Each migration runs exactly once per installation. +- **Tracked**: Laravel tracks which migrations have been executed. +- **Versioned**: New app versions can include new data seeding migrations. +- **Reversible**: You can create migrations to remove or update seed data. + +### Creating seed migrations + +Create dedicated migrations for seeding data: + +```shell +php artisan make:migration seed_app_settings +``` + +```php +use Illuminate\Database\Migrations\Migration; +use Illuminate\Support\Facades\DB; + +return new class extends Migration +{ + public function up() + { + DB::table('categories')->insert([ + ['name' => 'Work', 'color' => '#3B82F6'], + ['name' => 'Personal', 'color' => '#10B981'], + ]); + } +}; +``` + +### Test thoroughly + +This is the most important step when releasing new versions of your app, especially with new migrations. + +Your migrations should work both for users who are installing your app for the first time (or re-installing) _and_ +users who have updated your app to a new release. + +Make sure you test your migrations under the different scenarios that your users' databases are likely to be in. + +## Things to note + +- As your app is installed on a separate device, you do not have remote access to the database. +- If a user deletes your application from their device, any databases are also deleted. + +## Can I get MySQL/Postgres/other support? + +No. + +SQLite being the only supported database driver is a deliberate security decision to prevent developers from +accidentally embedding production database credentials directly in mobile applications. Why? + +- Mobile apps are distributed to user devices and can be reverse-engineered. +- Database credentials embedded in apps may be accessible to anyone with the app binary. +- Direct database connections bypass important security layers like rate limiting and access controls. +- Network connectivity issues make direct database connections unreliable from mobile devices and can be troublesome + for your database to handle. + +## API-first + +If a key part of your application relies on syncing data between a central database and your client apps, we strongly +recommend that you do so via a secure API backend that your mobile app can communicate with. + +This provides multiple security and architectural benefits: + +**Security Benefits:** +- Database credentials never leave your server +- Implement proper authentication and authorization +- Rate limiting and request validation +- Audit logs for all data access +- Ability to revoke access instantly + +**Technical Benefits:** +- Better error handling and offline support +- Easier to scale and maintain +- Version your API for backward compatibility +- Transform data specifically for mobile consumption + +### Securing your API + +For the same reasons that you shouldn't share database credentials in your `.env` file or elsewhere in your app code, +you shouldn't store API keys or tokens either. + +If anything, you should provide a client key that **only** allows client apps to request tokens. Once you have +authenticated your user, you can pass an access token back to your mobile app and use this for communicating with your +API. + +Store these tokens on your users' devices securely with the [`SecureStorage`](/docs/mobile/1/apis/secure-storage) API. + +It's a good practice to ensure these tokens have high entropy so that they are very hard to guess and a short lifespan. +Generating tokens is cheap; leaking personal customer data can get _very_ expensive! + +Use industry-standard tools like OAuth-2.0-based providers, Laravel Passport, or Laravel Sanctum. + + + +#### Considerations + +In your mobile apps: + +- Always store API tokens using `SecureStorage` +- Use HTTPS for all API communications +- Cache data locally using SQLite for offline functionality +- Check for connectivity before making API calls + +And on the API side: + +- Use token-based authentication +- Implement rate limiting to prevent abuse +- Validate and sanitize all input data +- Use HTTPS with proper SSL certificates +- Log all authentication attempts and API access diff --git a/resources/views/docs/mobile/2/concepts/deep-links.md b/resources/views/docs/mobile/2/concepts/deep-links.md new file mode 100644 index 00000000..3bb8a305 --- /dev/null +++ b/resources/views/docs/mobile/2/concepts/deep-links.md @@ -0,0 +1,92 @@ +--- +title: Deep Links +order: 300 +--- + +## Overview + +NativePHP for Mobile supports **deep linking** into your app via Custom URL Schemes and Associated Domains: + +- **Custom URL Scheme** + ``` + myapp://some/path + ``` +- **Associated Domains** (a.k.a. Universal Links on iOS, App Links on Android) + ``` + https://example.net/some/path + ``` + +In each case, your app can be opened directly at the route matching `/some/path`. + +Each method has its use cases, and NativePHP handles all the platform-specific configuration automatically when you +provide the proper environment variables. + +You can even use both approaches at the same time in a single app! + +## Custom URL Scheme + +Custom URL schemes are a great way to allow apps to pass data between themselves. If your app is installed when a user +uses a deep link that incorporates your custom scheme, your app will open immediately to the desired route. + +But note that custom URL schemes can only work when your app has been installed and cannot aid in app discovery. If a +user interacts with URL with a custom scheme for an app they don't have installed, there will be no prompt to install +an app that can load that URL. + +To enable your app's custom URL scheme, define it in your `.env`: + +```dotenv +NATIVEPHP_DEEPLINK_SCHEME=myapp +``` + +You should choose a scheme that is unique to your app to avoid confusion with other apps. Note that some schemes are +reserved by the system and cannot be used (e.g. `https`). + +## Associated domains + +Universal Links/App Links allow real HTTPS URLs to open your app instead of in a web browser, if the app is installed. +If the app is not installed, the URL will load as normal in the browser. + +This flow increases the opportunity for app discovery dramatically and provides a much better overall user experience. + +### How it works + +1. You must prove to the operating system on the user's device that your app is legitimately associated with the domain + you are trying to redirect by hosting special files on your server: + - `.well-known/apple-app-site-association` (for iOS) + - `.well-known/assetlinks.json` (for Android) +2. The mobile OS reads these files to verify the link association +3. Once verified, tapping a real URL will open your app instead of opening it in the user's browser + +**NativePHP handles all the technical setup automatically** - you just need to host the verification files and +configure your domain correctly. + +To enable an app-associated domain, define it in your `.env`: + +```dotenv +NATIVEPHP_DEEPLINK_HOST=example.net +``` + +## Testing & troubleshooting + +Associated Domains do not usually work in simulators. Testing on a real device that connects to a publicly-accessible +server for verification is often the best way to ensure these are operating correctly. + +If you are experiencing issues getting your associated domain to open your app, try: +- Completely deleting and reinstalling the app. Registration verifications (including failures) are often cached + against the app. +- Validating that your associated domain verification files are formatted correctly and contain the correct data. + +There is usually no such limitation for Custom URL Schemes. + +## Use cases + +Deep linking is great for bringing users from another context directly to a key place in your app. Universal/App Links +are usually the more appropriate choice for this because of their flexibility in falling back to simple loading a URL +in the browser. + +They're also more likely to behave the same across both platforms. + +Then you could use Universal/App Links in: +- NFC tags +- QR codes +- Email/SMS marketing diff --git a/resources/views/docs/mobile/2/concepts/push-notifications.md b/resources/views/docs/mobile/2/concepts/push-notifications.md new file mode 100644 index 00000000..024ad509 --- /dev/null +++ b/resources/views/docs/mobile/2/concepts/push-notifications.md @@ -0,0 +1,93 @@ +--- +title: Push Notifications +order: 400 +--- + +## Overview + +NativePHP for Mobile uses Firebase Cloud Messaging (FCM) to send push notifications to your users on both iOS and +Android devices. + +To send a push notification to a user, your app must request a token. That token must then be stored securely (ideally +on a server application via a secure API) and associated with that user/device. + +Requesting push notification will trigger an alert for the user to either approve or deny your request. If they approve, +your app will receive the token. + +When you want to send a notification to that user, you pass this token along with a request to the FCM service and +Firebase handles sending the message to the right device. + + + +## Firebase + +1. Create a [Firebase](https://firebase.google.com/) account +2. Create a project +3. Download the `google-services.json` file (for Android) and `GoogleService-Info.plist` file (for iOS) +4. These files contain the configuration for your app and is used by the Firebase SDK to retrieve tokens for each device + +Place these files in the root of your application and NativePHP will automatically handle setting them up appropriately +for each platform. + +You can ignore Firebase's further setup instructions as this is already taken care of by NativePHP. + +### Service account + +For sending push notifications from your server-side application, you'll also need a Firebase service account: + +1. Go to your Firebase Console → Project Settings → Service Accounts +2. Click "Generate New Private Key" to download the service account JSON file +3. Save this file as `fcm-service-account.json` somewhere safe in your server application + +## Getting push tokens + +It's common practice to request push notification permissions during app bootup as tokens can change when: +- The app is restored on a new device +- The app data is restored from backup +- The app is updated +- Other internal FCM operations + +To request a token, use the `PushNotifications::getToken()` method: + +```php +use Native\Mobile\Facades\PushNotifications; + +PushNotifications::getToken(); +``` + +If the user has approved your app to use push notifications and the request to FCM succeeded, a `TokenGenerated` event +will fire. + +Listen for this event to receive the token. Here's an example in a Livewire component: + +```php +use App\Services\APIService; +use Native\Mobile\Attributes\OnNative; +use Native\Mobile\Facades\PushNotifications; +use Native\Mobile\Events\PushNotification\TokenGenerated; + +class PushNotifications extends Component +{ + #[OnNative(TokenGenerated::class)] + public function storePushToken(APIService $api, string $token) + { + $api->storePushToken($token); + } +} +``` + +## Sending push notifications + +Once you have a token, you may use it from your server-side applications to trigger Push Notifications directly to your +user's device. + + diff --git a/resources/views/docs/mobile/2/concepts/security.md b/resources/views/docs/mobile/2/concepts/security.md new file mode 100644 index 00000000..0d3cf311 --- /dev/null +++ b/resources/views/docs/mobile/2/concepts/security.md @@ -0,0 +1,98 @@ +--- +title: Security +order: 100 +--- + +## Security + +Although NativePHP tries to make it as easy as possible to make your application secure, it is your responsibility to +protect your users. + +### Secrets and .env + +As your application is being installed on systems outside your/your organisation's control, it is important to think +of the environment that it's in as _potentially_ hostile, which is to say that any secrets, passwords or keys +could fall into the hands of someone who might try to abuse them. + +This means you should, where possible, use unique keys for each installation, preferring to generate these at first-run +or on every run rather than sharing the same key for every user across many installations. + +Especially if your application is communicating with any private APIs over the network, we highly recommend that your +application and any API use a robust and secure authentication protocol, such as OAuth2, that enables you to create and +distribute unique and expiring tokens (an expiration date less than 48 hours in the future is recommended) with a high +level of entropy, as this makes them hard to guess and hard to abuse. + +**Always use HTTPS.** + +If your application allows users to connect _their own_ API keys for a service, you should treat these keys with great +care. If you choose to store them anywhere (either in a file or +[Database](databases)), make sure you store them +[encrypted](../the-basics/system#encryption-decryption) and decrypt them only when needed. + +## Secure Storage + +NativePHP provides access to your users' device's native Keystore/Keychain through the +[`SecureStorage`](/docs/apis/secure-storage) facade, which +allow you to store small amounts of data in a secure way. + +The device's secure storage encrypts and decrypts data on the fly and that means you can safely rely on it to store +critical things like API tokens, keeping your users and your systems safe. + +This data is only accessible by your app and is persisted beyond the lifetime of your app, so it will still be available +the next time your app is opened. + + + + +### When to use the Laravel `Crypt` facade + +When a user first opens your app, NativePHP generates a **unique `APP_KEY` just for their device** and stores it in the +device's secure storage. This means each instance of your application has its own encryption key that is securely +stored on the device. + +NativePHP securely reads the `APP_KEY` from secure storage and makes it available to Laravel. So you can safely use the +`Crypt` facade to encrypt and decrypt data! + + + +This is great for encrypting larger amounts of data that wouldn't easily fit in secure storage. You can encrypt values +and store them in the file system or in the SQLite database, knowing that they are safe at rest: + +```php +use Illuminate\Support\Facades\Crypt; + +$encryptedContents = Crypt::encryptString( + $request->file('super_private_file') +); + +Storage::put('my_secure_file', $encryptedContents); +``` + +And then decrypt it later: + +```php +$decryptedContents = Crypt::decryptString( + Storage::get('my_secure_file') +); +``` + + + diff --git a/resources/views/docs/mobile/2/edge-components/_index.md b/resources/views/docs/mobile/2/edge-components/_index.md new file mode 100644 index 00000000..e5f3a985 --- /dev/null +++ b/resources/views/docs/mobile/2/edge-components/_index.md @@ -0,0 +1,4 @@ +--- +title: EDGE Components +order: 30 +--- diff --git a/resources/views/docs/mobile/2/edge-components/bottom-nav.md b/resources/views/docs/mobile/2/edge-components/bottom-nav.md new file mode 100644 index 00000000..ff047141 --- /dev/null +++ b/resources/views/docs/mobile/2/edge-components/bottom-nav.md @@ -0,0 +1,67 @@ +--- +title: Bottom Navigation +order: 100 +--- + +## Overview + +
+ +![](/img/docs/edge-bottom-nav-ios.png) + +![](/img/docs/edge-bottom-nav-android.png) + +
+ +A bottom navigation bar with up to 5 items. Used for your app's primary navigation. + +@verbatim +```blade + + + + +``` +@endverbatim + +## Props + +- `label-visibility` - `labeled`, `selected`, or `unlabeled` (optional, default: `labeled`) +- `dark` - Force dark mode styling (optional) + +## Children + +A `` can contain up to 5 `` elements. + +- `id` - Unique identifier (required) +- `icon` - A named [icon](icons) (required) +- `label` - Accessibility label (required) +- `url` - A URL to navigate to in the web view (required) +- `active` - Highlight this item as active (optional, default: `false`) +- `badge` - Badge text/number (optional) +- `news` - Show "new" indicator dot (optional, default: `false`) + + + +### `badge` example +
+ +![](/img/docs/edge-bottom-nav-item-badge.png) + +
diff --git a/resources/views/docs/mobile/2/edge-components/icons.md b/resources/views/docs/mobile/2/edge-components/icons.md new file mode 100644 index 00000000..bfbe1cb3 --- /dev/null +++ b/resources/views/docs/mobile/2/edge-components/icons.md @@ -0,0 +1,288 @@ +--- +title: Icons +order: 9999 +--- + +## Overview + +NativePHP EDGE components use a smart icon mapping system that automatically converts icon names to platform-specific +icons. On iOS, icons render as [SF Symbols](https://developer.apple.com/sf-symbols/), while Android uses +[Material Icons](https://fonts.google.com/icons?icon.set=Material+Icons). + +You don't need to worry about the differences! Just use a single, consistent icon name in your components, and the EDGE +handles the platform translation automatically. + +## How It Works + +The icon system uses a four-tier resolution strategy: + +1. **Direct Platform Icons** - On iOS, if the name contains a `.` it's used as a direct SF Symbol path (e.g., `car.side.fill`). On Android, any Material Icon ligature name works directly (e.g., `shopping_cart`). +2. **Manual Mapping** - Explicit mappings for common icons and aliases (e.g., `home`, `settings`, `user`) +3. **Smart Fallback** - Attempts to auto-convert unmapped icon names to platform equivalents +4. **Default Fallback** - Uses a circle icon if no match is found + +This approach means you can use intuitive icon names for common cases, leverage direct platform icons for advanced use +cases, and get consistent results across iOS and Android. + +## Platform Differences + +### iOS (SF Symbols) + +On iOS, icons render as SF Symbols. Manual mappings convert common icon names to their SF Symbol equivalents. +For example: + +- `home` → `house.fill` +- `settings` → `gearshape.fill` +- `check` → `checkmark.circle.fill` + +If an icon name isn't manually mapped, the system attempts to find a matching SF Symbol by trying variations like +`.fill`, `.circle.fill`, and `.square.fill`. + +### Android (Material Icons) + +On Android, icons render using a lightweight font-based approach that supports the entire Material Icons library. You +can use any Material Icon by its ligature name directly (e.g., `shopping_cart`, `qr_code_2`). + +Manual mappings provide convenient aliases for common icon names. For example: + +- `home` → `home` +- `settings` → `settings` +- `check` → `check` +- `cart` → `shopping_cart` + +## Direct Platform Icons + +For advanced use cases, you can use platform-specific icon names directly. + +### iOS SF Symbols + +On iOS, include a `.` in the icon name to use an SF Symbol path directly: + +@verbatim +```blade + + + +``` +@endverbatim + +### Android Material Icons + +On Android, use any Material Icon ligature name (with underscores): + +@verbatim +```blade + + + +``` +@endverbatim + +## Platform-Specific Icons + +When you need different icons on each platform, use the `System` facade: + +@verbatim +```blade + +``` +@endverbatim + +This is useful when the mapped icon doesn't match your needs or you want to use platform-specific variants. + +## Basic Usage + +Use the `icon` attribute in any EDGE component that supports icons, simply passing the name of the icon you wish to use: + +@verbatim +```blade + +``` +@endverbatim + +## Icon Reference + +All icons listed here are manually mapped and guaranteed to work consistently across iOS and Android. + +### Navigation + +| Icon | Description | +|------|-------------| +| `dashboard` | Grid-style dashboard view | +| `home` | House/home screen | +| `menu` | Three-line hamburger menu | +| `settings` | Gear/settings | +| `account`, `profile`, `user` | User account or profile | +| `person` | Single person | +| `people`, `connections`, `contacts` | Multiple people | +| `group`, `groups` | Group of people | + +### Business & Commerce + +| Icon | Description | +|------|-------------| +| `orders`, `receipt` | Receipt or order | +| `cart`, `shopping` | Shopping cart | +| `shop`, `store` | Store or storefront | +| `products`, `inventory` | Products or inventory | + +### Charts & Data + +| Icon | Description | +|------|-------------| +| `chart`, `barchart` | Bar chart | +| `analytics` | Analytics/analysis | +| `summary`, `report`, `assessment` | Summary or report | + +### Time & Scheduling + +| Icon | Description | +|------|-------------| +| `clock`, `schedule`, `time` | Clock or time | +| `calendar` | Calendar | +| `history` | History or recent | + +### Actions + +| Icon | Description | +|------|-------------| +| `add`, `plus` | Add or create new | +| `edit` | Edit or modify | +| `delete` | Delete or remove | +| `save` | Save | +| `search` | Search | +| `filter` | Filter | +| `refresh` | Refresh or reload | +| `share` | Share | +| `download` | Download | +| `upload` | Upload | + +### Communication + +| Icon | Description | +|------|-------------| +| `notifications` | Notifications or alerts | +| `message` | Message or SMS | +| `email`, `mail` | Email | +| `chat` | Chat or conversation | +| `phone` | Phone or call | + +### Navigation Arrows + +| Icon | Description | +|------|-------------| +| `back` | Back or previous | +| `forward` | Forward or next | +| `up` | Up arrow | +| `down` | Down arrow | + +### Status + +| Icon | Description | +|------|-------------| +| `check`, `done` | Check or complete | +| `close` | Close or dismiss | +| `warning` | Warning | +| `error` | Error | +| `info` | Information | + +### Authentication + +| Icon | Description | +|------|-------------| +| `login` | Login | +| `logout`, `exit` | Logout or exit | +| `lock` | Locked | +| `unlock` | Unlocked | + +### Content + +| Icon | Description | +|------|-------------| +| `favorite`, `heart` | Favorite or like | +| `star` | Star or rating | +| `bookmark` | Bookmark | +| `image`, `photo` | Image or photo | +| `image-plus` | Add photo | +| `video` | Video | +| `folder` | Folder | +| `folder-lock` | Locked folder | +| `file`, `description` | Document or file | +| `book-open` | Book | +| `newspaper`, `news`, `article` | News or article | + +### Device & Hardware + +| Icon | Description | +|------|-------------| +| `camera` | Camera | +| `qr`, `qrcode`, `qr-code` | QR code scanner | +| `device-phone-mobile`, `smartphone` | Mobile phone | +| `vibrate` | Vibration | +| `bell` | Bell or notification | +| `finger-print`, `fingerprint` | Fingerprint or biometric | +| `light-bulb`, `lightbulb`, `flashlight` | Light bulb or flashlight | +| `map`, `location` | Map or location | +| `globe-alt`, `globe`, `web` | Globe or web | +| `bolt`, `flash` | Lightning bolt or flash | + +### Audio & Volume + +| Icon | Description | +|------|-------------| +| `speaker`, `speaker-wave` | Speaker with sound | +| `volume-up` | Volume up | +| `volume-down` | Volume down | +| `volume-mute`, `mute` | Muted | +| `volume-off` | Volume off | +| `music`, `audio`, `music-note` | Music or audio | +| `microphone`, `mic` | Microphone | + +### Miscellaneous + +| Icon | Description | +|------|-------------| +| `help` | Help or question | +| `about`, `information-circle` | Information or about | +| `more` | More options | +| `list` | List view | +| `visibility` | Visible | +| `visibility_off` | Hidden | + +## Best Practices + +Icons have meaning and most users will associate the visual cues of icons and the underlying behavior or section of an +application across apps. So try to maintain consistent use of icons to help guide users through your app. + +- **Stay consistent** - Use the same icon name throughout your app for the same action +- **Test on both platforms** - If you use auto-converted icons, verify they appear correctly on iOS and Android + +## Finding Icons + +### Android Material Icons + +Browse the complete Material Icons library at [Google Fonts Icons](https://fonts.google.com/icons). Use the icon name +exactly as shown (with underscores, e.g., `shopping_cart`, `qr_code_2`). + +### iOS SF Symbols + +Browse SF Symbols using this [community Figma file](https://www.figma.com/community/file/1549047589273604548). While not +comprehensive, it's a great starting point for discovering available symbols. + +For the complete library, download the [SF Symbols app](https://developer.apple.com/sf-symbols/) for macOS. + + diff --git a/resources/views/docs/mobile/2/edge-components/introduction.md b/resources/views/docs/mobile/2/edge-components/introduction.md new file mode 100644 index 00000000..a48d98ca --- /dev/null +++ b/resources/views/docs/mobile/2/edge-components/introduction.md @@ -0,0 +1,108 @@ +--- +title: Introduction +order: 1 +--- + +## What is EDGE? + +EDGE (Element Definition and Generation Engine) is NativePHP for Mobile's component system that transforms Blade +template syntax into platform-native UI elements that look beautiful whichever device your users are using. + +![](/img/docs/edge.png) + +Instead of rendering in the web view, EDGE components are compiled into truly native elements and live apart from the +web view's lifecycle. This means they are persistent and offer truly native performance. + +There's no custom rendering engine and complex ahead-of-time compilation process, just a lightweight transformation +step that happens at runtime. You end up with pure, fast and flexible native components — all configured by PHP! + +## Available Components + +Our first set of components are focused on navigation, framing your application with beautiful, platform-dependent UI +components. These familiar navigational elements will help your users feel immediately at home in your app and elevate +your app to feeling built for their chosen platform, just like a true native app. + +And all that without compromising your ability to build using tools and techniques you're already the most comfortable +with. + +For now, we have 3 main native components that you can configure: + +- **[Bottom Navigation](bottom-nav)** - The always-accessible bottom navigation bar +- **[Top Bar](top-bar)** - A title bar with action buttons +- **[Side Navigation](side-nav)** - A slide-out navigation drawer + +## How It Works + +@verbatim +```blade + + + +``` +@endverbatim + +You simply define your components in Blade and EDGE processes these during each request, passing instructions to the +native side. The native UI rendering pipeline takes over to generate your defined components and builds the interface +just the way your users would expect, enabling your app to use the latest and greatest parts of each platform, +such as Liquid Glass on iOS. + +Under the hood, the Blade components are compiled down to a simple JSON configuration which we pass to the native side. +The native code already contains the generic components compiled-in. These are then rendered as needed based on the +JSON configuration. + + + +## Why Blade? + +Blade is an expressive and straightforward templating language that is very familiar to most Laravel users, and also +super accessible to anyone who's used to writing HTML. All of our components are Blade components, which allows us to +use Blade's battle-tested processing engine to rapidly compile the necessary transformation just in time. + +## Where to define your native components + +They can be defined in any Blade file, but for them to be processed, that Blade file will need to be rendered. We +recommend putting your components in a Blade component that is likely to be rendered on every request, such as your +main layout, e.g. `layouts/app.blade.php` or one of its child views/components. + +## Props Validation + +EDGE components enforce required props validation to prevent misconfiguration. If you're missing required props, you'll +see a clear error message that tells you exactly what's missing and how to fix it. + +For example, if you forget the `label` prop on a bottom navigation item: + +``` +EDGE Component is missing required properties: 'label'. +Add these attributes to your component: label="..." +``` + +The error message will list all missing required props and show you exactly which attributes you need to add. This +validation happens at render time, making it easy to catch configuration issues during development. + +Each component's documentation page indicates which props are required vs optional. + +## Using Inertia? + +Each link in an EDGE component will do a full post back to PHP, which may not be what you want if you are using Inertia. To transform these requests into Inertia ``, add `router` to your `window` object: + +```typescript +import { router } from '@inertiajs/vue3'; + +declare global { + interface Window { + router: typeof router; + } +} + +window.router = router; +``` diff --git a/resources/views/docs/mobile/2/edge-components/side-nav.md b/resources/views/docs/mobile/2/edge-components/side-nav.md new file mode 100644 index 00000000..fd3dde74 --- /dev/null +++ b/resources/views/docs/mobile/2/edge-components/side-nav.md @@ -0,0 +1,109 @@ +--- +title: Side Navigation +order: 400 +--- + +## Overview + +
+ +![](/img/docs/edge-side-nav-ios.png) + +![](/img/docs/edge-side-nav-android.png) + +
+ +A slide-out navigation drawer with support for groups, headers, and dividers. + +@verbatim +```blade + + + + + + + + + + + + + + +``` +@endverbatim + +## Props + +- `gestures-enabled` - Swipe to open (default: `false`) [Android] +- `dark` - Force dark mode (optional) + + + +## Children + +### `` + +- `title` - Title text (optional) +- `subtitle` - Subtitle text (optional) +- `icon` - A named [icon](icons) (optional) +- `background-color` - Background color. Hex code (optional) +- `show-close-button` - Show a close × (optional, default: `true`) [Android] +- `pinned` - Keep header visible when scrolling (optional, default: `false`) + +### `` + +- `id` - Unique identifier (required) +- `label` - Display text (required) +- `icon` - A named [icon](icons) (required) +- `url` - A URL to navigate to in the web view (required) +- `active` - Highlight this item as active (optional, default: `false`) +- `badge` - Badge text (optional) +- `badge-color` - Hex code or named color (optional) + + + +### `` + +- `heading` - The group's heading (required) +- `expanded` - Initially expanded (optional, default: `false`) +- `icon` - Material icon (optional) + +### `` + +Add visual separators between navigation items. This item has no properties. diff --git a/resources/views/docs/mobile/2/edge-components/top-bar.md b/resources/views/docs/mobile/2/edge-components/top-bar.md new file mode 100644 index 00000000..5f551015 --- /dev/null +++ b/resources/views/docs/mobile/2/edge-components/top-bar.md @@ -0,0 +1,68 @@ +--- +title: Top Bar +order: 50 +--- + +## Overview +
+ +![](/img/docs/edge-top-bar-ios.png) + +![](/img/docs/edge-top-bar-android.png) + +
+ +A top bar with title and action buttons. This renders at the top of the screen. + +@verbatim +```blade + + + + +``` +@endverbatim + +## Props + +- `title` - The title text (required) +- `show-navigation-icon` - Show back/menu button (optional, default: `true`) +- `label` - If more than 5 actions, iOS will display an overflow menu and the labels assigned to each item (optional) +- `background-color` - Background color. Hex code (optional) +- `text-color` - Text color. Hex code (optional) +- `elevation` - Shadow depth 0-24 (optional) [Android] + +## Children + +A `` can contain up to 10 `` elements. These are displayed on the trailing edge of the bar. + +### Props +- `id` - Unique identifier (required) +- `icon` - A named [icon](icons) (required) +- `label` - Accessibility label (optional) +- `url` - A URL to navigate to in the web view (optional) + +On Android, the first 3 actions are shown as icon buttons; additional actions collapse into an overflow menu (⋮). On iOS, if more than 5 actions are provided, they collapse into an overflow menu. + +### `` Props + +- `id` - Unique identifier (required) +- `icon` - A named [icon](icons) (required) +- `label` - Text label for the action. Used for accessibility and displayed in overflow menus (optional but recommended) +- `url` - A URL to navigate to when tapped + + diff --git a/resources/views/docs/mobile/2/getting-started/_index.md b/resources/views/docs/mobile/2/getting-started/_index.md new file mode 100644 index 00000000..5c684744 --- /dev/null +++ b/resources/views/docs/mobile/2/getting-started/_index.md @@ -0,0 +1,4 @@ +--- +title: Getting Started +order: 1 +--- \ No newline at end of file diff --git a/resources/views/docs/mobile/2/getting-started/changelog.md b/resources/views/docs/mobile/2/getting-started/changelog.md new file mode 100644 index 00000000..5e9a01d0 --- /dev/null +++ b/resources/views/docs/mobile/2/getting-started/changelog.md @@ -0,0 +1,354 @@ +--- +title: Changelog +order: 2 +--- + +## v2.2.0 + +This release includes significant improvements to EDGE components, installation experience, and build tooling. + +### EDGE Required Props Validation +EDGE components now validate required props at render time. A new `MissingRequiredPropsException` provides clear error messages showing exactly which props are missing and how to fix them. Validation is enforced on `BottomNavItem`, `SideNavItem`, `TopBarAction`, `SideNavGroup`, and `TopBar`. + +### Font-Based Icon Rendering +Replaced the 30MB `material-icons-extended` library with a 348KB font file using font ligatures for efficient icon rendering. + +- **iOS**: Supports direct SF Symbol paths (e.g., `car.side.fill`, `flashlight.on.fill`) +- **Android**: Any Material Icon ligature name works directly +- Cross-platform friendly name aliases are maintained for convenience + +@verbatim +```blade +@use(Native\Mobile\Facades\System) + + +``` +@endverbatim + +### Auto-Prompt for App ID +`native:install` now prompts for `NATIVEPHP_APP_ID` if not already set and auto-generates a suggested bundle ID using the format `com.{username}.{randomwords}`. + +### Improved iOS Build Logging +Standardized console output using `twoColumnDetail()` format for App Store Connect operations. Replaced emoji-prefixed logs with consistent Laravel Prompts styling. + +Elaborate error diagnostics with pattern matching for common Xcode build errors now provide specific actionable solutions for: +- Certificate/provisioning profile mismatches +- Expired provisioning profiles +- Missing signing certificates +- Code signing errors + +When builds fail, the last 30 lines of error output are shown along with the build log path for debugging. + +### Other Improvements +- Better handling of false positives from Apple during upload +- Build log paths use `note()` for consistent styling + +## v2.1.1 + +### Foreground permissions +Prevent removal of FOREGROUND_SERVICE and POST_NOTIFICATIONS permissions when they're needed by camera features, even if push notifications are disabled + +### Symlink fix +Run storage:unlink before storage:link to handle stale symlinks, and exclude public/storage from build to prevent symlink conflicts + +### iOS Push Notifications +Handles push notification APNS flow differently, fires off the native event as soon as the token is received from FCM vs assuming the AppDelegate will ahndle it. + +### Fix Missing $id param on some events +Some events were missing an `$id` parameter, which would cause users to experience errors when trying to receive an ID from the event. + +## v2.1.0 + +### Cleaner Console Output +The `native:run` command now provides cleaner, more readable output, making it easier to follow what's happening during development. + +### Improved Windows Support +Better compatibility and smoother development experience for Windows users. + + +### Blade Directives +New Blade directives for conditional rendering based on platform: + +```blade +Only rendered in mobile apps +@mobile / @endmobile + +Only rendered in web browsers +@web / @endweb + +Only rendered on iOS +@ios / @endios + +Only rendered on Android +@android / @endandroid +``` + +### Improved File Watcher +The file watcher has been completely overhauled, switching from fswatch to [Watchman](https://facebook.github.io/watchman/) for better performance and reliability. The watcher is now combined with Vite HMR for a unified development experience. + +### Common URL Schemes +NativePHP now automatically handles common URL schemes, opening them in the appropriate native app: +- `tel:` - Phone calls +- `mailto:` - Email +- `sms:` - Text messages +- `geo:` - Maps/location +- `facetime:` - FaceTime video calls +- `facetime-audio:` - FaceTime audio calls + +### Android Deep Links +Support for custom deep links and app links on Android, allowing other apps and websites to link directly into your app. + +### Other Changes +- `System::appSettings()` to open your app's settings screen in the OS Settings app +- `Edge::clear()` to remove all EDGE components +- Added `Native.shareUrl()` to the JavaScript library +- `native:install`: Added `--fresh` and `-F` as aliases of `--force` +- `native:install`: Increased timeout for slower networks + +### Bug Fixes +- Fixed Scanner permissions +- Fixed Android edge-to-edge display +- Fixed `Browser::auth` on iOS +- Fixed text alignment in native top-bar component on iOS +- Fixed plist issues on iOS +- Fixed `NATIVEPHP_START_URL` configuration +- Fixed camera cancelled events on Android +- Fixed bottom-nav values not updating dynamically + +## v2.0.0 + +### JavaScript/TypeScript Library +A brand-new JavaScript bridge library with full TypeScript declarations for Vue, React, Inertia, and vanilla JS apps. +This enables calling native device features directly from your frontend code. Read more about it +[here](../the-basics/native-functions#run-from-anywhere). + +### EDGE - Element Definition and Generation Engine +A new native UI system for rendering navigation components natively on device using Blade. Read more about it [here](../edge-components/introduction). + +### Laravel Boost Support +Full integration with Laravel Boost for AI-assisted development. Read more about it [here](../getting-started/development#laravel-boost). + +### Hot Module Replacement (HMR) Overhauled +Full Vite HMR for rapid development. Read more about it [here](../getting-started/development#hot-reloading). + +Features: +- Custom Vite plugin +- Automatic HMR server configuration for iOS/Android +- PHP protocol adapter for axios on iOS (no more `patch-inertia` command!) +- Works over the network even without a physical device plugged in! + +### Fluent Pending API (PHP) +All [Asynchronous Methods](../the-basics/events#understanding-async-vs-sync) now implement a fluent API for better IDE support and ease of use. + + + + + +```php +Dialog::alert('Confirm', 'Delete this?', ['Cancel', 'Delete']) + ->remember() + ->show(); +``` + + + + +```js +import { dialog, on, off, Events } from '#nativephp'; +const label = ref(''); + +const openAlert = async () => { + await dialog.alert() + .title('Alert') + .message('This is an alert dialog.') + .buttons(['OK', 'Cool', 'Cancel']); +}; + +const buttonPressed = (payload: any) => { + label.value = payload.label; +}; + +onMounted(() => { + on(Events.Alert.ButtonPressed, buttonPressed); +}); +``` + + + + +### `#[OnNative]` Livewire Attribute +Forget the silly string concatenation of yesterday; get into today's fashionable attribute usage with this drop-in +replacement: + +```php +use Livewire\Attributes\OnNative; // [tl! remove] +use Native\Mobile\Attributes\OnNative; // [tl! add] + +#[On('native:'.ButtonPressed::class)] // [tl! remove] +#[OnNative(ButtonPressed::class)] // [tl! add] +public function handle() +``` + +### Video Recording +Learn more about the new Video Recorder support [here](../apis/camera#coderecordvideocode). + +### QR/Barcode Scanner +Learn more about the new QR/Barcode Scanner support [here](../apis/scanner). + +### Microphone +Learn more about the new Microphone support [here](../apis/microphone). + +### Network Detection +Learn more about the new Network Detection support [here](../apis/network). + +### Background Audio Recording +Just update your config and record audio even while the device is locked! + +```php +// config/nativephp.php +'permissions' => [ + 'microphone' => true, + 'microphone_background' => true, +], +``` +### Push Notifications API +New fluent API for push notification enrollment: + + + + +```php +use Native\Mobile\Facades\PushNotifications; +use Native\Mobile\Events\PushNotification\TokenGenerated; + +PushNotifications::enroll(); + +#[OnNative(TokenGenerated::class)] +public function handlePushNotificationsToken($token) +{ + $this->token = $token; +} +``` + + +```js +import { pushNotifications, on, off, Events } from '#nativephp'; + +const token = ref(''); + +const promptForPushNotifications = async () => { + await pushNotifications.enroll(); +}; + +const handlePushNotificationsToken = (payload: any) => { + token.value = payload.token; +}; + +onMounted(() => { + on(Events.PushNotification.TokenGenerated, handlePushNotificationsToken); +}); + +onUnmounted(() => { + off(Events.PushNotification.TokenGenerated, handlePushNotificationsToken); +}); +``` + + + +**Deprecated Methods:** +- `enrollForPushNotifications()` → use `enroll()` +- `getPushNotificationsToken()` → use `getToken()` + +### Platform Improvements + +#### iOS +- **Platform detection** - `nativephp-ios` class on body +- **Keyboard detection** - `keyboard-visible` class when keyboard shown +- **iOS 26 Liquid Glass** support +- **Improved device selector** on `native:run` showing last-used device +- **Load Times** dramatically improved. Now 60-80% faster! + +#### Android +- **Complete Android 16+ 16KB page size** compatibility +- **Jetpack Compose UI** - Migrated from XML layouts +- **Platform detection** - `nativephp-android` class on body +- **Keyboard detection** - `keyboard-visible` class when keyboard shown +- **Parallel zip extraction** for faster installations +- **Load Times** dramatically improved. ~40% faster! +- **Page Load Times** dramatically decreased by ~40%! +--- + +### Configuration + +#### New Options +```php +'start_url' => env('NATIVEPHP_START_URL', '/'), + +'permissions' => [ + 'microphone' => false, + 'microphone_background' => false, + 'scanner' => false, + 'network_state' => true, // defaults to true +], + +'ipad' => false, + +'orientation' => [ + 'iphone' => [...], + 'android' => [...], +], +``` + +#### Custom Permission Reasons (iOS) +```php +'camera' => 'We need camera access to scan membership cards.', +'location' => 'Location is used to find nearby stores.', +``` + +### New Events + +- `Camera\VideoRecorded`, `Camera\VideoCancelled`, `Camera\PhotoCancelled` +- `Microphone\MicrophoneRecorded`, `Microphone\MicrophoneCancelled` +- `Scanner\CodeScanned` + +### Custom Events + +Many native calls now accept custom event classes! + +```php +Dialog::alert('Confirm', 'Delete this?', ['Cancel', 'Delete']) + ->event(MyCustomEvent::class) +``` + +### Better File System Support +NativePHP now symlinks your filesystems! Persisted storage stays in storage but is symlinked to the public directory for +display in the web view! Plus a pre-configured `mobile_public` filesystem disk. + +```dotenv +FILESYSTEM_DISK=mobile_public +``` +```php +$imageUrl = Storage::url($path); +``` +```html + +``` + +### Bug Fixes + +- Fixed infinite recursion during bundling in some Laravel setups +- Fixed iOS toolbar padding for different device sizes +- Fixed Android debug mode forcing `APP_DEBUG=true` +- Fixed orientation config key case sensitivity (`iPhone` vs `iphone`) + +### Breaking Changes + +- None diff --git a/resources/views/docs/mobile/2/getting-started/configuration.md b/resources/views/docs/mobile/2/getting-started/configuration.md new file mode 100644 index 00000000..5b8eae02 --- /dev/null +++ b/resources/views/docs/mobile/2/getting-started/configuration.md @@ -0,0 +1,175 @@ +--- +title: Configuration +order: 200 +--- + +## Overview + +NativePHP for Mobile is designed so that most configuration happens **inside your Laravel application**, without +requiring you to open Xcode or Android Studio and manually update config files. + +This page explains the key configuration points you can control through Laravel. + +## The `nativephp.php` Config File + +The `config/nativephp.php` config file contains a number of useful options. + +NativePHP uses sensible defaults and makes several assumptions based on default installations for tools required to +build and run apps from your computer. + +You can override these defaults by editing the `nativephp.php` config file in your Laravel project, and in many cases +simply by changing environment variables. + +## `NATIVEPHP_APP_ID` + +You must set your app ID to something unique. A common practice is to use a reverse-DNS-style name, e.g. +`com.yourcompany.yourapp`. + +Your app ID (also known as a *Bundle Identifier*) is a critical piece of identification across both Android and iOS +platforms. Different app IDs are treated as separate apps. + +And it is often referenced across multiple services, such as Apple Developer Center and the Google Play Console. + +So it's not something you want to be changing very often. + +## `NATIVEPHP_APP_VERSION` + +The `NATIVEPHP_APP_VERSION` environment variable controls your app's versioning behavior. + +When your app is compiling, NativePHP first copies the relevant Laravel files into a temporary directory, zips them up, +and embeds the archive into the native application. + +When your app boots, it checks the embedded version against the previously installed version to see if it needs to +extract the bundled Laravel application. + +If the versions match, the app uses the existing files without re-extracting the archive. + +To force your application to always install the latest version of your code - especially useful during development - +set this to `DEBUG`: + +```dotenv +NATIVEPHP_APP_VERSION=DEBUG +``` + +Note that this will make your application's boot up slightly slower as it must unpack the zip every time it loads. + +But this ensures that you can iterate quickly during development, while providing a faster, more stable experience for +end users once an app is published. + +## Cleanup `env` keys + +The `cleanup_env_keys` array in the config file allows you to specify keys that should be removed from the `.env` file +before bundling. This is useful for removing sensitive information like API keys or other secrets. + +## Cleanup `exclude_files` + +The `cleanup_exclude_files` array in the config file allows you to specify files and folders that should be removed +before bundling. This is useful for removing files like logs or other temporary files that aren't required for your app +to function and bloat your downloads. + +## Permissions +In general, the app stores don't want your app to have permissions (a.k.a entitlements) it doesn't need. + +By default, all optional permissions are disabled. + +You may enable the features you intend to use simply by changing the value of the appropriate permission to `true`: + +```php + 'permissions' => [ + 'biometric' => false, + 'camera' => false, + 'location' => false, + 'microphone' => false, + 'microphone_background' => false, + 'network_state' => true, + 'nfc' => false, + 'push_notifications' => false, + 'storage_read' => false, + 'storage_write' => false, + 'scanner' => false, + 'vibrate' => false, + ], +``` + +For iOS, this will provide a sensible default description. + +### Custom permission descriptions + +For iOS, it's possible to define custom permission descriptions. In most cases, you are required to provide clear +reasons why your app needs certain permissions. You can do this easily from the config file: + +```php + 'permissions' => [ + 'biometric' => 'Access to the biometric sensor is needed to secure user resources', + //... + ], +``` + +### Available permissions + +- `biometric` - Allows your application to use fingerprint or face-recognition hardware (with a fallback to PIN code) + to secure parts of your application. +- `camera` - Allows your application to request access to the device's camera, if present. Required for taking photos and + recording video. Note that the user may deny access and any camera functions will then result in a no-op. +- `nfc` - Allows your application to request access to the device's NFC reader, if present. +- `push_notifications` - Allows your application to request permissions to send push notifications. Note that the user + may deny this and any push notification functions will then result in a no-op. +- `location` - Allows your application to request access to the device's GPS receiver, if present. Note that the user + may deny this and any location functions will then result in a no-op. +- `vibrate` - In modern Android devices this is a requirement for most haptic feedback. +- `storage_read` - Grants your app access to read from device storage locations. This is not required for basic app file manipulation. +- `storage_write` - Allows your app to write to device storage. This is not required for basic app file manipulation. +- `microphone` - Allows your application to request access to the device's microphone, if present. Required for audio + recording functionality. Note that the user may deny access and any microphone functions will then result in a no-op. +- `microphone_background` - Allows your application to request access to the device's microphone, if present. Required + for audio recording functionality. Note that the user may deny access and any microphone functions will then result in + a no-op. +- `scanner` - Allows your application to scan QR codes and barcodes. Note that the user may deny camera access and any + scanning functions will then result in a no-op. +- `network_state` - Allows your application to access information about the device's network connectivity status. This + permission is enabled by default as it's commonly needed for basic network state detection. + +## Orientation + +NativePHP (as of v1.10.3) allows users to custom specific orientations per device through the config file. The config +allows for granularity for iPad, iPhone and Android devices. Options for each device can be seen below. + +NOTE: if you want to disable iPad support completely simply apply `false` for each option. + +```php +'orientation' => [ + 'iphone' => [ + 'portrait' => true, + 'upside_down' => false, + 'landscape_left' => false, + 'landscape_right' => false, + ], + 'android' => [ + 'portrait' => true, + 'upside_down' => false, + 'landscape_left' => false, + 'landscape_right' => false, + ], +], +``` + +Regardless of these orientation settings, if your app supports iPad, it will be available in all orientations. + +## iPad Support + +With NativePHP, your app can work on iPad too! If you wish to support iPad, simply set the `ipad` config option to `true`: + +```php +'ipad' => true, +``` + +Using standard CSS responsive design principles, you can make your app work beautifully across all screen sizes. + + diff --git a/resources/views/docs/mobile/2/getting-started/deployment.md b/resources/views/docs/mobile/2/getting-started/deployment.md new file mode 100644 index 00000000..68bed419 --- /dev/null +++ b/resources/views/docs/mobile/2/getting-started/deployment.md @@ -0,0 +1,512 @@ +--- +title: Deployment +order: 300 +--- + +Deploying mobile apps is a complicated process — and it's different for each platform! + + + +Generally speaking you need to: + +1. **Releasing**: Create a _release build_ for each platform. +2. **Testing**: Test this build on real devices. +3. **Packaging**: Sign and distribute this build to the stores. +4. **Submitting for Review**: Go through each store's submission process to have your app reviewed. +5. **Publishing**: Releasing the new version to the stores and your users. + +It's initially more time-consuming when creating a brand new app in the stores, as you need to get the listing set up +in each store and create your signing credentials. + +If you've never done it before, allow a couple of hours so you can focus on getting things right and understand +everything you need. + +Don't rush through the app store processes! There are compliance items that if handled incorrectly will either prevent +you from publishing your app, being unable to release it in the territories you want to make it available to, or simply +having it get rejected immediately when you submit it for review if you don't get those right. + +It's typically easier once you've released the first version of your app and after you've done 2 or 3 apps, you'll fly +through the process! + + + +## Releasing + +To prepare your app for release, you should set the version number to a new version number that you have not used +before and increment the build number: + +```dotenv +NATIVEPHP_APP_VERSION=1.2.3 +NATIVEPHP_APP_VERSION_CODE=48 +``` + +### Versioning + +You have complete freedom in how you version your applications. You may use semantic versioning, codenames, +date-based versions, or any scheme that works for your project, team or business. + +Remember that your app versions are usually public-facing (e.g. in store listings and on-device settings and update +screens) and can be useful for customers to reference if they need to contact you for help and support. + +The build number is managed via the `NATIVEPHP_APP_VERSION` key in your `.env`. + +### Build numbers + +Both the Google Play Store and Apple App Store require your app's build number to increase for each release you submit. + +The build number is managed via the `NATIVEPHP_APP_VERSION_CODE` key in your `.env`. + +### Run a `release` build + +Then run a release build: + +```shell +php artisan native:run --build=release +``` + +This builds your application with various optimizations that reduce its overall size and improve its performance, such +as removing debugging code and unnecessary features (i.e. Composer dev dependencies). + +**You should test this build on a real device.** Once you're happy that everything is working as intended you can then +submit it to the stores for approval and distribution. + +- [Google Play Store submission guidelines](https://support.google.com/googleplay/android-developer/answer/9859152?hl=en-GB#zippy=%2Cmaximum-size-limit) +- [Apple App Store submission guidelines](https://developer.apple.com/ios/submit/) + +## Packaging Your App + +The `native:package` command creates signed, production-ready apps for distribution to the App Store and Play Store. +This command handles all the complexity of code signing, building release artifacts, and preparing files for submission. + +## Before You Begin + +Before you can package your app for distribution, ensure: + +1. Your app is fully developed and tested on both platforms +2. You have a valid bundle ID and app ID configured in your `nativephp.php` config +3. For Android: You have a signing keystore with a valid key alias +4. For iOS: You have the necessary signing certificates and provisioning profiles from Apple Developer +5. All configuration is complete (see the [configuration guide](/docs/mobile/2/getting-started/configuration)) + +## Android Packaging + +### Creating Android Signing Credentials + +NativePHP provides a convenient command to generate all the signing credentials you need for Android: + +```bash +php artisan native:credentials android +``` + +This command will: +- Generate a new JKS keystore file +- Create all necessary signing keys +- Automatically add the credentials to your `.env` file +- Add the keystore to your `.gitignore` to keep it secure + +The credentials will be saved in the `nativephp/credentials/android/` directory and automatically configured for use with the package command. + +### Required Signing Credentials + +To build a signed Android app, you need four pieces of information: + +| Credential | Option | Environment Variable | Description | +|-----------|--------|----------------------|-------------| +| Keystore file | `--keystore` | `ANDROID_KEYSTORE_FILE` | Path to your `.keystore` file | +| Keystore password | `--keystore-password` | `ANDROID_KEYSTORE_PASSWORD` | Password for the keystore | +| Key alias | `--key-alias` | `ANDROID_KEY_ALIAS` | Name of the key within the keystore | +| Key password | `--key-password` | `ANDROID_KEY_PASSWORD` | Password for the specific key | + +### Building a Release APK + +An APK (Android Package) is a single binary file suitable for direct distribution or testing on specific devices: + +```bash +php artisan native:package android \ + --keystore=/path/to/my-app.keystore \ + --keystore-password=mykeystorepassword \ + --key-alias=my-app-key \ + --key-password=mykeypassword +``` + +The build process prepares your Android project, compiles the code, and signs the APK with your certificate. When complete, the output directory opens automatically, showing your signed `app-release.apk` file. + +### Building an Android App Bundle (AAB) + +An AAB (Android App Bundle) is required for distribution through the Play Store. It's an optimized format that the Play Store uses to generate device-specific APKs automatically: + +```bash +php artisan native:package android \ + --build-type=bundle \ + --keystore=/path/to/my-app.keystore \ + --keystore-password=mykeystorepassword \ + --key-alias=my-app-key \ + --key-password=mykeypassword +``` + +This creates a signed `app-release.aab` file ready for Play Store submission. + +### Using Environment Variables + +Instead of passing credentials as command options, you can store them in your `.env` file: + +```env +ANDROID_KEYSTORE_FILE=/path/to/my-app.keystore +ANDROID_KEYSTORE_PASSWORD=mykeystorepassword +ANDROID_KEY_ALIAS=my-app-key +ANDROID_KEY_PASSWORD=mykeypassword +``` + +Then simply run: + +```bash +php artisan native:package android --build-type=bundle +``` + +### Uploading to Play Store + +If you have a Google Service Account with Play Console access, you can upload directly from the command: + +```bash +php artisan native:package android \ + --build-type=bundle \ + --keystore=/path/to/my-app.keystore \ + --keystore-password=mykeystorepassword \ + --key-alias=my-app-key \ + --key-password=mykeypassword \ + --upload-to-play-store \ + --play-store-track=internal \ + --google-service-key=/path/to/service-account-key.json +``` + +The `--play-store-track` option controls where the build is released: + +- `internal` - Internal testing (default, fastest review) +- `alpha` - Closed alpha testing +- `beta` - Closed beta testing +- `production` - Production release + +### Testing Play Store Uploads + +If you already have an AAB file and want to test uploading without rebuilding, use `--test-push`: + +```bash +php artisan native:package android \ + --test-push=/path/to/app-release.aab \ + --upload-to-play-store \ + --play-store-track=internal \ + --google-service-key=/path/to/service-account-key.json +``` + +This skips the entire build process and only handles the upload. + +### Skipping Build Preparation + +For incremental builds where you haven't changed native code, you can skip the preparation phase: + +```bash +php artisan native:package android \ + --build-type=bundle \ + --keystore=/path/to/my-app.keystore \ + --keystore-password=mykeystorepassword \ + --key-alias=my-app-key \ + --key-password=mykeypassword \ + --skip-prepare +``` + +## iOS Packaging + +### Required Signing Credentials + +iOS apps require several credentials from Apple Developer: + +| Credential | Option | Environment Variable | Description | +|-----------|--------|----------------------|-------------| +| API Key file | `--api-key-path` | `APP_STORE_API_KEY_PATH` | Path to `.p8` file from App Store Connect | +| API Key ID | `--api-key-id` | `APP_STORE_API_KEY_ID` | Key ID from App Store Connect | +| API Issuer ID | `--api-issuer-id` | `APP_STORE_API_ISSUER_ID` | Issuer ID from App Store Connect | +| Certificate | `--certificate-path` | `IOS_DISTRIBUTION_CERTIFICATE_PATH` | Distribution certificate (`.p12` or `.cer`) | +| Certificate password | `--certificate-password` | `IOS_DISTRIBUTION_CERTIFICATE_PASSWORD` | Password for the certificate | +| Provisioning profile | `--provisioning-profile-path` | `IOS_DISTRIBUTION_PROVISIONING_PROFILE_PATH` | Profile file (`.mobileprovision`) | +| Team ID | `--team-id` | `IOS_TEAM_ID` | Apple Developer Team ID | + +### Setting Up App Store Connect API + +To upload to the App Store directly from the command line, you'll need an API key: + +1. Log in to [App Store Connect](https://appstoreconnect.apple.com) +2. Navigate to Users & Access +3. Click Integrations tab → Keys → App Store Connect API +4. Click the "+" or "Request Access" button to create a new key with "Developer" access +5. Download the `.p8` file immediately (you can't download it again later) +6. Note the Key ID and Issuer ID displayed on the page + +### Export Methods + +The `--export-method` option controls how your app is packaged: + +- `app-store` - For App Store distribution (default) +- `ad-hoc` - For distribution to specific registered devices +- `enterprise` - For enterprise distribution (requires enterprise program) +- `development` - For development and testing + +### Building for App Store + +To build a production app ready for App Store submission: + +```bash +php artisan native:package ios \ + --export-method=app-store \ + --api-key-path=/path/to/api-key.p8 \ + --api-key-id=ABC123DEF \ + --api-issuer-id=01234567-89ab-cdef-0123-456789abcdef \ + --certificate-path=/path/to/distribution.p12 \ + --certificate-password=certificatepassword \ + --provisioning-profile-path=/path/to/profile.mobileprovision \ + --team-id=ABC1234567 +``` + +### Building for Ad-Hoc Distribution + +For distributing to specific devices without going through the App Store: + +```bash +php artisan native:package ios \ + --export-method=ad-hoc \ + --certificate-path=/path/to/distribution.p12 \ + --certificate-password=certificatepassword \ + --provisioning-profile-path=/path/to/ad-hoc-profile.mobileprovision +``` + +### Building for Development + +For testing on your own device: + +```bash +php artisan native:package ios \ + --export-method=development \ + --certificate-path=/path/to/development.p12 \ + --certificate-password=certificatepassword \ + --provisioning-profile-path=/path/to/development-profile.mobileprovision +``` + +### Using Environment Variables + +Store your iOS credentials in `.env`: + +```env +APP_STORE_API_KEY_PATH=/path/to/api-key.p8 +APP_STORE_API_KEY_ID=ABC123DEF +APP_STORE_API_ISSUER_ID=01234567-89ab-cdef-0123-456789abcdef +IOS_DISTRIBUTION_CERTIFICATE_PATH=/path/to/distribution.p12 +IOS_DISTRIBUTION_CERTIFICATE_PASSWORD=certificatepassword +IOS_DISTRIBUTION_PROVISIONING_PROFILE_PATH=/path/to/profile.mobileprovision +IOS_TEAM_ID=ABC1234567 +``` + +Then build with: + +```bash +php artisan native:package ios --export-method=app-store +``` + +### Uploading to App Store Connect + +To automatically upload after building: + +```bash +php artisan native:package ios \ + --export-method=app-store \ + --api-key-path=/path/to/api-key.p8 \ + --api-key-id=ABC123DEF \ + --api-issuer-id=01234567-89ab-cdef-0123-456789abcdef \ + --certificate-path=/path/to/distribution.p12 \ + --certificate-password=certificatepassword \ + --provisioning-profile-path=/path/to/profile.mobileprovision \ + --team-id=ABC1234567 \ + --upload-to-app-store +``` + +The upload uses the App Store Connect API, so API credentials are required only when using `--upload-to-app-store`. + +### Validating Provisioning Profiles + +Before building, you can validate your provisioning profile to check push notification support and entitlements: + +```bash +php artisan native:package ios \ + --validate-profile \ + --provisioning-profile-path=/path/to/profile.mobileprovision +``` + +This extracts and displays: + +- Profile name +- All entitlements configured in the profile +- Push notification support status +- Associated domains +- APS environment matching + +### Testing App Store Uploads + +To test uploading an existing IPA without rebuilding: + +```bash +php artisan native:package ios \ + --test-upload \ + --api-key-path=/path/to/api-key.p8 \ + --api-key-id=ABC123DEF \ + --api-issuer-id=01234567-89ab-cdef-0123-456789abcdef +``` + +### Clearing Xcode Caches + +If you encounter build issues, clear Xcode and Swift Package Manager caches: + +```bash +php artisan native:package ios \ + --export-method=app-store \ + --clean-caches +``` + +### Forcing a Clean Rebuild + +To force a complete rebuild and create a new archive: + +```bash +php artisan native:package ios \ + --export-method=app-store \ + --rebuild +``` + +### Validating Without Exporting + +To validate the archive without creating an IPA: + +```bash +php artisan native:package ios \ + --validate-only +``` + +## Version Management + +### Build Numbers and Version Codes + +The `native:version` command handles version management. When building AABs for the Play Store with valid Google Service credentials, NativePHP automatically checks the Play Store for the latest build number and increments it. + +### Auto-Incrementing from Play Store + +When building a bundle with Play Store access: + +```bash +php artisan native:package android \ + --build-type=bundle \ + --keystore=/path/to/my-app.keystore \ + --keystore-password=mykeystorepassword \ + --key-alias=my-app-key \ + --key-password=mykeypassword \ + --google-service-key=/path/to/service-account-key.json +``` + +The command automatically queries the Play Store to find your latest published build number and increments it. + +### Jumping Ahead in Version Numbers + +If you need to skip version numbers or jump ahead: + +```bash +php artisan native:package android \ + --build-type=bundle \ + --keystore=/path/to/my-app.keystore \ + --keystore-password=mykeystorepassword \ + --key-alias=my-app-key \ + --key-password=mykeypassword \ + --jump-by=10 +``` + +This adds 10 to the version code that would normally be used. + +## Tips & Troubleshooting + +### Common Android Issues + +**Keystore-related errors:** +- Verify the keystore file exists and the path is correct +- Check that the keystore password is correct +- Confirm the key alias exists in the keystore with: `keytool -list -v -keystore /path/to/keystore` +- Verify the key password matches + +**Build failures:** +- Ensure you have the latest Android SDK and build tools installed +- Check that your `nativephp/android` directory exists and is properly initialized +- If you've modified native code, don't use `--skip-prepare` + +**Play Store upload failures:** +- Verify the Google Service Account has access to your app in Play Console +- Ensure the service account key file is valid and readable +- Check that your bundle ID matches your Play Console app ID + +### Custom Output Directories + +By default, the build output opens in your system's file manager. To copy the artifact to a custom location: + +```bash +php artisan native:package android \ + --build-type=bundle \ + --keystore=/path/to/my-app.keystore \ + --keystore-password=mykeystorepassword \ + --key-alias=my-app-key \ + --key-password=mykeypassword \ + --output=/path/to/custom/directory +``` + +### Building in Non-Interactive Environments + +For CI/CD pipelines or automated builds, disable TTY mode: + +```bash +php artisan native:package android \ + --build-type=bundle \ + --keystore=/path/to/my-app.keystore \ + --keystore-password=mykeystorepassword \ + --key-alias=my-app-key \ + --key-password=mykeypassword \ + --no-tty +``` + +### Artifact Locations + +Once complete, signed artifacts are located at: + +**Android:** +- APK (release): `nativephp/android/app/build/outputs/apk/release/app-release.apk` +- AAB (bundle): `nativephp/android/app/build/outputs/bundle/release/app-release.aab` + +**iOS:** +- IPA: Generated in Xcode's build output directory diff --git a/resources/views/docs/mobile/2/getting-started/development.md b/resources/views/docs/mobile/2/getting-started/development.md new file mode 100644 index 00000000..b27b01a5 --- /dev/null +++ b/resources/views/docs/mobile/2/getting-started/development.md @@ -0,0 +1,226 @@ +--- +title: Development +order: 250 +--- + +Developing your NativePHP apps can be done in the browser, using workflows with which you're already familiar. + +This allows you to iterate rapidly on parts like the UI and major functionality, even using your favorite tools for +testing etc. + +But when you want to test _native_ features, then you must run your app on a real or emulated device. + +Whether you run your native app on an emulated or real device, it will require compilation after changes have been made. + + + +## Build your frontend + +If you're using Vite or similar tooling to build any part of your UI (e.g. for React/Vue, Tailwind etc), you'll need +to run your asset build command _before_ compiling your app. + +To facilitate ease of development, you should install the `nativephpMobile` Vite plugin. + +### The `nativephpMobile` Vite plugin + +To make your frontend build process works well with NativePHP, simply add the `nativephpMobile` plugin to your +`vite.config.js`: + +```js +import { nativephpMobile, nativephpHotFile } from './vendor/nativephp/mobile/resources/js/vite-plugin.js'; // [tl! focus] + +export default defineConfig({ + plugins: [ + laravel({ + input: ['resources/css/app.css', 'resources/js/app.js'], + refresh: true, + hotFile: nativephpHotFile(), // [tl! focus] + }), + tailwindcss(), + nativephpMobile(), // [tl! focus] + ] +}); +``` + +Once that's done, you'll need to adjust your Vite build command when creating builds for each platform — simply add the +`--mode=[ios|android]` option. Run these before compiling your app for each platform in turn: + +```shell +npm run build -- --mode=ios + +npm run build -- --mode=android +``` + +## Compile your app + +To compile and run your app, simply run: + +```shell +php artisan native:run +``` + +This single command takes care of everything and allows you to run new builds of your application without having to +learn any new editors or platform-specific tools. + + + +## Working with Xcode or Android Studio + +On occasion, it is useful to compile your app from inside the target platform's dedicated development tools, Android +Studio and Xcode. + +If you're familiar with these tools, you can easily open the projects using the following Artisan command: + +```shell +php artisan native:open +``` + +### Configuration + +You can configure the folders that the `watch` command pays attention to in your `config/nativephp.php` file: + +```php +'hot_reload' => [ + 'watch_paths' => [ + 'app', + 'routes', + 'config', + 'database', + // Make sure "public" is listed in your config [tl! highlight:1] + 'public', + ], +] +``` + + + + +## Hot Reloading + +We've tried to make compiling your apps as fast as possible, but when coming from the 'make a change; hit refresh'-world +of typical browser-based PHP development that we all love, compiling apps can feel like a slow and time-consuming +process. + +Hot reloading aims to make your app development experience feel just like home. + +You can start hot reloading by running the following command: + +```shell +php artisan native:watch +``` + + + +This will start a long-lived process that watches your application's source files for changes, pushing them into the +emulator after any updates and reloading the current screen. + +If you're using Vite, we'll also use your Node CLI tool of choice (`npm`, `bun`, `pnpm`, or `yarn`) to run Vite's HMR +server. + +### Enabling HMR + +To make HMR work, you'll need to add the `hot` file helper to your `laravel` plugin's config in your `vite.config.js`: + +```js +import { nativephpMobile, nativephpHotFile } from './vendor/nativephp/mobile/resources/js/vite-plugin.js'; // [tl! focus] + +export default defineConfig({ + plugins: [ + laravel({ + input: ['resources/css/app.css', 'resources/js/app.js'], + refresh: true, + hotFile: nativephpHotFile(), // [tl! focus] + }), + tailwindcss(), + nativephpMobile(), + ] +}); +``` + +**Note:** When testing on real devices, hot reloading is communicating with the Vite server running on your development machine. +For this to work, ensure the test device is connected to the same Wi-Fi network as your development machine. + + + +This is useful during development for quickly testing changes without re-compiling your entire app. When you make +changes to any files in your Laravel app, the web view will be reloaded and your changes should show almost immediately. + +Vite HMR is perfect for apps that use SPA frameworks like Vue or React to build the UI. It even works on real devices, +not just simulators! As long as the device is on the same network as the development machine. + +**Don't forget to add `public/ios-hot` and `public/android-hot` to your `.gitignore` file!** + + + +## Laravel Boost + +NativePHP for Mobile supports [Laravel Boost](https://github.com/laravel/boost) which aims to accelerate AI-assisted development by providing +the essential context and structure that AI needs to generate high-quality, Laravel-specific code. + +After installing `nativephp/mobile` and `laravel/boost` simply run `php artisan boost:install` and follow the prompts +to activate NativePHP for Laravel Boost! diff --git a/resources/views/docs/mobile/2/getting-started/environment-setup.md b/resources/views/docs/mobile/2/getting-started/environment-setup.md new file mode 100644 index 00000000..0e8f1c53 --- /dev/null +++ b/resources/views/docs/mobile/2/getting-started/environment-setup.md @@ -0,0 +1,145 @@ +--- +title: Environment Setup +order: 100 +--- + +## Requirements + +1. PHP 8.3+ +2. Laravel 11+ +3. [A NativePHP for Mobile license](https://nativephp.com/mobile) + +If you don't already have PHP installed on your machine, the most painless way to get PHP up and running on Mac and +Windows is with [Laravel Herd](https://herd.laravel.com). It's fast and free! + +## iOS Requirements + + + +1. macOS (required - iOS development is only possible on an Apple silicon Mac, M1+) +2. [Xcode 16.0 or later](https://apps.apple.com/app/xcode/id497799835) +3. Xcode Command Line Tools +4. Homebrew & CocoaPods +5. _Optional_ iOS device for testing + +### Setting up iOS Development Environment + +1. **Install Xcode** + - Download from the [Mac App Store](https://apps.apple.com/app/xcode/id497799835) + - Minimum version: Xcode 16.0 + +2. **Install Xcode Command Line Tools** + ```shell + xcode-select --install + ``` + Verify installation: + ```shell + xcode-select -p + ``` + +3. **Install Homebrew** (if not already installed) + ```shell + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + ``` + +4. **Install CocoaPods** + ```shell + brew install cocoapods + ``` + Verify installation: + ```shell + pod --version + ``` + +### Apple Developer Account +You **do not** need to enroll in the [Apple Developer Program](https://developer.apple.com/programs/enroll/) ($99/year) +to develop and test your apps on a Simulator. However, you will need to enroll when you want to: +- Test your apps on real devices +- Distribute your apps via the App Store +- Test features that rely on a paid Apple Developer accounts, such as Push Notifications + +## Android Requirements + +1. [Android Studio 2024.2.1 or later](https://developer.android.com/studio) +2. Android SDK with API 33 or higher +3. **Windows only**: You must have [7zip](https://www.7-zip.org/) installed. + + + +### Setting up Android Studio and SDK + +1. **Download and Install Android Studio** + - Download from the [Android Studio download page](https://developer.android.com/studio) + - Minimum version required: Android Studio 2024.2.1 + +2. **Install Android SDK** + - Open Android Studio + - Navigate to **Tools → SDK Manager** + - In the **SDK Platforms** tab, install at least one Android SDK platform for API 33 or higher + - Latest stable version: Android 15 (API 35) + - You only need to install one API version to get started + - In the **SDK Tools** tab, ensure **Android SDK Build-Tools** and **Android SDK Platform-Tools** are installed + +That's it! Android Studio handles all the necessary configuration automatically. + +### Preparing for NativePHP + +1. Check that you can run `java -version` and `adb devices` from the terminal. +2. The following environment variables set: + +#### On macOS +```shell +# This isn't required if JAVA_HOME is already set in your environment variables (check using `printenv | grep JAVA_HOME`) +export JAVA_HOME=$(/usr/libexec/java_home -v 17) + +export ANDROID_HOME=$HOME/Library/Android/sdk +export PATH=$PATH:$JAVA_HOME/bin:$ANDROID_HOME/emulator:$ANDROID_HOME/tools:$ANDROID_HOME/tools/bin:$ANDROID_HOME/platform-tools +``` + +#### On Windows +The example below assumes default installation paths for the Android SDK and JDK: + +```shell +set ANDROID_HOME=C:\Users\yourname\AppData\Local\Android\Sdk +set PATH=%PATH%;%JAVA_HOME%\bin;%ANDROID_HOME%\platform-tools + +# This isn't required if JAVA_HOME is already set in the Windows Env Variables +set JAVA_HOME=C:\Program Files\Microsoft\jdk-17.0.8.7-hotspot +``` + +### "No AVDs found" error +If you encounter this error, it means no Virtual Devices are configured in Android Studio. +To resolve it, open Android Studio, navigate to Virtual Devices, and create at least one device. + +## Testing on Real Devices + +You don't _need_ a physical iOS/Android device to compile and test your application, as NativePHP for Mobile supports +the iOS Simulator and Android emulators. However, we highly recommend that you test your application on a real device +before submitting to the Apple App Store and Google Play Store. + +### On iOS +If you want to run your app on a real iOS device, you need to make sure it is in +[Developer Mode](https://developer.apple.com/documentation/xcode/enabling-developer-mode-on-a-device) +and that it's been added to your Apple Developer account as +[a registered device](https://developer.apple.com/account/resources/devices/list). + +### On Android +On Android you need to [enable developer options](https://developer.android.com/studio/debug/dev-options#enable) +and have USB debugging (ADB) enabled. diff --git a/resources/views/docs/mobile/2/getting-started/installation.md b/resources/views/docs/mobile/2/getting-started/installation.md new file mode 100644 index 00000000..1e4d8168 --- /dev/null +++ b/resources/views/docs/mobile/2/getting-started/installation.md @@ -0,0 +1,167 @@ +--- +title: Installation +order: 100 +--- + +## Get a license + +Before you begin, you will need to [purchase a license](/mobile). + +To make NativePHP for Mobile a reality has taken a lot of work and will continue to require even more. For this reason, +it's not open source, and you are not free to distribute or modify its source code. + +Your license fee goes straight back into the NativePHP project and community, enabling us to: +- Develop premium features for everyone. +- Provide first-class support. +- Sponsor our dependencies. +- Donate to our contributors. +- Support community events. +- Ensure that the whole NativePHP project remains viable for a long time to come. + +Thank you for supporting the project in this way! 🙏 + +## Install the Composer package + + + +Once you have your license, you will need to add the following to your `composer.json`: + +```json +"repositories": [ + { + "type": "composer", + "url": "https://nativephp.composer.sh" + } +], +``` + +Then run: +```shell +composer require nativephp/mobile +``` + + + + + + +If this is the first time you're installing the package, you will be prompted to authenticate. Your username is the +email address you used when purchasing your license. Your password is your license key. + +This package contains all the libraries, classes, commands, and interfaces that your application will need to work with +iOS and Android. + +## Run the NativePHP installer + +**Before** running the `install` command, it is important to set the following variables in your `.env`: + +```dotenv +NATIVEPHP_APP_ID=com.yourcompany.yourapp +NATIVEPHP_APP_VERSION="DEBUG" +NATIVEPHP_APP_VERSION_CODE="1" +``` + +Find out more about these options in +[Configuration](/docs/getting-started/configuration#codenativephp-app-idcode). + + + + +```shell +php artisan native:install +``` + +The NativePHP installer takes care of setting up and configuring your Laravel application to work with iOS and Android. + +You may be prompted about whether you would like to install the ICU-enabled PHP binaries. You should install these if +your application relies on the `intl` PHP extension. + +If you don't need `intl` or are not sure, choose the default, non-ICU builds. + + + +### The `nativephp` Directory + +After running: `php artisan native:install` you’ll see a new `nativephp` directory at the root of your Laravel project +as well as a `config/nativephp.php` config file. + +The `nativephp` folder contains the native application project files needed to build your app for the desired platforms. + +You should not need to manually open or edit any native project files under normal circumstances. NativePHP handles +the heavy lifting for you. + +**You should treat this directory as ephemeral.** When upgrading the NativePHP package, it will be necessary to run +`php artisan native:install --force`, which completely rebuilds this directory, deleting all files within. + +For this reason, we also recommend you add the `nativephp` folder to your `.gitignore`. + +## Start your app + +**Heads up!** Before starting your app in a native context, try running it in the browser. You may bump into exceptions +which need addressing before you can run your app natively, and may be trickier to spot when doing so. + +Once you're ready: + +```shell +php artisan native:run +``` + +Just follow the prompts! This will start compiling your application and boot it on whichever device you select. + +### Running on a real device + +#### On iOS +If you want to run your app on a real iOS device, you need to make sure it is in +[Developer Mode](https://developer.apple.com/documentation/xcode/enabling-developer-mode-on-a-device) +and that it's been added to your Apple Developer account as +[a registered device](https://developer.apple.com/account/resources/devices/list). + +#### On Android +On Android you need to [enable developer options](https://developer.android.com/studio/debug/dev-options#enable) +and have USB debugging (ADB) enabled. + +And that's it! You should now see your Laravel application running as a native app! 🎉 diff --git a/resources/views/docs/mobile/2/getting-started/introduction.md b/resources/views/docs/mobile/2/getting-started/introduction.md new file mode 100644 index 00000000..79b76fb0 --- /dev/null +++ b/resources/views/docs/mobile/2/getting-started/introduction.md @@ -0,0 +1,62 @@ +--- +title: Introduction +order: 1 +--- + +## Enjoy building mobile apps! + +NativePHP for Mobile is the first library of its kind that lets you run full PHP applications natively on mobile +devices — no web server required. + +By embedding a pre-compiled PHP runtime alongside Laravel, and bridging directly into each platform’s native +APIs, NativePHP brings the power of modern PHP to truly native mobile apps. Build performant, offline-capable +experiences using the tools you already know. + +**It's never been this easy to build beautiful, offline-first apps for iOS and Android.** + +## What makes NativePHP for Mobile special? + +- 📱 **Native performance** + Your app runs natively through an embedded PHP runtime optimized for each platform. +- 🔥 **True native APIs** + Access camera, biometrics, push notifications, and more. Build beautiful UIs with native components. All from one + cohesive library that does it all. +- ⚡ **Laravel powered** + Leverage the entire Laravel ecosystem and your existing skillset. +- 🚫 **No web server required** + Your app runs entirely on-device and can operate completely offline-first. +- 🔄 **Cross platform** + Build apps for both iOS and Android from a single codebase. + +## Old tools, new tricks + +With NativePHP for Mobile, you don’t need to learn Swift, Kotlin, or anything new. +No new languages. No unfamiliar build tools. No fighting with Gradle or Xcode. + +Just PHP. + +Developers around the world are using the skills they already have to build and ship real mobile apps — faster than +ever. In just a few minutes, you can go from code to app store submission. + +## How does it work? + +1. A pre-compiled version of PHP is bundled with your code into a Swift/Kotlin shell application. +2. NativePHP's custom Swift/Kotlin bridges manage the PHP environment, running your PHP code directly. +3. A custom PHP extension is compiled into PHP, that exposes PHP interfaces to native functions. +4. Build with HTML, JavaScript, Tailwind, Blade, Livewire, React, Vue, Svelte — whatever you're most comfortable with! +5. And now in v2: use truly native UI components too with [EDGE](/docs/mobile/2/edge-components/)! + +You simply interact with an easy-to-use set of functions from PHP and everything just works! + +## Batteries included + +NativePHP for Mobile is way more than just a web view wrapper for your server-based application. Your application lives +_on device_ and is shipped with each installation. + +Thanks to our custom PHP extension, you can interact with many native APIs today, with more coming all the time. Check out the API documentation section to see everything that's available. + +You have the full power of PHP and Laravel at your fingertips... literally! And you're not sandboxed into the web view; +this goes way beyond what's possible with PWAs and WASM without any of the complexity... we've got full-cream PHP at +the ready! + +**What are you waiting for!? [Let's go!](quick-start)** diff --git a/resources/views/docs/mobile/2/getting-started/quick-start.md b/resources/views/docs/mobile/2/getting-started/quick-start.md new file mode 100644 index 00000000..4702beb1 --- /dev/null +++ b/resources/views/docs/mobile/2/getting-started/quick-start.md @@ -0,0 +1,89 @@ +--- +title: Quick Start +order: 2 +--- + +## Let's go! + + + +If you've already got your [environment set up](environment-setup) to build mobile apps using Xcode and/or Android +Studio, then you can get building your first mobile app with NativePHP in minutes: + +### 1. Update your `composer.json` +Add the NativePHP Composer repository: + +```json +"repositories": [ + { + "type": "composer", + "url": "https://nativephp.composer.sh" + } +] +``` + +#### Running Composer 2.9+? + +If you're running Composer 2.9 or above, you can just use a single command, instead of copy-pasting the above: + +```shell +composer repo add nativephp composer https://nativephp.composer.sh +``` + +### 2. Set your app's identifier +You must set a `NATIVEPHP_APP_ID` in your `.env` file: + +```dotenv +NATIVEPHP_APP_ID=com.cocacola.cokezero +``` + + + +### 3. Install & run + +```bash +# Install NativePHP for Mobile into a new Laravel app +composer require nativephp/mobile + +# Ready your app to go native +php artisan native:install + +# Run your app on a mobile device +php artisan native:run +``` + +#### The `native` command + +When you run `native:install`, NativePHP installs a `native` script helper that can be used as a convenient wrapper to +the `native` Artisan command namespace. Once this is installed you can do the following: + +```shell +# Instead of... +php artisan native:run + +# Do +php native run + +# Or +./native run +``` + +## Need help? + +- **Community** - Join our [Discord](/discord) for support and discussions. +- **Examples** - Check out the Kitchen Sink demo app + on [Android](https://play.google.com/store/apps/details?id=com.nativephp.kitchensinkapp) and + [iOS](https://testflight.apple.com/join/vm9Qtshy)! diff --git a/resources/views/docs/mobile/2/getting-started/roadmap.md b/resources/views/docs/mobile/2/getting-started/roadmap.md new file mode 100644 index 00000000..4a1523b1 --- /dev/null +++ b/resources/views/docs/mobile/2/getting-started/roadmap.md @@ -0,0 +1,40 @@ +--- +title: Roadmap +order: 400 +--- + +NativePHP for Mobile is stable and already deployed in production apps released on the app stores. But it's still early +days. We haven't yet built interfaces to all of the available mobile APIs. + +We're working on adding more and more features, including (in no particular order): + - Bluetooth + - SMS (Android only) + - File picker + - Document scanner + - Background tasks + - Geofencing + - Calendar access + - Local notifications, scheduled notifications + - Clipboard API + - Contacts access + - App badges + - OTA Updates + - App review prompt + - Proximity sensor + - Gyroscope + - Accelerometer + - Screen brightness + - More Haptic feedback + - Ads + - In-app purchases and wallet payments + + diff --git a/resources/views/docs/mobile/2/getting-started/support-policy.md b/resources/views/docs/mobile/2/getting-started/support-policy.md new file mode 100644 index 00000000..396f4fc6 --- /dev/null +++ b/resources/views/docs/mobile/2/getting-started/support-policy.md @@ -0,0 +1,22 @@ +--- +title: Support Policy +order: 600 +--- + +NativePHP for Mobile is still very new. We aim to make it workable with as many versions of iOS and Android as is +reasonable. Considering that we have a very small team and a lot of work, our current stance on version support is this: + +**We aim (but do not guarantee) to support all the current and upcoming major, currently vendor-supported versions of +the platforms, with a focus on the current major release as a priority.** + +In practical terms, as of September 2025, this means we intend for NativePHP to be compatible — in part or in whole — +with: + +- iOS 18+ +- Android 13+ + +We do not guarantee support of all features across these versions, and whilst NativePHP may work in part on even older +versions than the currently-supported ones, we do not provide support for these under this standard policy. + +If you require explicit backwards compatibility with older or unsupported versions, we will be happy to have you join +our [partner](/partners) program, where a custom support policy can be arranged. diff --git a/resources/views/docs/mobile/2/getting-started/versioning.md b/resources/views/docs/mobile/2/getting-started/versioning.md new file mode 100644 index 00000000..07640b03 --- /dev/null +++ b/resources/views/docs/mobile/2/getting-started/versioning.md @@ -0,0 +1,69 @@ +--- +title: Versioning Policy +order: 500 +--- + +NativePHP for Mobile follows [semantic versioning](https://semver.org) with a mobile-specific approach that distinguishes between +Laravel-only changes and native code changes. This ensures predictable updates and optimal compatibility. + +Our aim is to limit the amount of work you need to do to get the latest updates and ensure everything works. + +We will aim to post update instructions with each release. + +## Release types + +### Patch releases + +Patch releases of `nativephp/mobile` should have **no breaking changes** and **only change Laravel/PHP code**. +This will typically include bug fixes and dependency updates that don't affect native code. + +These releases should be completely compatible with the existing version of your native applications. + +This means that you can: + +- Safely update via `composer update`. +- Avoid a complete rebuild (no need to `native:install --force`). +- Allow for easier app updates avoiding the app stores. + +### Minor releases + +Minor releases may contain **native code changes**. Respecting semantic versioning, these still should not contain +breaking changes, but there may be new native APIs, Kotlin/Swift updates, platform-specific features, or native +dependency changes. + +Minor releases will: + +- Require a complete rebuild (`php artisan native:install --force`) to work with the latest APIs. +- Need app store submission for distribution. +- Include advance notice and migration guides where necessary. + +### Major releases + +Major releases are reserved for breaking changes. This will usually follow a period of deprecations so that you have +time to make the necessary changes to your application code. + +## Version constraints + +We recommend using the [tilde range operator](https://getcomposer.org/doc/articles/versions.md#tilde-version-range-) +with a full minimum patch release defined in your `composer.json`: + +```json +{ + "require": { + "nativephp/mobile": "~2.0.0" + } +} +``` + +This automatically receives patch updates while giving you control over minor releases. + +## Your application versioning + +Just because we're using semantic versioning for the `nativephp/mobile` package, doesn't mean your app must follow that +same scheme. + +You have complete freedom in versioning your own applications! You may use semantic versioning, codenames, +date-based versions, or any scheme that works for your project, team or business. + +Remember that your app versions are usually public-facing (e.g. in store listings and on-device settings and update +screens) and can be useful for customers to reference if they need to contact you for help and support. diff --git a/resources/views/docs/mobile/2/the-basics/_index.md b/resources/views/docs/mobile/2/the-basics/_index.md new file mode 100644 index 00000000..62b29f06 --- /dev/null +++ b/resources/views/docs/mobile/2/the-basics/_index.md @@ -0,0 +1,4 @@ +--- +title: The Basics +order: 20 +--- diff --git a/resources/views/docs/mobile/2/the-basics/app-icon.md b/resources/views/docs/mobile/2/the-basics/app-icon.md new file mode 100644 index 00000000..5b1bfdbb --- /dev/null +++ b/resources/views/docs/mobile/2/the-basics/app-icon.md @@ -0,0 +1,25 @@ +--- +title: App Icons +order: 300 +--- + +NativePHP makes it easy to apply a custom app icon to your iOS and Android apps. + +## Supply your icon + +Place a single high-resolution icon file at: `public/icon.png`. + +### Requirements +- Format: PNG +- Size: 1024 × 1024 pixels +- Background: Must not contain any transparencies. +- GD PHP extension must be enabled, ensure it has enough memory (~2GB should be enough) + +This image will be automatically resized for all Android densities and used as the base iOS app icon. +You must have the GD extension installed in your development machine's PHP environment for this to work. + + diff --git a/resources/views/docs/mobile/2/the-basics/assets.md b/resources/views/docs/mobile/2/the-basics/assets.md new file mode 100644 index 00000000..40eb46fe --- /dev/null +++ b/resources/views/docs/mobile/2/the-basics/assets.md @@ -0,0 +1,53 @@ +--- +title: Assets +order: 500 +--- + +## Compiling CSS and JavaScript + +If you are using React, Vue or another JavaScript library, or Tailwind CSS, tools that requires your frontend to be +built by build tooling like Vite, you will need to run your build process _before_ compiling the native application. + +For example, if you're using Vite with NPM to build a React application that is using Tailwind, to ensure that your +latest styles and JavaScript are included, always run `npm run build` before running `php artisan native:run`. + +## Other files + +NativePHP will include all files from the root of your Laravel application. So you can store any files that you wish to +make available to your application wherever makes the most sense for you. + + + +## Public files + +If your application receives files from the user — either generated by your application or imported from elsewhere, +such as their photo gallery — you may wish to render these to the web view so they can be played or displayed as part +of your app. + +For this to work, they must be in the `public` directory. But you may also wish to persist these files across app +updates. For this reason, they are stored outside of the `public` directory in a persistent storage location and this +folder is symlinked to `public/storage`. + +You can then access these files using the `mobile_public` disk: + +```php +Storage::disk('mobile_public')->url('user_content.jpg'); +``` + + diff --git a/resources/views/docs/mobile/2/the-basics/events.md b/resources/views/docs/mobile/2/the-basics/events.md new file mode 100644 index 00000000..6036f1de --- /dev/null +++ b/resources/views/docs/mobile/2/the-basics/events.md @@ -0,0 +1,215 @@ +--- +title: Events +order: 200 +--- + +## Overview + +Many native mobile operations take time to complete and await user interaction. PHP isn't really set up to handle this +sort of asynchronous behaviour; it is built to do its work, send a response and move on as quickly as possible. + +NativePHP for Mobile smooths over this disparity between the different paradigms using a simple event system that +handles completion of asynchronous methods using a webhook-/websocket-style approach to notify your Laravel app. + +## Understanding Async vs Sync + +Not all actions are async. Some methods run immediately, and in some cases return a result straight away. + +Here are a few of the **synchronous** APIs: + +```php +Haptics::vibrate(); +System::flashlight(); +Dialog::toast('Hello!'); +``` +Asynchronous actions trigger operations that may complete later. These return immediately, usually with a `bool` or +`void`, allowing PHP's execution to finish. In many of these cases, the user interacts directly with a native component. + +When the user has completed their task and the native UI is dismissed, the app will emit an event that represents the +outcome. + +The _type_ (the class name) of the event and its properties all help you to choose the appropriate action to take in +response to the outcome. + +```php +// These trigger operations and fire events when complete +Camera::getPhoto(); // → PhotoTaken event +Biometrics::prompt(); // → Completed event +PushNotifications::enroll(); // → TokenGenerated event +``` + +## Basic Event Structure + +All events are standard [Laravel Event classes](https://laravel.com/docs/12.x/events#defining-events). The public +properties of the events contain the pertinent data coming from the native app side. + +## Custom Events + +Almost every function that emits events can be customized to emit events that you define. This is a great way to ensure +only the relevant listeners are executed when these events are fired. + +Events are simple PHP classes that receive some parameters. You can extend existing events for convenience. + +Let's see a complete example... + +### Define your custom event class + +```php +namespace App\Events; + +use Native\Mobile\Events\Alert\ButtonPressed; + +class MyButtonPressedEvent extends ButtonPressed +{} +``` + +### Pass this class to an async function + +```php +use App\Events\MyButtonPressedEvent; + +Dialog::alert('Warning!', 'You are about to delete everything! Are you sure?', [ + 'Cancel', + 'Do it!' + ]) + ->event(MyButtonPressedEvent::class) +``` + +### Handle the event + +Here's an example handling a custom event class inside a Livewire component. + +```php +use App\Events\MyButtonPressed; +use Native\Mobile\Attributes\OnNative; + +#[OnNative(MyButtonPressed::class)] +public function buttonPressed() +{ + // Do stuff +} +``` + +## Event Handling + +All asynchronous methods follow the same pattern: + +1. **Call the method** to trigger the operation. +2. **Listen for the appropriate events** to handle the result. +3. **Update your UI** based on the outcome. + +All events get sent directly to JavaScript in the web view _and_ to your PHP application via a special route. This +allows you to listen for these events in the context that best suits your application. + +### On the frontend + +Events are 'broadcast' to the frontend of your application via the web view through a custom `Native` helper. You can +easily listen for these events through JavaScript in a few ways: + +- The globally available `Native.on()` helper +- Directly importing the `on` function +- The `#[OnNative()]` PHP attribute Livewire extension + + + +#### The `Native.on()` helper + +Register the event listener directly in JavaScript: + +```blade +@@use(Native\Mobile\Events\Alert\ButtonPressed) + + +``` + +This approach is useful if you're not using any particular frontend JavaScript framework. + +#### The `on` import + + + +If you're using a SPA framework like Vue or React, it's more convenient to import the `on` function directly to +register your event listeners. Here's an example using the amazing Vue: + +```js +import { on, Events } from '#nativephp'; +import { onMounted } from 'vue'; + +const handleButtonPressed = (payload: any) => {}; + +onMounted(() => { + on(Events.Alert.ButtonPressed, handleButtonPressed); +}); +``` + +Note how we're also using the `Events` object above to simplify our use of built-in event names. For custom event +classes, you will need to reference these by their full name: + +```js +on('App\\Events\\MyButtonPressedEvent', handleButtonPressed); +``` + +In SPA land, don't forget to de-register your event handlers using the `off` function too: + +```js +import { off, Events } from '#nativephp'; +import { onUnmounted } from 'vue'; + +onUnmounted(() => { + off(Events.Alert.ButtonPressed, handleButtonPressed); +}); +``` + +#### The `#[OnNative()]` attribute + +Livewire makes listening to 'broadcast' events simple. Just add the `#[OnNative()]` attribute attached to the Livewire +component method you want to use as its handler: + +```php +use Native\Mobile\Attributes\OnNative; +use Native\Mobile\Events\Camera\PhotoTaken; + +#[OnNative(PhotoTaken::class)] +public function handlePhoto(string $path) +{ + // Handle captured photo +} +``` + +### On the backend + +You can also listen for these events on the PHP side as they are simultaneously passed to your Laravel application. + +Simply [add a listener](https://laravel.com/docs/12.x/events#registering-events-and-listeners) as you normally would: + +```php +use App\Services\APIService; +use Native\Mobile\Events\Camera\PhotoTaken; + +class UpdateAvatar +{ + public function __construct(private APIService $api) {} + + public function handle(PhotoTaken $event): void + { + $imageData = base64_encode( + file_get_contents($event->path) + ); + + $this->api->updateAvatar($imageData); + } +} +``` diff --git a/resources/views/docs/mobile/2/the-basics/native-components.md b/resources/views/docs/mobile/2/the-basics/native-components.md new file mode 100644 index 00000000..2e7c1166 --- /dev/null +++ b/resources/views/docs/mobile/2/the-basics/native-components.md @@ -0,0 +1,34 @@ +--- +title: Native Components +order: 150 +--- + +![](/img/docs/edge.png) + +NativePHP for Mobile also supports rendering native UI components! + +Starting with v2, we've introduced a few key navigation components to give your apps an even more native feel. + +We call this **EDGE** - Element Definition and Generation Engine. + +## Living on the EDGE + +EDGE components are **truly native** elements that match each platform's design guidelines. + +Built on top of Laravel's Blade, EDGE gives you the power of native UI components with the simplicity you expect. + +@verbatim +```blade + + + +``` +@endverbatim + +We take a single definition and turn it into fully native UI that works beautifully across all the supported mobile OS +versions, in both light and dark mode. + +And they're fully compatible with hot reloading, which means you can swap them in and out at runtime without needing +to recompile your app! + +You can find out all about EDGE and the available components in the [EDGE Components](../edge-components/) section. diff --git a/resources/views/docs/mobile/2/the-basics/native-functions.md b/resources/views/docs/mobile/2/the-basics/native-functions.md new file mode 100644 index 00000000..75738c02 --- /dev/null +++ b/resources/views/docs/mobile/2/the-basics/native-functions.md @@ -0,0 +1,91 @@ +--- +title: Native Functions +order: 100 +--- + +Our custom PHP extension enables tight integration with each platform, providing a consistent and performant abstraction +that lets you focus on building your app. Build for both platforms while you develop on one. + +Native device functions are called directly from your PHP code, giving you access to platform-specific features while +maintaining the productivity and familiarity of Laravel development. + +These functions are called from your PHP code using an ever-growing list of classes. These classes are also wrapped in +Laravel Facades for ease of access and testing, such as: + +```php +Native\Mobile\Facades\Biometrics +Native\Mobile\Facades\Browser +Native\Mobile\Facades\Camera +``` + +Each of these is covered in our [APIs](../apis/) section. + + +## Run from anywhere + +All of our supported APIs are called through PHP. This means NativePHP for Mobile is not reliant upon a web view to +function, and eventually we will fully support apps that don't use a web view as their main tool for rendering the UI. +Though this will be optional, so you have total freedom of choice and could even build completely hybrid solutions. + +When using a web view and using JavaScript you may also interact with the native functions easily from JavaScript using +our convenient `Native` library. + +This is especially useful if you're building applications with a SPA framework, like Vue or React, as you can simply +import the functions you need and move a lot of work into the reactive part of your UI. + +### Install the plugin + +To use the `Native` JavaScript library, you must install the plugin in your `package.json` file. Add the following +section to the JSON: + +```js +{ + "dependencies": { + ... + }, + "imports": { // [tl! focus:start] + "#nativephp": "./vendor/nativephp/mobile/resources/dist/native.js" + } // [tl! focus:end] +} +``` + +Run `npm install`, then in your JavaScript, simply import the relevant functions from the plugin: + +```js +import { on, off, Microphone, Events } from '#nativephp'; +import { onMounted, onUnmounted } from 'vue'; + +const buttonClicked = () => { + Microphone.record(); +}; + +const handleRecordingFinished = () => { + // Update the UI +}; + +onMounted(() => { + on(Events.Microphone.MicrophoneRecorded, handleRecordingFinished); +}); + +onUnmounted(() => { + off(Events.Microphone.MicrophoneRecorded, handleRecordingFinished); +}); +``` + +The library is fully typed, so your IDE should be able to pick up the available properties and methods to provide you +with inline hints and code completion support. + +For the most part, the JavaScript APIs mirror the PHP APIs. Any key differences are noted in our API docs. + +This approach uses the PHP interface under the hood, meaning the two implementations stay in lock-step with each other. +So you'll never need to worry about whether an API is available only in one place or the other; they're all always +available wherever you need them. + + diff --git a/resources/views/docs/mobile/2/the-basics/overview.md b/resources/views/docs/mobile/2/the-basics/overview.md new file mode 100644 index 00000000..75b12003 --- /dev/null +++ b/resources/views/docs/mobile/2/the-basics/overview.md @@ -0,0 +1,56 @@ +--- +title: Overview +order: 50 +--- + +NativePHP for Mobile is made up of multiple parts: + +- A Laravel application (PHP) +- The `nativephp/mobile` Composer package +- A custom build of PHP with custom NativePHP extension +- Native applications (Swift & Kotlin) + +## Your Laravel app + +You can build your Laravel application just as you normally would, for the most part, sprinkling native functionality +in where desired by using NativePHP's built-in APIs. + +## `nativephp/mobile` + +The package is a pretty normal Composer package. It contains the PHP code needed to interface with the NativePHP +extension, the tools to install and run your applications, and all the code for each native application - iOS and +Android. + +## The PHP builds + +When you run the `native:install` Artisan command, the package will fetch the appropriate versions of the custom-built +PHP binaries. + +NativePHP for Mobile currently bundles **PHP 8.4**. You should ensure that your application is built to work with this +version of PHP. + +These custom PHP builds have been compiled specifically to target the mobile platforms and cannot be used in other +contexts. + +They are compiled as embeddable C libraries and embedded _into_ the native application. In this way, PHP doesn't run as +a separate process/service under a typical web server environment; essentially, the native application itself is +extended with the capability to execute your PHP code. + +Your Laravel application is then executed directly by the native app, using the embedded PHP engine to run the code. +This runs PHP as close to natively as it can get. It is very fast and efficient on modern hardware. + +## The native apps + +NativePHP ships one app for iOS and one for Android. When you run the `native:run` Artisan command, your Laravel app is +packaged up and copied into one of these apps. + +To build for both platforms, you must run the `native:run` command twice, targeting each platform. + +Each native app "shell" runs a number of steps to prepare the environment each time your application is booted, +including: + +- Checking to see if the bundled version of your Laravel app is newer than the installed version +- Installing the newer version if necessary +- Running migrations +- Clearing caches +- Creating storage symlinks diff --git a/resources/views/docs/mobile/2/the-basics/splash-screens.md b/resources/views/docs/mobile/2/the-basics/splash-screens.md new file mode 100644 index 00000000..1c803e4e --- /dev/null +++ b/resources/views/docs/mobile/2/the-basics/splash-screens.md @@ -0,0 +1,18 @@ +--- +title: Splash Screens +order: 400 +--- + +NativePHP makes it easy to add custom splash screens to your iOS and Android apps. + +## Supply your Splash Screens + +Place the relevant files in the locations specified: + +- `public/splash.png` - for the Light Mode splash screen +- `public/splash-dark.png` - for the Dark Mode splash screen + +### Requirements +- Format: PNG +- Minimum Size/Ratio: 1080 × 1920 pixels +- GD PHP extension must be enabled, ensure it has enough memory (~2GB should be enough) diff --git a/resources/views/docs/mobile/2/the-basics/web-view.md b/resources/views/docs/mobile/2/the-basics/web-view.md new file mode 100644 index 00000000..2f4183e3 --- /dev/null +++ b/resources/views/docs/mobile/2/the-basics/web-view.md @@ -0,0 +1,132 @@ +--- +title: Web View +order: 60 +--- + +Every mobile app built with NativePHP centers around a single native web view. The web view allows you to use whichever +web technologies you are most comfortable with to build your app's user interface (UI). + +You're not limited to any one tool or framework — you can use Livewire, Vue, React, Svelte, HTMX... even jQuery! +Whatever you're most comfortable with for building a web UI, you can use to build a mobile app with NativePHP. + +The web view is rendered to fill the entire view of your application and is intended to remain visible to your users at +all times — except when another full-screen action takes place, such as accessing the camera or an in-app browser. + +## The Viewport + +Just like a normal browser, the web view has the concept of a **viewport** which represents the viewable area of the +page. The viewport can be controlled with the `viewport` meta tag, just as you would in a traditional web application: + +```html + +``` + +### Disable Zoom +When building mobile apps, you may want to have a little more control over the experience. For example, you may +want to disable user-controlled zoom, allowing your app to behave similarly to a traditional native app. + +To achieve this, you can set `user-scalable=no`: + +```html + +``` + +## Edge-to-Edge + +To give you the most flexibility in how you design your app's UI, the web view occupies the entire screen, allowing you +to render anything anywhere on the display whilst your app is in the foreground using just HTML, CSS and JavaScript. + +But you should bear in mind that not all parts of the display are visible to the user. Many devices have camera +notches, rounded corners and curved displays. These areas may still be considered part of the `viewport`, but they may +be invisible and/or non-interactive. + +To account for this in your UI, you should set the `viewport-fit=cover` option in your `viewport` meta tag and use the +safe area insets. + +### Safe Areas + +Safe areas are the sections of the display which are not obscured by either a physical interruption (a rounded corner +or camera), or some persistent UI, such as the Home Indicator (a.k.a. the bottom bar) or notch. + +Safe areas are calculated for your app by the device at runtime and adjust according to its orientation, allowing your +UI to be responsive to the various device configurations with a simple and predictable set of CSS rules. + +The fundamental building blocks are a set of four values known as `insets`. These are injected into your pages as the +following CSS variables: + +- `--inset-top` +- `--inset-bottom` +- `--inset-left` +- `--inset-right` + +You can apply these insets in whichever way you need to build a usable interface. + +There is also a handy `nativephp-safe-area` CSS class that can be applied to most elements to ensure they sit within +the safe areas of the display. + +Say you want a `fixed`-position header bar like this: + +![](/img/docs/viewport-fit-cover.png) + +If you're using Tailwind, you might try something like this: + +```html +
+ ... +
+``` + +If you tried to do this without `viewport-fit=cover` and use of the safe areas, here's what you'd end up with in +portrait view: + +![](/img/docs/viewport-default.png) + +And it may be even worse in landscape view: + +![](/img/docs/viewport-default-landscape.png) + +But by adding a few simple adjustments to our page, we can make it beautiful again (Well, maybe we should lose the +red...): + +```html + +
+ ... +
+``` + +![](/img/docs/viewport-fit-cover-landscape.png) + +### Status Bar Style + +On Android, the icons in the Status Bar do not change color automatically based on the background color in your app. +By default, they change based on whether the device is in Light/Dark Mode. + +If you have a consistent background color in both light and dark mode, you may use the `nativephp.status_bar_style` +config key to set the appropriate status bar style for your app to give users the best experience. + +The possible options are: + +- `auto` - the default, which changes based on the device's Dark Mode setting +- `light` - ideal if your app's background is dark-colored +- `dark` - better if your app's background is light-colored + + + +With just a few small changes, we've been able to define a layout that will work well on a multitude of devices +without having to add complex calculations or lots of device-specific CSS rules to our code. diff --git a/resources/views/docs/mobile/3/_index.md b/resources/views/docs/mobile/3/_index.md new file mode 100644 index 00000000..c719fbe2 --- /dev/null +++ b/resources/views/docs/mobile/3/_index.md @@ -0,0 +1,4 @@ +--- +title: Mobile +order: 1 +--- diff --git a/resources/views/docs/mobile/3/concepts/_index.md b/resources/views/docs/mobile/3/concepts/_index.md new file mode 100644 index 00000000..6e3f1cfd --- /dev/null +++ b/resources/views/docs/mobile/3/concepts/_index.md @@ -0,0 +1,4 @@ +--- +title: Concepts +order: 40 +--- diff --git a/resources/views/docs/mobile/3/concepts/authentication.md b/resources/views/docs/mobile/3/concepts/authentication.md new file mode 100644 index 00000000..e0c54a7c --- /dev/null +++ b/resources/views/docs/mobile/3/concepts/authentication.md @@ -0,0 +1,107 @@ +--- +title: Authentication +order: 150 +--- + +## Authenticating Your Users + +Most apps will want to have some concept of who the user is so that they can use your app to connect securely with +external services, such as your API or cloud service. + +In a mobile app, you will always need to call an external service (external to the user's device) that acts as the +source of truth about your users credentials. It could be a service you manage or a third-party service like WorkOS, +Auth0 or Amazon Cognito. + +Authenticating the user serves two purposes: + +1. It allows you to prove who they are when they connect to your API. +2. It gives you an opportunity to grant or deny access to certain features of your app based on the authenticated state + of the user. + +Whilst some user data may be stored on the user's device for convenience, you should not rely on this data to +authenticate the user. This is because **the data is outside of your control**. So you will not be using the typical +Laravel authentication mechanisms to check for an authenticated user. + + + +## Tokens FTW! + +Most mobile apps opt for some form of "auth token" (e.g. a JWT or an expiring API key) that is generated by your auth +service and stored securely on the user's device. + +These tokens should only live for a short period, usually no more than a few days. It's useful to have a single-use +"refresh token" that lives for a longer time (e.g. 30 days) also shared with your user when they have successfully +authenticated. This can be exchanged for a new auth token when the user's current auth token has expired. + +You should store both auth and refresh tokens in secure storage. **Checking for an auth token's existence to validate +that the user is authenticated is _not sufficient_.** If the token has expired or been revoked, you should force the +user to re-authenticate. The only way to know for certain is to exercise the token. + + + +### Laravel Sanctum + +[Laravel Sanctum](https://laravel.com/docs/12.x/sanctum) is a very convenient and easy-to-use mechanism for generating +auth tokens for your users. They simply provide their login credentials and if authenticated, receive a token. Using a +simple login form, you can collect their username and password in your app and `POST` it securely to your auth service +via an API call. + +Note that, by default, Sanctum tokens don't expire. +[You should enable token expiration](https://laravel.com/docs/12.x/sanctum#token-expiration) for increased +security. You may only find out that a token has expired when your app attempts to use it unsuccessfully. + +### OAuth + +OAuth is a robust and battle-tested solution to the mobile app auth problem. If you're running +[Laravel Passport](https://laravel.com/docs/12.x/passport) or your authentication service support OAuth, you should use +it! + +You will likely want to use an OAuth client library in your app to make interacting with your OAuth service easier. + +When initiating the auth flow for the user, you should use the `Native\Mobile\Facades\Browser::auth()` API, as this is +purpose-built for securely passing authorization codes back from the OAuth service to your app. + +For this to work, you must set a `NATIVEPHP_DEEPLINK_SCHEME` that will be unique for your application on users' devices. + +```dotenv +NATIVEPHP_DEEPLINK_SCHEME=myapp +``` + +Then you must define your redirect URL. It should match your scheme and the route in your app that will handle the callback +data. + +```php +Browser::auth('https://workos.com/my-company/auth?redirect=myapp://auth/handle') +``` + +Most services will expect you to pre-define your redirect URLs as a security feature. You should be able to provide your +exact URL, as this will be the most secure method. + +How you handle the response in your app depends on how that particular API operates and the needs of your application. + + diff --git a/resources/views/docs/mobile/3/concepts/databases.md b/resources/views/docs/mobile/3/concepts/databases.md new file mode 100644 index 00000000..c25122ef --- /dev/null +++ b/resources/views/docs/mobile/3/concepts/databases.md @@ -0,0 +1,149 @@ +--- +title: Databases +order: 200 +--- + +## Working with Databases + +You'll almost certainly want your application to persist structured data. For this, NativePHP supports +[SQLite](https://sqlite.org/), which works on both iOS and Android devices. + +You can interact with SQLite from PHP in whichever way you're used to. + +## Configuration + +You do not need to do anything special to configure your application to use SQLite. NativePHP will automatically: +- Switch to using SQLite when building your application. +- Create the database for you in the app container. +- Run your migrations each time your app starts, as needed. + +## Migrations + +When writing migrations, you need to consider any special recommendations for working with SQLite. + +For example, prior to Laravel 11, SQLite foreign key constraints are turned off by default. If your application relies +upon foreign key constraints, [you need to enable SQLite support for them](https://laravel.com/docs/database#configuration) before running your migrations. + +**It's important to test your migrations on [prod builds](/docs/mobile/1/getting-started/development#releasing) +before releasing updates!** You don't want to accidentally delete your user's data when they update your app. + +## Seeding data with migrations + +Migrations are the perfect mechanism for seeding data in mobile applications. They provide the natural behavior you +want for data seeding: + +- **Run once**: Each migration runs exactly once per installation. +- **Tracked**: Laravel tracks which migrations have been executed. +- **Versioned**: New app versions can include new data seeding migrations. +- **Reversible**: You can create migrations to remove or update seed data. + +### Creating seed migrations + +Create dedicated migrations for seeding data: + +```shell +php artisan make:migration seed_app_settings +``` + +```php +use Illuminate\Database\Migrations\Migration; +use Illuminate\Support\Facades\DB; + +return new class extends Migration +{ + public function up() + { + DB::table('categories')->insert([ + ['name' => 'Work', 'color' => '#3B82F6'], + ['name' => 'Personal', 'color' => '#10B981'], + ]); + } +}; +``` + +### Test thoroughly + +This is the most important step when releasing new versions of your app, especially with new migrations. + +Your migrations should work both for users who are installing your app for the first time (or re-installing) _and_ +users who have updated your app to a new release. + +Make sure you test your migrations under the different scenarios that your users' databases are likely to be in. + +## Things to note + +- As your app is installed on a separate device, you do not have remote access to the database. +- If a user deletes your application from their device, any databases are also deleted. + +## Can I get MySQL/Postgres/other support? + +No. + +SQLite being the only supported database driver is a deliberate security decision to prevent developers from +accidentally embedding production database credentials directly in mobile applications. Why? + +- Mobile apps are distributed to user devices and can be reverse-engineered. +- Database credentials embedded in apps may be accessible to anyone with the app binary. +- Direct database connections bypass important security layers like rate limiting and access controls. +- Network connectivity issues make direct database connections unreliable from mobile devices and can be troublesome + for your database to handle. + +## API-first + +If a key part of your application relies on syncing data between a central database and your client apps, we strongly +recommend that you do so via a secure API backend that your mobile app can communicate with. + +This provides multiple security and architectural benefits: + +**Security Benefits:** +- Database credentials never leave your server +- Implement proper authentication and authorization +- Rate limiting and request validation +- Audit logs for all data access +- Ability to revoke access instantly + +**Technical Benefits:** +- Better error handling and offline support +- Easier to scale and maintain +- Version your API for backward compatibility +- Transform data specifically for mobile consumption + +### Securing your API + +For the same reasons that you shouldn't share database credentials in your `.env` file or elsewhere in your app code, +you shouldn't store API keys or tokens either. + +If anything, you should provide a client key that **only** allows client apps to request tokens. Once you have +authenticated your user, you can pass an access token back to your mobile app and use this for communicating with your +API. + +Store these tokens on your users' devices securely with the [`SecureStorage`](/docs/mobile/1/apis/secure-storage) API. + +It's a good practice to ensure these tokens have high entropy so that they are very hard to guess and a short lifespan. +Generating tokens is cheap; leaking personal customer data can get _very_ expensive! + +Use industry-standard tools like OAuth-2.0-based providers, Laravel Passport, or Laravel Sanctum. + + + +#### Considerations + +In your mobile apps: + +- Always store API tokens using `SecureStorage` +- Use HTTPS for all API communications +- Cache data locally using SQLite for offline functionality +- Check for connectivity before making API calls + +And on the API side: + +- Use token-based authentication +- Implement rate limiting to prevent abuse +- Validate and sanitize all input data +- Use HTTPS with proper SSL certificates +- Log all authentication attempts and API access diff --git a/resources/views/docs/mobile/3/concepts/deep-links.md b/resources/views/docs/mobile/3/concepts/deep-links.md new file mode 100644 index 00000000..3bb8a305 --- /dev/null +++ b/resources/views/docs/mobile/3/concepts/deep-links.md @@ -0,0 +1,92 @@ +--- +title: Deep Links +order: 300 +--- + +## Overview + +NativePHP for Mobile supports **deep linking** into your app via Custom URL Schemes and Associated Domains: + +- **Custom URL Scheme** + ``` + myapp://some/path + ``` +- **Associated Domains** (a.k.a. Universal Links on iOS, App Links on Android) + ``` + https://example.net/some/path + ``` + +In each case, your app can be opened directly at the route matching `/some/path`. + +Each method has its use cases, and NativePHP handles all the platform-specific configuration automatically when you +provide the proper environment variables. + +You can even use both approaches at the same time in a single app! + +## Custom URL Scheme + +Custom URL schemes are a great way to allow apps to pass data between themselves. If your app is installed when a user +uses a deep link that incorporates your custom scheme, your app will open immediately to the desired route. + +But note that custom URL schemes can only work when your app has been installed and cannot aid in app discovery. If a +user interacts with URL with a custom scheme for an app they don't have installed, there will be no prompt to install +an app that can load that URL. + +To enable your app's custom URL scheme, define it in your `.env`: + +```dotenv +NATIVEPHP_DEEPLINK_SCHEME=myapp +``` + +You should choose a scheme that is unique to your app to avoid confusion with other apps. Note that some schemes are +reserved by the system and cannot be used (e.g. `https`). + +## Associated domains + +Universal Links/App Links allow real HTTPS URLs to open your app instead of in a web browser, if the app is installed. +If the app is not installed, the URL will load as normal in the browser. + +This flow increases the opportunity for app discovery dramatically and provides a much better overall user experience. + +### How it works + +1. You must prove to the operating system on the user's device that your app is legitimately associated with the domain + you are trying to redirect by hosting special files on your server: + - `.well-known/apple-app-site-association` (for iOS) + - `.well-known/assetlinks.json` (for Android) +2. The mobile OS reads these files to verify the link association +3. Once verified, tapping a real URL will open your app instead of opening it in the user's browser + +**NativePHP handles all the technical setup automatically** - you just need to host the verification files and +configure your domain correctly. + +To enable an app-associated domain, define it in your `.env`: + +```dotenv +NATIVEPHP_DEEPLINK_HOST=example.net +``` + +## Testing & troubleshooting + +Associated Domains do not usually work in simulators. Testing on a real device that connects to a publicly-accessible +server for verification is often the best way to ensure these are operating correctly. + +If you are experiencing issues getting your associated domain to open your app, try: +- Completely deleting and reinstalling the app. Registration verifications (including failures) are often cached + against the app. +- Validating that your associated domain verification files are formatted correctly and contain the correct data. + +There is usually no such limitation for Custom URL Schemes. + +## Use cases + +Deep linking is great for bringing users from another context directly to a key place in your app. Universal/App Links +are usually the more appropriate choice for this because of their flexibility in falling back to simple loading a URL +in the browser. + +They're also more likely to behave the same across both platforms. + +Then you could use Universal/App Links in: +- NFC tags +- QR codes +- Email/SMS marketing diff --git a/resources/views/docs/mobile/3/concepts/push-notifications.md b/resources/views/docs/mobile/3/concepts/push-notifications.md new file mode 100644 index 00000000..024ad509 --- /dev/null +++ b/resources/views/docs/mobile/3/concepts/push-notifications.md @@ -0,0 +1,93 @@ +--- +title: Push Notifications +order: 400 +--- + +## Overview + +NativePHP for Mobile uses Firebase Cloud Messaging (FCM) to send push notifications to your users on both iOS and +Android devices. + +To send a push notification to a user, your app must request a token. That token must then be stored securely (ideally +on a server application via a secure API) and associated with that user/device. + +Requesting push notification will trigger an alert for the user to either approve or deny your request. If they approve, +your app will receive the token. + +When you want to send a notification to that user, you pass this token along with a request to the FCM service and +Firebase handles sending the message to the right device. + + + +## Firebase + +1. Create a [Firebase](https://firebase.google.com/) account +2. Create a project +3. Download the `google-services.json` file (for Android) and `GoogleService-Info.plist` file (for iOS) +4. These files contain the configuration for your app and is used by the Firebase SDK to retrieve tokens for each device + +Place these files in the root of your application and NativePHP will automatically handle setting them up appropriately +for each platform. + +You can ignore Firebase's further setup instructions as this is already taken care of by NativePHP. + +### Service account + +For sending push notifications from your server-side application, you'll also need a Firebase service account: + +1. Go to your Firebase Console → Project Settings → Service Accounts +2. Click "Generate New Private Key" to download the service account JSON file +3. Save this file as `fcm-service-account.json` somewhere safe in your server application + +## Getting push tokens + +It's common practice to request push notification permissions during app bootup as tokens can change when: +- The app is restored on a new device +- The app data is restored from backup +- The app is updated +- Other internal FCM operations + +To request a token, use the `PushNotifications::getToken()` method: + +```php +use Native\Mobile\Facades\PushNotifications; + +PushNotifications::getToken(); +``` + +If the user has approved your app to use push notifications and the request to FCM succeeded, a `TokenGenerated` event +will fire. + +Listen for this event to receive the token. Here's an example in a Livewire component: + +```php +use App\Services\APIService; +use Native\Mobile\Attributes\OnNative; +use Native\Mobile\Facades\PushNotifications; +use Native\Mobile\Events\PushNotification\TokenGenerated; + +class PushNotifications extends Component +{ + #[OnNative(TokenGenerated::class)] + public function storePushToken(APIService $api, string $token) + { + $api->storePushToken($token); + } +} +``` + +## Sending push notifications + +Once you have a token, you may use it from your server-side applications to trigger Push Notifications directly to your +user's device. + + diff --git a/resources/views/docs/mobile/3/concepts/queues.md b/resources/views/docs/mobile/3/concepts/queues.md new file mode 100644 index 00000000..67551806 --- /dev/null +++ b/resources/views/docs/mobile/3/concepts/queues.md @@ -0,0 +1,89 @@ +--- +title: Queues +order: 250 +--- + +## Background Queue Worker + +NativePHP runs a background queue worker alongside your app's main thread. Queued jobs execute off the main thread, +so they won't block your UI or slow down user interactions. + +Both iOS and Android are supported. + +## Setup + +Set your queue connection to `database` in your `.env` file: + +```dotenv +QUEUE_CONNECTION=database +``` + +That's it. NativePHP handles the rest — the worker starts automatically when your app boots. + +## Usage + +Use Laravel's standard queue dispatching. Everything works exactly as you'd expect: + +```php +use App\Jobs\SyncData; + +SyncData::dispatch($payload); +``` + +Or using the `dispatch()` helper: + +```php +dispatch(new App\Jobs\ProcessUpload($file)); +``` + +### Example Job + +Here's a simple job that makes an API call in the background: + +```php +namespace App\Jobs; + +use Illuminate\Bus\Queueable; +use Illuminate\Contracts\Queue\ShouldQueue; +use Illuminate\Foundation\Bus\Dispatchable; +use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Http; +use NativePHP\Plugins\Dialog\Dialog; + +class SyncData implements ShouldQueue +{ + use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; + + public function __construct(public array $payload) {} + + public function handle() + { + Http::post('https://api.example.com/sync', $this->payload); + + Dialog::toast('Sync complete!'); + } +} +``` + +## How It Works + +When your app boots, NativePHP automatically starts a dedicated PHP runtime on a separate thread. This worker +polls `queue:work --once` in a loop, picking up and executing queued jobs as they come in. + +Because it runs on its own thread with its own PHP runtime, your queued jobs are fully isolated from the main +request cycle — long-running tasks won't affect app responsiveness. + + + +## Things to Note + +- The queue worker requires [ZTS (Thread-Safe) PHP](/docs/mobile/3/getting-started/changelog), which is included by default in v3.1+. +- Only the `database` queue connection is supported. This uses the same SQLite database as your app. +- Jobs are persisted to the database, so they survive app restarts. +- If a job fails, Laravel's standard retry and failure handling applies. \ No newline at end of file diff --git a/resources/views/docs/mobile/3/concepts/security.md b/resources/views/docs/mobile/3/concepts/security.md new file mode 100644 index 00000000..0d3cf311 --- /dev/null +++ b/resources/views/docs/mobile/3/concepts/security.md @@ -0,0 +1,98 @@ +--- +title: Security +order: 100 +--- + +## Security + +Although NativePHP tries to make it as easy as possible to make your application secure, it is your responsibility to +protect your users. + +### Secrets and .env + +As your application is being installed on systems outside your/your organisation's control, it is important to think +of the environment that it's in as _potentially_ hostile, which is to say that any secrets, passwords or keys +could fall into the hands of someone who might try to abuse them. + +This means you should, where possible, use unique keys for each installation, preferring to generate these at first-run +or on every run rather than sharing the same key for every user across many installations. + +Especially if your application is communicating with any private APIs over the network, we highly recommend that your +application and any API use a robust and secure authentication protocol, such as OAuth2, that enables you to create and +distribute unique and expiring tokens (an expiration date less than 48 hours in the future is recommended) with a high +level of entropy, as this makes them hard to guess and hard to abuse. + +**Always use HTTPS.** + +If your application allows users to connect _their own_ API keys for a service, you should treat these keys with great +care. If you choose to store them anywhere (either in a file or +[Database](databases)), make sure you store them +[encrypted](../the-basics/system#encryption-decryption) and decrypt them only when needed. + +## Secure Storage + +NativePHP provides access to your users' device's native Keystore/Keychain through the +[`SecureStorage`](/docs/apis/secure-storage) facade, which +allow you to store small amounts of data in a secure way. + +The device's secure storage encrypts and decrypts data on the fly and that means you can safely rely on it to store +critical things like API tokens, keeping your users and your systems safe. + +This data is only accessible by your app and is persisted beyond the lifetime of your app, so it will still be available +the next time your app is opened. + + + + +### When to use the Laravel `Crypt` facade + +When a user first opens your app, NativePHP generates a **unique `APP_KEY` just for their device** and stores it in the +device's secure storage. This means each instance of your application has its own encryption key that is securely +stored on the device. + +NativePHP securely reads the `APP_KEY` from secure storage and makes it available to Laravel. So you can safely use the +`Crypt` facade to encrypt and decrypt data! + + + +This is great for encrypting larger amounts of data that wouldn't easily fit in secure storage. You can encrypt values +and store them in the file system or in the SQLite database, knowing that they are safe at rest: + +```php +use Illuminate\Support\Facades\Crypt; + +$encryptedContents = Crypt::encryptString( + $request->file('super_private_file') +); + +Storage::put('my_secure_file', $encryptedContents); +``` + +And then decrypt it later: + +```php +$decryptedContents = Crypt::decryptString( + Storage::get('my_secure_file') +); +``` + + + diff --git a/resources/views/docs/mobile/3/contributing/_index.md b/resources/views/docs/mobile/3/contributing/_index.md new file mode 100644 index 00000000..294b5e73 --- /dev/null +++ b/resources/views/docs/mobile/3/contributing/_index.md @@ -0,0 +1,4 @@ +--- +title: Contributing +order: 70 +--- diff --git a/resources/views/docs/mobile/3/contributing/contributing.md b/resources/views/docs/mobile/3/contributing/contributing.md new file mode 100644 index 00000000..8f82fd70 --- /dev/null +++ b/resources/views/docs/mobile/3/contributing/contributing.md @@ -0,0 +1,83 @@ +--- +title: Contributing +order: 1 +--- + +## Contributing + +We welcome contributions to NativePHP for Mobile! Whether it's bug fixes, new features, documentation improvements, or bug reports, every contribution helps make the project better. + +### How to Contribute + +1. **Fork the repository** on GitHub. +2. **Clone your fork** locally: + ```bash + git clone git@github.com:your-username/mobile-air.git + cd mobile-air + ``` +3. **Create a new branch** for your feature or fix: + ```bash + git checkout -b feature/my-new-feature + ``` +4. **Make your changes** and ensure all tests pass. +5. **Commit your changes** with a clear, descriptive commit message. +6. **Push your branch** to your fork: + ```bash + git push origin feature/my-new-feature + ``` +7. **Open a Pull Request** against the `main` branch of the NativePHP Mobile repository. + +### Pull Request Guidelines + +- Keep your changes focused. If you have multiple unrelated changes, please submit them as separate pull requests. +- Write clear, descriptive commit messages. +- Include tests for any new functionality. +- Ensure all existing tests pass before submitting. +- Update documentation if your changes affect the public API. + +### Reporting Bugs + +If you discover a bug, please [open an issue](https://github.com/NativePHP/mobile-air/issues) on GitHub. Include as much detail as possible: + +- A clear, descriptive title. +- Steps to reproduce the issue. +- Expected behavior vs. actual behavior. +- Your environment details (OS, PHP version, Laravel version, etc.). + +### Security Vulnerabilities + +If you discover a security vulnerability, please **do not** open a public issue. Instead, please send an email to [support@nativephp.com](mailto:support@nativephp.com). All security vulnerabilities will be promptly addressed. + +## Code of Conduct + +The NativePHP community is dedicated to providing a welcoming and inclusive experience for everyone. We expect all participants to adhere to the following standards: + +### Our Pledge + +We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +### Our Standards + +Examples of behavior that contributes to a positive environment: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior: + +- The use of sexualized language or imagery, and sexual attention or advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information without explicit permission +- Other conduct which could reasonably be considered inappropriate in a professional setting + +### Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the project team at [support@nativephp.com](mailto:support@nativephp.com). All complaints will be reviewed and investigated promptly and fairly. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. diff --git a/resources/views/docs/mobile/3/edge-components/_index.md b/resources/views/docs/mobile/3/edge-components/_index.md new file mode 100644 index 00000000..e5f3a985 --- /dev/null +++ b/resources/views/docs/mobile/3/edge-components/_index.md @@ -0,0 +1,4 @@ +--- +title: EDGE Components +order: 30 +--- diff --git a/resources/views/docs/mobile/3/edge-components/bottom-nav.md b/resources/views/docs/mobile/3/edge-components/bottom-nav.md new file mode 100644 index 00000000..ff047141 --- /dev/null +++ b/resources/views/docs/mobile/3/edge-components/bottom-nav.md @@ -0,0 +1,67 @@ +--- +title: Bottom Navigation +order: 100 +--- + +## Overview + +
+ +![](/img/docs/edge-bottom-nav-ios.png) + +![](/img/docs/edge-bottom-nav-android.png) + +
+ +A bottom navigation bar with up to 5 items. Used for your app's primary navigation. + +@verbatim +```blade + + + + +``` +@endverbatim + +## Props + +- `label-visibility` - `labeled`, `selected`, or `unlabeled` (optional, default: `labeled`) +- `dark` - Force dark mode styling (optional) + +## Children + +A `` can contain up to 5 `` elements. + +- `id` - Unique identifier (required) +- `icon` - A named [icon](icons) (required) +- `label` - Accessibility label (required) +- `url` - A URL to navigate to in the web view (required) +- `active` - Highlight this item as active (optional, default: `false`) +- `badge` - Badge text/number (optional) +- `news` - Show "new" indicator dot (optional, default: `false`) + + + +### `badge` example +
+ +![](/img/docs/edge-bottom-nav-item-badge.png) + +
diff --git a/resources/views/docs/mobile/3/edge-components/icons.md b/resources/views/docs/mobile/3/edge-components/icons.md new file mode 100644 index 00000000..bfbe1cb3 --- /dev/null +++ b/resources/views/docs/mobile/3/edge-components/icons.md @@ -0,0 +1,288 @@ +--- +title: Icons +order: 9999 +--- + +## Overview + +NativePHP EDGE components use a smart icon mapping system that automatically converts icon names to platform-specific +icons. On iOS, icons render as [SF Symbols](https://developer.apple.com/sf-symbols/), while Android uses +[Material Icons](https://fonts.google.com/icons?icon.set=Material+Icons). + +You don't need to worry about the differences! Just use a single, consistent icon name in your components, and the EDGE +handles the platform translation automatically. + +## How It Works + +The icon system uses a four-tier resolution strategy: + +1. **Direct Platform Icons** - On iOS, if the name contains a `.` it's used as a direct SF Symbol path (e.g., `car.side.fill`). On Android, any Material Icon ligature name works directly (e.g., `shopping_cart`). +2. **Manual Mapping** - Explicit mappings for common icons and aliases (e.g., `home`, `settings`, `user`) +3. **Smart Fallback** - Attempts to auto-convert unmapped icon names to platform equivalents +4. **Default Fallback** - Uses a circle icon if no match is found + +This approach means you can use intuitive icon names for common cases, leverage direct platform icons for advanced use +cases, and get consistent results across iOS and Android. + +## Platform Differences + +### iOS (SF Symbols) + +On iOS, icons render as SF Symbols. Manual mappings convert common icon names to their SF Symbol equivalents. +For example: + +- `home` → `house.fill` +- `settings` → `gearshape.fill` +- `check` → `checkmark.circle.fill` + +If an icon name isn't manually mapped, the system attempts to find a matching SF Symbol by trying variations like +`.fill`, `.circle.fill`, and `.square.fill`. + +### Android (Material Icons) + +On Android, icons render using a lightweight font-based approach that supports the entire Material Icons library. You +can use any Material Icon by its ligature name directly (e.g., `shopping_cart`, `qr_code_2`). + +Manual mappings provide convenient aliases for common icon names. For example: + +- `home` → `home` +- `settings` → `settings` +- `check` → `check` +- `cart` → `shopping_cart` + +## Direct Platform Icons + +For advanced use cases, you can use platform-specific icon names directly. + +### iOS SF Symbols + +On iOS, include a `.` in the icon name to use an SF Symbol path directly: + +@verbatim +```blade + + + +``` +@endverbatim + +### Android Material Icons + +On Android, use any Material Icon ligature name (with underscores): + +@verbatim +```blade + + + +``` +@endverbatim + +## Platform-Specific Icons + +When you need different icons on each platform, use the `System` facade: + +@verbatim +```blade + +``` +@endverbatim + +This is useful when the mapped icon doesn't match your needs or you want to use platform-specific variants. + +## Basic Usage + +Use the `icon` attribute in any EDGE component that supports icons, simply passing the name of the icon you wish to use: + +@verbatim +```blade + +``` +@endverbatim + +## Icon Reference + +All icons listed here are manually mapped and guaranteed to work consistently across iOS and Android. + +### Navigation + +| Icon | Description | +|------|-------------| +| `dashboard` | Grid-style dashboard view | +| `home` | House/home screen | +| `menu` | Three-line hamburger menu | +| `settings` | Gear/settings | +| `account`, `profile`, `user` | User account or profile | +| `person` | Single person | +| `people`, `connections`, `contacts` | Multiple people | +| `group`, `groups` | Group of people | + +### Business & Commerce + +| Icon | Description | +|------|-------------| +| `orders`, `receipt` | Receipt or order | +| `cart`, `shopping` | Shopping cart | +| `shop`, `store` | Store or storefront | +| `products`, `inventory` | Products or inventory | + +### Charts & Data + +| Icon | Description | +|------|-------------| +| `chart`, `barchart` | Bar chart | +| `analytics` | Analytics/analysis | +| `summary`, `report`, `assessment` | Summary or report | + +### Time & Scheduling + +| Icon | Description | +|------|-------------| +| `clock`, `schedule`, `time` | Clock or time | +| `calendar` | Calendar | +| `history` | History or recent | + +### Actions + +| Icon | Description | +|------|-------------| +| `add`, `plus` | Add or create new | +| `edit` | Edit or modify | +| `delete` | Delete or remove | +| `save` | Save | +| `search` | Search | +| `filter` | Filter | +| `refresh` | Refresh or reload | +| `share` | Share | +| `download` | Download | +| `upload` | Upload | + +### Communication + +| Icon | Description | +|------|-------------| +| `notifications` | Notifications or alerts | +| `message` | Message or SMS | +| `email`, `mail` | Email | +| `chat` | Chat or conversation | +| `phone` | Phone or call | + +### Navigation Arrows + +| Icon | Description | +|------|-------------| +| `back` | Back or previous | +| `forward` | Forward or next | +| `up` | Up arrow | +| `down` | Down arrow | + +### Status + +| Icon | Description | +|------|-------------| +| `check`, `done` | Check or complete | +| `close` | Close or dismiss | +| `warning` | Warning | +| `error` | Error | +| `info` | Information | + +### Authentication + +| Icon | Description | +|------|-------------| +| `login` | Login | +| `logout`, `exit` | Logout or exit | +| `lock` | Locked | +| `unlock` | Unlocked | + +### Content + +| Icon | Description | +|------|-------------| +| `favorite`, `heart` | Favorite or like | +| `star` | Star or rating | +| `bookmark` | Bookmark | +| `image`, `photo` | Image or photo | +| `image-plus` | Add photo | +| `video` | Video | +| `folder` | Folder | +| `folder-lock` | Locked folder | +| `file`, `description` | Document or file | +| `book-open` | Book | +| `newspaper`, `news`, `article` | News or article | + +### Device & Hardware + +| Icon | Description | +|------|-------------| +| `camera` | Camera | +| `qr`, `qrcode`, `qr-code` | QR code scanner | +| `device-phone-mobile`, `smartphone` | Mobile phone | +| `vibrate` | Vibration | +| `bell` | Bell or notification | +| `finger-print`, `fingerprint` | Fingerprint or biometric | +| `light-bulb`, `lightbulb`, `flashlight` | Light bulb or flashlight | +| `map`, `location` | Map or location | +| `globe-alt`, `globe`, `web` | Globe or web | +| `bolt`, `flash` | Lightning bolt or flash | + +### Audio & Volume + +| Icon | Description | +|------|-------------| +| `speaker`, `speaker-wave` | Speaker with sound | +| `volume-up` | Volume up | +| `volume-down` | Volume down | +| `volume-mute`, `mute` | Muted | +| `volume-off` | Volume off | +| `music`, `audio`, `music-note` | Music or audio | +| `microphone`, `mic` | Microphone | + +### Miscellaneous + +| Icon | Description | +|------|-------------| +| `help` | Help or question | +| `about`, `information-circle` | Information or about | +| `more` | More options | +| `list` | List view | +| `visibility` | Visible | +| `visibility_off` | Hidden | + +## Best Practices + +Icons have meaning and most users will associate the visual cues of icons and the underlying behavior or section of an +application across apps. So try to maintain consistent use of icons to help guide users through your app. + +- **Stay consistent** - Use the same icon name throughout your app for the same action +- **Test on both platforms** - If you use auto-converted icons, verify they appear correctly on iOS and Android + +## Finding Icons + +### Android Material Icons + +Browse the complete Material Icons library at [Google Fonts Icons](https://fonts.google.com/icons). Use the icon name +exactly as shown (with underscores, e.g., `shopping_cart`, `qr_code_2`). + +### iOS SF Symbols + +Browse SF Symbols using this [community Figma file](https://www.figma.com/community/file/1549047589273604548). While not +comprehensive, it's a great starting point for discovering available symbols. + +For the complete library, download the [SF Symbols app](https://developer.apple.com/sf-symbols/) for macOS. + + diff --git a/resources/views/docs/mobile/3/edge-components/introduction.md b/resources/views/docs/mobile/3/edge-components/introduction.md new file mode 100644 index 00000000..a48d98ca --- /dev/null +++ b/resources/views/docs/mobile/3/edge-components/introduction.md @@ -0,0 +1,108 @@ +--- +title: Introduction +order: 1 +--- + +## What is EDGE? + +EDGE (Element Definition and Generation Engine) is NativePHP for Mobile's component system that transforms Blade +template syntax into platform-native UI elements that look beautiful whichever device your users are using. + +![](/img/docs/edge.png) + +Instead of rendering in the web view, EDGE components are compiled into truly native elements and live apart from the +web view's lifecycle. This means they are persistent and offer truly native performance. + +There's no custom rendering engine and complex ahead-of-time compilation process, just a lightweight transformation +step that happens at runtime. You end up with pure, fast and flexible native components — all configured by PHP! + +## Available Components + +Our first set of components are focused on navigation, framing your application with beautiful, platform-dependent UI +components. These familiar navigational elements will help your users feel immediately at home in your app and elevate +your app to feeling built for their chosen platform, just like a true native app. + +And all that without compromising your ability to build using tools and techniques you're already the most comfortable +with. + +For now, we have 3 main native components that you can configure: + +- **[Bottom Navigation](bottom-nav)** - The always-accessible bottom navigation bar +- **[Top Bar](top-bar)** - A title bar with action buttons +- **[Side Navigation](side-nav)** - A slide-out navigation drawer + +## How It Works + +@verbatim +```blade + + + +``` +@endverbatim + +You simply define your components in Blade and EDGE processes these during each request, passing instructions to the +native side. The native UI rendering pipeline takes over to generate your defined components and builds the interface +just the way your users would expect, enabling your app to use the latest and greatest parts of each platform, +such as Liquid Glass on iOS. + +Under the hood, the Blade components are compiled down to a simple JSON configuration which we pass to the native side. +The native code already contains the generic components compiled-in. These are then rendered as needed based on the +JSON configuration. + + + +## Why Blade? + +Blade is an expressive and straightforward templating language that is very familiar to most Laravel users, and also +super accessible to anyone who's used to writing HTML. All of our components are Blade components, which allows us to +use Blade's battle-tested processing engine to rapidly compile the necessary transformation just in time. + +## Where to define your native components + +They can be defined in any Blade file, but for them to be processed, that Blade file will need to be rendered. We +recommend putting your components in a Blade component that is likely to be rendered on every request, such as your +main layout, e.g. `layouts/app.blade.php` or one of its child views/components. + +## Props Validation + +EDGE components enforce required props validation to prevent misconfiguration. If you're missing required props, you'll +see a clear error message that tells you exactly what's missing and how to fix it. + +For example, if you forget the `label` prop on a bottom navigation item: + +``` +EDGE Component is missing required properties: 'label'. +Add these attributes to your component: label="..." +``` + +The error message will list all missing required props and show you exactly which attributes you need to add. This +validation happens at render time, making it easy to catch configuration issues during development. + +Each component's documentation page indicates which props are required vs optional. + +## Using Inertia? + +Each link in an EDGE component will do a full post back to PHP, which may not be what you want if you are using Inertia. To transform these requests into Inertia ``, add `router` to your `window` object: + +```typescript +import { router } from '@inertiajs/vue3'; + +declare global { + interface Window { + router: typeof router; + } +} + +window.router = router; +``` diff --git a/resources/views/docs/mobile/3/edge-components/side-nav.md b/resources/views/docs/mobile/3/edge-components/side-nav.md new file mode 100644 index 00000000..fd3dde74 --- /dev/null +++ b/resources/views/docs/mobile/3/edge-components/side-nav.md @@ -0,0 +1,109 @@ +--- +title: Side Navigation +order: 400 +--- + +## Overview + +
+ +![](/img/docs/edge-side-nav-ios.png) + +![](/img/docs/edge-side-nav-android.png) + +
+ +A slide-out navigation drawer with support for groups, headers, and dividers. + +@verbatim +```blade + + + + + + + + + + + + + + +``` +@endverbatim + +## Props + +- `gestures-enabled` - Swipe to open (default: `false`) [Android] +- `dark` - Force dark mode (optional) + + + +## Children + +### `` + +- `title` - Title text (optional) +- `subtitle` - Subtitle text (optional) +- `icon` - A named [icon](icons) (optional) +- `background-color` - Background color. Hex code (optional) +- `show-close-button` - Show a close × (optional, default: `true`) [Android] +- `pinned` - Keep header visible when scrolling (optional, default: `false`) + +### `` + +- `id` - Unique identifier (required) +- `label` - Display text (required) +- `icon` - A named [icon](icons) (required) +- `url` - A URL to navigate to in the web view (required) +- `active` - Highlight this item as active (optional, default: `false`) +- `badge` - Badge text (optional) +- `badge-color` - Hex code or named color (optional) + + + +### `` + +- `heading` - The group's heading (required) +- `expanded` - Initially expanded (optional, default: `false`) +- `icon` - Material icon (optional) + +### `` + +Add visual separators between navigation items. This item has no properties. diff --git a/resources/views/docs/mobile/3/edge-components/top-bar.md b/resources/views/docs/mobile/3/edge-components/top-bar.md new file mode 100644 index 00000000..5f551015 --- /dev/null +++ b/resources/views/docs/mobile/3/edge-components/top-bar.md @@ -0,0 +1,68 @@ +--- +title: Top Bar +order: 50 +--- + +## Overview +
+ +![](/img/docs/edge-top-bar-ios.png) + +![](/img/docs/edge-top-bar-android.png) + +
+ +A top bar with title and action buttons. This renders at the top of the screen. + +@verbatim +```blade + + + + +``` +@endverbatim + +## Props + +- `title` - The title text (required) +- `show-navigation-icon` - Show back/menu button (optional, default: `true`) +- `label` - If more than 5 actions, iOS will display an overflow menu and the labels assigned to each item (optional) +- `background-color` - Background color. Hex code (optional) +- `text-color` - Text color. Hex code (optional) +- `elevation` - Shadow depth 0-24 (optional) [Android] + +## Children + +A `` can contain up to 10 `` elements. These are displayed on the trailing edge of the bar. + +### Props +- `id` - Unique identifier (required) +- `icon` - A named [icon](icons) (required) +- `label` - Accessibility label (optional) +- `url` - A URL to navigate to in the web view (optional) + +On Android, the first 3 actions are shown as icon buttons; additional actions collapse into an overflow menu (⋮). On iOS, if more than 5 actions are provided, they collapse into an overflow menu. + +### `` Props + +- `id` - Unique identifier (required) +- `icon` - A named [icon](icons) (required) +- `label` - Text label for the action. Used for accessibility and displayed in overflow menus (optional but recommended) +- `url` - A URL to navigate to when tapped + + diff --git a/resources/views/docs/mobile/3/getting-started/_index.md b/resources/views/docs/mobile/3/getting-started/_index.md new file mode 100644 index 00000000..5c684744 --- /dev/null +++ b/resources/views/docs/mobile/3/getting-started/_index.md @@ -0,0 +1,4 @@ +--- +title: Getting Started +order: 1 +--- \ No newline at end of file diff --git a/resources/views/docs/mobile/3/getting-started/changelog.md b/resources/views/docs/mobile/3/getting-started/changelog.md new file mode 100644 index 00000000..ba5a4d78 --- /dev/null +++ b/resources/views/docs/mobile/3/getting-started/changelog.md @@ -0,0 +1,42 @@ +--- +title: Changelog +order: 2 +--- + +For changes prior to v3, see the [v2 documentation](/docs/mobile/2/getting-started/changelog). + +## v3.1 — Persistent Runtime & Performance + +### New Features + +- **Persistent PHP Runtime** — Laravel boots once and the kernel is reused across requests, yielding ~5-30ms response times vs ~200-300ms previously. +- **ZTS (Thread-Safe) PHP** support enabling background queue workers +- **PHP Queue Worker** — a dedicated background thread runs queued Laravel jobs off the main thread on both iOS and Android. Just set `QUEUE_CONNECTION=database` and dispatch jobs as normal. See [Queues](../concepts/queues) for details. +- **Binary caching** — PHP binaries are cached in `nativephp/binaries` to avoid re-downloading on every build +- **Versions manifest** — binary URLs fetched from `versions.json` instead of being hardcoded +- **Android 8+ support** — minimum SDK lowered from Android 13 (API 33) to Android 8 (API 26), dramatically expanding device reach +- **PHP 8.3–8.5 support** — NativePHP now detects your app's PHP version from `composer.json` and matches it automatically, with PHP 8.3 as the lowest supported version +- **ICU/Intl support on iOS** — iOS now ships with full ICU support, enabling Filament and other packages that depend on the `intl` extension to work on both platforms +- **Configurable Android SDK versions** — `compile_sdk`, `min_sdk`, and `target_sdk` in your config +- **Plugin multi-register** — `native:plugin:register` discovers and registers multiple plugins in one pass +- **Unregistered plugin warnings** during `native:run` +- **`ios/i` and `android/a` flags** for the `native:jump` command + +### Improvements + +- Static linking on Android for better performance and reliability +- Plugin compilation during `native:package` builds +- URL encoding preserved on Android redirects +- Removed unused `react/http` and `react/socket` dependencies + +### Developer Experience + +- Laravel Boost skill support (shoutout Pushpak!) LINK TO PRS + +## v3.0 — Plugin Architecture + +- **Plugin-based architecture** — the framework is built around a modular plugin system +- **All core APIs shipped as plugins** — Camera, Biometrics, Dialog, and more are all individual plugins +- **`NativeServiceProvider`** for registering third-party plugins +- **Plugin management commands** — install, register, and manage plugins from the CLI +- **Free and open source** diff --git a/resources/views/docs/mobile/3/getting-started/commands.md b/resources/views/docs/mobile/3/getting-started/commands.md new file mode 100644 index 00000000..e500c222 --- /dev/null +++ b/resources/views/docs/mobile/3/getting-started/commands.md @@ -0,0 +1,301 @@ +--- +title: Command Reference +order: 350 +--- + +A complete reference of all `native:*` Artisan commands available in NativePHP Mobile. + +## Development Commands + +### native:install + +Install NativePHP into your Laravel application. + +```shell +php artisan native:install {platform?} +``` + +| Option | Description | +|--------|-------------| +| `platform` | Target platform: `android`, `ios`, or `both` | +| `--force` | Overwrite existing files | +| `--fresh` | Alias for `--force` | +| `--with-icu` | Include ICU support for Android (adds ~30MB) | +| `--without-icu` | Exclude ICU support for Android | +| `--skip-php` | Do not download PHP binaries | + +### native:run + +Build and run your app on a device or simulator. + +```shell +php artisan native:run {os?} {udid?} +``` + +| Option | Description | +|--------|---------------------------------------------------| +| `os` | Target platform: `ios/i` or `android/a` | +| `udid` | Specific device/simulator UDID | +| `--build=debug` | Build type: `debug`, `release`, or `bundle` | +| `--watch` | Enable hot reloading during development | +| `--start-url=` | Initial URL/path to load (e.g., `/dashboard`) | +| `--no-tty` | Disable TTY mode for non-interactive environments | + + + +### native:watch + +Watch for file changes and sync to a running mobile app. + +```shell +php artisan native:watch {platform?} {target?} +``` + +| Option | Description | +|--------|-----------------------------------------| +| `platform` | Target platform: `ios/i` or `android/a` | +| `target` | The device/simulator UDID to watch | + +### native:jump + +Start the NativePHP development server for testing mobile apps without building. + +```shell +php artisan native:jump +``` + +| Option | Description | +|-----------------------|-------------| +| `--platform=` | Target platform: `android` or `ios` | +| `ios/i` | Shorthand for `--platform=ios` | +| `android/a` | Shorthand for `--platform=android` | +| `--host=0.0.0.0` | Host address to serve on | +| `--http-port=` | HTTP port to serve on | +| `--laravel-port=8000` | Laravel dev server port to proxy to | +| `--no-mdns` | Disable mDNS service advertisement | +| `--skip-build` | Skip building if `app.zip` exists | + +### native:open + +Open the native project in Xcode or Android Studio. + +```shell +php artisan native:open {os?} +``` + +| Option | Description | +|--------|-----------------------------------------| +| `os` | Target platform: `ios/i` or `android/a` | + +### native:tail + +Tail Laravel logs from a running Android app. (Android only) + +```shell +php artisan native:tail +``` + +### native:version + +Display the current NativePHP Mobile version. + +```shell +php artisan native:version +``` + +## Building & Release Commands + + + +### native:package + +Package your app for distribution with signing. + +```shell +php artisan native:package {platform} +``` + +| Option | Description | +|--------|---------------------------------------------------| +| `platform` | Target platform: `android/a` or `ios/i` | +| `--build-type=release` | Build type: `release` or `bundle` | +| `--output=` | Output directory for signed artifacts | +| `--jump-by=` | Skip ahead in version numbering | +| `--no-tty` | Disable TTY mode for non-interactive environments | + +**Android Options:** + +| Option | Description | +|--------|-------------| +| `--keystore=` | Path to Android keystore file | +| `--keystore-password=` | Keystore password | +| `--key-alias=` | Key alias for signing | +| `--key-password=` | Key password | +| `--fcm-key=` | FCM Server Key for push notifications | +| `--google-service-key=` | Google Service Account Key file path | +| `--upload-to-play-store` | Upload to Play Store after packaging | +| `--play-store-track=internal` | Play Store track: `internal`, `alpha`, `beta`, `production` | +| `--test-push=` | Test Play Store upload with existing AAB file (skip build) | +| `--skip-prepare` | Skip prepareAndroidBuild() to preserve existing project files | + +**iOS Options:** + +| Option | Description | +|--------|-------------| +| `--export-method=app-store` | Export method: `app-store`, `ad-hoc`, `enterprise`, `development` | +| `--upload-to-app-store` | Upload to App Store Connect after packaging | +| `--test-upload` | Test upload existing IPA (skip build) | +| `--validate-only` | Only validate the archive without exporting | +| `--validate-profile` | Validate provisioning profile entitlements | +| `--rebuild` | Force rebuild by removing existing archive | +| `--clean-caches` | Clear Xcode and SPM caches before building | +| `--api-key=` | Path to App Store Connect API key file (.p8) | +| `--api-key-id=` | App Store Connect API key ID | +| `--api-issuer-id=` | App Store Connect API issuer ID | +| `--certificate-path=` | Path to distribution certificate (.p12/.cer) | +| `--certificate-password=` | Certificate password | +| `--provisioning-profile-path=` | Path to provisioning profile (.mobileprovision) | +| `--team-id=` | Apple Developer Team ID | + +### native:release + +Bump the version number in your `.env` file. + +```shell +php artisan native:release {type} +``` + +| Option | Description | +|--------|-------------| +| `type` | Release type: `patch`, `minor`, or `major` | + +### native:credentials + +Generate signing credentials for iOS and Android. + +```shell +php artisan native:credentials {platform?} +``` + +| Option | Description | +|--------|--------------------------------------------------| +| `platform` | Target platform: `android/a`, `ios/i`, or `both` | +| `--reset` | Generate new keystore and PEM certificate | + +### native:check-build-number + +Validate and suggest build numbers for your app. + +```shell +php artisan native:check-build-number +``` + +## Plugin Commands + +### native:plugin:create + +Scaffold a new NativePHP plugin interactively. + +```shell +php artisan native:plugin:create +``` + +### native:plugin:list + +List all installed NativePHP plugins. + +```shell +php artisan native:plugin:list +``` + +| Option | Description | +|--------|-------------| +| `--json` | Output as JSON | +| `--all` | Show all installed plugins, including unregistered | + +### native:plugin:register + +Register a plugin in your NativeServiceProvider. When called without arguments, discovers all unregistered plugins and lets you register them. + +```shell +php artisan native:plugin:register {plugin?} +``` + +| Option | Description | +|--------|-------------| +| `plugin` | Package name (e.g., `vendor/plugin-name`). Optional — omit to discover unregistered plugins | +| `--remove` | Remove the plugin instead of adding it | +| `--force` | Skip conflict warnings | + +### native:plugin:uninstall + +Completely uninstall a plugin. + +```shell +php artisan native:plugin:uninstall {plugin} +``` + +| Option | Description | +|--------|-------------| +| `plugin` | Package name (e.g., `vendor/plugin-name`) | +| `--force` | Skip confirmation prompts | +| `--keep-files` | Do not delete the plugin source directory | + +### native:plugin:validate + +Validate a plugin's structure and manifest. + +```shell +php artisan native:plugin:validate {path?} +``` + +| Option | Description | +|--------|-------------| +| `path` | Path to a specific plugin directory | + +### native:plugin:make-hook + +Create lifecycle hook commands for a plugin. + +```shell +php artisan native:plugin:make-hook +``` + +### native:plugin:boost + +Create Boost AI guidelines for a plugin. + +```shell +php artisan native:plugin:boost {plugin?} +``` + +| Option | Description | +|--------|-------------| +| `plugin` | Plugin name or path | +| `--force` | Overwrite existing guidelines | + +### native:plugin:install-agent + +Install AI agents for plugin development. + +```shell +php artisan native:plugin:install-agent +``` + +| Option | Description | +|--------|-------------| +| `--force` | Overwrite existing agent files | +| `--all` | Install all agents without prompting | diff --git a/resources/views/docs/mobile/3/getting-started/configuration.md b/resources/views/docs/mobile/3/getting-started/configuration.md new file mode 100644 index 00000000..6481e0b5 --- /dev/null +++ b/resources/views/docs/mobile/3/getting-started/configuration.md @@ -0,0 +1,317 @@ +--- +title: Configuration +order: 200 +--- + +## Overview + +NativePHP for Mobile is designed so that most configuration happens **inside your Laravel application**, without +requiring you to open Xcode or Android Studio and manually update config files. + +This page explains the key configuration points you can control through Laravel. + +## The `nativephp.php` Config File + +The `config/nativephp.php` config file contains a number of useful options. + +NativePHP uses sensible defaults and makes several assumptions based on default installations for tools required to +build and run apps from your computer. + +You can override these defaults by editing the `nativephp.php` config file in your Laravel project, and in many cases +simply by changing environment variables. + +## `NATIVEPHP_APP_ID` + +You must set your app ID to something unique. A common practice is to use a reverse-DNS-style name, e.g. +`com.yourcompany.yourapp`. + +Your app ID (also known as a *Bundle Identifier*) is a critical piece of identification across both Android and iOS +platforms. Different app IDs are treated as separate apps. + +And it is often referenced across multiple services, such as Apple Developer Center and the Google Play Console. + +So it's not something you want to be changing very often. + +## `NATIVEPHP_APP_VERSION` + +The `NATIVEPHP_APP_VERSION` environment variable controls your app's versioning behavior. + +When your app is compiling, NativePHP first copies the relevant Laravel files into a temporary directory, zips them up, +and embeds the archive into the native application. + +When your app boots, it checks the embedded version against the previously installed version to see if it needs to +extract the bundled Laravel application. + +If the versions match, the app uses the existing files without re-extracting the archive. + +To force your application to always install the latest version of your code - especially useful during development - +set this to `DEBUG`: + +```dotenv +NATIVEPHP_APP_VERSION=DEBUG +``` + +Note that this will make your application's boot up slightly slower as it must unpack the zip every time it loads. + +But this ensures that you can iterate quickly during development, while providing a faster, more stable experience for +end users once an app is published. + +## Persistent Runtime + +v3.1 introduces a persistent PHP runtime that boots Laravel once and reuses the kernel across all subsequent requests. +This dramatically improves performance — from ~200-300ms per request down to ~5-30ms. + +The `runtime` section controls this behavior: + +```php +'runtime' => [ + 'mode' => env('NATIVEPHP_RUNTIME_MODE', 'persistent'), // [tl! highlight] + 'reset_instances' => true, + 'gc_between_dispatches' => false, +], +``` + +- `mode` — Set to `persistent` (default) to reuse the Laravel kernel, or `classic` to boot/shutdown per request. + If persistent boot fails, it falls back to classic mode automatically. +- `reset_instances` — Whether to clear resolved facade instances between dispatches. (default: `true`) +- `gc_between_dispatches` — Whether to run garbage collection between dispatches. Enable this if you notice memory + growth over time. (default: `false`) + + + +## Deep Links + +Configure deep linking to allow URLs to open your app directly: + +```php +'deeplink_scheme' => env('NATIVEPHP_DEEPLINK_SCHEME'), +'deeplink_host' => env('NATIVEPHP_DEEPLINK_HOST'), +``` + +The `deeplink_scheme` enables custom URL schemes (e.g. `myapp://some/path`), while `deeplink_host` enables +verified HTTPS links and NFC tags (e.g. `https://your-host.com/path`). + +See the [Deep Links](../concepts/deep-links) documentation for full details. + +## Start URL + +Set the initial path that loads when your app starts: + +```php +'start_url' => env('NATIVEPHP_START_URL', '/'), +``` + +This is useful if you want to land users on a specific page like `/dashboard` or `/onboarding` instead of the root. + +## Cleanup `env` keys + +The `cleanup_env_keys` array in the config file allows you to specify keys that should be removed from the `.env` file +before bundling. This is useful for removing sensitive information like API keys or other secrets. + +## Cleanup `exclude_files` + +The `cleanup_exclude_files` array in the config file allows you to specify files and folders that should be removed +before bundling. This is useful for removing files like logs or other temporary files that aren't required for your app +to function and bloat your downloads. + +## Orientation + +NativePHP (as of v1.10.3) allows users to custom specific orientations per device through the config file. The config +allows for granularity for iPad, iPhone and Android devices. Options for each device can be seen below. + +NOTE: if you want to disable iPad support completely simply apply `false` for each option. + +```php +'orientation' => [ + 'iphone' => [ + 'portrait' => true, + 'upside_down' => false, + 'landscape_left' => false, + 'landscape_right' => false, + ], + 'android' => [ + 'portrait' => true, + 'upside_down' => false, + 'landscape_left' => false, + 'landscape_right' => false, + ], +], +``` + +Regardless of these orientation settings, if your app supports iPad, it will be available in all orientations. + +## iPad Support + +With NativePHP, your app can work on iPad too! If you wish to support iPad, simply set the `ipad` config option to `true`: + +```php +'ipad' => true, +``` + +Using standard CSS responsive design principles, you can make your app work beautifully across all screen sizes. + + + +## Android SDK Versions + +The `android` section of your config file lets you control which Android SDK versions are used when building your app. +These are nested under the `android` key in `config/nativephp.php`: + +```php +'android' => [ + 'compile_sdk' => env('NATIVEPHP_ANDROID_COMPILE_SDK', 36), + 'min_sdk' => env('NATIVEPHP_ANDROID_MIN_SDK', 33), + 'target_sdk' => env('NATIVEPHP_ANDROID_TARGET_SDK', 36), +], +``` + +- `compile_sdk` - The SDK version used to compile your app. This determines which Android APIs are available to you + at build time. (default: `36`) +- `min_sdk` - The minimum Android version your app supports. Devices running an older version won't be able to install + your app. (default: `26`, Android 8) +- `target_sdk` - The SDK version your app is designed and tested against. Google Play uses this to apply appropriate + compatibility behaviors. (default: `36`) + +You can also set these via environment variables: + +```dotenv +NATIVEPHP_ANDROID_COMPILE_SDK=36 +NATIVEPHP_ANDROID_MIN_SDK=26 +NATIVEPHP_ANDROID_TARGET_SDK=36 +``` + + + +## Android Build Configuration + +Fine-tune your Android build process with these options under the `android.build` key: + +```php +'android' => [ + 'build' => [ + 'minify_enabled' => env('NATIVEPHP_ANDROID_MINIFY_ENABLED', false), + 'shrink_resources' => env('NATIVEPHP_ANDROID_SHRINK_RESOURCES', false), + 'obfuscate' => env('NATIVEPHP_ANDROID_OBFUSCATE', false), + 'debug_symbols' => env('NATIVEPHP_ANDROID_DEBUG_SYMBOLS', 'FULL'), + 'parallel_builds' => env('NATIVEPHP_ANDROID_PARALLEL_BUILDS', true), + 'incremental_builds' => env('NATIVEPHP_ANDROID_INCREMENTAL_BUILDS', true), + ], +], +``` + +- `minify_enabled` — Enable R8/ProGuard code shrinking. (default: `false`) +- `shrink_resources` — Remove unused resources from the APK. (default: `false`) +- `obfuscate` — Obfuscate class and method names. (default: `false`) +- `debug_symbols` — Include debug symbols. Set to `FULL` for symbolicated crash reports. (default: `FULL`) +- `parallel_builds` / `incremental_builds` — Gradle build performance options. (default: `true`) + + + +## Android Status Bar Style + +Control the color of the status bar and navigation bar icons: + +```php +'android' => [ + 'status_bar_style' => env('NATIVEPHP_ANDROID_STATUS_BAR_STYLE', 'auto'), +], +``` + +Options: `auto` (detect from system theme), `light` (white icons), or `dark` (dark icons). + +## Development Server + +Configure the development server used by `native:jump` and `native:watch`: + +```php +'server' => [ + 'http_port' => env('NATIVEPHP_HTTP_PORT', 3000), + 'ws_port' => env('NATIVEPHP_WS_PORT', 8081), + 'service_name' => env('NATIVEPHP_SERVICE_NAME', 'NativePHP Server'), + 'open_browser' => env('NATIVEPHP_OPEN_BROWSER', true), +], +``` + +- `http_port` — The port for serving your app during development. (default: `3000`) +- `ws_port` — The WebSocket port for hot reload communication. (default: `8081`) +- `service_name` — The mDNS service name advertised on your network. (default: `NativePHP Server`) +- `open_browser` — Automatically open a browser with a QR code when the server starts. (default: `true`) + +## Hot Reload + +Customize which files trigger hot reloads during development: + +```php +'hot_reload' => [ + 'watch_paths' => [ + 'app', + 'resources', + 'routes', + 'config', + 'public', + ], + 'exclude_patterns' => [ + '\.git', + 'storage', + 'node_modules', + ], +], +``` + +## Development Team (iOS) + +Set your Apple Developer Team ID for code signing: + +```php +'development_team' => env('NATIVEPHP_DEVELOPMENT_TEAM'), +``` + +This is typically detected from your installed certificates, but you can override it here. Find your Team ID +in your Apple Developer account under Membership details. + +## App Store Connect + +Configure automated iOS uploads with the App Store Connect API: + +```php +'app_store_connect' => [ + 'api_key' => env('APP_STORE_API_KEY'), + 'api_key_id' => env('APP_STORE_API_KEY_ID'), + 'api_issuer_id' => env('APP_STORE_API_ISSUER_ID'), + 'app_name' => env('APP_STORE_APP_NAME'), +], +``` + +These credentials are used by `native:package --upload-to-app-store` to upload your IPA directly to +App Store Connect without opening Xcode. + + diff --git a/resources/views/docs/mobile/3/getting-started/deployment.md b/resources/views/docs/mobile/3/getting-started/deployment.md new file mode 100644 index 00000000..f12891a4 --- /dev/null +++ b/resources/views/docs/mobile/3/getting-started/deployment.md @@ -0,0 +1,511 @@ +--- +title: Deployment +order: 300 +--- + +Deploying mobile apps is a complicated process — and it's different for each platform! + + + +Generally speaking you need to: + +1. **Releasing**: Create a _release build_ for each platform. +2. **Testing**: Test this build on real devices. +3. **Packaging**: Sign and distribute this build to the stores. +4. **Submitting for Review**: Go through each store's submission process to have your app reviewed. +5. **Publishing**: Releasing the new version to the stores and your users. + +It's initially more time-consuming when creating a brand new app in the stores, as you need to get the listing set up +in each store and create your signing credentials. + +If you've never done it before, allow a couple of hours so you can focus on getting things right and understand +everything you need. + +Don't rush through the app store processes! There are compliance items that if handled incorrectly will either prevent +you from publishing your app, being unable to release it in the territories you want to make it available to, or simply +having it get rejected immediately when you submit it for review if you don't get those right. + +It's typically easier once you've released the first version of your app and after you've done 2 or 3 apps, you'll fly +through the process! + + + +## Releasing + +To prepare your app for release, you should set the version number to a new version number that you have not used +before and increment the build number: + +```dotenv +NATIVEPHP_APP_VERSION=1.2.3 +NATIVEPHP_APP_VERSION_CODE=48 +``` + +### Versioning + +You have complete freedom in how you version your applications. You may use semantic versioning, codenames, +date-based versions, or any scheme that works for your project, team or business. + +Remember that your app versions are usually public-facing (e.g. in store listings and on-device settings and update +screens) and can be useful for customers to reference if they need to contact you for help and support. + +The build number is managed via the `NATIVEPHP_APP_VERSION` key in your `.env`. + +### Build numbers + +Both the Google Play Store and Apple App Store require your app's build number to increase for each release you submit. + +The build number is managed via the `NATIVEPHP_APP_VERSION_CODE` key in your `.env`. + +### Run a `release` build + +Then run a release build: + +```shell +php artisan native:run --build=release +``` + +This builds your application with various optimizations that reduce its overall size and improve its performance, such +as removing debugging code and unnecessary features (i.e. Composer dev dependencies). + +**You should test this build on a real device.** Once you're happy that everything is working as intended you can then +submit it to the stores for approval and distribution. + +- [Google Play Store submission guidelines](https://support.google.com/googleplay/android-developer/answer/9859152?hl=en-GB#zippy=%2Cmaximum-size-limit) +- [Apple App Store submission guidelines](https://developer.apple.com/ios/submit/) + +## Packaging Your App + +The `native:package` command creates signed, production-ready apps for distribution to the App Store and Play Store. +This command handles all the complexity of code signing, building release artifacts, and preparing files for submission. + +## Before You Begin + +Before you can package your app for distribution, ensure: + +1. Your app is fully developed and tested on both platforms +2. You have a valid bundle ID and app ID configured in your `nativephp.php` config +3. For Android: You have a signing keystore with a valid key alias +4. For iOS: You have the necessary signing certificates and provisioning profiles from Apple Developer +5. All configuration is complete (see the [configuration guide](/docs/mobile/3/getting-started/configuration)) + +## Android Packaging + +### Creating Android Signing Credentials + +NativePHP provides a convenient command to generate all the signing credentials you need for Android: + +```bash +php artisan native:credentials android +``` + +This command will: +- Generate a new JKS keystore file +- Create all necessary signing keys +- Automatically add the credentials to your `.env` file +- Add the keystore to your `.gitignore` to keep it secure + +The credentials will be saved in the `nativephp/credentials/android/` directory and automatically configured for use with the package command. + +### Required Signing Credentials + +To build a signed Android app, you need four pieces of information: + +| Credential | Option | Environment Variable | Description | +|-----------|--------|----------------------|-------------| +| Keystore file | `--keystore` | `ANDROID_KEYSTORE_FILE` | Path to your `.keystore` file | +| Keystore password | `--keystore-password` | `ANDROID_KEYSTORE_PASSWORD` | Password for the keystore | +| Key alias | `--key-alias` | `ANDROID_KEY_ALIAS` | Name of the key within the keystore | +| Key password | `--key-password` | `ANDROID_KEY_PASSWORD` | Password for the specific key | + +### Building a Release APK + +An APK (Android Package) is a single binary file suitable for direct distribution or testing on specific devices: + +```bash +php artisan native:package android \ + --keystore=/path/to/my-app.keystore \ + --keystore-password=mykeystorepassword \ + --key-alias=my-app-key \ + --key-password=mykeypassword +``` + +The build process prepares your Android project, compiles the code, and signs the APK with your certificate. When complete, the output directory opens automatically, showing your signed `app-release.apk` file. + +### Building an Android App Bundle (AAB) + +An AAB (Android App Bundle) is required for distribution through the Play Store. It's an optimized format that the Play Store uses to generate device-specific APKs automatically: + +```bash +php artisan native:package android \ + --build-type=bundle \ + --keystore=/path/to/my-app.keystore \ + --keystore-password=mykeystorepassword \ + --key-alias=my-app-key \ + --key-password=mykeypassword +``` + +This creates a signed `app-release.aab` file ready for Play Store submission. + +### Using Environment Variables + +Instead of passing credentials as command options, you can store them in your `.env` file: + +```env +ANDROID_KEYSTORE_FILE=/path/to/my-app.keystore +ANDROID_KEYSTORE_PASSWORD=mykeystorepassword +ANDROID_KEY_ALIAS=my-app-key +ANDROID_KEY_PASSWORD=mykeypassword +``` + +Then simply run: + +```bash +php artisan native:package android --build-type=bundle +``` + +### Uploading to Play Store + +If you have a Google Service Account with Play Console access, you can upload directly from the command: + +```bash +php artisan native:package android \ + --build-type=bundle \ + --keystore=/path/to/my-app.keystore \ + --keystore-password=mykeystorepassword \ + --key-alias=my-app-key \ + --key-password=mykeypassword \ + --upload-to-play-store \ + --play-store-track=internal \ + --google-service-key=/path/to/service-account-key.json +``` + +The `--play-store-track` option controls where the build is released: + +- `internal` - Internal testing (default, fastest review) +- `alpha` - Closed alpha testing +- `beta` - Closed beta testing +- `production` - Production release + +### Testing Play Store Uploads + +If you already have an AAB file and want to test uploading without rebuilding, use `--test-push`: + +```bash +php artisan native:package android \ + --test-push=/path/to/app-release.aab \ + --upload-to-play-store \ + --play-store-track=internal \ + --google-service-key=/path/to/service-account-key.json +``` + +This skips the entire build process and only handles the upload. + +### Skipping Build Preparation + +For incremental builds where you haven't changed native code, you can skip the preparation phase: + +```bash +php artisan native:package android \ + --build-type=bundle \ + --keystore=/path/to/my-app.keystore \ + --keystore-password=mykeystorepassword \ + --key-alias=my-app-key \ + --key-password=mykeypassword \ + --skip-prepare +``` + +## iOS Packaging + +### Required Signing Credentials + +iOS apps require several credentials from Apple Developer: + +| Credential | Option | Environment Variable | Description | +|-----------|--------|----------------------|-------------| +| API Key file | `--api-key-path` | `APP_STORE_API_KEY_PATH` | Path to `.p8` file from App Store Connect | +| API Key ID | `--api-key-id` | `APP_STORE_API_KEY_ID` | Key ID from App Store Connect | +| API Issuer ID | `--api-issuer-id` | `APP_STORE_API_ISSUER_ID` | Issuer ID from App Store Connect | +| Certificate | `--certificate-path` | `IOS_DISTRIBUTION_CERTIFICATE_PATH` | Distribution certificate (`.p12` or `.cer`) | +| Certificate password | `--certificate-password` | `IOS_DISTRIBUTION_CERTIFICATE_PASSWORD` | Password for the certificate | +| Provisioning profile | `--provisioning-profile-path` | `IOS_DISTRIBUTION_PROVISIONING_PROFILE_PATH` | Profile file (`.mobileprovision`) | +| Team ID | `--team-id` | `IOS_TEAM_ID` | Apple Developer Team ID | + +### Setting Up App Store Connect API + +To upload to the App Store directly from the command line, you'll need an API key: + +1. Log in to [App Store Connect](https://appstoreconnect.apple.com) +2. Navigate to Users & Access → Keys +3. Click the "+" button to create a new key with "Developer" access +4. Download the `.p8` file immediately (you can't download it again later) +5. Note the Key ID and Issuer ID displayed on the page + +### Export Methods + +The `--export-method` option controls how your app is packaged: + +- `app-store` - For App Store distribution (default) +- `ad-hoc` - For distribution to specific registered devices +- `enterprise` - For enterprise distribution (requires enterprise program) +- `development` - For development and testing + +### Building for App Store + +To build a production app ready for App Store submission: + +```bash +php artisan native:package ios \ + --export-method=app-store \ + --api-key-path=/path/to/api-key.p8 \ + --api-key-id=ABC123DEF \ + --api-issuer-id=01234567-89ab-cdef-0123-456789abcdef \ + --certificate-path=/path/to/distribution.p12 \ + --certificate-password=certificatepassword \ + --provisioning-profile-path=/path/to/profile.mobileprovision \ + --team-id=ABC1234567 +``` + +### Building for Ad-Hoc Distribution + +For distributing to specific devices without going through the App Store: + +```bash +php artisan native:package ios \ + --export-method=ad-hoc \ + --certificate-path=/path/to/distribution.p12 \ + --certificate-password=certificatepassword \ + --provisioning-profile-path=/path/to/ad-hoc-profile.mobileprovision +``` + +### Building for Development + +For testing on your own device: + +```bash +php artisan native:package ios \ + --export-method=development \ + --certificate-path=/path/to/development.p12 \ + --certificate-password=certificatepassword \ + --provisioning-profile-path=/path/to/development-profile.mobileprovision +``` + +### Using Environment Variables + +Store your iOS credentials in `.env`: + +```env +APP_STORE_API_KEY_PATH=/path/to/api-key.p8 +APP_STORE_API_KEY_ID=ABC123DEF +APP_STORE_API_ISSUER_ID=01234567-89ab-cdef-0123-456789abcdef +IOS_DISTRIBUTION_CERTIFICATE_PATH=/path/to/distribution.p12 +IOS_DISTRIBUTION_CERTIFICATE_PASSWORD=certificatepassword +IOS_DISTRIBUTION_PROVISIONING_PROFILE_PATH=/path/to/profile.mobileprovision +IOS_TEAM_ID=ABC1234567 +``` + +Then build with: + +```bash +php artisan native:package ios --export-method=app-store +``` + +### Uploading to App Store Connect + +To automatically upload after building: + +```bash +php artisan native:package ios \ + --export-method=app-store \ + --api-key-path=/path/to/api-key.p8 \ + --api-key-id=ABC123DEF \ + --api-issuer-id=01234567-89ab-cdef-0123-456789abcdef \ + --certificate-path=/path/to/distribution.p12 \ + --certificate-password=certificatepassword \ + --provisioning-profile-path=/path/to/profile.mobileprovision \ + --team-id=ABC1234567 \ + --upload-to-app-store +``` + +The upload uses the App Store Connect API, so API credentials are required only when using `--upload-to-app-store`. + +### Validating Provisioning Profiles + +Before building, you can validate your provisioning profile to check push notification support and entitlements: + +```bash +php artisan native:package ios \ + --validate-profile \ + --provisioning-profile-path=/path/to/profile.mobileprovision +``` + +This extracts and displays: + +- Profile name +- All entitlements configured in the profile +- Push notification support status +- Associated domains +- APS environment matching + +### Testing App Store Uploads + +To test uploading an existing IPA without rebuilding: + +```bash +php artisan native:package ios \ + --test-upload \ + --api-key-path=/path/to/api-key.p8 \ + --api-key-id=ABC123DEF \ + --api-issuer-id=01234567-89ab-cdef-0123-456789abcdef +``` + +### Clearing Xcode Caches + +If you encounter build issues, clear Xcode and Swift Package Manager caches: + +```bash +php artisan native:package ios \ + --export-method=app-store \ + --clean-caches +``` + +### Forcing a Clean Rebuild + +To force a complete rebuild and create a new archive: + +```bash +php artisan native:package ios \ + --export-method=app-store \ + --rebuild +``` + +### Validating Without Exporting + +To validate the archive without creating an IPA: + +```bash +php artisan native:package ios \ + --validate-only +``` + +## Version Management + +### Build Numbers and Version Codes + +The `native:version` command handles version management. When building AABs for the Play Store with valid Google Service credentials, NativePHP automatically checks the Play Store for the latest build number and increments it. + +### Auto-Incrementing from Play Store + +When building a bundle with Play Store access: + +```bash +php artisan native:package android \ + --build-type=bundle \ + --keystore=/path/to/my-app.keystore \ + --keystore-password=mykeystorepassword \ + --key-alias=my-app-key \ + --key-password=mykeypassword \ + --google-service-key=/path/to/service-account-key.json +``` + +The command automatically queries the Play Store to find your latest published build number and increments it. + +### Jumping Ahead in Version Numbers + +If you need to skip version numbers or jump ahead: + +```bash +php artisan native:package android \ + --build-type=bundle \ + --keystore=/path/to/my-app.keystore \ + --keystore-password=mykeystorepassword \ + --key-alias=my-app-key \ + --key-password=mykeypassword \ + --jump-by=10 +``` + +This adds 10 to the version code that would normally be used. + +## Tips & Troubleshooting + +### Common Android Issues + +**Keystore-related errors:** +- Verify the keystore file exists and the path is correct +- Check that the keystore password is correct +- Confirm the key alias exists in the keystore with: `keytool -list -v -keystore /path/to/keystore` +- Verify the key password matches + +**Build failures:** +- Ensure you have the latest Android SDK and build tools installed +- Check that your `nativephp/android` directory exists and is properly initialized +- If you've modified native code, don't use `--skip-prepare` + +**Play Store upload failures:** +- Verify the Google Service Account has access to your app in Play Console +- Ensure the service account key file is valid and readable +- Check that your bundle ID matches your Play Console app ID + +### Custom Output Directories + +By default, the build output opens in your system's file manager. To copy the artifact to a custom location: + +```bash +php artisan native:package android \ + --build-type=bundle \ + --keystore=/path/to/my-app.keystore \ + --keystore-password=mykeystorepassword \ + --key-alias=my-app-key \ + --key-password=mykeypassword \ + --output=/path/to/custom/directory +``` + +### Building in Non-Interactive Environments + +For CI/CD pipelines or automated builds, disable TTY mode: + +```bash +php artisan native:package android \ + --build-type=bundle \ + --keystore=/path/to/my-app.keystore \ + --keystore-password=mykeystorepassword \ + --key-alias=my-app-key \ + --key-password=mykeypassword \ + --no-tty +``` + +### Artifact Locations + +Once complete, signed artifacts are located at: + +**Android:** +- APK (release): `nativephp/android/app/build/outputs/apk/release/app-release.apk` +- AAB (bundle): `nativephp/android/app/build/outputs/bundle/release/app-release.aab` + +**iOS:** +- IPA: Generated in Xcode's build output directory diff --git a/resources/views/docs/mobile/3/getting-started/development.md b/resources/views/docs/mobile/3/getting-started/development.md new file mode 100644 index 00000000..62506e6a --- /dev/null +++ b/resources/views/docs/mobile/3/getting-started/development.md @@ -0,0 +1,226 @@ +--- +title: Development +order: 250 +--- + +Developing your NativePHP apps can be done in the browser, using workflows with which you're already familiar. + +This allows you to iterate rapidly on parts like the UI and major functionality, even using your favorite tools for +testing etc. + +But when you want to test _native_ features, then you must run your app on a real or emulated device. + +Whether you run your native app on an emulated or real device, it will require compilation after changes have been made. + + + +## Build your frontend + +If you're using Vite or similar tooling to build any part of your UI (e.g. for React/Vue, Tailwind etc), you'll need +to run your asset build command _before_ compiling your app. + +To facilitate ease of development, you should install the `nativephpMobile` Vite plugin. + +### The `nativephpMobile` Vite plugin + +To make your frontend build process works well with NativePHP, simply add the `nativephpMobile` plugin to your +`vite.config.js`: + +```js +import { nativephpMobile, nativephpHotFile } from './vendor/nativephp/mobile/resources/js/vite-plugin.js'; // [tl! focus] + +export default defineConfig({ + plugins: [ + laravel({ + input: ['resources/css/app.css', 'resources/js/app.js'], + refresh: true, + hotFile: nativephpHotFile(), // [tl! focus] + }), + tailwindcss(), + nativephpMobile(), // [tl! focus] + ] +}); +``` + +Once that's done, you'll need to adjust your Vite build command when creating builds for each platform — simply add the +`--mode=[ios|android]` option. Run these before compiling your app for each platform in turn: + +```shell +npm run build -- --mode=ios + +npm run build -- --mode=android +``` + +## Compile your app + +To compile and run your app, simply run: + +```shell +php artisan native:run +``` + +This single command takes care of everything and allows you to run new builds of your application without having to +learn any new editors or platform-specific tools. + + + +## Working with Xcode or Android Studio + +On occasion, it is useful to compile your app from inside the target platform's dedicated development tools, Android +Studio and Xcode. + +If you're familiar with these tools, you can easily open the projects using the following Artisan command: + +```shell +php artisan native:open +``` + +### Configuration + +You can configure the folders that the `watch` command pays attention to in your `config/nativephp.php` file: + +```php +'hot_reload' => [ + 'watch_paths' => [ + 'app', + 'routes', + 'config', + 'database', + // Make sure "public" is listed in your config [tl! highlight:1] + 'public', + ], +] +``` + + + + +## Hot Reloading + +We've tried to make compiling your apps as fast as possible, but when coming from the 'make a change; hit refresh'-world +of typical browser-based PHP development that we all love, compiling apps can feel like a slow and time-consuming +process. + +Hot reloading aims to make your app development experience feel just like home. + +You can start hot reloading by running the following command: + +```shell +php artisan native:watch +``` + + + +This will start a long-lived process that watches your application's source files for changes, pushing them into the +emulator after any updates and reloading the current screen. + +If you're using Vite, we'll also use your Node CLI tool of choice (`npm`, `bun`, `pnpm`, or `yarn`) to run Vite's HMR +server. + +### Enabling HMR + +To make HMR work, you'll need to add the `hot` file helper to your `laravel` plugin's config in your `vite.config.js`: + +```js +import { nativephpMobile, nativephpHotFile } from './vendor/nativephp/mobile/resources/js/vite-plugin.js'; // [tl! focus] + +export default defineConfig({ + plugins: [ + laravel({ + input: ['resources/css/app.css', 'resources/js/app.js'], + refresh: true, + hotFile: nativephpHotFile(), // [tl! focus] + }), + tailwindcss(), + nativephpMobile(), + ] +}); +``` + +**Note:** When testing on real devices, hot reloading is communicating with the Vite server running on your development machine. +For this to work, ensure the test device is connected to the same Wi-Fi network as your development machine. + + + +This is useful during development for quickly testing changes without re-compiling your entire app. When you make +changes to any files in your Laravel app, the web view will be reloaded and your changes should show almost immediately. + +Vite HMR is perfect for apps that use SPA frameworks like Vue or React to build the UI. It even works on real devices, +not just simulators! As long as the device is on the same network as the development machine. + +**Don't forget to add `public/ios-hot` and `public/android-hot` to your `.gitignore` file!** + + + +## Laravel Boost + +NativePHP for Mobile supports [Laravel Boost](https://laravel.com/ai/boost) which aims to accelerate AI-assisted development by providing +the essential context and structure that AI needs to generate high-quality, Laravel-specific code. + +After installing `nativephp/mobile` and `laravel/boost` simply run `php artisan boost:install` and follow the prompts +to activate NativePHP for Laravel Boost! diff --git a/resources/views/docs/mobile/3/getting-started/environment-setup.md b/resources/views/docs/mobile/3/getting-started/environment-setup.md new file mode 100644 index 00000000..1847e743 --- /dev/null +++ b/resources/views/docs/mobile/3/getting-started/environment-setup.md @@ -0,0 +1,144 @@ +--- +title: Environment Setup +order: 100 +--- + +## Requirements + +1. PHP 8.3+ +2. Laravel 11+ + +If you don't already have PHP installed on your machine, the most painless way to get PHP up and running on Mac and +Windows is with [Laravel Herd](https://herd.laravel.com). It's fast and free! + +## iOS Requirements + + + +1. macOS (required - iOS development is only possible on an Apple silicon Mac, M1+) +2. [Xcode 16.0 or later](https://apps.apple.com/app/xcode/id497799835) +3. Xcode Command Line Tools +4. Homebrew & CocoaPods +5. _Optional_ iOS device for testing + +### Setting up iOS Development Environment + +1. **Install Xcode** + - Download from the [Mac App Store](https://apps.apple.com/app/xcode/id497799835) + - Minimum version: Xcode 16.0 + +2. **Install Xcode Command Line Tools** + ```shell + xcode-select --install + ``` + Verify installation: + ```shell + xcode-select -p + ``` + +3. **Install Homebrew** (if not already installed) + ```shell + /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" + ``` + +4. **Install CocoaPods** + ```shell + brew install cocoapods + ``` + Verify installation: + ```shell + pod --version + ``` + +### Apple Developer Account +You **do not** need to enroll in the [Apple Developer Program](https://developer.apple.com/programs/enroll/) ($99/year) +to develop and test your apps on a Simulator. However, you will need to enroll when you want to: +- Test your apps on real devices +- Distribute your apps via the App Store +- Test features that rely on a paid Apple Developer accounts, such as Push Notifications + +## Android Requirements + +1. [Android Studio 2024.2.1 or later](https://developer.android.com/studio) +2. Android SDK with API 29 or higher +3. **Windows only**: You must have [7zip](https://www.7-zip.org/) installed. + + + +### Setting up Android Studio and SDK + +1. **Download and Install Android Studio** + - Download from the [Android Studio download page](https://developer.android.com/studio) + - Minimum version required: Android Studio 2024.2.1 + +2. **Install Android SDK** + - Open Android Studio + - Navigate to **Tools → SDK Manager** + - In the **SDK Platforms** tab, install at least one Android SDK platform for API 29 or higher + - Latest stable version: Android 16 (API 36) + - You only need to install one API version to get started + - In the **SDK Tools** tab, ensure **Android SDK Build-Tools** and **Android SDK Platform-Tools** are installed + +That's it! Android Studio handles all the necessary configuration automatically. + +### Preparing for NativePHP + +1. Check that you can run `java -version` and `adb devices` from the terminal. +2. The following environment variables set: + +#### On macOS +```shell +# This isn't required if JAVA_HOME is already set in your environment variables (check using `printenv | grep JAVA_HOME`) +export JAVA_HOME=$(/usr/libexec/java_home -v 17) + +export ANDROID_HOME=$HOME/Library/Android/sdk +export PATH=$PATH:$JAVA_HOME/bin:$ANDROID_HOME/emulator:$ANDROID_HOME/tools:$ANDROID_HOME/tools/bin:$ANDROID_HOME/platform-tools +``` + +#### On Windows +The example below assumes default installation paths for the Android SDK and JDK: + +```shell +set ANDROID_HOME=C:\Users\yourname\AppData\Local\Android\Sdk +set PATH=%PATH%;%JAVA_HOME%\bin;%ANDROID_HOME%\platform-tools + +# This isn't required if JAVA_HOME is already set in the Windows Env Variables +set JAVA_HOME=C:\Program Files\Microsoft\jdk-17.0.8.7-hotspot +``` + +### "No AVDs found" error +If you encounter this error, it means no Virtual Devices are configured in Android Studio. +To resolve it, open Android Studio, navigate to Virtual Devices, and create at least one device. + +## Testing on Real Devices + +You don't _need_ a physical iOS/Android device to compile and test your application, as NativePHP for Mobile supports +the iOS Simulator and Android emulators. However, we highly recommend that you test your application on a real device +before submitting to the Apple App Store and Google Play Store. + +### On iOS +If you want to run your app on a real iOS device, you need to make sure it is in +[Developer Mode](https://developer.apple.com/documentation/xcode/enabling-developer-mode-on-a-device) +and that it's been added to your Apple Developer account as +[a registered device](https://developer.apple.com/account/resources/devices/list). + +### On Android +On Android you need to [enable developer options](https://developer.android.com/studio/debug/dev-options#enable) +and have USB debugging (ADB) enabled. diff --git a/resources/views/docs/mobile/3/getting-started/installation.md b/resources/views/docs/mobile/3/getting-started/installation.md new file mode 100644 index 00000000..e2e91aac --- /dev/null +++ b/resources/views/docs/mobile/3/getting-started/installation.md @@ -0,0 +1,120 @@ +--- +title: Installation +order: 100 +--- + +## Install the Composer package + +NativePHP contains all the libraries, classes, commands, and interfaces that your application will need to work with +iOS and Android. And it's a single command away: + +```shell +composer require nativephp/mobile +``` + +### We love Laravel + +NativePHP for Mobile is built to work with Laravel. We recommend that you install it into a +[new Laravel application](https://laravel.com/docs/installation) for your NativePHP application. + +### Notes for Windows users + +#### Windows Defender + +Add `C:\temp`, as well as your project folder, to your Windows Defender exclusions list to significantly speed up +Composer installs during app compilation. This prevents its real-time scanning from processing the many temporary files +created during the build process, which slows the process considerably. + +#### No WSL support + +NativePHP does not work in WSL (Windows Subsystem for Linux). You must install and run NativePHP directly on Windows. + +## Run the NativePHP installer + +**Before** running the `install` command, it is important to set the following variables in your `.env`: + +```dotenv +NATIVEPHP_APP_ID=com.yourcompany.yourapp +NATIVEPHP_APP_VERSION="DEBUG" +NATIVEPHP_APP_VERSION_CODE="1" +``` + +Find out more about these options in +[Configuration](/docs/getting-started/configuration#codenativephp-app-idcode). + + + + +```shell +php artisan native:install +``` + +The NativePHP installer takes care of setting up and configuring your Laravel application to work with iOS and Android. + +You may be prompted about whether you would like to install the ICU-enabled PHP binaries. You should install these if +your application relies on the `intl` PHP extension. + +If you don't need `intl` or are not sure, choose the default, non-ICU builds. + + + +### The `nativephp` Directory + +After running: `php artisan native:install` you’ll see a new `nativephp` directory at the root of your Laravel project +as well as a `config/nativephp.php` config file. + +The `nativephp` folder contains the native application project files needed to build your app for the desired platforms. + +You should not need to manually open or edit any native project files under normal circumstances. NativePHP handles +the heavy lifting for you. + +**You should treat this directory as ephemeral.** When upgrading the NativePHP package, it will be necessary to run +`php artisan native:install --force`, which completely rebuilds this directory, deleting all files within. + +For this reason, we also recommend you add the `nativephp` folder to your `.gitignore`. + +## Start your app + +**Heads up!** Before starting your app in a native context, try running it in the browser. You may bump into exceptions +which need addressing before you can run your app natively, and may be trickier to spot when doing so. + +Once you're ready: + +```shell +php artisan native:run +``` + +Just follow the prompts! This will start compiling your application and boot it on whichever device you select. + +### Running on a real device + +#### On iOS +If you want to run your app on a real iOS device, you need to make sure it is in +[Developer Mode](https://developer.apple.com/documentation/xcode/enabling-developer-mode-on-a-device) +and that it's been added to your Apple Developer account as +[a registered device](https://developer.apple.com/account/resources/devices/list). + +#### On Android +On Android you need to [enable developer options](https://developer.android.com/studio/debug/dev-options#enable) +and have USB debugging (ADB) enabled. + +And that's it! You should now see your Laravel application running as a native app! 🎉 diff --git a/resources/views/docs/mobile/3/getting-started/introduction.md b/resources/views/docs/mobile/3/getting-started/introduction.md new file mode 100644 index 00000000..15a2bf6a --- /dev/null +++ b/resources/views/docs/mobile/3/getting-started/introduction.md @@ -0,0 +1,62 @@ +--- +title: Introduction +order: 1 +--- + +## Enjoy building mobile apps! + +NativePHP for Mobile is the first library of its kind that lets you run full PHP applications natively on mobile +devices — no web server required. + +By embedding a pre-compiled PHP runtime alongside Laravel, and bridging directly into each platform’s native +APIs, NativePHP brings the power of modern PHP to truly native mobile apps. Build performant, offline-capable +experiences using the tools you already know. + +**It's never been this easy to build beautiful, offline-first apps for iOS and Android.** + +## What makes NativePHP for Mobile special? + +- 📱 **Native performance** + Your app runs natively through an embedded PHP runtime optimized for each platform. +- 🔥 **True native APIs** + Access camera, biometrics, push notifications, and more. Build beautiful UIs with native components. All from one + cohesive library that does it all. +- ⚡ **Laravel powered** + Leverage the entire Laravel ecosystem and your existing skillset. +- 🚫 **No web server required** + Your app runs entirely on-device and can operate completely offline-first. +- 🔄 **Cross platform** + Build apps for both iOS and Android from a single codebase. + +## Old tools, new tricks + +With NativePHP for Mobile, you don’t need to learn Swift, Kotlin, or anything new. +No new languages. No unfamiliar build tools. No fighting with Gradle or Xcode. + +Just PHP. + +Developers around the world are using the skills they already have to build and ship real mobile apps — faster than +ever. In just a few minutes, you can go from code to app store submission. + +## How does it work? + +1. A pre-compiled version of PHP is bundled with your code into a Swift/Kotlin shell application. +2. NativePHP's custom Swift/Kotlin bridges manage the PHP environment, running your PHP code directly. +3. A custom PHP extension is compiled into PHP, that exposes PHP interfaces to native functions. +4. Build with HTML, JavaScript, Tailwind, Blade, Livewire, React, Vue, Svelte — whatever you're most comfortable with! +5. And now in v3: use truly native UI components too with [EDGE](/docs/mobile/3/edge-components/)! + +You simply interact with an easy-to-use set of functions from PHP and everything just works! + +## Batteries included + +NativePHP for Mobile is way more than just a web view wrapper for your server-based application. Your application lives +_on device_ and is shipped with each installation. + +Thanks to our custom PHP extension, you can interact with many native APIs today, with more coming all the time. Check out the API documentation section to see everything that's available. + +You have the full power of PHP and Laravel at your fingertips... literally! And you're not sandboxed into the web view; +this goes way beyond what's possible with PWAs and WASM without any of the complexity... we've got full-cream PHP at +the ready! + +**What are you waiting for!? [Let's go!](quick-start)** diff --git a/resources/views/docs/mobile/3/getting-started/quick-start.md b/resources/views/docs/mobile/3/getting-started/quick-start.md new file mode 100644 index 00000000..7fbce38f --- /dev/null +++ b/resources/views/docs/mobile/3/getting-started/quick-start.md @@ -0,0 +1,75 @@ +--- +title: Quick Start +order: 2 +--- + +## Jump in + +Don't waste hours downloading, installing, and configuring Xcode and Android Studio; just +[Jump](https://bifrost.nativephp.com/jump): + +1. Install the Jump app on your iOS or Android device +2. Run the following commands: + +### New Laravel app + +If you are creating new Laravel app, you can build using our starter kit: + +```bash +laravel new my-app --using=nativephp/mobile-starter + +cd my-app + +php artisan native:jump +``` + +### Existing Laravel app + +If you already have a Laravel app: + +```bash +composer require nativephp/mobile + +php artisan native:jump +``` + +Scan the QR code with Jump and you're off! + +## Install & run + +If you've already got your [environment set up](environment-setup) to build mobile apps using Xcode and/or Android +Studio, you can build and run your app locally: + +```bash +# Install NativePHP for Mobile into a new Laravel app +composer require nativephp/mobile + +# Ready your app to go native +php artisan native:install + +# Run your app on a mobile device +php artisan native:run +``` + +#### The `native` command + +When you run `native:install`, NativePHP installs a `native` script helper that can be used as a convenient wrapper to +the `native` Artisan command namespace. Once this is installed you can do the following: + +```shell +# Instead of... +php artisan native:run + +# Do +php native run + +# Or +./native run +``` + +## Need help? + +- **Community** - Join our [Discord](/discord) for support and discussions. +- **Examples** - Check out the Kitchen Sink demo app + on [Android](https://play.google.com/store/apps/details?id=com.nativephp.kitchensinkapp) and + [iOS](https://testflight.apple.com/join/vm9Qtshy)! diff --git a/resources/views/docs/mobile/3/getting-started/roadmap.md b/resources/views/docs/mobile/3/getting-started/roadmap.md new file mode 100644 index 00000000..c5d347f2 --- /dev/null +++ b/resources/views/docs/mobile/3/getting-started/roadmap.md @@ -0,0 +1,32 @@ +--- +title: Roadmap +order: 400 +--- + +NativePHP for Mobile is stable and already deployed in production apps released on the app stores. But we're not done +yet! Here's what we're focusing on next: + +## Background tasks + +We will be adding the ability to run code in the background, even when your app isn't in the foreground. Perfect for +syncing data, processing uploads, or handling push notifications. + +## Native UI through EDGE + +We will be expanding EDGE's (Element Definition and Generation Engine) capabilities, letting you define more truly +native UI components from your PHP code. Build navigation bars, tab bars, and other native elements that feel right at +home on each platform. + +## Performance + +We will be improving NativePHP's performance, making apps faster and more efficient. Expect improvements to startup +time, memory usage, and overall responsiveness. + + diff --git a/resources/views/docs/mobile/3/getting-started/support-policy.md b/resources/views/docs/mobile/3/getting-started/support-policy.md new file mode 100644 index 00000000..396f4fc6 --- /dev/null +++ b/resources/views/docs/mobile/3/getting-started/support-policy.md @@ -0,0 +1,22 @@ +--- +title: Support Policy +order: 600 +--- + +NativePHP for Mobile is still very new. We aim to make it workable with as many versions of iOS and Android as is +reasonable. Considering that we have a very small team and a lot of work, our current stance on version support is this: + +**We aim (but do not guarantee) to support all the current and upcoming major, currently vendor-supported versions of +the platforms, with a focus on the current major release as a priority.** + +In practical terms, as of September 2025, this means we intend for NativePHP to be compatible — in part or in whole — +with: + +- iOS 18+ +- Android 13+ + +We do not guarantee support of all features across these versions, and whilst NativePHP may work in part on even older +versions than the currently-supported ones, we do not provide support for these under this standard policy. + +If you require explicit backwards compatibility with older or unsupported versions, we will be happy to have you join +our [partner](/partners) program, where a custom support policy can be arranged. diff --git a/resources/views/docs/mobile/3/getting-started/upgrade-guide.md b/resources/views/docs/mobile/3/getting-started/upgrade-guide.md new file mode 100644 index 00000000..ea5cca07 --- /dev/null +++ b/resources/views/docs/mobile/3/getting-started/upgrade-guide.md @@ -0,0 +1,180 @@ +--- +title: Upgrade Guide +order: 3 +--- + +## Upgrading To 3.1 From 3.0 + +v3.1 is a drop-in upgrade with no breaking changes. The headline feature is a **persistent PHP runtime** that +dramatically improves request performance. + + +### Update your dependency + +```json +"require": { + "nativephp/mobile": "~3.0.0" // [tl! remove] + "nativephp/mobile": "~3.1.0" // [tl! add] +} +``` + +```sh +composer update +php artisan native:install --force +``` + + + + +## Android 8+ Support + +In order to support Android API 26 add the following to your `config/nativephp.php`: + +```php +'android' => [ + 'compile_sdk' => env('NATIVEPHP_ANDROID_COMPILE_SDK', 36), + 'min_sdk' => env('NATIVEPHP_ANDROID_MIN_SDK', 33), + 'target_sdk' => env('NATIVEPHP_ANDROID_TARGET_SDK', 36), +], +``` + +No code changes are required — this is handled entirely at the build level. + +## ICU/Intl Support on iOS + +iOS builds now include full **ICU support**, which means the PHP `intl` extension works on both platforms. +This was previously only available on Android. + +This is a big deal — packages like [Filament](https://filamentphp.com) depend on `intl` for number formatting, +date formatting, and pluralization. With v3.1, **Filament works on both iOS and Android** out of the box. + +ICU support remains optional via the `--with-icu` / `--without-icu` flags during installation it adds ~30MB to your app on Android and ~100MB on iOS. +--- + +## Upgrading To 3.0 From 2.x + +NativePHP for Mobile v3 introduces a plugin-based architecture that makes the entire native layer extensible. +All core functionality continues to work as before, but the underlying system is now modular and open to +third-party plugins. + +## Remove the NativePHP Composer Repository + +v3 no longer requires the private Composer repository or license authentication. Remove the `nativephp.composer.sh` +repository from your `composer.json`: + +```json +"repositories": [ + { + "type": "composer", + "url": "https://nativephp.composer.sh" + } +] +``` + +Delete the entire `repositories` block above (or just the NativePHP entry if you have other repositories). + +You can also remove any stored credentials for `nativephp.composer.sh` from your `auth.json` if you have one. + +Then update your version constraint and run the upgrade: + +```json +"require": { + "nativephp/mobile": "~2.0.0" // [tl! remove] + "nativephp/mobile": "~3.0.0" // [tl! add] +} +``` + +```sh +composer update +php artisan native:install --force +``` + +## Plugin Architecture + +v3 introduces a comprehensive plugin system. Native functionality is now delivered through plugins — including all +the official core APIs you already use (Camera, Biometrics, Scanner, etc.). These continue to work exactly as before; +you don't need to change how you call them. + +The difference is that **third-party developers can now create plugins** that add new native functionality to your +app. Plugins are standard Composer packages that include Swift (iOS) and Kotlin (Android) code alongside their +PHP interface. + +Read more about the plugin system in the [Plugins documentation](../plugins/introduction), or browse +ready-made plugins on the [NativePHP Plugin Marketplace](https://nativephp.com/plugins). + +## NativeServiceProvider + +v3 introduces a `NativeServiceProvider` for registering third-party plugins. Publish it with: + +```shell +php artisan vendor:publish --tag=nativephp-plugins-provider +``` + +This creates `app/Providers/NativeServiceProvider.php`. Any third-party plugins you install must be registered +here before their native code is compiled into your app. This is a security measure to prevent transitive +dependencies from automatically including native code without your consent. + +```shell +php artisan native:plugin:register vendor/some-plugin +``` + +Core APIs provided by `nativephp/mobile` do not need to be registered manually — they are included automatically. + +Read more about [Using Plugins](../plugins/using-plugins). + +## Core APIs Are Now Plugins + +All core APIs (Camera, Biometrics, Dialog, Scanner, Geolocation, etc.) are now implemented as plugins internally. +The PHP facades and events you use remain the same — no changes to your application code are needed. + +Browse the full list of available core plugins in the [Plugins documentation](../plugins/introduction). + +## Plugin Management Commands + +v3 adds several new Artisan commands for working with plugins: + +| Command | Description | +|---------|-------------| +| `native:plugin:create` | Scaffold a new plugin | +| `native:plugin:register` | Register a plugin in your NativeServiceProvider | +| `native:plugin:list` | List installed plugins | +| `native:plugin:uninstall` | Remove a plugin | +| `native:plugin:validate` | Validate plugin structure | +| `native:plugin:make-hook` | Create a lifecycle hook | + +## Bridge Functions + +Plugins communicate with native code through **bridge functions** — a standardized pattern for calling +Swift and Kotlin code from PHP via `nativephp_call()`. Each plugin declares its bridge functions in a +`nativephp.json` manifest. + +If you've been using the core APIs through their facades, nothing changes for you. Bridge functions are primarily +relevant if you're building your own plugins. + +Read more in the [Bridge Functions documentation](../plugins/bridge-functions). + +## Plugin Marketplace + +Find ready-made plugins for common use cases, or get the Dev Kit to build your own. +[Visit the NativePHP Plugin Marketplace](https://nativephp.com/plugins). + +## Command Reference + +v3 includes a comprehensive [Command Reference](commands) documenting all `native:*` Artisan commands +with their options and usage. + +## Rebuild Required + +After upgrading, you must rebuild your native application: + +```shell +php artisan native:install --force +php artisan native:run +``` + +The `--force` flag ensures the `nativephp` directory is completely rebuilt with the v3 native project files. diff --git a/resources/views/docs/mobile/3/getting-started/versioning.md b/resources/views/docs/mobile/3/getting-started/versioning.md new file mode 100644 index 00000000..07640b03 --- /dev/null +++ b/resources/views/docs/mobile/3/getting-started/versioning.md @@ -0,0 +1,69 @@ +--- +title: Versioning Policy +order: 500 +--- + +NativePHP for Mobile follows [semantic versioning](https://semver.org) with a mobile-specific approach that distinguishes between +Laravel-only changes and native code changes. This ensures predictable updates and optimal compatibility. + +Our aim is to limit the amount of work you need to do to get the latest updates and ensure everything works. + +We will aim to post update instructions with each release. + +## Release types + +### Patch releases + +Patch releases of `nativephp/mobile` should have **no breaking changes** and **only change Laravel/PHP code**. +This will typically include bug fixes and dependency updates that don't affect native code. + +These releases should be completely compatible with the existing version of your native applications. + +This means that you can: + +- Safely update via `composer update`. +- Avoid a complete rebuild (no need to `native:install --force`). +- Allow for easier app updates avoiding the app stores. + +### Minor releases + +Minor releases may contain **native code changes**. Respecting semantic versioning, these still should not contain +breaking changes, but there may be new native APIs, Kotlin/Swift updates, platform-specific features, or native +dependency changes. + +Minor releases will: + +- Require a complete rebuild (`php artisan native:install --force`) to work with the latest APIs. +- Need app store submission for distribution. +- Include advance notice and migration guides where necessary. + +### Major releases + +Major releases are reserved for breaking changes. This will usually follow a period of deprecations so that you have +time to make the necessary changes to your application code. + +## Version constraints + +We recommend using the [tilde range operator](https://getcomposer.org/doc/articles/versions.md#tilde-version-range-) +with a full minimum patch release defined in your `composer.json`: + +```json +{ + "require": { + "nativephp/mobile": "~2.0.0" + } +} +``` + +This automatically receives patch updates while giving you control over minor releases. + +## Your application versioning + +Just because we're using semantic versioning for the `nativephp/mobile` package, doesn't mean your app must follow that +same scheme. + +You have complete freedom in versioning your own applications! You may use semantic versioning, codenames, +date-based versions, or any scheme that works for your project, team or business. + +Remember that your app versions are usually public-facing (e.g. in store listings and on-device settings and update +screens) and can be useful for customers to reference if they need to contact you for help and support. diff --git a/resources/views/docs/mobile/3/plugins/_index.md b/resources/views/docs/mobile/3/plugins/_index.md new file mode 100644 index 00000000..bbc1a5ec --- /dev/null +++ b/resources/views/docs/mobile/3/plugins/_index.md @@ -0,0 +1,4 @@ +--- +title: Plugins +order: 60 +--- diff --git a/resources/views/docs/mobile/3/plugins/advanced-configuration.md b/resources/views/docs/mobile/3/plugins/advanced-configuration.md new file mode 100644 index 00000000..c8840b30 --- /dev/null +++ b/resources/views/docs/mobile/3/plugins/advanced-configuration.md @@ -0,0 +1,454 @@ +--- +title: Advanced Configuration +order: 750 +--- + + + +## Secrets & Environment Variables + +Plugins that require API keys, tokens, or other sensitive configuration can declare required environment variables using +the `secrets` field. NativePHP validates these before building. + +```json +{ + "secrets": { + "MAPBOX_DOWNLOADS_TOKEN": { + "description": "Mapbox SDK download token from mapbox.com/account/access-tokens", + "required": true + }, + "FIREBASE_API_KEY": { + "description": "Firebase project API key", + "required": false + } + } +} +``` + +Each secret has: +- **description** — Instructions for obtaining the value +- **required** — Whether the build should fail if missing (default: `true`) + +### Using Secrets + +Reference secrets anywhere in your manifest using `${ENV_VAR}` syntax: + +```json +{ + "android": { + "repositories": [ + { + "url": "https://api.mapbox.com/downloads/v2/releases/maven", + "credentials": { + "password": "${MAPBOX_DOWNLOADS_TOKEN}" + } + } + ] + } +} +``` + +Placeholders are substituted at build time. If a required secret is missing, the build fails with a helpful message +telling users exactly which variables to set in their `.env` file. + +## Android Manifest Components + +Plugins can register Android components (Activities, Services, Receivers, Providers) that get merged into the app's +`AndroidManifest.xml`: + +```json +{ + "android": { + "activities": [ + { + "name": ".MyPluginActivity", + "theme": "@style/Theme.AppCompat.Light.NoActionBar", + "exported": false, + "configChanges": "orientation|screenSize" + } + ], + "services": [ + { + "name": ".BackgroundSyncService", + "exported": false, + "foregroundServiceType": "dataSync" + } + ], + "receivers": [ + { + "name": ".BootReceiver", + "exported": true, + "intent-filters": [ + { + "action": "android.intent.action.BOOT_COMPLETED", + "category": "android.intent.category.DEFAULT" + } + ] + } + ], + "providers": [ + { + "name": ".MyContentProvider", + "authorities": "${applicationId}.myplugin.provider", + "exported": false, + "grantUriPermissions": true + } + ] + } +} +``` + +### Component Names + +Names starting with `.` are relative to your plugin's package. For example, if your plugin uses the package +`com.nativephp.plugins.mlplugin`, then `.MyActivity` becomes `com.nativephp.plugins.mlplugin.MyActivity`. + +Use fully qualified names for components outside your plugin's package. + +### Activity Attributes + +| Attribute | Description | +|-----------|-------------| +| `name` | Component class name (required) | +| `theme` | Activity theme resource | +| `exported` | Whether other apps can start this activity | +| `configChanges` | Configuration changes the activity handles itself | +| `launchMode` | Launch mode (standard, singleTop, singleTask, singleInstance) | +| `screenOrientation` | Orientation lock (portrait, landscape, etc.) | +| `intent-filters` | Array of intent filter configurations | + +### Service Attributes + +| Attribute | Description | +|-----------|-------------| +| `name` | Component class name (required) | +| `exported` | Whether other apps can bind to this service | +| `permission` | Permission required to access the service | +| `foregroundServiceType` | Type for foreground services (camera, microphone, location, etc.). Supports array format for multiple types. | + +## Android Features + +Declare hardware or software features your plugin requires using the `features` array. These are added as +`` elements in `AndroidManifest.xml`: + +```json +{ + "android": { + "features": [ + {"name": "android.hardware.camera", "required": true}, + {"name": "android.hardware.camera.autofocus", "required": false}, + {"name": "android.hardware.bluetooth_le", "required": true} + ] + } +} +``` + +Each feature has: +- **name** — The feature name (e.g., `android.hardware.camera`) +- **required** — Whether the app requires this feature (default: `true`) + +Setting `required: false` allows your app to be installed on devices without the feature, but you must check +for availability at runtime. + +## Android Meta-Data + +Add application-level `` elements for SDK configuration: + +```json +{ + "android": { + "meta_data": [ + { + "name": "com.google.android.geo.API_KEY", + "value": "${GOOGLE_MAPS_API_KEY}" + }, + { + "name": "com.google.firebase.messaging.default_notification_icon", + "value": "@drawable/ic_notification" + } + ] + } +} +``` + +Each entry has: +- **name** — The meta-data key +- **value** — The value (supports `${ENV_VAR}` placeholders) + +## Declarative Assets + +Copy static files to the native projects using the `assets` field. This is simpler than writing a `copy_assets` hook for +basic file copying: + +```json +{ + "assets": { + "android": { + "models/detector.tflite": "assets/ml/detector.tflite", + "config/settings.xml": "res/raw/plugin_settings.xml" + }, + "ios": { + "models/detector.mlmodel": "Resources/ml/detector.mlmodel", + "config/settings.plist": "Resources/plugin_settings.plist" + } + } +} +``` + +The format is `"source": "destination"`: +- **source** — Relative path from your plugin's `resources/` directory +- **destination** — Where to place the file in the native project + +### Android Destinations + +- `assets/...` — App assets (accessible via `AssetManager`) +- `res/raw/...` — Raw resources (accessible via `R.raw.*`) +- `res/drawable/...` — Drawable resources + +### iOS Destinations + +- `Resources/...` — Bundle resources + +### Placeholder Substitution + +Text-based assets (XML, JSON, plist, etc.) support `${ENV_VAR}` placeholders that are replaced with environment +variable values during the build: + +```xml + + + ${MY_PLUGIN_API_KEY} + +``` + + + +## iOS Background Modes + +Enable background execution capabilities with the `background_modes` array. These values are added to +`UIBackgroundModes` in `Info.plist`: + +```json +{ + "ios": { + "background_modes": ["audio", "fetch", "processing", "location"] + } +} +``` + +Common values: +- `audio` — Audio playback or recording +- `fetch` — Background fetch +- `processing` — Background processing tasks +- `location` — Location updates +- `remote-notification` — Push notification processing +- `bluetooth-central` — Bluetooth LE central mode +- `bluetooth-peripheral` — Bluetooth LE peripheral mode + + + +## iOS Entitlements + +Configure app entitlements for capabilities like Maps, App Groups, HealthKit, or iCloud: + +```json +{ + "ios": { + "entitlements": { + "com.apple.developer.maps": true, + "com.apple.security.application-groups": ["group.com.example.shared"], + "com.apple.developer.associated-domains": ["applinks:example.com"], + "com.apple.developer.healthkit": true + } + } +} +``` + +Values can be: +- **Boolean** — `true`/`false` for simple capabilities +- **Array** — For capabilities requiring multiple values (App Groups, Associated Domains) +- **String** — For single-value entitlements + +Entitlements are written to `NativePHP.entitlements`. If the file doesn't exist, it's created automatically. + + + +## iOS Capabilities + +Declare iOS capabilities your plugin requires. These are separate from entitlements and are used for Xcode project +configuration: + +```json +{ + "ios": { + "capabilities": ["push-notifications", "background-modes", "healthkit"] + } +} +``` + +## Minimum Platform Versions + +Specify minimum platform versions your plugin requires: + +```json +{ + "android": { + "min_version": 29 + }, + "ios": { + "min_version": "18.0" + } +} +``` + +- **Android** — Minimum SDK version (integer, e.g., `29` for Android 10) +- **iOS** — Minimum iOS version (string, e.g., `"18.0"`) + +NativePHP currently supports a minimum of Android SDK 29 and iOS 18. Your plugin's minimum versions cannot be lower +than these. Use this field when your plugin requires a higher version than NativePHP's baseline. + +If a user's app targets a lower version than your plugin requires, they'll receive a warning during plugin validation. + +## Initialization Functions + +Plugins can specify native functions to call during app initialization. This is useful for SDKs that require early +setup before any bridge functions are called: + +```json +{ + "android": { + "init_function": "com.myvendor.plugins.myplugin.MyPluginInit.initialize" + }, + "ios": { + "init_function": "MyPluginInit.initialize" + } +} +``` + +The init function is called once when the app starts, before any bridge functions are available. Use this for: +- SDK initialization that must happen early +- Setting up global state or singletons +- Registering observers or listeners + + + + + +## Complete Example + +Here's a complete manifest for a plugin that integrates Firebase ML Kit with a custom Activity: + +```json +{ + "namespace": "FirebaseML", + "bridge_functions": [ + { + "name": "FirebaseML.Analyze", + "android": "com.nativephp.plugins.firebaseml.AnalyzeFunctions.Analyze", + "ios": "FirebaseMLFunctions.Analyze" + } + ], + "events": [ + "Vendor\\FirebaseML\\Events\\AnalysisComplete" + ], + "android": { + "permissions": [ + "android.permission.CAMERA", + "android.permission.INTERNET" + ], + "features": [ + {"name": "android.hardware.camera", "required": true} + ], + "dependencies": { + "implementation": [ + "com.google.firebase:firebase-ml-vision:24.1.0", + "com.google.firebase:firebase-core:21.1.1" + ] + }, + "activities": [ + { + "name": ".CameraPreviewActivity", + "theme": "@style/Theme.AppCompat.Light.NoActionBar", + "exported": false, + "configChanges": "orientation|screenSize|keyboardHidden" + } + ], + "meta_data": [ + { + "name": "com.google.firebase.ml.vision.DEPENDENCIES", + "value": "ocr" + } + ] + }, + "ios": { + "info_plist": { + "NSCameraUsageDescription": "Camera is used for ML analysis" + }, + "dependencies": { + "pods": [ + {"name": "Firebase/MLVision", "version": "~> 10.0"}, + {"name": "Firebase/Core", "version": "~> 10.0"} + ] + }, + "background_modes": ["processing"], + "entitlements": { + "com.apple.developer.associated-domains": ["applinks:example.com"] + } + }, + "assets": { + "android": { + "google-services.json": "google-services.json" + }, + "ios": { + "GoogleService-Info.plist": "Resources/GoogleService-Info.plist" + } + }, + "secrets": { + "FIREBASE_API_KEY": { + "description": "Firebase API key from Firebase Console", + "required": true + } + }, + "hooks": { + "pre_compile": "nativephp:firebase-ml:setup" + } +} +``` + +## Official Plugins & Dev Kit + +Browse ready-made plugins for common use cases, or get the Plugin Dev Kit to build your own. +[Visit the NativePHP Plugin Marketplace →](https://nativephp.com/plugins) diff --git a/resources/views/docs/mobile/3/plugins/best-practices.md b/resources/views/docs/mobile/3/plugins/best-practices.md new file mode 100644 index 00000000..2d3f942d --- /dev/null +++ b/resources/views/docs/mobile/3/plugins/best-practices.md @@ -0,0 +1,362 @@ +--- +title: Best Practices +order: 900 +--- + +## Overview + +Building a plugin that works is only the first step. Building one that's easy to install, well-documented, and +tested across platforms and frontend stacks is what makes a plugin worth publishing. + +This page covers the standards we expect from plugins listed on the +[NativePHP Plugin Marketplace](https://nativephp.com/plugins) and what makes the difference between a plugin +developers trust and one they abandon after 10 minutes. + +## Documentation + +Every plugin must ship with a comprehensive README. This is the first thing developers see and it determines +whether they'll bother installing your plugin at all. + +### Required README Sections + +Your README should include all of the following: + +**Installation:** + +```markdown +## Installation + +composer require vendor/my-plugin + +php artisan native:plugin:register vendor/my-plugin +``` + +**PHP usage with complete examples:** + +```markdown +## Usage (PHP) + +use Vendor\MyPlugin\Facades\MyPlugin; + +// Basic usage +$result = MyPlugin::doSomething(['option' => 'value']); + +// Listening for events in Livewire +#[OnNative(SomethingCompleted::class)] +public function handleResult($data) +{ + $this->result = $data['result']; +} +``` + +**JavaScript usage for SPA frameworks:** + +```markdown +## Usage (JavaScript) + +import { DoSomething } from 'vendor-my-plugin'; + +// In Vue/React components +const result = await DoSomething({ option: 'value' }); +``` + +**Available methods, events, and required permissions** — document every public method your facade exposes, +every event your plugin dispatches, and every permission it requires. Don't make developers read your source +code to figure out what your plugin does. + +**Environment variables and secrets** — if your plugin requires API keys or tokens, document exactly where to +get them and how to configure them. + +### Keep Documentation Current + +Update your README whenever you change your plugin's API. Outdated documentation is worse than no documentation — it +actively misleads developers and wastes their time. If a method signature changes, update the README in the same +commit. + +## JavaScript Implementations + +Every plugin must provide a JavaScript library alongside the PHP facade. Many NativePHP apps use Inertia with +Vue or React, and those developers need to call your native functions directly from their components without +going through Livewire. + +Your `resources/js/` directory should export clean, documented functions: + +```js +// resources/js/index.js +const baseUrl = '/_native/api/call'; + +async function bridgeCall(method, params = {}) { + const response = await fetch(baseUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ method, params }) + }); + return response.json(); +} + +export async function DoSomething(options = {}) { + return bridgeCall('MyPlugin.DoSomething', options); +} + +export async function DoSomethingElse(id, options = {}) { + return bridgeCall('MyPlugin.DoSomethingElse', { id, ...options }); +} +``` + +Provide a named export for every bridge function your plugin exposes. If your plugin dispatches events, document +how to listen for them in both Livewire and SPA contexts. + +### npm Package + +Consider publishing your JavaScript library as an npm package so developers can `npm install` it and get proper +TypeScript definitions, autocompletion, and tree-shaking. + +## Testing on Real Devices + +Simulators and emulators are useful for development, but they don't catch everything. Many native APIs behave +differently — or don't work at all — on real hardware. Camera, biometrics, Bluetooth, NFC, GPS, and sensors all +require real device testing. + +### Requirements + +- **Android** — test on a physical Android device. Don't rely solely on the Android emulator. +- **iOS** — test on a physical iPhone or iPad. The iOS simulator doesn't support camera, biometrics, or many + hardware features. +- Test on devices running both current and previous major OS versions when possible. + +### Provide a Test App + +Ideally, provide a link to a test build so that the NativePHP team and other developers can verify your plugin +works without having to set up the full build chain: + +- **iOS** — distribute via [TestFlight](https://developer.apple.com/testflight/) +- **Android** — distribute via a [Google Play testing track](https://support.google.com/googleplay/android-developer/answer/9845334) + (internal, closed, or open testing) + +Include the test app link in your README and your plugin marketplace submission. This significantly speeds up the +review process and builds trust with users. + +## Frontend Stack Compatibility + +NativePHP apps use different frontend stacks. Your plugin must work with all of them. + +### Test With + +- **Livewire v3** — the most common stack for NativePHP apps. Test that `#[OnNative]` event listeners work, + that facade calls from Livewire actions return correct data, and that loading states behave properly. +- **Livewire v4** — test forward compatibility. +- **Inertia + Vue** — test your JavaScript library imports and bridge calls from Vue components. Verify events + are received correctly. +- **Inertia + React** — same as Vue. Test imports, bridge calls, and event handling from React components. + +If your plugin only supports a subset of these stacks, document this clearly in your README. But aim for full +compatibility — it's the difference between a plugin that works for everyone and one that fragments the ecosystem. + +### Example: Livewire Component + +```php +use Livewire\Component; +use Native\Mobile\Attributes\OnNative; +use Vendor\MyPlugin\Facades\MyPlugin; +use Vendor\MyPlugin\Events\ScanComplete; + +class Scanner extends Component +{ + public ?string $result = null; + + public function scan(): void + { + MyPlugin::startScan(); + } + + #[OnNative(ScanComplete::class)] + public function handleScan($data): void + { + $this->result = $data['value']; + } + + public function render() + { + return view('livewire.scanner'); + } +} +``` + +### Example: Vue Component (Inertia) + +```vue + + + +``` + +## Boost Guidelines + +If your users use [Laravel Boost](https://laravel.com/ai/boost), providing Boost guidelines makes your +plugin dramatically easier to work with. When developers ask their assistant to use your plugin, it will +know exactly how — which methods to call, what events to listen for, and how to handle responses. + +Generate guidelines with: + +```shell +php artisan native:plugin:boost +``` + +This creates `resources/boost/guidelines/core.blade.php` in your plugin. Edit it to include: + +- All available facade methods with descriptions and parameter types +- All events your plugin dispatches with their payload shapes +- JavaScript usage examples +- Common patterns and gotchas +- Required permissions and configuration + +When users install your plugin and run `php artisan boost:install`, these guidelines are automatically loaded. + +## Validation + +Run the validation command before every release: + +```shell +php artisan native:plugin:validate +``` + +This catches: +- Manifest syntax errors and missing required fields +- Bridge function declarations that don't match native code +- Hook commands that aren't registered +- Missing declared assets + +Your plugin should pass validation with zero errors. If you're using the +[Plugin Dev Kit](/products/plugin-dev-kit), use the `/validate-nativephp-plugin` command which runs additional +checks beyond the Artisan command. + +Fix every warning too — they often indicate issues that will cause confusing failures for your users at build +time or runtime. + +## Automated Review Checks + +When you submit your plugin, we run automated checks against your repository. These must all pass before +your plugin can be approved. You can also run `php artisan native:plugin:validate` locally to catch issues early. + +### Required for Approval + +The following checks must pass before your plugin can be approved: + +**License file** — Your repository must include a `LICENSE`, `LICENSE.md`, or `LICENSE.txt` file at the root. + +**Release version** — Your repository must have at least one GitHub release or tag. + +**Webhook configured** — A GitHub webhook must be configured for your repository so that your plugin data +syncs automatically when you push changes or create releases. We'll attempt to set this up automatically +when you submit. If it can't be installed automatically, you'll see manual setup instructions on your plugin +dashboard. + +**Support channel** — You must provide a support email address or URL when submitting your plugin so +developers can reach you with questions or issues. You can update this anytime from your plugin dashboard. + +### Additional Checks + +**iOS native code** — Your plugin must include native Swift code in `resources/ios/Sources/`. See +[Bridge Functions](/docs/mobile/3/plugins/bridge-functions) for the implementation pattern. + +**Android native code** — Your plugin must include native Kotlin code in `resources/android/src/`. See +[Bridge Functions](/docs/mobile/3/plugins/bridge-functions) for the implementation pattern. + +**JavaScript library** — Your plugin must include a JavaScript library in `resources/js/` that exports +a function for every bridge function. This allows Inertia + Vue/React developers to call your native functions +directly. See the [JavaScript Implementations](#javascript-implementations) section above. + +**Require `nativephp/mobile`** — Your `composer.json` must require the `nativephp/mobile` SDK. This ensures +your plugin is properly integrated with the NativePHP build pipeline: + +```json +{ + "require": { + "nativephp/mobile": "^3.0" + } +} +``` + +**iOS `min_version`** — Your `nativephp.json` must specify a minimum iOS version. See +[Advanced Configuration](/docs/mobile/3/plugins/advanced-configuration) for details: + +```json +{ + "ios": { + "min_version": "18.0" + } +} +``` + +**Android `min_version`** — Your `nativephp.json` must specify a minimum Android SDK version. See +[Advanced Configuration](/docs/mobile/3/plugins/advanced-configuration) for details: + +```json +{ + "android": { + "min_version": 29 + } +} +``` + +## Checklist + +Before submitting your plugin to the [NativePHP Plugin Marketplace](https://nativephp.com/plugins), verify: + +**Required for approval:** + +- [ ] LICENSE, LICENSE.md, or LICENSE.txt file in your repository +- [ ] At least one GitHub release or tag +- [ ] GitHub webhook configured (automatic or manual) +- [ ] Support channel provided (email or URL) + +**Automated checks:** + +- [ ] iOS native code in `resources/ios/Sources/` +- [ ] Android native code in `resources/android/src/` +- [ ] JavaScript library in `resources/js/` +- [ ] `nativephp/mobile` required in `composer.json` +- [ ] iOS `min_version` set in `nativephp.json` +- [ ] Android `min_version` set in `nativephp.json` + +**Documentation & quality:** + +- [ ] README documents installation, PHP usage, and JS usage with complete examples +- [ ] README documents all public methods, events, and required permissions +- [ ] `php artisan native:plugin:validate` passes with zero errors +- [ ] Tested on a physical Android device +- [ ] Tested on a physical iOS device (if iOS is supported) +- [ ] Tested with Livewire v3 and v4 +- [ ] Tested with Inertia + Vue +- [ ] Tested with Inertia + React +- [ ] Boost guidelines are included (`php artisan native:plugin:boost`) +- [ ] TestFlight and/or Google Play testing track link provided +- [ ] All secrets and environment variables are documented +- [ ] Changelog is maintained for version history + +## Official Plugins & Dev Kit + +The official NativePHP plugins follow all of these best practices and serve as reference implementations. Browse +them on the [Plugin Marketplace](https://nativephp.com/plugins) for examples of well-structured, well-documented +plugins. + +Need help building your plugin to these standards? The [Plugin Dev Kit](/products/plugin-dev-kit) generates +production-ready plugins with proper structure, documentation, and Boost guidelines built in. + +[Get the Plugin Dev Kit →](/products/plugin-dev-kit) \ No newline at end of file diff --git a/resources/views/docs/mobile/3/plugins/bridge-functions.md b/resources/views/docs/mobile/3/plugins/bridge-functions.md new file mode 100644 index 00000000..5de9df57 --- /dev/null +++ b/resources/views/docs/mobile/3/plugins/bridge-functions.md @@ -0,0 +1,140 @@ +--- +title: Bridge Functions +order: 400 +--- + +## How Bridge Functions Work + +Bridge functions are the connection between your PHP code and native platform code. When you call a method like +`MyPlugin::doSomething()`, NativePHP routes that to your Swift or Kotlin implementation running on the device. + +The flow: +1. PHP calls `nativephp_call('MyPlugin.DoSomething', $params)` +2. The native bridge locates the registered function +3. Your native code executes and returns a response +4. PHP receives the result + +## Declaring Bridge Functions + +In your `nativephp.json`, declare each function with its platform implementations: + +```json +{ + "bridge_functions": [ + { + "name": "MyPlugin.DoSomething", + "ios": "MyPluginFunctions.DoSomething", + "android": "com.myvendor.plugins.myplugin.MyPluginFunctions.DoSomething", + "description": "Does something useful" + } + ] +} +``` + +The `name` is what PHP uses. The platform-specific values point to your native class and method. + +### Naming Convention + +- **`name`** — A unique identifier like `MyPlugin.DoSomething`. This is what PHP code uses. +- **`ios`** — Swift enum/class path: `EnumName.ClassName` +- **`android`** — Full Kotlin class path including your vendor package (e.g., `com.myvendor.plugins.myplugin.ClassName`) + +## Swift Implementation (iOS) + +Create your functions in `resources/ios/Sources/`: + +```swift +import Foundation + +enum MyPluginFunctions { + + class DoSomething: BridgeFunction { + func execute(parameters: [String: Any]) throws -> [String: Any] { + let option = parameters["option"] as? String ?? "" + + // Do your native work here + + return BridgeResponse.success(data: [ + "result": "completed", + "option": option + ]) + } + } +} +``` + +Key points: +- Implement the `BridgeFunction` protocol +- Parameters come as a dictionary +- Return using `BridgeResponse.success()` or `BridgeResponse.error()` + +## Kotlin Implementation (Android) + +Create your functions in `resources/android/src/`. Use your own vendor-namespaced package: + +```kotlin +package com.myvendor.plugins.myplugin + +import com.nativephp.mobile.bridge.BridgeFunction +import com.nativephp.mobile.bridge.BridgeResponse + +object MyPluginFunctions { + + class DoSomething : BridgeFunction { + override fun execute(parameters: Map): Map { + val option = parameters["option"] as? String ?: "" + + // Do your native work here + + return BridgeResponse.success(mapOf( + "result" to "completed", + "option" to option + )) + } + } +} +``` + +The package declaration determines where your file is placed during compilation. Using `com.myvendor.plugins.myplugin` ensures +your code is isolated from other plugins and the core NativePHP code. + +## Calling from PHP + +Create a facade method that calls your bridge function: + +```php +class MyPlugin +{ + public function doSomething(array $options = []): mixed + { + if (function_exists('nativephp_call')) { + $result = nativephp_call('MyPlugin.DoSomething', json_encode($options)); + + return json_decode($result)?->data; + } + + return null; + } +} +``` + +## Error Handling + +Return errors from native code using `BridgeResponse.error()`: + +```swift +// Swift +return BridgeResponse.error(message: "Something went wrong") +``` + +```kotlin +// Kotlin +return BridgeResponse.error("Something went wrong") +``` + +The error message is available in PHP through the response. + +## Official Plugins & Dev Kit + +Need native functionality without writing Kotlin or Swift? Browse ready-made plugins or get the Dev Kit to build your own. +[Visit the NativePHP Plugin Marketplace →](https://nativephp.com/plugins) diff --git a/resources/views/docs/mobile/3/plugins/core/_index.md b/resources/views/docs/mobile/3/plugins/core/_index.md new file mode 100644 index 00000000..00b3c8ea --- /dev/null +++ b/resources/views/docs/mobile/3/plugins/core/_index.md @@ -0,0 +1,4 @@ +--- +title: Core Plugins +order: 900 +--- diff --git a/resources/views/docs/mobile/3/plugins/core/biometrics.md b/resources/views/docs/mobile/3/plugins/core/biometrics.md new file mode 100644 index 00000000..90d4d0b5 --- /dev/null +++ b/resources/views/docs/mobile/3/plugins/core/biometrics.md @@ -0,0 +1,4 @@ +--- +title: Biometrics +order: 100 +--- diff --git a/resources/views/docs/mobile/3/plugins/core/browser.md b/resources/views/docs/mobile/3/plugins/core/browser.md new file mode 100644 index 00000000..b4a7af4e --- /dev/null +++ b/resources/views/docs/mobile/3/plugins/core/browser.md @@ -0,0 +1,4 @@ +--- +title: Browser +order: 200 +--- diff --git a/resources/views/docs/mobile/3/plugins/core/camera.md b/resources/views/docs/mobile/3/plugins/core/camera.md new file mode 100644 index 00000000..b865c1d6 --- /dev/null +++ b/resources/views/docs/mobile/3/plugins/core/camera.md @@ -0,0 +1,4 @@ +--- +title: Camera +order: 300 +--- diff --git a/resources/views/docs/mobile/3/plugins/core/device.md b/resources/views/docs/mobile/3/plugins/core/device.md new file mode 100644 index 00000000..8ed74240 --- /dev/null +++ b/resources/views/docs/mobile/3/plugins/core/device.md @@ -0,0 +1,4 @@ +--- +title: Device +order: 400 +--- diff --git a/resources/views/docs/mobile/3/plugins/core/dialog.md b/resources/views/docs/mobile/3/plugins/core/dialog.md new file mode 100644 index 00000000..788aec6e --- /dev/null +++ b/resources/views/docs/mobile/3/plugins/core/dialog.md @@ -0,0 +1,4 @@ +--- +title: Dialog +order: 500 +--- diff --git a/resources/views/docs/mobile/3/plugins/core/file.md b/resources/views/docs/mobile/3/plugins/core/file.md new file mode 100644 index 00000000..9136481d --- /dev/null +++ b/resources/views/docs/mobile/3/plugins/core/file.md @@ -0,0 +1,4 @@ +--- +title: File +order: 600 +--- diff --git a/resources/views/docs/mobile/3/plugins/core/firebase.md b/resources/views/docs/mobile/3/plugins/core/firebase.md new file mode 100644 index 00000000..146d6479 --- /dev/null +++ b/resources/views/docs/mobile/3/plugins/core/firebase.md @@ -0,0 +1,4 @@ +--- +title: Firebase (Push Notifications) +order: 650 +--- diff --git a/resources/views/docs/mobile/3/plugins/core/geolocation.md b/resources/views/docs/mobile/3/plugins/core/geolocation.md new file mode 100644 index 00000000..1aebb4c4 --- /dev/null +++ b/resources/views/docs/mobile/3/plugins/core/geolocation.md @@ -0,0 +1,4 @@ +--- +title: Geolocation +order: 700 +--- diff --git a/resources/views/docs/mobile/3/plugins/core/microphone.md b/resources/views/docs/mobile/3/plugins/core/microphone.md new file mode 100644 index 00000000..da177ff6 --- /dev/null +++ b/resources/views/docs/mobile/3/plugins/core/microphone.md @@ -0,0 +1,4 @@ +--- +title: Microphone +order: 900 +--- diff --git a/resources/views/docs/mobile/3/plugins/core/network.md b/resources/views/docs/mobile/3/plugins/core/network.md new file mode 100644 index 00000000..d15b6462 --- /dev/null +++ b/resources/views/docs/mobile/3/plugins/core/network.md @@ -0,0 +1,4 @@ +--- +title: Network +order: 1000 +--- diff --git a/resources/views/docs/mobile/3/plugins/core/scanner.md b/resources/views/docs/mobile/3/plugins/core/scanner.md new file mode 100644 index 00000000..d393b24d --- /dev/null +++ b/resources/views/docs/mobile/3/plugins/core/scanner.md @@ -0,0 +1,4 @@ +--- +title: Scanner +order: 1200 +--- diff --git a/resources/views/docs/mobile/3/plugins/core/secure-storage.md b/resources/views/docs/mobile/3/plugins/core/secure-storage.md new file mode 100644 index 00000000..b63e58ce --- /dev/null +++ b/resources/views/docs/mobile/3/plugins/core/secure-storage.md @@ -0,0 +1,4 @@ +--- +title: SecureStorage +order: 1300 +--- diff --git a/resources/views/docs/mobile/3/plugins/core/share.md b/resources/views/docs/mobile/3/plugins/core/share.md new file mode 100644 index 00000000..9c63624d --- /dev/null +++ b/resources/views/docs/mobile/3/plugins/core/share.md @@ -0,0 +1,4 @@ +--- +title: Share +order: 1400 +--- diff --git a/resources/views/docs/mobile/3/plugins/core/system.md b/resources/views/docs/mobile/3/plugins/core/system.md new file mode 100644 index 00000000..44f7e38d --- /dev/null +++ b/resources/views/docs/mobile/3/plugins/core/system.md @@ -0,0 +1,4 @@ +--- +title: System +order: 1500 +--- diff --git a/resources/views/docs/mobile/3/plugins/creating-plugins.md b/resources/views/docs/mobile/3/plugins/creating-plugins.md new file mode 100644 index 00000000..216a8208 --- /dev/null +++ b/resources/views/docs/mobile/3/plugins/creating-plugins.md @@ -0,0 +1,376 @@ +--- +title: Creating Plugins +order: 300 +--- + +## Scaffolding a Plugin + +The quickest way to create a plugin is with the interactive scaffolding command: + +```shell +php artisan native:plugin:create +``` + +This walks you through naming, namespace selection, and feature options, then generates a complete plugin structure. + + + +## Plugin Structure + +A plugin follows a standard layout: + +``` +my-plugin/ +├── composer.json # Package metadata, type must be "nativephp-plugin" +├── nativephp.json # Plugin manifest +├── src/ +│ ├── MyPluginServiceProvider.php +│ ├── MyPlugin.php # Main class +│ ├── Facades/ +│ │ └── MyPlugin.php +│ ├── Events/ +│ │ └── SomethingHappened.php +│ └── Commands/ # Lifecycle hook commands +├── resources/ +│ ├── android/src/ # Kotlin bridge functions +│ ├── ios/Sources/ # Swift bridge functions +│ └── js/ # JavaScript library stubs +``` + +## Android Package Naming + +Android/Kotlin code must declare a package at the top of each file. Use your own vendor-namespaced package to avoid +conflicts: + +```kotlin +// resources/android/src/MyPluginFunctions.kt +package com.myvendor.plugins.myplugin + +import com.nativephp.mobile.bridge.BridgeFunction +import com.nativephp.mobile.bridge.BridgeResponse + +object MyPluginFunctions { + class DoSomething : BridgeFunction { + override fun execute(parameters: Map): Map { + return BridgeResponse.success(mapOf("status" to "done")) + } + } +} +``` + +The compiler places files based on their package declaration, so `package com.myvendor.plugins.myplugin` results in the file +being placed at `app/src/main/java/com/myvendor/plugins/myplugin/MyPluginFunctions.kt`. + + + +Reference the full package path in your manifest's bridge functions: + +```json +{ + "bridge_functions": [{ + "name": "MyPlugin.DoSomething", + "android": "com.myvendor.plugins.myplugin.MyPluginFunctions.DoSomething" + }] +} +``` + +## The composer.json + +Your `composer.json` must specify the plugin type: + +```json +{ + "name": "vendor/my-plugin", + "type": "nativephp-plugin", + "extra": { + "laravel": { + "providers": ["Vendor\\MyPlugin\\MyPluginServiceProvider"] + }, + "nativephp": { + "manifest": "nativephp.json" + } + } +} +``` + +The `type: nativephp-plugin` tells NativePHP to look for native code in this package. + +## The nativephp.json Manifest + +The manifest declares native-specific configuration for your plugin. Package metadata (`name`, `version`, `description`, +`service_provider`) comes from your `composer.json` — don't duplicate it here. + +```json +{ + "namespace": "MyPlugin", + "bridge_functions": [ + { + "name": "MyPlugin.DoSomething", + "ios": "MyPluginFunctions.DoSomething", + "android": "com.nativephp.plugins.myplugin.MyPluginFunctions.DoSomething" + } + ], + "events": ["Vendor\\MyPlugin\\Events\\SomethingHappened"], + "android": { + "permissions": ["android.permission.CAMERA"], + "dependencies": { + "implementation": ["com.google.mlkit:barcode-scanning:17.2.0"] + } + }, + "ios": { + "info_plist": { + "NSCameraUsageDescription": "Camera is used for scanning" + }, + "dependencies": { + "pods": [{"name": "GoogleMLKit/BarcodeScanning", "version": "~> 4.0"}] + } + } +} +``` + +### Manifest Fields + +| Field | Required | Description | +|-------|----------|-------------| +| `namespace` | Yes | Namespace for the plugin (used for code generation and directory structure) | +| `bridge_functions` | No | Array of native function mappings | +| `events` | No | Event classes the plugin dispatches | +| `android.permissions` | No | Android permission strings | +| `android.features` | No | Android uses-feature declarations | +| `android.dependencies` | No | Gradle dependencies | +| `android.repositories` | No | Custom Maven repositories | +| `android.activities` | No | Activities to register in manifest | +| `android.services` | No | Services to register in manifest | +| `android.receivers` | No | Broadcast receivers to register | +| `android.providers` | No | Content providers to register | +| `android.meta_data` | No | Application meta-data entries | +| `android.min_version` | No | Minimum Android SDK version required | +| `android.init_function` | No | Native function to call during app initialization | +| `ios.info_plist` | No | Info.plist entries (permissions, API keys) | +| `ios.dependencies` | No | Swift packages and CocoaPods | +| `ios.background_modes` | No | UIBackgroundModes values | +| `ios.entitlements` | No | App entitlements | +| `ios.capabilities` | No | iOS capabilities for Xcode project | +| `ios.min_version` | No | Minimum iOS version required | +| `ios.init_function` | No | Native function to call during app initialization | +| `assets` | No | Declarative asset copying | +| `hooks` | No | Lifecycle hook commands | +| `secrets` | No | Required environment variables | + +See [Advanced Configuration](advanced-configuration) for detailed documentation on each field. + +## Local Development + +During development, add your plugin to your app's `composer.json` as a path repository: + +```json +{ + "repositories": [ + {"type": "path", "url": "../packages/my-plugin"} + ] +} +``` + +Then require it: + +```shell +composer require vendor/my-plugin +``` + +Changes to your plugin's PHP code are picked up immediately. Changes to native code require a rebuild with +`php artisan native:run`. + +When testing significant changes to your plugin's native code or manifest, you may need to force a fresh install of +the native projects: + +```shell +php artisan native:install --force +``` + +This ensures the native projects are rebuilt from scratch with your latest plugin configuration. + + + +## Registering Plugins + +After installing a plugin with Composer, you need to register it so it gets compiled into your native builds. + +### First Time Setup + +Publish the NativeServiceProvider: + +```shell +php artisan vendor:publish --tag=nativephp-plugins-provider +``` + +This creates `app/Providers/NativeServiceProvider.php`. + +### Register a Plugin + +```shell +php artisan native:plugin:register vendor/plugin-name +``` + +This automatically adds the plugin's service provider to your `plugins()` array: + +```php +public function plugins(): array +{ + return [ + \Vendor\PluginName\PluginNameServiceProvider::class, + ]; +} +``` + +### List Plugins + +```shell +# Show registered plugins +php artisan native:plugin:list + +# Show all installed plugins (including unregistered) +php artisan native:plugin:list --all +``` + +### Remove a Plugin + +```shell +php artisan native:plugin:register vendor/plugin-name --remove +``` + + + +## JavaScript Library + +Plugins can provide a JavaScript library for SPA frameworks. The scaffolding creates a stub in `resources/js/`: + +```js +// resources/js/myPlugin.js +const baseUrl = '/_native/api/call'; + +async function bridgeCall(method, params = {}) { + const response = await fetch(baseUrl, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ method, params }) + }); + return response.json(); +} + +export async function doSomething(options = {}) { + return bridgeCall('MyPlugin.DoSomething', options); +} +``` + +Users can then import your functions directly in Vue, React, or vanilla JS. + +## NativePHP Plugin Development Kit + + + +If you're using [Claude Code](https://claude.com/claude-code), the Plugin Development Kit supercharges your workflow +with specialized agents trained on NativePHP's architecture. + +### What's Included + +- **Kotlin/Android Expert Agent** — Writes correct bridge functions, handles Android lifecycles, configures Gradle +- **Swift/iOS Expert Agent** — Implements iOS bridge functions, manages Info.plist, configures SPM/CocoaPods +- **Plugin Architect Agent** — Designs plugin structure, manifest configuration, and Laravel integration +- **Interactive Commands** — `/create-nativephp-plugin` scaffolds complete plugins from a description +- **Validation Tools** — `/validate-nativephp-plugin` catches errors before you build + +### Why It's Worth It + +Writing native mobile code is hard. These agents understand: + +- NativePHP's bridge function patterns and response formats +- Platform-specific APIs and how to expose them to PHP +- Permission declarations, entitlements, and manifest configuration +- Event dispatching from native code to Livewire components +- Dependency management across Gradle, CocoaPods, and SPM + +Instead of learning two new languages and their ecosystems, describe what you need and let the agents handle the +implementation details. + +[Get the Plugin Dev Kit →](/products/plugin-dev-kit) + +## AI Development Tools + +NativePHP includes built-in commands for AI-assisted plugin development. + +### Install Development Agents + +Install specialized AI agents for plugin development: + +```shell +php artisan native:plugin:install-agent +``` + +This copies agent definition files to your project's `.claude/agents/` directory. Available agents include: + +- **kotlin-android-expert** — Deep Android/Kotlin native development +- **swift-ios-expert** — Deep iOS/Swift native development +- **js-bridge-expert** — JavaScript/TypeScript client integration +- **plugin-writer** — General plugin scaffolding and structure +- **plugin-docs-writer** — Documentation and Boost guidelines + +Use `--all` to install all agents without prompting, or `--force` to overwrite existing files. + +### Create Boost Guidelines + +If you're using [Boost](https://laravel.com/ai/boost), create AI guidelines for your plugin: + +```shell +php artisan native:plugin:boost +``` + +This generates a `resources/boost/guidelines/core.blade.php` file in your plugin that documents: + +- How to use your plugin's facade +- Available methods and their descriptions +- Events and how to listen for them +- JavaScript usage examples + +When users install your plugin and run `php artisan boost:install`, these guidelines are automatically loaded, +helping AI assistants understand how to use your plugin correctly. + +## Ready to Build? + +You now have everything you need to create NativePHP plugins. For most developers, the +[Plugin Development Kit](/products/plugin-dev-kit) is the fastest path from idea to working plugin — it +handles the native code complexity so you can focus on what your plugin does, not how to write Kotlin and Swift. diff --git a/resources/views/docs/mobile/3/plugins/events.md b/resources/views/docs/mobile/3/plugins/events.md new file mode 100644 index 00000000..1b9eb9ce --- /dev/null +++ b/resources/views/docs/mobile/3/plugins/events.md @@ -0,0 +1,109 @@ +--- +title: Events +order: 500 +--- + +## Dispatching Events from Native Code + +Many native operations are asynchronous — ML inference, sensor readings, background tasks. Your native code needs a +way to send results back to PHP when they're ready. That's where events come in. + +Events are dispatched from native code and received by your Livewire components. + +## Declaring Events + +Add your event classes to the manifest: + +```json +{ + "events": [ + "Vendor\\MyPlugin\\Events\\ProcessingComplete", + "Vendor\\MyPlugin\\Events\\ProcessingError" + ] +} +``` + +## Creating Event Classes + +Events are simple PHP classes: + +```php +namespace Vendor\MyPlugin\Events; + +use Illuminate\Foundation\Events\Dispatchable; +use Illuminate\Queue\SerializesModels; + +class ProcessingComplete +{ + use Dispatchable, SerializesModels; + + public function __construct( + public string $result, + public ?string $id = null + ) {} +} +``` + + + +## Swift Event Dispatching + +Dispatch events using `LaravelBridge.shared.send`: + +```swift +// Build your payload +let payload: [String: Any] = [ + "result": processedData, + "id": requestId +] + +// Dispatch to PHP +LaravelBridge.shared.send?( + "Vendor\\MyPlugin\\Events\\ProcessingComplete", + payload +) +``` + +This runs synchronously on the main thread, so wrap in `DispatchQueue.main.async` if needed. + +## Kotlin Event Dispatching + +Dispatch events using `NativeActionCoordinator.dispatchEvent`: + +```kotlin +import android.os.Handler +import android.os.Looper + +// Build your payload +val payload = JSONObject().apply { + put("result", processedData) + put("id", requestId) +} + +// Must dispatch on main thread +Handler(Looper.getMainLooper()).post { + NativeActionCoordinator.dispatchEvent( + activity, + "Vendor\\MyPlugin\\Events\\ProcessingComplete", + payload.toString() + ) +} +``` + + + +## Official Plugins & Dev Kit + +Skip the complexity — browse ready-made plugins or get the Dev Kit to build your own. +[Visit the NativePHP Plugin Marketplace →](https://nativephp.com/plugins) diff --git a/resources/views/docs/mobile/3/plugins/introduction.md b/resources/views/docs/mobile/3/plugins/introduction.md new file mode 100644 index 00000000..111120df --- /dev/null +++ b/resources/views/docs/mobile/3/plugins/introduction.md @@ -0,0 +1,69 @@ +--- +title: Introduction +order: 100 +--- + +## What are Plugins? + +Plugins extend NativePHP for Mobile with native functionality. Need on-device ML, +Bluetooth, or a custom hardware integration? Plugins let you add these capabilities without forking the core package. + +A plugin is a Composer package that bundles: +- **PHP code** — Facades, events, and service providers you use in Laravel +- **Native code** — Swift (iOS) and Kotlin (Android) implementations +- **A manifest** — Declares what the plugin provides and needs + +When you build your app, NativePHP compiles registered plugins' native code into your app. + +## Why Plugins? + +All native functionality in NativePHP Mobile comes through plugins — including official plugins for camera, biometrics, +push notifications, and more. This architecture means: + +- **Official plugins** provide core functionality and serve as reference implementations +- **Community plugins** extend the platform with new capabilities +- **Your own plugins** let you integrate proprietary SDKs or custom native code + +Install a plugin and its native features become available to your PHP code through a simple facade. + +## What Plugins Can Do + +Plugins have full access to native platform capabilities: + +- **Bridge functions** — Call Swift/Kotlin code from PHP and get results back +- **Events** — Dispatch events from native code to your Livewire components +- **Permissions** — Declare required permissions (camera, location, etc.) +- **Dependencies** — Include native libraries via Gradle, CocoaPods, or Swift Package Manager +- **Custom repositories** — Use private Maven repos for enterprise SDKs +- **Android components** — Register Activities, Services, Receivers, and Content Providers +- **Assets** — Bundle ML models, configuration files, and other resources +- **Lifecycle hooks** — Run code at build time to download models, validate config, etc. +- **Secrets** — Declare required environment variables with validation + +## Plugin Architecture + +Plugins follow the same patterns as NativePHP's core: + +```php +use Vendor\MyPlugin\Facades\MyPlugin; + +// Call native functions +MyPlugin::doSomething(); + +// Listen for events +#[OnNative(MyPlugin\Events\SomethingHappened::class)] +public function handleResult($data) +{ + // Handle it +} +``` + +The native code runs on-device, communicates with your PHP through the bridge, and dispatches events back to your +Livewire components. It's the same model you're already using. + +## Getting Started + +Ready to build your own plugin? Check out [Creating Plugins](./creating-plugins) for the full guide. + +Or browse the [NativePHP Plugin Marketplace](https://nativephp.com/plugins) for ready-made plugins and the Dev Kit +to build your own. diff --git a/resources/views/docs/mobile/3/plugins/lifecycle-hooks.md b/resources/views/docs/mobile/3/plugins/lifecycle-hooks.md new file mode 100644 index 00000000..315c0b13 --- /dev/null +++ b/resources/views/docs/mobile/3/plugins/lifecycle-hooks.md @@ -0,0 +1,139 @@ +--- +title: Lifecycle Hooks +order: 600 +--- + +## What are Lifecycle Hooks? + +Hooks let your plugin run code at specific points during the build process. Need to download an ML model before +compilation? Copy assets to the right platform directory? Run validation? Hooks handle these scenarios. + + + +## Available Hooks + +| Hook | When it Runs | +|------|--------------| +| `pre_compile` | Before native code compilation | +| `post_compile` | After compilation, before build | +| `copy_assets` | When copying assets to native projects (runs after declarative asset copying) | +| `post_build` | After a successful build | + +## Creating Hook Commands + +Generate a hook command with the scaffolding tool: + +```shell +php artisan native:plugin:make-hook +``` + +This walks you through selecting your plugin and which hooks to create. It generates the command class, updates +your manifest, and registers the command in your service provider. + +## Hook Command Structure + +Hook commands extend `NativePluginHookCommand`: + +```php +use Native\Mobile\Plugins\Commands\NativePluginHookCommand; + +class CopyAssetsCommand extends NativePluginHookCommand +{ + protected $signature = 'nativephp:my-plugin:copy-assets'; + + public function handle(): int + { + if ($this->isAndroid()) { + $this->copyToAndroidAssets('models/model.tflite', 'models/model.tflite'); + } + + if ($this->isIos()) { + $this->copyToIosBundle('models/model.mlmodel', 'models/model.mlmodel'); + } + + return self::SUCCESS; + } +} +``` + +## Available Helpers + +The base command provides helpers for common tasks: + +**Platform Detection:** +- `$this->platform()` — Returns `'ios'` or `'android'` +- `$this->isIos()`, `$this->isAndroid()` — Boolean checks + +**Paths:** +- `$this->buildPath()` — Path to the native project being built +- `$this->pluginPath()` — Path to your plugin package +- `$this->appId()` — The app's bundle ID (e.g., `com.example.app`) + +**File Operations:** +- `$this->copyToAndroidAssets($src, $dest)` — Copy to Android assets +- `$this->copyToIosBundle($src, $dest)` — Copy to iOS bundle +- `$this->downloadIfMissing($url, $dest)` — Download a file if it doesn't exist +- `$this->unzip($zipPath, $extractTo)` — Extract a zip file + +## Declaring Hooks in the Manifest + +Add hooks to your `nativephp.json`: + +```json +{ + "hooks": { + "copy_assets": "nativephp:my-plugin:copy-assets", + "pre_compile": "nativephp:my-plugin:pre-compile" + } +} +``` + +The value is your Artisan command signature. + +## Example: Downloading an ML Model + +```php +public function handle(): int +{ + $modelPath = $this->pluginPath() . '/resources/models/model.tflite'; + + // Download if not cached locally + $this->downloadIfMissing( + 'https://example.com/models/v2/model.tflite', + $modelPath + ); + + // Copy to the appropriate platform location + if ($this->isAndroid()) { + $this->copyToAndroidAssets('models/model.tflite', 'models/model.tflite'); + $this->info('Model copied to Android assets'); + } + + if ($this->isIos()) { + $this->copyToIosBundle('models/model.tflite', 'models/model.tflite'); + $this->info('Model copied to iOS bundle'); + } + + return self::SUCCESS; +} +``` + + + +## Official Plugins & Dev Kit + +Browse ready-made plugins or get the Dev Kit to build your own. +[Visit the NativePHP Plugin Marketplace →](https://nativephp.com/plugins) diff --git a/resources/views/docs/mobile/3/plugins/permissions-dependencies.md b/resources/views/docs/mobile/3/plugins/permissions-dependencies.md new file mode 100644 index 00000000..30f0ccea --- /dev/null +++ b/resources/views/docs/mobile/3/plugins/permissions-dependencies.md @@ -0,0 +1,258 @@ +--- +title: Permissions & Dependencies +order: 700 +--- + +## Platform Configuration + +Platform-specific settings are grouped under `android` and `ios` keys in your manifest. This keeps all configuration for +each platform together: + +```json +{ + "android": { + "permissions": [...], + "dependencies": {...}, + "repositories": [...], + "activities": [...], + "services": [...] + }, + "ios": { + "info_plist": {...}, + "dependencies": {...} + } +} +``` + +## Permissions + +### Android Permissions + +List Android permissions as strings under `android.permissions`: + +```json +{ + "android": { + "permissions": [ + "android.permission.CAMERA", + "android.permission.RECORD_AUDIO", + "android.permission.ACCESS_FINE_LOCATION" + ] + } +} +``` + +These are added to the app's `AndroidManifest.xml` at build time. + +See the Android Manifest.permission reference +for a complete list of available permissions. + +### iOS Info.plist Entries + +iOS requires usage descriptions for each permission, plus any API keys or configuration tokens your plugin needs. Provide +these as key-value pairs under `ios.info_plist`: + +```json +{ + "ios": { + "info_plist": { + "NSCameraUsageDescription": "This app uses the camera for scanning", + "NSMicrophoneUsageDescription": "This app records audio for transcription", + "NSLocationWhenInUseUsageDescription": "This app needs your location", + "MBXAccessToken": "${MAPBOX_ACCESS_TOKEN}" + } + } +} +``` + +These are merged into the app's `Info.plist`. You can include: +- Permission usage descriptions (`NS*UsageDescription` keys) +- API tokens and configuration keys +- Any other Info.plist entries your plugin requires + +See the Apple Information Property List reference +for all available keys. + +Use `${ENV_VAR}` placeholders for sensitive values like API tokens. + + + +## Dependencies + +### Android Dependencies + +Add Gradle dependencies under `android.dependencies`: + +```json +{ + "android": { + "dependencies": { + "implementation": [ + "com.google.mlkit:face-detection:16.1.5", + "org.tensorflow:tensorflow-lite:2.13.0" + ] + } + } +} +``` + +These are added to the app's `build.gradle.kts` during compilation. You can use any Gradle dependency type: + +- `implementation` — Standard dependency +- `api` — Exposed to consumers +- `compileOnly` — Compile-time only +- `runtimeOnly` — Runtime only + +### iOS Dependencies + +#### CocoaPods + +For CocoaPods dependencies, use the `pods` array: + +```json +{ + "ios": { + "dependencies": { + "pods": [ + {"name": "GoogleMLKit/FaceDetection", "version": "~> 4.0"}, + {"name": "TensorFlowLiteSwift", "version": "~> 2.13"} + ] + } + } +} +``` + +Each pod object accepts: +- `name` — The pod name (required) +- `version` — Version constraint (optional, e.g., `~> 4.0`, `>= 1.0`) + +NativePHP generates a `Podfile` and runs `pod install` during the iOS build process. + +#### Swift Packages + +For Swift Package Manager dependencies: + +```json +{ + "ios": { + "dependencies": { + "swift_packages": [ + { + "url": "https://github.com/example/SomePackage", + "version": "1.0.0" + } + ] + } + } +} +``` + + + +## Custom Repositories + +Some dependencies require private or non-standard Maven repositories (like Mapbox). Add them under +`android.repositories`: + +```json +{ + "android": { + "repositories": [ + { + "url": "https://api.mapbox.com/downloads/v2/releases/maven", + "credentials": { + "username": "mapbox", + "password": "${MAPBOX_DOWNLOADS_TOKEN}" + } + } + ] + } +} +``` + +Repository configuration: +- `url` — The repository URL (required) +- `credentials` — Optional authentication + - `username` — Username or token name + - `password` — Password or token (supports `${ENV_VAR}` placeholders) + +These are added to the app's `settings.gradle.kts`. + + + +## Full Example + +Here's a complete manifest for an ML plugin that uses Mapbox maps: + +```json +{ + "name": "vendor/ml-maps-plugin", + "namespace": "MLMaps", + "android": { + "permissions": [ + "android.permission.CAMERA", + "android.permission.ACCESS_FINE_LOCATION" + ], + "dependencies": { + "implementation": [ + "com.google.mlkit:object-detection:17.0.0", + "com.mapbox.maps:android:11.0.0" + ] + }, + "repositories": [ + { + "url": "https://api.mapbox.com/downloads/v2/releases/maven", + "credentials": { + "username": "mapbox", + "password": "${MAPBOX_DOWNLOADS_TOKEN}" + } + } + ] + }, + "ios": { + "info_plist": { + "NSCameraUsageDescription": "Camera is used for real-time object detection", + "NSLocationWhenInUseUsageDescription": "Location is used to display your position on the map", + "MBXAccessToken": "${MAPBOX_PUBLIC_TOKEN}" + }, + "dependencies": { + "pods": [ + {"name": "MapboxMaps", "version": "~> 11.0"} + ] + } + }, + "secrets": { + "MAPBOX_DOWNLOADS_TOKEN": { + "description": "Mapbox SDK download token from mapbox.com/account/access-tokens", + "required": true + }, + "MAPBOX_PUBLIC_TOKEN": { + "description": "Mapbox public access token for runtime API calls", + "required": true + } + } +} +``` + +## Official Plugins & Dev Kit + +Skip the configuration complexity — browse ready-made plugins or get the Dev Kit to build your own. +[Visit the NativePHP Plugin Marketplace →](https://nativephp.com/plugins) diff --git a/resources/views/docs/mobile/3/plugins/using-plugins.md b/resources/views/docs/mobile/3/plugins/using-plugins.md new file mode 100644 index 00000000..ed9b1d0e --- /dev/null +++ b/resources/views/docs/mobile/3/plugins/using-plugins.md @@ -0,0 +1,119 @@ +--- +title: Using Plugins +order: 200 +--- + +## Installing a Plugin + +Plugins are standard Composer packages. Install them like any other Laravel package: + +```shell +composer require vendor/nativephp-plugin-name +``` + +The plugin's PHP service provider will be auto-discovered by Laravel, but the native code won't be included in builds +until you explicitly register it. + +## Register the Plugin + +For security, plugins must be explicitly registered before their native code is compiled into your app. This prevents +transitive dependencies from automatically including native code without your consent. + +First, ensure you've published the NativeServiceProvider: + +```shell +php artisan vendor:publish --tag=nativephp-plugins-provider +``` + +Then register the plugin: + +```shell +php artisan native:plugin:register vendor/nativephp-plugin-name +``` + +This adds the plugin to your `app/Providers/NativeServiceProvider.php` file. + +## Verify Installation + +Check that NativePHP sees your plugin: + +```shell +php artisan native:plugin:list +``` + +You'll see the plugin name, version, and what it provides (bridge functions, events, hooks). + +## Rebuild Your App + +After installing a plugin, rebuild to compile its native code: + +```shell +php artisan native:run +``` + +The plugin's Swift and Kotlin code gets compiled into your app automatically. + +## Using Plugin Features + +Each plugin provides its own facade for interacting with native functionality. + +```php +use Vendor\PluginName\Facades\PluginName; + +// Call a native function +$result = PluginName::doSomething(['option' => 'value']); +``` + +## Listening to Plugin Events + +Plugins dispatch events to your Livewire components. Use the `#[OnNative]` attribute to listen for them: + +```php +use Native\Mobile\Attributes\OnNative; +use Vendor\PluginName\Events\SomethingCompleted; + +#[OnNative(SomethingCompleted::class)] +public function handleCompletion($result) +{ + // React to the event +} +``` + + + +## Permissions + +Some plugins require additional permissions. These are declared in the plugin's manifest and automatically merged +into your app's configuration during build. + +If a plugin needs camera access, microphone, or other sensitive permissions, you'll see them listed when you run +`native:plugin:list`. + +## Removing a Plugin + +To completely uninstall a plugin: + +```shell +php artisan native:plugin:uninstall vendor/nativephp-plugin-name +``` + +This command: +- Unregisters the plugin from your `NativeServiceProvider` +- Removes the package via Composer +- Removes the path repository from `composer.json` (if applicable) +- Optionally deletes the plugin source directory (for local path repositories) + +Use `--force` to skip confirmation prompts, or `--keep-files` to preserve the source directory when uninstalling +a local plugin. + +## Official Plugins & Dev Kit + +Find ready-made plugins for common use cases, or get the Dev Kit to build your own. +[Visit the NativePHP Plugin Marketplace →](https://nativephp.com/plugins) diff --git a/resources/views/docs/mobile/3/plugins/validation-testing.md b/resources/views/docs/mobile/3/plugins/validation-testing.md new file mode 100644 index 00000000..6ea09d21 --- /dev/null +++ b/resources/views/docs/mobile/3/plugins/validation-testing.md @@ -0,0 +1,87 @@ +--- +title: Validation & Testing +order: 800 +--- + +## Validating Your Plugin + +Before building, validate your plugin to catch common issues: + +```shell +php artisan native:plugin:validate +``` + +This checks: +- Manifest syntax and required fields +- Bridge function declarations match native code +- Hook commands are registered and exist +- Declared assets are present + +## Common Validation Errors + +**"Bridge function not found in native code"** + +Your manifest declares a function, but the Swift or Kotlin implementation is missing or named differently. Check +that class names and function names match exactly. + +**"Invalid manifest JSON"** + +Your `nativephp.json` has a syntax error. Check for trailing commas, missing quotes, or unclosed brackets. + +**"Hook command not registered"** + +The manifest references an Artisan command that isn't registered in your service provider. Make sure +`native:plugin:make-hook` has updated your service provider, or add it manually. + +## Testing During Development + +### Test PHP Code + +Your PHP facades and event handling work like any Laravel code. Write standard PHPUnit tests: + +```php +public function test_plugin_facade_is_accessible() +{ + $this->assertInstanceOf(MyPlugin::class, app(MyPlugin::class)); +} +``` + +### Test Native Code + +Native code can only be tested by running the app. Use this workflow: + +1. Install your plugin locally via path repository +2. Run `php artisan native:run` +3. Trigger your plugin's functionality in the app +4. Check the console output for errors + + + +## Debugging Tips + +**Plugin not discovered?** +- Verify `composer.json` has `"type": "nativephp-plugin"` +- Run `composer dump-autoload` +- Check `php artisan native:plugin:list` + +**Native function not found at runtime?** +- Rebuild the app after changing native code +- Check the manifest's function names match exactly +- Verify the Kotlin package name is correct + +**Events not firing?** +- Confirm you're dispatching on the main thread +- Check the event class name matches the manifest +- Verify the `#[OnNative]` attribute uses the correct class + +## Official Plugins & Dev Kit + +Skip the debugging — browse ready-made plugins or get the Dev Kit to build your own. +[Visit the NativePHP Plugin Marketplace →](https://nativephp.com/plugins) diff --git a/resources/views/docs/mobile/3/the-basics/_index.md b/resources/views/docs/mobile/3/the-basics/_index.md new file mode 100644 index 00000000..62b29f06 --- /dev/null +++ b/resources/views/docs/mobile/3/the-basics/_index.md @@ -0,0 +1,4 @@ +--- +title: The Basics +order: 20 +--- diff --git a/resources/views/docs/mobile/3/the-basics/app-icon.md b/resources/views/docs/mobile/3/the-basics/app-icon.md new file mode 100644 index 00000000..5b1bfdbb --- /dev/null +++ b/resources/views/docs/mobile/3/the-basics/app-icon.md @@ -0,0 +1,25 @@ +--- +title: App Icons +order: 300 +--- + +NativePHP makes it easy to apply a custom app icon to your iOS and Android apps. + +## Supply your icon + +Place a single high-resolution icon file at: `public/icon.png`. + +### Requirements +- Format: PNG +- Size: 1024 × 1024 pixels +- Background: Must not contain any transparencies. +- GD PHP extension must be enabled, ensure it has enough memory (~2GB should be enough) + +This image will be automatically resized for all Android densities and used as the base iOS app icon. +You must have the GD extension installed in your development machine's PHP environment for this to work. + + diff --git a/resources/views/docs/mobile/3/the-basics/assets.md b/resources/views/docs/mobile/3/the-basics/assets.md new file mode 100644 index 00000000..40eb46fe --- /dev/null +++ b/resources/views/docs/mobile/3/the-basics/assets.md @@ -0,0 +1,53 @@ +--- +title: Assets +order: 500 +--- + +## Compiling CSS and JavaScript + +If you are using React, Vue or another JavaScript library, or Tailwind CSS, tools that requires your frontend to be +built by build tooling like Vite, you will need to run your build process _before_ compiling the native application. + +For example, if you're using Vite with NPM to build a React application that is using Tailwind, to ensure that your +latest styles and JavaScript are included, always run `npm run build` before running `php artisan native:run`. + +## Other files + +NativePHP will include all files from the root of your Laravel application. So you can store any files that you wish to +make available to your application wherever makes the most sense for you. + + + +## Public files + +If your application receives files from the user — either generated by your application or imported from elsewhere, +such as their photo gallery — you may wish to render these to the web view so they can be played or displayed as part +of your app. + +For this to work, they must be in the `public` directory. But you may also wish to persist these files across app +updates. For this reason, they are stored outside of the `public` directory in a persistent storage location and this +folder is symlinked to `public/storage`. + +You can then access these files using the `mobile_public` disk: + +```php +Storage::disk('mobile_public')->url('user_content.jpg'); +``` + + diff --git a/resources/views/docs/mobile/3/the-basics/events.md b/resources/views/docs/mobile/3/the-basics/events.md new file mode 100644 index 00000000..de179ef1 --- /dev/null +++ b/resources/views/docs/mobile/3/the-basics/events.md @@ -0,0 +1,215 @@ +--- +title: Events +order: 200 +--- + +## Overview + +Many native mobile operations take time to complete and await user interaction. PHP isn't really set up to handle this +sort of asynchronous behaviour; it is built to do its work, send a response and move on as quickly as possible. + +NativePHP for Mobile smooths over this disparity between the different paradigms using a simple event system that +handles completion of asynchronous methods using a webhook-/websocket-style approach to notify your Laravel app. + +## Understanding Async vs Sync + +Not all actions are async. Some methods run immediately, and in some cases return a result straight away. + +Here are a few of the **synchronous** APIs: + +```php +Haptics::vibrate(); +System::flashlight(); +Dialog::toast('Hello!'); +``` +Asynchronous actions trigger operations that may complete later. These return immediately, usually with a `bool` or +`void`, allowing PHP's execution to finish. In many of these cases, the user interacts directly with a native component. + +When the user has completed their task and the native UI is dismissed, the app will emit an event that represents the +outcome. + +The _type_ (the class name) of the event and its properties all help you to choose the appropriate action to take in +response to the outcome. + +```php +// These trigger operations and fire events when complete +Camera::getPhoto(); // → PhotoTaken event +Biometrics::prompt(); // → Completed event +PushNotifications::enroll(); // → TokenGenerated event +``` + +## Basic Event Structure + +All events are standard [Laravel Event classes](https://laravel.com/docs/12.x/events#defining-events). The public +properties of the events contain the pertinent data coming from the native app side. + +## Custom Events + +Almost every function that emits events can be customized to emit events that you define. This is a great way to ensure +only the relevant listeners are executed when these events are fired. + +Events are simple PHP classes that receive some parameters. You can extend existing events for convenience. + +Let's see a complete example... + +### Define your custom event class + +```php +namespace App\Events; + +use Native\Mobile\Events\Alert\ButtonPressed; + +class MyButtonPressedEvent extends ButtonPressed +{} +``` + +### Pass this class to an async function + +```php +use App\Events\MyButtonPressedEvent; + +Dialog::alert('Warning!', 'You are about to delete everything! Are you sure?', [ + 'Cancel', + 'Do it!' + ]) + ->event(MyButtonPressedEvent::class) +``` + +### Handle the event + +Here's an example handling a custom event class inside a Livewire component. + +```php +use App\Events\MyButtonPressed; +use Native\Mobile\Attributes\OnNative; + +#[OnNative(MyButtonPressed::class)] +public function buttonPressed() +{ + // Do stuff +} +``` + +## Event Handling + +All asynchronous methods follow the same pattern: + +1. **Call the method** to trigger the operation. +2. **Listen for the appropriate events** to handle the result. +3. **Update your UI** based on the outcome. + +All events get sent directly to JavaScript in the web view _and_ to your PHP application via a special route. This +allows you to listen for these events in the context that best suits your application. + +### On the frontend + +Events are 'broadcast' to the frontend of your application via the web view through a custom `Native` helper. You can +easily listen for these events through JavaScript in a few ways: + +- The globally available `Native.on()` helper +- Directly importing the `On` function +- The `#[OnNative()]` PHP attribute Livewire extension + + + +#### The `Native.on()` helper + +Register the event listener directly in JavaScript: + +```blade +@@use(Native\Mobile\Events\Alert\ButtonPressed) + + +``` + +This approach is useful if you're not using any particular frontend JavaScript framework. + +#### The `On` import + + + +If you're using a SPA framework like Vue or React, it's more convenient to import the `On` function directly to +register your event listeners. Here's an example using the amazing Vue: + +```js +import { On, Events } from '#nativephp'; +import { onMounted } from 'vue'; + +const handleButtonPressed = (payload: any) => {}; + +onMounted(() => { + On(Events.Alert.ButtonPressed, handleButtonPressed); +}); +``` + +Note how we're also using the `Events` object above to simplify our use of built-in event names. For custom event +classes, you will need to reference these by their full name: + +```js +On('App\\Events\\MyButtonPressedEvent', handleButtonPressed); +``` + +In SPA land, don't forget to de-register your event handlers using the `Off` function too: + +```js +import { Off, Events } from '#nativephp'; +import { onUnmounted } from 'vue'; + +onUnmounted(() => { + Off(Events.Alert.ButtonPressed, handleButtonPressed); +}); +``` + +#### The `#[OnNative()]` attribute + +Livewire makes listening to 'broadcast' events simple. Just add the `#[OnNative()]` attribute attached to the Livewire +component method you want to use as its handler: + +```php +use Native\Mobile\Attributes\OnNative; +use Native\Mobile\Events\Camera\PhotoTaken; + +#[OnNative(PhotoTaken::class)] +public function handlePhoto(string $path) +{ + // Handle captured photo +} +``` + +### On the backend + +You can also listen for these events on the PHP side as they are simultaneously passed to your Laravel application. + +Simply [add a listener](https://laravel.com/docs/12.x/events#registering-events-and-listeners) as you normally would: + +```php +use App\Services\APIService; +use Native\Mobile\Events\Camera\PhotoTaken; + +class UpdateAvatar +{ + public function __construct(private APIService $api) {} + + public function handle(PhotoTaken $event): void + { + $imageData = base64_encode( + file_get_contents($event->path) + ); + + $this->api->updateAvatar($imageData); + } +} +``` diff --git a/resources/views/docs/mobile/3/the-basics/native-components.md b/resources/views/docs/mobile/3/the-basics/native-components.md new file mode 100644 index 00000000..2e7c1166 --- /dev/null +++ b/resources/views/docs/mobile/3/the-basics/native-components.md @@ -0,0 +1,34 @@ +--- +title: Native Components +order: 150 +--- + +![](/img/docs/edge.png) + +NativePHP for Mobile also supports rendering native UI components! + +Starting with v2, we've introduced a few key navigation components to give your apps an even more native feel. + +We call this **EDGE** - Element Definition and Generation Engine. + +## Living on the EDGE + +EDGE components are **truly native** elements that match each platform's design guidelines. + +Built on top of Laravel's Blade, EDGE gives you the power of native UI components with the simplicity you expect. + +@verbatim +```blade + + + +``` +@endverbatim + +We take a single definition and turn it into fully native UI that works beautifully across all the supported mobile OS +versions, in both light and dark mode. + +And they're fully compatible with hot reloading, which means you can swap them in and out at runtime without needing +to recompile your app! + +You can find out all about EDGE and the available components in the [EDGE Components](../edge-components/) section. diff --git a/resources/views/docs/mobile/3/the-basics/native-functions.md b/resources/views/docs/mobile/3/the-basics/native-functions.md new file mode 100644 index 00000000..bdde3966 --- /dev/null +++ b/resources/views/docs/mobile/3/the-basics/native-functions.md @@ -0,0 +1,90 @@ +--- +title: Native Functions +order: 100 +--- + +Our custom PHP extension enables tight integration with each platform, providing a consistent and performant abstraction +that lets you focus on building your app. Build for both platforms while you develop on one. + +Native device functions are called directly from your PHP code, giving you access to platform-specific features while +maintaining the productivity and familiarity of Laravel development. + +These functions are called from your PHP code using an ever-growing set of [Plugins](../plugins/). Plugins contain PHP classes that +are also wrapped in Laravel Facades for ease of access and testing, such as: + +```php +Native\Mobile\Facades\Biometrics +Native\Mobile\Facades\Browser +Native\Mobile\Facades\Camera +``` + +Each of these is covered in their respective plugin's documentation. + + +## Run from anywhere + +All native APIs are called through PHP. This means that your application is not reliant upon a web view to +function. + +However, when using a web view you may also interact with the native functions easily from JavaScript using +our convenient `Native` library. + +This is especially useful if you're building applications with a SPA framework, like Vue or React, as you can simply +import the functions you need and move a lot of work into the reactive part of your UI. + +### Install the Node plugin + +To use the `Native` JavaScript library, you must install the plugin in your `package.json` file. Add the following +section to the JSON: + +```js +{ + "dependencies": { + ... + }, + "imports": { // [tl! focus:start] + "#nativephp": "./vendor/nativephp/mobile/resources/dist/native.js" + } // [tl! focus:end] +} +``` + +Run `npm install`, then in your JavaScript, simply import the relevant functions from the plugin: + +```js +import { On, Off, Microphone, Events } from '#nativephp'; +import { onMounted, onUnmounted } from 'vue'; + +const buttonClicked = () => { + Microphone.record(); +}; + +const handleRecordingFinished = () => { + // Update the UI +}; + +onMounted(() => { + On(Events.Microphone.MicrophoneRecorded, handleRecordingFinished); +}); + +onUnmounted(() => { + Off(Events.Microphone.MicrophoneRecorded, handleRecordingFinished); +}); +``` + +The library is fully typed, so your IDE should be able to pick up the available properties and methods to provide you +with inline hints and code completion support. + +For the most part, the JavaScript APIs mirror the PHP APIs. Any key differences are noted in each Plugin's docs. + +This approach uses the PHP interface under the hood, meaning the two implementations stay in lock-step with each other. +So you'll never need to worry about whether an API is available only in one place or the other; they're all always +available wherever you need them. + + diff --git a/resources/views/docs/mobile/3/the-basics/overview.md b/resources/views/docs/mobile/3/the-basics/overview.md new file mode 100644 index 00000000..75b12003 --- /dev/null +++ b/resources/views/docs/mobile/3/the-basics/overview.md @@ -0,0 +1,56 @@ +--- +title: Overview +order: 50 +--- + +NativePHP for Mobile is made up of multiple parts: + +- A Laravel application (PHP) +- The `nativephp/mobile` Composer package +- A custom build of PHP with custom NativePHP extension +- Native applications (Swift & Kotlin) + +## Your Laravel app + +You can build your Laravel application just as you normally would, for the most part, sprinkling native functionality +in where desired by using NativePHP's built-in APIs. + +## `nativephp/mobile` + +The package is a pretty normal Composer package. It contains the PHP code needed to interface with the NativePHP +extension, the tools to install and run your applications, and all the code for each native application - iOS and +Android. + +## The PHP builds + +When you run the `native:install` Artisan command, the package will fetch the appropriate versions of the custom-built +PHP binaries. + +NativePHP for Mobile currently bundles **PHP 8.4**. You should ensure that your application is built to work with this +version of PHP. + +These custom PHP builds have been compiled specifically to target the mobile platforms and cannot be used in other +contexts. + +They are compiled as embeddable C libraries and embedded _into_ the native application. In this way, PHP doesn't run as +a separate process/service under a typical web server environment; essentially, the native application itself is +extended with the capability to execute your PHP code. + +Your Laravel application is then executed directly by the native app, using the embedded PHP engine to run the code. +This runs PHP as close to natively as it can get. It is very fast and efficient on modern hardware. + +## The native apps + +NativePHP ships one app for iOS and one for Android. When you run the `native:run` Artisan command, your Laravel app is +packaged up and copied into one of these apps. + +To build for both platforms, you must run the `native:run` command twice, targeting each platform. + +Each native app "shell" runs a number of steps to prepare the environment each time your application is booted, +including: + +- Checking to see if the bundled version of your Laravel app is newer than the installed version +- Installing the newer version if necessary +- Running migrations +- Clearing caches +- Creating storage symlinks diff --git a/resources/views/docs/mobile/3/the-basics/splash-screens.md b/resources/views/docs/mobile/3/the-basics/splash-screens.md new file mode 100644 index 00000000..1c803e4e --- /dev/null +++ b/resources/views/docs/mobile/3/the-basics/splash-screens.md @@ -0,0 +1,18 @@ +--- +title: Splash Screens +order: 400 +--- + +NativePHP makes it easy to add custom splash screens to your iOS and Android apps. + +## Supply your Splash Screens + +Place the relevant files in the locations specified: + +- `public/splash.png` - for the Light Mode splash screen +- `public/splash-dark.png` - for the Dark Mode splash screen + +### Requirements +- Format: PNG +- Minimum Size/Ratio: 1080 × 1920 pixels +- GD PHP extension must be enabled, ensure it has enough memory (~2GB should be enough) diff --git a/resources/views/docs/mobile/3/the-basics/web-view.md b/resources/views/docs/mobile/3/the-basics/web-view.md new file mode 100644 index 00000000..7e8fd718 --- /dev/null +++ b/resources/views/docs/mobile/3/the-basics/web-view.md @@ -0,0 +1,145 @@ +--- +title: Web View +order: 60 +--- + +Every mobile app built with NativePHP centers around a single native web view. The web view allows you to use whichever +web technologies you are most comfortable with to build your app's user interface (UI). + +You're not limited to any one tool or framework — you can use Livewire, Vue, React, Svelte, HTMX... even jQuery! +Whatever you're most comfortable with for building a web UI, you can use to build a mobile app with NativePHP. + +The web view is rendered to fill the entire view of your application and is intended to remain visible to your users at +all times — except when another full-screen action takes place, such as accessing the camera or an in-app browser. + +## The Viewport + +Just like a normal browser, the web view has the concept of a **viewport** which represents the viewable area of the +page. The viewport can be controlled with the `viewport` meta tag, just as you would in a traditional web application: + +```html + +``` + +### Disable Zoom +When building mobile apps, you may want to have a little more control over the experience. For example, you may +want to disable user-controlled zoom, allowing your app to behave similarly to a traditional native app. + +To achieve this, you can set `user-scalable=no`: + +```html + +``` + +## Edge-to-Edge + +To give you the most flexibility in how you design your app's UI, the web view occupies the entire screen, allowing you +to render anything anywhere on the display whilst your app is in the foreground using just HTML, CSS and JavaScript. + +But you should bear in mind that not all parts of the display are visible to the user. Many devices have camera +notches, rounded corners and curved displays. These areas may still be considered part of the `viewport`, but they may +be invisible and/or non-interactive. + +To account for this in your UI, you should set the `viewport-fit=cover` option in your `viewport` meta tag and use the +safe area insets. + +### Safe Areas + +Safe areas are the sections of the display which are not obscured by either a physical interruption (a rounded corner +or camera), or some persistent UI, such as the Home Indicator (a.k.a. the bottom bar) or notch. + +Safe areas are calculated for your app by the device at runtime and adjust according to its orientation, allowing your +UI to be responsive to the various device configurations with a simple and predictable set of CSS rules. + +The fundamental building blocks are a set of four values known as `insets`. These are injected into your pages as the +following CSS variables: + +- `--inset-top` +- `--inset-bottom` +- `--inset-left` +- `--inset-right` + +You can apply these insets in whichever way you need to build a usable interface. + +There is also a handy `nativephp-safe-area` CSS class that can be applied to most elements to ensure they sit within +the safe areas of the display. + +Say you want a `fixed`-position header bar like this: + +![](/img/docs/viewport-fit-cover.png) + +If you're using Tailwind, you might try something like this: + +```html +
+ ... +
+``` + +If you tried to do this without `viewport-fit=cover` and use of the safe areas, here's what you'd end up with in +portrait view: + +![](/img/docs/viewport-default.png) + +And it may be even worse in landscape view: + +![](/img/docs/viewport-default-landscape.png) + +But by adding a few simple adjustments to our page, we can make it beautiful again (Well, maybe we should lose the +red...): + +```html + +
+ ... +
+``` + +![](/img/docs/viewport-fit-cover-landscape.png) + +### Status Bar Style + +On Android, the icons in the Status Bar do not change color automatically based on the background color in your app. +By default, they change based on whether the device is in Light/Dark Mode. + +If you have a consistent background color in both light and dark mode, you may use the `nativephp.status_bar_style` +config key to set the appropriate status bar style for your app to give users the best experience. + +The possible options are: + +- `auto` - the default, which changes based on the device's Dark Mode setting +- `light` - ideal if your app's background is dark-colored +- `dark` - better if your app's background is light-colored + + + +## WebView Compatibility + +On Android, the web view is powered by the system's built-in WebView component, which varies by device and OS version. +Older Android versions ship with older WebView engines that may not support modern CSS features. + +For example, Tailwind CSS v4 uses `@theme` and other newer CSS features that are not supported on older WebView +versions. If you are targeting a lower `min_sdk` to support older devices, consider using Tailwind CSS v3 or another +CSS framework that generates compatible output. You can configure your minimum SDK version in your +[Android SDK Versions](/docs/mobile/3/getting-started/configuration#android-sdk-versions) settings. + +Always test your app on emulators running your minimum supported Android version to catch these issues early. You can +create emulators for older API levels in Android Studio's Virtual Device Manager. + +With just a few small changes, we've been able to define a layout that will work well on a multitude of devices +without having to add complex calculations or lots of device-specific CSS rules to our code. diff --git a/resources/views/early-adopter.blade.php b/resources/views/early-adopter.blade.php deleted file mode 100644 index befa878e..00000000 --- a/resources/views/early-adopter.blade.php +++ /dev/null @@ -1,159 +0,0 @@ - - - -
-
- -

- NativePHP for - iOS - and - Android - is coming! -

-

- Development of NativePHP for iOS has already started
and you can get access right now!

- Join the Early Access Program by
becoming a sponsor at $250 (one-off) or above. -

- - -

- Not ready to join the EAP?
- Sign up for the newsletter to keep up with the - latest developments. -

-
-
- - -
-
-
-

Why Join the Early Access Program?

-

- Up to now, NativePHP has focused on Windows, Mac, and Linux. But we believe that breaking the mobile - frontier is what makes the project truly compelling... and cross-platform. -

-

- With significant progress already made towards enabling - NativePHP on iOS, we are excited about the possibilities that lie ahead. -

-

- However, to make this vision a reality for both iOS and Android, we need your support. -

-

- As an EAP member, you will be supporting the continued development of all of NativePHP, but - especially of NativePHP for mobile. -

-

- You'll have the opportunity to influence the direction of the project and provide critical - feedback right from an early stage. -

-

- You'll get exclusive access to all the latest features first and special treatment for the life of - the NativePHP project, a project we plan to be working on for a long time to come! -

-

- Please join us on this exciting journey to expand NativePHP onto mobile platforms. -

-

- We can't wait to see what you build! -

-

- Simon & Marcel
- Creators of NativePHP -

-
-
-
- - -
-
-
-

Early Access Program

-

- Program Benefits -

- {{--

Quis tellus eget adipiscing convallis sit sit eget aliquet quis. Suspendisse eget egestas a elementum pulvinar et feugiat blandit at. In mi viverra elit nunc.

--}} -
- -
- -
-

- - Get the code! -

-
-

- You'll get early access to the code, documentation and more, and watch closely as it - evolves and start to build your projects before anyone else. -

-
-
- -
-

- - Access to an exclusive Discord channel -

-
-

- Decide the direction of the project and what gets worked on next. Plus have a direct - line of with the creators of NativePHP and other EAP members. -

-
-
- -
-

- - A lifetime of rewards and discounts -

-
-

- The NativePHP ecosystem is about to explode! There will be premium tools and - packages, starter kits and more. As an EAP member, you'll get you exclusive discounts - not available to anyone else. -

-
-
- -
-

- - Your name in NativePHP's history -

-
-

- All EAP members will be immortalized across the NativePHP ecosystem, showing the - rest of the community the valuable part you've played in supporting this project's - creation and and ongoing development. -

-
-
-
- - -
-
- - -
diff --git a/resources/views/errors/400.blade.php b/resources/views/errors/400.blade.php new file mode 100644 index 00000000..1dd04ac2 --- /dev/null +++ b/resources/views/errors/400.blade.php @@ -0,0 +1,43 @@ + +
+
+ {{-- Error illustration or icon --}} +
+
+ + + +
+
+ + {{-- Error message --}} +

+ Not Available +

+ +

+ This feature is currently not available. Please check back later or contact support if you need assistance. +

+ + {{-- Action buttons --}} + +
+
+
diff --git a/resources/views/errors/404.blade.php b/resources/views/errors/404.blade.php new file mode 100644 index 00000000..36801abc --- /dev/null +++ b/resources/views/errors/404.blade.php @@ -0,0 +1,43 @@ + +
+
+ {{-- Error illustration or icon --}} +
+
+ + + +
+
+ + {{-- Error message --}} +

+ Page Not Found +

+ +

+ The page you're looking for doesn't exist. It may have been moved, deleted, or you entered the wrong URL. +

+ + {{-- Action buttons --}} + +
+
+
diff --git a/resources/views/filament/resources/support-ticket-resource/widgets/ticket-replies.blade.php b/resources/views/filament/resources/support-ticket-resource/widgets/ticket-replies.blade.php new file mode 100644 index 00000000..03cdcadb --- /dev/null +++ b/resources/views/filament/resources/support-ticket-resource/widgets/ticket-replies.blade.php @@ -0,0 +1,139 @@ + + + + {{-- Pinned Note --}} + @php + $pinnedNote = $record->replies()->with('user')->where('note', true)->where('pinned', true)->first(); + @endphp + @if ($pinnedNote) +
+
+
+ Pinned Note + + {{ $pinnedNote->user?->name ?? 'System' }} + +
+
+ + {{ $pinnedNote->created_at->diffForHumans() }} + + + Unpin + +
+
+
{!! App\Support\CommonMark\CommonMark::convertToHtml($pinnedNote->message) !!}
+
+ @endif + + {{-- Reply form --}} +
+
+ + @error('newMessage') +

{{ $message }}

+ @enderror +
+ +
+ + Send Reply + Sending... + +
+
+
+
+ + {{-- Messages list (reverse chronological) --}} +
+ @forelse ($record->replies()->with('user')->orderBy('created_at', 'desc')->get() as $reply) + @php + $isAdmin = $reply->user?->isAdmin(); + $isNote = $reply->note; + + if ($isNote) { + $bgColor = '#fefce8'; + $borderColor = '#facc15'; + } elseif ($isAdmin) { + $bgColor = '#ede9fe'; + $borderColor = '#c4b5fd'; + } else { + $bgColor = '#f3f4f6'; + $borderColor = '#d1d5db'; + } + @endphp +
+
+
+ @if ($reply->user_id === null) + System + @else + + {{ $reply->user->name }} + + @if ($isNote) + Note + @if ($reply->pinned) + Pinned + @endif + @elseif ($isAdmin) + Staff + @endif + @endif +
+
+ + {{ $reply->created_at->diffForHumans() }} + + @if ($isNote) + + {{ $reply->pinned ? 'Unpin' : 'Pin' }} + + @endif +
+
+
{!! App\Support\CommonMark\CommonMark::convertToHtml($reply->message) !!}
+
+ @empty +

No replies yet.

+ @endforelse +
+
+
diff --git a/resources/views/filament/tables/columns/platforms.blade.php b/resources/views/filament/tables/columns/platforms.blade.php new file mode 100644 index 00000000..07e28213 --- /dev/null +++ b/resources/views/filament/tables/columns/platforms.blade.php @@ -0,0 +1,8 @@ +
+ @if($getRecord()->has_mobile) + + @endif + @if($getRecord()->has_desktop) + + @endif +
diff --git a/resources/views/laracon-us-2025-giveaway.blade.php b/resources/views/laracon-us-2025-giveaway.blade.php new file mode 100644 index 00000000..57dd6ed0 --- /dev/null +++ b/resources/views/laracon-us-2025-giveaway.blade.php @@ -0,0 +1,742 @@ + + + {{-- Hero Section --}} +
+
+ {{-- Countdown Header --}} +

+ Hurry! Entry Closes In: +

+ + {{-- Countdown Timer --}} +
+
+ +
+ Days +
+
+
+ +
+ Hours +
+
+
+ +
+ Minutes +
+
+
+ +
+ Seconds +
+
+
+ + {{-- Ticket --}} +
+
+
+
+
+ Laracon US 2025 Ticket + Laracon +
+
+
+
+ +
+ +
+ {{-- Primary Heading --}} +

+ Ticket Giveaway +

+ + {{-- Introduction Description --}} +

+ Laracon US is an annual gathering of people who are + passionate about building amazing applications with the + Laravel web framework. +

+ + {{-- Primary CTA - Email --}} + +
+
+
+ + {{-- Prizes --}} +
+ {{-- Header --}} +

+ Prizes +

+ + {{-- List --}} +
+
+ {{-- Card --}} +
+ {{-- Title --}} +
+ Laracon +
+ Ticket +
+ {{-- Illustration --}} + + {{-- Shiny circle --}} +
+
+ + {{-- Description --}} +
+
+ + {{-- Title --}} +
+ 1st Place +
+
+
+
+
+ {{-- Card --}} +
+ {{-- Title --}} +
+ NativePHP +
+ T-Shirt +
+ {{-- Illustration --}} + + {{-- Shiny circle --}} +
+
+ + {{-- Description --}} +
+
+ + {{-- Title --}} +
+ 1st Place +
+
+
+ + {{-- Title --}} +
+ 2nd Place +
+
+
+
+
+
+ {{-- Title --}} +
+ NativePHP +
+ License +
+ {{-- Illustration --}} + + {{-- Shiny circle --}} +
+
+ + {{-- Description --}} +
+
+ + {{-- Title --}} +
+ 1st Place +
+
+
+ + {{-- Title --}} +
+ 2nd Place +
+
+
+ + {{-- Title --}} +
+ 3rd Place +
+
+
+
+
+
+ + {{-- How to enter --}} +
+
+ {{-- Header --}} +
+

How to enter

+
+ + {{-- List --}} +
+
+
+
+ Step 1: +
+

+ Provide Your Contact Information +

+
+ @if ((int) request()->query('pending') === 1) +

+ Thank you! Please check your inbox for an email to + confirm your entry. +

+ @elseif ((int) request()->query('subscribed') === 1) +

+ Thank you! You have been entered into the giveaway. +

+ @else +
+
+
+ + +
+
+ + +
+ + + +
+ +
+ @endif +
+ + {{-- Left side --}} +
+
+ Step 2: +
+

+ Repost the Ticket Giveaway Announcement on X +

+
+ + {{-- Icon --}} +
+ + {{-- Left side --}} +
+
+ Step 3: +
+

+ Repost the Ticket Giveaway Announcement on Bluesky +

+
+ + {{-- Icon --}} +
+ + {{-- Left side --}} +
+
+ Step 4: +
+

+ Subscribe to NativePHP on YouTube +

+
+ + {{-- Icon --}} +
+
+
+
+ + {{-- Legal --}} +
+
+

Rules

+
+
    +
  • + To enter, you must provide your contact information + (step 1 above). This will subscribe you to our + giveaway-specific newsletter list. Even if you are + already subscribed to the NativePHP newsletter, you must + still subscribe to the giveaway-specific newsletter list + via the form above. Steps 2-4 are optional but + appreciated. No purchase is necessary to enter. +
  • +
  • Only one entry is permitted per person.
  • +
  • Must be 18 years or older to enter.
  • +
  • + This giveaway is open until July 1st, 2025 at 12:00AM + UTC and winners will be drawn within 24 hours of the + giveaway closing. +
  • +
  • + The winners will be selected by randomly drawing names + from the list of giveaway newsletter subscribers. + Winners will be notified via email and must respond + within 48 hours to claim their prize. If a winner does + not respond within 48 hours, another winner will be + selected in their place. +
  • +
  • + Winners are responsible for adhering to applicable laws, + regulations, and taxes within their jurisdiction. +
  • +
  • + The approximate prize values (in USD) are as follows: + 1st Place: $880, 2nd Place: $130, 3rd Place: $100. +
  • +
  • + Reasonable shipping & import costs will be covered by + Bifrost Technology, LLC. Shipping times will vary and + delivery cannot be guaranteed. +
  • +
  • + This giveaway is provided by Bifrost Technology, LLC + located at 1111B S Governors Ave STE 2838, Dover, DE + 19904. The giveaway is not affiliated with or endorsed + by Laracon US, X, Bluesky, YouTube, or any other entity. + By participating, you agree to the terms and conditions + outlined in these official rules. +
  • +
+
+
+
+
diff --git a/resources/views/license/renewal-success.blade.php b/resources/views/license/renewal-success.blade.php new file mode 100644 index 00000000..bb58940b --- /dev/null +++ b/resources/views/license/renewal-success.blade.php @@ -0,0 +1,84 @@ + +
+
+
+
+
+ + + +
+ +

+ License Renewal Successful! +

+ +

+ Your automatic renewal has been set up successfully.
+ Your license will now automatically renew before it expires. +

+
+ +
+
+
+
+ + + +
+
+

+ What's Next? +

+
+
    +
  • Your existing license key continues to work without any changes
  • +
  • Your license will automatically renew before the expiry date
  • +
  • You'll receive a confirmation email with your subscription details
  • +
  • You can manage your subscription from your account dashboard
  • +
+
+
+
+
+ +
+

License Information:

+
+
+
License Key
+
+ + {{ $license->key }} + +
+
+
+
Current Expiry
+
+ {{ $license->expires_at->format('F j, Y') }} +
+
+
+
+ + + +
+

+ Questions about your renewal? Contact our support team +

+
+
+
+
+
+
diff --git a/resources/views/license/renewal.blade.php b/resources/views/license/renewal.blade.php new file mode 100644 index 00000000..f64eab73 --- /dev/null +++ b/resources/views/license/renewal.blade.php @@ -0,0 +1,64 @@ + +
+
+ Upgrade to Ultra + Your Early Access license qualifies you for special upgrade pricing. +
+ + + Early Access Pricing + + As an early adopter, you can upgrade to Ultra at a special discounted rate. + This pricing is only available until your license expires. After that you will have to renew at full price. + + + +
+ {{-- Yearly Option --}} + +
+ Recommended + Yearly +
+ ${{ config('subscriptions.plans.max.eap_price_yearly') }} + /year +
+ Early Access Price + +
+ @csrf + + + Upgrade Yearly + +
+
+
+ + {{-- Monthly Option --}} + +
+
+ Monthly +
+ ${{ config('subscriptions.plans.max.price_monthly') }} + /month +
+ Billed monthly + +
+ @csrf + + + Upgrade Monthly + +
+
+
+
+ + + You'll be redirected to Stripe to complete your subscription setup. + +
+
diff --git a/resources/views/livewire/claim-donation-license.blade.php b/resources/views/livewire/claim-donation-license.blade.php new file mode 100644 index 00000000..5ef6789b --- /dev/null +++ b/resources/views/livewire/claim-donation-license.blade.php @@ -0,0 +1,169 @@ +
+
+ @if ($claimed) +
+
+ + + +
+

+ License Claimed! +

+

+ Your license is being generated and will be sent to your email shortly. +

+ +
+ @else +
+

+ Claim Your License +

+

+ Thank you for supporting NativePHP via OpenCollective! +
Enter your details below to claim your Mini license. +

+
+ +
+
+ @auth +
+

+ Claiming as {{ auth()->user()->email }} +

+
+ @endauth + +
+ + + @error('order_id') +

{{ $message }}

+ @enderror +
+ + @guest +
+ + + @error('name') +

{{ $message }}

+ @enderror +
+ +
+ + + @error('email') +

{{ $message }}

+ @enderror +
+ +
+ + + @error('password') +

{{ $message }}

+ @enderror +
+ +
+ + +
+ @endguest +
+ +
+ +
+ + @guest +

+ Already have an account? + + Sign in + +

+ @endguest +
+ +
+

+ How to find your Transaction ID +

+

+ You can find your Transaction ID on your OpenCollective transactions page. +

+
+ Screenshot showing where to find your OpenCollective Transaction ID +
+
+ @endif +
+
diff --git a/resources/views/livewire/claude-plugins-access-banner.blade.php b/resources/views/livewire/claude-plugins-access-banner.blade.php new file mode 100644 index 00000000..948239c2 --- /dev/null +++ b/resources/views/livewire/claude-plugins-access-banner.blade.php @@ -0,0 +1,74 @@ +
+@php + $user = auth()->user(); + $pluginDevKit = \App\Models\Product::where('slug', 'plugin-dev-kit')->first(); + $hasLicense = ($pluginDevKit && $user->hasProductLicense($pluginDevKit)) || $user->hasActiveUltraSubscription(); +@endphp + +@if($hasLicense) +
!$inline])> +
+
+
+
+ +
+
+

+ nativephp/claude-code Repo Access +

+
+ @if($user->github_username) +

Connected as {{ '@' . $user->github_username }}

+ + @if($collaboratorStatus === 'active') +

+ + Access Granted + +

+ @elseif($collaboratorStatus === 'pending') +

+ + Invitation Pending + +

+ @endif + @else +

Connect your GitHub account to access the Plugin Dev Kit repository.

+ @endif +
+
+
+
+ @if($user->github_username) + @if($collaboratorStatus === 'active') + + View Repo + + @elseif($collaboratorStatus === 'pending') + + @else +
+ @csrf + +
+ @endif + @else + + Connect GitHub + + @endif +
+
+
+
+@endif +
diff --git a/resources/views/livewire/customer/dashboard.blade.php b/resources/views/livewire/customer/dashboard.blade.php new file mode 100644 index 00000000..8aa292c7 --- /dev/null +++ b/resources/views/livewire/customer/dashboard.blade.php @@ -0,0 +1,178 @@ +
+
+ Dashboard + Welcome back, {{ auth()->user()->first_name ?? auth()->user()->name }} +
+ + {{-- Session Messages --}} + @if (session('success')) + + {{ session('success') }} + + @endif + + @if (session('message')) + + {{ session('message') }} + + @endif + + @if (session('error')) + + {{ session('error') }} + + @endif + + {{-- Ultra Upsell Banner --}} + @if(!$this->hasUltraSubscription) +
+
+
+ +
+
+

+ Upgrade to NativePHP Ultra +

+

+ Access all first-party plugins at no extra cost, premium support, team management, and more. +

+ +
+
+
+ @endif + + {{-- Banners --}} +
+ @feature(App\Features\ShowPlugins::class) + @if(auth()->user()->shouldSeeFreePluginsOffer() && !$this->hasUltraSubscription) + + @endif + @else + + @endfeature +
+ + {{-- Dashboard Cards --}} +
+ {{-- Licenses Card --}} + @if($this->licenseCount > 0) + + @endif + + {{-- EAP Status Card --}} + + + {{-- Subscription Card --}} + @if($this->activeSubscription?->active()) + + @else + + @endif + + {{-- Team Card --}} + @if($this->hasUltraSubscription) + @if($this->ownedTeam) + + @else + + @endif + @endif + + {{-- Premium Plugins Card --}} + @feature(App\Features\ShowPlugins::class) + + + {{-- Submit Plugin Card --}} + + @endfeature + + {{-- Connected Accounts Card --}} + + + {{-- Purchase History Card --}} + +
+ +
diff --git a/resources/views/livewire/customer/developer/dashboard.blade.php b/resources/views/livewire/customer/developer/dashboard.blade.php new file mode 100644 index 00000000..60ebdae6 --- /dev/null +++ b/resources/views/livewire/customer/developer/dashboard.blade.php @@ -0,0 +1,129 @@ +
+
+
+ Developer Dashboard + Manage your plugins and track your earnings +
+ + Submit Plugin + +
+ + {{-- Session Messages --}} + @if (session('success')) + + {{ session('success') }} + + @endif + + @if (session('message')) + + {{ session('message') }} + + @endif + + {{-- Stats Grid --}} +
+ + Total Earnings +

+ ${{ number_format($this->totalEarnings / 100, 2) }} +

+
+ + + Pending Payouts +

+ ${{ number_format($this->pendingEarnings / 100, 2) }} +

+
+ + + Published Plugins +

+ {{ $this->plugins->where('status', \App\Enums\PluginStatus::Approved)->count() }} +

+
+ + + Total Sales +

+ {{ $this->plugins->sum('licenses_count') }} +

+
+
+ + {{-- Two Column Layout --}} +
+ {{-- Plugins --}} + +
+ Your Premium Plugins + View all +
+ + +
+ @forelse ($this->plugins->take(5) as $plugin) + +
+
+

{{ $plugin->name }}

+
+
+ {{ $plugin->licenses_count }} sales +
+
+
+ @empty + + Submit a plugin + + @endforelse +
+
+ + {{-- Recent Payouts --}} + + Recent Payouts + + +
+ @forelse ($this->payouts as $payout) +
+
+
+

+ {{ $payout->pluginLicense->plugin->name ?? 'Unknown Plugin' }} +

+ {{ $payout->created_at->format('M j, Y') }} +
+
+

+ ${{ number_format($payout->developer_amount / 100, 2) }} +

+ @if ($payout->status === \App\Enums\PayoutStatus::Transferred) + Paid + @elseif ($payout->status === \App\Enums\PayoutStatus::Pending) + Pending + @else + Failed + @endif +
+
+
+ @empty + + @endforelse +
+
+
+
diff --git a/resources/views/livewire/customer/developer/onboarding.blade.php b/resources/views/livewire/customer/developer/onboarding.blade.php new file mode 100644 index 00000000..005c7183 --- /dev/null +++ b/resources/views/livewire/customer/developer/onboarding.blade.php @@ -0,0 +1,220 @@ +
+
+ Become a Plugin Developer + Set up your account to sell plugins on NativePHP +
+ + {{-- Session Messages --}} + @if (session('success')) + + {{ session('success') }} + + @endif + + @if (session('error')) + + {{ session('error') }} + + @endif + + @if (session('message')) + + {{ session('message') }} + + @endif + +
+ {{-- Status for existing account --}} + @if ($this->hasExistingAccount && $this->developerAccount) + + Onboarding Incomplete + Your Stripe account requires additional information before you can receive payouts. + + @endif + + {{-- Hero Card --}} + +
+
+ +
+ + @if ($this->hasExistingAccount) + Complete Your Onboarding + @else + Start Selling Plugins + @endif + + + Connect your Stripe account to receive payments when users purchase your plugins. + +
+ + {{-- Benefits --}} +
+ Why sell on NativePHP? +
    +
  • + + 70% Revenue Share — You keep the majority of every sale +
  • +
  • + + Built-in Distribution — Automatic Composer repository hosting +
  • +
  • + + Targeted Audience — Reach NativePHP developers directly +
  • +
  • + + Automatic Payouts — Get paid directly to your bank account +
  • +
+
+ + {{-- Country & Currency Selection --}} +
+ Your Country + + Select the country where your bank account is located. This determines which currencies are available for payouts. + + +
+
+ + @foreach ($this->countries as $code => $details) + + {{ $details['flag'] }} {{ $details['name'] }} + + @endforeach + + @error('country') + {{ $message }} + @enderror +
+ + @if (count($this->availableCurrencies) > 0) +
+ + @foreach ($this->availableCurrencies as $code => $name) + + {{ $name }} ({{ $code }}) + + @endforeach + + @error('payout_currency') + {{ $message }} + @enderror +
+ @endif +
+
+ + {{-- Developer Terms Agreement & CTA Button --}} +
+
+ @csrf + + + + + @if ($this->developerAccount?->hasAcceptedCurrentTerms()) + + + + + You accepted the Plugin Developer Terms and Conditions on {{ $this->developerAccount->accepted_plugin_terms_at->format('F j, Y') }}. + + + @else +
+ Plugin Developer Terms and Conditions + + Before you can sell plugins on the Marketplace, you must agree to the following key terms: + + +
    +
  • + + 30% Platform Fee — NativePHP retains 30% of each sale to cover payment processing, hosting, and platform maintenance +
  • +
  • + + Your Responsibility — You are solely responsible for your plugin's quality, performance, and customer support +
  • +
  • + + Listing Criteria — NativePHP sets and may change listing standards at any time, and may remove plugins at its discretion +
  • +
  • + + Pricing & Discounts — NativePHP sets plugin prices and may offer discounts at its discretion +
  • +
+ +
+ + @error('accepted_plugin_terms') + {{ $message }} + @enderror +
+
+ @endif + + + @if ($this->hasExistingAccount) + Continue Onboarding + @else + Connect with Stripe + @endif + +
+
+ + + You'll be redirected to Stripe to complete the onboarding process securely. + +
+ + {{-- FAQ --}} + + Frequently Asked Questions + +
+
+ How does the revenue share work? + + You receive 70% of each sale. NativePHP retains 30% to cover payment processing, hosting, and platform maintenance. + +
+ +
+ When do I get paid? + + Payouts are processed daily through Stripe Connect. However, all purchases include a 14-day refund period. Your earnings from a sale will only be paid out once this 14-day period has passed and the customer hasn't requested a refund. + +
+ +
+ What do I need to get started? + + A new Stripe account will be created for you as part of the onboarding process. You'll also need a GitHub account with a private repository for your plugin. Check out the plugin documentation to start building. + +
+
+
+
+
diff --git a/resources/views/livewire/customer/developer/settings.blade.php b/resources/views/livewire/customer/developer/settings.blade.php new file mode 100644 index 00000000..16547e3e --- /dev/null +++ b/resources/views/livewire/customer/developer/settings.blade.php @@ -0,0 +1,101 @@ +
+
+ Developer Settings + Manage your developer profile and account status. +
+ + @if (session('success')) + + {{ session('success') }} + + @endif + + {{-- Author Display Name --}} + +
+ Author Display Name + This is how your name will appear on your plugins in the directory. + +
+ +
+ + + Leave blank to use your account name: {{ auth()->user()->name }} + + + Save +
+
+ + {{-- Stripe Account Status --}} + @feature(App\Features\AllowPaidPlugins::class) + + Stripe Account Status + + + @if ($this->developerAccount && $this->developerAccount->hasCompletedOnboarding()) +
+
+ @if ($this->developerAccount->canReceivePayouts()) +
+ +
+
+

Account Active

+ Your account is fully set up to receive payouts +
+ @else +
+ +
+
+

Action Required

+ Additional information needed for payouts +
+ @endif +
+ @if (! $this->developerAccount->canReceivePayouts()) + + Complete Setup + + @endif +
+ @elseif ($this->developerAccount) +
+
+
+ +
+
+

Setup Incomplete

+ You've started the Stripe Connect setup but there are still some steps remaining. +
+
+ + Continue Setup + +
+ @else +
+
+
+ +
+
+

Not Connected

+ Connect your Stripe account to receive payouts for paid plugin sales. +
+
+ + Connect Stripe + +
+ @endif +
+ @endfeature +
diff --git a/resources/views/livewire/customer/integrations.blade.php b/resources/views/livewire/customer/integrations.blade.php new file mode 100644 index 00000000..59919566 --- /dev/null +++ b/resources/views/livewire/customer/integrations.blade.php @@ -0,0 +1,54 @@ +
+
+ Integrations + Connect your accounts to unlock additional features +
+ + {{-- Flash Messages --}} + @if(session()->has('success')) + + {{ session('success') }} + + @endif + + @if(session()->has('warning')) + + {{ session('warning') }} + + @endif + + @if(session()->has('error')) + + {{ session('error') }} + + @endif + + {{-- Info Section --}} + + About Integrations +
+
    +
  • GitHub: Max license holders can access the private nativephp/mobile repository. Plugin Dev Kit license holders and Ultra subscribers can access nativephp/claude-code.
  • +
  • Discord: Max license holders receive a special "Max" role in the NativePHP Discord server.
  • +
+

+ Need help? Join our Discord community. +

+
+
+ + {{-- Claude Plugins Access --}} +
+ +
+ +
+ @if(auth()->user()->hasMobileRepoAccess()) + + @endif + + @if(auth()->user()->hasMaxAccess()) + + @endif +
+
diff --git a/resources/views/livewire/customer/licenses.blade.php b/resources/views/livewire/customer/licenses.blade.php new file mode 100644 index 00000000..5d94250b --- /dev/null +++ b/resources/views/livewire/customer/licenses.blade.php @@ -0,0 +1,140 @@ +
+
+ Your Licenses + Manage your NativePHP licenses +
+ + @if($this->licenses->count() > 0) + + + License + Key + Status + Expires + + + + @foreach($this->licenses as $license) + @php + $isLegacyLicense = $license->isLegacy(); + $daysUntilExpiry = $license->expires_at ? (int) now()->diffInDays($license->expires_at, false) : null; + $needsRenewal = $isLegacyLicense && $daysUntilExpiry !== null && !$license->expires_at->isPast(); + + $status = match(true) { + $license->is_suspended => 'Suspended', + $license->expires_at && $license->expires_at->isPast() => 'Expired', + $needsRenewal => 'Needs Renewal', + default => 'Active', + }; + @endphp + + +
+ + {{ $license->name ?: $license->policy_name }} + + @if($license->name) + {{ $license->policy_name }} + @endif +
+
+ + + + + + + + + + + @if($needsRenewal) +
+ + {{ $daysUntilExpiry }} day{{ $daysUntilExpiry === 1 ? '' : 's' }} + + @if($isLegacyLicense) + Lock in Early Access Pricing + @endif +
+ @elseif($license->expires_at) +
+ {{ $license->expires_at->format('M j, Y') }} + @if($license->expires_at->isPast()) + Expired {{ $license->expires_at->diffForHumans() }} + @endif +
+ @else + No expiration + @endif +
+
+ @endforeach +
+
+ @endif + + {{-- Assigned Sub-Licenses --}} + @if($this->assignedSubLicenses->count() > 0) +
+ Assigned Sub-Licenses + + + License + Key + Status + Expires + + + + @foreach($this->assignedSubLicenses as $subLicense) + @php + $subStatus = match(true) { + $subLicense->is_suspended => 'Suspended', + $subLicense->expires_at && $subLicense->expires_at->isPast() => 'Expired', + default => 'Active', + }; + @endphp + + +
+ {{ $subLicense->parentLicense->policy_name ?? 'Sub-License' }} + Sub-license +
+
+ + + + + + + + + + + @if($subLicense->expires_at) +
+ {{ $subLicense->expires_at->format('M j, Y') }} + @if($subLicense->expires_at->isPast()) + Expired {{ $subLicense->expires_at->diffForHumans() }} + @endif +
+ @else + No expiration + @endif +
+
+ @endforeach +
+
+
+ @endif + + @if($this->licenses->count() === 0 && $this->assignedSubLicenses->count() === 0) + + @endif +
diff --git a/resources/views/livewire/customer/licenses/show.blade.php b/resources/views/livewire/customer/licenses/show.blade.php new file mode 100644 index 00000000..25c1eab7 --- /dev/null +++ b/resources/views/livewire/customer/licenses/show.blade.php @@ -0,0 +1,197 @@ +
+
+ + + Licenses + + {{ $license->name ?: $license->policy_name }} + @if($license->name) + {{ $license->policy_name }} + @endif +
+ + @if(session('success')) + + {{ session('success') }} + + @endif + + @if(session('error')) + + {{ session('error') }} + + @endif + + {{-- License Information Card --}} + +
+
+ License Information + Details about your NativePHP license. +
+ +
+ + + + + + {{-- License Key --}} + + License Key + +
+ + @if(! $license->is_suspended && ! ($license->expires_at && $license->expires_at->isPast())) + + + + @endif +
+
+
+ + {{-- License Name --}} + + License Name + +
+ + {{ $license->name ?: 'No name set' }} + + + Edit + +
+
+
+ + {{-- License Type --}} + + License Type + {{ $license->policy_name }} + + + {{-- Created --}} + + Created + + {{ $license->created_at->format('F j, Y \a\t g:i A') }} + ({{ $license->created_at->diffForHumans() }}) + + + + {{-- Expires --}} + + Expires + + @if($license->expires_at) + {{ $license->expires_at->format('F j, Y \a\t g:i A') }} + @if($license->expires_at->isPast()) + (Expired {{ $license->expires_at->diffForHumans() }}) + @else + ({{ $license->expires_at->diffForHumans() }}) + @endif + @else + Never + @endif + + +
+
+
+ + {{-- Sub-license Manager --}} + @if($license->supportsSubLicenses()) + @livewire('sub-license-manager', ['license' => $license]) + @endif + + {{-- Renewal CTA --}} + @php + $isLegacyLicense = $license->isLegacy(); + $daysUntilExpiry = $license->expires_at ? (int) now()->diffInDays($license->expires_at, false) : null; + $needsRenewal = $isLegacyLicense && $daysUntilExpiry !== null; + @endphp + + @if($needsRenewal && !$license->expires_at->isPast()) + + Renewal Available with Early Access Pricing + + Your license expires in {{ $daysUntilExpiry }} day{{ $daysUntilExpiry === 1 ? '' : 's' }}. + Set up automatic renewal now to upgrade to Ultra with your Early Access Pricing! + + + Set Up Renewal + + + @endif + + @if($license->is_suspended || ($license->expires_at && $license->expires_at->isPast())) + + + {{ $license->is_suspended ? 'License Suspended' : 'License Expired' }} + + + @if($license->is_suspended) + This license has been suspended. Please contact support for assistance. + @elseif($isLegacyLicense) + This license has expired. You can still renew it to restore access. + Renew now + @else + This license has expired. Please renew your subscription to continue using NativePHP. + @endif + + + @endif + + {{-- Edit License Name Modal --}} + +
+ Edit License Name + +
+ + Give your license a descriptive name to help organize your licenses. +
+ +
+ Update Name +
+
+
+ + {{-- Rotate License Key Confirmation Modal --}} + +
+
+ Rotate License Key + + Are you sure you want to rotate this license key? This action cannot be undone. + +
+ + + After rotating your key, you will need to: + +
    +
  • Update the license key in all your NativePHP applications
  • +
  • Update any CI/CD pipelines or deployment scripts
  • +
  • Notify any team members using this key
  • +
+
+
+ +
+ + + Cancel + + Rotate Key +
+
+
+
diff --git a/resources/views/livewire/customer/notifications.blade.php b/resources/views/livewire/customer/notifications.blade.php new file mode 100644 index 00000000..6858eca2 --- /dev/null +++ b/resources/views/livewire/customer/notifications.blade.php @@ -0,0 +1,76 @@ +
+
+
+ Notifications + Stay up to date with your account activity. +
+ +
+ @if (auth()->user()->unreadNotifications->count() > 0) + + Mark all as read + + @endif + + + Settings + +
+
+ + @forelse ($this->notifications as $notification) + +
+ {{-- Unread indicator --}} +
+ @if (is_null($notification->read_at)) +
+ @else +
+ @endif +
+ +
+
+ + {{ $notification->data['title'] ?? 'Notification' }} + + + {{ $notification->created_at->diffForHumans() }} + +
+ + @if (! empty($notification->data['body'])) + {{ $notification->data['body'] }} + @endif + +
+ @if (! empty($notification->data['action_url'])) + + {{ $notification->data['action_label'] ?? 'View' }} + + @endif + + @if (is_null($notification->read_at)) + + Mark as read + + @endif +
+
+
+
+ @empty + +
+ + No notifications + You're all caught up! +
+
+ @endforelse + +
+ {{ $this->notifications->links() }} +
+
diff --git a/resources/views/livewire/customer/plugins/create.blade.php b/resources/views/livewire/customer/plugins/create.blade.php new file mode 100644 index 00000000..8622305b --- /dev/null +++ b/resources/views/livewire/customer/plugins/create.blade.php @@ -0,0 +1,196 @@ +
+
+ + Submit Your Plugin + Add your plugin to the NativePHP Plugin Marketplace +
+ +
+ {{-- Session Error --}} + @if (session('error')) + + {{ session('error') }} + + @endif + + {{-- Validation Errors --}} + @if ($errors->any()) + + Please fix the following errors: + +
    + @foreach ($errors->all() as $error) +
  • {{ $error }}
  • + @endforeach +
+
+
+ @endif + + {{-- GitHub Connection Required --}} + @if (!auth()->user()->github_id) + + GitHub Connection Required + + To submit a plugin, you need to connect your GitHub account so we can access your repository and automatically set up webhooks. + + + + + Connect GitHub Account + + + + @else +
+ {{-- Plugin Type --}} + @feature(App\Features\AllowPaidPlugins::class) + + Plugin Type + Is your plugin free or paid? + +
+ + + +
+ + @error('pluginType') + {{ $message }} + @enderror +
+ @else + + @endfeature + + {{-- Repository Selection --}} + +
+ + + Connected as {{ auth()->user()->github_username }} + +
+ + Select Repository + + Choose the repository containing your plugin. We'll automatically set up a webhook to keep your plugin in sync. + + +
+ @if($loadingRepos) +
+ + Loading repositories... +
+ @elseif($reposLoaded) + + @foreach($this->owners as $owner) + {{ $owner }} + @endforeach + + + @if($selectedOwner) + + @foreach($this->ownerRepositories as $repo) + {{ $repo['name'] }}@if($repo['private']) (private)@endif + @endforeach + + @endif + @endif + @error('repository') + {{ $message }} + @enderror +
+
+ + {{-- Paid Plugin Info --}} + @feature(App\Features\AllowPaidPlugins::class) + @if($pluginType === 'paid') + + How paid plugins work +
    +
  • + + We pull your code from GitHub when you tag a release +
  • +
  • + + We host and distribute your plugin via plugins.nativephp.com +
  • +
  • + + Customers install via Composer with their license key +
  • +
  • + + You get paid automatically via Stripe Connect +
  • +
+
+ @endif + @endfeature + + @if($repository) + {{-- Support Channel --}} + + Support Channel + + How can users get support for your plugin? Provide an email address or a URL. If you enter a URL, ensure that it clearly details how a visitor goes about getting support for this plugin. + + +
+ +
+
+ + {{-- Notes --}} + + Notes + + Any notes for the review team? Feel free to share links to videos of the plugin working. These won't be displayed on your plugin listing. + + +
+ +
+
+ @endif + + {{-- Submit Button --}} +
+ Cancel + Submit Plugin +
+
+ @endif +
+
diff --git a/resources/views/livewire/customer/plugins/index.blade.php b/resources/views/livewire/customer/plugins/index.blade.php new file mode 100644 index 00000000..d8da94d0 --- /dev/null +++ b/resources/views/livewire/customer/plugins/index.blade.php @@ -0,0 +1,130 @@ +
+
+ Plugins + Extend NativePHP Mobile with powerful native features +
+ + {{-- Action Cards --}} +
+ {{-- Submit Plugin Card --}} + +
+
+
+ +
+ Submit Your Plugin + + Built a plugin? Submit it to the NativePHP Plugin Marketplace and share it with the community. + + + Submit a Plugin + +
+
+ + {{-- Browse Plugins Card --}} + +
+ +
+ Browse Plugins + + Discover plugins built by the community to add native features to your mobile apps. + + View Directory +
+ + {{-- Learn to Build Card --}} + +
+ +
+ Learn to Build Plugins + + Read the documentation to learn how to create your own NativePHP Mobile plugins. + + Read the Docs +
+
+ + {{-- Success/Error Messages --}} + @if (session('success')) + + {{ session('success') }} + + @endif + + @if (session('error')) + + {{ session('error') }} + + @endif + + {{-- Submitted Plugins List --}} +
+ Your Submitted Plugins + Track the status of your plugin submissions. + + + + + + + + @if ($this->plugins->count() > 0) + + + Plugin + Status + + + + + @foreach ($this->plugins as $plugin) + + +
+
+ @if ($plugin->isPending()) +
+ @elseif ($plugin->isApproved()) +
+ @else +
+ @endif +
+
+ {{ $plugin->name }} + + {{ $plugin->type->label() }} plugin • Submitted {{ $plugin->created_at->diffForHumans() }} + +
+
+
+ + + + + + + + Edit + + +
+ + @endforeach +
+
+ @else +
+ +
+ @endif +
+
diff --git a/resources/views/livewire/customer/plugins/show.blade.php b/resources/views/livewire/customer/plugins/show.blade.php new file mode 100644 index 00000000..03066669 --- /dev/null +++ b/resources/views/livewire/customer/plugins/show.blade.php @@ -0,0 +1,302 @@ +
+
+ + + Plugins + + Edit Plugin + {{ $plugin->name }} +
+ +
+ {{-- Success/Error Messages --}} + @if (session('success')) + + {{ session('success') }} + + @endif + + @if (session('error')) + + {{ session('error') }} + + @endif + + {{-- Rejection Reason --}} + @if ($plugin->isRejected() && $plugin->rejection_reason) + + Rejection Reason + {{ $plugin->rejection_reason }} + + Resubmit for Review + + + @endif + + {{-- Plugin Status --}} + +
+
+ @if ($plugin->hasLogo()) + {{ $plugin->name }} logo + @elseif ($plugin->hasGradientIcon()) +
+ +
+ @else +
+ +
+ @endif +
+ {{ $plugin->name }} + + {{ $plugin->type->label() }} plugin + @if ($plugin->latest_version) + + v{{ $plugin->latest_version }} + @endif + +
+
+ +
+
+ + {{-- Review Checks --}} + @if ($plugin->review_checks) + + Review Checks + Automated checks run against your repository. + + @php + $requiredChecks = [ + ['key' => 'has_license_file', 'label' => 'License file (LICENSE or LICENSE.md)'], + ['key' => 'has_release_version', 'label' => 'Release version'], + ['key' => 'webhook_configured', 'label' => 'Webhook configured'], + ]; + $optionalChecks = [ + ['key' => 'supports_ios', 'label' => 'iOS support (resources/ios/)'], + ['key' => 'supports_android', 'label' => 'Android support (resources/android/)'], + ['key' => 'supports_js', 'label' => 'JavaScript support (resources/js/)'], + ['key' => 'requires_mobile_sdk', 'label' => 'Requires nativephp/mobile SDK'], + ]; + @endphp + + Required for approval +
    + @foreach ($requiredChecks as $check) + @php + $isPassing = $check['key'] === 'webhook_configured' + ? $plugin->webhook_installed + : ($plugin->review_checks[$check['key']] ?? false); + @endphp +
  • +
    + @if ($isPassing) + + + {{ $check['label'] }} + @if ($check['key'] === 'has_release_version' && ($plugin->review_checks['release_version'] ?? null)) + {{ $plugin->review_checks['release_version'] }} + @endif + + @else + + {{ $check['label'] }} + @endif +
    + + @if ($check['key'] === 'webhook_configured' && ! $isPassing && $plugin->webhook_secret) +
    +

    + We couldn't automatically install the webhook. Please set it up manually: +

    +
    + +
    + {{ $plugin->getWebhookUrl() }} + + + +
    +
    +
      +
    1. Go to your repository's Settings → Webhooks
    2. +
    3. Click Add webhook
    4. +
    5. Paste the URL above into the Payload URL field
    6. +
    7. Set Content type to application/json
    8. +
    9. Select events: Pushes and Releases
    10. +
    11. Click Add webhook
    12. +
    +
    + @endif +
  • + @endforeach +
+ + Additional checks +
    + @foreach ($optionalChecks as $check) +
  • + @if ($plugin->review_checks[$check['key']] ?? false) + + @else + + @endif + {{ $check['label'] }} +
  • + @endforeach +
+ + @if ($plugin->review_checks['mobile_sdk_constraint'] ?? null) + + SDK constraint: {{ $plugin->review_checks['mobile_sdk_constraint'] }} + + @endif + + @if ($plugin->reviewed_at) + + Last checked {{ $plugin->reviewed_at->diffForHumans() }} + + @endif +
+ @endif + + {{-- Support Channel --}} + + Support Channel + How can users get support for your plugin? Provide an email address or a URL. + +
+ + @error('supportChannel') + {{ $message }} + @enderror + +
+ Save Support Channel +
+ +
+ + {{-- Plugin Icon --}} + + Plugin Icon + Choose a gradient and icon, or upload your own logo. + +
+ {{-- Current Icon Preview --}} + @if ($plugin->hasCustomIcon()) +
+ @if ($plugin->hasLogo()) + {{ $plugin->name }} logo + @elseif ($plugin->hasGradientIcon()) +
+ +
+ @endif + Remove icon +
+ @endif + + {{-- Gradient Icon Picker --}} +
+
+
+
+ +
+ @foreach (\App\Models\Plugin::gradientPresets() as $key => $classes) + + @endforeach +
+ @error('iconGradient') + {{ $message }} + @enderror +
+ + + @error('iconName') + {{ $message }} + @enderror + + Save Icon +
+
+ + + +
+ + {{-- Custom Logo Upload --}} +
+
+
+ +
+ + Upload +
+ @error('logo') + {{ $message }} + @enderror + PNG, JPG, SVG, or WebP. Max 1MB. Recommended: 256x256 pixels, square. +
+
+ + + +
+
+
+ + {{-- Description Form --}} + + Plugin Description + Describe what your plugin does. This will be displayed in the plugin directory. + +
+ + @error('description') + {{ $message }} + @enderror + Maximum 1000 characters + +
+ Save Description +
+ +
+ +
+
diff --git a/resources/views/livewire/customer/purchase-history.blade.php b/resources/views/livewire/customer/purchase-history.blade.php new file mode 100644 index 00000000..0e14aa0e --- /dev/null +++ b/resources/views/livewire/customer/purchase-history.blade.php @@ -0,0 +1,68 @@ +
+
+ Purchase History + All your NativePHP purchases in one place. Bifrost subscriptions are managed separately and won't appear here. +
+ + @if($this->purchases->count() > 0) + + + Purchase + Price + Date + + + + @foreach($this->purchases as $purchase) + + +
+ @if($purchase['href']) + + {{ $purchase['name'] }} + + @else + {{ $purchase['name'] }} + @endif + @if($purchase['description']) + {{ $purchase['description'] }} + @endif +
+
+ + + @if($purchase['price'] !== null && $purchase['price'] > 0) + ${{ number_format($purchase['price'] / 100, 2) }} + @elseif($purchase['price'] === 0 || (isset($purchase['is_grandfathered']) && $purchase['is_grandfathered'])) + Free + @else + — + @endif + + + +
+ {{ $purchase['purchased_at']->format('M j, Y') }} + @if($purchase['expires_at']) + + @if($purchase['expires_at']->isPast()) + Expired {{ $purchase['expires_at']->format('M j, Y') }} + @else + Expires {{ $purchase['expires_at']->format('M j, Y') }} + @endif + + @endif +
+
+
+ @endforeach +
+
+ @else + + @endif +
diff --git a/resources/views/livewire/customer/purchased-plugins.blade.php b/resources/views/livewire/customer/purchased-plugins.blade.php new file mode 100644 index 00000000..0eb8ec75 --- /dev/null +++ b/resources/views/livewire/customer/purchased-plugins.blade.php @@ -0,0 +1,81 @@ +
+
+
+ Purchased Plugins + Your premium plugins and Composer configuration +
+ Browse Plugins +
+ + @if(session('success')) + + {{ session('success') }} + + @endif + + + + @if($this->pluginLicenses->count() > 0) + Your Plugins + + + Plugin + Status + Purchased + + + + @foreach($this->pluginLicenses as $pluginLicense) + + +
+
+ @if($pluginLicense->plugin->hasLogo()) + {{ $pluginLicense->plugin->name }} + @elseif($pluginLicense->plugin->hasGradientIcon()) +
+ +
+ @else +
+ +
+ @endif +
+
+ + {{ $pluginLicense->plugin->name }} + + @if($pluginLicense->plugin->description) + {{ $pluginLicense->plugin->description }} + @endif +
+
+
+ + +
+ + @if($pluginLicense->wasPurchasedAsBundle() && $pluginLicense->pluginBundle) + Part of {{ $pluginLicense->pluginBundle->name }} + @endif +
+
+ + + {{ $pluginLicense->purchased_at->format('M j, Y') }} + +
+ @endforeach +
+
+ @else + + Browse Plugins + + @endif +
diff --git a/resources/views/livewire/customer/settings.blade.php b/resources/views/livewire/customer/settings.blade.php new file mode 100644 index 00000000..b7692f11 --- /dev/null +++ b/resources/views/livewire/customer/settings.blade.php @@ -0,0 +1,144 @@ +
+
+ Settings + Manage your account settings. +
+ +
+ + +
+ + @if ($tab === 'account') + {{-- Update Name --}} + +
+ Name + Update the name associated with your account. + + @if (session('name-updated')) + + {{ session('name-updated') }} + + @endif + +
+ +
+ + Save +
+
+ + {{-- Email Address --}} + + Email Address + The email address associated with your account. + + + + + {{-- Change Password --}} + + Password + Update your password to keep your account secure. + + @if (session('password-updated')) + + {{ session('password-updated') }} + + @endif + + @if (auth()->user()->github_id && ! auth()->user()->password) + + + Your account uses GitHub for authentication. To set a password, use the + password reset flow. + + + @else +
+
+ + + +
+ + Update Password +
+ @endif +
+ + {{-- Delete Account --}} + + Delete Account + Permanently delete your account and all associated data. + + + + This action is irreversible. All your licenses and data will be permanently removed. + @if (auth()->user()->subscription()?->active()) + Your active subscription will also be cancelled immediately. + @endif + + + + + Delete Account + + + + {{-- Delete Account Confirmation Modal --}} + +
+ Confirm Account Deletion + + + Please enter your password to confirm you want to permanently delete your account. + + +
+ +
+ +
+ + Cancel + + Delete My Account +
+
+
+ @endif + + @if ($tab === 'notifications') + + + + + + @endif +
diff --git a/resources/views/livewire/customer/showcase/create.blade.php b/resources/views/livewire/customer/showcase/create.blade.php new file mode 100644 index 00000000..09f5b421 --- /dev/null +++ b/resources/views/livewire/customer/showcase/create.blade.php @@ -0,0 +1,22 @@ +
+
+ Submit Your App to the Showcase + Share your NativePHP app with the community! Your submission will be reviewed by our team. +
+ + + + Showcase Guidelines + +
    +
  • Your app must be built with NativePHP
  • +
  • Include clear screenshots showcasing your app
  • +
  • Provide download links or store URLs where users can get your app
  • +
  • Submissions are reviewed before being published
  • +
+
+
+ + +
+
diff --git a/resources/views/livewire/customer/showcase/edit.blade.php b/resources/views/livewire/customer/showcase/edit.blade.php new file mode 100644 index 00000000..ab5c017e --- /dev/null +++ b/resources/views/livewire/customer/showcase/edit.blade.php @@ -0,0 +1,13 @@ +
+
+
+ Edit Your Submission + Update the details of your showcase submission. +
+ +
+ + + + +
diff --git a/resources/views/livewire/customer/showcase/index.blade.php b/resources/views/livewire/customer/showcase/index.blade.php new file mode 100644 index 00000000..b6fd7e12 --- /dev/null +++ b/resources/views/livewire/customer/showcase/index.blade.php @@ -0,0 +1,82 @@ +@use('Illuminate\Support\Facades\Storage') + +
+
+
+ Your Showcase Submissions + Submit your NativePHP apps to be featured on our showcase +
+ Submit New App +
+ + @if(session()->has('success')) + + {{ session('success') }} + + @endif + + @if(session()->has('warning')) + + {{ session('warning') }} + + @endif + + @if($this->showcases->count() > 0) + + + App + Platforms + Status + + + + + @foreach($this->showcases as $showcase) + + +
+ @if($showcase->image) + {{ $showcase->title }} + @else +
+ +
+ @endif + + {{ $showcase->title }} + +
+
+ + +
+ @if($showcase->has_mobile) + Mobile + @endif + @if($showcase->has_desktop) + Desktop + @endif +
+
+ + + + + + + Edit + +
+ @endforeach +
+
+ @else + + Submit Your App + + @endif +
diff --git a/resources/views/livewire/customer/support/create.blade.php b/resources/views/livewire/customer/support/create.blade.php new file mode 100644 index 00000000..2507b15d --- /dev/null +++ b/resources/views/livewire/customer/support/create.blade.php @@ -0,0 +1,270 @@ +
+
+ + + Support Tickets + + Submit a Request + We'll get back to you as soon as possible. +
+ +
+ {{-- Step Indicator --}} + + @foreach ([1 => 'Product', 2 => 'Details', 3 => 'Review'] as $step => $label) + + {{ $step }} + + {{ $label }} + + + @endforeach + + + +
+ {{-- Step 1: Product Selection --}} + @if ($currentStep === 1) +
+ Which product is this about? + Select the product related to your request. + + @error('selectedProduct') +

{{ $message }}

+ @enderror + + + @foreach ([ + 'mobile' => ['label' => 'Mobile', 'desc' => 'iOS & Android apps'], + 'desktop' => ['label' => 'Desktop', 'desc' => 'macOS, Windows & Linux apps'], + 'nativephp.com' => ['label' => 'nativephp.com', 'desc' => 'Website, account & billing'], + ] as $value => $product) + + @endforeach + +
+ @endif + + {{-- Step 2: Context Questions --}} + @if ($currentStep === 2) +
+ {{-- Mobile Area --}} + @if ($this->showMobileArea) +
+ What is the issue related to? + Is this about NativePHP for Mobile itself, or a specific plugin/tool? + + + + + + + @error('mobileAreaType') +

{{ $message }}

+ @enderror + + @if ($mobileAreaType === 'plugin') +
+ + @foreach ($officialPlugins as $id => $pluginName) + {{ $pluginName }} + @endforeach + Jump + Other + + @error('mobileArea') +

{{ $message }}

+ @enderror +
+ @endif +
+ @endif + + {{-- Bug Report Fields --}} + @if ($this->showBugReportFields) +
+ Bug report details + Help us understand and reproduce the issue. + +
+ + + + + + + + Environment + + Run php artisan native:debug in your project and paste the output here. + + + + +
+
+ @endif + + {{-- Issue Type --}} + @if ($this->showIssueType) +
+ Issue type + What kind of issue are you experiencing? + + + + + + + + + @error('issueType') +

{{ $message }}

+ @enderror +
+ @endif + + {{-- Subject + Message (only for non-bug-report flows) --}} + @if (! $this->showBugReportFields) +
+ Describe your issue + Provide a summary and any additional details. + +
+ + + +
+
+ @endif +
+ @endif + + {{-- Step 3: Review & Submit --}} + @if ($currentStep === 3) +
+ Review your request + Please review the details below before submitting. + +
+
+
Product
+
+ {{ ['mobile' => 'Mobile', 'desktop' => 'Desktop', 'bifrost' => 'Bifrost', 'nativephp.com' => 'nativephp.com'][$selectedProduct] ?? $selectedProduct }} +
+
+ + @if ($mobileAreaType) +
+
Area
+
+ {{ $mobileAreaType === 'core' ? 'NativePHP Mobile (core)' : $mobileArea }} +
+
+ @endif + + @if ($issueType) +
+
Issue type
+
+ {{ ['account_query' => 'Account query', 'bug' => 'Bug', 'feature_request' => 'Feature request', 'other' => 'Other'][$issueType] ?? $issueType }} +
+
+ @endif + + @if (! $this->showBugReportFields) +
+
Subject
+
{{ $subject }}
+
+ +
+
Message
+
{{ $message }}
+
+ @endif + + @if ($tryingToDo) +
+
What you were trying to do
+
{{ $tryingToDo }}
+
+ @endif + + @if ($whatHappened) +
+
What happened instead
+
{{ $whatHappened }}
+
+ @endif + + @if ($reproductionSteps) +
+
Steps to reproduce
+
{{ $reproductionSteps }}
+
+ @endif + + @if ($environment) +
+
Environment
+
{{ $environment }}
+
+ @endif +
+
+ @endif + + {{-- Navigation --}} +
+ @if ($currentStep > 1) + + Back + + @else +
+ @endif + + @if ($currentStep < 3) + + Continue + + @else + + Submit Request + Submitting... + + @endif +
+
+
+
+
diff --git a/resources/views/livewire/customer/support/index.blade.php b/resources/views/livewire/customer/support/index.blade.php new file mode 100644 index 00000000..626fa96c --- /dev/null +++ b/resources/views/livewire/customer/support/index.blade.php @@ -0,0 +1,57 @@ +
+
+
+ Support Tickets + Manage your support tickets +
+ Submit a new request +
+ + @if(session()->has('success')) + + {{ session('success') }} + + @endif + + @if($this->supportTickets->count() > 0) + + + Ticket ID + Subject + Status + + + + @foreach($this->supportTickets as $ticket) + + + + #{{ $ticket->mask }} + + + + {{ $ticket->subject }} + + + + + + @endforeach + + + + @if($this->supportTickets->hasPages()) +
+ {{ $this->supportTickets->links() }} +
+ @endif + @else + + Submit a new request + + @endif +
diff --git a/resources/views/livewire/customer/support/show.blade.php b/resources/views/livewire/customer/support/show.blade.php new file mode 100644 index 00000000..0a41f76e --- /dev/null +++ b/resources/views/livewire/customer/support/show.blade.php @@ -0,0 +1,133 @@ +
+
+ + + Support Tickets + +
+
+ #{{ $supportTicket->mask }} » {{ $supportTicket->subject }} +
+ + Created {{ $supportTicket->created_at->format('d M Y, H:i') }} +
+
+ @if($supportTicket->status === \App\SupportTicket\Status::CLOSED) + + Reopen Ticket + + @else + + Close Ticket + + @endif +
+
+ + @if(session()->has('success')) + + {{ session('success') }} + + @endif + + {{-- Submission Details --}} + + + Submission Details + +
+
+
Product
+
{{ ucfirst($supportTicket->product) }}
+
+ @if($supportTicket->issue_type) +
+
Issue Type
+
{{ str_replace('_', ' ', ucfirst($supportTicket->issue_type)) }}
+
+ @endif +
+ + @if(! in_array($supportTicket->product, ['mobile', 'desktop'])) +
+
Original Message
+
{{ $supportTicket->message }}
+
+ @endif + + @if($supportTicket->metadata) +
+
Additional Details
+
+
+ @foreach($supportTicket->metadata as $key => $value) +
+
{{ str_replace('_', ' ', ucfirst($key)) }}
+
{{ $value }}
+
+ @endforeach +
+
+
+ @endif +
+
+
+ + {{-- Messages --}} +
+ Messages + + {{-- Reply Form --}} + @if($supportTicket->status !== \App\SupportTicket\Status::CLOSED) +
+
+ +
+ ⌘/Ctrl + Enter to send + + Send Reply + +
+ +
+ @endif + + @foreach($supportTicket->replies->where('note', false) as $reply) + @if($reply->user_id === null) + {{-- System message --}} +
+
+ {{ $reply->message }} · {{ $reply->created_at->format('d M Y, H:i') }} +
+
+ @else +
+
+
+

+ {{ $reply->user->name }} + @if($reply->is_from_user) + (You) + @elseif($reply->is_from_admin) + (Staff) + @endif +

+
{!! App\Support\CommonMark\CommonMark::convertToHtml($reply->message) !!}
+
+
+
+ {{ $reply->created_at->format('d M Y, H:i') }} +
+
+ @endif + @endforeach +
+
diff --git a/resources/views/livewire/customer/wall-of-love/create.blade.php b/resources/views/livewire/customer/wall-of-love/create.blade.php new file mode 100644 index 00000000..f0188a37 --- /dev/null +++ b/resources/views/livewire/customer/wall-of-love/create.blade.php @@ -0,0 +1,24 @@ +
+
+ Join our Wall of Love! + As an early adopter, your story matters. Share your experience with NativePHP and inspire other developers in the community. +
+ + + @if(session()->has('success')) + + {{ session('success') }} + + @endif + + + You're an Early Adopter! + +

Thank you for supporting NativePHP from the beginning. As a reward, you can appear permanently on our Wall of Love.

+

Your submission will be reviewed by our team and, once approved, will appear on the page.

+
+
+ + +
+
diff --git a/resources/views/livewire/customer/wall-of-love/edit.blade.php b/resources/views/livewire/customer/wall-of-love/edit.blade.php new file mode 100644 index 00000000..a518ab0d --- /dev/null +++ b/resources/views/livewire/customer/wall-of-love/edit.blade.php @@ -0,0 +1,10 @@ +
+
+ Edit Your Listing + Update your photo and company name on the Wall of Love. +
+ + + + +
diff --git a/resources/views/livewire/discord-access-banner.blade.php b/resources/views/livewire/discord-access-banner.blade.php new file mode 100644 index 00000000..f542db93 --- /dev/null +++ b/resources/views/livewire/discord-access-banner.blade.php @@ -0,0 +1,80 @@ +
+
!$inline])> +
+
+
+
+ +
+
+

+ Discord Max Role +

+
+ @if(auth()->user()->discord_username) +

Connected as {{ auth()->user()->discord_username }}

+ + @if(!$isGuildMember) +

+ + Not in Server + +

+ @elseif($hasMaxRole) +

+ + Max Role Active + +

+ @elseif(auth()->user()->hasMaxAccess()) +

+ + Eligible + +

+ @endif + @else +

Connect your Discord account to receive the Max role.

+ @endif +
+
+
+
+ @if(auth()->user()->discord_username) + @if($hasMaxRole) + + Open Discord + + @elseif(!$isGuildMember) + + Join Discord Server + + + @elseif(auth()->user()->hasMaxAccess()) + + @endif +
+ @csrf + @method('DELETE') + +
+ @else + + Connect Discord + + @endif +
+
+
+
+
diff --git a/resources/views/livewire/git-hub-access-banner.blade.php b/resources/views/livewire/git-hub-access-banner.blade.php new file mode 100644 index 00000000..736ed9c9 --- /dev/null +++ b/resources/views/livewire/git-hub-access-banner.blade.php @@ -0,0 +1,75 @@ +
+@if(auth()->user()->hasMobileRepoAccess()) +
!$inline])> +
+
+
+
+ +
+
+

+ nativephp/mobile Repo Access +

+
+ @if(auth()->user()->github_username) +

Connected as {{ '@' . auth()->user()->github_username }}

+ + @if($collaboratorStatus === 'active') +

+ + Access Granted + +

+ @elseif($collaboratorStatus === 'pending') +

+ + Invitation Pending + +

+ @endif + @else +

Connect your GitHub account to access the nativephp/mobile repository.

+ @endif +
+
+
+
+ @if(auth()->user()->github_username) + @if($collaboratorStatus === 'active') + + View Repo + + @elseif($collaboratorStatus === 'pending') + + @else +
+ @csrf + +
+ @endif +
+ @csrf + @method('DELETE') + +
+ @else + + Connect GitHub + + @endif +
+
+
+
+@endif +
diff --git a/resources/views/livewire/lead-submission-form.blade.php b/resources/views/livewire/lead-submission-form.blade.php new file mode 100644 index 00000000..5ae6acac --- /dev/null +++ b/resources/views/livewire/lead-submission-form.blade.php @@ -0,0 +1,126 @@ +
+ @if ($submitted) +
+ + + +

Thank you for your enquiry!

+

We've received your details and will be in touch soon.

+
+ @else +
+ @error('form') +
+

{{ $message }}

+
+ @enderror + +
+ + + @error('name')

{{ $message }}

@enderror +
+ +
+ + + @error('email')

{{ $message }}

@enderror +
+ +
+ + + @error('company')

{{ $message }}

@enderror +
+ +
+ + + @error('description')

{{ $message }}

@enderror +
+ +
+ + + @error('budget')

{{ $message }}

@enderror +
+ +
+ @if (config('services.turnstile.site_key')) +
+
+
+ @endif + @error('turnstileToken')

{{ $message }}

@enderror +
+ +
+ +
+
+ @endif +
diff --git a/resources/views/livewire/license-renewal-success.blade.php b/resources/views/livewire/license-renewal-success.blade.php new file mode 100644 index 00000000..78485daa --- /dev/null +++ b/resources/views/livewire/license-renewal-success.blade.php @@ -0,0 +1,167 @@ +@if($renewalCompleted) +
+@else +
+@endif +
+
+
+
+ @if($renewalCompleted) + + + + @elseif($renewalFailed) + + + + @else + + + + + @endif +
+ +

+ @if($renewalCompleted) + License Renewal Complete! + @elseif($renewalFailed) + Renewal Processing Failed + @else + Processing Your Renewal... + @endif +

+ +

+ @if($renewalCompleted) + Your automatic renewal has been set up successfully and your license expiry date has been updated. + @elseif($renewalFailed) + There was an issue processing your renewal. Please contact support for assistance. + @else + We're updating your license details. This usually takes a few moments. + @endif +

+
+ +
+ @if($renewalCompleted) +
+
+
+ + + +
+
+

+ Renewal Successful! +

+
+
    +
  • Your license has been successfully renewed
  • +
  • Automatic renewal has been set up for future renewals
  • +
  • Your existing license key continues to work
  • +
  • You'll receive a confirmation email shortly
  • +
+
+
+
+
+ @elseif($renewalFailed) +
+
+
+ + + +
+
+

+ Processing Failed +

+
+

We encountered an issue while processing your renewal. Your payment may have been successful, but we're having trouble updating your license details.

+

Please contact our support team and reference session ID: {{ $sessionId }}

+
+
+
+
+ @else +
+
+
+ + + +
+
+

+ Processing Your Renewal +

+
+

We're currently updating your license details with the new expiry date. This process usually completes within a few minutes.

+

This page will automatically refresh when processing is complete.

+
+
+
+
+ @endif + +
+

License Information:

+
+
+
License Key
+
+ + {{ $license->key }} + +
+
+
+
+ @if($renewalCompleted) + New Expiry Date + @else + Current Expiry + @endif +
+
+ @if($renewalCompleted && $license->expires_at) + + {{ $license->expires_at->format('F j, Y') }} + + + ({{ $license->expires_at->diffForHumans() }}) + + @else + {{ $originalExpiryDate ?: 'N/A' }} + @endif +
+
+
+
+ + + +
+

+ Questions about your renewal? Contact our support team + @if(!$renewalCompleted && !$renewalFailed) +
Session ID: {{ $sessionId }} + @endif +

+
+
+
+
+
diff --git a/resources/views/livewire/mobile-pricing.blade.php b/resources/views/livewire/mobile-pricing.blade.php new file mode 100644 index 00000000..8bf40afc --- /dev/null +++ b/resources/views/livewire/mobile-pricing.blade.php @@ -0,0 +1,425 @@ +
+
+

+ Choose your plan +

+ +

+ Get the most out of NativePHP +

+
+ + {{-- Interval Toggle --}} +
+
+ + +
+ + {{-- Ultra Plan Card --}} +
+ {{-- Plan Name --}} +

+ + Ultra + +

+ + {{-- Price --}} + @if($isEapCustomer) +
+
+ $ +
+
+ +
+
+
+ ${{ $regularYearlyPrice }}/yr + + {{ $eapDiscountPercent }}% off + +
+ @else +
+
+ $ +
+
+ +
+
+ @endif + + {{-- Savings note --}} +
+ @if($isEapCustomer) + Save ${{ $eapSavingsVsMonthly }}/year (compared to monthly pricing) with your Early Access discount + @else + Save $70/year vs monthly + @endif +
+ + {{-- CTA Button --}} + @auth + @if($isAlreadyUltra) +
+ You're on Ultra +
+ @elseif($hasExistingSubscription) + + @else + + @endif + @else + + @endauth + + {{-- Features --}} +
+
+ +
All first-party plugins at no extra cost
+
+
+ +
Keep up to 90% of Marketplace plugin earnings
+
+
+ +
Claude Code Plugin Dev Kit for free
+
+
+ +
Exclusive discounts on future NativePHP products
+
+
+ +
Premium support — private channels, expedited turnaround
+
+
+ +
Teams — invite your whole team to share your Ultra benefits
+
+
+ +
{{ config('subscriptions.plans.max.included_seats') }} seats included
+
+
+
+ + {{-- Upgrade Confirmation Modal --}} + @auth + @if($hasExistingSubscription && !$isAlreadyUltra) + + @endif + @endauth +
+ + @guest + + @endguest +
diff --git a/resources/views/livewire/order-success.blade.php b/resources/views/livewire/order-success.blade.php new file mode 100644 index 00000000..734d2231 --- /dev/null +++ b/resources/views/livewire/order-success.blade.php @@ -0,0 +1,534 @@ +
+ {{-- Hero Section --}} +
+
+ {{-- Primary Heading --}} +

+ You're In! +

+ + {{-- Introduction Description --}} +

+ We're excited to have you join the NativePHP community! +

+
+ + {{-- Success Card --}} +
+
+
+ + +
+
+ Payment Successful! +
+
+ You've purchased a license. +
+
+
+ +
+ @if ($licenseKey) +

+ License key +

+
+
+ + +
+
{{ $licenseKey }}
+
+

+ Store this somewhere safe. You'll need it later. +

+ @if ($email) +

+ Email +

+
+
+ + +
+
{{ $email }}
+
+ @endif + @else +
+
+
+
+ + License registration in progress + +
+

+ Please + + check your email + + shortly for a copy of your license key. This page + will also update if your license key is ready. +

+ +

+ Once you receive your license key, you can start + building amazing mobile apps with NativePHP! +

+ @endif +
+ + +
+ + + + + View Installation Guide +
+
+ + @if ($subscription === \App\Enums\Subscription::Max) +
+
+
+ +
+
+

+ Repo Access +

+
+

+ As a Max subscriber, you have access to + the NativePHP/mobile repository. To + access it, please log in to + + AnyStack.sh + using the same email address you used + for your purchase. +

+
+
+
+
+ @endif +
+
+ + {{-- Next Steps --}} +
+

+ What's Next? +

+ +
+ +
+ + + + + +
+

Install the Package

+

+ Follow our step-by-step guide to install and set up + NativePHP in your Laravel project. +

+
+ + +
+ + + + + + +
+

Join Our Community

+

+ Connect with other developers, get help, and share your + experiences in our Discord community. +

+
+ +
+
+ + + +
+

Build Your First App

+

+ Follow our tutorials to create your first mobile app + using PHP and Laravel. Coming soon. +

+
+ + @if ($subscription === \App\Enums\Subscription::Max) +
+
+ +
+

Access the Repo

+

+ + Create a customer account + + on Anystack to gain access to the NativePHP for + Mobile + + repository + + where you can let us know if you find any bugs as + you build. +

+
+ @endif +
+
+
+
diff --git a/resources/views/livewire/plugin-directory.blade.php b/resources/views/livewire/plugin-directory.blade.php new file mode 100644 index 00000000..73e369a5 --- /dev/null +++ b/resources/views/livewire/plugin-directory.blade.php @@ -0,0 +1,256 @@ +
+ {{-- Header --}} +
+
+

Plugin Marketplace

+

+ Browse all available plugins and bundles for NativePHP Mobile. +

+
+ + {{-- View Toggle --}} + @if ($bundles->isNotEmpty()) +
+
+ + +
+
+ @endif + + {{-- Search --}} +
+
+
+
+ + + +
+ + @if ($search) + + @endif +
+
+
+ + {{-- Active Filters --}} + @if ($search || $authorUser) +
+ @if ($authorUser) + + + + + {{ $authorUser->display_name }} + + + @endif + @if ($search) + + + + + "{{ $search }}" + + + @endif + + {{ $plugins->total() }} {{ Str::plural('result', $plugins->total()) }} + +
+ @endif +
+ + {{-- Content Grid --}} +
+ @if ($view === 'bundles') + {{-- Bundles Grid --}} + @if ($bundles->count() > 0) +
+ @foreach ($bundles as $bundle) + + @endforeach +
+ @else +
+ + + +

No bundles found

+ @if ($search) +

+ No bundles match your search. Try a different term. +

+ + @else +

+ Check back soon for plugin bundles! +

+ @endif +
+ @endif + @else + {{-- Plugins Grid --}} + @if ($plugins->count() > 0) +
+ @foreach ($plugins as $plugin) + + @endforeach +
+ + {{-- Pagination --}} + @if ($plugins->hasPages()) +
+ {{ $plugins->links() }} +
+ @endif + @else +
+ +

No plugins found

+ @if ($search || $authorUser) +

+ @if ($authorUser && $search) + No plugins by {{ $authorUser->display_name }} match your search. + @elseif ($authorUser) + {{ $authorUser->display_name }} hasn't published any plugins yet. + @else + No plugins match your search. Try a different term. + @endif +

+
+ @if ($search) + + @endif + @if ($authorUser) + + @endif +
+ @else +

+ Be the first to submit a plugin to the marketplace! +

+ + Submit a Plugin + + @endif +
+ @endif + @endif +
+ + {{-- Back to plugins landing --}} +
+ +
+ + {{-- Plugin Dev Kit Banner --}} +
+ +
+
+
+ +
+
+

Plugin Dev Kit

+

Build native plugins with Claude Code. Skip the Kotlin & Swift learning curve.

+
+
+
+ Learn More + + + +
+
+
+
+
diff --git a/resources/views/livewire/purchase-modal.blade.php b/resources/views/livewire/purchase-modal.blade.php new file mode 100644 index 00000000..9648b3eb --- /dev/null +++ b/resources/views/livewire/purchase-modal.blade.php @@ -0,0 +1,94 @@ +
+
+ +
+ +
+
+
+

+ Get started with NativePHP +

+

+ Enter your email to continue to checkout +

+
+ +
+
+ + + @error('email') +

+ {{ $message }} +

+ @enderror +
+ +
+ + +
+
+
+
+
+
diff --git a/resources/views/livewire/showcase-submission-form.blade.php b/resources/views/livewire/showcase-submission-form.blade.php new file mode 100644 index 00000000..f91f9857 --- /dev/null +++ b/resources/views/livewire/showcase-submission-form.blade.php @@ -0,0 +1,142 @@ +
+ {{-- Warning for approved submissions being edited --}} + @if ($isEditing && $showcase?->isApproved()) + + Re-review Required + + This submission is currently approved. If you make changes, it will need to be reviewed again before appearing in the showcase. + + + @endif + + {{-- Title Field --}} + + App Name * + + + + + {{-- Description Field --}} + + Description * + + + Maximum 2000 characters. + + + {{-- Main Image Field --}} + + App Icon / Main Image + @if ($existingImage) +
+ Current image + Remove +
+ @endif + + + Max 2MB. Recommended: Square image, at least 256x256px. +
+ + {{-- Screenshots Field --}} + + Screenshots (up to 5) + @if (count($existingScreenshots) > 0) +
+ @foreach ($existingScreenshots as $index => $screenshot) +
+ Screenshot {{ $index + 1 }} + +
+ @endforeach +
+ @endif + @if (count($existingScreenshots) < 5) + + Max 2MB each. You can add {{ 5 - count($existingScreenshots) }} more screenshot(s). + @endif + @error('screenshots.*') {{ $message }} @enderror +
+ + {{-- Platform Selection --}} + + Available Platforms * + +
+ {{-- Mobile Toggle --}} + + + {{-- Mobile Links (shown when hasMobile is true) --}} + @if ($hasMobile) +
+ + App Store URL + + + + + + Play Store URL + + + +
+ @endif + + {{-- Desktop Toggle --}} + + + {{-- Desktop Links (shown when hasDesktop is true) --}} + @if ($hasDesktop) +
+ + Windows Download URL + + + + + + macOS Download URL + + + + + + Linux Download URL + + + +
+ @endif +
+ + +
+ + {{-- Certification Checkbox --}} +
+ + +
+ + {{-- Form Actions --}} +
+
+ @if ($isEditing) + + Delete Submission + + @endif +
+
+ Cancel + + {{ $isEditing ? 'Update Submission' : 'Submit App' }} + +
+
+
diff --git a/resources/views/livewire/sub-license-manager.blade.php b/resources/views/livewire/sub-license-manager.blade.php new file mode 100644 index 00000000..26c46c20 --- /dev/null +++ b/resources/views/livewire/sub-license-manager.blade.php @@ -0,0 +1,203 @@ +
+ +
+
+ + Keys + + ({{ $activeSubLicenses->count() }}{{ $license->subLicenseLimit ? '/' . $license->subLicenseLimit : '' }}) + + + Manage license keys for team members or additional devices. +
+ @if($license->canCreateSubLicense()) + + Create Key + + @endif +
+ + @if($license->subLicenses->isEmpty()) +
+ No keys + Get started by creating your first key. +
+ @else + {{-- Active Sub-Licenses --}} + @if($activeSubLicenses->isNotEmpty()) + + + Key + Assigned To + Actions + + + + @foreach($activeSubLicenses as $subLicense) + + +
+ @if($subLicense->name) + {{ $subLicense->name }} + @endif + +
+
+ + + @if($subLicense->assigned_email) + {{ $subLicense->assigned_email }} + @else + Unassigned + @endif + + + +
+ + Edit + + @if($subLicense->assigned_email) +
+ @csrf + Send License +
+ @endif +
+ @csrf + @method('PATCH') + Suspend +
+
+
+
+ @endforeach +
+
+ @endif + + {{-- Suspended Sub-Licenses --}} + @if($suspendedSubLicenses->isNotEmpty()) +
+ + Suspended Keys + + ({{ $suspendedSubLicenses->count() }}) + + + These keys are permanently suspended and cannot be used or reactivated. + + + + Key + Assigned To + + + + @foreach($suspendedSubLicenses as $subLicense) + + +
+ @if($subLicense->name) + {{ $subLicense->name }} + @endif + +
+
+ + + @if($subLicense->assigned_email) + {{ $subLicense->assigned_email }} + @else + Unassigned + @endif + +
+ @endforeach +
+
+
+ @endif + @endif + + @if(!$license->canCreateSubLicense()) + + + @if($license->remainingSubLicenses === 0) + You have reached the maximum number of keys for this plan. + @elseif($license->is_suspended) + Keys cannot be created for suspended licenses. + @elseif($license->expires_at && $license->expires_at->isPast()) + Keys cannot be created for expired licenses. + @else + Keys cannot be created at this time. + @endif + + + @endif +
+ + {{-- Create Sub-License Modal --}} + +
+ Create Key + +
+
+ + Give your key a descriptive name to help identify its purpose. +
+ +
+ + Assign this license to a team member. +
+
+ +
+ Create Key +
+
+
+ + {{-- Edit Sub-License Modal --}} + +
+ Edit Key + +
+
+ + Give your key a descriptive name to help identify its purpose. +
+ +
+ + Assign this license to a team member. +
+
+ +
+ Update Key +
+
+
+
diff --git a/resources/views/livewire/team-manager.blade.php b/resources/views/livewire/team-manager.blade.php new file mode 100644 index 00000000..88dd3912 --- /dev/null +++ b/resources/views/livewire/team-manager.blade.php @@ -0,0 +1,285 @@ +
+ @if($team->is_suspended) + + Your team is currently suspended. Reactivate your Ultra subscription to restore team benefits. + + @endif + + {{-- Invite Form --}} + @if(!$team->is_suspended) + + Invite a Team Member +
+ @csrf +
+ +
+ Send Invite +
+ @error('email') + {{ $message }} + @enderror +
+ @endif + + {{-- Seat Management --}} + +
+
+ Seats + + {{ $team->occupiedSeatCount() }} of {{ $team->totalSeatCapacity() }} seats used + @if($team->extra_seats > 0) + ({{ $team->extra_seats }} extra) + @endif + +
+ @if(!$team->is_suspended) +
+ + Add Seats + + @if($removableSeats > 0) + + Remove Seats + + @endif +
+ @endif +
+
+ + {{-- Billing Summary --}} + @if($planPrice !== null) + + Billing Summary +
+
+ Ultra subscription + ${{ number_format($planPrice, 2) }}/{{ $billingInterval }} +
+ @if($seatsCost > 0) +
+ Extra seats ({{ $extraSeatsQty }}) + ${{ number_format($seatsCost, 2) }}/{{ $billingInterval }} +
+ @endif +
+ Estimated next bill + ${{ number_format($nextBillTotal, 2) }}/{{ $billingInterval }} +
+ @if($renewalDate) + + Next renewal on {{ $renewalDate }} + + @endif +
+
+ @endif + + {{-- Add Seats Modal --}} + +
+ Add Extra Seats + + Extra seats cost ${{ $extraSeatPrice }}/{{ $extraSeatInterval }} per seat. + +
+ +
+
+
+ × ${{ $extraSeatPrice }}/{{ $extraSeatInterval }} + ${{ $extraSeatPrice }}/{{ $extraSeatInterval }} +
+ @if($proRataFraction < 1) +
+
+ Charged today (pro-rated) + $ +
+ @if($renewalDate) + + Full price applies from {{ $renewalDate }}. + + @endif +
+ @endif +
+
+ + Cancel + + + Confirm + Processing... + +
+
+
+ + {{-- Remove Seats Modal --}} + @if($removableSeats > 0) + +
+ Remove Extra Seats + + You have {{ $removableSeats }} unused extra {{ Str::plural('seat', $removableSeats) }} available for removal. Seats are removed immediately and you'll be credited for the unused time on your next bill. + +
+ +
+
+ + Cancel + + + Remove + Processing... + +
+
+
+ @endif + + {{-- Pending Invitations --}} + @if($pendingInvitations->isNotEmpty()) +
+ + Pending Invitations + {{ $pendingInvitations->count() }} + + + + Email + Invited + Actions + + + + @foreach($pendingInvitations as $invitation) + + {{ $invitation->email }} + + @if($invitation->invited_at) + {{ $invitation->invited_at->diffForHumans() }} + @endif + + +
+
+ @csrf + Resend +
+ Cancel +
+
+
+ @endforeach +
+
+
+ @endif + + {{-- Team Members --}} + Team Members + + @if($activeMembers->isNotEmpty()) + + + Member + Joined + Actions + + + + @foreach($activeMembers as $member) + + +
+ {{ $member->user?->display_name ?? $member->email }} + {{ $member->email }} +
+
+ + @if($member->accepted_at) + {{ $member->accepted_at->diffForHumans() }} + @endif + + +
+ Remove +
+
+
+ @endforeach +
+
+ @else + + @endif + + {{-- Cancel Invitation Confirmation Modal --}} + + Cancel Invitation + + Are you sure you want to cancel the invitation for ? + +
+ + Keep + +
+ @csrf + @method('DELETE') + Cancel Invitation +
+
+
+ + {{-- Remove Member Confirmation Modal --}} + + Remove Team Member + + Are you sure you want to remove from your team? + +
+ + Cancel + +
+ @csrf + @method('DELETE') + Remove +
+
+
+
diff --git a/resources/views/livewire/version-switcher.blade.php b/resources/views/livewire/version-switcher.blade.php new file mode 100644 index 00000000..d2bd01ab --- /dev/null +++ b/resources/views/livewire/version-switcher.blade.php @@ -0,0 +1,30 @@ +
+ + + + + + diff --git a/resources/views/livewire/wall-of-love-submission-form.blade.php b/resources/views/livewire/wall-of-love-submission-form.blade.php new file mode 100644 index 00000000..aa7ea6fd --- /dev/null +++ b/resources/views/livewire/wall-of-love-submission-form.blade.php @@ -0,0 +1,60 @@ +
+ @if(session()->has('success')) + + {{ session('success') }} + + @endif + + @if(! $isEditing) + {{-- Name Field --}} + + Name * + + + + @endif + + {{-- Company Field --}} + + Company (optional) + + + + + {{-- Photo Field --}} + + Photo (optional) + + @if($isEditing && $existingPhoto) +
+ Current photo + Remove photo +
+ @endif + + + +
+ + @if(! $isEditing) + {{-- URL Field --}} + + Website or Social Media URL (optional) + + + + + {{-- Testimonial Field --}} + + Your story or testimonial (optional) + + + Share what you built, how NativePHP helped you, or what you love about the framework. + + @endif + +
+ Cancel + {{ $isEditing ? 'Save Changes' : 'Submit Your Story' }} +
+
diff --git a/resources/views/mail/bundle-granted.blade.php b/resources/views/mail/bundle-granted.blade.php new file mode 100644 index 00000000..2b27dc4e --- /dev/null +++ b/resources/views/mail/bundle-granted.blade.php @@ -0,0 +1,11 @@ + +# Great news! + +You've been granted access to the **{{ $bundle->name }}** bundle, which includes the following plugins: + +@foreach ($grantedPlugins as $plugin) +- [{{ $plugin->name }}]({{ $pluginUrls[$plugin->id] }}) +@endforeach + +Thank you for being a NativePHP customer! + diff --git a/resources/views/mail/product-granted.blade.php b/resources/views/mail/product-granted.blade.php new file mode 100644 index 00000000..8638d352 --- /dev/null +++ b/resources/views/mail/product-granted.blade.php @@ -0,0 +1,11 @@ + +# Great news! + +You've been granted access to **{{ $product->name }}**. + + +Check it out + + +Thank you for being a NativePHP customer! + diff --git a/resources/views/partners.blade.php b/resources/views/partners.blade.php new file mode 100644 index 00000000..9996fd9d --- /dev/null +++ b/resources/views/partners.blade.php @@ -0,0 +1,615 @@ + +
+ {{-- Hero Section --}} +
+
+ {{-- Illustration --}} +
+ +
+ + {{-- Title --}} +

+ + { + + Partner + + } + + with NativePHP +

+ + {{-- Subtitle --}} +

+ We're helping teams just like yours build beautiful mobile + and desktop apps faster than ever! +

+ + {{-- Call to Action Buttons --}} +
+ {{-- Primary CTA - Email --}} + + + {{-- Secondary CTA - Calendar --}} + +
+
+
+ + {{-- Partnership Details Section --}} +
+
+ {{-- Card --}} + + + + + + + + Dedicated Support + + + Get access to the creators of NativePHP, with dedicated + support channels and faster response times. + + + + {{-- Card --}} + + + + + + + + Priority Feature Development + + + Have a direct line to our development team and influence + our feature development to ensure your business needs + are met. + + + + {{-- Card --}} + + + + + + + + Training & Onboarding + + + Comprehensive training and onboarding for your team to + get them up to speed quickly. + + + + {{-- Card --}} + + + + + + + + Premium Plugins + + + Receive free access to all first-party premium plugins + for your team. + + + + {{-- Card --}} + + + + + + + + Development Support + + + Just need someone to build your app? We'll be delighted + to! + + + + {{-- Card --}} + + + + + + + + Strategic Partnership + + + Become a strategic partner in the NativePHP ecosystem + with co-marketing opportunities. + + +
+
+ + {{-- Ideal Partners Section --}} +
+
+

+ Who Should Partner With Us? +

+ +
+
+
+ + + +
+
+

+ App Development Companies +

+

+ Businesses building mobile applications that + want to leverage PHP and Laravel expertise. +

+
+
+ +
+
+ + + +
+
+

+ Digital Agencies +

+

+ Agencies looking to expand their service + offerings with native mobile app development. +

+
+
+ +
+
+ + + +
+
+

+ Freelance Developers +

+

+ PHP/Laravel freelancers who want to offer native + mobile app development to their clients. +

+
+
+
+
+
+ + {{-- Call to Action Section --}} +
+
+

+ Ready to Partner With Us? +

+ +

+ Contact our team to discuss how a partnership with NativePHP + can benefit your business and help you deliver exceptional + applications. +

+ +
+ {{-- Primary CTA - Email --}} + + + {{-- Secondary CTA - Calendar --}} + +
+
+
+
+
diff --git a/resources/views/plugin-license.blade.php b/resources/views/plugin-license.blade.php new file mode 100644 index 00000000..91d9c589 --- /dev/null +++ b/resources/views/plugin-license.blade.php @@ -0,0 +1,82 @@ + +
+
+ {{-- Back button --}} + + + {{-- Title --}} +
+

+ License +

+

+ {{ $plugin->name }} · {{ $plugin->getLicense() ?? 'License' }} +

+
+
+ + {{-- Divider --}} + + + {{-- License Content --}} +
+ {!! $plugin->license_html !!} +
+
+
diff --git a/resources/views/plugin-show.blade.php b/resources/views/plugin-show.blade.php new file mode 100644 index 00000000..6dbe932e --- /dev/null +++ b/resources/views/plugin-show.blade.php @@ -0,0 +1,392 @@ + +
+ @if ($isAdminPreview ?? false) +
+

+ Admin Preview — This plugin is not yet published. Status: {{ $plugin->status->label() }} +

+
+ @endif + +
+ {{-- Blurred circle - Decorative --}} + + + {{-- Back button --}} + + + {{-- Plugin icon and title --}} +
+ @if ($plugin->hasLogo()) + {{ $plugin->name }} logo + @elseif ($plugin->hasGradientIcon()) +
+ +
+ @else +
+ +
+ @endif +
+

+ {{ $plugin->name }} +

+ @if ($plugin->description) +

+ {{ $plugin->description }} +

+ @endif +
+
+
+ + {{-- Divider --}} + + +
+ {{-- Main content - README --}} +
+ @if ($plugin->readme_html) +
+
+ +
+
+ @endif + +
+ @if ($plugin->readme_html) + {!! $plugin->readme_html !!} + @else +
+

+ README not available yet. +

+
+ @endif +
+
+ + {{-- Sidebar - Plugin details --}} + +
+
+
diff --git a/resources/views/plugins.blade.php b/resources/views/plugins.blade.php new file mode 100644 index 00000000..31a7ee47 --- /dev/null +++ b/resources/views/plugins.blade.php @@ -0,0 +1,750 @@ + +
+ {{-- Hero Section --}} +
+
+ {{-- Icon --}} +
+
+ +
+
+ + {{-- Title --}} +

+ Mobile + + { + + Plugins + + } + Rock +

+ + {{-- Subtitle --}} +

+ Extend your NativePHP Mobile apps with powerful native features. + Install with Composer. Build anything for iOS and Android. +

+ + {{-- Call to Action Buttons --}} +
+ {{-- Primary CTA - Browse Plugins --}} + + + {{-- Secondary CTA - Documentation --}} + +
+
+
+ + {{-- Featured Plugins Section --}} + @if ($featuredPlugins->isNotEmpty()) +
+
+

+ Featured Plugins +

+

+ Hand-picked plugins to supercharge your mobile apps. +

+ + {{-- Plugin Cards Grid --}} +
+ @forelse ($featuredPlugins as $plugin) + + @empty +
+ +

+ Featured plugins coming soon +

+
+ + + @endforelse +
+
+
+ @endif + + {{-- Plugin Bundles Section --}} + @if ($bundles->isNotEmpty()) +
+
+

+ Plugin Bundles +

+

+ Save money with curated plugin collections. +

+ + {{-- Bundle Cards Grid --}} +
+ @foreach ($bundles as $bundle) + + @endforeach +
+
+
+ @endif + + {{-- Latest Plugins Section --}} +
+
+

+ Latest Plugins +

+

+ Freshly released plugins from our community. +

+ + {{-- Plugin Cards Grid --}} +
+ @forelse ($latestPlugins as $plugin) + + @empty +
+ +

+ New plugins coming soon +

+
+ + + @endforelse +
+
+
+ + {{-- Benefits Section --}} +
+
+

+ Why Use Plugins? +

+

+ Unlock native capabilities without leaving Laravel. +

+
+ +
+ {{-- Card - Composer Install --}} + + + + + + + + One Command Install + + + Add native features with a single composer require. No Xcode or Android Studio knowledge required. + + + + {{-- Card - Build Anything --}} + + + + + + + + Build Anything + + + There's no limit to what plugins can do. Access any native API, sensor, or hardware feature on iOS and Android. + + + + {{-- Card - Auto-Registered --}} + + + + + + + + Auto-Registered + + + Plugins are automatically discovered and registered. Just enable them in your config and you're ready to go. + + + + {{-- Card - Platform Dependencies --}} + + + + + + + + Native Dependencies + + + Plugins can add Gradle dependencies, CocoaPods, and Swift Package Manager packages automatically. + + + + {{-- Card - Lifecycle Hooks --}} + + + + + + + + Build Lifecycle Hooks + + + Hook into critical moments in the build pipeline. Run custom logic before, during, or after builds. + + + + {{-- Card - Security --}} + + + + + + + + Security First + + + Security is our top priority. Plugins are sandboxed and permissions are explicit, keeping your users safe. + + +
+
+ + {{-- For Plugin Authors Section --}} +
+
+

+ Build & Sell Your Own Plugins +

+ +

+ Know Swift or Kotlin? Create plugins for the NativePHP community and generate revenue from your expertise. +

+ +
+
+
+ + + +
+
+

+ Write Swift & Kotlin +

+

+ Build the native code and PHP bridging layer. We handle the rest, mapping everything so it just works. +

+
+
+ +
+
+ + + +
+
+

+ Full Laravel Power +

+

+ Set permissions, create config files, publish views, and do everything a Laravel package can do. +

+
+
+ +
+
+ + + +
+
+

+ Sell Your Plugins +

+

+ Sell your plugins through our marketplace and earn money from your native development skills. +

+
+
+
+ + +
+
+ + {{-- Plugin Dev Kit Banner --}} +
+ +
+
+
+ +
+
+

Plugin Dev Kit

+

Build native plugins with Claude Code. Skip the Kotlin & Swift learning curve.

+
+
+
+ Learn More + + + +
+
+
+
+ + {{-- Call to Action Section --}} +
+
+

+ Ready to Extend Your App? +

+ +

+ Discover plugins that add powerful native features to your NativePHP Mobile apps, or start building your own today. +

+ +
+ {{-- Primary CTA --}} + + + {{-- Secondary CTA --}} + +
+
+
+
+
diff --git a/resources/views/pricing.blade.php b/resources/views/pricing.blade.php new file mode 100644 index 00000000..51bb572b --- /dev/null +++ b/resources/views/pricing.blade.php @@ -0,0 +1,265 @@ + + {{-- Hero Section --}} +
+
+ {{-- Icon --}} +
+
+ +
+
+ + {{-- Title --}} +

+ NativePHP + { + Ultra + } +

+ + {{-- Subtitle --}} +

+ Premium plugins, tools, and support to supercharge your + NativePHP development. +

+
+
+ + {{-- Pricing Section --}} + + + {{-- FAQ Section --}} +
+

+ Frequently Asked Questions +

+ +
+ +

+ NativePHP Ultra is a premium subscription that gives you + access to all first-party plugins, the Claude Code + Plugin Dev Kit, discounts on NativePHP courses and + apps, Teams support, premium support through private channels with expedited + turnaround times, and up to 90% revenue share on + paid plugins you publish to the Marketplace. +

+
+ + +

+ No! NativePHP for Mobile is completely free. Ultra is + an optional premium subscription for developers who + want access to additional tools, plugins, and + priority support. +

+
+ + +

+ All first-party NativePHP plugins are included with + your Ultra subscription at no additional cost. As we + release new first-party plugins, they will be + automatically available to you. +

+
+ + +

+ The Claude Code Plugin Dev Kit is a set of tools and + resources that help you build NativePHP plugins using + Claude Code. It's available for free to Ultra + subscribers. +

+
+ + +

+ Ultra subscribers get discounts on NativePHP courses + and apps. As we release new educational content and + tools, you'll automatically be eligible for + subscriber pricing. +

+
+ + +

+ Ultra includes Teams support, which lets you invite + other users into your team so they can share your + plugins and other Ultra benefits. As the account + owner, you can manage your team members and remove + access at any time. +

+
+ + +

+ Ultra includes {{ config('subscriptions.plans.max.included_seats') }} team seats. If you need more, extra + seats can be purchased from your team settings page + at ${{ config('subscriptions.plans.max.extra_seat_price_monthly') }}/mo per seat on monthly plans or ${{ config('subscriptions.plans.max.extra_seat_price_yearly') }}/mo per seat + on annual plans. Extra seats are billed pro-rata to + match your subscription cycle. +

+
+ + +

+ Premium support gives you access to private support channels + with expedited turnaround on your issues. When you need + help, your requests are prioritized so you can get back + to building faster. +

+
+ + +

+ You can manage your billing via the Stripe billing + portal. If you'd like to switch between monthly and + annual, you can cancel your current subscription and + start a new one on the billing interval you prefer. +

+
+ + +

+ Yes, you can cancel at any time. You'll continue to + have access to Ultra benefits until the end of your + current billing period. +

+

+ After cancellation, you'll retain access to any + plugins you've purchased through the Marketplace. + However, free access to first-party plugins and + team member benefits will end when the subscription + expires. +

+
+ + +

+ Yes, invoices are sent automatically with your receipt + via email after each payment. +

+
+ + +

+ You can manage your subscription via the + + Stripe billing portal. + +

+
+
+
+
diff --git a/resources/views/privacy-policy.blade.php b/resources/views/privacy-policy.blade.php new file mode 100644 index 00000000..e5b81170 --- /dev/null +++ b/resources/views/privacy-policy.blade.php @@ -0,0 +1,395 @@ + + {{-- Hero --}} +
+
+ {{-- Blurred circle - Decorative --}} + + + {{-- Primary Heading --}} +

+ Privacy Policy +

+ + {{-- Date --}} +
+ Last updated +
+
+ + {{-- Divider --}} + + + {{-- Content --}} +
+

+ Bifrost Technology, LLC operates the nativephp.com website, + which provides the Service. +

+ +

+ This page is used to inform website visitors regarding our + policies with the collection, use, and disclosure of Personal + Information if anyone decided to use our Service, the NativePHP + website. +

+ +

+ If you choose to use our Service, then you agree to the + collection and use of information in relation with this policy. + The Personal Information that we collect are used for providing + and improving the Service. We will not use or share your + information with anyone except as described in this Privacy + Policy. +

+ +

+ The terms used in this Privacy Policy have the same meanings as + in our + Terms of Service + , unless otherwise defined in this Privacy Policy. +

+ +

Owner Of The Website

+ +

+ Bifrost Technology, LLC +
+ 1111B S Governors Ave STE 2838 +
+ Dover +
+ Delaware +
+ 19904 +

+ +

support@nativephp.com

+ +

Information Collection and Use

+ +

+ For a better experience while using our Service, we may require + you to provide us with certain personally identifiable + information, including but not limited to your name and email. + We only ask for the essential information we need to operate. +

+ +

Log Data

+

+ We want to inform you that whenever you visit our Service, we + collect information that your browser sends to us that is called + Log Data. This Log Data may include information such as your + computer’s Internet Protocol ("IP") address, browser version, + pages of our Service that you visit, the time and date of your + visit, the time spent on those pages, and other statistics. +

+ +

Cookies

+

+ Cookies are files with small amount of data that is commonly + used an anonymous unique identifier. These are sent to your + browser from the website that you visit and are stored on your + computer’s hard drive. +

+ +

+ Our website uses these "cookies" to collection information and + to improve our Service. You have the option to either accept or + refuse these cookies, and know when a cookie is being sent to + your computer. If you choose to refuse our cookies, you may not + be able to use some portions of our Service. +

+ +

+ For more general information on cookies, please read + + https://www.cookiesandyou.com + + . +

+ +

Service Providers

+

+ We may employ third-party companies and individuals due to the + following reasons: +

+ +
    +
  • To facilitate our Service;
  • +
  • To provide the Service on our behalf;
  • +
  • To perform Service-related services;
  • +
  • or To assist us in analyzing how our Service is used.
  • +
+ +

+ We want to inform our Service users that these third parties + have access to your Personal Information. The reason is to + perform the tasks assigned to them on our behalf. However, they + are obligated not to disclose or use the information for any + other purpose. +

+ +

+ For further information, you should consult the privacy policies + of these third-parties directly. +

+ +

Security

+

+ We value your trust in providing us your Personal Information, + thus we are striving to use commercially acceptable means of + protecting it. But remember that no method of transmission over + the internet, or method of electronic storage is 100% secure and + reliable, and we cannot guarantee its absolute security. +

+ +

Links to Other Sites

+

+ Our Service may contain links to other sites. If you click on a + third-party link, you will be directed to that site. Note that + these external sites are not operated by us. Therefore, we + strongly advise you to review the Privacy Policy of these + websites. We have no control over, and assume no responsibility + for the content, privacy policies, or practices of any + third-party sites or services. +

+ +

Children's Privacy

+

+ Our Services do not address anyone under the age of 13. We do + not knowingly collect personal identifiable information from + children under 13. In the case we discover that a child under 13 + has provided us with personal information, we immediately delete + this from our servers. If you are a parent or guardian and you + are aware that your child has provided us with personal + information, please contact us so that we will be able to take + the necessary actions. +

+ +

Third-Party Plugin Purchases

+ +

+ The Platform offers plugins developed by independent third-party + developers ("Third-Party Plugins"). When you purchase a + Third-Party Plugin through the Marketplace, certain personal + data may be shared with the plugin developer to facilitate the + transaction and enable them to provide support for their plugin. +

+ +

This data may include:

+ +
    +
  • Your name and email address;
  • +
  • License information associated with your purchase;
  • +
  • + Technical information necessary for plugin delivery and + activation. +
  • +
+ +

+ This data sharing constitutes a legitimate interest under + applicable data protection laws, as it is necessary to fulfil + the transaction and enable the developer to deliver and support + their plugin. We require third-party plugin developers to handle + your data in compliance with applicable data protection + regulations. +

+ +

+ We encourage you to review the privacy policies of third-party + plugin developers before making a purchase. We are not + responsible for the data practices of third-party developers + beyond the requirements set out in our + + Plugin Developer Terms and Conditions. +

+ +

Changes to This Privacy Policy

+

+ We may update our Privacy Policy from time to time. Thus, we + advise you to review this page periodically for any changes. We + will notify you of any changes by posting the new Privacy Policy + on this page. These changes are effective immediately, after + they are posted on this page. +

+ +

Cookie Policy

+

+ Bifrost Technology, LLC ("us", "we", or "our") uses cookies on + nativephp.com (the "Service"). By using the Service, you consent + to the use of cookies. +

+ +

+ Our Cookie Policy explains what cookies are, how we use cookies, + how third-parties we may partner with may use cookies on the + Service, your choices regarding cookies and further information + about cookies. +

+ +

What Are Cookies

+

+ Cookies are small pieces of text sent to your web browser by a + website you visit. A cookie file is stored in your web browser + and allows the Service or a third-party to recognize your + browser and make your next visit easier and the Service more + useful to you. +

+ +

+ Cookies can be "persistent" - they are persisted between + browsing sessions - or "session" cookies, which are deleted by + your browser when you end your session or after a certain time + limit. +

+ +

+ Cookies are associated with the browser, not the person, so they + do not usually store sensitive information about you such as + credit cards or bank details, photographs or personal + information etc. The data they keep are of a technical nature, + statistics, personal preferences, personalization of contents + etc. +

+ +

How We Use Cookies

+

+ When you use and access the Service, we may place a number of + cookie files in your web browser. +

+ +

We use cookies for the following purposes:

+
    +
  • to enable certain functions of the Service,
  • +
  • to provide analytics,
  • +
  • to store your preferences,
  • +
  • to enable advertisements delivery.
  • +
+ +

+ We use both session and persistent cookies on the Service and we + use different types of cookies to run the Service: +

+ +
    +
  • + Essential/Technical Cookies +

    + These allow the proper functioning of the web features. + Allow the user to navigate through a web page, platform + or application and the use of different options or + services that exist in it, such as controlling traffic + and data communication, identifying the session, access + restricted access parts, remember the elements that make + up an order, perform the purchase process of an order, + make the request for registration or participation in an + event, use security elements during navigation, store + contents for dissemination of videos or sound or share + content through social networks. +

    +
  • +
  • + Analysis Cookies +

    + Those that are well treated by us or by third parties, + allow us to quantify the number of users and thus + perform the measurement and statistical analysis of the + use made by users of the service offered. For this, your + browsing on our website is analyzed in order to improve + the offer of products or services we offer. +

    +
  • +
  • + Third-party Cookies +

    + The Site may use third-party services that, on behalf of + Google, will collect information for statistical + purposes, the use of the site by the user and for the + provision of other services related to the website + activity and other services from Internet. +

    +
  • +
+ +

Manage And Reject

+

+ At any time, you can adapt the browser settings to manage, + disregard the use of Cookies and be notified before they are + downloaded. +

+ +

+ If you'd like to delete cookies or instruct your web browser to + delete or refuse cookies, please visit the help pages of your + web browser. +

+ +

+ Please note, however, that if you delete cookies or refuse to + accept them, you might not be able to use all of the features we + offer, you may not be able to store your preferences, and some + of our pages might not display properly. +

+
+
+
diff --git a/resources/views/products/show.blade.php b/resources/views/products/show.blade.php new file mode 100644 index 00000000..c6e0c0d6 --- /dev/null +++ b/resources/views/products/show.blade.php @@ -0,0 +1,469 @@ + +
+
+ {{-- Blurred circle - Decorative --}} + + + {{-- Back button --}} + + + {{-- Product icon and title --}} +
+ @if ($product->logo_path) + {{ $product->name }} + @else +
+ +
+ @endif +
+

+ {{ $product->name }} +

+

+ Build native NativePHP plugins with AI assistance +

+
+
+
+ + {{-- Divider --}} + + + {{-- Session Messages --}} + @if (session('error')) +
+

{{ session('error') }}

+
+ @endif + + @if (session('success')) +
+

{{ session('success') }}

+
+ @endif + +
+ {{-- Main content --}} +
+ {{-- Hero Section --}} +
+

+ Skip the Kotlin & Swift learning curve +

+

+ The Plugin Dev Kit is a Claude Code plugin that scaffolds complete NativePHP plugin packages for you. Describe what you want in plain English and get production-ready native code for both platforms. +

+
+ + {{-- Installation --}} +

Install in 2 Steps

+
+
+
# Add the NativePHP plugin registry
+claude plugin marketplace add nativephp/claude-code
+
+# Install the Plugin Dev Kit
+claude plugin install nativephp-plugin-dev
+
+
+

+ That's it. The plugin is now available in every Claude Code session. +

+ + {{-- How to Use --}} +

Using the Plugin

+

+ Once installed, just tell Claude what kind of plugin you want to build: +

+
+
+

Example prompts:

+
    +
  • + » + "Use the nativephp-plugin-dev plugin to create a barcode scanner plugin using ML Kit" +
  • +
  • + » + "Create a NativePHP plugin that wraps the HealthKit API for step counting" +
  • +
  • + » + "Build me a Bluetooth Low Energy plugin for NativePHP Mobile" +
  • +
+
+
+ + {{-- What It Creates --}} +

What It Creates

+

+ Every scaffolded plugin is a complete, ready-to-develop Composer package with everything wired up: +

+
+
+
+ + + +
+
+

PHP Class & Laravel Facade

+

Service provider, facade, and public API

+
+
+
+
+ + + +
+
+

Kotlin (Android)

+

Bridge functions, Activities & Services

+
+
+
+
+ + + +
+
+

Swift (iOS)

+

Bridge functions & ViewControllers

+
+
+
+
+ + + +
+
+

nativephp.json Manifest

+

Dependencies, features & metadata

+
+
+
+
+ + + +
+
+

JavaScript Bridge

+

Client-side module for WebView calls

+
+
+
+
+ + + +
+
+

Permissions & Entitlements

+

Info.plist, AndroidManifest intents & activities

+
+
+
+
+ + + +
+
+

Lifecycle Hooks

+

Build pipeline hooks for custom logic

+
+
+
+
+ + + +
+
+

Readme, Docs & AI Guidelines

+

Documentation and Boost AI context

+
+
+
+

+ The agents also understand how to broadcast events from native code back to your Laravel app and WebView, so your app can react to native callbacks in real time. +

+ + {{-- Tips --}} +

Tips for Best Results

+ +

Tell it which SDK you're wrapping

+

+ If you're wrapping a specific native SDK, link to its documentation so the agent can reference the API surface. For example: "Build a plugin wrapping Google ML Kit for text recognition." +

+ +

Specify the package manager dependency

+

+ Tell the agent exactly which native packages to use so it can configure the manifest correctly: +

+
    +
  • Gradle: com.google.mlkit:text-recognition:16.0.0
  • +
  • CocoaPods: GoogleMLKit/TextRecognition
  • +
  • Swift Package Manager: provide the repository URL
  • +
+ +

Be specific about what you need

+

+ The more detail you give, the better the result. Mention specific features, permissions, or platform behaviors you care about. +

+ + {{-- Private Repository --}} + @if ($product->github_repo) +

Private Repository Access

+

+ Your purchase includes access to the private nativephp/{{ $product->github_repo }} repository containing: +

+
    +
  • Complete plugin examples — Real-world plugins you can learn from and customize
  • +
  • Agent definition files — Install directly into your Claude Code environment
  • +
  • Reference implementations — Camera, ML Kit, Bluetooth, and more
  • +
  • Ongoing updates — New agents and examples as NativePHP evolves
  • +
+ @endif + + {{-- Who It's For --}} +

Perfect For

+
    +
  • Laravel developers who want to extend NativePHP without learning native development
  • +
  • Teams building custom functionality for their mobile apps
  • +
  • Agencies delivering NativePHP projects with native integrations
  • +
  • Plugin authors who want to ship faster and with fewer bugs
  • +
+
+ + {{-- Sidebar --}} + +
+
+
diff --git a/resources/views/showcase.blade.php b/resources/views/showcase.blade.php new file mode 100644 index 00000000..19a59e58 --- /dev/null +++ b/resources/views/showcase.blade.php @@ -0,0 +1,177 @@ + + {{-- Hero Section --}} +
+
+ {{-- Blurred circle - Decorative --}} + + + {{-- Primary Heading --}} +

+
+
+ @if($platform === 'mobile') + Mobile + @elseif($platform === 'desktop') + Desktop + @else + App + @endif +
+
+
+
+ Showcase +
+
+

+ +
+
+
+
+
+
+ + {{-- Description --}} +

+ Discover amazing {{ $platform ?? '' }} apps built by the NativePHP community. From productivity tools to creative applications, see what's possible with NativePHP. +

+ + {{-- Platform Filter --}} + +
+
+ + {{-- Showcase Grid --}} +
+ @if ($showcases->count() > 0) +
+ @foreach ($showcases as $showcase) + + @endforeach +
+ + {{-- Pagination --}} + @if ($showcases->hasPages()) +
+ {{ $showcases->links() }} +
+ @endif + @else +
+
+
🚀
+

+ No Apps Yet +

+

+ @if($platform) + No {{ $platform }} apps have been showcased yet. Be the first to submit yours! + @else + The showcase is empty. Be the first to submit your NativePHP app! + @endif +

+
+
+ @endif +
+ + {{-- CTA Section --}} +
+
+

+ Built something with NativePHP? +

+

+ We'd love to feature your app in our showcase. Share your creation with the NativePHP community! +

+ @auth + + Submit Your App + + @else + + Log in to Submit + + @endauth +
+
+
diff --git a/resources/views/sponsoring.blade.php b/resources/views/sponsoring.blade.php new file mode 100644 index 00000000..01e68268 --- /dev/null +++ b/resources/views/sponsoring.blade.php @@ -0,0 +1,164 @@ + + {{-- Hero --}} +
+
+ {{-- Blurred circle - Decorative --}} + + + {{-- Primary Heading --}} +

+ Support NativePHP +

+
+ + {{-- Divider --}} + + + {{-- Content --}} +
+

+ NativePHP is managed by Bifrost Technology, LLC and backed by a team of dedicated + employees, maintainers, and contributors who commit their time + to ensure its continued development and improvement. +

+ +

+ We operate two major open source projects — NativePHP for Desktop and + NativePHP for Mobile — as well as Bifrost, + an optional paid SaaS that complements NativePHP by providing cloud-based build services. +

+ +

Sponsorship

+ +

+ NativePHP is free and open source. We encourage and appreciate any + contributions to the project, whether it's through code, + documentation, spreading the word, or a financial sponsorship. + We provide the following ways of making an easy financial + contribution: +

+ + + +

+ All contributions are welcome, at any amount, as a one-off + payment or on a recurring schedule. These funds are used to + support the maintainers and cover development costs. +

+ +

+ All monthly sponsors above $10/month will be bestowed the + Sponsor + role on the NativePHP + Discord + , granting access to private channels, early access to new + releases, and discounts on future premium services. +

+ +

NativePHP Ultra

+ +

+ Another way to support NativePHP is by subscribing to + NativePHP Ultra. + Ultra is our premium subscription plan that gives you access to exclusive benefits while + directly funding the continued development of NativePHP: +

+ +
    +
  • Teams — up to {{ config('subscriptions.plans.max.included_seats') }} seats (you + {{ config('subscriptions.plans.max.included_seats') - 1 }} collaborators) to share your plugin access
  • +
  • Free official plugins — every NativePHP-published plugin, included with your subscription
  • +
  • Plugin Dev Kit — tools and resources to build and publish your own plugins
  • +
  • 90% Marketplace revenue — keep up to 90% of earnings on paid plugins you publish
  • +
  • Priority support — get help faster when you need it
  • +
  • Early access — be first to try new features and plugins
  • +
  • Exclusive discounts — on future NativePHP products
  • +
  • Shape the roadmap — your feedback directly influences what we build next
  • +
+ +

+ Ultra is available with annual or monthly billing. + See Ultra plans. +

+ +

Corporate Partners

+ +

+ If your organization is using NativePHP, we strongly encourage + you to consider a Corporate Sponsorship. This level of support + will provide your team with the added benefits of increased + levels of support, hands-on help directly from the maintainers + of NativePHP and promotion of your brand as a supporter of + cutting-edge open source work. +

+ +

+ For more details, please view our + partners page + or email us at + + partners@nativephp.com + + . +

+
+
+
diff --git a/resources/views/support/index.blade.php b/resources/views/support/index.blade.php new file mode 100644 index 00000000..8379b1fd --- /dev/null +++ b/resources/views/support/index.blade.php @@ -0,0 +1,92 @@ + + {{-- Support Grid Section --}} +
+ {{-- Header --}} +
+

Support

+

+ Get help with NativePHP through our various support channels. +

+
+ + {{-- Additional Support Information --}} +
+

Read the docs

+

+ Before reaching out for help, take a look through our documentation. It's concise by design — you can read the whole thing in under an hour — and most questions are answered there. +

+
+ + {{-- Priority Support --}} +
+
+
+ + + +
+
+

Priority Support

+ @if (auth()->check() && auth()->user()->hasUltraAccess()) +

As an Ultra subscriber, you have access to priority support. Submit a ticket and our team will get back to you as quickly as possible.

+ + + + + Submit a Ticket + + @else +

Need direct help from the NativePHP team? Priority support with ticket-based assistance is available exclusively to Ultra subscribers.

+ + Learn about Ultra + + @endif +
+
+
+ + {{-- Support Grid --}} + +
+
diff --git a/resources/views/terms-of-service.blade.php b/resources/views/terms-of-service.blade.php new file mode 100644 index 00000000..932044eb --- /dev/null +++ b/resources/views/terms-of-service.blade.php @@ -0,0 +1,338 @@ + + {{-- Hero --}} +
+
+ {{-- Blurred circle - Decorative --}} + + + {{-- Primary Heading --}} +

+ Terms of Service +

+ + {{-- Date --}} +
+ Last updated +
+
+ + {{-- Divider --}} + + + {{-- Content --}} +
+

+ Please read these Terms of Service ("Terms", "Terms and + Conditions", "Terms of Use") carefully before using the + nativephp.com website (the "Service", "Platform") operated by + Bifrost Technology, LLC ("us", "we", or "our"). +

+ +

+ Your access to and use of the Service is conditional, based on + your acceptance of and compliance with these Terms. These Terms + apply to all visitors, users and others who access or use the + Service. +

+ +

+ By accessing or using the Service you agree to be bound by these + Terms. If you disagree with any part of the terms, you may not + access the Service. You should not create an account and you + should leave this website. +

+ +

Code Of Conduct

+
    +
  • No illegal activities
  • +
  • No fraud
  • +
  • No spam and data mining
  • +
  • No advertising
  • +
  • No exploitation
  • +
  • No impersonation
  • +
  • No activities related to bots
  • +
  • No use other than the intended
  • +
+ +

+ Any violation to this basic and common sense rules means the + deletion of your account and associated data. +

+ +

+ Whether conduct violates our Code of Conduct will be determined + in our sole discretion. +

+ +

Age Restriction

+

+ The platform is available to persons 18 years old or older. If + you are between 13 and 18 years old, you may still use the + Platform, but you must have a parent's or guardian's permission. +

+ +

+ By using the Platform, you confirm that you are least 18 years + old, or 13 years old with the permission of your parents or + guardians and that you can provide proof of this permission on + request. +

+ +

+ If you are under 13 years old, you may not use our Platform in + any manner. +

+ +

Intellectual Property

+

+ The content on the Platform, including all information, + software, technology, data, logos, marks, designs, text, + graphics, pictures, audio and video files, other data or + copyrightable materials or content, and their selection and + arrangement, is referred to herein as "NativePHP Content", and + is and remains the sole property of Bifrost Technology, LLC. + NativePHP Content, including our trademarks, may not be modified + by you in any way. +

+ +

Account Ownership

+ +

+ We have the right to request additional information from You to + determine account ownership. +

+ +

+ The information that We may request to assist in resolving + ownership disputes includes, but is not limited to, the + following: +

+
    +
  • A copy of Your photo ID
  • +
  • Your billing information and details
  • +
  • Certified copies of your tax forms
  • +
+ +

+ We reserve the right to determine the account ownership in its + sole judgment, and the ability to transfer the account to the + rightful owner, unless otherwise prohibited by law. +

+ +

Refund Policy

+ +

Platform Subscription

+

+ Due to the nature of the Service, no returns are accepted for + platform subscriptions. Refunds are offered on a case-by-case + basis at our sole discretion. +

+ +

Plugin Purchases

+

+ You may request a refund for any third-party Plugin purchase + within fourteen (14) days of the original purchase date. Refund + requests must be submitted to + support@nativephp.com. + After fourteen (14) days, all Plugin sales are final. Refunds + are processed at our sole discretion and may be declined if the + Plugin was used in a production application or if the refund + request is deemed abusive. +

+ +

Publicity

+

+ You grant Us the right to include Your company's name and/or + logo as a customer on our website and other advertising and + promotional materials. You may retract this right by giving + written notice to + support@nativephp.com + . +

+ +

+ Within thirty business days after such notice, We will remove + Your company's name from nativephp.com and will no longer + include the name/logo in any of Our advertising or promotional + materials. +

+ +

Cancellation And Deletion

+ +

+ If You cancel a paid plan, the cancellation will become + effective at the end of the then-current billing cycle. When You + cancel a paid plan, Your account will revert to a free account + and We may disable access to features available only to paid + plan users. +

+ +

You may delete Your account at any time.

+ +

+ Accounts on paid plans will be considered active accounts unless + You explicitly ask us to delete Your account. +

+ +

+ If Your account is deleted, Your Content may no longer be + available and all licenses granted will terminate. +

+ +

+ We are not responsible for the loss of such content upon + deletion. +

+ +

+ We shall not be liable to any party in any way for the inability + to access Content arising from any cancellation or deletion, + including any claims of interference with business or + contractual relations. +

+ +

Third-Party Plugins

+ +

+ The Platform may offer plugins, extensions, or add-ons developed + by independent third-party developers ("Third-Party Plugins"). + Third-Party Plugins are not developed, maintained, or supported + by Bifrost Technology, LLC. +

+ +

+ We make no representations or warranties regarding the quality, + performance, reliability, security, or compatibility of any + Third-Party Plugin. Third-Party Plugins are provided "as-is" + and your use of them is at your own risk. +

+ +

+ We are not liable for any damages, losses, or issues arising + from your use of or reliance on any Third-Party Plugin, + including but not limited to data loss, system failures, + security vulnerabilities, or incompatibility with your + applications. The developer of the Third-Party Plugin is solely + responsible for its functionality, support, and any claims + arising from its use. +

+ +

+ Third-party plugin developers are subject to separate + Plugin Developer Terms and Conditions. +

+ +

Warranties

+

+ nativephp.com is provided as-is. We cannot guarantee that + unexpected errors will not prevent normal use of the Service and + that the software wil be accessible 100% of the time, although + every effort is made to reduce the likelihood of such issues. +

+ +

+ We reserve the right to amend the Platform, and any service or + material we provide on the Platform, in our sole discretion + without notice. We will not be liable if for any reason all or + any part of the Platform is unavailable at any time or for any + period. +

+ +

Changes

+

+ We reserve the right, at our sole discretion, to modify or + replace these Terms at any time. If a revision is made we will + try to provide at least 30 days notice prior to any new terms + taking effect. What constitutes a material change will be + determined at our sole discretion. +

+ +

Waivers

+

+ No delay or failure to exercise any right or remedy provided for + in this Agreement will be deemed to be a waiver. +

+ +

Severability

+

+ If any provision of this Agreement is held invalid or + unenforceable, for any reason, by any arbitrator, court or + governmental agency, department, body or tribunal, the remaining + provisions will remain in effect. +

+ +

Governing Law

+

+ This Agreement will be governed by and construed in accordance + with the laws of the United States of America. +

+ +

Contact

+

+ If you have any questions regarding these or the practices of + this Site, please contact us at + support@nativephp.com + . +

+
+
+
diff --git a/resources/views/the-vibes.blade.php b/resources/views/the-vibes.blade.php new file mode 100644 index 00000000..c29af52a --- /dev/null +++ b/resources/views/the-vibes.blade.php @@ -0,0 +1,973 @@ + +
+ {{-- Hero Section --}} +
+
+ {{-- Badge --}} +
+ + + + July 30, 2026 · Boston, MA +
+ + {{-- Title --}} +

+ The + Vibes +

+ + {{-- Subtitle --}} +

+ Don't let the energy end after Laracon. Join us for Day 3 — + an intimate, community-powered gathering to keep the momentum going. +

+ + {{-- CTA --}} +
+ + Grab Your Spot — $89 + + Only 100 spots available +
+
+
+ + {{-- Early Bird Countdown --}} +
+
+
+ {{-- Price Info --}} +
+

+ Early Bird Pricing +

+
+ $89 + $129 +
+

+ Price increases April 1st +

+
+ + {{-- Countdown --}} +
+
+ 00 + Days +
+ : +
+ 00 + Hours +
+ : +
+ 00 + Mins +
+ : +
+ 00 + Secs +
+
+ + {{-- CTA --}} + + Lock In $89 + +
+
+
+ + {{-- Hero Image --}} +
+
+ People at a lively social event +
+
+ + + + {{-- What Is It Section --}} +
+
+
+ {{-- Text --}} +
+

+ What is The Vibes? +

+ +

+ Laracon US is two days of incredible talks, new connections, and pure + excitement for building with Laravel. But when it's over, you're left + buzzing with ideas and wanting more. +

+ +

+ The Vibes is a curated, single-day gathering on the day after Laracon — + a place to decompress, collaborate, and ride that wave of energy just a little + longer. Think local Boston catering, plenty of Coke Zero's, good company, + and the kind of conversations that happen when you put 100 passionate + developers in a room together. +

+
+ + {{-- Image --}} +
+ The Vibes event atmosphere +
+
+
+
+
+ + {{-- What's Included Section --}} +
+

+ What's Included +

+ +
+ {{-- Catering --}} + + + + + + + Catered Food & Drinks + + Local Boston food, snacks, and drinks all day long. Yes, there will be plenty of Coke Zero's. + + + + {{-- Networking --}} + + + + + + + Community Networking + + An intimate setting with 100 like-minded developers. Real conversations, not small talk. + + + + {{-- Atmosphere --}} + + + + + + + The Atmosphere + + A curated space designed to keep the Laracon energy alive. Relaxed, creative, and fun. + + + + {{-- Sponsors --}} + + + + + + + Sponsor Surprises + + Our sponsors will be bringing some extras to the table. Stay tuned for announcements. + + + + {{-- Collaboration --}} + + + + + + + Open Collaboration + + Space to hack on ideas, pair program, or just geek out about what you learned at Laracon. + + + + {{-- Exclusive --}} + + + + + + + Limited & Exclusive + + Only 100 spots. This isn't a massive conference — it's a handpicked gathering of the community. + + +
+
+ + {{-- Photo Break --}} +
+
+ Crowd gathered at a SoWa Power Station event +
+
+

+ 100 devs. One room. All vibes. +

+
+
+
+ + {{-- Venue Gallery Section --}} +
+

+ The Venue +

+ + {{-- Featured Image --}} +
+ Atrium lounge area +
+ + {{-- Thumbnail Grid --}} +
+ +
+ + {{-- Lightbox Overlay --}} + +
+ + + + {{-- Event Details Section --}} +
+

+ Event Details +

+ +
+ {{-- Date & Time --}} +
+
+
+ + + +
+

Date & Time

+
+

+ Thursday, July 30, 2026 +

+

+ 9:00 AM – 4:00 PM +

+
+ + {{-- Location --}} +
+
+
+ + + + +
+

Location

+
+

+ Loft on Two +

+

+ One Financial Center, Boston, MA 02111 +

+
+ + {{-- Price --}} +
+
+
+ + + +
+

Ticket Price

+
+
+

$89 per person

+ $129 +
+

+ Early bird pricing until April 1st. Includes catering, drinks, and full event access. +

+
+ + {{-- Capacity --}} +
+
+
+ + + +
+

Capacity

+
+

+ 100 attendees +

+

+ Limited to keep it personal. Once they're gone, they're gone. +

+
+
+
+ + {{-- Sponsors Section --}} +
+

+ Sponsored By +

+ +
+ {{-- Web Mavens --}} + +
+
+ Web Mavens +
+ + {{-- Nexcalia --}} + +
+
+ Nexcalia +
+ + {{-- Bifrost Technology --}} + +
+ +
+ Bifrost Technology +
+ + {{-- Beyond Code --}} + +
+ BeyondCode logo + +
+ Beyond Code +
+
+ + {{-- Become a Sponsor CTA --}} +
+

+ Interested in sponsoring The Vibes? +

+ + + + + + Get in Touch + +
+
+ + {{-- Bottom CTA --}} +
+
+

+ Don't Let the + Vibes + End +

+ +

+ Laracon gives you the spark. The Vibes keeps it lit. Grab your spot before they sell out. +

+ +
+ + Get Your Ticket — $89 + + + Early bird $89 · $129 after April 1st · 100 spots + +
+
+
+
+
diff --git a/resources/views/vendor/mail/html/button.blade.php b/resources/views/vendor/mail/html/button.blade.php new file mode 100644 index 00000000..050e969d --- /dev/null +++ b/resources/views/vendor/mail/html/button.blade.php @@ -0,0 +1,24 @@ +@props([ + 'url', + 'color' => 'primary', + 'align' => 'center', +]) + + + + + diff --git a/resources/views/vendor/mail/html/footer.blade.php b/resources/views/vendor/mail/html/footer.blade.php new file mode 100644 index 00000000..3ff41f89 --- /dev/null +++ b/resources/views/vendor/mail/html/footer.blade.php @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/resources/views/vendor/mail/html/header.blade.php b/resources/views/vendor/mail/html/header.blade.php new file mode 100644 index 00000000..78e0dd09 --- /dev/null +++ b/resources/views/vendor/mail/html/header.blade.php @@ -0,0 +1,8 @@ +@props(['url']) + + + + + + + diff --git a/resources/views/vendor/mail/html/layout.blade.php b/resources/views/vendor/mail/html/layout.blade.php new file mode 100644 index 00000000..037efe3e --- /dev/null +++ b/resources/views/vendor/mail/html/layout.blade.php @@ -0,0 +1,58 @@ + + + +{{ config('app.name') }} + + + + + +{!! $head ?? '' !!} + + + + + + + + + + diff --git a/resources/views/vendor/mail/html/message.blade.php b/resources/views/vendor/mail/html/message.blade.php new file mode 100644 index 00000000..27d6e0ca --- /dev/null +++ b/resources/views/vendor/mail/html/message.blade.php @@ -0,0 +1,27 @@ + +{{-- Header --}} + + +{{ config('app.name') }} + + + +{{-- Body --}} +{!! $slot !!} + +{{-- Subcopy --}} +@isset($subcopy) + + +{!! $subcopy !!} + + +@endisset + +{{-- Footer --}} + + +© {{ date('Y') }} Bifrost Technology LLC. {{ __('All rights reserved.') }} + + + diff --git a/resources/views/vendor/mail/html/panel.blade.php b/resources/views/vendor/mail/html/panel.blade.php new file mode 100644 index 00000000..2975a60a --- /dev/null +++ b/resources/views/vendor/mail/html/panel.blade.php @@ -0,0 +1,14 @@ + + + + + + diff --git a/resources/views/vendor/mail/html/subcopy.blade.php b/resources/views/vendor/mail/html/subcopy.blade.php new file mode 100644 index 00000000..790ce6c2 --- /dev/null +++ b/resources/views/vendor/mail/html/subcopy.blade.php @@ -0,0 +1,7 @@ + + + + + diff --git a/resources/views/vendor/mail/html/table.blade.php b/resources/views/vendor/mail/html/table.blade.php new file mode 100644 index 00000000..a5f3348b --- /dev/null +++ b/resources/views/vendor/mail/html/table.blade.php @@ -0,0 +1,3 @@ +
+{{ Illuminate\Mail\Markdown::parse($slot) }} +
diff --git a/resources/views/vendor/mail/html/themes/default.css b/resources/views/vendor/mail/html/themes/default.css new file mode 100644 index 00000000..6523b40d --- /dev/null +++ b/resources/views/vendor/mail/html/themes/default.css @@ -0,0 +1,297 @@ +/* Base */ + +body, +body *:not(html):not(style):not(br):not(tr):not(code) { + box-sizing: border-box; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif, + 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; + position: relative; +} + +body { + -webkit-text-size-adjust: none; + background-color: #ffffff; + color: #52525b; + height: 100%; + line-height: 1.4; + margin: 0; + padding: 0; + width: 100% !important; +} + +p, +ul, +ol, +blockquote { + line-height: 1.4; + text-align: start; +} + +a { + color: #18181b; +} + +a img { + border: none; +} + +/* Typography */ + +h1 { + color: #18181b; + font-size: 18px; + font-weight: bold; + margin-top: 0; + text-align: start; +} + +h2 { + font-size: 16px; + font-weight: bold; + margin-top: 0; + text-align: start; +} + +h3 { + font-size: 14px; + font-weight: bold; + margin-top: 0; + text-align: left; +} + +p { + font-size: 16px; + line-height: 1.5em; + margin-top: 0; + text-align: left; +} + +p.sub { + font-size: 12px; +} + +img { + max-width: 100%; +} + +/* Layout */ + +.wrapper { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + background-color: #fafafa; + margin: 0; + padding: 0; + width: 100%; +} + +.content { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + margin: 0; + padding: 0; + width: 100%; +} + +/* Header */ + +.header { + padding: 25px 0; + text-align: center; +} + +.header a { + color: #18181b; + font-size: 19px; + font-weight: bold; + text-decoration: none; +} + +/* Logo */ + +.logo { + height: auto; + margin-top: 15px; + margin-bottom: 10px; + max-width: 150px; + width: 150px; +} + +/* Body */ + +.body { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + background-color: #fafafa; + border-bottom: 1px solid #fafafa; + border-top: 1px solid #fafafa; + margin: 0; + padding: 0; + width: 100%; +} + +.inner-body { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 570px; + background-color: #ffffff; + border-color: #e4e4e7; + border-radius: 4px; + border-width: 1px; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1); + margin: 0 auto; + padding: 0; + width: 570px; +} + +.inner-body a { + word-break: break-all; +} + +/* Subcopy */ + +.subcopy { + border-top: 1px solid #e4e4e7; + margin-top: 25px; + padding-top: 25px; +} + +.subcopy p { + font-size: 14px; +} + +/* Footer */ + +.footer { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 570px; + margin: 0 auto; + padding: 0; + text-align: center; + width: 570px; +} + +.footer p { + color: #a1a1aa; + font-size: 12px; + text-align: center; +} + +.footer a { + color: #a1a1aa; + text-decoration: underline; +} + +/* Tables */ + +.table table { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + margin: 30px auto; + width: 100%; +} + +.table th { + border-bottom: 1px solid #e4e4e7; + margin: 0; + padding-bottom: 8px; +} + +.table td { + color: #52525b; + font-size: 15px; + line-height: 18px; + margin: 0; + padding: 10px 0; +} + +.content-cell { + max-width: 100vw; + padding: 32px; +} + +/* Buttons */ + +.action { + -premailer-cellpadding: 0; + -premailer-cellspacing: 0; + -premailer-width: 100%; + margin: 30px auto; + padding: 0; + text-align: center; + width: 100%; + float: unset; +} + +.button { + -webkit-text-size-adjust: none; + border-radius: 4px; + color: #fff; + display: inline-block; + overflow: hidden; + text-decoration: none; +} + +.button-blue, +.button-primary { + background-color: #18181b; + border-bottom: 8px solid #18181b; + border-left: 18px solid #18181b; + border-right: 18px solid #18181b; + border-top: 8px solid #18181b; +} + +.button-green, +.button-success { + background-color: #16a34a; + border-bottom: 8px solid #16a34a; + border-left: 18px solid #16a34a; + border-right: 18px solid #16a34a; + border-top: 8px solid #16a34a; +} + +.button-red, +.button-error { + background-color: #dc2626; + border-bottom: 8px solid #dc2626; + border-left: 18px solid #dc2626; + border-right: 18px solid #dc2626; + border-top: 8px solid #dc2626; +} + +/* Panels */ + +.panel { + border-left: #18181b solid 4px; + margin: 21px 0; +} + +.panel-content { + background-color: #fafafa; + color: #52525b; + padding: 16px; +} + +.panel-content p { + color: #52525b; +} + +.panel-item { + padding: 0; +} + +.panel-item p:last-of-type { + margin-bottom: 0; + padding-bottom: 0; +} + +/* Utilities */ + +.break-all { + word-break: break-all; +} diff --git a/resources/views/vendor/mail/text/button.blade.php b/resources/views/vendor/mail/text/button.blade.php new file mode 100644 index 00000000..97444ebd --- /dev/null +++ b/resources/views/vendor/mail/text/button.blade.php @@ -0,0 +1 @@ +{{ $slot }}: {{ $url }} diff --git a/resources/views/vendor/mail/text/footer.blade.php b/resources/views/vendor/mail/text/footer.blade.php new file mode 100644 index 00000000..3338f620 --- /dev/null +++ b/resources/views/vendor/mail/text/footer.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/resources/views/vendor/mail/text/header.blade.php b/resources/views/vendor/mail/text/header.blade.php new file mode 100644 index 00000000..97444ebd --- /dev/null +++ b/resources/views/vendor/mail/text/header.blade.php @@ -0,0 +1 @@ +{{ $slot }}: {{ $url }} diff --git a/resources/views/vendor/mail/text/layout.blade.php b/resources/views/vendor/mail/text/layout.blade.php new file mode 100644 index 00000000..ec58e83c --- /dev/null +++ b/resources/views/vendor/mail/text/layout.blade.php @@ -0,0 +1,9 @@ +{!! strip_tags($header ?? '') !!} + +{!! strip_tags($slot) !!} +@isset($subcopy) + +{!! strip_tags($subcopy) !!} +@endisset + +{!! strip_tags($footer ?? '') !!} diff --git a/resources/views/vendor/mail/text/message.blade.php b/resources/views/vendor/mail/text/message.blade.php new file mode 100644 index 00000000..40cfe79a --- /dev/null +++ b/resources/views/vendor/mail/text/message.blade.php @@ -0,0 +1,27 @@ + + {{-- Header --}} + + + {{ config('app.name') }} + + + + {{-- Body --}} + {{ $slot }} + + {{-- Subcopy --}} + @isset($subcopy) + + + {{ $subcopy }} + + + @endisset + + {{-- Footer --}} + + + © {{ date('Y') }} Bifrost Technology LLC. @lang('All rights reserved.') + + + diff --git a/resources/views/vendor/mail/text/panel.blade.php b/resources/views/vendor/mail/text/panel.blade.php new file mode 100644 index 00000000..3338f620 --- /dev/null +++ b/resources/views/vendor/mail/text/panel.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/resources/views/vendor/mail/text/subcopy.blade.php b/resources/views/vendor/mail/text/subcopy.blade.php new file mode 100644 index 00000000..3338f620 --- /dev/null +++ b/resources/views/vendor/mail/text/subcopy.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/resources/views/vendor/mail/text/table.blade.php b/resources/views/vendor/mail/text/table.blade.php new file mode 100644 index 00000000..3338f620 --- /dev/null +++ b/resources/views/vendor/mail/text/table.blade.php @@ -0,0 +1 @@ +{{ $slot }} diff --git a/resources/views/vendor/notifications/email.blade.php b/resources/views/vendor/notifications/email.blade.php new file mode 100644 index 00000000..79c24083 --- /dev/null +++ b/resources/views/vendor/notifications/email.blade.php @@ -0,0 +1,58 @@ + +{{-- Greeting --}} +@if (! empty($greeting)) +# {{ $greeting }} +@else +@if ($level === 'error') +# @lang('Whoops!') +@else +# @lang('Hello!') +@endif +@endif + +{{-- Intro Lines --}} +@foreach ($introLines as $line) +{{ $line }} + +@endforeach + +{{-- Action Button --}} +@isset($actionText) + $level, + default => 'primary', + }; +?> + +{{ $actionText }} + +@endisset + +{{-- Outro Lines --}} +@foreach ($outroLines as $line) +{{ $line }} + +@endforeach + +{{-- Salutation --}} +@if (! empty($salutation)) +{{ $salutation }} +@else +@lang('Regards,')
+{{ config('app.name') }} +@endif + +{{-- Subcopy --}} +@isset($actionText) + +@lang( + "If you're having trouble clicking the \":actionText\" button, copy and paste the URL below\n". + 'into your web browser:', + [ + 'actionText' => $actionText, + ] +) [{{ $displayableActionUrl }}]({{ $actionUrl }}) + +@endisset +
diff --git a/resources/views/vs-flutter.blade.php b/resources/views/vs-flutter.blade.php new file mode 100644 index 00000000..49704c60 --- /dev/null +++ b/resources/views/vs-flutter.blade.php @@ -0,0 +1,944 @@ + +
+ {{-- Hero Section --}} +
+
+ {{-- Title --}} +

+ NativePHP + vs + + Flutter + +

+ + {{-- Subtitle --}} +

+ Build native mobile apps without learning a new language. + Use the PHP and Laravel skills you already have. +

+
+
+ + {{-- Quick Stats Section --}} +
+

+ Quick comparison stats +

+
+ {{-- Stat Card --}} +
+
+ ~50MB +
+
+ NativePHP Download +
+
+ + {{-- Stat Card --}} +
+
+ 3GB+ +
+
+ Flutter SDK + Tools +
+
+ + {{-- Stat Card --}} +
+
+ 0 +
+
+ New Languages to Learn +
+
+ + {{-- Stat Card --}} +
+
+ 1 +
+
+ Dart Required for Flutter +
+
+
+
+ + {{-- Developer Experience Section --}} +
+
+

+ Developer Experience Comparison +

+

+ See how NativePHP simplifies mobile development compared to + Flutter. +

+
+ + {{-- Comparison Cards --}} +
+ {{-- NativePHP Card --}} +
+
+
+ +
+

+ NativePHP +

+
+ +
+
+
+ +
+
+
+ Use your existing PHP/Laravel skills +
+
+ No need to learn Dart or Flutter's widget system +
+
+
+ +
+
+ +
+
+
+ ~50MB total download +
+
+ Just add Xcode and Android Studio +
+
+
+ +
+
+ +
+
+
+ Leverage Laravel ecosystem +
+
+ Eloquent, Blade, Livewire, and thousands of packages +
+
+
+ +
+
+ +
+
+
+ Easily share code with your web app +
+
+ Reuse your PHP models, services, and business logic +
+
+
+
+
+ + {{-- Flutter Card --}} +
+
+
+ + + +
+

+ Flutter +

+
+ +
+
+
+ + + +
+
+
+ Must learn Dart programming +
+
+ A completely new language and paradigm +
+
+
+ +
+
+ + + +
+
+
+ 3GB+ SDK download +
+
+ Plus Flutter SDK, Dart, and Android SDK +
+
+
+ +
+
+ + + +
+
+
+ Separate ecosystem +
+
+ pub.dev packages, different from your backend +
+
+
+ +
+
+ + + +
+
+
+ Slow first builds +
+
+ First flutter run downloads and compiles extensively +
+
+
+
+
+
+
+ + {{-- Native Features Grid --}} + + + {{-- Size Comparison Charts --}} +
+
+

+ Size & Speed Comparison +

+
+ +
+ {{-- SDK Download Size Chart --}} +
+

+ SDK Download Size +

+
+ +
+

+ Flutter requires the Dart SDK, Flutter framework, and + often Android SDK components +

+
+ + {{-- App Bundle Size Chart --}} +
+

+ Minimum App Size +

+
+ +
+

+ App size varies widely based on bundled features, assets + and platform optimizations +

+
+ + {{-- First Boot Time Chart --}} +
+

+ First Boot Time +

+
+ +
+

+ Cold start after fresh install +

+
+
+
+ + {{-- Getting Started Comparison --}} +
+
+

+ Getting Started +

+

+ Compare the setup process side by side +

+
+ +
+ {{-- NativePHP Setup --}} +
+

+ + NativePHP Setup +

+
+
+ $ + composer require nativephp/mobile +
+
+ $ + php artisan native:install +
+
+ $ + php artisan native:run +
+
+

+ That's it. Your app is running. +

+
+ + {{-- Flutter Setup --}} +
+

+ Flutter Setup +

+
+
+ # + Download & extract Flutter SDK (~1.6GB) +
+
+ $ + export PATH="$PATH:/path/to/flutter/bin" +
+
+ $ + flutter doctor +
+
+ $ + flutter create my_app +
+
+ $ + cd my_app && flutter run +
+
+

+ First run downloads Dart SDK and compiles the engine... +

+
+
+
+ + {{-- Language Comparison --}} +
+
+

+ Use What You Know +

+

+ Why learn a new language when you can build mobile apps + with PHP? +

+
+ +
+ {{-- PHP Code Example --}} +
+

+ + NativePHP (PHP) +

+
<button
+    wire:click="increment"
+    class="btn btn-primary"
+>
+    Count: @{{ $count }}
+</button>
+
+ + {{-- Dart Code Example --}} +
+

+ Flutter (Dart) +

+
ElevatedButton(
+  onPressed: () {
+    setState(() {
+      _count++;
+    });
+  },
+  child: Text('Count: $_count'),
+)
+
+
+
+ + {{-- Video Comparison Section --}} + {{-- +
+
+

+ See the Difference +

+

+ Watch real apps boot up side by side +

+
+ +
+ + +
+
+ --}} + + {{-- Bifrost Section --}} +
+
+ {{-- Background decoration --}} + + + +
+
+ + + +

+ Supercharge with Bifrost +

+
+ +

+ Bifrost is our first-party Continuous Deployment + platform that integrates tightly with NativePHP. Get + your apps built and into the stores in + minutes + , not hours. +

+ +
    +
  • + + Cloud builds for iOS & Android +
  • +
  • + + Automatic code signing +
  • +
  • + + One-click App Store submission +
  • +
  • + + Team collaboration built-in +
  • +
+ + +
+
+
+ + {{-- CTA Section --}} +
+
+

+ Ready to Try NativePHP? +

+

+ Skip learning Dart. Build native mobile apps with the PHP + skills you already have. +

+ +
+
+
+
diff --git a/resources/views/vs-react-native-expo.blade.php b/resources/views/vs-react-native-expo.blade.php new file mode 100644 index 00000000..9336d607 --- /dev/null +++ b/resources/views/vs-react-native-expo.blade.php @@ -0,0 +1,884 @@ + +
+ {{-- Hero Section --}} +
+
+ {{-- Title --}} +

+ NativePHP + vs + + React Native & Expo + +

+ + {{-- Subtitle --}} +

+ Build native mobile apps with the tools you already know. + No JavaScript ecosystem complexity required. +

+
+
+ + {{-- Quick Stats Section --}} +
+

+ Quick comparison stats +

+
+ {{-- Stat Card --}} +
+
+ ~50MB +
+
+ NativePHP Download +
+
+ + {{-- Stat Card --}} +
+
+ 200MB+ +
+
+ node_modules Typical +
+
+ + {{-- Stat Card --}} +
+
+ 3 +
+
+ Commands to Build +
+
+ + {{-- Stat Card --}} +
+
+ 1 +
+
+ Language to Learn +
+
+
+
+ + {{-- Developer Experience Section --}} +
+
+

+ Developer Experience Comparison +

+

+ See how NativePHP simplifies mobile development compared to + the React Native ecosystem. +

+
+ + {{-- Comparison Cards --}} +
+ {{-- NativePHP Card --}} +
+
+
+ +
+

+ NativePHP +

+
+ +
+
+
+ +
+
+
+ Use your existing PHP/Laravel skills +
+
+ No need to learn JavaScript, TypeScript, or JSX +
+
+
+ +
+
+ +
+
+
+ ~50MB total download +
+
+ Just add Xcode and Android Studio +
+
+
+ +
+
+ +
+
+
+ Three commands to build +
+
+ require, native:install, native:run +
+
+
+ +
+
+ +
+
+
+ No complex build tooling +
+
+ No Babel, Metro, or bundler configuration +
+
+
+
+
+ + {{-- React Native Card --}} +
+
+
+ + + + + + +
+

+ React Native / Expo +

+
+ +
+
+
+ + + +
+
+
+ Must learn JavaScript/TypeScript +
+
+ Plus React, JSX, and the entire ecosystem +
+
+
+ +
+
+ + + +
+
+
+ 200MB+ node_modules +
+
+ Typical create-react-native-app project +
+
+
+ +
+
+ + + +
+
+
+ Complex setup process +
+
+ npm install, configure Metro, Babel, CocoaPods... +
+
+
+ +
+
+ + + +
+
+
+ JavaScript ecosystem complexity +
+
+ Multiple bundlers, transpilers, and package managers +
+
+
+
+
+
+
+ + {{-- Native Features Grid --}} + + + {{-- Size Comparison Charts --}} +
+
+

+ Size & Speed Comparison +

+
+ +
+ {{-- Download Size Chart --}} +
+

+ Initial Download Size +

+
+ +
+
+ + {{-- App Bundle Size Chart --}} +
+

+ Minimum App Size +

+
+ +
+

+ App size varies widely based on bundled features, assets + and platform optimizations +

+
+ + {{-- First Boot Time Chart --}} +
+

+ First Boot Time +

+
+ +
+

+ Cold start after fresh install +

+
+
+
+ + {{-- Getting Started Comparison --}} +
+
+

+ Getting Started +

+

+ Compare the setup process side by side +

+
+ +
+ {{-- NativePHP Setup --}} +
+

+ + NativePHP Setup +

+
+
+ $ + composer require nativephp/mobile +
+
+ $ + php artisan native:install +
+
+ $ + php artisan native:run +
+
+

+ That's it. Your app is running. +

+
+ + {{-- React Native Setup --}} +
+

+ React Native / Expo Setup +

+
+
+ $ + npx create-expo-app my-app +
+
+ $ + cd my-app && npm install +
+
+ $ + npx expo prebuild +
+
+ $ + cd ios && pod install +
+
+ $ + npx expo run:ios +
+
+

+ Plus configuring Metro, Babel, and native dependencies... +

+
+
+
+ + {{-- Video Comparison Section --}} + {{-- +
+
+

+ See the Difference +

+

+ Watch real apps boot up side by side +

+
+ +
+ + +
+
+ --}} + + {{-- Bifrost Section --}} +
+
+ {{-- Background decoration --}} + + + +
+
+ + + +

+ Supercharge with Bifrost +

+
+ +

+ Bifrost is our first-party Continuous Deployment + platform that integrates tightly with NativePHP. Get + your apps built and into the stores in + minutes + , not hours. +

+ +
    +
  • + + Cloud builds for iOS & Android +
  • +
  • + + Automatic code signing +
  • +
  • + + One-click App Store submission +
  • +
  • + + Team collaboration built-in +
  • +
+ + +
+
+
+ + {{-- CTA Section --}} +
+
+

+ Ready to Try NativePHP? +

+

+ Skip the JavaScript complexity. Build native mobile apps + with the PHP skills you already have. +

+ +
+
+
+
diff --git a/resources/views/wall-of-love.blade.php b/resources/views/wall-of-love.blade.php new file mode 100644 index 00000000..2da33759 --- /dev/null +++ b/resources/views/wall-of-love.blade.php @@ -0,0 +1,175 @@ + + {{-- Hero Section --}} +
+
+ {{-- Blurred circle - Decorative --}} + + + {{-- Primary Heading --}} +

+
+
+ Thank +
+ +
+
+
+
+
+ You! +
+
+
Early
+
Adopters
+
+
+

+ +
+
+
+
+
+
+
+
+ + {{-- Description --}} +

+ Every great story starts with a small circle of believers. You + stood with us at the beginning, and your support will always be + part of the NativePHP story. +

+
+
+ + {{-- List --}} + @php + // Get approved submissions + $approvedSubmissions = App\Models\WallOfLoveSubmission::whereNotNull('approved_at') + ->inRandomOrder() + ->get(); + + // Check if any submissions have user-uploaded images + $hasAnyUserImages = $approvedSubmissions->contains(fn ($s) => ! empty($s->photo_path)); + + // Convert approved submissions to the format expected by the component + $earlyAdopters = $approvedSubmissions + ->map(function ($submission) use ($hasAnyUserImages) { + $hasUserImage = ! empty($submission->photo_path); + + return [ + 'name' => $submission->name, + 'title' => $submission->company, + 'url' => $submission->url, + 'image' => $hasUserImage + ? asset('storage/' . $submission->photo_path) + : 'https://avatars.laravel.cloud/' . rand(1, 70) . '?vibe=' . array_rand(['ocean', 'stealth', 'bubble', 'ice']), + 'hasUserImage' => $hasUserImage, + // Only allow featured if has user image (unless no submissions have images) + 'featured' => ($hasAnyUserImages ? $hasUserImage : true) && rand(0, 4) === 0, + 'testimonial' => $submission->testimonial, + ]; + }) + ->toArray(); + @endphp + + @if (count($earlyAdopters) > 0) +
+ @foreach ($earlyAdopters as $adopter) + + @endforeach +
+ @else +
+
+
🚀
+

+ Coming Soon! +

+

+ Our early adopters will appear here soon. +

+
+
+ @endif +
diff --git a/resources/views/welcome.blade.php b/resources/views/welcome.blade.php index 921b2aa0..663b7fba 100644 --- a/resources/views/welcome.blade.php +++ b/resources/views/welcome.blade.php @@ -1,67 +1,19 @@ - - - - - + + {{-- Hero --}} + - NativePHP | Baking Delicious Native Apps + {{-- Testimonials --}} + - - - - - @vite(["resources/css/app.css", "resources/js/app.js"]) + {{-- Explainer --}} + - - - - - - + {{-- Announcements: Plugins, Bifrost, Mimi, Jump --}} + -
-
- - -

NativePHP

-

- NativePHP is a new way to build native applications, - - using the tools you already know. -

- + {{-- Partners --}} + -
-

Featured Sponsors

- -
- -
- -

Corporate Sponsors

- -
- -
- - - Want your logo here? - -
-
- - -
- - - + {{-- Feedback --}} + +
diff --git a/routes/api.php b/routes/api.php index 889937e1..8d36506d 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,5 +1,9 @@ get('/user', function (Request $request) { - return $request->user(); +// MCP Server routes (no session/cookies - fixes CSRF 419 errors) +Route::prefix('mcp')->group(function (): void { + Route::get('sse', [McpController::class, 'sse'])->name('mcp.sse'); + Route::post('message', [McpController::class, 'message'])->name('mcp.message'); + Route::get('health', [McpController::class, 'health'])->name('mcp.health'); + + // REST API endpoints + Route::get('search', [McpController::class, 'searchApi'])->name('mcp.api.search'); + Route::get('page/{platform}/{version}/{section}/{slug}', [McpController::class, 'pageApi'])->name('mcp.api.page'); + Route::get('apis/{platform}/{version}', [McpController::class, 'apisApi'])->name('mcp.api.apis'); + Route::get('navigation/{platform}/{version}', [McpController::class, 'navigationApi'])->name('mcp.api.navigation'); +}); + +Route::middleware('auth.api_key')->group(function (): void { + Route::prefix('plugins')->name('api.plugins.')->group(function (): void { + Route::get('/access', [PluginAccessController::class, 'index'])->name('access'); + Route::get('/access/{vendor}/{package}', [PluginAccessController::class, 'checkAccess'])->name('access.check'); + }); + + Route::post('/licenses', [LicenseController::class, 'store']); + Route::get('/licenses/{key}', [LicenseController::class, 'show']); + Route::get('/licenses', [LicenseController::class, 'index']); + Route::post('/temp-links', [TemporaryLinkController::class, 'store']); +}); + +Route::middleware('auth:sanctum')->group(function (): void { + Route::get('/user', fn (Request $request) => $request->user()); }); diff --git a/routes/console.php b/routes/console.php index e05f4c9a..071f4f66 100644 --- a/routes/console.php +++ b/routes/console.php @@ -1,8 +1,5 @@ comment(Inspiring::quote()); -})->purpose('Display an inspiring quote'); diff --git a/routes/web.php b/routes/web.php index 1a185d10..38bc0c86 100644 --- a/routes/web.php +++ b/routes/web.php @@ -1,8 +1,49 @@ where('page', '[a-z-]+'); + +// Redirect old mobile v3 API docs to plugin directory pages +Route::get('docs/mobile/3/apis/{page}', function (string $page) { + return redirect("/plugins/nativephp/mobile-{$page}", 301); +})->where('page', '[a-z-]+'); + +// Webhook routes (must be outside web middleware for CSRF bypass) +Route::post('opencollective/contribution', [OpenCollectiveWebhookController::class, 'handle'])->name('opencollective.webhook'); + +// OpenCollective donation claim route +Route::get('opencollective/claim', ClaimDonationLicense::class)->name('opencollective.claim'); Route::view('/', 'welcome')->name('welcome'); -Route::view('ios', 'early-adopter')->name('early-adopter'); +Route::view('ultra', 'pricing')->name('pricing'); +Route::view('alt-pricing', 'alt-pricing')->name('alt-pricing')->middleware('signed'); +Route::get('course', function () { + $user = auth()->user(); + $product = Product::where('slug', 'nativephp-masterclass')->first(); + $alreadyOwned = $user && $product && $product->isOwnedBy($user); + + return view('course', [ + 'alreadyOwned' => $alreadyOwned, + ]); +})->name('course'); + +Route::post('course/checkout', function (Request $request) { + $user = $request->user(); + + if (! $user) { + session(['url.intended' => route('course', ['checkout' => 1])]); + + return to_route('customer.login') + ->with('message', 'Please log in or create an account to complete your purchase.'); + } + + $product = Product::where('slug', 'nativephp-masterclass')->firstOrFail(); + + if ($product->isOwnedBy($user)) { + return to_route('course')->with('error', 'You already own this course.'); + } + + $cartService = resolve(CartService::class); + $cart = $cartService->getCart($user); + $cartService->addProduct($cart, $product); + + $cart->load('items.product'); + $item = $cart->items->where('product_id', $product->id)->first(); + + $user->createOrGetStripeCustomer(); + + $metadata = ['cart_id' => (string) $cart->id]; + + $session = Cashier::stripe()->checkout->sessions->create([ + 'mode' => 'payment', + 'line_items' => [[ + 'price_data' => [ + 'currency' => strtolower($item->currency), + 'unit_amount' => $item->product_price_at_addition, + 'product_data' => [ + 'name' => $product->name, + 'description' => $product->description, + ], + ], + 'quantity' => 1, + ]], + 'success_url' => route('cart.success').'?session_id={CHECKOUT_SESSION_ID}', + 'cancel_url' => route('course'), + 'customer' => $user->stripe_id, + 'customer_update' => [ + 'name' => 'auto', + 'address' => 'auto', + ], + 'metadata' => $metadata, + 'allow_promotion_codes' => true, + 'billing_address_collection' => 'required', + 'tax_id_collection' => ['enabled' => true], + 'invoice_creation' => [ + 'enabled' => true, + 'invoice_data' => [ + 'description' => 'NativePHP Masterclass Purchase', + 'metadata' => $metadata, + ], + ], + ]); + + $cart->update(['stripe_checkout_session_id' => $session->id]); + + return redirect($session->url); +})->name('course.checkout'); -Route::get('/docs/{version}/{page?}', ShowDocumentationController::class) +Route::view('wall-of-love', 'wall-of-love')->name('wall-of-love'); +Route::view('brand', 'brand')->name('brand'); +Route::get('showcase/{platform?}', [ShowcaseController::class, 'index']) + ->where('platform', 'mobile|desktop') + ->name('showcase'); +Route::view('laracon-us-2025-giveaway', 'laracon-us-2025-giveaway')->name('laracon-us-2025-giveaway'); +Route::view('privacy-policy', 'privacy-policy')->name('privacy-policy'); +Route::view('terms-of-service', 'terms-of-service')->name('terms-of-service'); +Route::view('developer-terms', 'developer-terms')->name('developer-terms'); +Route::view('partners', 'partners')->name('partners'); +Route::view('build-my-app', 'build-my-app')->name('build-my-app'); +Route::view('the-vibes', 'the-vibes')->name('the-vibes'); + +// Public plugin directory routes +Route::middleware(EnsureFeaturesAreActive::using(ShowPlugins::class))->group(function (): void { + Route::get('plugins', [PluginDirectoryController::class, 'index'])->name('plugins'); + Route::get('plugins/marketplace', PluginDirectory::class)->name('plugins.marketplace'); + Route::get('plugins/{vendor}/{package}', [PluginDirectoryController::class, 'show'])->name('plugins.show'); + Route::get('plugins/{vendor}/{package}/license', [PluginDirectoryController::class, 'license'])->name('plugins.license'); +}); + +Route::view('sponsor', 'sponsoring')->name('sponsoring'); +Route::view('vs-react-native-expo', 'vs-react-native-expo')->name('vs-react-native-expo'); +Route::view('vs-flutter', 'vs-flutter')->name('vs-flutter'); + +Route::get('blog', [ShowBlogController::class, 'index'])->name('blog'); +Route::get('blog/{article}', [ShowBlogController::class, 'show'])->name('article'); + +Route::get('docs/{platform}/{version}/{page}.md', [ShowDocumentationController::class, 'serveRawMarkdown']) + ->where('page', '(.*)') + ->where('platform', '[a-z]+') + ->where('version', '[0-9]+') + ->name('docs.raw'); + +Route::get('docs/{platform}/{version}/{page?}', ShowDocumentationController::class) ->where('page', '(.*)') - ->where('version', '[0-9]+'); + ->where('platform', '[a-z]+') + ->where('version', '[0-9]+') + ->name('docs.show'); + +// Forward platform requests without version to the latest version +Route::get('docs/{platform}/{page?}', function (string $platform, $page = null) { + $page ??= 'getting-started/introduction'; + + // Find the latest version for this platform + $docsPath = resource_path('views/docs/'.$platform); + + if (! is_dir($docsPath)) { + abort(404); + } + + $versions = collect(scandir($docsPath)) + ->filter(fn ($dir) => is_numeric($dir)) + ->sort() + ->values(); + + $latestVersion = $versions->last() ?? '1'; + + return redirect("/docs/{$platform}/{$latestVersion}/{$page}", 301); +}) + ->where('platform', 'desktop|mobile') + ->where('page', '.*') + ->name('docs.latest'); + +// Docs platform chooser +Route::view('docs', 'docs.chooser')->name('docs'); // Forward unversioned requests to the latest version -Route::get('/docs/{page?}', function ($page = null) { +Route::get('docs/{page}', function (string $page) { $version = session('viewing_docs_version', '1'); + $platform = session('viewing_docs_platform', 'mobile'); $referer = request()->header('referer'); // If coming from elsewhere in the docs, match the current version being viewed if ( - ! session()->has('viewing_docs_version') - && parse_url($referer, PHP_URL_HOST) === parse_url(url('/'), PHP_URL_HOST) + parse_url($referer, PHP_URL_HOST) === parse_url(url('/'), PHP_URL_HOST) && str($referer)->contains('/docs/') ) { - $version = Str::before(ltrim(Str::after($referer, url('/docs/')), '/'), '/'); + $path = Str::after($referer, url('/docs/')); + $path = ltrim($path, '/'); + $segments = explode('/', $path); + + if (count($segments) >= 2 && in_array($segments[0], ['desktop', 'mobile']) && is_numeric($segments[1])) { + $platform = $segments[0]; + $version = $segments[1]; + } + } + + try { + return to_route('docs.show', [ + 'platform' => $platform, + 'version' => $version, + 'page' => $page, + ]); + } catch (UrlGenerationException) { + return to_route('docs.show', [ + 'platform' => $platform, + 'version' => $version, + 'page' => 'introduction', + ]); + } +})->name('docs.unversioned')->where('page', '.*'); + +Route::get('order/{checkoutSessionId}', OrderSuccess::class)->name('order.success'); + +// License renewal routes (public success page) +Route::get('license/{license:key}/renewal/success', LicenseRenewalSuccess::class)->name('license.renewal.success'); + +// Customer authentication routes +Route::middleware(['guest'])->group(function (): void { + Route::get('login', [CustomerAuthController::class, 'showLogin'])->name('customer.login'); + Route::post('login', [CustomerAuthController::class, 'login']); + + Route::get('register', [CustomerAuthController::class, 'showRegister'])->name('customer.register'); + Route::post('register', [CustomerAuthController::class, 'register'])->middleware('throttle:5,1'); + + Route::get('forgot-password', [CustomerAuthController::class, 'showForgotPassword'])->name('password.request'); + Route::post('forgot-password', [CustomerAuthController::class, 'sendPasswordResetLink'])->name('password.email'); + + Route::get('reset-password/{token}', [CustomerAuthController::class, 'showResetPassword'])->name('password.reset'); + Route::post('reset-password', [CustomerAuthController::class, 'resetPassword'])->name('password.update'); + + Route::get('auth/github/login', [GitHubAuthController::class, 'redirect'])->name('login.github'); +}); + +Route::post('logout', [CustomerAuthController::class, 'logout']) + ->middleware(EnsureFeaturesAreActive::using(ShowAuthButtons::class)) + ->name('customer.logout'); + +// GitHub OAuth callback (no auth required - handles both login and linking) +Route::get('auth/github/callback', [GitHubIntegrationController::class, 'handleCallback'])->name('github.callback'); + +// GitHub OAuth routes (auth required) +Route::middleware(['auth', EnsureFeaturesAreActive::using(ShowAuthButtons::class)])->group(function (): void { + Route::get('auth/github', [GitHubIntegrationController::class, 'redirectToGitHub'])->name('github.redirect'); + Route::post('dashboard/github/request-access', [GitHubIntegrationController::class, 'requestRepoAccess'])->name('github.request-access'); + Route::post('dashboard/github/request-claude-plugins-access', [GitHubIntegrationController::class, 'requestClaudePluginsAccess'])->name('github.request-claude-plugins-access'); + Route::delete('dashboard/github/disconnect', [GitHubIntegrationController::class, 'disconnect'])->name('github.disconnect'); + Route::get('dashboard/github/repositories', [GitHubIntegrationController::class, 'repositories'])->name('github.repositories'); +}); + +// Discord OAuth routes +Route::middleware(['auth', EnsureFeaturesAreActive::using(ShowAuthButtons::class)])->group(function (): void { + Route::get('auth/discord', [DiscordIntegrationController::class, 'redirectToDiscord'])->name('discord.redirect'); + Route::get('auth/discord/callback', [DiscordIntegrationController::class, 'handleCallback'])->name('discord.callback'); + Route::delete('dashboard/discord/disconnect', [DiscordIntegrationController::class, 'disconnect'])->name('discord.disconnect'); +}); + +Route::get('callback', function (Request $request) { + $url = $request->query('url'); + + if ($url && ! str_starts_with($url, 'http')) { + return redirect()->away($url.'?token='.uuid_create()); } - return redirect("/docs/{$version}/{$page}"); -})->name('docs')->where('page', '.*'); + return response('Goodbye'); +})->name('callback'); + +// Team invitation acceptance (public route - no auth required) +Route::get('team/invitation/{token}', [TeamUserController::class, 'accept'])->name('team.invitation.accept'); + +// Dashboard route +Route::middleware(['auth', EnsureFeaturesAreActive::using(ShowAuthButtons::class)])->group(function (): void { + Route::livewire('dashboard', Dashboard::class)->name('dashboard'); +}); + +// Customer license management routes +Route::middleware(['auth', EnsureFeaturesAreActive::using(ShowAuthButtons::class)])->prefix('dashboard')->group(function (): void { + // License renewal routes (no customer. prefix to preserve route names) + Route::get('license/{license}/renewal', [LicenseRenewalController::class, 'show'])->name('license.renewal'); + Route::post('license/{license}/renewal/checkout', [LicenseRenewalController::class, 'createCheckoutSession'])->name('license.renewal.checkout'); +}); + +Route::middleware(['auth', EnsureFeaturesAreActive::using(ShowAuthButtons::class)])->prefix('dashboard')->name('customer.')->group(function (): void { + // Settings page + Route::livewire('settings', Settings::class)->name('settings'); + + // Notifications page + Route::livewire('notifications', Notifications::class)->name('notifications'); + + // License list page + Route::livewire('licenses', Index::class)->name('licenses.list'); + Route::livewire('integrations', Integrations::class)->name('integrations'); + + // Purchased plugins page (requires ShowPlugins feature) + Route::middleware(EnsureFeaturesAreActive::using(ShowPlugins::class))->group(function (): void { + Route::livewire('purchased-plugins', App\Livewire\Customer\PurchasedPlugins\Index::class)->name('purchased-plugins.index'); + }); + + // Purchase history page + Route::livewire('purchase-history', App\Livewire\Customer\PurchaseHistory\Index::class)->name('purchase-history.index'); + + // Support tickets + Route::livewire('support/tickets', App\Livewire\Customer\Support\Index::class)->name('support.tickets'); + Route::livewire('support/tickets/create', App\Livewire\Customer\Support\Create::class)->name('support.tickets.create'); + Route::livewire('support/tickets/{supportTicket}', App\Livewire\Customer\Support\Show::class)->name('support.tickets.show'); + + Route::livewire('licenses/{licenseKey}', Show::class)->name('licenses.show'); + Route::patch('licenses/{licenseKey}', [CustomerLicenseController::class, 'update'])->name('licenses.update'); + Route::post('plugin-license-key/rotate', [CustomerLicenseController::class, 'rotatePluginLicenseKey'])->name('plugin-license-key.rotate'); + Route::post('claim-free-plugins', [CustomerLicenseController::class, 'claimFreePlugins'])->name('claim-free-plugins'); + + // Wall of Love submission + Route::livewire('wall-of-love/create', Create::class)->name('wall-of-love.create'); + Route::livewire('wall-of-love/{wallOfLoveSubmission}/edit', App\Livewire\Customer\WallOfLove\Edit::class)->name('wall-of-love.edit'); + + // Showcase submissions + Route::livewire('showcase', App\Livewire\Customer\Showcase\Index::class)->name('showcase.index'); + Route::livewire('showcase/create', App\Livewire\Customer\Showcase\Create::class)->name('showcase.create'); + Route::livewire('showcase/{showcase}/edit', Edit::class)->name('showcase.edit'); + + // Billing portal + Route::get('billing-portal', function (Request $request) { + $user = $request->user(); + + // Check if user exists in Stripe, create if they don't + if (! $user->hasStripeId()) { + $user->createAsStripeCustomer(); + } + + return $user->redirectToBillingPortal(route('dashboard')); + })->name('billing-portal'); + + // Ultra benefits page + Route::get('ultra', [UltraController::class, 'index'])->name('ultra.index'); + + // Team management routes + Route::get('team', [TeamController::class, 'index'])->name('team.index'); + Route::post('team', [TeamController::class, 'store'])->name('team.store'); + Route::patch('team', [TeamController::class, 'update'])->name('team.update'); + Route::post('team/invite', [TeamUserController::class, 'invite'])->name('team.invite'); + Route::delete('team/users/{teamUser}', [TeamUserController::class, 'remove'])->name('team.users.remove'); + Route::post('team/users/{teamUser}/resend', [TeamUserController::class, 'resend'])->name('team.users.resend'); + Route::get('team/{team}', [TeamController::class, 'show'])->name('team.show'); + + // Sub-license management routes + Route::post('licenses/{licenseKey}/sub-licenses', [CustomerSubLicenseController::class, 'store'])->name('licenses.sub-licenses.store'); + Route::patch('licenses/{licenseKey}/sub-licenses/{subLicense}', [CustomerSubLicenseController::class, 'update'])->name('licenses.sub-licenses.update'); + Route::delete('licenses/{licenseKey}/sub-licenses/{subLicense}', [CustomerSubLicenseController::class, 'destroy'])->name('licenses.sub-licenses.destroy'); + Route::patch('licenses/{licenseKey}/sub-licenses/{subLicense}/suspend', [CustomerSubLicenseController::class, 'suspend'])->name('licenses.sub-licenses.suspend'); + Route::post('licenses/{licenseKey}/sub-licenses/{subLicense}/send-email', [CustomerSubLicenseController::class, 'sendEmail'])->name('licenses.sub-licenses.send-email'); +}); + +Route::get('.well-known/assetlinks.json', [ApplinksController::class, 'assetLinks']); + +Route::post('webhooks/plugins/{secret}', PluginWebhookController::class)->name('webhooks.plugins'); + +// Bundle routes (public) +Route::middleware(EnsureFeaturesAreActive::using(ShowPlugins::class))->group(function (): void { + Route::get('bundles/{bundle:slug}', [BundleController::class, 'show'])->name('bundles.show'); +}); + +// Product routes (public) +Route::middleware(EnsureFeaturesAreActive::using(ShowPlugins::class))->group(function (): void { + Route::get('products/{product:slug}', [ProductController::class, 'show'])->name('products.show'); +}); + +// Cart routes (public - allows guest cart) +Route::middleware(EnsureFeaturesAreActive::using(ShowPlugins::class))->group(function (): void { + Route::get('cart', [CartController::class, 'show'])->name('cart.show'); + Route::post('cart/add/{vendor}/{package}', [CartController::class, 'add'])->name('cart.add'); + Route::delete('cart/remove/{vendor}/{package}', [CartController::class, 'remove'])->name('cart.remove'); + Route::post('cart/bundle/{bundle:slug}', [CartController::class, 'addBundle'])->name('cart.bundle.add'); + Route::post('cart/bundle/{bundle:slug}/exchange', [CartController::class, 'exchangeForBundle'])->name('cart.bundle.exchange'); + Route::delete('cart/bundle/{bundle:slug}', [CartController::class, 'removeBundle'])->name('cart.bundle.remove'); + Route::post('cart/product/{product:slug}', [CartController::class, 'addProduct'])->name('cart.product.add'); + Route::delete('cart/product/{product:slug}', [CartController::class, 'removeProduct'])->name('cart.product.remove'); + Route::delete('cart/clear', [CartController::class, 'clear'])->name('cart.clear'); + Route::get('cart/count', [CartController::class, 'count'])->name('cart.count'); + Route::match(['get', 'post'], 'cart/checkout', [CartController::class, 'checkout'])->name('cart.checkout'); + Route::get('cart/success', [CartController::class, 'success'])->name('cart.success')->middleware('auth'); + Route::get('cart/status/{sessionId}', [CartController::class, 'status'])->name('cart.status')->middleware('auth'); + Route::get('cart/cancel', [CartController::class, 'cancel'])->name('cart.cancel'); +}); + +// Support (public hub page) +Route::get('support', fn () => view('support.index'))->name('support.index'); + +// Developer routes +Route::middleware(['auth', EnsureFeaturesAreActive::using(ShowAuthButtons::class), EnsureFeaturesAreActive::using(ShowPlugins::class)])->prefix('dashboard/developer')->group(function (): void { + Route::name('customer.developer.')->group(function (): void { + Route::livewire('/', App\Livewire\Customer\Developer\Dashboard::class)->name('dashboard'); + Route::livewire('onboarding', Onboarding::class)->name('onboarding'); + Route::post('onboarding/start', [DeveloperOnboardingController::class, 'start'])->name('onboarding.start'); + Route::get('onboarding/return', [DeveloperOnboardingController::class, 'return'])->name('onboarding.return'); + Route::get('onboarding/refresh', [DeveloperOnboardingController::class, 'refresh'])->name('onboarding.refresh'); + Route::livewire('settings', App\Livewire\Customer\Developer\Settings::class)->name('settings'); + }); + + // Plugin management (keeps customer.plugins.* route names) + Route::name('customer.')->group(function (): void { + Route::livewire('plugins', App\Livewire\Customer\Plugins\Index::class)->name('plugins.index'); + Route::livewire('plugins/submit', App\Livewire\Customer\Plugins\Create::class)->name('plugins.create'); + Route::livewire('plugins/{vendor}/{package}', App\Livewire\Customer\Plugins\Show::class)->name('plugins.show'); + }); +}); diff --git a/tailwind.config.js b/tailwind.config.js deleted file mode 100644 index edee5462..00000000 --- a/tailwind.config.js +++ /dev/null @@ -1,24 +0,0 @@ -import defaultTheme from "tailwindcss/defaultTheme"; - -/** @type {import('tailwindcss').Config} */ -export default { - content: [ - "./vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php", - "./storage/framework/views/*.php", - "./resources/views/**/*.blade.php", - "./app/**/*.php", - // "./app/Extensions/**/*.php", - ], - - safelist: ['inline', 'text-red-600', 'mr-2', 'font-bold', 'no-underline'], - - theme: { - container: { - center: true, - }, - }, - - plugins: [ - require('@tailwindcss/typography'), - ], -}; diff --git a/tests/CreatesApplication.php b/tests/CreatesApplication.php deleted file mode 100644 index cc683011..00000000 --- a/tests/CreatesApplication.php +++ /dev/null @@ -1,21 +0,0 @@ -make(Kernel::class)->bootstrap(); - - return $app; - } -} diff --git a/tests/Feature/Actions/Licenses/RotateLicenseKeyTest.php b/tests/Feature/Actions/Licenses/RotateLicenseKeyTest.php new file mode 100644 index 00000000..be4f4276 --- /dev/null +++ b/tests/Feature/Actions/Licenses/RotateLicenseKeyTest.php @@ -0,0 +1,129 @@ + Http::response([ + 'data' => [ + 'id' => $newAnystackId, + 'key' => $newKey, + 'expires_at' => now()->addYear()->toIso8601String(), + 'created_at' => now()->toIso8601String(), + 'updated_at' => now()->toIso8601String(), + ], + ], 201), + 'https://api.anystack.sh/v1/products/*/licenses/*' => Http::response([ + 'data' => [ + 'suspended' => true, + ], + ], 200), + ]); + + $user = User::factory()->create(['anystack_contact_id' => 'contact-123']); + $license = License::factory()->active()->create([ + 'user_id' => $user->id, + 'anystack_id' => 'old-anystack-id', + 'key' => 'old-license-key', + 'policy_name' => 'mini', + ]); + + $action = resolve(RotateLicenseKey::class); + $result = $action->handle($license); + + $license->refresh(); + $this->assertEquals($newAnystackId, $license->anystack_id); + $this->assertEquals($newKey, $license->key); + + Http::assertSent(function ($request) { + return $request->method() === 'POST' && + str_contains($request->url(), '/products/') && + str_contains($request->url(), '/licenses'); + }); + + Http::assertSent(function ($request) { + return $request->method() === 'PATCH' && + str_contains($request->url(), '/licenses/old-anystack-id') && + $request->data() === ['suspended' => true]; + }); + } + + #[Test] + public function it_preserves_other_license_attributes_when_rotating(): void + { + Http::fake([ + 'https://api.anystack.sh/v1/products/*/licenses' => Http::response([ + 'data' => [ + 'id' => 'new-anystack-id', + 'key' => 'new-key', + 'expires_at' => now()->addYear()->toIso8601String(), + 'created_at' => now()->toIso8601String(), + 'updated_at' => now()->toIso8601String(), + ], + ], 201), + 'https://api.anystack.sh/v1/products/*/licenses/*' => Http::response([], 200), + ]); + + $user = User::factory()->create(['anystack_contact_id' => 'contact-123']); + $license = License::factory()->active()->create([ + 'user_id' => $user->id, + 'policy_name' => 'pro', + 'name' => 'My Production License', + ]); + + $originalSubscriptionItemId = $license->subscription_item_id; + + $action = resolve(RotateLicenseKey::class); + $action->handle($license); + + $license->refresh(); + $this->assertEquals($user->id, $license->user_id); + $this->assertEquals('pro', $license->policy_name); + $this->assertEquals('My Production License', $license->name); + $this->assertEquals($originalSubscriptionItemId, $license->subscription_item_id); + $this->assertFalse($license->is_suspended); + } + + #[Test] + public function it_fails_when_create_api_call_fails(): void + { + Http::fake([ + 'https://api.anystack.sh/v1/products/*/licenses' => Http::response([], 500), + ]); + + $user = User::factory()->create(['anystack_contact_id' => 'contact-123']); + $license = License::factory()->active()->create([ + 'user_id' => $user->id, + 'anystack_id' => 'original-id', + 'key' => 'original-key', + 'policy_name' => 'mini', + ]); + + $this->expectException(RequestException::class); + + $action = resolve(RotateLicenseKey::class); + $action->handle($license); + + $license->refresh(); + $this->assertEquals('original-id', $license->anystack_id); + $this->assertEquals('original-key', $license->key); + } +} diff --git a/tests/Feature/Actions/Licenses/SuspendLicenseTest.php b/tests/Feature/Actions/Licenses/SuspendLicenseTest.php new file mode 100644 index 00000000..84eda1d8 --- /dev/null +++ b/tests/Feature/Actions/Licenses/SuspendLicenseTest.php @@ -0,0 +1,68 @@ + Http::response([ + 'data' => [ + 'id' => 'license-123', + 'suspended' => true, + ], + ], 200), + ]); + + $license = License::factory()->create([ + 'anystack_id' => 'license-123', + 'policy_name' => 'max', + 'is_suspended' => false, + ]); + + $action = resolve(SuspendLicense::class); + $result = $action->handle($license); + + $this->assertTrue($license->fresh()->is_suspended); + + Http::assertSent(function ($request) use ($license) { + return str_contains($request->url(), '/products/') && + str_contains($request->url(), "/licenses/{$license->anystack_id}") && + $request->method() === 'PATCH' && + $request->data() === ['suspended' => true]; + }); + } + + #[Test] + public function it_fails_when_api_call_fails() + { + Http::fake([ + 'https://api.anystack.sh/v1/products/*/licenses/*' => Http::response([], 500), + ]); + + $license = License::factory()->create([ + 'anystack_id' => 'license-123', + 'policy_name' => 'max', + 'is_suspended' => false, + ]); + + $this->expectException(RequestException::class); + + $action = resolve(SuspendLicense::class); + $result = $action->handle($license); + + $this->assertFalse($license->fresh()->is_suspended); + } +} diff --git a/tests/Feature/AdminPluginPreviewTest.php b/tests/Feature/AdminPluginPreviewTest.php new file mode 100644 index 00000000..40f5d2a9 --- /dev/null +++ b/tests/Feature/AdminPluginPreviewTest.php @@ -0,0 +1,86 @@ +pending()->create(); + + $this->get(route('plugins.show', $plugin->routeParams())) + ->assertStatus(404); + } + + public function test_regular_user_cannot_view_pending_plugin(): void + { + $user = User::factory()->create(); + $plugin = Plugin::factory()->pending()->create(); + + $this->actingAs($user) + ->get(route('plugins.show', $plugin->routeParams())) + ->assertStatus(404); + } + + public function test_admin_can_view_pending_plugin(): void + { + $admin = User::factory()->create(['email' => 'admin@test.com']); + config(['filament.users' => ['admin@test.com']]); + + $plugin = Plugin::factory()->pending()->create(); + + $this->actingAs($admin) + ->get(route('plugins.show', $plugin->routeParams())) + ->assertStatus(200); + } + + public function test_admin_sees_preview_banner_on_pending_plugin(): void + { + $admin = User::factory()->create(['email' => 'admin@test.com']); + config(['filament.users' => ['admin@test.com']]); + + $plugin = Plugin::factory()->pending()->create(); + + $this->actingAs($admin) + ->get(route('plugins.show', $plugin->routeParams())) + ->assertSee('Admin Preview') + ->assertSee('Pending Review'); + } + + public function test_approved_plugin_does_not_show_preview_banner(): void + { + $plugin = Plugin::factory()->approved()->create(); + + $this->get(route('plugins.show', $plugin->routeParams())) + ->assertDontSee('Admin Preview'); + } + + public function test_admin_can_view_approved_plugin_without_preview_banner(): void + { + $admin = User::factory()->create(['email' => 'admin@test.com']); + config(['filament.users' => ['admin@test.com']]); + + $plugin = Plugin::factory()->approved()->create(); + + $this->actingAs($admin) + ->get(route('plugins.show', $plugin->routeParams())) + ->assertStatus(200) + ->assertDontSee('Admin Preview'); + } +} diff --git a/tests/Feature/Api/CreateLicenseTest.php b/tests/Feature/Api/CreateLicenseTest.php new file mode 100644 index 00000000..1c04494c --- /dev/null +++ b/tests/Feature/Api/CreateLicenseTest.php @@ -0,0 +1,207 @@ + Http::response(['data' => ['id' => 'contact_123']], 200), + 'https://api.anystack.sh/v1/products/*/licenses' => Http::response([ + 'data' => [ + 'id' => 'license_123', + 'key' => 'TEST-LICENSE-KEY', + 'expires_at' => null, + 'created_at' => now()->toISOString(), + 'updated_at' => now()->toISOString(), + ], + ], 200), + ]); + } + + public function test_requires_authentication() + { + $response = $this->postJson('/api/licenses', [ + 'email' => 'test@example.com', + 'name' => 'Test User', + 'subscription' => 'pro', + ]); + + $response->assertStatus(401); + } + + public function test_validates_required_fields() + { + $token = config('services.bifrost.api_key'); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$token, + ])->postJson('/api/licenses', []); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['email', 'name', 'subscription']); + } + + public function test_validates_subscription_enum() + { + $token = config('services.bifrost.api_key'); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$token, + ])->postJson('/api/licenses', [ + 'email' => 'test@example.com', + 'name' => 'Test User', + 'subscription' => 'invalid', + ]); + + $response->assertStatus(422) + ->assertJsonValidationErrors(['subscription']); + } + + public function test_creates_new_user_when_email_not_exists() + { + $token = config('services.bifrost.api_key'); + + $this->withHeaders([ + 'Authorization' => 'Bearer '.$token, + ])->postJson('/api/licenses', [ + 'email' => 'newuser@example.com', + 'name' => 'New User', + 'subscription' => 'pro', + ]); + + $this->assertDatabaseHas('users', [ + 'email' => 'newuser@example.com', + 'name' => 'New User', + ]); + + $newUser = User::where('email', 'newuser@example.com')->first(); + $this->assertNotNull($newUser->password); + } + + public function test_finds_existing_user_when_email_exists() + { + $existingUser = User::factory()->create([ + 'email' => 'existing@example.com', + 'name' => 'Original Name', + ]); + + $token = config('services.bifrost.api_key'); + + $this->withHeaders([ + 'Authorization' => 'Bearer '.$token, + ])->postJson('/api/licenses', [ + 'email' => 'existing@example.com', + 'name' => 'New Name', + 'subscription' => 'pro', + ]); + + // User should not be updated, original name should remain + $this->assertDatabaseHas('users', [ + 'email' => 'existing@example.com', + 'name' => 'Original Name', + ]); + } + + public function test_creates_license_with_bifrost_source() + { + $token = config('services.bifrost.api_key'); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$token, + ])->postJson('/api/licenses', [ + 'email' => 'test@example.com', + 'name' => 'Test User', + 'subscription' => 'pro', + ]); + + $response->assertStatus(200) + ->assertJsonStructure([ + 'data' => [ + 'id', + 'anystack_id', + 'key', + 'policy_name', + 'source', + 'expires_at', + 'created_at', + 'updated_at', + 'email', + ], + ]) + ->assertJson([ + 'data' => [ + 'email' => 'test@example.com', + ], + ]); + + // Verify the license was created with correct attributes + $this->assertDatabaseHas('licenses', [ + 'policy_name' => 'pro', + 'source' => 'bifrost', + 'key' => 'TEST-LICENSE-KEY', + ]); + + // Verify user was created/found + $this->assertDatabaseHas('users', [ + 'email' => 'test@example.com', + ]); + } + + public function test_creates_license_for_existing_user() + { + // Create an existing user + $existingUser = User::factory()->create([ + 'email' => 'existing@example.com', + 'name' => 'Existing User', + ]); + + $token = config('services.bifrost.api_key'); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$token, + ])->postJson('/api/licenses', [ + 'email' => 'existing@example.com', + 'name' => 'Different Name', // This should be ignored + 'subscription' => 'max', + ]); + + $response->assertStatus(200) + ->assertJsonStructure([ + 'data' => [ + 'id', + 'anystack_id', + 'key', + 'policy_name', + 'source', + 'expires_at', + 'created_at', + 'updated_at', + 'email', + ], + ]) + ->assertJson([ + 'data' => [ + 'email' => 'existing@example.com', + ], + ]); + + // Verify license was created for the existing user + $license = License::where('user_id', $existingUser->id)->first(); + $this->assertNotNull($license); + $this->assertEquals('max', $license->policy_name); + $this->assertEquals('bifrost', $license->source->value); + } +} diff --git a/tests/Feature/Api/GetLicenseTest.php b/tests/Feature/Api/GetLicenseTest.php new file mode 100644 index 00000000..088542d5 --- /dev/null +++ b/tests/Feature/Api/GetLicenseTest.php @@ -0,0 +1,115 @@ +create(); + $license = License::factory()->create([ + 'user_id' => $user->id, + 'key' => 'TEST-KEY-123', + ]); + + $response = $this->getJson('/api/licenses/'.$license->key); + + $response->assertStatus(401); + } + + public function test_returns_404_for_non_existent_license() + { + $token = config('services.bifrost.api_key'); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$token, + ])->getJson('/api/licenses/NON-EXISTENT-KEY'); + + $response->assertStatus(404); + } + + public function test_returns_license_with_user_email() + { + $user = User::factory()->create([ + 'email' => 'test@example.com', + 'name' => 'Test User', + ]); + + $license = License::factory()->create([ + 'user_id' => $user->id, + 'key' => 'TEST-LICENSE-KEY-123', + 'policy_name' => 'pro', + 'source' => 'bifrost', + 'anystack_id' => 'anystack_123', + ]); + + $token = config('services.bifrost.api_key'); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$token, + ])->getJson('/api/licenses/'.$license->key); + + $response->assertStatus(200) + ->assertJson([ + 'data' => [ + 'id' => $license->id, + 'anystack_id' => 'anystack_123', + 'key' => 'TEST-LICENSE-KEY-123', + 'policy_name' => 'pro', + 'source' => 'bifrost', + 'email' => 'test@example.com', + ], + ]) + ->assertJsonStructure([ + 'data' => [ + 'id', + 'anystack_id', + 'key', + 'policy_name', + 'source', + 'expires_at', + 'created_at', + 'updated_at', + 'email', + ], + ]); + } + + public function test_returns_correct_license_by_key() + { + $user1 = User::factory()->create(['email' => 'user1@example.com']); + $user2 = User::factory()->create(['email' => 'user2@example.com']); + + $license1 = License::factory()->create([ + 'user_id' => $user1->id, + 'key' => 'KEY-USER-1', + ]); + + $license2 = License::factory()->create([ + 'user_id' => $user2->id, + 'key' => 'KEY-USER-2', + ]); + + $token = config('services.bifrost.api_key'); + + $response = $this->withHeaders([ + 'Authorization' => 'Bearer '.$token, + ])->getJson('/api/licenses/KEY-USER-2'); + + $response->assertStatus(200) + ->assertJson([ + 'data' => [ + 'id' => $license2->id, + 'key' => 'KEY-USER-2', + 'email' => 'user2@example.com', + ], + ]); + } +} diff --git a/tests/Feature/Api/PluginAccessTest.php b/tests/Feature/Api/PluginAccessTest.php new file mode 100644 index 00000000..cae1b7b8 --- /dev/null +++ b/tests/Feature/Api/PluginAccessTest.php @@ -0,0 +1,230 @@ +getJson('/api/plugins/access'); + + $response->assertStatus(401) + ->assertJson(['message' => 'Unauthorized']); + } + + public function test_returns_401_without_credentials(): void + { + $response = $this->withApiKey() + ->getJson('/api/plugins/access'); + + $response->assertStatus(401) + ->assertJson(['error' => 'Authentication required']); + } + + public function test_returns_401_with_invalid_credentials(): void + { + $response = $this->withApiKey() + ->asBasicAuth('invalid@example.com', 'invalid-key') + ->getJson('/api/plugins/access'); + + $response->assertStatus(401) + ->assertJson(['error' => 'Invalid credentials']); + } + + public function test_returns_accessible_plugins_with_valid_credentials(): void + { + $user = User::factory()->create([ + 'plugin_license_key' => 'test-license-key-123', + ]); + + // Create a free plugin + $freePlugin = Plugin::factory()->create([ + 'name' => 'vendor/free-plugin', + 'type' => PluginType::Free, + 'status' => PluginStatus::Approved, + 'is_active' => true, + ]); + + // Create a paid plugin + $paidPlugin = Plugin::factory()->create([ + 'name' => 'vendor/paid-plugin', + 'type' => PluginType::Paid, + 'status' => PluginStatus::Approved, + 'is_active' => true, + ]); + + // Give user a license for the paid plugin + PluginLicense::factory()->create([ + 'user_id' => $user->id, + 'plugin_id' => $paidPlugin->id, + 'expires_at' => null, // Never expires + ]); + + $response = $this->withApiKey() + ->asBasicAuth($user->email, 'test-license-key-123') + ->getJson('/api/plugins/access'); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'user' => ['email' => $user->email], + ]); + + $plugins = $response->json('plugins'); + $pluginNames = array_column($plugins, 'name'); + + // Only paid plugins with licenses are returned (not free plugins) + $this->assertNotContains('vendor/free-plugin', $pluginNames); + $this->assertContains('vendor/paid-plugin', $pluginNames); + } + + public function test_excludes_expired_plugin_licenses(): void + { + $user = User::factory()->create([ + 'plugin_license_key' => 'test-license-key-123', + ]); + + $paidPlugin = Plugin::factory()->create([ + 'name' => 'vendor/paid-plugin', + 'type' => PluginType::Paid, + 'status' => PluginStatus::Approved, + 'is_active' => true, + ]); + + // Create an expired license + PluginLicense::factory()->create([ + 'user_id' => $user->id, + 'plugin_id' => $paidPlugin->id, + 'expires_at' => now()->subDay(), + ]); + + $response = $this->withApiKey() + ->asBasicAuth($user->email, 'test-license-key-123') + ->getJson('/api/plugins/access'); + + $response->assertStatus(200); + + $plugins = $response->json('plugins'); + $pluginNames = array_column($plugins, 'name'); + + $this->assertNotContains('vendor/paid-plugin', $pluginNames); + } + + public function test_check_access_returns_true_for_licensed_plugin(): void + { + $user = User::factory()->create([ + 'plugin_license_key' => 'test-license-key-123', + ]); + + $paidPlugin = Plugin::factory()->create([ + 'name' => 'vendor/paid-plugin', + 'type' => PluginType::Paid, + 'status' => PluginStatus::Approved, + 'is_active' => true, + ]); + + PluginLicense::factory()->create([ + 'user_id' => $user->id, + 'plugin_id' => $paidPlugin->id, + ]); + + $response = $this->withApiKey() + ->asBasicAuth($user->email, 'test-license-key-123') + ->getJson('/api/plugins/access/vendor/paid-plugin'); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'package' => 'vendor/paid-plugin', + 'has_access' => true, + ]); + } + + public function test_check_access_returns_false_for_unlicensed_plugin(): void + { + $user = User::factory()->create([ + 'plugin_license_key' => 'test-license-key-123', + ]); + + Plugin::factory()->create([ + 'name' => 'vendor/paid-plugin', + 'type' => PluginType::Paid, + 'status' => PluginStatus::Approved, + 'is_active' => true, + ]); + + $response = $this->withApiKey() + ->asBasicAuth($user->email, 'test-license-key-123') + ->getJson('/api/plugins/access/vendor/paid-plugin'); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'package' => 'vendor/paid-plugin', + 'has_access' => false, + ]); + } + + public function test_check_access_returns_true_for_free_plugin(): void + { + $user = User::factory()->create([ + 'plugin_license_key' => 'test-license-key-123', + ]); + + Plugin::factory()->create([ + 'name' => 'vendor/free-plugin', + 'type' => PluginType::Free, + 'status' => PluginStatus::Approved, + 'is_active' => true, + ]); + + $response = $this->withApiKey() + ->asBasicAuth($user->email, 'test-license-key-123') + ->getJson('/api/plugins/access/vendor/free-plugin'); + + $response->assertStatus(200) + ->assertJson([ + 'success' => true, + 'package' => 'vendor/free-plugin', + 'has_access' => true, + ]); + } + + public function test_check_access_returns_404_for_nonexistent_plugin(): void + { + $user = User::factory()->create([ + 'plugin_license_key' => 'test-license-key-123', + ]); + + $response = $this->withApiKey() + ->asBasicAuth($user->email, 'test-license-key-123') + ->getJson('/api/plugins/access/vendor/nonexistent'); + + $response->assertStatus(404) + ->assertJson(['error' => 'Plugin not found']); + } + + protected function asBasicAuth(string $username, string $password): static + { + return $this->withHeaders([ + 'Authorization' => 'Basic '.base64_encode("{$username}:{$password}"), + ]); + } + + protected function withApiKey(): static + { + return $this->withHeaders([ + 'X-API-Key' => config('services.bifrost.api_key'), + ]); + } +} diff --git a/tests/Feature/Auth/AuthPagesTest.php b/tests/Feature/Auth/AuthPagesTest.php new file mode 100644 index 00000000..37bfb88e --- /dev/null +++ b/tests/Feature/Auth/AuthPagesTest.php @@ -0,0 +1,220 @@ +withoutVite()->get('/login'); + + $response->assertStatus(200); + $response->assertSee('Sign in to your account'); + $response->assertSee('create a new account'); + $response->assertSee('Sign in with GitHub'); + } + + public function test_login_page_shows_status_session_message(): void + { + $response = $this->withoutVite()->get('/login', ['status' => 'test']); + + $response->assertStatus(200); + } + + public function test_login_with_valid_credentials_redirects_to_dashboard(): void + { + $user = User::factory()->create([ + 'password' => Hash::make('password123'), + ]); + + $response = $this->post('/login', [ + 'email' => $user->email, + 'password' => 'password123', + ]); + + $response->assertRedirect(route('dashboard')); + $this->assertAuthenticatedAs($user); + } + + public function test_login_with_invalid_credentials_shows_error(): void + { + $user = User::factory()->create([ + 'password' => Hash::make('password123'), + ]); + + $response = $this->from('/login')->post('/login', [ + 'email' => $user->email, + 'password' => 'wrong-password', + ]); + + $response->assertRedirect('/login'); + $response->assertSessionHasErrors('email'); + $this->assertGuest(); + } + + public function test_authenticated_user_is_redirected_from_login(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->get('/login'); + + $response->assertRedirect(route('dashboard')); + } + + // --- Register page --- + + public function test_register_page_renders(): void + { + $response = $this->withoutVite()->get('/register'); + + $response->assertStatus(200); + $response->assertSee('Create your account'); + $response->assertSee('Terms of Service'); + $response->assertSee('Privacy Policy'); + $response->assertSee('Sign up with GitHub'); + } + + public function test_register_creates_user_and_logs_in(): void + { + $response = $this->post('/register', [ + 'name' => 'Test User', + 'email' => 'testuser@gmail.com', + 'password' => 'password123', + 'password_confirmation' => 'password123', + ]); + + $response->assertRedirect(route('dashboard')); + $this->assertAuthenticated(); + $this->assertDatabaseHas('users', ['email' => 'testuser@gmail.com', 'name' => 'Test User']); + } + + public function test_register_validates_required_fields(): void + { + $response = $this->from('/register')->post('/register', []); + + $response->assertRedirect('/register'); + $response->assertSessionHasErrors(['name', 'email', 'password']); + } + + public function test_register_validates_password_confirmation(): void + { + $response = $this->from('/register')->post('/register', [ + 'name' => 'Test User', + 'email' => 'test@example.com', + 'password' => 'password123', + 'password_confirmation' => 'different', + ]); + + $response->assertRedirect('/register'); + $response->assertSessionHasErrors('password'); + } + + public function test_register_validates_unique_email(): void + { + User::factory()->create(['email' => 'taken@example.com']); + + $response = $this->from('/register')->post('/register', [ + 'name' => 'Test User', + 'email' => 'taken@example.com', + 'password' => 'password123', + 'password_confirmation' => 'password123', + ]); + + $response->assertRedirect('/register'); + $response->assertSessionHasErrors('email'); + } + + // --- Forgot password page --- + + public function test_forgot_password_page_renders(): void + { + $response = $this->withoutVite()->get('/forgot-password'); + + $response->assertStatus(200); + $response->assertSee('Reset your password'); + $response->assertSee('Back to login'); + } + + public function test_forgot_password_sends_reset_link(): void + { + $user = User::factory()->create(['email' => 'resettest@gmail.com']); + + $response = $this->post('/forgot-password', [ + 'email' => $user->email, + ]); + + $response->assertSessionHas('status'); + } + + public function test_forgot_password_validates_email(): void + { + $response = $this->from('/forgot-password')->post('/forgot-password', [ + 'email' => 'not-an-email', + ]); + + $response->assertRedirect('/forgot-password'); + $response->assertSessionHasErrors('email'); + } + + // --- Reset password page --- + + public function test_reset_password_page_renders(): void + { + $response = $this->withoutVite()->get('/reset-password/test-token'); + + $response->assertStatus(200); + $response->assertSee('Set new password'); + } + + public function test_reset_password_updates_password(): void + { + $user = User::factory()->create(['email' => 'resetpw@gmail.com']); + + $token = Password::createToken($user); + + $response = $this->post('/reset-password', [ + 'token' => $token, + 'email' => $user->email, + 'password' => 'new-password-123', + 'password_confirmation' => 'new-password-123', + ]); + + $response->assertRedirect(route('customer.login')); + $this->assertTrue(Hash::check('new-password-123', $user->fresh()->password)); + } + + public function test_reset_password_validates_password_confirmation(): void + { + $user = User::factory()->create(); + + $token = Password::createToken($user); + + $response = $this->from("/reset-password/{$token}")->post('/reset-password', [ + 'token' => $token, + 'email' => $user->email, + 'password' => 'new-password-123', + 'password_confirmation' => 'different-password', + ]); + + $response->assertSessionHasErrors('password'); + } +} diff --git a/tests/Feature/BlogTest.php b/tests/Feature/BlogTest.php new file mode 100644 index 00000000..8bc2e63b --- /dev/null +++ b/tests/Feature/BlogTest.php @@ -0,0 +1,114 @@ +published()->create(); + + $this->get(route('blog')) + ->assertOk() + ->assertSee($article->title) + ->assertSee(route('article', $article)); + } + + #[Test] + public function published_articles_are_shown_in_antichronological_order() + { + [$article1, $article2, $article3] = [ + Article::factory()->create([ + 'published_at' => now()->subDays(2), + ]), + Article::factory()->create([ + 'published_at' => now()->subDays(1), + ]), + Article::factory()->create([ + 'published_at' => now()->subDays(3), + ]), + ]; + + $this->get(route('blog')) + ->assertOk() + ->assertSeeInOrder([ + $article2->title, + $article1->title, + $article3->title, + ]); + } + + #[Test] + public function scheduled_articles_are_not_shown_on_the_blog_listing() + { + $article = Article::factory()->scheduled()->create(); + + $this->get(route('blog')) + ->assertOk() + ->assertDontSee($article->title) + ->assertDontSee(route('article', $article)); + } + + #[Test] + public function published_articles_are_visitable() + { + $article = Article::factory()->published()->create(); + + $this->get(route('article', $article)) + ->assertOk(); + } + + #[Test] + public function scheduled_articles_are_visitable_via_direct_link() + { + $article = Article::factory()->scheduled()->create(); + + $this->get(route('article', $article)) + ->assertOk(); + } + + #[Test] + public function articles_can_be_previewed_by_admin_users() + { + $article = Article::factory()->create([ + 'published_at' => null, + ]); + + $admin = User::factory()->create(); + Config::set('filament.users', [$admin->email]); + + // Visitors + $this->get(route('article', $article)) + ->assertStatus(404); + + // Admins + $this->actingAs($admin) + ->get(route('article', $article)) + ->assertOk(); + } + + #[Test] + public function articles_cant_be_previewed_by_regular_users() + { + $article = Article::factory()->create([ + 'published_at' => null, + ]); + + $user = User::factory()->create(); + + // Non-admin users + $this->actingAs($user) + ->get(route('article', $article)) + ->assertStatus(404); + } +} diff --git a/tests/Feature/CallbackRedirectTest.php b/tests/Feature/CallbackRedirectTest.php new file mode 100644 index 00000000..26531a4a --- /dev/null +++ b/tests/Feature/CallbackRedirectTest.php @@ -0,0 +1,53 @@ +get('/callback?url=nativephp://127.0.0.1/some/url'); + + $response->assertRedirect(); + + $redirectUrl = $response->headers->get('Location'); + + $this->assertStringStartsWith('nativephp://127.0.0.1/some/url?token=', $redirectUrl); + + // Extract and validate the token is a valid UUID + $token = str_replace('nativephp://127.0.0.1/some/url?token=', '', $redirectUrl); + $this->assertTrue( + preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i', $token) === 1, + "Token should be a valid UUID, got: {$token}" + ); + } + + public function test_callback_shows_goodbye_for_non_nativephp_url(): void + { + $response = $this->get('/callback?url=https://example.com'); + + $response->assertStatus(200); + $response->assertSee('Goodbye'); + } + + public function test_callback_shows_goodbye_when_no_url_provided(): void + { + $response = $this->get('/callback'); + + $response->assertStatus(200); + $response->assertSee('Goodbye'); + } + + public function test_callback_redirects_for_any_non_http_scheme(): void + { + $response = $this->get('/callback?url=myapp://some/path'); + + $response->assertRedirect(); + + $redirectUrl = $response->headers->get('Location'); + + $this->assertStringStartsWith('myapp://some/path?token=', $redirectUrl); + } +} diff --git a/tests/Feature/ClaimDonationLicenseTest.php b/tests/Feature/ClaimDonationLicenseTest.php new file mode 100644 index 00000000..2ea40e90 --- /dev/null +++ b/tests/Feature/ClaimDonationLicenseTest.php @@ -0,0 +1,316 @@ +get('/opencollective/claim'); + + $response->assertStatus(200); + $response->assertSeeLivewire(ClaimDonationLicense::class); + } + + #[Test] + public function user_can_claim_a_valid_donation(): void + { + Queue::fake(); + + $donation = OpenCollectiveDonation::factory()->create([ + 'order_id' => 51763, + ]); + + Livewire::test(ClaimDonationLicense::class) + ->set('order_id', '51763') + ->set('name', 'John Doe') + ->set('email', 'john@example.com') + ->set('password', 'password123') + ->set('password_confirmation', 'password123') + ->call('claim') + ->assertSet('claimed', true); + + // Verify user was created + $this->assertDatabaseHas('users', [ + 'email' => 'john@example.com', + 'name' => 'John Doe', + ]); + + // Verify donation was marked as claimed + $donation->refresh(); + $this->assertTrue($donation->isClaimed()); + $this->assertNotNull($donation->user_id); + + // Verify license creation job was dispatched + Queue::assertPushed(CreateAnystackLicenseJob::class, function ($job) { + return $job->user->email === 'john@example.com' + && $job->subscription === Subscription::Mini + && $job->source === LicenseSource::OpenCollective + && $job->firstName === 'John' + && $job->lastName === 'Doe'; + }); + } + + #[Test] + public function claim_fails_with_invalid_order_id(): void + { + Queue::fake(); + + Livewire::test(ClaimDonationLicense::class) + ->set('order_id', '99999') + ->set('name', 'John Doe') + ->set('email', 'john@example.com') + ->set('password', 'password123') + ->set('password_confirmation', 'password123') + ->call('claim') + ->assertHasErrors(['order_id']) + ->assertSet('claimed', false); + + Queue::assertNotPushed(CreateAnystackLicenseJob::class); + } + + #[Test] + public function claim_fails_for_already_claimed_donation(): void + { + Queue::fake(); + + $user = User::factory()->create(); + $donation = OpenCollectiveDonation::factory()->claimed()->create([ + 'order_id' => 51763, + 'user_id' => $user->id, + ]); + + Livewire::test(ClaimDonationLicense::class) + ->set('order_id', '51763') + ->set('name', 'Jane Doe') + ->set('email', 'jane@example.com') + ->set('password', 'password123') + ->set('password_confirmation', 'password123') + ->call('claim') + ->assertHasErrors(['order_id']) + ->assertSet('claimed', false); + + Queue::assertNotPushed(CreateAnystackLicenseJob::class); + } + + #[Test] + public function claim_fails_if_contributor_already_claimed_another_donation(): void + { + Queue::fake(); + + $user = User::factory()->create(); + + // First donation from this contributor - already claimed + OpenCollectiveDonation::factory()->claimed()->create([ + 'order_id' => 11111, + 'from_collective_id' => 99999, + 'user_id' => $user->id, + ]); + + // Second donation from the same contributor - not yet claimed + $donation = OpenCollectiveDonation::factory()->create([ + 'order_id' => 22222, + 'from_collective_id' => 99999, + ]); + + Livewire::test(ClaimDonationLicense::class) + ->set('order_id', '22222') + ->set('name', 'Jane Doe') + ->set('email', 'jane@example.com') + ->set('password', 'password123') + ->set('password_confirmation', 'password123') + ->call('claim') + ->assertHasErrors(['order_id']) + ->assertSet('claimed', false); + + Queue::assertNotPushed(CreateAnystackLicenseJob::class); + + // Verify the donation was not claimed + $this->assertNull($donation->fresh()->claimed_at); + } + + #[Test] + public function existing_user_not_logged_in_is_told_to_log_in_first(): void + { + Queue::fake(); + + $user = User::factory()->create([ + 'email' => 'john@example.com', + ]); + + $donation = OpenCollectiveDonation::factory()->create([ + 'order_id' => 51763, + ]); + + Livewire::test(ClaimDonationLicense::class) + ->set('order_id', '51763') + ->set('name', 'John Doe') + ->set('email', 'john@example.com') + ->set('password', 'password123') + ->set('password_confirmation', 'password123') + ->call('claim') + ->assertHasErrors(['email']) + ->assertSet('claimed', false); + + Queue::assertNotPushed(CreateAnystackLicenseJob::class); + } + + #[Test] + public function logged_in_user_cannot_claim_if_they_already_have_opencollective_license(): void + { + Queue::fake(); + + $user = User::factory()->create([ + 'email' => 'john@example.com', + ]); + + License::factory()->create([ + 'user_id' => $user->id, + 'source' => LicenseSource::OpenCollective, + ]); + + $donation = OpenCollectiveDonation::factory()->create([ + 'order_id' => 51763, + ]); + + $this->actingAs($user); + + Livewire::test(ClaimDonationLicense::class) + ->set('order_id', '51763') + ->call('claim') + ->assertHasErrors(['order_id']) + ->assertSet('claimed', false); + + Queue::assertNotPushed(CreateAnystackLicenseJob::class); + } + + #[Test] + public function logged_in_user_without_opencollective_license_can_claim(): void + { + Queue::fake(); + + $user = User::factory()->create([ + 'email' => 'john@example.com', + 'name' => 'John Doe', + ]); + + // User has a Stripe license, not OpenCollective + License::factory()->create([ + 'user_id' => $user->id, + 'source' => LicenseSource::Stripe, + ]); + + $donation = OpenCollectiveDonation::factory()->create([ + 'order_id' => 51763, + ]); + + $this->actingAs($user); + + Livewire::test(ClaimDonationLicense::class) + ->set('order_id', '51763') + ->call('claim') + ->assertSet('claimed', true); + + Queue::assertPushed(CreateAnystackLicenseJob::class); + + // Verify donation was claimed by the existing user + $donation->refresh(); + $this->assertEquals($user->id, $donation->user_id); + } + + #[Test] + public function logged_in_user_only_needs_order_id_to_claim(): void + { + Queue::fake(); + + $user = User::factory()->create(); + + $donation = OpenCollectiveDonation::factory()->create([ + 'order_id' => 51763, + ]); + + $this->actingAs($user); + + Livewire::test(ClaimDonationLicense::class) + ->set('order_id', '51763') + ->call('claim') + ->assertHasNoErrors() + ->assertSet('claimed', true); + + Queue::assertPushed(CreateAnystackLicenseJob::class); + } + + #[Test] + public function validation_requires_all_fields_for_guests(): void + { + Livewire::test(ClaimDonationLicense::class) + ->call('claim') + ->assertHasErrors(['order_id', 'name', 'email', 'password']); + } + + #[Test] + public function validation_only_requires_order_id_for_logged_in_users(): void + { + $user = User::factory()->create(); + + $this->actingAs($user); + + Livewire::test(ClaimDonationLicense::class) + ->call('claim') + ->assertHasErrors(['order_id']) + ->assertHasNoErrors(['name', 'email', 'password']); + } + + #[Test] + public function password_confirmation_must_match(): void + { + OpenCollectiveDonation::factory()->create([ + 'order_id' => 51763, + ]); + + Livewire::test(ClaimDonationLicense::class) + ->set('order_id', '51763') + ->set('name', 'John Doe') + ->set('email', 'john@example.com') + ->set('password', 'password123') + ->set('password_confirmation', 'different') + ->call('claim') + ->assertHasErrors(['password']); + } + + #[Test] + public function user_is_logged_in_after_claiming(): void + { + Queue::fake(); + + $donation = OpenCollectiveDonation::factory()->create([ + 'order_id' => 51763, + ]); + + Livewire::test(ClaimDonationLicense::class) + ->set('order_id', '51763') + ->set('name', 'John Doe') + ->set('email', 'john@example.com') + ->set('password', 'password123') + ->set('password_confirmation', 'password123') + ->call('claim'); + + $this->assertAuthenticated(); + } +} diff --git a/tests/Feature/ClaimFreePluginsTest.php b/tests/Feature/ClaimFreePluginsTest.php new file mode 100644 index 00000000..270fce09 --- /dev/null +++ b/tests/Feature/ClaimFreePluginsTest.php @@ -0,0 +1,125 @@ +travelTo(Carbon::parse('2026-03-15')); + + $user = User::factory()->create(); + License::factory()->create([ + 'user_id' => $user->id, + 'created_at' => '2025-12-01', + ]); + + foreach (User::FREE_PLUGINS_OFFER as $name) { + Plugin::factory()->approved()->create(['name' => $name]); + } + + $response = $this->actingAs($user) + ->post(route('customer.claim-free-plugins')); + + $response->assertRedirectToRoute('dashboard'); + $response->assertSessionHas('success'); + + $this->assertDatabaseCount('plugin_licenses', count(User::FREE_PLUGINS_OFFER)); + } + + public function test_claim_is_rejected_after_offer_expires(): void + { + $this->travelTo(Carbon::parse('2026-06-01')); + + $user = User::factory()->create(); + License::factory()->create([ + 'user_id' => $user->id, + 'created_at' => '2025-12-01', + ]); + + foreach (User::FREE_PLUGINS_OFFER as $name) { + Plugin::factory()->approved()->create(['name' => $name]); + } + + $response = $this->actingAs($user) + ->post(route('customer.claim-free-plugins')); + + $response->assertRedirectToRoute('dashboard'); + $response->assertSessionHas('error', 'This offer has expired.'); + + $this->assertDatabaseCount('plugin_licenses', 0); + } + + public function test_claim_is_allowed_on_last_day_of_offer(): void + { + $this->travelTo(Carbon::parse('2026-05-31 23:00:00')); + + $user = User::factory()->create(); + License::factory()->create([ + 'user_id' => $user->id, + 'created_at' => '2025-12-01', + ]); + + foreach (User::FREE_PLUGINS_OFFER as $name) { + Plugin::factory()->approved()->create(['name' => $name]); + } + + $response = $this->actingAs($user) + ->post(route('customer.claim-free-plugins')); + + $response->assertRedirectToRoute('dashboard'); + $response->assertSessionHas('success'); + } + + public function test_ineligible_user_cannot_claim_free_plugins(): void + { + $this->travelTo(Carbon::parse('2026-03-15')); + + $user = User::factory()->create(); + + foreach (User::FREE_PLUGINS_OFFER as $name) { + Plugin::factory()->approved()->create(['name' => $name]); + } + + $response = $this->actingAs($user) + ->post(route('customer.claim-free-plugins')); + + $response->assertRedirectToRoute('dashboard'); + $response->assertSessionHas('error', 'You are not eligible for this offer.'); + } + + public function test_user_cannot_claim_plugins_twice(): void + { + $this->travelTo(Carbon::parse('2026-03-15')); + + $user = User::factory()->create(); + License::factory()->create([ + 'user_id' => $user->id, + 'created_at' => '2025-12-01', + ]); + + foreach (User::FREE_PLUGINS_OFFER as $name) { + $plugin = Plugin::factory()->approved()->create(['name' => $name]); + PluginLicense::factory()->create([ + 'user_id' => $user->id, + 'plugin_id' => $plugin->id, + ]); + } + + $response = $this->actingAs($user) + ->post(route('customer.claim-free-plugins')); + + $response->assertRedirectToRoute('dashboard'); + $response->assertSessionHas('message', 'You have already claimed all the free plugins.'); + } +} diff --git a/tests/Feature/Commands/BackfillSubscriptionPricesTest.php b/tests/Feature/Commands/BackfillSubscriptionPricesTest.php new file mode 100644 index 00000000..96591bde --- /dev/null +++ b/tests/Feature/Commands/BackfillSubscriptionPricesTest.php @@ -0,0 +1,114 @@ +data = $invoiceData; + + $invoicesMock = Mockery::mock(); + $invoicesMock->shouldReceive('all')->andReturn($invoiceList); + + $stripeMock = Mockery::mock(StripeClient::class); + $stripeMock->invoices = $invoicesMock; + + $this->app->bind(StripeClient::class, fn () => $stripeMock); + } + + public function test_backfills_price_paid_from_stripe_invoices(): void + { + $subscription = Subscription::factory()->active()->create([ + 'price_paid' => null, + ]); + + $this->mockStripeInvoices([(object) ['total' => 9900]]); + + $this->artisan('subscriptions:backfill-prices') + ->assertSuccessful(); + + $this->assertDatabaseHas('subscriptions', [ + 'id' => $subscription->id, + 'price_paid' => 9900, + ]); + } + + public function test_skips_subscriptions_that_already_have_price_paid(): void + { + Subscription::factory()->active()->create([ + 'price_paid' => 4900, + ]); + + $this->artisan('subscriptions:backfill-prices') + ->expectsOutput('No subscriptions need backfilling.') + ->assertSuccessful(); + } + + public function test_handles_stripe_api_errors_gracefully(): void + { + $subscription = Subscription::factory()->active()->create([ + 'price_paid' => null, + ]); + + $invoicesMock = Mockery::mock(); + $invoicesMock->shouldReceive('all') + ->andThrow(new \Exception('Stripe API error')); + + $stripeMock = Mockery::mock(StripeClient::class); + $stripeMock->invoices = $invoicesMock; + + $this->app->bind(StripeClient::class, fn () => $stripeMock); + + $this->artisan('subscriptions:backfill-prices') + ->assertSuccessful(); + + $this->assertDatabaseHas('subscriptions', [ + 'id' => $subscription->id, + 'price_paid' => null, + ]); + } + + public function test_handles_negative_invoice_total(): void + { + $subscription = Subscription::factory()->active()->create([ + 'price_paid' => null, + ]); + + $this->mockStripeInvoices([(object) ['total' => -500]]); + + $this->artisan('subscriptions:backfill-prices') + ->assertSuccessful(); + + $this->assertDatabaseHas('subscriptions', [ + 'id' => $subscription->id, + 'price_paid' => 0, + ]); + } + + public function test_handles_empty_invoice_list(): void + { + $subscription = Subscription::factory()->active()->create([ + 'price_paid' => null, + ]); + + $this->mockStripeInvoices([]); + + $this->artisan('subscriptions:backfill-prices') + ->assertSuccessful(); + + $this->assertDatabaseHas('subscriptions', [ + 'id' => $subscription->id, + 'price_paid' => null, + ]); + } +} diff --git a/tests/Feature/Commands/ProcessEligiblePayoutsTest.php b/tests/Feature/Commands/ProcessEligiblePayoutsTest.php new file mode 100644 index 00000000..a51387c3 --- /dev/null +++ b/tests/Feature/Commands/ProcessEligiblePayoutsTest.php @@ -0,0 +1,149 @@ +create(); + $plugin = Plugin::factory()->paid()->create(['user_id' => $developerAccount->user_id]); + $license = PluginLicense::factory()->create(['plugin_id' => $plugin->id]); + + $payout = PluginPayout::create([ + 'plugin_license_id' => $license->id, + 'developer_account_id' => $developerAccount->id, + 'gross_amount' => 1000, + 'platform_fee' => 300, + 'developer_amount' => 700, + 'status' => PayoutStatus::Pending, + 'eligible_for_payout_at' => now()->subDay(), + ]); + + $this->artisan('payouts:process-eligible') + ->expectsOutputToContain('Dispatched 1 payout transfer job(s)') + ->assertExitCode(0); + + Queue::assertPushed(ProcessPayoutTransfer::class, function ($job) use ($payout) { + return $job->payout->id === $payout->id; + }); + } + + public function test_skips_payouts_still_within_holding_period(): void + { + Queue::fake(); + + $developerAccount = DeveloperAccount::factory()->create(); + $plugin = Plugin::factory()->paid()->create(['user_id' => $developerAccount->user_id]); + $license = PluginLicense::factory()->create(['plugin_id' => $plugin->id]); + + PluginPayout::create([ + 'plugin_license_id' => $license->id, + 'developer_account_id' => $developerAccount->id, + 'gross_amount' => 1000, + 'platform_fee' => 300, + 'developer_amount' => 700, + 'status' => PayoutStatus::Pending, + 'eligible_for_payout_at' => now()->addDays(10), + ]); + + $this->artisan('payouts:process-eligible') + ->expectsOutputToContain('No eligible payouts') + ->assertExitCode(0); + + Queue::assertNothingPushed(); + } + + public function test_skips_non_pending_payouts(): void + { + Queue::fake(); + + $developerAccount = DeveloperAccount::factory()->create(); + $plugin = Plugin::factory()->paid()->create(['user_id' => $developerAccount->user_id]); + $license = PluginLicense::factory()->create(['plugin_id' => $plugin->id]); + + PluginPayout::create([ + 'plugin_license_id' => $license->id, + 'developer_account_id' => $developerAccount->id, + 'gross_amount' => 1000, + 'platform_fee' => 300, + 'developer_amount' => 700, + 'status' => PayoutStatus::Transferred, + 'eligible_for_payout_at' => now()->subDay(), + 'transferred_at' => now()->subDay(), + 'stripe_transfer_id' => 'tr_test', + ]); + + $this->artisan('payouts:process-eligible') + ->expectsOutputToContain('No eligible payouts') + ->assertExitCode(0); + + Queue::assertNothingPushed(); + } + + public function test_dispatches_only_eligible_payouts_among_mixed(): void + { + Queue::fake(); + + $developerAccount = DeveloperAccount::factory()->create(); + $plugin = Plugin::factory()->paid()->create(['user_id' => $developerAccount->user_id]); + + // Eligible payout (past holding period) + $eligibleLicense = PluginLicense::factory()->create(['plugin_id' => $plugin->id]); + $eligiblePayout = PluginPayout::create([ + 'plugin_license_id' => $eligibleLicense->id, + 'developer_account_id' => $developerAccount->id, + 'gross_amount' => 1000, + 'platform_fee' => 300, + 'developer_amount' => 700, + 'status' => PayoutStatus::Pending, + 'eligible_for_payout_at' => now()->subDays(2), + ]); + + // Not yet eligible payout (still in holding period) + $futureLicense = PluginLicense::factory()->create(['plugin_id' => $plugin->id]); + PluginPayout::create([ + 'plugin_license_id' => $futureLicense->id, + 'developer_account_id' => $developerAccount->id, + 'gross_amount' => 2000, + 'platform_fee' => 600, + 'developer_amount' => 1400, + 'status' => PayoutStatus::Pending, + 'eligible_for_payout_at' => now()->addDays(10), + ]); + + $this->artisan('payouts:process-eligible') + ->expectsOutputToContain('Dispatched 1 payout transfer job(s)') + ->assertExitCode(0); + + Queue::assertPushed(ProcessPayoutTransfer::class, 1); + Queue::assertPushed(ProcessPayoutTransfer::class, function ($job) use ($eligiblePayout) { + return $job->payout->id === $eligiblePayout->id; + }); + } + + public function test_returns_success_when_no_payouts_exist(): void + { + Queue::fake(); + + $this->artisan('payouts:process-eligible') + ->expectsOutputToContain('No eligible payouts') + ->assertExitCode(0); + + Queue::assertNothingPushed(); + } +} diff --git a/tests/Feature/CoursePageTest.php b/tests/Feature/CoursePageTest.php new file mode 100644 index 00000000..55427a75 --- /dev/null +++ b/tests/Feature/CoursePageTest.php @@ -0,0 +1,113 @@ +withoutVite() + ->get(route('course')) + ->assertStatus(200) + ->assertSee('The NativePHP Masterclass') + ->assertSee('Early Bird'); + } + + #[Test] + public function course_page_contains_mailcoach_signup_form(): void + { + $this + ->withoutVite() + ->get(route('course')) + ->assertSee('simonhamp.mailcoach.app/subscribe/', false) + ->assertSee('Join Waitlist'); + } + + #[Test] + public function course_page_contains_checkout_form(): void + { + $this + ->withoutVite() + ->get(route('course')) + ->assertSee(route('course.checkout'), false) + ->assertSee('Get Early Bird Access'); + } + + #[Test] + public function course_checkout_redirects_guests_to_login(): void + { + $this + ->post(route('course.checkout')) + ->assertRedirect(route('customer.login')); + } + + #[Test] + public function course_checkout_redirects_to_stripe_with_cart_success_url(): void + { + $user = User::factory()->create(['stripe_id' => 'cus_test123']); + + $stripeSessionUrl = 'https://checkout.stripe.com/test-session'; + $capturedParams = null; + + $mockCheckoutSessions = new class($stripeSessionUrl, $capturedParams) + { + public function __construct( + private string $url, + private &$capturedParams, + ) {} + + public function create(array $params): object + { + $this->capturedParams = $params; + + return (object) [ + 'id' => 'cs_test123', + 'url' => $this->url, + ]; + } + }; + + $mockCheckout = new \stdClass; + $mockCheckout->sessions = $mockCheckoutSessions; + + $mockCustomers = new class + { + public function retrieve(): Customer + { + return Customer::constructFrom([ + 'id' => 'cus_test123', + 'name' => 'Test User', + 'email' => 'test@example.com', + ]); + } + }; + + $mockStripeClient = $this->createMock(StripeClient::class); + $mockStripeClient->checkout = $mockCheckout; + $mockStripeClient->customers = $mockCustomers; + + $this->app->bind(StripeClient::class, fn () => $mockStripeClient); + + $this + ->actingAs($user) + ->post(route('course.checkout')) + ->assertRedirect($stripeSessionUrl); + + $this->assertNotNull($capturedParams, 'Stripe checkout session should have been created'); + $this->assertStringContainsString(route('cart.success'), $capturedParams['success_url']); + $this->assertStringContainsString('{CHECKOUT_SESSION_ID}', $capturedParams['success_url']); + $this->assertEquals(['enabled' => true], $capturedParams['tax_id_collection']); + $this->assertEquals(['name' => 'auto', 'address' => 'auto'], $capturedParams['customer_update']); + } +} diff --git a/tests/Feature/CustomerAuthenticationTest.php b/tests/Feature/CustomerAuthenticationTest.php new file mode 100644 index 00000000..2673937c --- /dev/null +++ b/tests/Feature/CustomerAuthenticationTest.php @@ -0,0 +1,98 @@ +get('/login'); + + $response->assertStatus(200); + $response->assertSee('Sign in to your account'); + $response->assertSee('create a new account'); + } + + public function test_customer_can_login_with_valid_credentials(): void + { + $user = User::factory()->create([ + 'email' => 'customer@example.com', + 'password' => Hash::make('password'), + ]); + + $response = $this->post('/login', [ + 'email' => 'customer@example.com', + 'password' => 'password', + ]); + + $response->assertRedirect('/dashboard'); + $this->assertAuthenticatedAs($user); + } + + public function test_customer_cannot_login_with_invalid_credentials(): void + { + $user = User::factory()->create([ + 'email' => 'customer@example.com', + 'password' => Hash::make('password'), + ]); + + $response = $this->post('/login', [ + 'email' => 'customer@example.com', + 'password' => 'wrong-password', + ]); + + $response->assertSessionHasErrors(['email']); + $this->assertGuest(); + } + + public function test_customer_can_logout(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->post('/logout'); + + $response->assertRedirect('/login'); + $this->assertGuest(); + } + + public function test_customer_can_view_forgot_password_page(): void + { + $response = $this->get('/forgot-password'); + + $response->assertStatus(200); + $response->assertSee('Reset your password'); + } + + public function test_authenticated_customer_is_redirected_from_login_page(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->get('/login'); + + $response->assertRedirect('/dashboard'); + } + + public function test_unauthenticated_customer_is_redirected_to_login(): void + { + $response = $this->get('/dashboard'); + + $response->assertRedirect('/login'); + } +} diff --git a/tests/Feature/CustomerLicenseManagementTest.php b/tests/Feature/CustomerLicenseManagementTest.php new file mode 100644 index 00000000..76756db8 --- /dev/null +++ b/tests/Feature/CustomerLicenseManagementTest.php @@ -0,0 +1,412 @@ +create(); + + $response = $this->actingAs($user)->get('/dashboard/licenses'); + + $response->assertStatus(200); + $response->assertSee('Your Licenses'); + $response->assertSee('Manage your NativePHP licenses'); + } + + public function test_customer_sees_no_licenses_message_when_no_licenses_exist(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->get('/dashboard/licenses'); + + $response->assertStatus(200); + $response->assertSee('No licenses found'); + $response->assertSee('believe this is an error'); + } + + public function test_customer_can_view_their_licenses(): void + { + $user = User::factory()->create(); + $license1 = License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'Standard License', + 'key' => 'test-key-1', + ]); + $license2 = License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'Premium License', + 'key' => 'test-key-2', + ]); + + $response = $this->actingAs($user)->get('/dashboard/licenses'); + + $response->assertStatus(200); + $response->assertSee('Standard License'); + $response->assertSee('Premium License'); + $response->assertSee('test-key-1'); + $response->assertSee('test-key-2'); + } + + public function test_customer_cannot_view_other_customers_licenses(): void + { + $user1 = User::factory()->create(); + $user2 = User::factory()->create(); + + $license1 = License::factory()->create([ + 'user_id' => $user1->id, + 'policy_name' => 'User 1 License', + ]); + $license2 = License::factory()->create([ + 'user_id' => $user2->id, + 'policy_name' => 'User 2 License', + ]); + + $response = $this->actingAs($user1)->get('/dashboard/licenses'); + + $response->assertStatus(200); + $response->assertSee('User 1 License'); + $response->assertDontSee('User 2 License'); + } + + public function test_customer_can_view_individual_license_details(): void + { + $user = User::factory()->create(); + $license = License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'pro', + 'key' => 'test-license-key-123', + 'expires_at' => now()->addDays(30), + ]); + + $response = $this->actingAs($user)->get('/dashboard/licenses/'.$license->key); + + $response->assertStatus(200); + $response->assertSee('pro'); + $response->assertSee('test-license-key-123'); + $response->assertSee('License Information'); + $response->assertSee('Active'); + } + + public function test_customer_cannot_view_other_customers_license_details(): void + { + $user1 = User::factory()->create(); + $user2 = User::factory()->create(); + + $license = License::factory()->create([ + 'user_id' => $user2->id, + 'key' => 'other-user-license', + ]); + + $response = $this->actingAs($user1)->get('/dashboard/licenses/'.$license->key); + + $response->assertStatus(404); + } + + public function test_license_status_displays_correctly(): void + { + $user = User::factory()->create(); + + // Active license + $activeLicense = License::factory()->create([ + 'user_id' => $user->id, + 'expires_at' => now()->addDays(30), + 'is_suspended' => false, + ]); + + // Expired license + $expiredLicense = License::factory()->create([ + 'user_id' => $user->id, + 'expires_at' => now()->subDays(1), + 'is_suspended' => false, + ]); + + // Suspended license + $suspendedLicense = License::factory()->create([ + 'user_id' => $user->id, + 'is_suspended' => true, + ]); + + $response = $this->actingAs($user)->get('/dashboard/licenses'); + + $response->assertStatus(200); + $response->assertSee('Active'); + $response->assertSee('Expired'); + $response->assertSee('Suspended'); + } + + public function test_customer_can_update_license_name(): void + { + $user = User::factory()->create(); + $license = License::factory()->create([ + 'user_id' => $user->id, + 'key' => 'test-license-key', + 'name' => null, + ]); + + $response = $this->actingAs($user) + ->patch('/dashboard/licenses/'.$license->key, [ + 'name' => 'My Production License', + ]); + + $response->assertRedirect('/dashboard/licenses/'.$license->key); + $response->assertSessionHas('success', 'License name updated successfully!'); + + $this->assertDatabaseHas('licenses', [ + 'id' => $license->id, + 'name' => 'My Production License', + ]); + } + + public function test_customer_can_clear_license_name(): void + { + $user = User::factory()->create(); + $license = License::factory()->create([ + 'user_id' => $user->id, + 'key' => 'test-license-key', + 'name' => 'Old Name', + ]); + + $response = $this->actingAs($user) + ->patch('/dashboard/licenses/'.$license->key, [ + 'name' => '', + ]); + + $response->assertRedirect('/dashboard/licenses/'.$license->key); + $response->assertSessionHas('success', 'License name updated successfully!'); + + $this->assertDatabaseHas('licenses', [ + 'id' => $license->id, + 'name' => null, + ]); + } + + public function test_license_name_validation(): void + { + $user = User::factory()->create(); + $license = License::factory()->create([ + 'user_id' => $user->id, + 'key' => 'test-license-key', + ]); + + $response = $this->actingAs($user) + ->patch('/dashboard/licenses/'.$license->key, [ + 'name' => str_repeat('a', 256), // Too long + ]); + + $response->assertSessionHasErrors(['name']); + } + + public function test_customer_cannot_update_other_customers_license_name(): void + { + $user1 = User::factory()->create(); + $user2 = User::factory()->create(); + + $license = License::factory()->create([ + 'user_id' => $user2->id, + 'key' => 'other-user-license', + ]); + + $response = $this->actingAs($user1) + ->patch('/dashboard/licenses/'.$license->key, [ + 'name' => 'Hacked Name', + ]); + + $response->assertStatus(404); + } + + public function test_license_names_display_on_list_page(): void + { + $user = User::factory()->create(); + + $namedLicense = License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'pro', + 'name' => 'My Custom License Name', + ]); + + $unnamedLicense = License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'starter', + 'name' => null, + ]); + + $response = $this->actingAs($user)->get('/dashboard/licenses'); + + $response->assertStatus(200); + // Named license should show custom name prominently + $response->assertSee('My Custom License Name'); + $response->assertSee('pro'); + // Unnamed license should show policy name + $response->assertSee('starter'); + } + + public function test_license_name_displays_on_show_page(): void + { + $user = User::factory()->create(); + $license = License::factory()->create([ + 'user_id' => $user->id, + 'key' => 'test-license-key', + 'name' => 'My Custom License', + ]); + + $response = $this->actingAs($user)->get('/dashboard/licenses/'.$license->key); + + $response->assertStatus(200); + $response->assertSee('My Custom License'); + $response->assertSee('License Name'); + } + + public function test_license_show_page_displays_no_name_set_when_name_is_null(): void + { + $user = User::factory()->create(); + $license = License::factory()->create([ + 'user_id' => $user->id, + 'key' => 'test-license-key', + 'name' => null, + ]); + + $response = $this->actingAs($user)->get('/dashboard/licenses/'.$license->key); + + $response->assertStatus(200); + $response->assertSee('No name set'); + } + + public function test_dashboard_shows_license_count(): void + { + $user = User::factory()->create(); + License::factory()->count(3)->create(['user_id' => $user->id]); + + $response = $this->actingAs($user)->get('/dashboard'); + + $response->assertStatus(200); + $response->assertSee('View licenses'); + } + + public function test_dashboard_hides_licenses_card_when_user_has_no_licenses(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->get('/dashboard'); + + $response->assertStatus(200); + $response->assertDontSee('View licenses'); + } + + public function test_customer_can_rotate_license_key(): void + { + Http::fake([ + 'https://api.anystack.sh/v1/products/*/licenses' => Http::response([ + 'data' => [ + 'id' => 'new-anystack-id', + 'key' => 'new-rotated-key', + 'expires_at' => now()->addYear()->toIso8601String(), + 'created_at' => now()->toIso8601String(), + 'updated_at' => now()->toIso8601String(), + ], + ], 201), + 'https://api.anystack.sh/v1/products/*/licenses/*' => Http::response([ + 'data' => ['suspended' => true], + ], 200), + ]); + + $user = User::factory()->create(['anystack_contact_id' => 'contact-123']); + $license = License::factory()->active()->create([ + 'user_id' => $user->id, + 'policy_name' => 'mini', + 'key' => 'old-key-to-rotate', + 'anystack_id' => 'old-anystack-id', + ]); + + Livewire::actingAs($user) + ->test(Show::class, ['licenseKey' => 'old-key-to-rotate']) + ->call('rotateLicenseKey') + ->assertRedirect(route('customer.licenses.show', 'new-rotated-key')); + + $license->refresh(); + $this->assertEquals('new-rotated-key', $license->key); + $this->assertEquals('new-anystack-id', $license->anystack_id); + $this->assertFalse($license->is_suspended); + } + + public function test_customer_cannot_rotate_suspended_license_key(): void + { + $user = User::factory()->create(); + $license = License::factory()->suspended()->create([ + 'user_id' => $user->id, + 'key' => 'suspended-license-key', + ]); + + Livewire::actingAs($user) + ->test(Show::class, ['licenseKey' => 'suspended-license-key']) + ->call('rotateLicenseKey') + ->assertNoRedirect(); + + Http::assertNothingSent(); + } + + public function test_customer_cannot_rotate_expired_license_key(): void + { + $user = User::factory()->create(); + $license = License::factory()->expired()->create([ + 'user_id' => $user->id, + 'key' => 'expired-license-key', + ]); + + Livewire::actingAs($user) + ->test(Show::class, ['licenseKey' => 'expired-license-key']) + ->call('rotateLicenseKey') + ->assertNoRedirect(); + + Http::assertNothingSent(); + } + + public function test_active_license_shows_rotate_button(): void + { + $user = User::factory()->create(); + $license = License::factory()->active()->create([ + 'user_id' => $user->id, + 'key' => 'active-license-key', + ]); + + $response = $this->actingAs($user)->get('/dashboard/licenses/active-license-key'); + + $response->assertStatus(200); + $response->assertSee('Rotate key'); + } + + public function test_suspended_license_does_not_show_rotate_button(): void + { + $user = User::factory()->create(); + $license = License::factory()->suspended()->create([ + 'user_id' => $user->id, + 'key' => 'suspended-license-key', + ]); + + $response = $this->actingAs($user)->get('/dashboard/licenses/suspended-license-key'); + + $response->assertStatus(200); + $response->assertDontSee('Rotate key'); + } +} diff --git a/tests/Feature/CustomerPluginReviewChecksTest.php b/tests/Feature/CustomerPluginReviewChecksTest.php new file mode 100644 index 00000000..1d38e97d --- /dev/null +++ b/tests/Feature/CustomerPluginReviewChecksTest.php @@ -0,0 +1,184 @@ +create([ + 'github_id' => '12345', + 'github_token' => encrypt('fake-token'), + ]); + DeveloperAccount::factory()->withAcceptedTerms()->create([ + 'user_id' => $user->id, + ]); + + $repoSlug = 'acme/test-plugin'; + $base = "https://api.github.com/repos/{$repoSlug}"; + $composerJson = json_encode([ + 'name' => 'acme/test-plugin', + 'description' => 'A test plugin', + 'require' => [ + 'php' => '^8.1', + 'nativephp/mobile' => '^3.0.0', + ], + ]); + + Http::fake([ + // PluginSyncService calls + "{$base}/contents/README.md" => Http::response([ + 'content' => base64_encode('# Test Plugin'), + 'encoding' => 'base64', + ]), + "{$base}/contents/composer.json*" => Http::response([ + 'content' => base64_encode($composerJson), + 'encoding' => 'base64', + ]), + "{$base}/contents/nativephp.json" => Http::response([], 404), + "{$base}/contents/LICENSE*" => Http::response([], 404), + "{$base}/releases/latest" => Http::response(['tag_name' => 'v1.0.0']), + "{$base}/tags*" => Http::response([]), + "https://raw.githubusercontent.com/{$repoSlug}/*" => Http::response('', 404), + + // Webhook creation + "{$base}/hooks" => Http::response(['id' => 1], 201), + + // ReviewPluginRepository calls + $base => Http::response(['default_branch' => 'main']), + "{$base}/git/trees/main*" => Http::response([ + 'tree' => [ + ['path' => 'LICENSE', 'type' => 'blob'], + ['path' => 'resources/ios/Plugin.swift', 'type' => 'blob'], + ['path' => 'resources/android/Plugin.kt', 'type' => 'blob'], + ['path' => 'resources/js/index.js', 'type' => 'blob'], + ['path' => 'src/ServiceProvider.php', 'type' => 'blob'], + ], + ]), + "{$base}/readme" => Http::response([ + 'content' => base64_encode('# Test Plugin'), + 'encoding' => 'base64', + ]), + ]); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('repository', $repoSlug) + ->set('pluginType', 'free') + ->set('supportChannel', 'dev@testplugin.io') + ->call('submitPlugin') + ->assertRedirect(); + + $plugin = $user->plugins()->where('repository_url', "https://github.com/{$repoSlug}")->first(); + + $this->assertNotNull($plugin, 'Plugin should exist after submission'); + $this->assertNotNull($plugin->review_checks, 'review_checks should be populated'); + $this->assertTrue($plugin->review_checks['has_license_file']); + $this->assertTrue($plugin->review_checks['has_release_version']); + $this->assertEquals('v1.0.0', $plugin->review_checks['release_version']); + $this->assertTrue($plugin->review_checks['supports_ios']); + $this->assertTrue($plugin->review_checks['supports_android']); + $this->assertTrue($plugin->review_checks['supports_js']); + $this->assertTrue($plugin->review_checks['requires_mobile_sdk']); + $this->assertEquals('^3.0.0', $plugin->review_checks['mobile_sdk_constraint']); + $this->assertNotNull($plugin->reviewed_at); + + Notification::assertSentTo($user, PluginSubmitted::class, function (PluginSubmitted $notification) use ($plugin) { + return $notification->plugin->id === $plugin->id; + }); + } + + /** @test */ + public function plugin_submitted_email_includes_failing_checks(): void + { + Notification::fake(); + + $user = User::factory()->create([ + 'github_id' => '12345', + 'github_token' => encrypt('fake-token'), + ]); + DeveloperAccount::factory()->withAcceptedTerms()->create([ + 'user_id' => $user->id, + ]); + + $repoSlug = 'acme/bare-plugin'; + $base = "https://api.github.com/repos/{$repoSlug}"; + $composerJson = json_encode([ + 'name' => 'acme/bare-plugin', + 'description' => 'A bare plugin', + 'require' => ['php' => '^8.1'], + ]); + + Http::fake([ + "{$base}/contents/README.md" => Http::response([ + 'content' => base64_encode('# Bare Plugin'), + 'encoding' => 'base64', + ]), + "{$base}/contents/composer.json*" => Http::response([ + 'content' => base64_encode($composerJson), + 'encoding' => 'base64', + ]), + "{$base}/contents/nativephp.json" => Http::response([], 404), + "{$base}/contents/LICENSE*" => Http::response([], 404), + "{$base}/releases/latest" => Http::response([], 404), + "{$base}/tags*" => Http::response([]), + "https://raw.githubusercontent.com/{$repoSlug}/*" => Http::response('', 404), + + // Webhook creation (fails) + "{$base}/hooks" => Http::response([], 422), + + $base => Http::response(['default_branch' => 'main']), + "{$base}/git/trees/main*" => Http::response([ + 'tree' => [ + ['path' => 'src/ServiceProvider.php', 'type' => 'blob'], + ], + ]), + "{$base}/readme" => Http::response([ + 'content' => base64_encode('# Bare Plugin'), + 'encoding' => 'base64', + ]), + ]); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('repository', $repoSlug) + ->set('pluginType', 'free') + ->set('supportChannel', 'support@bare-plugin.io') + ->call('submitPlugin'); + + $plugin = $user->plugins()->where('repository_url', "https://github.com/{$repoSlug}")->first(); + + Notification::assertSentTo($user, PluginSubmitted::class, function (PluginSubmitted $notification) use ($plugin) { + $mail = $notification->toMail($plugin->user); + $rendered = $mail->render()->toHtml(); + + // Should mention failing required checks + $this->assertStringContainsString('LICENSE', $rendered); + $this->assertStringContainsString('release version', $rendered); + $this->assertStringContainsString('webhook', $rendered); + + // Should mention failing optional checks + $this->assertStringContainsString('Add iOS support', $rendered); + $this->assertStringContainsString('Add Android support', $rendered); + $this->assertStringContainsString('Add JavaScript support', $rendered); + $this->assertStringContainsString('nativephp/mobile SDK', $rendered); + + return true; + }); + } +} diff --git a/tests/Feature/CustomerPurchasedPluginsIndexTest.php b/tests/Feature/CustomerPurchasedPluginsIndexTest.php new file mode 100644 index 00000000..bd4a06f5 --- /dev/null +++ b/tests/Feature/CustomerPurchasedPluginsIndexTest.php @@ -0,0 +1,107 @@ +get('/dashboard/purchased-plugins'); + + $response->assertRedirect('/login'); + } + + public function test_customer_can_view_purchased_plugins_page(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->get('/dashboard/purchased-plugins'); + + $response->assertStatus(200); + $response->assertSee('Purchased Plugins'); + $response->assertSee('Your Plugin Credentials'); + } + + public function test_customer_sees_empty_state_when_no_plugins(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->get('/dashboard/purchased-plugins'); + + $response->assertStatus(200); + $response->assertSee('No plugins yet'); + $response->assertSee('Browse Plugins'); + } + + public function test_customer_sees_their_purchased_plugins(): void + { + $user = User::factory()->create(); + $plugin = Plugin::factory()->approved()->create(['name' => 'acme/test-plugin-123']); + + PluginLicense::factory()->create([ + 'user_id' => $user->id, + 'plugin_id' => $plugin->id, + 'purchased_at' => now(), + ]); + + $response = $this->actingAs($user)->get('/dashboard/purchased-plugins'); + + $response->assertStatus(200); + $response->assertSee('acme/test-plugin-123'); + $response->assertSee('Licensed'); + } + + public function test_customer_does_not_see_other_users_plugins(): void + { + $user1 = User::factory()->create(); + $user2 = User::factory()->create(); + $plugin1 = Plugin::factory()->approved()->create(['name' => 'acme/user1-plugin-111']); + $plugin2 = Plugin::factory()->approved()->create(['name' => 'acme/user2-plugin-222']); + + PluginLicense::factory()->create([ + 'user_id' => $user1->id, + 'plugin_id' => $plugin1->id, + ]); + + PluginLicense::factory()->create([ + 'user_id' => $user2->id, + 'plugin_id' => $plugin2->id, + ]); + + $response = $this->actingAs($user1)->get('/dashboard/purchased-plugins'); + + $response->assertStatus(200); + $response->assertSee('acme/user1-plugin-111'); + $response->assertDontSee('acme/user2-plugin-222'); + } + + public function test_plugin_credentials_section_displays(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->get('/dashboard/purchased-plugins'); + + $response->assertStatus(200); + $response->assertSee('Your Plugin Credentials'); + $response->assertSee('Composer'); + } +} diff --git a/tests/Feature/CustomerShowcaseIndexTest.php b/tests/Feature/CustomerShowcaseIndexTest.php new file mode 100644 index 00000000..5b6496e6 --- /dev/null +++ b/tests/Feature/CustomerShowcaseIndexTest.php @@ -0,0 +1,124 @@ +get('/dashboard/showcase'); + + $response->assertRedirect('/login'); + } + + public function test_customer_can_view_showcase_page(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->get('/dashboard/showcase'); + + $response->assertStatus(200); + $response->assertSee('Your Showcase Submissions'); + $response->assertSee('Submit New App'); + } + + public function test_customer_sees_empty_state_when_no_submissions(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->get('/dashboard/showcase'); + + $response->assertStatus(200); + $response->assertSee('No submissions yet'); + $response->assertSee('Submit Your App'); + } + + public function test_customer_sees_their_showcase_submissions(): void + { + $user = User::factory()->create(); + + $showcase = Showcase::factory()->approved()->create([ + 'user_id' => $user->id, + 'title' => 'My Test App', + 'has_mobile' => true, + 'has_desktop' => false, + ]); + + $response = $this->actingAs($user)->get('/dashboard/showcase'); + + $response->assertStatus(200); + $response->assertSee('My Test App'); + $response->assertSee('Approved'); + $response->assertSee('Mobile'); + } + + public function test_customer_does_not_see_other_users_submissions(): void + { + $user1 = User::factory()->create(); + $user2 = User::factory()->create(); + + Showcase::factory()->create([ + 'user_id' => $user1->id, + 'title' => 'User 1 App', + ]); + + Showcase::factory()->create([ + 'user_id' => $user2->id, + 'title' => 'User 2 App', + ]); + + $response = $this->actingAs($user1)->get('/dashboard/showcase'); + + $response->assertStatus(200); + $response->assertSee('User 1 App'); + $response->assertDontSee('User 2 App'); + } + + public function test_pending_showcase_shows_pending_review_status(): void + { + $user = User::factory()->create(); + + Showcase::factory()->pending()->create([ + 'user_id' => $user->id, + 'title' => 'Pending App', + ]); + + $response = $this->actingAs($user)->get('/dashboard/showcase'); + + $response->assertStatus(200); + $response->assertSee('Pending App'); + $response->assertSee('Pending Review'); + } + + public function test_showcase_displays_platform_badges(): void + { + $user = User::factory()->create(); + + Showcase::factory()->both()->create([ + 'user_id' => $user->id, + 'title' => 'Both Platforms App', + ]); + + $response = $this->actingAs($user)->get('/dashboard/showcase'); + + $response->assertStatus(200); + $response->assertSee('Mobile'); + $response->assertSee('Desktop'); + } +} diff --git a/tests/Feature/CustomerSubLicenseManagementTest.php b/tests/Feature/CustomerSubLicenseManagementTest.php new file mode 100644 index 00000000..5cf01958 --- /dev/null +++ b/tests/Feature/CustomerSubLicenseManagementTest.php @@ -0,0 +1,523 @@ +create([ + 'anystack_contact_id' => fake()->uuid(), + ]); + $license = License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'pro', // Pro supports sub-licenses with limit of 10 + 'is_suspended' => false, + 'expires_at' => now()->addDays(30), + 'anystack_id' => fake()->uuid(), + ]); + + $response = $this->actingAs($user) + ->post("/dashboard/licenses/{$license->key}/sub-licenses", [ + 'name' => 'Development Team', + ]); + + $response->assertRedirect("/dashboard/licenses/{$license->key}") + ->assertSessionHas('success', 'Sub-license is being created. You will receive an email notification when it\'s ready.'); + + Queue::assertPushed(CreateAnystackSubLicenseJob::class); + } + + public function test_customer_can_create_sub_license_without_name(): void + { + Queue::fake(); + + $user = User::factory()->create([ + 'anystack_contact_id' => fake()->uuid(), + ]); + $license = License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'pro', + 'is_suspended' => false, + 'expires_at' => now()->addDays(30), + 'anystack_id' => fake()->uuid(), + ]); + + $response = $this->actingAs($user) + ->post("/dashboard/licenses/{$license->key}/sub-licenses", [ + 'name' => '', + ]); + + $response->assertRedirect("/dashboard/licenses/{$license->key}") + ->assertSessionHas('success', 'Sub-license is being created. You will receive an email notification when it\'s ready.'); + + Queue::assertPushed(CreateAnystackSubLicenseJob::class); + } + + public function test_customer_cannot_create_sub_license_for_suspended_license(): void + { + $user = User::factory()->create(); + $license = License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'pro', + 'is_suspended' => true, + ]); + + $response = $this->actingAs($user) + ->post("/dashboard/licenses/{$license->key}/sub-licenses", [ + 'name' => 'Development Team', + ]); + + $response->assertRedirect("/dashboard/licenses/{$license->key}") + ->assertSessionHasErrors(['sub_license']); + + $this->assertDatabaseMissing('sub_licenses', [ + 'parent_license_id' => $license->id, + ]); + } + + public function test_customer_cannot_create_sub_license_for_expired_license(): void + { + $user = User::factory()->create(); + $license = License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'pro', + 'is_suspended' => false, + 'expires_at' => now()->subDays(1), + ]); + + $response = $this->actingAs($user) + ->post("/dashboard/licenses/{$license->key}/sub-licenses", [ + 'name' => 'Development Team', + ]); + + $response->assertRedirect("/dashboard/licenses/{$license->key}") + ->assertSessionHasErrors(['sub_license']); + + $this->assertDatabaseMissing('sub_licenses', [ + 'parent_license_id' => $license->id, + ]); + } + + public function test_customer_can_update_sub_license_name(): void + { + $user = User::factory()->create(); + $license = License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'pro', + ]); + $subLicense = SubLicense::factory()->create([ + 'parent_license_id' => $license->id, + 'name' => 'Old Name', + ]); + + $response = $this->actingAs($user) + ->patch("/dashboard/licenses/{$license->key}/sub-licenses/{$subLicense->id}", [ + 'name' => 'New Name', + ]); + + $response->assertRedirect("/dashboard/licenses/{$license->key}") + ->assertSessionHas('success', 'Sub-license updated successfully!'); + + $this->assertDatabaseHas('sub_licenses', [ + 'id' => $subLicense->id, + 'name' => 'New Name', + ]); + } + + public function test_customer_can_suspend_sub_license(): void + { + Http::fake([ + 'api.anystack.sh/v1/products/*/licenses/*' => Http::response(['success' => true], 200), + ]); + + $user = User::factory()->create(); + $license = License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'pro', + ]); + $subLicense = SubLicense::factory()->create([ + 'parent_license_id' => $license->id, + 'is_suspended' => false, + ]); + + $response = $this->actingAs($user) + ->patch("/dashboard/licenses/{$license->key}/sub-licenses/{$subLicense->id}/suspend"); + + $response->assertRedirect("/dashboard/licenses/{$license->key}") + ->assertSessionHas('success', 'Sub-license suspended successfully!'); + + $this->assertDatabaseHas('sub_licenses', [ + 'id' => $subLicense->id, + 'is_suspended' => true, + ]); + } + + public function test_customer_can_delete_sub_license(): void + { + Http::fake([ + 'api.anystack.sh/v1/products/*/licenses/*' => Http::response(['success' => true], 200), + ]); + + $user = User::factory()->create(); + $license = License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'pro', + ]); + $subLicense = SubLicense::factory()->create([ + 'parent_license_id' => $license->id, + ]); + + $response = $this->actingAs($user) + ->delete("/dashboard/licenses/{$license->key}/sub-licenses/{$subLicense->id}"); + + $response->assertRedirect("/dashboard/licenses/{$license->key}") + ->assertSessionHas('success', 'Sub-license deleted successfully!'); + + $this->assertDatabaseMissing('sub_licenses', [ + 'id' => $subLicense->id, + ]); + } + + public function test_customer_cannot_access_other_customer_sub_licenses(): void + { + $user1 = User::factory()->create(); + $user2 = User::factory()->create(); + + $license1 = License::factory()->create(['user_id' => $user1->id]); + $license2 = License::factory()->create(['user_id' => $user2->id]); + + $subLicense = SubLicense::factory()->create([ + 'parent_license_id' => $license2->id, + ]); + + // Try to update another user's sub-license + $response = $this->actingAs($user1) + ->patch("/dashboard/licenses/{$license2->key}/sub-licenses/{$subLicense->id}", [ + 'name' => 'Malicious Update', + ]); + + $response->assertStatus(404); + + // Try to delete another user's sub-license + $response = $this->actingAs($user1) + ->delete("/dashboard/licenses/{$license2->key}/sub-licenses/{$subLicense->id}"); + + $response->assertStatus(404); + } + + public function test_customer_cannot_manage_sub_license_with_wrong_parent_license(): void + { + $user = User::factory()->create(); + $license1 = License::factory()->create(['user_id' => $user->id]); + $license2 = License::factory()->create(['user_id' => $user->id]); + + $subLicense = SubLicense::factory()->create([ + 'parent_license_id' => $license2->id, + ]); + + // Try to manage sub-license using wrong parent license key + $response = $this->actingAs($user) + ->patch("/dashboard/licenses/{$license1->key}/sub-licenses/{$subLicense->id}", [ + 'name' => 'Wrong Parent', + ]); + + $response->assertStatus(404); + } + + public function test_sub_license_inherits_expiry_from_parent_license(): void + { + $user = User::factory()->create(); + $expiresAt = now()->addDays(30); + $license = License::factory()->create([ + 'user_id' => $user->id, + 'expires_at' => $expiresAt, + ]); + + // Test the model boot logic directly by creating a sub-license + $subLicense = SubLicense::factory()->create([ + 'parent_license_id' => $license->id, + 'expires_at' => null, // Let the boot method set it + ]); + + $this->assertEquals($expiresAt->toDateString(), $subLicense->expires_at->toDateString()); + } + + public function test_sub_license_shows_correct_status(): void + { + $user = User::factory()->create(); + $license = License::factory()->create(['user_id' => $user->id]); + + // Test active status + $activeSubLicense = SubLicense::factory()->create([ + 'parent_license_id' => $license->id, + 'is_suspended' => false, + 'expires_at' => now()->addDays(30), + ]); + + $this->assertEquals('Active', $activeSubLicense->status); + $this->assertTrue($activeSubLicense->isActive()); + $this->assertFalse($activeSubLicense->isExpired()); + + // Test suspended status + $suspendedSubLicense = SubLicense::factory()->create([ + 'parent_license_id' => $license->id, + 'is_suspended' => true, + 'expires_at' => now()->addDays(30), + ]); + + $this->assertEquals('Suspended', $suspendedSubLicense->status); + $this->assertFalse($suspendedSubLicense->isActive()); + + // Test expired status + $expiredSubLicense = SubLicense::factory()->create([ + 'parent_license_id' => $license->id, + 'is_suspended' => false, + 'expires_at' => now()->subDays(1), + ]); + + $this->assertEquals('Expired', $expiredSubLicense->status); + $this->assertFalse($expiredSubLicense->isActive()); + $this->assertTrue($expiredSubLicense->isExpired()); + } + + public function test_license_show_page_displays_sub_licenses(): void + { + $user = User::factory()->create(); + $license = License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'pro', + ]); + + $subLicense1 = SubLicense::factory()->create([ + 'parent_license_id' => $license->id, + 'name' => 'Development Team', + 'is_suspended' => false, + ]); + + $subLicense2 = SubLicense::factory()->create([ + 'parent_license_id' => $license->id, + 'name' => 'Testing Team', + 'is_suspended' => true, + ]); + + $response = $this->actingAs($user)->get("/dashboard/licenses/{$license->key}"); + + $response->assertStatus(200); + $response->assertSee('Keys'); + $response->assertSee('Development Team'); + $response->assertSee('Testing Team'); + $response->assertSee($subLicense1->key); + $response->assertSee($subLicense2->key); + $response->assertSee('Active'); + $response->assertSee('Suspended'); + } + + public function test_validation_for_sub_license_name(): void + { + $user = User::factory()->create(); + $license = License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'pro', + ]); + + // Test name too long + $response = $this->actingAs($user) + ->post("/dashboard/licenses/{$license->key}/sub-licenses", [ + 'name' => str_repeat('a', 256), // 256 characters, should fail + ]); + + $response->assertSessionHasErrors(['name']); + } + + public function test_livewire_create_sub_license_dispatches_job_and_starts_polling(): void + { + Queue::fake(); + + $user = User::factory()->create(); + $license = License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'pro', + 'is_suspended' => false, + 'expires_at' => now()->addDays(30), + 'anystack_id' => fake()->uuid(), + ]); + + $this->actingAs($user); + + Livewire::test(SubLicenseManager::class, ['license' => $license]) + ->assertSet('isPolling', false) + ->call('openCreateModal') + ->set('createName', 'Dev Team') + ->set('createAssignedEmail', 'dev@example.com') + ->call('createSubLicense') + ->assertSet('isPolling', true) + ->assertSet('createName', '') + ->assertSet('createAssignedEmail', ''); + + Queue::assertPushed(CreateAnystackSubLicenseJob::class); + } + + public function test_livewire_create_sub_license_validates_email(): void + { + $user = User::factory()->create(); + $license = License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'pro', + 'is_suspended' => false, + 'expires_at' => now()->addDays(30), + ]); + + $this->actingAs($user); + + Livewire::test(SubLicenseManager::class, ['license' => $license]) + ->call('openCreateModal') + ->set('createAssignedEmail', 'not-an-email') + ->call('createSubLicense') + ->assertHasErrors(['createAssignedEmail']); + } + + public function test_livewire_component_stops_polling_when_new_sublicense_appears(): void + { + Queue::fake(); + + $user = User::factory()->create(); + $license = License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'pro', + 'is_suspended' => false, + 'expires_at' => now()->addDays(30), + 'anystack_id' => fake()->uuid(), + ]); + + $this->actingAs($user); + + $component = Livewire::test(SubLicenseManager::class, ['license' => $license]) + ->assertSet('isPolling', false) + ->assertSet('initialSubLicenseCount', 0) + ->call('openCreateModal') + ->set('createName', 'Test') + ->call('createSubLicense') + ->assertSet('isPolling', true); + + // Create a new sublicense (simulating the async job completing) + SubLicense::factory()->create([ + 'parent_license_id' => $license->id, + ]); + + // Re-render the component (simulating a poll) + $component->call('$refresh') + ->assertSet('isPolling', false) + ->assertSet('initialSubLicenseCount', 1); + } + + public function test_livewire_edit_sub_license_updates_name_and_email(): void + { + Queue::fake(); + + $user = User::factory()->create(); + $license = License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'pro', + ]); + $subLicense = SubLicense::factory()->create([ + 'parent_license_id' => $license->id, + 'name' => 'Old Name', + 'assigned_email' => null, + ]); + + $this->actingAs($user); + + Livewire::test(SubLicenseManager::class, ['license' => $license]) + ->call('editSubLicense', $subLicense->id) + ->assertSet('editingSubLicenseId', $subLicense->id) + ->assertSet('editName', 'Old Name') + ->assertSet('editAssignedEmail', '') + ->set('editName', 'New Name') + ->set('editAssignedEmail', 'team@example.com') + ->call('updateSubLicense'); + + $this->assertDatabaseHas('sub_licenses', [ + 'id' => $subLicense->id, + 'name' => 'New Name', + 'assigned_email' => 'team@example.com', + ]); + + Queue::assertPushed(UpdateAnystackContactAssociationJob::class); + } + + public function test_livewire_edit_sub_license_validates_email(): void + { + $user = User::factory()->create(); + $license = License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'pro', + ]); + $subLicense = SubLicense::factory()->create([ + 'parent_license_id' => $license->id, + ]); + + $this->actingAs($user); + + Livewire::test(SubLicenseManager::class, ['license' => $license]) + ->call('editSubLicense', $subLicense->id) + ->set('editAssignedEmail', 'not-an-email') + ->call('updateSubLicense') + ->assertHasErrors(['editAssignedEmail']); + } + + public function test_livewire_component_displays_sublicenses(): void + { + $user = User::factory()->create(); + $license = License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'pro', + ]); + + $activeSubLicense = SubLicense::factory()->create([ + 'parent_license_id' => $license->id, + 'name' => 'Active Key', + 'is_suspended' => false, + ]); + + $suspendedSubLicense = SubLicense::factory()->create([ + 'parent_license_id' => $license->id, + 'name' => 'Suspended Key', + 'is_suspended' => true, + ]); + + $this->actingAs($user); + + Livewire::test(SubLicenseManager::class, ['license' => $license]) + ->assertSee('Active Key') + ->assertSee('Suspended Key') + ->assertSee($activeSubLicense->key) + ->assertSee($suspendedSubLicense->key); + } +} diff --git a/tests/Feature/DashboardLayoutTest.php b/tests/Feature/DashboardLayoutTest.php new file mode 100644 index 00000000..b0f126b2 --- /dev/null +++ b/tests/Feature/DashboardLayoutTest.php @@ -0,0 +1,212 @@ + self::MAX_PRICE_ID]); + } + + private function createUltraUser(): User + { + $user = User::factory()->create(); + Subscription::factory()->for($user)->active()->create([ + 'stripe_price' => self::MAX_PRICE_ID, + ]); + + return $user; + } + + public function test_user_name_with_apostrophe_is_not_double_escaped_in_dashboard(): void + { + $user = User::factory()->create([ + 'name' => "Timmy D'Hooghe", + ]); + + $response = $this->withoutVite()->actingAs($user)->get('/dashboard'); + + $response->assertStatus(200); + $response->assertDontSee('D&#039;Hooghe', false); + $response->assertSee("Timmy D'Hooghe"); + } + + // ======================================== + // Subscription Card Tests + // ======================================== + + public function test_dashboard_shows_no_active_subscription_when_subscription_is_canceled(): void + { + $user = User::factory()->create(); + Subscription::factory()->for($user)->create([ + 'stripe_price' => self::MAX_PRICE_ID, + 'stripe_status' => 'canceled', + 'ends_at' => now()->subDay(), + ]); + + Livewire::actingAs($user) + ->test(Dashboard::class) + ->assertOk() + ->assertSee('No active subscription') + ->assertDontSee('badge="Active"'); + } + + // ======================================== + // Sidebar Team Item Tests + // ======================================== + + public function test_sidebar_shows_create_team_for_ultra_subscriber_without_team(): void + { + $user = $this->createUltraUser(); + + $response = $this->withoutVite()->actingAs($user)->get('/dashboard'); + + $response->assertOk(); + $response->assertSee('Create Team'); + } + + public function test_sidebar_shows_team_name_for_ultra_subscriber_with_team(): void + { + $user = $this->createUltraUser(); + $team = Team::factory()->create(['user_id' => $user->id, 'name' => 'My Ultra Team']); + + $response = $this->withoutVite()->actingAs($user)->get('/dashboard'); + + $response->assertOk(); + $response->assertSee('My Ultra Team'); + } + + public function test_sidebar_shows_team_name_for_ultra_team_member(): void + { + $owner = $this->createUltraUser(); + $team = Team::factory()->create(['user_id' => $owner->id, 'name' => 'Owner Team']); + + $member = User::factory()->create(); + TeamUser::factory()->active()->create([ + 'team_id' => $team->id, + 'user_id' => $member->id, + 'email' => $member->email, + ]); + + $response = $this->withoutVite()->actingAs($member)->get('/dashboard'); + + $response->assertOk(); + $response->assertSee('Owner Team'); + } + + public function test_sidebar_hides_team_item_for_non_ultra_user(): void + { + $user = User::factory()->create(); + + $response = $this->withoutVite()->actingAs($user)->get('/dashboard'); + + $response->assertOk(); + $response->assertDontSee('Create Team'); + } + + // ======================================== + // Dashboard Team Card Tests + // ======================================== + + public function test_dashboard_shows_team_card_for_ultra_subscriber_with_team(): void + { + $user = $this->createUltraUser(); + $team = Team::factory()->create(['user_id' => $user->id, 'name' => 'My Ultra Team']); + TeamUser::factory()->active()->count(3)->create(['team_id' => $team->id]); + + Livewire::actingAs($user) + ->test(Dashboard::class) + ->assertOk() + ->assertSee('My Ultra Team') + ->assertSee('3 active members') + ->assertSee('Manage members'); + } + + public function test_dashboard_shows_create_team_cta_for_ultra_subscriber_without_team(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Dashboard::class) + ->assertOk() + ->assertSee('No team yet') + ->assertSee('Create a team'); + } + + public function test_dashboard_hides_team_card_for_non_ultra_user(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Dashboard::class) + ->assertOk() + ->assertDontSee('No team yet') + ->assertDontSee('Create a team'); + } + + // ======================================== + // Ultra Upsell Banner Tests + // ======================================== + + public function test_dashboard_shows_ultra_banner_for_non_ultra_user(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Dashboard::class) + ->assertOk() + ->assertSee('Upgrade to NativePHP Ultra') + ->assertSee('Learn more'); + } + + public function test_dashboard_hides_ultra_banner_for_ultra_subscriber(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Dashboard::class) + ->assertOk() + ->assertDontSee('Upgrade to NativePHP Ultra'); + } + + // ======================================== + // Free Plugins Offer Banner Tests + // ======================================== + + public function test_dashboard_hides_free_plugins_banner_for_ultra_subscriber(): void + { + Feature::define(ShowPlugins::class, true); + + $user = $this->createUltraUser(); + License::factory()->withoutSubscriptionItem()->for($user)->create([ + 'created_at' => '2025-12-01', + ]); + + Livewire::actingAs($user) + ->test(Dashboard::class) + ->assertOk() + ->assertDontSee('Claim Your Free Plugins!'); + } +} diff --git a/tests/Feature/DeveloperTermsTest.php b/tests/Feature/DeveloperTermsTest.php new file mode 100644 index 00000000..f7986f2e --- /dev/null +++ b/tests/Feature/DeveloperTermsTest.php @@ -0,0 +1,367 @@ +get('/developer-terms'); + + $response->assertStatus(200); + $response->assertSee('Plugin Developer Terms and Conditions'); + } + + /** @test */ + public function developer_terms_page_contains_key_sections(): void + { + $response = $this->get('/developer-terms'); + + $response->assertSee('Revenue Share and Platform Fee'); + $response->assertSee('thirty percent (30%)'); + $response->assertSee('Developer Responsibilities and Liability'); + $response->assertSee('Listing Criteria and Marketplace Standards'); + $response->assertSee('Plugin Pricing and Discounts'); + } + + /** @test */ + public function onboarding_start_requires_terms_acceptance(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user) + ->post(route('customer.developer.onboarding.start')); + + $response->assertSessionHasErrors('accepted_plugin_terms'); + } + + /** @test */ + public function onboarding_start_rejects_unchecked_terms(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user) + ->post(route('customer.developer.onboarding.start'), [ + 'accepted_plugin_terms' => '0', + ]); + + $response->assertSessionHasErrors('accepted_plugin_terms'); + } + + /** @test */ + public function onboarding_start_records_terms_acceptance(): void + { + $user = User::factory()->create(); + + $mockService = Mockery::mock(StripeConnectService::class); + $mockService->shouldReceive('createConnectAccount') + ->once() + ->with($user, 'US', 'USD') + ->andReturnUsing(fn () => DeveloperAccount::factory()->pending()->create(['user_id' => $user->id])); + $mockService->shouldReceive('createOnboardingLink') + ->once() + ->andReturn('https://connect.stripe.com/setup/test'); + + $this->app->instance(StripeConnectService::class, $mockService); + + $response = $this->actingAs($user) + ->post(route('customer.developer.onboarding.start'), [ + 'accepted_plugin_terms' => '1', + 'country' => 'US', + 'payout_currency' => 'USD', + ]); + + $response->assertRedirect('https://connect.stripe.com/setup/test'); + + $developerAccount = $user->fresh()->developerAccount; + $this->assertNotNull($developerAccount->accepted_plugin_terms_at); + $this->assertEquals( + DeveloperAccount::CURRENT_PLUGIN_TERMS_VERSION, + $developerAccount->plugin_terms_version + ); + } + + /** @test */ + public function onboarding_start_does_not_overwrite_existing_terms_acceptance(): void + { + $user = User::factory()->create(); + $originalTime = now()->subDays(30); + + $developerAccount = DeveloperAccount::factory()->pending()->create([ + 'user_id' => $user->id, + 'accepted_plugin_terms_at' => $originalTime, + 'plugin_terms_version' => DeveloperAccount::CURRENT_PLUGIN_TERMS_VERSION, + ]); + + $mockService = Mockery::mock(StripeConnectService::class); + $mockService->shouldReceive('createOnboardingLink') + ->once() + ->andReturn('https://connect.stripe.com/setup/test'); + + $this->app->instance(StripeConnectService::class, $mockService); + + $this->actingAs($user) + ->post(route('customer.developer.onboarding.start'), [ + 'accepted_plugin_terms' => '1', + 'country' => 'GB', + 'payout_currency' => 'GBP', + ]); + + $developerAccount->refresh(); + $this->assertEquals( + $originalTime->toDateTimeString(), + $developerAccount->accepted_plugin_terms_at->toDateTimeString() + ); + $this->assertEquals('GB', $developerAccount->country); + $this->assertEquals('GBP', $developerAccount->payout_currency); + } + + /** @test */ + public function developer_account_has_accepted_plugin_terms_returns_true_when_accepted(): void + { + $account = DeveloperAccount::factory()->withAcceptedTerms()->create(); + + $this->assertTrue($account->hasAcceptedPluginTerms()); + } + + /** @test */ + public function developer_account_has_accepted_plugin_terms_returns_false_when_not_accepted(): void + { + $account = DeveloperAccount::factory()->create(); + + $this->assertFalse($account->hasAcceptedPluginTerms()); + } + + /** @test */ + public function developer_account_has_accepted_current_terms_returns_false_for_old_version(): void + { + $account = DeveloperAccount::factory()->withAcceptedTerms('0.9')->create(); + + $this->assertTrue($account->hasAcceptedPluginTerms()); + $this->assertFalse($account->hasAcceptedCurrentTerms()); + } + + /** @test */ + public function developer_account_has_accepted_current_terms_returns_true_for_current_version(): void + { + $account = DeveloperAccount::factory()->withAcceptedTerms()->create(); + + $this->assertTrue($account->hasAcceptedCurrentTerms()); + } + + /** @test */ + public function terms_of_service_mentions_third_party_plugins(): void + { + $response = $this->get('/terms-of-service'); + + $response->assertStatus(200); + $response->assertSee('Third-Party Plugins'); + } + + /** @test */ + public function privacy_policy_mentions_third_party_plugin_purchases(): void + { + $response = $this->get('/privacy-policy'); + + $response->assertStatus(200); + $response->assertSee('Third-Party Plugin Purchases'); + } + + /** @test */ + public function onboarding_page_renders_for_new_developer(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Onboarding::class) + ->assertStatus(200) + ->assertSee('Become a Plugin Developer') + ->assertSee('Start Selling Plugins'); + } + + /** @test */ + public function onboarding_page_redirects_when_fully_onboarded(): void + { + $user = User::factory()->create(); + DeveloperAccount::factory()->withAcceptedTerms()->create([ + 'user_id' => $user->id, + ]); + + Livewire::actingAs($user) + ->test(Onboarding::class) + ->assertRedirect(route('customer.developer.dashboard')); + } + + /** @test */ + public function plugin_create_page_renders_for_github_connected_user(): void + { + $user = User::factory()->create([ + 'github_id' => '12345', + 'github_username' => 'testdev', + ]); + DeveloperAccount::factory()->withAcceptedTerms()->create([ + 'user_id' => $user->id, + ]); + + Livewire::actingAs($user) + ->test(Create::class) + ->assertStatus(200) + ->assertSee('Submit Your Plugin') + ->assertSee('Select Repository'); + } + + /** @test */ + public function plugin_create_page_shows_github_required_for_non_connected_user(): void + { + $user = User::factory()->create([ + 'github_id' => null, + 'github_username' => null, + ]); + + Livewire::actingAs($user) + ->test(Create::class) + ->assertStatus(200) + ->assertSee('GitHub Connection Required'); + } + + /** @test */ + public function onboarding_start_requires_country(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user) + ->post(route('customer.developer.onboarding.start'), [ + 'accepted_plugin_terms' => '1', + 'payout_currency' => 'USD', + ]); + + $response->assertSessionHasErrors('country'); + } + + /** @test */ + public function onboarding_start_rejects_invalid_country_code(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user) + ->post(route('customer.developer.onboarding.start'), [ + 'accepted_plugin_terms' => '1', + 'country' => 'XX', + 'payout_currency' => 'USD', + ]); + + $response->assertSessionHasErrors('country'); + } + + /** @test */ + public function onboarding_start_requires_payout_currency(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user) + ->post(route('customer.developer.onboarding.start'), [ + 'accepted_plugin_terms' => '1', + 'country' => 'US', + ]); + + $response->assertSessionHasErrors('payout_currency'); + } + + /** @test */ + public function onboarding_start_rejects_invalid_currency_for_country(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user) + ->post(route('customer.developer.onboarding.start'), [ + 'accepted_plugin_terms' => '1', + 'country' => 'US', + 'payout_currency' => 'EUR', + ]); + + $response->assertSessionHasErrors('payout_currency'); + } + + /** @test */ + public function onboarding_start_stores_country_and_currency_on_developer_account(): void + { + $user = User::factory()->create(); + + $mockService = Mockery::mock(StripeConnectService::class); + $mockService->shouldReceive('createConnectAccount') + ->once() + ->with($user, 'FR', 'EUR') + ->andReturnUsing(fn () => DeveloperAccount::factory()->pending()->create([ + 'user_id' => $user->id, + 'country' => 'FR', + 'payout_currency' => 'EUR', + ])); + $mockService->shouldReceive('createOnboardingLink') + ->once() + ->andReturn('https://connect.stripe.com/setup/test'); + + $this->app->instance(StripeConnectService::class, $mockService); + + $this->actingAs($user) + ->post(route('customer.developer.onboarding.start'), [ + 'accepted_plugin_terms' => '1', + 'country' => 'FR', + 'payout_currency' => 'EUR', + ]); + + $developerAccount = $user->fresh()->developerAccount; + $this->assertEquals('FR', $developerAccount->country); + $this->assertEquals('EUR', $developerAccount->payout_currency); + } + + /** @test */ + public function onboarding_page_shows_country_and_currency_fields(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Onboarding::class) + ->assertSee('Your Country') + ->assertSee('Select your country') + ->assertStatus(200); + } + + /** @test */ + public function onboarding_component_updates_currency_when_country_changes(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Onboarding::class) + ->set('country', 'FR') + ->assertSet('payoutCurrency', 'EUR') + ->set('country', 'US') + ->assertSet('payoutCurrency', 'USD') + ->set('country', 'GB') + ->assertSet('payoutCurrency', 'GBP'); + } +} diff --git a/tests/Feature/DocsSearchMetaTagsTest.php b/tests/Feature/DocsSearchMetaTagsTest.php new file mode 100644 index 00000000..41d77f7b --- /dev/null +++ b/tests/Feature/DocsSearchMetaTagsTest.php @@ -0,0 +1,45 @@ +withoutVite() + ->get('/docs/mobile/3/getting-started/introduction') + ->assertStatus(200) + ->assertSee('', false) + ->assertSee('', false); + } + + #[Test] + public function docs_page_includes_docsearch_platform_and_version_meta_tags_for_desktop(): void + { + $this + ->withoutVite() + ->get('/docs/desktop/2/getting-started/introduction') + ->assertStatus(200) + ->assertSee('', false) + ->assertSee('', false); + } + + #[Test] + public function non_docs_page_does_not_include_docsearch_meta_tags(): void + { + $this + ->withoutVite() + ->get('/') + ->assertStatus(200) + ->assertDontSee('docsearch:platform', false) + ->assertDontSee('docsearch:version', false); + } +} diff --git a/tests/Feature/DocumentationTableOfContentsTest.php b/tests/Feature/DocumentationTableOfContentsTest.php new file mode 100644 index 00000000..0192fd12 --- /dev/null +++ b/tests/Feature/DocumentationTableOfContentsTest.php @@ -0,0 +1,84 @@ +#Installation

' + .'

Some text.

' + .'

#Sub Heading

'; + + $controller = new ShowDocumentationController; + + $result = $this->invokeMethod($controller, 'extractTableOfContents', [$html]); + + $this->assertCount(2, $result); + $this->assertEquals(['level' => 2, 'title' => 'Installation', 'anchor' => 'installation'], $result[0]); + $this->assertEquals(['level' => 3, 'title' => 'Sub Heading', 'anchor' => 'sub-heading'], $result[1]); + } + + public function test_anchors_match_heading_ids_for_inline_code(): void + { + $html = '

#Using config()

'; + + $controller = new ShowDocumentationController; + + $result = $this->invokeMethod($controller, 'extractTableOfContents', [$html]); + + $this->assertCount(1, $result); + $this->assertEquals('using-codeconfigcode', $result[0]['anchor']); + $this->assertEquals('Using config()', $result[0]['title']); + } + + public function test_returns_empty_array_when_no_headings(): void + { + $html = '

Just a paragraph.

'; + + $controller = new ShowDocumentationController; + + $result = $this->invokeMethod($controller, 'extractTableOfContents', [$html]); + + $this->assertEmpty($result); + } + + public function test_decodes_html_entities_in_title(): void + { + $html = '

#Installation & Setup

'; + + $controller = new ShowDocumentationController; + + $result = $this->invokeMethod($controller, 'extractTableOfContents', [$html]); + + $this->assertCount(1, $result); + $this->assertEquals('Installation & Setup', $result[0]['title']); + } + + public function test_ignores_h1_and_h4_headings(): void + { + $html = '

#Title

' + .'

#Section

' + .'

#Deep

'; + + $controller = new ShowDocumentationController; + + $result = $this->invokeMethod($controller, 'extractTableOfContents', [$html]); + + $this->assertCount(1, $result); + $this->assertEquals('section', $result[0]['anchor']); + } + + /** + * @param array $args + */ + protected function invokeMethod(object $object, string $method, array $args = []): mixed + { + $reflection = new \ReflectionMethod($object, $method); + + return $reflection->invoke($object, ...$args); + } +} diff --git a/tests/Feature/ExampleTest.php b/tests/Feature/ExampleTest.php deleted file mode 100644 index 8364a84e..00000000 --- a/tests/Feature/ExampleTest.php +++ /dev/null @@ -1,19 +0,0 @@ -get('/'); - - $response->assertStatus(200); - } -} diff --git a/tests/Feature/Filament/BundlePurchasesRelationManagerTest.php b/tests/Feature/Filament/BundlePurchasesRelationManagerTest.php new file mode 100644 index 00000000..cb4eaf6c --- /dev/null +++ b/tests/Feature/Filament/BundlePurchasesRelationManagerTest.php @@ -0,0 +1,240 @@ +admin = User::factory()->create(['email' => 'admin@test.com']); + config(['filament.users' => ['admin@test.com']]); + + $this->bundle = PluginBundle::factory()->active()->create(); + } + + public function test_it_groups_bundle_licenses_into_a_single_sale_row(): void + { + $buyer = User::factory()->create(); + $purchasedAt = now(); + + $plugins = Plugin::factory()->count(3)->approved()->create(); + $this->bundle->plugins()->attach($plugins->pluck('id')); + + foreach ($plugins as $plugin) { + PluginLicense::factory()->create([ + 'user_id' => $buyer->id, + 'plugin_id' => $plugin->id, + 'plugin_bundle_id' => $this->bundle->id, + 'price_paid' => 1000, + 'purchased_at' => $purchasedAt, + ]); + } + + Livewire::actingAs($this->admin) + ->test(LicensesRelationManager::class, [ + 'ownerRecord' => $this->bundle, + 'pageClass' => ViewPluginBundle::class, + ]) + ->assertCanSeeTableRecords( + PluginLicense::where('plugin_bundle_id', $this->bundle->id) + ->whereIn('id', function ($sub) { + $sub->selectRaw('MIN(id)') + ->from('plugin_licenses as pl') + ->where('pl.plugin_bundle_id', $this->bundle->id) + ->groupBy('pl.user_id', 'pl.purchased_at'); + }) + ->get() + ) + ->assertCountTableRecords(1); + } + + public function test_it_computes_correct_totals_per_sale(): void + { + $buyer = User::factory()->create(); + $purchasedAt = now(); + + $plugins = Plugin::factory()->count(3)->approved()->create(); + $this->bundle->plugins()->attach($plugins->pluck('id')); + + foreach ($plugins as $plugin) { + PluginLicense::factory()->create([ + 'user_id' => $buyer->id, + 'plugin_id' => $plugin->id, + 'plugin_bundle_id' => $this->bundle->id, + 'price_paid' => 1000, + 'purchased_at' => $purchasedAt, + ]); + } + + $bundleId = $this->bundle->id; + + $row = PluginLicense::query() + ->select('plugin_licenses.*') + ->addSelect([ + 'sale_total' => PluginLicense::query() + ->from('plugin_licenses as sale_pl') + ->selectRaw('SUM(sale_pl.price_paid)') + ->whereColumn('sale_pl.user_id', 'plugin_licenses.user_id') + ->whereColumn('sale_pl.purchased_at', 'plugin_licenses.purchased_at') + ->where('sale_pl.plugin_bundle_id', $bundleId), + 'sale_plugins_count' => PluginLicense::query() + ->from('plugin_licenses as count_pl') + ->selectRaw('COUNT(*)') + ->whereColumn('count_pl.user_id', 'plugin_licenses.user_id') + ->whereColumn('count_pl.purchased_at', 'plugin_licenses.purchased_at') + ->where('count_pl.plugin_bundle_id', $bundleId), + ]) + ->where('plugin_bundle_id', $bundleId) + ->whereIn('plugin_licenses.id', function ($sub) use ($bundleId): void { + $sub->selectRaw('MIN(id)') + ->from('plugin_licenses as pl') + ->where('pl.plugin_bundle_id', $bundleId) + ->groupBy('pl.user_id', 'pl.purchased_at'); + }) + ->sole(); + + $this->assertEquals(3, $row->sale_plugins_count); + $this->assertEquals(3000, $row->sale_total); + } + + public function test_it_shows_separate_rows_for_different_sales(): void + { + $buyer1 = User::factory()->create(); + $buyer2 = User::factory()->create(); + $purchasedAt = now(); + + $plugins = Plugin::factory()->count(2)->approved()->create(); + $this->bundle->plugins()->attach($plugins->pluck('id')); + + foreach ($plugins as $plugin) { + PluginLicense::factory()->create([ + 'user_id' => $buyer1->id, + 'plugin_id' => $plugin->id, + 'plugin_bundle_id' => $this->bundle->id, + 'price_paid' => 1500, + 'purchased_at' => $purchasedAt, + ]); + } + + foreach ($plugins as $plugin) { + PluginLicense::factory()->create([ + 'user_id' => $buyer2->id, + 'plugin_id' => $plugin->id, + 'plugin_bundle_id' => $this->bundle->id, + 'price_paid' => 1500, + 'purchased_at' => $purchasedAt, + ]); + } + + Livewire::actingAs($this->admin) + ->test(LicensesRelationManager::class, [ + 'ownerRecord' => $this->bundle, + 'pageClass' => ViewPluginBundle::class, + ]) + ->assertCountTableRecords(2); + } + + public function test_it_computes_correct_totals_for_each_buyer(): void + { + $buyer1 = User::factory()->create(); + $buyer2 = User::factory()->create(); + $purchasedAt = now(); + + $plugins = Plugin::factory()->count(2)->approved()->create(); + $this->bundle->plugins()->attach($plugins->pluck('id')); + + foreach ($plugins as $plugin) { + PluginLicense::factory()->create([ + 'user_id' => $buyer1->id, + 'plugin_id' => $plugin->id, + 'plugin_bundle_id' => $this->bundle->id, + 'price_paid' => 1500, + 'purchased_at' => $purchasedAt, + ]); + } + + foreach ($plugins as $plugin) { + PluginLicense::factory()->create([ + 'user_id' => $buyer2->id, + 'plugin_id' => $plugin->id, + 'plugin_bundle_id' => $this->bundle->id, + 'price_paid' => 2000, + 'purchased_at' => $purchasedAt, + ]); + } + + $bundleId = $this->bundle->id; + + $rows = PluginLicense::query() + ->select('plugin_licenses.*') + ->addSelect([ + 'sale_total' => PluginLicense::query() + ->from('plugin_licenses as sale_pl') + ->selectRaw('SUM(sale_pl.price_paid)') + ->whereColumn('sale_pl.user_id', 'plugin_licenses.user_id') + ->whereColumn('sale_pl.purchased_at', 'plugin_licenses.purchased_at') + ->where('sale_pl.plugin_bundle_id', $bundleId), + 'sale_plugins_count' => PluginLicense::query() + ->from('plugin_licenses as count_pl') + ->selectRaw('COUNT(*)') + ->whereColumn('count_pl.user_id', 'plugin_licenses.user_id') + ->whereColumn('count_pl.purchased_at', 'plugin_licenses.purchased_at') + ->where('count_pl.plugin_bundle_id', $bundleId), + ]) + ->where('plugin_bundle_id', $bundleId) + ->whereIn('plugin_licenses.id', function ($sub) use ($bundleId): void { + $sub->selectRaw('MIN(id)') + ->from('plugin_licenses as pl') + ->where('pl.plugin_bundle_id', $bundleId) + ->groupBy('pl.user_id', 'pl.purchased_at'); + }) + ->get() + ->keyBy('user_id'); + + $this->assertEquals(2, $rows[$buyer1->id]->sale_plugins_count); + $this->assertEquals(3000, $rows[$buyer1->id]->sale_total); + + $this->assertEquals(2, $rows[$buyer2->id]->sale_plugins_count); + $this->assertEquals(4000, $rows[$buyer2->id]->sale_total); + } + + public function test_it_shows_grandfathered_status(): void + { + $buyer = User::factory()->create(); + $plugin = Plugin::factory()->approved()->create(); + $this->bundle->plugins()->attach($plugin); + + PluginLicense::factory()->grandfathered()->create([ + 'user_id' => $buyer->id, + 'plugin_id' => $plugin->id, + 'plugin_bundle_id' => $this->bundle->id, + 'price_paid' => 0, + 'purchased_at' => now(), + ]); + + Livewire::actingAs($this->admin) + ->test(LicensesRelationManager::class, [ + 'ownerRecord' => $this->bundle, + 'pageClass' => ViewPluginBundle::class, + ]) + ->assertCountTableRecords(1); + } +} diff --git a/tests/Feature/Filament/ConvertPluginToPaidTest.php b/tests/Feature/Filament/ConvertPluginToPaidTest.php new file mode 100644 index 00000000..bdc338d6 --- /dev/null +++ b/tests/Feature/Filament/ConvertPluginToPaidTest.php @@ -0,0 +1,71 @@ +admin = User::factory()->create(['email' => 'admin@test.com']); + config(['filament.users' => ['admin@test.com']]); + } + + public function test_convert_to_paid_changes_type_and_dispatches_satis(): void + { + Bus::fake([SyncPluginReleases::class]); + + $plugin = Plugin::factory()->free()->approved()->create(); + + Livewire::actingAs($this->admin) + ->test(EditPlugin::class, ['record' => $plugin->getRouteKey()]) + ->callAction('convertToPaid', data: [ + 'tier' => PluginTier::Silver->value, + ]) + ->assertNotified(); + + $plugin->refresh(); + + $this->assertEquals(PluginType::Paid, $plugin->type); + $this->assertEquals(PluginTier::Silver, $plugin->tier); + $this->assertTrue($plugin->prices()->exists()); + + Bus::assertDispatched(SyncPluginReleases::class, function ($job) use ($plugin) { + return $job->plugin->is($plugin); + }); + } + + public function test_convert_to_paid_is_not_visible_on_paid_plugins(): void + { + $plugin = Plugin::factory()->paid()->approved()->create(); + + Livewire::actingAs($this->admin) + ->test(EditPlugin::class, ['record' => $plugin->getRouteKey()]) + ->assertActionHidden('convertToPaid'); + } + + public function test_convert_to_paid_is_visible_on_free_plugins(): void + { + $plugin = Plugin::factory()->free()->approved()->create(); + + Livewire::actingAs($this->admin) + ->test(EditPlugin::class, ['record' => $plugin->getRouteKey()]) + ->assertActionVisible('convertToPaid'); + } +} diff --git a/tests/Feature/Filament/GrantBundleToUserActionTest.php b/tests/Feature/Filament/GrantBundleToUserActionTest.php new file mode 100644 index 00000000..43d2237c --- /dev/null +++ b/tests/Feature/Filament/GrantBundleToUserActionTest.php @@ -0,0 +1,54 @@ +admin = User::factory()->create(['email' => 'admin@test.com']); + config(['filament.users' => ['admin@test.com']]); + + $this->bundle = PluginBundle::factory()->active()->create(); + } + + public function test_grant_to_user_action_can_be_called_with_user_id(): void + { + $recipient = User::factory()->create(); + $plugins = Plugin::factory()->count(2)->approved()->create(); + $this->bundle->plugins()->attach($plugins->pluck('id')); + + Livewire::actingAs($this->admin) + ->test(ListPluginBundles::class) + ->callAction( + TestAction::make('grantToUser')->table($this->bundle), + data: ['user_id' => $recipient->id], + ) + ->assertHasNoFormErrors(); + + foreach ($plugins as $plugin) { + $this->assertDatabaseHas('plugin_licenses', [ + 'user_id' => $recipient->id, + 'plugin_id' => $plugin->id, + 'plugin_bundle_id' => $this->bundle->id, + ]); + } + } +} diff --git a/tests/Feature/Filament/PluginSalesResourceTest.php b/tests/Feature/Filament/PluginSalesResourceTest.php new file mode 100644 index 00000000..b2edde89 --- /dev/null +++ b/tests/Feature/Filament/PluginSalesResourceTest.php @@ -0,0 +1,89 @@ +admin = User::factory()->create(['email' => 'admin@test.com']); + config(['filament.users' => ['admin@test.com']]); + } + + public function test_sales_page_renders_successfully(): void + { + PluginLicense::factory()->count(3)->create(); + + Livewire::actingAs($this->admin) + ->test(ListSales::class) + ->assertSuccessful(); + } + + public function test_sales_page_renders_when_plugin_has_null_name(): void + { + $pluginWithName = Plugin::factory()->approved()->create(['name' => 'acme/camera-123']); + $pluginWithoutName = Plugin::factory()->approved()->create(['name' => null]); + + PluginLicense::factory()->create(['plugin_id' => $pluginWithName->id]); + PluginLicense::factory()->create(['plugin_id' => $pluginWithoutName->id]); + + Livewire::actingAs($this->admin) + ->test(ListSales::class) + ->assertSuccessful(); + } + + public function test_sales_page_shows_product_license_sales(): void + { + ProductLicense::factory()->count(2)->create(); + + Livewire::actingAs($this->admin) + ->test(ListSales::class) + ->assertSuccessful(); + } + + public function test_sales_page_shows_both_plugin_and_product_sales(): void + { + PluginLicense::factory()->count(2)->create(); + ProductLicense::factory()->count(2)->create(); + + Livewire::actingAs($this->admin) + ->test(ListSales::class) + ->assertSuccessful(); + } + + public function test_sales_page_shows_bundle_name_for_plugin_sales(): void + { + $bundle = PluginBundle::factory()->create(['name' => 'Pro Bundle']); + PluginLicense::factory()->create(['plugin_bundle_id' => $bundle->id]); + + Livewire::actingAs($this->admin) + ->test(ListSales::class) + ->assertSuccessful() + ->assertSee('Pro Bundle'); + } + + public function test_sales_page_shows_comped_column(): void + { + PluginLicense::factory()->grandfathered()->create(); + + Livewire::actingAs($this->admin) + ->test(ListSales::class) + ->assertSuccessful(); + } +} diff --git a/tests/Feature/Filament/ProductLicensesRelationManagerTest.php b/tests/Feature/Filament/ProductLicensesRelationManagerTest.php new file mode 100644 index 00000000..8937f39d --- /dev/null +++ b/tests/Feature/Filament/ProductLicensesRelationManagerTest.php @@ -0,0 +1,147 @@ +admin = User::factory()->create(['email' => 'admin@test.com']); + config(['filament.users' => ['admin@test.com']]); + + $this->user = User::factory()->create(); + } + + public function test_it_lists_product_licenses_for_user(): void + { + $licenses = ProductLicense::factory()->count(3)->create([ + 'user_id' => $this->user->id, + ]); + + Livewire::actingAs($this->admin) + ->test(ProductLicensesRelationManager::class, [ + 'ownerRecord' => $this->user, + 'pageClass' => EditUser::class, + ]) + ->assertCanSeeTableRecords($licenses) + ->assertCountTableRecords(3); + } + + public function test_it_does_not_show_other_users_licenses(): void + { + $otherUser = User::factory()->create(); + + ProductLicense::factory()->create([ + 'user_id' => $this->user->id, + ]); + + ProductLicense::factory()->create([ + 'user_id' => $otherUser->id, + ]); + + Livewire::actingAs($this->admin) + ->test(ProductLicensesRelationManager::class, [ + 'ownerRecord' => $this->user, + 'pageClass' => EditUser::class, + ]) + ->assertCountTableRecords(1); + } + + public function test_it_shows_comped_status(): void + { + ProductLicense::factory()->create([ + 'user_id' => $this->user->id, + 'is_comped' => true, + 'price_paid' => 0, + ]); + + Livewire::actingAs($this->admin) + ->test(ProductLicensesRelationManager::class, [ + 'ownerRecord' => $this->user, + 'pageClass' => EditUser::class, + ]) + ->assertCountTableRecords(1); + } + + public function test_it_can_create_a_comped_product_license(): void + { + $product = Product::factory()->create(); + + Livewire::actingAs($this->admin) + ->test(ProductLicensesRelationManager::class, [ + 'ownerRecord' => $this->user, + 'pageClass' => EditUser::class, + ]) + ->callTableAction('create', data: [ + 'product_id' => $product->id, + 'is_comped' => true, + 'purchased_at' => now()->toDateTimeString(), + ]) + ->assertHasNoTableActionErrors(); + + $this->assertDatabaseHas('product_licenses', [ + 'user_id' => $this->user->id, + 'product_id' => $product->id, + 'is_comped' => true, + 'price_paid' => 0, + 'currency' => 'USD', + ]); + } + + public function test_it_can_delete_a_product_license(): void + { + $license = ProductLicense::factory()->create([ + 'user_id' => $this->user->id, + ]); + + Livewire::actingAs($this->admin) + ->test(ProductLicensesRelationManager::class, [ + 'ownerRecord' => $this->user, + 'pageClass' => EditUser::class, + ]) + ->callTableAction('delete', $license) + ->assertHasNoTableActionErrors(); + + $this->assertDatabaseMissing('product_licenses', [ + 'id' => $license->id, + ]); + } + + public function test_it_can_filter_by_comped_status(): void + { + ProductLicense::factory()->create([ + 'user_id' => $this->user->id, + 'is_comped' => true, + ]); + + ProductLicense::factory()->create([ + 'user_id' => $this->user->id, + 'is_comped' => false, + ]); + + Livewire::actingAs($this->admin) + ->test(ProductLicensesRelationManager::class, [ + 'ownerRecord' => $this->user, + 'pageClass' => EditUser::class, + ]) + ->filterTable('is_comped', true) + ->assertCountTableRecords(1); + } +} diff --git a/tests/Feature/Filament/ResyncPluginActionTest.php b/tests/Feature/Filament/ResyncPluginActionTest.php new file mode 100644 index 00000000..76dba0d7 --- /dev/null +++ b/tests/Feature/Filament/ResyncPluginActionTest.php @@ -0,0 +1,90 @@ +admin = User::factory()->create(['email' => 'admin@test.com']); + config(['filament.users' => ['admin@test.com']]); + } + + public function test_resync_action_dispatches_job(): void + { + Bus::fake([SyncPlugin::class]); + + $plugin = Plugin::factory()->free()->approved()->create([ + 'repository_url' => 'https://github.com/acme/test-plugin', + ]); + + Livewire::actingAs($this->admin) + ->test(EditPlugin::class, ['record' => $plugin->getRouteKey()]) + ->callAction('resync') + ->assertNotified(); + + Bus::assertDispatched(SyncPlugin::class, function ($job) use ($plugin) { + return $job->plugin->is($plugin); + }); + } + + public function test_resync_action_visible_when_repository_url_exists(): void + { + $plugin = Plugin::factory()->free()->approved()->create([ + 'repository_url' => 'https://github.com/acme/test-plugin', + ]); + + Livewire::actingAs($this->admin) + ->test(EditPlugin::class, ['record' => $plugin->getRouteKey()]) + ->assertActionVisible('resync'); + } + + public function test_resync_action_hidden_when_no_repository_url(): void + { + $plugin = Plugin::factory()->free()->approved()->create([ + 'repository_url' => null, + ]); + + Livewire::actingAs($this->admin) + ->test(EditPlugin::class, ['record' => $plugin->getRouteKey()]) + ->assertActionHidden('resync'); + } + + public function test_view_github_action_uses_repository_url(): void + { + $plugin = Plugin::factory()->free()->approved()->create([ + 'repository_url' => 'https://github.com/acme/test-plugin', + ]); + + Livewire::actingAs($this->admin) + ->test(EditPlugin::class, ['record' => $plugin->getRouteKey()]) + ->assertActionVisible('viewGithub') + ->assertActionHasUrl('viewGithub', 'https://github.com/acme/test-plugin'); + } + + public function test_view_github_action_hidden_when_no_repository_url(): void + { + $plugin = Plugin::factory()->free()->approved()->create([ + 'repository_url' => null, + ]); + + Livewire::actingAs($this->admin) + ->test(EditPlugin::class, ['record' => $plugin->getRouteKey()]) + ->assertActionHidden('viewGithub'); + } +} diff --git a/tests/Feature/Filament/SubscriberIncomeChartTest.php b/tests/Feature/Filament/SubscriberIncomeChartTest.php new file mode 100644 index 00000000..c0518ef8 --- /dev/null +++ b/tests/Feature/Filament/SubscriberIncomeChartTest.php @@ -0,0 +1,145 @@ +admin = User::factory()->create(['email' => 'admin@test.com']); + config(['filament.users' => ['admin@test.com']]); + } + + public function test_chart_renders_with_default_filter(): void + { + Subscription::factory()->for($this->admin)->active()->create([ + 'price_paid' => 9900, + ]); + + Livewire::actingAs($this->admin) + ->test(SubscriberIncomeChart::class) + ->assertSuccessful(); + } + + public function test_chart_renders_with_this_month_filter(): void + { + Subscription::factory()->for($this->admin)->active()->create([ + 'price_paid' => 9900, + 'created_at' => now()->startOfMonth()->addDays(3), + ]); + + Livewire::actingAs($this->admin) + ->test(SubscriberIncomeChart::class) + ->set('filter', 'this_month') + ->assertSuccessful(); + } + + public function test_chart_renders_with_last_month_filter(): void + { + Subscription::factory()->for($this->admin)->active()->create([ + 'price_paid' => 4900, + 'created_at' => now()->subMonth()->startOfMonth()->addDays(5), + ]); + + Livewire::actingAs($this->admin) + ->test(SubscriberIncomeChart::class) + ->set('filter', 'last_month') + ->assertSuccessful(); + } + + public function test_chart_renders_with_this_year_filter(): void + { + Subscription::factory()->for($this->admin)->active()->create([ + 'price_paid' => 9900, + 'created_at' => now()->startOfYear()->addMonths(2), + ]); + + Livewire::actingAs($this->admin) + ->test(SubscriberIncomeChart::class) + ->set('filter', 'this_year') + ->assertSuccessful(); + } + + public function test_chart_renders_with_last_year_filter(): void + { + Subscription::factory()->for($this->admin)->active()->create([ + 'price_paid' => 9900, + 'created_at' => now()->subYear()->startOfYear()->addMonths(3), + ]); + + Livewire::actingAs($this->admin) + ->test(SubscriberIncomeChart::class) + ->set('filter', 'last_year') + ->assertSuccessful(); + } + + public function test_chart_renders_with_all_time_filter(): void + { + Subscription::factory()->for($this->admin)->active()->create([ + 'price_paid' => 9900, + 'created_at' => now()->subYears(2), + ]); + Subscription::factory()->for($this->admin)->active()->create([ + 'price_paid' => 4900, + ]); + + Livewire::actingAs($this->admin) + ->test(SubscriberIncomeChart::class) + ->set('filter', 'all_time') + ->assertSuccessful(); + } + + public function test_chart_renders_with_no_subscriptions(): void + { + Livewire::actingAs($this->admin) + ->test(SubscriberIncomeChart::class) + ->set('filter', 'this_month') + ->assertSuccessful(); + } + + public function test_all_time_filter_works_with_no_paid_subscriptions(): void + { + Livewire::actingAs($this->admin) + ->test(SubscriberIncomeChart::class) + ->set('filter', 'all_time') + ->assertSuccessful(); + } + + public function test_comped_subscriptions_with_zero_price_are_excluded(): void + { + Subscription::factory()->for($this->admin)->active()->create([ + 'price_paid' => 0, + 'is_comped' => true, + ]); + + Livewire::actingAs($this->admin) + ->test(SubscriberIncomeChart::class) + ->set('filter', 'this_year') + ->assertSuccessful(); + } + + public function test_subscriptions_with_null_price_are_excluded(): void + { + Subscription::factory()->for($this->admin)->active()->create([ + 'price_paid' => null, + ]); + + Livewire::actingAs($this->admin) + ->test(SubscriberIncomeChart::class) + ->set('filter', 'this_year') + ->assertSuccessful(); + } +} diff --git a/tests/Feature/Filament/UsersChartTest.php b/tests/Feature/Filament/UsersChartTest.php new file mode 100644 index 00000000..d51269ae --- /dev/null +++ b/tests/Feature/Filament/UsersChartTest.php @@ -0,0 +1,100 @@ +admin = User::factory()->create(['email' => 'admin@test.com']); + config(['filament.users' => ['admin@test.com']]); + } + + public function test_chart_renders_with_default_filter(): void + { + User::factory()->count(3)->create(); + + Livewire::actingAs($this->admin) + ->test(UsersChart::class) + ->assertSuccessful(); + } + + public function test_chart_renders_with_this_month_filter(): void + { + User::factory()->count(2)->create(['created_at' => now()->startOfMonth()->addDays(3)]); + + Livewire::actingAs($this->admin) + ->test(UsersChart::class) + ->set('filter', 'this_month') + ->assertSuccessful(); + } + + public function test_chart_renders_with_last_month_filter(): void + { + User::factory()->count(2)->create(['created_at' => now()->subMonth()->startOfMonth()->addDays(5)]); + + Livewire::actingAs($this->admin) + ->test(UsersChart::class) + ->set('filter', 'last_month') + ->assertSuccessful(); + } + + public function test_chart_renders_with_this_year_filter(): void + { + User::factory()->count(2)->create(['created_at' => now()->startOfYear()->addMonths(2)]); + + Livewire::actingAs($this->admin) + ->test(UsersChart::class) + ->set('filter', 'this_year') + ->assertSuccessful(); + } + + public function test_chart_renders_with_last_year_filter(): void + { + User::factory()->count(2)->create(['created_at' => now()->subYear()->startOfYear()->addMonths(3)]); + + Livewire::actingAs($this->admin) + ->test(UsersChart::class) + ->set('filter', 'last_year') + ->assertSuccessful(); + } + + public function test_chart_renders_with_all_time_filter(): void + { + User::factory()->create(['created_at' => now()->subYears(2)]); + User::factory()->count(3)->create(); + + Livewire::actingAs($this->admin) + ->test(UsersChart::class) + ->set('filter', 'all_time') + ->assertSuccessful(); + } + + public function test_chart_renders_with_no_users(): void + { + Livewire::actingAs($this->admin) + ->test(UsersChart::class) + ->set('filter', 'this_month') + ->assertSuccessful(); + } + + public function test_all_time_filter_works_with_no_historical_users(): void + { + Livewire::actingAs($this->admin) + ->test(UsersChart::class) + ->set('filter', 'all_time') + ->assertSuccessful(); + } +} diff --git a/tests/Feature/Filament/ViewSubscriptionPageTest.php b/tests/Feature/Filament/ViewSubscriptionPageTest.php new file mode 100644 index 00000000..fcd4f662 --- /dev/null +++ b/tests/Feature/Filament/ViewSubscriptionPageTest.php @@ -0,0 +1,48 @@ +admin = User::factory()->create(['email' => 'admin@test.com']); + config(['filament.users' => ['admin@test.com']]); + } + + public function test_view_subscription_page_renders_successfully(): void + { + $subscription = Subscription::factory()->create(); + + Livewire::actingAs($this->admin) + ->test(ViewSubscription::class, ['record' => $subscription->id]) + ->assertSuccessful(); + } + + public function test_view_subscription_item_page_renders_successfully(): void + { + $subscription = Subscription::factory()->create(); + $item = SubscriptionItem::factory()->create([ + 'subscription_id' => $subscription->id, + ]); + + Livewire::actingAs($this->admin) + ->test(ViewSubscriptionItem::class, ['record' => $item->id]) + ->assertSuccessful(); + } +} diff --git a/tests/Feature/Filament/WallOfLoveSubmissionResourceTest.php b/tests/Feature/Filament/WallOfLoveSubmissionResourceTest.php new file mode 100644 index 00000000..895675ae --- /dev/null +++ b/tests/Feature/Filament/WallOfLoveSubmissionResourceTest.php @@ -0,0 +1,62 @@ +admin = User::factory()->create(['email' => 'admin@test.com']); + config(['filament.users' => ['admin@test.com']]); + } + + public function test_list_page_renders_successfully(): void + { + WallOfLoveSubmission::factory()->count(3)->create(); + + Livewire::actingAs($this->admin) + ->test(ListWallOfLoveSubmissions::class) + ->assertSuccessful(); + } + + public function test_edit_page_renders_for_approved_submission(): void + { + $submission = WallOfLoveSubmission::factory()->approved()->create(); + + Livewire::actingAs($this->admin) + ->test(EditWallOfLoveSubmission::class, ['record' => $submission->getRouteKey()]) + ->assertSuccessful(); + } + + public function test_edit_page_renders_for_pending_submission(): void + { + $submission = WallOfLoveSubmission::factory()->pending()->create(); + + Livewire::actingAs($this->admin) + ->test(EditWallOfLoveSubmission::class, ['record' => $submission->getRouteKey()]) + ->assertSuccessful(); + } + + public function test_edit_page_renders_for_promoted_submission(): void + { + $submission = WallOfLoveSubmission::factory()->approved()->promoted()->create(); + + Livewire::actingAs($this->admin) + ->test(EditWallOfLoveSubmission::class, ['record' => $submission->getRouteKey()]) + ->assertSuccessful(); + } +} diff --git a/tests/Feature/GitHubIntegrationTest.php b/tests/Feature/GitHubIntegrationTest.php new file mode 100644 index 00000000..e3a89ef2 --- /dev/null +++ b/tests/Feature/GitHubIntegrationTest.php @@ -0,0 +1,293 @@ + Http::response([], 404)]); + + $user = User::factory()->create(); + License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'max', + 'expires_at' => now()->addDays(30), + 'is_suspended' => false, + ]); + + $response = $this->actingAs($user)->get('/dashboard/integrations'); + + $response->assertStatus(200); + $response->assertSee('nativephp/mobile'); + $response->assertSee('Repo Access'); + $response->assertSee('Connect GitHub'); + } + + public function test_user_without_max_license_does_not_see_github_integration_card(): void + { + Http::fake(['api.github.com/*' => Http::response([], 404)]); + + $user = User::factory()->create(); + License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'pro', + 'expires_at' => now()->addDays(30), + 'is_suspended' => false, + ]); + + $response = $this->actingAs($user)->get('/dashboard/integrations'); + + $response->assertStatus(200); + $response->assertDontSee('Repo Access'); + } + + public function test_user_with_connected_github_sees_username(): void + { + Http::fake(['api.github.com/*' => Http::response([], 404)]); + + $user = User::factory()->create([ + 'github_username' => 'testuser', + 'github_id' => '123456', + ]); + License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'max', + 'expires_at' => now()->addDays(30), + 'is_suspended' => false, + ]); + + $response = $this->actingAs($user)->get('/dashboard/integrations'); + + $response->assertStatus(200); + $response->assertSee('Connected as'); + $response->assertSee('@testuser'); + $response->assertSee('Request Access'); + } + + public function test_user_can_request_repo_access_with_active_max_license(): void + { + Http::fake([ + 'api.github.com/repos/nativephp/mobile/collaborators/testuser' => Http::response([], 201), + ]); + + $user = User::factory()->create([ + 'github_username' => 'testuser', + 'github_id' => '123456', + ]); + License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'max', + 'expires_at' => now()->addDays(30), + 'is_suspended' => false, + ]); + + $response = $this->actingAs($user) + ->post('/dashboard/github/request-access'); + + $response->assertRedirect(); + $response->assertSessionHas('success'); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + ]); + + $user->refresh(); + $this->assertNotNull($user->mobile_repo_access_granted_at); + } + + public function test_user_cannot_request_repo_access_without_max_license(): void + { + $user = User::factory()->create([ + 'github_username' => 'testuser', + 'github_id' => '123456', + ]); + License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'pro', + 'expires_at' => now()->addDays(30), + 'is_suspended' => false, + ]); + + $response = $this->actingAs($user) + ->post('/dashboard/github/request-access'); + + $response->assertRedirect(); + $response->assertSessionHas('error'); + } + + public function test_user_cannot_request_repo_access_without_connected_github(): void + { + $user = User::factory()->create(); + License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'max', + 'expires_at' => now()->addDays(30), + 'is_suspended' => false, + ]); + + $response = $this->actingAs($user) + ->post('/dashboard/github/request-access'); + + $response->assertRedirect(); + $response->assertSessionHas('error', 'Please connect your GitHub account first.'); + } + + public function test_user_can_disconnect_github_account(): void + { + Http::fake([ + 'api.github.com/repos/nativephp/mobile/collaborators/testuser' => Http::response([], 204), + ]); + + $user = User::factory()->create([ + 'github_username' => 'testuser', + 'github_id' => '123456', + 'mobile_repo_access_granted_at' => now(), + ]); + License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'max', + 'expires_at' => now()->addDays(30), + 'is_suspended' => false, + ]); + + $response = $this->actingAs($user) + ->delete('/dashboard/github/disconnect'); + + $response->assertRedirect(); + $response->assertSessionHas('success'); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'github_username' => null, + 'github_id' => null, + 'mobile_repo_access_granted_at' => null, + ]); + } + + public function test_scheduled_command_removes_access_for_expired_max_license(): void + { + Http::fake([ + 'api.github.com/repos/nativephp/mobile/collaborators/testuser' => Http::response([], 204), + ]); + + $user = User::factory()->create([ + 'github_username' => 'testuser', + 'github_id' => '123456', + 'mobile_repo_access_granted_at' => now()->subDays(10), + ]); + License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'max', + 'expires_at' => now()->subDays(1), // Expired + 'is_suspended' => false, + ]); + + $this->artisan('github:remove-expired-access') + ->assertExitCode(0); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'mobile_repo_access_granted_at' => null, + ]); + } + + public function test_scheduled_command_does_not_remove_access_for_active_max_license(): void + { + $user = User::factory()->create([ + 'github_username' => 'testuser', + 'github_id' => '123456', + 'mobile_repo_access_granted_at' => now()->subDays(10), + ]); + License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'max', + 'expires_at' => now()->addDays(30), // Active + 'is_suspended' => false, + ]); + + $this->artisan('github:remove-expired-access') + ->assertExitCode(0); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'github_username' => 'testuser', + 'mobile_repo_access_granted_at' => $user->mobile_repo_access_granted_at, + ]); + } + + public function test_user_with_active_collaborator_status_sees_access_granted(): void + { + Http::fake([ + 'api.github.com/repos/nativephp/mobile/collaborators/testuser' => Http::response([], 204), + ]); + + $user = User::factory()->create([ + 'github_username' => 'testuser', + 'github_id' => '123456', + ]); + License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'max', + 'expires_at' => now()->addDays(30), + 'is_suspended' => false, + ]); + + $response = $this->actingAs($user)->get('/dashboard/integrations'); + + $response->assertStatus(200); + $response->assertSee('Access Granted'); + $response->assertSee('@testuser'); + $response->assertSee('View Repo'); + $response->assertDontSee('Request Access'); + } + + public function test_user_with_pending_invitation_sees_pending_status(): void + { + Http::fake([ + 'api.github.com/repos/nativephp/mobile/collaborators/testuser' => Http::response([], 404), + 'api.github.com/repos/nativephp/mobile/invitations' => Http::response([ + ['invitee' => ['login' => 'testuser']], + ], 200), + ]); + + $user = User::factory()->create([ + 'github_username' => 'testuser', + 'github_id' => '123456', + ]); + License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'max', + 'expires_at' => now()->addDays(30), + 'is_suspended' => false, + ]); + + $response = $this->actingAs($user)->get('/dashboard/integrations'); + + $response->assertStatus(200); + $response->assertSee('Invitation Pending'); + $response->assertSee('@testuser'); + $response->assertSee('Check Status'); + } +} diff --git a/tests/Feature/GrantPluginToBundleOwnersTest.php b/tests/Feature/GrantPluginToBundleOwnersTest.php new file mode 100644 index 00000000..4675a19c --- /dev/null +++ b/tests/Feature/GrantPluginToBundleOwnersTest.php @@ -0,0 +1,239 @@ +active()->create(); + $existingPlugin = Plugin::factory()->paid()->create(); + $newPlugin = Plugin::factory()->paid()->create(); + + $bundle->plugins()->attach($existingPlugin); + + // Create 3 users who purchased the bundle + $users = User::factory()->count(3)->create(); + + foreach ($users as $user) { + PluginLicense::factory()->create([ + 'user_id' => $user->id, + 'plugin_id' => $existingPlugin->id, + 'plugin_bundle_id' => $bundle->id, + ]); + } + + $this->artisan('plugins:grant-to-bundle-owners', [ + 'bundle' => $bundle->slug, + 'plugin' => $newPlugin->name, + ])->assertSuccessful(); + + foreach ($users as $user) { + $this->assertDatabaseHas('plugin_licenses', [ + 'user_id' => $user->id, + 'plugin_id' => $newPlugin->id, + 'plugin_bundle_id' => $bundle->id, + 'price_paid' => 0, + 'is_grandfathered' => true, + ]); + } + + Notification::assertSentTo($users, BundlePluginAdded::class); + } + + public function test_skips_users_who_already_have_the_plugin(): void + { + Notification::fake(); + + $bundle = PluginBundle::factory()->active()->create(); + $existingPlugin = Plugin::factory()->paid()->create(); + $newPlugin = Plugin::factory()->paid()->create(); + + $bundle->plugins()->attach($existingPlugin); + + $userWithLicense = User::factory()->create(); + $userWithoutLicense = User::factory()->create(); + + // Both users purchased the bundle + foreach ([$userWithLicense, $userWithoutLicense] as $user) { + PluginLicense::factory()->create([ + 'user_id' => $user->id, + 'plugin_id' => $existingPlugin->id, + 'plugin_bundle_id' => $bundle->id, + ]); + } + + // One user already has the new plugin + PluginLicense::factory()->create([ + 'user_id' => $userWithLicense->id, + 'plugin_id' => $newPlugin->id, + ]); + + $this->artisan('plugins:grant-to-bundle-owners', [ + 'bundle' => $bundle->slug, + 'plugin' => $newPlugin->name, + ])->assertSuccessful(); + + // Should only create one new license (for the user without) + $this->assertDatabaseCount('plugin_licenses', 4); // 2 bundle + 1 existing + 1 new + + Notification::assertSentTo($userWithoutLicense, BundlePluginAdded::class); + Notification::assertNotSentTo($userWithLicense, BundlePluginAdded::class); + } + + public function test_dry_run_does_not_create_licenses_or_send_emails(): void + { + Notification::fake(); + + $bundle = PluginBundle::factory()->active()->create(); + $existingPlugin = Plugin::factory()->paid()->create(); + $newPlugin = Plugin::factory()->paid()->create(); + + $bundle->plugins()->attach($existingPlugin); + + $user = User::factory()->create(); + + PluginLicense::factory()->create([ + 'user_id' => $user->id, + 'plugin_id' => $existingPlugin->id, + 'plugin_bundle_id' => $bundle->id, + ]); + + $this->artisan('plugins:grant-to-bundle-owners', [ + 'bundle' => $bundle->slug, + 'plugin' => $newPlugin->name, + '--dry-run' => true, + ])->assertSuccessful(); + + $this->assertDatabaseMissing('plugin_licenses', [ + 'user_id' => $user->id, + 'plugin_id' => $newPlugin->id, + ]); + + Notification::assertNothingSent(); + } + + public function test_no_email_option_grants_without_sending_notification(): void + { + Notification::fake(); + + $bundle = PluginBundle::factory()->active()->create(); + $existingPlugin = Plugin::factory()->paid()->create(); + $newPlugin = Plugin::factory()->paid()->create(); + + $bundle->plugins()->attach($existingPlugin); + + $user = User::factory()->create(); + + PluginLicense::factory()->create([ + 'user_id' => $user->id, + 'plugin_id' => $existingPlugin->id, + 'plugin_bundle_id' => $bundle->id, + ]); + + $this->artisan('plugins:grant-to-bundle-owners', [ + 'bundle' => $bundle->slug, + 'plugin' => $newPlugin->name, + '--no-email' => true, + ])->assertSuccessful(); + + $this->assertDatabaseHas('plugin_licenses', [ + 'user_id' => $user->id, + 'plugin_id' => $newPlugin->id, + 'plugin_bundle_id' => $bundle->id, + ]); + + Notification::assertNothingSent(); + } + + public function test_fails_with_invalid_bundle_slug(): void + { + $plugin = Plugin::factory()->create(); + + $this->artisan('plugins:grant-to-bundle-owners', [ + 'bundle' => 'nonexistent-bundle', + 'plugin' => $plugin->name, + ])->assertFailed(); + } + + public function test_fails_with_invalid_plugin_name(): void + { + $bundle = PluginBundle::factory()->active()->create(); + + $this->artisan('plugins:grant-to-bundle-owners', [ + 'bundle' => $bundle->slug, + 'plugin' => 'nonexistent/plugin', + ])->assertFailed(); + } + + public function test_handles_bundle_with_no_purchasers(): void + { + $bundle = PluginBundle::factory()->active()->create(); + $plugin = Plugin::factory()->paid()->create(); + + $this->artisan('plugins:grant-to-bundle-owners', [ + 'bundle' => $bundle->slug, + 'plugin' => $plugin->name, + ])->assertSuccessful() + ->expectsOutput('No users found who have purchased this bundle.'); + } + + public function test_skips_users_with_expired_bundle_licenses(): void + { + Notification::fake(); + + $bundle = PluginBundle::factory()->active()->create(); + $existingPlugin = Plugin::factory()->paid()->create(); + $newPlugin = Plugin::factory()->paid()->create(); + + $bundle->plugins()->attach($existingPlugin); + + $activeUser = User::factory()->create(); + $expiredUser = User::factory()->create(); + + // Active license + PluginLicense::factory()->create([ + 'user_id' => $activeUser->id, + 'plugin_id' => $existingPlugin->id, + 'plugin_bundle_id' => $bundle->id, + ]); + + // Expired license + PluginLicense::factory()->expired()->create([ + 'user_id' => $expiredUser->id, + 'plugin_id' => $existingPlugin->id, + 'plugin_bundle_id' => $bundle->id, + ]); + + $this->artisan('plugins:grant-to-bundle-owners', [ + 'bundle' => $bundle->slug, + 'plugin' => $newPlugin->name, + ])->assertSuccessful(); + + $this->assertDatabaseHas('plugin_licenses', [ + 'user_id' => $activeUser->id, + 'plugin_id' => $newPlugin->id, + ]); + + $this->assertDatabaseMissing('plugin_licenses', [ + 'user_id' => $expiredUser->id, + 'plugin_id' => $newPlugin->id, + ]); + + Notification::assertSentTo($activeUser, BundlePluginAdded::class); + Notification::assertNotSentTo($expiredUser, BundlePluginAdded::class); + } +} diff --git a/tests/Feature/GrantProductTest.php b/tests/Feature/GrantProductTest.php new file mode 100644 index 00000000..c28c29cf --- /dev/null +++ b/tests/Feature/GrantProductTest.php @@ -0,0 +1,124 @@ +active()->create(); + $user = User::factory()->create(); + + $this->artisan('products:grant', [ + 'product' => $product->slug, + 'user' => $user->email, + ])->assertSuccessful(); + + $this->assertDatabaseHas('product_licenses', [ + 'user_id' => $user->id, + 'product_id' => $product->id, + 'price_paid' => 0, + 'currency' => 'USD', + 'is_comped' => true, + ]); + + Notification::assertSentTo($user, ProductGranted::class); + } + + public function test_skips_user_who_already_has_the_product(): void + { + Notification::fake(); + + $product = Product::factory()->active()->create(); + $user = User::factory()->create(); + + ProductLicense::factory()->create([ + 'user_id' => $user->id, + 'product_id' => $product->id, + ]); + + $this->artisan('products:grant', [ + 'product' => $product->slug, + 'user' => $user->email, + ])->assertSuccessful() + ->expectsOutput("User {$user->email} already has a license for this product."); + + $this->assertDatabaseCount('product_licenses', 1); + + Notification::assertNothingSent(); + } + + public function test_dry_run_does_not_create_license_or_send_email(): void + { + Notification::fake(); + + $product = Product::factory()->active()->create(); + $user = User::factory()->create(); + + $this->artisan('products:grant', [ + 'product' => $product->slug, + 'user' => $user->email, + '--dry-run' => true, + ])->assertSuccessful(); + + $this->assertDatabaseMissing('product_licenses', [ + 'user_id' => $user->id, + 'product_id' => $product->id, + ]); + + Notification::assertNothingSent(); + } + + public function test_no_email_option_grants_without_sending_notification(): void + { + Notification::fake(); + + $product = Product::factory()->active()->create(); + $user = User::factory()->create(); + + $this->artisan('products:grant', [ + 'product' => $product->slug, + 'user' => $user->email, + '--no-email' => true, + ])->assertSuccessful(); + + $this->assertDatabaseHas('product_licenses', [ + 'user_id' => $user->id, + 'product_id' => $product->id, + ]); + + Notification::assertNothingSent(); + } + + public function test_fails_with_invalid_product_slug(): void + { + $user = User::factory()->create(); + + $this->artisan('products:grant', [ + 'product' => 'nonexistent-product', + 'user' => $user->email, + ])->assertFailed(); + } + + public function test_fails_with_invalid_user_email(): void + { + $product = Product::factory()->active()->create(); + + $this->artisan('products:grant', [ + 'product' => $product->slug, + 'user' => 'nobody@example.com', + ])->assertFailed(); + } +} diff --git a/tests/Feature/HeadingRendererTest.php b/tests/Feature/HeadingRendererTest.php new file mode 100644 index 00000000..b4b29f6d --- /dev/null +++ b/tests/Feature/HeadingRendererTest.php @@ -0,0 +1,60 @@ +assertStringContainsString('My HeadingassertStringNotContainsString('#
My Heading', $html); + } + + public function test_heading_anchor_has_correct_class(): void + { + $html = CommonMark::convertToHtml('## Test Heading'); + + $this->assertStringContainsString('heading-anchor', $html); + $this->assertStringContainsString('ml-2', $html); + } + + public function test_heading_has_id_attribute(): void + { + $html = CommonMark::convertToHtml('## My Section'); + + $this->assertStringContainsString('id="my-section"', $html); + } + + public function test_heading_anchor_links_to_id(): void + { + $html = CommonMark::convertToHtml('## My Section'); + + $this->assertStringContainsString('href="#my-section"', $html); + } + + public function test_h1_gets_anchor(): void + { + $html = CommonMark::convertToHtml('# Title'); + + $this->assertStringContainsString('heading-anchor', $html); + } + + public function test_h3_gets_anchor(): void + { + $html = CommonMark::convertToHtml('### Sub Section'); + + $this->assertStringContainsString('heading-anchor', $html); + } + + public function test_h4_does_not_get_anchor(): void + { + $html = CommonMark::convertToHtml('#### Deep Heading'); + + $this->assertStringNotContainsString('heading-anchor', $html); + } +} diff --git a/tests/Feature/Jobs/CreateAnystackLicenseJobTest.php b/tests/Feature/Jobs/CreateAnystackLicenseJobTest.php new file mode 100644 index 00000000..8a269cba --- /dev/null +++ b/tests/Feature/Jobs/CreateAnystackLicenseJobTest.php @@ -0,0 +1,248 @@ +now = now()->toImmutable(); + + Http::fake([ + 'https://api.anystack.sh/v1/contacts' => Http::response([ + 'data' => [ + 'id' => 'contact-123', + 'email' => 'test@example.com', + 'first_name' => 'John', + 'last_name' => 'Doe', + 'created_at' => $this->now->toIso8601String(), + 'updated_at' => $this->now->toIso8601String(), + ], + ], 201), + + 'https://api.anystack.sh/v1/products/*/licenses' => Http::response([ + 'data' => [ + 'id' => 'license-123', + 'key' => 'test-license-key-12345', + 'contact_id' => 'contact-123', + 'policy_id' => 'policy-123', + 'name' => null, + 'activations' => 0, + 'max_activations' => 10, + 'suspended' => false, + 'expires_at' => $this->now->addYear()->toIso8601String(), + 'created_at' => $this->now->toIso8601String(), + 'updated_at' => $this->now->toIso8601String(), + ], + ], 201), + ]); + + Notification::fake(); + } + + /** @test */ + public function it_creates_a_contact_and_license_on_anystack_via_api() + { + $user = User::factory()->create([ + 'email' => 'test@example.com', + 'name' => 'John Doe', + ]); + + $job = new CreateAnystackLicenseJob( + $user, + Subscription::Max, + null, + 'John', + 'Doe' + ); + + $job->handle(); + + Http::assertSent(function ($request) { + return $request->url() === 'https://api.anystack.sh/v1/contacts' && + $request->method() === 'POST' && + $request->data() === [ + 'first_name' => 'John', + 'last_name' => 'Doe', + 'email' => 'test@example.com', + ]; + }); + + $productId = Subscription::Max->anystackProductId(); + + Http::assertSent(function ($request) use ($productId) { + return $request->url() === "https://api.anystack.sh/v1/products/$productId/licenses" && + $request->method() === 'POST' && + $request->data() === [ + 'policy_id' => Subscription::Max->anystackPolicyId(), + 'contact_id' => 'contact-123', + ]; + }); + } + + /** @test */ + public function it_does_not_create_a_contact_when_the_user_already_has_a_contact_id() + { + $user = User::factory()->create([ + 'email' => 'test@example.com', + 'name' => 'John Doe', + 'anystack_contact_id' => 'contact-123', + ]); + + $job = new CreateAnystackLicenseJob( + $user, + Subscription::Max, + null, + 'John', + 'Doe' + ); + + $job->handle(); + + Http::assertNotSent(function ($request) { + return Str::contains($request->url(), 'https://api.anystack.sh/v1/contacts'); + }); + } + + /** @test */ + public function it_stores_the_license_key_in_database() + { + $user = User::factory()->create([ + 'email' => 'test@example.com', + 'name' => 'John Doe', + ]); + + $job = new CreateAnystackLicenseJob( + $user, + Subscription::Max, + null, + 'John', + 'Doe' + ); + + $job->handle(); + + $this->assertDatabaseHas('licenses', [ + 'anystack_id' => 'license-123', + 'user_id' => $user->id, + 'subscription_item_id' => null, + 'policy_name' => 'max', + 'key' => 'test-license-key-12345', + 'is_suspended' => false, + 'expires_at' => $this->now->addYear(), + 'created_at' => $this->now, + 'updated_at' => $this->now, + ]); + } + + /** @test */ + public function the_subscription_item_id_is_filled_when_provided() + { + $user = User::factory()->create([ + 'email' => 'test@example.com', + 'name' => 'John Doe', + ]); + + $job = new CreateAnystackLicenseJob( + $user, + Subscription::Max, + 123, + 'John', + 'Doe' + ); + + $job->handle(); + + $this->assertDatabaseHas('licenses', [ + 'user_id' => $user->id, + 'subscription_item_id' => 123, + 'policy_name' => 'max', + 'key' => 'test-license-key-12345', + 'is_suspended' => false, + 'expires_at' => $this->now->addYear(), + 'created_at' => $this->now, + 'updated_at' => $this->now, + ]); + } + + /** @test */ + public function it_sends_a_license_key_notification() + { + $user = User::factory()->create([ + 'email' => 'test@example.com', + 'name' => 'John Doe', + ]); + + $job = new CreateAnystackLicenseJob( + $user, + Subscription::Max, + null, + 'John', + 'Doe' + ); + + $job->handle(); + + Notification::assertSentTo( + $user, + function (LicenseKeyGenerated $notification, array $channels, object $notifiable) { + return $notification->licenseKey === 'test-license-key-12345' && + $notification->subscription === Subscription::Max && + $notification->firstName === 'John'; + } + ); + } + + /** @test */ + public function it_handles_missing_name_components() + { + $user = User::factory()->create([ + 'email' => 'test@example.com', + 'name' => null, + ]); + + // Create and run the job with missing name components + $job = new CreateAnystackLicenseJob( + $user, + Subscription::Max, + ); + + $job->handle(); + + // Assert HTTP request was made with correct data (no name components) + Http::assertSent(function ($request) { + return $request->url() === 'https://api.anystack.sh/v1/contacts' && + $request->method() === 'POST' && + $request->data() === [ + 'email' => 'test@example.com', + ]; + }); + + // Assert notification was sent with null firstName + Notification::assertSentTo( + $user, + function (LicenseKeyGenerated $notification, array $channels, object $notifiable) { + return $notification->licenseKey === 'test-license-key-12345' && + $notification->subscription === Subscription::Max && + $notification->firstName === null; + } + ); + } +} diff --git a/tests/Feature/Jobs/CreateUserFromStripeCustomerTest.php b/tests/Feature/Jobs/CreateUserFromStripeCustomerTest.php new file mode 100644 index 00000000..e2166db2 --- /dev/null +++ b/tests/Feature/Jobs/CreateUserFromStripeCustomerTest.php @@ -0,0 +1,118 @@ + 'cus_minimal123', + 'name' => 'Test User', + 'email' => 'test@example.com', + ]); + + $job = new CreateUserFromStripeCustomer($customer); + $job->handle(); + + $user = User::where('email', 'test@example.com')->first(); + + $this->assertNotNull($user); + $this->assertEquals('Test User', $user->name); + $this->assertEquals('test@example.com', $user->email); + $this->assertEquals('cus_minimal123', $user->stripe_id); + + $this->assertNotNull($user->password); + $this->assertTrue(Hash::isHashed($user->password)); + } + + /** @test */ + public function it_fails_when_a_user_with_the_same_stripe_id_already_exists() + { + $existingUser = User::factory()->create([ + 'stripe_id' => 'cus_existing123', + ]); + + $customer = Customer::constructFrom([ + 'id' => 'cus_existing123', + 'name' => 'Another User', + 'email' => 'another@example.com', + ]); + + $job = new CreateUserFromStripeCustomer($customer); + + $job->handle(); + + $this->assertDatabaseCount('users', 1); + $this->assertEquals($existingUser->id, User::first()->id); + } + + /** @test */ + public function it_fails_when_a_user_with_the_same_email_already_exists() + { + $existingUser = User::factory()->create([ + 'email' => 'existing@example.com', + ]); + + $customer = Customer::constructFrom([ + 'id' => 'cus_existing123', + 'name' => 'Another User', + 'email' => 'existing@example.com', + ]); + + $job = new CreateUserFromStripeCustomer($customer); + + $job->handle(); + + $this->assertDatabaseCount('users', 1); + $this->assertEquals($existingUser->id, User::first()->id); + } + + /** @test */ + public function it_handles_a_null_name_in_stripe_customer() + { + $customer = Customer::constructFrom([ + 'id' => 'cus_noname123', + 'name' => null, + 'email' => 'noname@example.com', + ]); + + $job = new CreateUserFromStripeCustomer($customer); + $job->handle(); + + $this->assertDatabaseHas('users', [ + 'name' => null, + 'email' => 'noname@example.com', + 'stripe_id' => 'cus_noname123', + ]); + } + + /** @test */ + public function it_fails_when_customer_has_no_email() + { + $customer = Customer::constructFrom([ + 'id' => 'cus_noemail123', + 'name' => 'No Email', + 'email' => '', + ]); + + $job = new CreateUserFromStripeCustomer($customer); + + $this->expectException(ValidationException::class); + + $job->handle(); + + $this->assertDatabaseCount('users', 0); + } +} diff --git a/tests/Feature/Jobs/HandleCustomerSubscriptionCreatedJobTest.php b/tests/Feature/Jobs/HandleCustomerSubscriptionCreatedJobTest.php new file mode 100644 index 00000000..c9579c04 --- /dev/null +++ b/tests/Feature/Jobs/HandleCustomerSubscriptionCreatedJobTest.php @@ -0,0 +1,297 @@ +createTestData('John Doe'); + + Bus::fake(); + + $webhookCall = new WebhookHandled($this->getTestWebhookPayload()); + + $job = new HandleCustomerSubscriptionCreatedJob($webhookCall); + $job->handle(); + + Bus::assertNotDispatched(CreateAnystackLicenseJob::class); + } + + /** @test */ + public function it_fails_when_customer_has_no_email() + { + $mockCustomer = Customer::constructFrom([ + 'id' => 'cus_S9dhoV2rJK2Auy', + 'email' => '', + 'name' => 'John Doe', + ]); + + $this->mockStripeClient($mockCustomer); + + User::factory()->create([ + 'stripe_id' => 'cus_S9dhoV2rJK2Auy', + 'name' => 'John Doe', + 'email' => '', + ]); + + Bus::fake(); + + $webhookCall = new WebhookHandled($this->getTestWebhookPayload()); + + $job = new HandleCustomerSubscriptionCreatedJob($webhookCall); + $job->handle(); + + Bus::assertNotDispatched(CreateAnystackLicenseJob::class); + } + + protected function createTestData(?string $customerName) + { + $mockCustomer = Customer::constructFrom([ + 'id' => $this->getTestWebhookPayload()['data']['object']['customer'], + 'email' => $email = 'test@example.com', + 'name' => $customerName, + ]); + + $this->mockStripeClient($mockCustomer); + + dispatch_sync(new CreateUserFromStripeCustomer($mockCustomer)); + + $user = User::query()->where('email', $email)->firstOrFail(); + + $subscription = \Laravel\Cashier\Subscription::factory() + ->for($user, 'user') + ->create([ + 'stripe_id' => $this->getTestWebhookPayload()['data']['object']['id'], + 'stripe_status' => 'active', + 'stripe_price' => $this->getTestWebhookPayload()['data']['object']['items']['data'][0]['price']['id'], + 'quantity' => 1, + ]); + $subscriptionItem = SubscriptionItem::factory() + ->for($subscription, 'subscription') + ->create([ + 'stripe_id' => $this->getTestWebhookPayload()['data']['object']['items']['data'][0]['id'], + 'stripe_price' => $this->getTestWebhookPayload()['data']['object']['items']['data'][0]['price']['id'], + 'quantity' => 1, + ]); + } + + protected function getTestWebhookPayload(): array + { + return [ + 'type' => 'customer.subscription.created', + 'data' => [ + 'object' => [ + 'id' => 'sub_1RFKQDAyFo6rlwXq6Wuu642C', + 'object' => 'subscription', + 'application' => null, + 'application_fee_percent' => null, + 'automatic_tax' => [ + 'disabled_reason' => null, + 'enabled' => false, + 'liability' => null, + ], + 'billing_cycle_anchor' => 1745003875, + 'billing_cycle_anchor_config' => null, + 'billing_thresholds' => null, + 'cancel_at' => null, + 'cancel_at_period_end' => false, + 'canceled_at' => null, + 'cancellation_details' => [ + 'comment' => null, + 'feedback' => null, + 'reason' => null, + ], + 'collection_method' => 'charge_automatically', + 'created' => 1745003875, + 'currency' => 'usd', + 'current_period_end' => 1776539875, + 'current_period_start' => 1745003875, + 'customer' => 'cus_S9dhoV2rJK2Auy', + 'days_until_due' => null, + 'default_payment_method' => 'pm_1RFKQBAyFo6rlwXq0zprYwdm', + 'default_source' => null, + 'default_tax_rates' => [], + 'description' => null, + 'discount' => null, + 'discounts' => [], + 'ended_at' => null, + 'invoice_settings' => [ + 'account_tax_ids' => null, + 'issuer' => [ + 'type' => 'self', + ], + ], + 'items' => [ + 'object' => 'list', + 'data' => [ + [ + 'id' => 'si_S9dhjbP3rnMPYq', + 'object' => 'subscription_item', + 'billing_thresholds' => null, + 'created' => 1745003876, + 'current_period_end' => 1776539875, + 'current_period_start' => 1745003875, + 'discounts' => [], + 'metadata' => [], + 'plan' => [ + 'id' => Subscription::Max->stripePriceId(), + 'object' => 'plan', + 'active' => true, + 'aggregate_usage' => null, + 'amount' => 25000, + 'amount_decimal' => '25000', + 'billing_scheme' => 'per_unit', + 'created' => 1744986706, + 'currency' => 'usd', + 'interval' => 'year', + 'interval_count' => 1, + 'livemode' => false, + 'metadata' => [], + 'meter' => null, + 'nickname' => null, + 'product' => 'prod_S9Z5CgycbP7P4y', + 'tiers_mode' => null, + 'transform_usage' => null, + 'trial_period_days' => null, + 'usage_type' => 'licensed', + ], + 'price' => [ + 'id' => Subscription::Max->stripePriceId(), + 'object' => 'price', + 'active' => true, + 'billing_scheme' => 'per_unit', + 'created' => 1744986706, + 'currency' => 'usd', + 'custom_unit_amount' => null, + 'livemode' => false, + 'lookup_key' => null, + 'metadata' => [], + 'nickname' => null, + 'product' => 'prod_S9Z5CgycbP7P4y', + 'recurring' => [ + 'aggregate_usage' => null, + 'interval' => 'year', + 'interval_count' => 1, + 'meter' => null, + 'trial_period_days' => null, + 'usage_type' => 'licensed', + ], + 'tax_behavior' => 'unspecified', + 'tiers_mode' => null, + 'transform_quantity' => null, + 'type' => 'recurring', + 'unit_amount' => 25000, + 'unit_amount_decimal' => '25000', + ], + 'quantity' => 1, + 'subscription' => 'sub_1RFKQDAyFo6rlwXq6Wuu642C', + 'tax_rates' => [], + ], + ], + 'has_more' => false, + 'total_count' => 1, + 'url' => '/v1/subscription_items?subscription=sub_1RFKQDAyFo6rlwXq6Wuu642C', + ], + 'latest_invoice' => 'in_1RFKQEAyFo6rlwXqBa5IhGhF', + 'livemode' => false, + 'metadata' => [], + 'next_pending_invoice_item_invoice' => null, + 'on_behalf_of' => null, + 'pause_collection' => null, + 'payment_settings' => [ + 'payment_method_options' => [ + 'acss_debit' => null, + 'bancontact' => null, + 'card' => [ + 'network' => null, + 'request_three_d_secure' => 'automatic', + ], + 'customer_balance' => null, + 'konbini' => null, + 'sepa_debit' => null, + 'us_bank_account' => null, + ], + 'payment_method_types' => null, + 'save_default_payment_method' => 'off', + ], + 'pending_invoice_item_interval' => null, + 'pending_setup_intent' => null, + 'pending_update' => null, + 'plan' => [ + 'id' => Subscription::Max->stripePriceId(), + 'object' => 'plan', + 'active' => true, + 'aggregate_usage' => null, + 'amount' => 25000, + 'amount_decimal' => '25000', + 'billing_scheme' => 'per_unit', + 'created' => 1744986706, + 'currency' => 'usd', + 'interval' => 'year', + 'interval_count' => 1, + 'livemode' => false, + 'metadata' => [], + 'meter' => null, + 'nickname' => null, + 'product' => 'prod_S9Z5CgycbP7P4y', + 'tiers_mode' => null, + 'transform_usage' => null, + 'trial_period_days' => null, + 'usage_type' => 'licensed', + ], + 'quantity' => 1, + 'schedule' => null, + 'start_date' => 1745003875, + 'status' => 'active', + 'test_clock' => null, + 'transfer_data' => null, + 'trial_end' => null, + 'trial_settings' => [ + 'end_behavior' => [ + 'missing_payment_method' => 'create_invoice', + ], + ], + 'trial_start' => null, + ], + ], + ]; + } + + protected function mockStripeClient(Customer $mockCustomer): void + { + $mockStripeClient = $this->createMock(StripeClient::class); + $mockStripeClient->customers = new class($mockCustomer) + { + private $mockCustomer; + + public function __construct($mockCustomer) + { + $this->mockCustomer = $mockCustomer; + } + + public function retrieve() + { + return $this->mockCustomer; + } + }; + + $this->app->instance(StripeClient::class, $mockStripeClient); + } +} diff --git a/tests/Feature/Jobs/HandleInvoicePaidJobTest.php b/tests/Feature/Jobs/HandleInvoicePaidJobTest.php new file mode 100644 index 00000000..6bc2c3ea --- /dev/null +++ b/tests/Feature/Jobs/HandleInvoicePaidJobTest.php @@ -0,0 +1,184 @@ +create([ + 'stripe_id' => 'cus_test123', + ]); + + $priceId = 'price_test_'.$planKey; + config(["subscriptions.plans.{$planKey}.stripe_price_id" => $priceId]); + + $subscription = \Laravel\Cashier\Subscription::factory() + ->for($user, 'user') + ->create([ + 'stripe_id' => 'sub_test123', + 'stripe_status' => 'active', + 'stripe_price' => $priceId, + 'quantity' => 1, + ]); + + SubscriptionItem::factory() + ->for($subscription, 'subscription') + ->create([ + 'stripe_id' => 'si_test123', + 'stripe_price' => $priceId, + 'quantity' => 1, + ]); + + $this->mockStripeSubscriptionRetrieve('sub_test123'); + + $invoice = $this->createStripeInvoice( + customerId: 'cus_test123', + subscriptionId: 'sub_test123', + billingReason: Invoice::BILLING_REASON_SUBSCRIPTION_CREATE, + priceId: $priceId, + subscriptionItemId: 'si_test123', + ); + + $job = new HandleInvoicePaidJob($invoice); + $job->handle(); + + Bus::assertNotDispatched(CreateAnystackLicenseJob::class); + } + + #[Test] + public function it_does_not_auto_set_is_comped_when_invoice_total_is_zero(): void + { + Bus::fake(); + + $user = User::factory()->create([ + 'stripe_id' => 'cus_test123', + ]); + + $priceId = 'price_test_mini'; + config(['subscriptions.plans.mini.stripe_price_id' => $priceId]); + + $subscription = \Laravel\Cashier\Subscription::factory() + ->for($user, 'user') + ->create([ + 'stripe_id' => 'sub_test123', + 'stripe_status' => 'active', + 'stripe_price' => $priceId, + 'quantity' => 1, + 'is_comped' => false, + ]); + + SubscriptionItem::factory() + ->for($subscription, 'subscription') + ->create([ + 'stripe_id' => 'si_test123', + 'stripe_price' => $priceId, + 'quantity' => 1, + ]); + + $this->mockStripeSubscriptionRetrieve('sub_test123'); + + $invoice = $this->createStripeInvoice( + customerId: 'cus_test123', + subscriptionId: 'sub_test123', + billingReason: Invoice::BILLING_REASON_SUBSCRIPTION_CREATE, + priceId: $priceId, + subscriptionItemId: 'si_test123', + total: 0, + ); + + $job = new HandleInvoicePaidJob($invoice); + $job->handle(); + + $subscription->refresh(); + + $this->assertFalse((bool) $subscription->is_comped); + $this->assertEquals(0, $subscription->price_paid); + } + + public static function subscriptionPlanProvider(): array + { + return [ + 'mini' => ['mini'], + 'pro' => ['pro'], + 'max' => ['max'], + ]; + } + + private function createStripeInvoice( + string $customerId, + string $subscriptionId, + string $billingReason, + string $priceId, + string $subscriptionItemId, + int $total = 25000, + ): Invoice { + return Invoice::constructFrom([ + 'id' => 'in_test_'.uniqid(), + 'object' => 'invoice', + 'customer' => $customerId, + 'subscription' => $subscriptionId, + 'billing_reason' => $billingReason, + 'total' => $total, + 'currency' => 'usd', + 'payment_intent' => 'pi_test_'.uniqid(), + 'metadata' => [], + 'lines' => [ + 'object' => 'list', + 'data' => [ + [ + 'id' => 'il_test_'.uniqid(), + 'object' => 'line_item', + 'subscription_item' => $subscriptionItemId, + 'price' => [ + 'id' => $priceId, + 'object' => 'price', + 'active' => true, + 'currency' => 'usd', + 'unit_amount' => 25000, + ], + ], + ], + 'has_more' => false, + 'total_count' => 1, + ], + ]); + } + + private function mockStripeSubscriptionRetrieve(string $subscriptionId): void + { + $mockSubscription = Subscription::constructFrom([ + 'id' => $subscriptionId, + 'metadata' => [], + 'current_period_end' => now()->addYear()->timestamp, + ]); + + $mockSubscriptionsService = $this->createMock(SubscriptionService::class); + $mockSubscriptionsService->method('retrieve')->willReturn($mockSubscription); + + $mockStripeClient = $this->createMock(StripeClient::class); + $mockStripeClient->subscriptions = $mockSubscriptionsService; + + $this->app->bind(StripeClient::class, fn () => $mockStripeClient); + } +} diff --git a/tests/Feature/Jobs/ProcessPayoutTransferTest.php b/tests/Feature/Jobs/ProcessPayoutTransferTest.php new file mode 100644 index 00000000..0e94432e --- /dev/null +++ b/tests/Feature/Jobs/ProcessPayoutTransferTest.php @@ -0,0 +1,45 @@ +create(); + $plugin = Plugin::factory()->paid()->create(['user_id' => $developerAccount->user_id]); + $license = PluginLicense::factory()->create(['plugin_id' => $plugin->id]); + + $payout = PluginPayout::create([ + 'plugin_license_id' => $license->id, + 'developer_account_id' => $developerAccount->id, + 'gross_amount' => 1000, + 'platform_fee' => 300, + 'developer_amount' => 700, + 'status' => PayoutStatus::Pending, + 'eligible_for_payout_at' => now()->subDay(), + ]); + + $this->mock(StripeConnectService::class, function (MockInterface $mock) use ($payout) { + $mock->shouldReceive('processTransfer') + ->once() + ->with(\Mockery::on(fn ($arg) => $arg->id === $payout->id)) + ->andReturn(true); + }); + + ProcessPayoutTransfer::dispatchSync($payout); + } +} diff --git a/tests/Feature/Jobs/ReviewPluginRepositoryTest.php b/tests/Feature/Jobs/ReviewPluginRepositoryTest.php new file mode 100644 index 00000000..d15dc9e4 --- /dev/null +++ b/tests/Feature/Jobs/ReviewPluginRepositoryTest.php @@ -0,0 +1,380 @@ + + */ + protected function fakeGitHub(string $repoSlug, array $tree = [], array $composerRequire = [], ?array $nativephpJson = null, string $defaultBranch = 'main', ?string $latestRelease = null): array + { + $base = "https://api.github.com/repos/{$repoSlug}"; + + $fakes = [ + $base => Http::response(['default_branch' => $defaultBranch]), + "{$base}/git/trees/{$defaultBranch}*" => Http::response(['tree' => $tree]), + "{$base}/contents/composer.json" => Http::response([ + 'content' => base64_encode(json_encode(['require' => $composerRequire])), + 'encoding' => 'base64', + ]), + ]; + + if ($nativephpJson !== null) { + $fakes["{$base}/contents/nativephp.json"] = Http::response([ + 'content' => base64_encode(json_encode($nativephpJson)), + 'encoding' => 'base64', + ]); + } else { + $fakes["{$base}/contents/nativephp.json"] = Http::response([], 404); + } + + if ($latestRelease !== null) { + $fakes["{$base}/releases/latest"] = Http::response(['tag_name' => $latestRelease]); + } else { + $fakes["{$base}/releases/latest"] = Http::response([], 404); + $fakes["{$base}/tags*"] = Http::response([]); + } + + return $fakes; + } + + /** @test */ + public function it_detects_ios_android_and_js_support_from_repo_tree(): void + { + $plugin = Plugin::factory()->create([ + 'repository_url' => 'https://github.com/acme/test-plugin', + ]); + + Http::fake($this->fakeGitHub('acme/test-plugin', tree: [ + ['path' => 'resources/ios/Plugin.swift', 'type' => 'blob'], + ['path' => 'resources/android/Plugin.kt', 'type' => 'blob'], + ['path' => 'resources/js/index.js', 'type' => 'blob'], + ['path' => 'src/ServiceProvider.php', 'type' => 'blob'], + ])); + + $checks = (new ReviewPluginRepository($plugin))->handle(); + + $this->assertTrue($checks['supports_ios']); + $this->assertTrue($checks['supports_android']); + $this->assertTrue($checks['supports_js']); + } + + /** @test */ + public function it_reports_missing_platform_directories(): void + { + $plugin = Plugin::factory()->create([ + 'repository_url' => 'https://github.com/acme/bare-plugin', + ]); + + Http::fake($this->fakeGitHub('acme/bare-plugin', tree: [ + ['path' => 'src/ServiceProvider.php', 'type' => 'blob'], + ])); + + $checks = (new ReviewPluginRepository($plugin))->handle(); + + $this->assertFalse($checks['supports_ios']); + $this->assertFalse($checks['supports_android']); + $this->assertFalse($checks['supports_js']); + } + + /** @test */ + public function it_only_counts_blobs_not_trees_for_directory_check(): void + { + $plugin = Plugin::factory()->create([ + 'repository_url' => 'https://github.com/acme/tree-plugin', + ]); + + Http::fake($this->fakeGitHub('acme/tree-plugin', tree: [ + ['path' => 'resources/ios', 'type' => 'tree'], + ['path' => 'resources/android', 'type' => 'tree'], + ])); + + $checks = (new ReviewPluginRepository($plugin))->handle(); + + $this->assertFalse($checks['supports_ios']); + $this->assertFalse($checks['supports_android']); + } + + /** @test */ + public function it_detects_nativephp_mobile_dependency(): void + { + $plugin = Plugin::factory()->create([ + 'repository_url' => 'https://github.com/acme/sdk-plugin', + ]); + + Http::fake($this->fakeGitHub('acme/sdk-plugin', composerRequire: [ + 'php' => '^8.1', + 'nativephp/mobile' => '^3.0.0', + ])); + + $checks = (new ReviewPluginRepository($plugin))->handle(); + + $this->assertTrue($checks['requires_mobile_sdk']); + $this->assertEquals('^3.0.0', $checks['mobile_sdk_constraint']); + } + + /** @test */ + public function it_reports_missing_mobile_sdk_dependency(): void + { + $plugin = Plugin::factory()->create([ + 'repository_url' => 'https://github.com/acme/no-sdk-plugin', + ]); + + Http::fake($this->fakeGitHub('acme/no-sdk-plugin', composerRequire: ['php' => '^8.1'])); + + $checks = (new ReviewPluginRepository($plugin))->handle(); + + $this->assertFalse($checks['requires_mobile_sdk']); + $this->assertNull($checks['mobile_sdk_constraint']); + } + + /** @test */ + public function it_detects_ios_and_android_min_versions_from_nativephp_json(): void + { + $plugin = Plugin::factory()->create([ + 'repository_url' => 'https://github.com/acme/versioned-plugin', + ]); + + Http::fake($this->fakeGitHub('acme/versioned-plugin', nativephpJson: [ + 'ios' => ['min_version' => '16.0'], + 'android' => ['min_version' => '24'], + ])); + + $checks = (new ReviewPluginRepository($plugin))->handle(); + + $this->assertTrue($checks['has_ios_min_version']); + $this->assertEquals('16.0', $checks['ios_min_version']); + $this->assertTrue($checks['has_android_min_version']); + $this->assertEquals('24', $checks['android_min_version']); + } + + /** @test */ + public function it_reports_missing_min_versions_when_no_nativephp_json(): void + { + $plugin = Plugin::factory()->create([ + 'repository_url' => 'https://github.com/acme/no-manifest-plugin', + ]); + + Http::fake($this->fakeGitHub('acme/no-manifest-plugin')); + + $checks = (new ReviewPluginRepository($plugin))->handle(); + + $this->assertFalse($checks['has_ios_min_version']); + $this->assertNull($checks['ios_min_version']); + $this->assertFalse($checks['has_android_min_version']); + $this->assertNull($checks['android_min_version']); + } + + /** @test */ + public function it_reports_missing_min_version_when_nativephp_json_has_partial_data(): void + { + $plugin = Plugin::factory()->create([ + 'repository_url' => 'https://github.com/acme/partial-plugin', + ]); + + Http::fake($this->fakeGitHub('acme/partial-plugin', nativephpJson: [ + 'ios' => ['min_version' => '15.0'], + ])); + + $checks = (new ReviewPluginRepository($plugin))->handle(); + + $this->assertTrue($checks['has_ios_min_version']); + $this->assertEquals('15.0', $checks['ios_min_version']); + $this->assertFalse($checks['has_android_min_version']); + $this->assertNull($checks['android_min_version']); + } + + /** @test */ + public function it_stores_results_in_review_checks_and_stamps_reviewed_at(): void + { + $plugin = Plugin::factory()->create([ + 'repository_url' => 'https://github.com/acme/store-plugin', + ]); + + Http::fake($this->fakeGitHub('acme/store-plugin', + tree: [['path' => 'resources/ios/Bridge.swift', 'type' => 'blob']], + composerRequire: ['nativephp/mobile' => '^3.0.0'], + )); + + $this->assertNull($plugin->reviewed_at); + $this->assertNull($plugin->review_checks); + + (new ReviewPluginRepository($plugin))->handle(); + + $plugin->refresh(); + + $this->assertNotNull($plugin->reviewed_at); + $this->assertIsArray($plugin->review_checks); + $this->assertTrue($plugin->review_checks['supports_ios']); + $this->assertFalse($plugin->review_checks['supports_android']); + $this->assertTrue($plugin->review_checks['requires_mobile_sdk']); + } + + /** @test */ + public function it_returns_empty_array_when_no_repository_url(): void + { + $plugin = Plugin::factory()->create([ + 'repository_url' => null, + ]); + + $checks = (new ReviewPluginRepository($plugin))->handle(); + + $this->assertEmpty($checks); + $this->assertNull($plugin->fresh()->reviewed_at); + } + + /** @test */ + public function it_uses_the_repos_default_branch_not_hardcoded_main(): void + { + $plugin = Plugin::factory()->create([ + 'repository_url' => 'https://github.com/acme/master-plugin', + ]); + + Http::fake($this->fakeGitHub('acme/master-plugin', + tree: [ + ['path' => 'resources/ios/Plugin.swift', 'type' => 'blob'], + ['path' => 'resources/android/Plugin.kt', 'type' => 'blob'], + ], + defaultBranch: 'master', + )); + + $checks = (new ReviewPluginRepository($plugin))->handle(); + + $this->assertTrue($checks['supports_ios']); + $this->assertTrue($checks['supports_android']); + + Http::assertSent(fn ($request) => str_contains($request->url(), '/git/trees/master')); + Http::assertNotSent(fn ($request) => str_contains($request->url(), '/git/trees/main')); + } + + /** @test */ + public function it_detects_license_file_in_repo(): void + { + $plugin = Plugin::factory()->create([ + 'repository_url' => 'https://github.com/acme/licensed-plugin', + ]); + + Http::fake($this->fakeGitHub('acme/licensed-plugin', tree: [ + ['path' => 'LICENSE', 'type' => 'blob'], + ['path' => 'src/ServiceProvider.php', 'type' => 'blob'], + ])); + + $checks = (new ReviewPluginRepository($plugin))->handle(); + + $this->assertTrue($checks['has_license_file']); + } + + /** @test */ + public function it_detects_license_md_file_in_repo(): void + { + $plugin = Plugin::factory()->create([ + 'repository_url' => 'https://github.com/acme/licensed-md-plugin', + ]); + + Http::fake($this->fakeGitHub('acme/licensed-md-plugin', tree: [ + ['path' => 'LICENSE.md', 'type' => 'blob'], + ['path' => 'src/ServiceProvider.php', 'type' => 'blob'], + ])); + + $checks = (new ReviewPluginRepository($plugin))->handle(); + + $this->assertTrue($checks['has_license_file']); + } + + /** @test */ + public function it_reports_missing_license_file(): void + { + $plugin = Plugin::factory()->create([ + 'repository_url' => 'https://github.com/acme/unlicensed-plugin', + ]); + + Http::fake($this->fakeGitHub('acme/unlicensed-plugin', tree: [ + ['path' => 'src/ServiceProvider.php', 'type' => 'blob'], + ])); + + $checks = (new ReviewPluginRepository($plugin))->handle(); + + $this->assertFalse($checks['has_license_file']); + } + + /** @test */ + public function it_does_not_count_license_directory_as_license_file(): void + { + $plugin = Plugin::factory()->create([ + 'repository_url' => 'https://github.com/acme/license-dir-plugin', + ]); + + Http::fake($this->fakeGitHub('acme/license-dir-plugin', tree: [ + ['path' => 'LICENSE', 'type' => 'tree'], + ])); + + $checks = (new ReviewPluginRepository($plugin))->handle(); + + $this->assertFalse($checks['has_license_file']); + } + + /** @test */ + public function it_detects_release_version_from_github_release(): void + { + $plugin = Plugin::factory()->create([ + 'repository_url' => 'https://github.com/acme/released-plugin', + ]); + + Http::fake($this->fakeGitHub('acme/released-plugin', latestRelease: 'v1.0.0')); + + $checks = (new ReviewPluginRepository($plugin))->handle(); + + $this->assertTrue($checks['has_release_version']); + $this->assertEquals('v1.0.0', $checks['release_version']); + } + + /** @test */ + public function it_falls_back_to_tags_when_no_release_exists(): void + { + $plugin = Plugin::factory()->create([ + 'repository_url' => 'https://github.com/acme/tagged-plugin', + ]); + + $base = 'https://api.github.com/repos/acme/tagged-plugin'; + + Http::fake(array_merge( + $this->fakeGitHub('acme/tagged-plugin'), + [ + "{$base}/releases/latest" => Http::response([], 404), + "{$base}/tags*" => Http::response([ + ['name' => 'v0.5.0'], + ]), + ] + )); + + $checks = (new ReviewPluginRepository($plugin))->handle(); + + $this->assertTrue($checks['has_release_version']); + $this->assertEquals('v0.5.0', $checks['release_version']); + } + + /** @test */ + public function it_reports_missing_release_version(): void + { + $plugin = Plugin::factory()->create([ + 'repository_url' => 'https://github.com/acme/unreleased-plugin', + ]); + + Http::fake($this->fakeGitHub('acme/unreleased-plugin')); + + $checks = (new ReviewPluginRepository($plugin))->handle(); + + $this->assertFalse($checks['has_release_version']); + $this->assertNull($checks['release_version']); + } +} diff --git a/tests/Feature/Jobs/UpsertLicenseFromAnystackLicenseTest.php b/tests/Feature/Jobs/UpsertLicenseFromAnystackLicenseTest.php new file mode 100644 index 00000000..e916c408 --- /dev/null +++ b/tests/Feature/Jobs/UpsertLicenseFromAnystackLicenseTest.php @@ -0,0 +1,54 @@ +create([ + 'anystack_contact_id' => 'contact-123', + ]); + + $now = Date::now()->toImmutable(); + + $licenseData = [ + 'id' => 'license-123', + 'key' => 'test-license-key-12345', + 'contact_id' => 'contact-123', + 'policy_id' => Subscription::Mini->anystackPolicyId(), + 'name' => null, + 'activations' => 0, + 'max_activations' => 10, + 'suspended' => true, + 'expires_at' => $now->addYear()->toIso8601String(), + 'created_at' => $now->toIso8601String(), + 'updated_at' => $now->toIso8601String(), + ]; + + $job = new UpsertLicenseFromAnystackLicense($licenseData); + $job->handle(); + + $this->assertDatabaseHas('licenses', [ + 'anystack_id' => 'license-123', + 'user_id' => $user->id, + 'key' => 'test-license-key-12345', + 'policy_name' => Subscription::Mini->value, + 'is_suspended' => true, + 'expires_at' => $now->addYear(), + 'created_at' => $now, + 'updated_at' => $now, + ]); + } +} diff --git a/tests/Feature/LeadSubmissionTest.php b/tests/Feature/LeadSubmissionTest.php new file mode 100644 index 00000000..96989f1f --- /dev/null +++ b/tests/Feature/LeadSubmissionTest.php @@ -0,0 +1,241 @@ +get(route('build-my-app')) + ->assertOk() + ->assertSeeLivewire(LeadSubmissionForm::class); + } + + #[Test] + public function lead_can_be_submitted_successfully(): void + { + Notification::fake(); + + Livewire::test(LeadSubmissionForm::class) + ->set('name', 'John Doe') + ->set('email', 'john@example.com') + ->set('company', 'Acme Corp') + ->set('description', 'I need a mobile app for my business.') + ->set('budget', 'less_than_5k') + ->set('turnstileToken', 'test-token') + ->call('submit') + ->assertSet('submitted', true) + ->assertHasNoErrors(); + + $this->assertDatabaseHas('leads', [ + 'name' => 'John Doe', + 'email' => 'john@example.com', + 'company' => 'Acme Corp', + 'description' => 'I need a mobile app for my business.', + 'budget' => 'less_than_5k', + ]); + + Notification::assertSentTo( + Lead::first(), + LeadReceived::class + ); + + Notification::assertSentOnDemand( + NewLeadSubmitted::class, + function ($notification, $channels, $notifiable) { + return $notifiable->routes['mail'] === 'sales@nativephp.com'; + } + ); + } + + #[Test] + public function all_fields_are_required(): void + { + Livewire::test(LeadSubmissionForm::class) + ->set('name', '') + ->set('email', '') + ->set('company', '') + ->set('description', '') + ->set('budget', '') + ->set('turnstileToken', 'test-token') + ->call('submit') + ->assertHasErrors(['name', 'email', 'company', 'description', 'budget']); + } + + #[Test] + public function email_must_be_valid(): void + { + Livewire::test(LeadSubmissionForm::class) + ->set('name', 'John Doe') + ->set('email', 'not-an-email') + ->set('company', 'Acme Corp') + ->set('description', 'I need a mobile app.') + ->set('budget', 'less_than_5k') + ->set('turnstileToken', 'test-token') + ->call('submit') + ->assertHasErrors(['email']); + } + + #[Test] + public function budget_must_be_a_valid_option(): void + { + Livewire::test(LeadSubmissionForm::class) + ->set('name', 'John Doe') + ->set('email', 'john@example.com') + ->set('company', 'Acme Corp') + ->set('description', 'I need a mobile app.') + ->set('budget', 'invalid-budget') + ->set('turnstileToken', 'test-token') + ->call('submit') + ->assertHasErrors(['budget']); + } + + #[Test] + public function rate_limiting_is_enforced(): void + { + Notification::fake(); + + for ($i = 0; $i < 5; $i++) { + Livewire::test(LeadSubmissionForm::class) + ->set('name', 'John Doe '.$i) + ->set('email', "john{$i}@example.com") + ->set('company', 'Acme Corp') + ->set('description', 'I need a mobile app.') + ->set('budget', 'less_than_5k') + ->set('turnstileToken', 'test-token') + ->call('submit') + ->assertSet('submitted', true); + } + + $this->assertDatabaseCount('leads', 5); + + Livewire::test(LeadSubmissionForm::class) + ->set('name', 'Rate Limited User') + ->set('email', 'limited@example.com') + ->set('company', 'Acme Corp') + ->set('description', 'I need a mobile app.') + ->set('budget', 'less_than_5k') + ->set('turnstileToken', 'test-token') + ->call('submit') + ->assertHasErrors(['form']); + + $this->assertDatabaseCount('leads', 5); + } + + #[Test] + public function turnstile_validation_passes_when_secret_key_is_not_configured(): void + { + Notification::fake(); + + config(['services.turnstile.secret_key' => null]); + + Livewire::test(LeadSubmissionForm::class) + ->set('name', 'John Doe') + ->set('email', 'john@example.com') + ->set('company', 'Acme Corp') + ->set('description', 'I need a mobile app.') + ->set('budget', 'less_than_5k') + ->set('turnstileToken', '') + ->call('submit') + ->assertSet('submitted', true); + + $this->assertDatabaseCount('leads', 1); + } + + #[Test] + public function turnstile_validation_fails_with_invalid_token(): void + { + config(['services.turnstile.secret_key' => 'test-secret']); + + Http::fake([ + 'challenges.cloudflare.com/*' => Http::response([ + 'success' => false, + ]), + ]); + + Livewire::test(LeadSubmissionForm::class) + ->set('name', 'John Doe') + ->set('email', 'john@example.com') + ->set('company', 'Acme Corp') + ->set('description', 'I need a mobile app.') + ->set('budget', 'less_than_5k') + ->set('turnstileToken', 'invalid-token') + ->call('submit') + ->assertHasErrors(['turnstileToken']); + + $this->assertDatabaseCount('leads', 0); + } + + #[Test] + public function turnstile_validation_passes_with_valid_token(): void + { + Notification::fake(); + + config(['services.turnstile.secret_key' => 'test-secret']); + + Http::fake([ + 'challenges.cloudflare.com/*' => Http::response([ + 'success' => true, + ]), + ]); + + Livewire::test(LeadSubmissionForm::class) + ->set('name', 'John Doe') + ->set('email', 'john@example.com') + ->set('company', 'Acme Corp') + ->set('description', 'I need a mobile app.') + ->set('budget', 'less_than_5k') + ->set('turnstileToken', 'valid-token') + ->call('submit') + ->assertSet('submitted', true); + + $this->assertDatabaseCount('leads', 1); + } + + #[Test] + public function budgets_are_passed_to_the_view(): void + { + Livewire::test(LeadSubmissionForm::class) + ->assertViewHas('budgets', Lead::BUDGETS); + } + + #[Test] + public function ip_address_is_recorded_with_submission(): void + { + Notification::fake(); + + Livewire::test(LeadSubmissionForm::class) + ->set('name', 'John Doe') + ->set('email', 'john@example.com') + ->set('company', 'Acme Corp') + ->set('description', 'I need a mobile app.') + ->set('budget', 'less_than_5k') + ->set('turnstileToken', 'test-token') + ->call('submit'); + + $lead = Lead::first(); + $this->assertNotNull($lead->ip_address); + } +} diff --git a/tests/Feature/LicenseRenewalTest.php b/tests/Feature/LicenseRenewalTest.php new file mode 100644 index 00000000..d34ed80a --- /dev/null +++ b/tests/Feature/LicenseRenewalTest.php @@ -0,0 +1,195 @@ + 'price_test_max_eap', + 'subscriptions.plans.max.stripe_price_id_monthly' => 'price_test_max_monthly', + ]); + } + + private function createLegacyLicense(?User $user = null): License + { + return License::factory() + ->for($user ?? User::factory()->create()) + ->withoutSubscriptionItem() + ->active() + ->max() + ->create(); + } + + public function test_renewal_page_requires_authentication(): void + { + $license = $this->createLegacyLicense(); + + $response = $this->withoutVite()->get(route('license.renewal', $license->key)); + + $response->assertRedirect(route('customer.login')); + } + + public function test_renewal_page_shows_upgrade_to_ultra_heading(): void + { + $license = $this->createLegacyLicense(); + + $response = $this->withoutVite() + ->actingAs($license->user) + ->get(route('license.renewal', $license->key)); + + $response->assertStatus(200); + $response->assertSee('Upgrade to Ultra'); + $response->assertSee('Early Access Pricing'); + } + + public function test_renewal_page_shows_yearly_and_monthly_options(): void + { + $license = $this->createLegacyLicense(); + + $response = $this->withoutVite() + ->actingAs($license->user) + ->get(route('license.renewal', $license->key)); + + $response->assertStatus(200); + $response->assertSee('$250'); + $response->assertSee('/year'); + $response->assertSee('$35'); + $response->assertSee('/month'); + } + + public function test_renewal_page_does_not_show_license_details(): void + { + $license = $this->createLegacyLicense(); + + $response = $this->withoutVite() + ->actingAs($license->user) + ->get(route('license.renewal', $license->key)); + + $response->assertStatus(200); + $response->assertDontSee('License Key'); + $response->assertDontSee('Current Expiry'); + } + + public function test_renewal_page_does_not_show_what_happens_section(): void + { + $license = $this->createLegacyLicense(); + + $response = $this->withoutVite() + ->actingAs($license->user) + ->get(route('license.renewal', $license->key)); + + $response->assertStatus(200); + $response->assertDontSee('What happens when you renew'); + } + + public function test_renewal_page_does_not_show_same_great_rates(): void + { + $license = $this->createLegacyLicense(); + + $response = $this->withoutVite() + ->actingAs($license->user) + ->get(route('license.renewal', $license->key)); + + $response->assertStatus(200); + $response->assertDontSee('same great rates'); + } + + public function test_renewal_page_uses_dashboard_layout(): void + { + $license = $this->createLegacyLicense(); + + $response = $this->withoutVite() + ->actingAs($license->user) + ->get(route('license.renewal', $license->key)); + + $response->assertStatus(200); + // Dashboard layout includes sidebar with navigation items + $response->assertSee('Manage Subscription'); + } + + public function test_renewal_page_returns_403_for_other_users_license(): void + { + $owner = User::factory()->create(); + $otherUser = User::factory()->create(); + $license = $this->createLegacyLicense($owner); + + $response = $this->withoutVite() + ->actingAs($otherUser) + ->get(route('license.renewal', $license->key)); + + $response->assertStatus(403); + } + + public function test_renewal_page_returns_404_for_non_legacy_license(): void + { + $user = User::factory()->create(); + $license = License::factory() + ->for($user) + ->active() + ->max() + ->create(); // Has subscription_item_id (not legacy) + + $response = $this->withoutVite() + ->actingAs($user) + ->get(route('license.renewal', $license->key)); + + $response->assertStatus(404); + } + + public function test_checkout_requires_billing_period(): void + { + $license = $this->createLegacyLicense(); + + $response = $this->actingAs($license->user) + ->post(route('license.renewal.checkout', $license->key)); + + $response->assertSessionHasErrors('billing_period'); + } + + public function test_checkout_rejects_invalid_billing_period(): void + { + $license = $this->createLegacyLicense(); + + $response = $this->actingAs($license->user) + ->post(route('license.renewal.checkout', $license->key), [ + 'billing_period' => 'weekly', + ]); + + $response->assertSessionHasErrors('billing_period'); + } + + public function test_renewal_route_is_under_dashboard_prefix(): void + { + $license = $this->createLegacyLicense(); + + $url = route('license.renewal', $license->key); + + $this->assertStringContains('/dashboard/license/', $url); + } + + /** + * Assert that a string contains a substring. + */ + private function assertStringContains(string $needle, string $haystack): void + { + $this->assertTrue( + str_contains($haystack, $needle), + "Failed asserting that '{$haystack}' contains '{$needle}'." + ); + } +} diff --git a/tests/Feature/Livewire/Customer/Developer/SettingsTest.php b/tests/Feature/Livewire/Customer/Developer/SettingsTest.php new file mode 100644 index 00000000..7ea2cbce --- /dev/null +++ b/tests/Feature/Livewire/Customer/Developer/SettingsTest.php @@ -0,0 +1,160 @@ +create(); + + $response = $this->withoutVite()->actingAs($user)->get('/dashboard/developer/settings'); + + $response->assertStatus(200); + } + + public function test_developer_settings_page_requires_authentication(): void + { + $response = $this->withoutVite()->get('/dashboard/developer/settings'); + + $response->assertRedirect('/login'); + } + + public function test_developer_settings_component_renders_headings(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Settings::class) + ->assertSee('Developer Settings') + ->assertSee('Author Display Name') + ->assertStatus(200); + } + + // --- Update Display Name --- + + public function test_user_can_update_display_name(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Settings::class) + ->set('displayName', 'My Dev Name') + ->call('updateDisplayName') + ->assertHasNoErrors(); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'display_name' => 'My Dev Name', + ]); + } + + public function test_user_can_clear_display_name(): void + { + $user = User::factory()->create(['display_name' => 'Old Name']); + + Livewire::actingAs($user) + ->test(Settings::class) + ->assertSet('displayName', 'Old Name') + ->set('displayName', '') + ->call('updateDisplayName') + ->assertHasNoErrors(); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'display_name' => null, + ]); + } + + public function test_display_name_has_max_length(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Settings::class) + ->set('displayName', str_repeat('a', 256)) + ->call('updateDisplayName') + ->assertHasErrors(['displayName' => 'max']); + } + + public function test_display_name_is_loaded_on_mount(): void + { + $user = User::factory()->create(['display_name' => 'Preset Name']); + + Livewire::actingAs($user) + ->test(Settings::class) + ->assertSet('displayName', 'Preset Name'); + } + + // --- Stripe Connect Status --- + + public function test_active_developer_account_shows_status_badge(): void + { + $user = User::factory()->create(); + DeveloperAccount::factory()->create(['user_id' => $user->id]); + + Livewire::actingAs($user) + ->test(Settings::class) + ->assertSee('Stripe Account Status') + ->assertSee('Account Active') + ->assertSee('Your account is fully set up to receive payouts'); + } + + public function test_pending_developer_account_shows_setup_incomplete(): void + { + $user = User::factory()->create(); + DeveloperAccount::factory()->pending()->create(['user_id' => $user->id]); + + Livewire::actingAs($user) + ->test(Settings::class) + ->assertSee('Stripe Account Status') + ->assertSee('Setup Incomplete') + ->assertSee('Continue Setup'); + } + + public function test_no_developer_account_shows_not_connected(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Settings::class) + ->assertSee('Stripe Account Status') + ->assertSee('Not Connected') + ->assertSee('Connect Stripe'); + } + + public function test_stripe_section_hidden_when_paid_plugins_disabled(): void + { + Feature::define(AllowPaidPlugins::class, false); + + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Settings::class) + ->assertDontSee('Stripe Account Status'); + } +} diff --git a/tests/Feature/Livewire/Customer/DeveloperPagesTest.php b/tests/Feature/Livewire/Customer/DeveloperPagesTest.php new file mode 100644 index 00000000..ff0b1c56 --- /dev/null +++ b/tests/Feature/Livewire/Customer/DeveloperPagesTest.php @@ -0,0 +1,202 @@ +create(); + + $response = $this->withoutVite()->actingAs($user)->get('/dashboard/developer/onboarding'); + + $response->assertStatus(200); + } + + public function test_onboarding_page_requires_authentication(): void + { + $response = $this->withoutVite()->get('/dashboard/developer/onboarding'); + + $response->assertRedirect('/login'); + } + + public function test_onboarding_component_shows_start_selling_for_new_user(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Onboarding::class) + ->assertSee('Start Selling Plugins') + ->assertSee('Connect with Stripe') + ->assertSee('Plugin Developer Terms and Conditions') + ->assertSee('Your Country') + ->assertStatus(200); + } + + public function test_onboarding_component_shows_continue_for_existing_account(): void + { + $user = User::factory()->create(); + DeveloperAccount::factory()->pending()->create(['user_id' => $user->id]); + + Livewire::actingAs($user) + ->test(Onboarding::class) + ->assertSee('Complete Your Onboarding') + ->assertSee('Continue Onboarding') + ->assertSee('Onboarding Incomplete') + ->assertSee('Plugin Developer Terms and Conditions') + ->assertStatus(200); + } + + public function test_onboarding_component_shows_terms_accepted_for_existing_account_with_terms(): void + { + $user = User::factory()->create(); + DeveloperAccount::factory()->pending()->withAcceptedTerms()->create(['user_id' => $user->id]); + + Livewire::actingAs($user) + ->test(Onboarding::class) + ->assertSee('Continue Onboarding') + ->assertSee('You accepted the') + ->assertDontSee('I have read and agree to the') + ->assertStatus(200); + } + + public function test_onboarding_redirects_if_already_completed(): void + { + $user = User::factory()->create(); + DeveloperAccount::factory()->create(['user_id' => $user->id]); + + Livewire::actingAs($user) + ->test(Onboarding::class) + ->assertRedirect(route('customer.developer.dashboard')); + } + + public function test_onboarding_shows_benefits_section(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Onboarding::class) + ->assertSee('Why sell on NativePHP?') + ->assertSee('70% Revenue Share') + ->assertSee('Built-in Distribution') + ->assertStatus(200); + } + + public function test_onboarding_shows_faq_section(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Onboarding::class) + ->assertSee('Frequently Asked Questions') + ->assertSee('How does the revenue share work?') + ->assertSee('14-day refund period') + ->assertSee('plugin documentation') + ->assertStatus(200); + } + + // --- Developer Dashboard --- + + public function test_developer_dashboard_renders_for_completed_account(): void + { + $user = User::factory()->create(); + DeveloperAccount::factory()->create(['user_id' => $user->id]); + + $this->mock(StripeConnectService::class, function ($mock): void { + $mock->shouldReceive('refreshAccountStatus')->once(); + }); + + $response = $this->withoutVite()->actingAs($user)->get('/dashboard/developer'); + + $response->assertStatus(200); + } + + public function test_developer_dashboard_requires_authentication(): void + { + $response = $this->withoutVite()->get('/dashboard/developer'); + + $response->assertRedirect('/login'); + } + + public function test_developer_dashboard_redirects_without_developer_account(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Dashboard::class) + ->assertRedirect(route('customer.developer.onboarding')); + } + + public function test_developer_dashboard_redirects_if_onboarding_incomplete(): void + { + $user = User::factory()->create(); + DeveloperAccount::factory()->pending()->create(['user_id' => $user->id]); + + $this->mock(StripeConnectService::class, function ($mock): void { + $mock->shouldReceive('refreshAccountStatus')->never(); + }); + + Livewire::actingAs($user) + ->test(Dashboard::class) + ->assertRedirect(route('customer.developer.onboarding')); + } + + public function test_developer_dashboard_shows_stats_and_status(): void + { + $user = User::factory()->create(); + DeveloperAccount::factory()->create(['user_id' => $user->id]); + + $this->mock(StripeConnectService::class, function ($mock): void { + $mock->shouldReceive('refreshAccountStatus')->once(); + }); + + Livewire::actingAs($user) + ->test(Dashboard::class) + ->assertSee('Developer Dashboard') + ->assertSee('Total Earnings') + ->assertSee('Pending Payouts') + ->assertSee('Published Plugins') + ->assertSee('Total Sales') + ->assertStatus(200); + } + + public function test_developer_dashboard_shows_empty_states(): void + { + $user = User::factory()->create(); + DeveloperAccount::factory()->create(['user_id' => $user->id]); + + $this->mock(StripeConnectService::class, function ($mock): void { + $mock->shouldReceive('refreshAccountStatus')->once(); + }); + + Livewire::actingAs($user) + ->test(Dashboard::class) + ->assertSee('No premium plugins yet') + ->assertSee('No payouts yet') + ->assertStatus(200); + } +} diff --git a/tests/Feature/Livewire/Customer/NotificationsTest.php b/tests/Feature/Livewire/Customer/NotificationsTest.php new file mode 100644 index 00000000..99fe7f42 --- /dev/null +++ b/tests/Feature/Livewire/Customer/NotificationsTest.php @@ -0,0 +1,189 @@ +create(); + + $response = $this->withoutVite()->actingAs($user)->get('/dashboard/notifications'); + + $response->assertStatus(200); + } + + public function test_notifications_page_requires_authentication(): void + { + $response = $this->withoutVite()->get('/dashboard/notifications'); + + $response->assertRedirect('/login'); + } + + public function test_notifications_component_renders_headings(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Notifications::class) + ->assertSee('Notifications') + ->assertSee('Stay up to date with your account activity.') + ->assertStatus(200); + } + + // --- Displaying notifications --- + + public function test_notifications_display_in_reverse_chronological_order(): void + { + $user = User::factory()->create(); + + $this->createNotification($user, ['title' => 'First'], now()->subHours(2)); + $this->createNotification($user, ['title' => 'Second'], now()->subHour()); + $this->createNotification($user, ['title' => 'Third'], now()); + + Livewire::actingAs($user) + ->test(Notifications::class) + ->assertSeeInOrder(['Third', 'Second', 'First']); + } + + public function test_empty_state_shown_when_no_notifications(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Notifications::class) + ->assertSee('No notifications') + ->assertSee("You're all caught up!", escape: false); + } + + public function test_notification_title_and_body_are_displayed(): void + { + $user = User::factory()->create(); + + $this->createNotification($user, [ + 'title' => 'License Renewed', + 'body' => 'Your license has been renewed successfully.', + ]); + + Livewire::actingAs($user) + ->test(Notifications::class) + ->assertSee('License Renewed') + ->assertSee('Your license has been renewed successfully.'); + } + + // --- Mark as read --- + + public function test_mark_single_notification_as_read(): void + { + $user = User::factory()->create(); + + $notification = $this->createNotification($user, ['title' => 'Test']); + + $this->assertNull($notification->read_at); + + Livewire::actingAs($user) + ->test(Notifications::class) + ->call('markAsRead', $notification->id); + + $this->assertNotNull($notification->fresh()->read_at); + } + + public function test_mark_all_as_read(): void + { + $user = User::factory()->create(); + + $n1 = $this->createNotification($user, ['title' => 'First']); + $n2 = $this->createNotification($user, ['title' => 'Second']); + + Livewire::actingAs($user) + ->test(Notifications::class) + ->call('markAllAsRead'); + + $this->assertNotNull($n1->fresh()->read_at); + $this->assertNotNull($n2->fresh()->read_at); + } + + public function test_mark_all_as_read_button_hidden_when_none_unread(): void + { + $user = User::factory()->create(); + + $this->createNotification($user, ['title' => 'Read one'], now(), now()); + + Livewire::actingAs($user) + ->test(Notifications::class) + ->assertDontSee('Mark all as read'); + } + + public function test_mark_all_as_read_button_shown_when_unread_exist(): void + { + $user = User::factory()->create(); + + $this->createNotification($user, ['title' => 'Unread one']); + + Livewire::actingAs($user) + ->test(Notifications::class) + ->assertSee('Mark all as read'); + } + + // --- Settings link --- + + public function test_notifications_page_shows_link_to_notification_settings(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Notifications::class) + ->assertSee('Settings'); + } + + // --- Bell icon in layout --- + + public function test_bell_icon_shows_in_dashboard_layout(): void + { + $user = User::factory()->create(); + + $response = $this->withoutVite()->actingAs($user)->get('/dashboard/settings'); + + $response->assertStatus(200); + $response->assertSee(route('customer.notifications')); + } + + /** + * Create a database notification for a user. + */ + private function createNotification(User $user, array $data, ?Carbon $createdAt = null, ?Carbon $readAt = null): DatabaseNotification + { + return DatabaseNotification::create([ + 'id' => Str::uuid()->toString(), + 'type' => 'App\\Notifications\\PluginApproved', + 'notifiable_type' => User::class, + 'notifiable_id' => $user->id, + 'data' => $data, + 'read_at' => $readAt, + 'created_at' => $createdAt ?? now(), + 'updated_at' => $createdAt ?? now(), + ]); + } +} diff --git a/tests/Feature/Livewire/Customer/PluginCreateTest.php b/tests/Feature/Livewire/Customer/PluginCreateTest.php new file mode 100644 index 00000000..a425283b --- /dev/null +++ b/tests/Feature/Livewire/Customer/PluginCreateTest.php @@ -0,0 +1,221 @@ +create([ + 'github_id' => '12345', + 'github_username' => 'testuser', + 'github_token' => encrypt('fake-token'), + ]); + } + + private function sampleRepos(): array + { + return [ + ['id' => 1, 'full_name' => 'testuser/alpha-plugin', 'name' => 'alpha-plugin', 'owner' => 'testuser', 'private' => false], + ['id' => 2, 'full_name' => 'testuser/beta-plugin', 'name' => 'beta-plugin', 'owner' => 'testuser', 'private' => true], + ['id' => 3, 'full_name' => 'my-org/org-repo', 'name' => 'org-repo', 'owner' => 'my-org', 'private' => false], + ['id' => 4, 'full_name' => 'my-org/another-repo', 'name' => 'another-repo', 'owner' => 'my-org', 'private' => false], + ]; + } + + private function fakeComposerJson(string $owner, string $repo, string $packageName): void + { + $composerJson = base64_encode(json_encode(['name' => $packageName])); + + Http::fake([ + "api.github.com/repos/{$owner}/{$repo}/contents/composer.json*" => Http::response([ + 'content' => $composerJson, + ]), + 'api.github.com/*' => Http::response([], 404), + ]); + } + + // ======================================== + // Owner/Repository Selection Tests + // ======================================== + + public function test_owners_are_extracted_from_repositories(): void + { + $user = $this->createGitHubUser(); + + $component = Livewire::actingAs($user)->test(Create::class) + ->set('repositories', $this->sampleRepos()) + ->set('reposLoaded', true); + + $owners = $component->get('owners'); + + $this->assertCount(2, $owners); + $this->assertContains('testuser', $owners); + $this->assertContains('my-org', $owners); + } + + public function test_selecting_owner_filters_repositories(): void + { + $user = $this->createGitHubUser(); + + $component = Livewire::actingAs($user)->test(Create::class) + ->set('repositories', $this->sampleRepos()) + ->set('reposLoaded', true) + ->set('selectedOwner', 'my-org'); + + $ownerRepos = $component->get('ownerRepositories'); + + $this->assertCount(2, $ownerRepos); + $this->assertEquals('another-repo', $ownerRepos[0]['name']); + $this->assertEquals('org-repo', $ownerRepos[1]['name']); + } + + public function test_changing_owner_resets_repository_selection(): void + { + $user = $this->createGitHubUser(); + + Livewire::actingAs($user)->test(Create::class) + ->set('repositories', $this->sampleRepos()) + ->set('reposLoaded', true) + ->set('selectedOwner', 'testuser') + ->set('repository', 'testuser/alpha-plugin') + ->set('selectedOwner', 'my-org') + ->assertSet('repository', ''); + } + + public function test_owner_repositories_sorted_alphabetically(): void + { + $user = $this->createGitHubUser(); + + $component = Livewire::actingAs($user)->test(Create::class) + ->set('repositories', $this->sampleRepos()) + ->set('reposLoaded', true) + ->set('selectedOwner', 'testuser'); + + $ownerRepos = $component->get('ownerRepositories'); + + $this->assertEquals('alpha-plugin', $ownerRepos[0]['name']); + $this->assertEquals('beta-plugin', $ownerRepos[1]['name']); + } + + public function test_no_owner_selected_returns_empty_repositories(): void + { + $user = $this->createGitHubUser(); + + $component = Livewire::actingAs($user)->test(Create::class) + ->set('repositories', $this->sampleRepos()) + ->set('reposLoaded', true); + + $ownerRepos = $component->get('ownerRepositories'); + + $this->assertEmpty($ownerRepos); + } + + // ======================================== + // Namespace Validation Tests + // ======================================== + + public function test_submission_blocked_when_namespace_claimed_by_another_user(): void + { + $existingUser = User::factory()->create(); + Plugin::factory()->for($existingUser)->create(['name' => 'acme/existing-plugin']); + + $user = $this->createGitHubUser(); + + $this->fakeComposerJson('acme', 'new-plugin', 'acme/new-plugin'); + + Livewire::actingAs($user)->test(Create::class) + ->set('repository', 'acme/new-plugin') + ->set('pluginType', 'free') + ->call('submitPlugin') + ->assertNoRedirect(); + + $this->assertDatabaseMissing('plugins', [ + 'repository_url' => 'https://github.com/acme/new-plugin', + ]); + } + + public function test_submission_blocked_for_reserved_namespace(): void + { + $user = $this->createGitHubUser(); + + $this->fakeComposerJson('nativephp', 'my-plugin', 'nativephp/my-plugin'); + + Livewire::actingAs($user)->test(Create::class) + ->set('repository', 'nativephp/my-plugin') + ->set('pluginType', 'free') + ->call('submitPlugin') + ->assertNoRedirect(); + + $this->assertDatabaseMissing('plugins', [ + 'repository_url' => 'https://github.com/nativephp/my-plugin', + ]); + } + + public function test_submission_allowed_for_own_namespace(): void + { + $user = $this->createGitHubUser(); + Plugin::factory()->for($user)->create(['name' => 'myvendor/first-plugin']); + + $composerJson = base64_encode(json_encode(['name' => 'myvendor/second-plugin'])); + + Http::fake([ + 'api.github.com/repos/myvendor/second-plugin/contents/composer.json*' => Http::response([ + 'content' => $composerJson, + ]), + 'api.github.com/repos/myvendor/second-plugin/hooks' => Http::response(['id' => 1]), + 'api.github.com/*' => Http::response([], 404), + ]); + + Livewire::actingAs($user)->test(Create::class) + ->set('repository', 'myvendor/second-plugin') + ->set('pluginType', 'free') + ->set('supportChannel', 'support@myvendor.io') + ->call('submitPlugin'); + + $this->assertDatabaseHas('plugins', [ + 'repository_url' => 'https://github.com/myvendor/second-plugin', + 'user_id' => $user->id, + ]); + } + + public function test_submission_blocked_when_composer_json_missing(): void + { + $user = $this->createGitHubUser(); + + Http::fake([ + 'api.github.com/*' => Http::response([], 404), + ]); + + Livewire::actingAs($user)->test(Create::class) + ->set('repository', 'testuser/no-composer') + ->set('pluginType', 'free') + ->call('submitPlugin') + ->assertNoRedirect(); + + $this->assertDatabaseMissing('plugins', [ + 'repository_url' => 'https://github.com/testuser/no-composer', + ]); + } +} diff --git a/tests/Feature/Livewire/Customer/SettingsTest.php b/tests/Feature/Livewire/Customer/SettingsTest.php new file mode 100644 index 00000000..0cbef381 --- /dev/null +++ b/tests/Feature/Livewire/Customer/SettingsTest.php @@ -0,0 +1,378 @@ +create(); + + $response = $this->withoutVite()->actingAs($user)->get('/dashboard/settings'); + + $response->assertStatus(200); + } + + public function test_settings_page_requires_authentication(): void + { + $response = $this->withoutVite()->get('/dashboard/settings'); + + $response->assertRedirect('/login'); + } + + public function test_settings_component_renders_headings(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Settings::class) + ->assertSee('Settings') + ->assertSee('Name') + ->assertSee('Email Address') + ->assertSee('Password') + ->assertSee('Delete Account') + ->assertStatus(200); + } + + public function test_settings_page_displays_user_email(): void + { + $user = User::factory()->create(['email' => 'jane@example.com']); + + $response = $this->withoutVite()->actingAs($user)->get('/dashboard/settings'); + + $response->assertOk(); + $response->assertSee('jane@example.com'); + } + + // --- Update Name --- + + public function test_user_can_update_name(): void + { + $user = User::factory()->create(['name' => 'Old Name']); + + Livewire::actingAs($user) + ->test(Settings::class) + ->assertSet('name', 'Old Name') + ->set('name', 'New Name') + ->call('updateName') + ->assertHasNoErrors(); + + $this->assertDatabaseHas('users', [ + 'id' => $user->id, + 'name' => 'New Name', + ]); + } + + public function test_name_is_required(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Settings::class) + ->set('name', '') + ->call('updateName') + ->assertHasErrors(['name' => 'required']); + } + + public function test_name_has_max_length(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Settings::class) + ->set('name', str_repeat('a', 256)) + ->call('updateName') + ->assertHasErrors(['name' => 'max']); + } + + // --- Update Password --- + + public function test_user_can_update_password(): void + { + $user = User::factory()->create([ + 'password' => Hash::make('old-password'), + ]); + + Livewire::actingAs($user) + ->test(Settings::class) + ->set('currentPassword', 'old-password') + ->set('newPassword', 'new-password-123') + ->set('newPassword_confirmation', 'new-password-123') + ->call('updatePassword') + ->assertHasNoErrors() + ->assertSet('currentPassword', '') + ->assertSet('newPassword', '') + ->assertSet('newPassword_confirmation', ''); + + $this->assertTrue(Hash::check('new-password-123', $user->fresh()->password)); + } + + public function test_wrong_current_password_fails(): void + { + $user = User::factory()->create([ + 'password' => Hash::make('correct-password'), + ]); + + Livewire::actingAs($user) + ->test(Settings::class) + ->set('currentPassword', 'wrong-password') + ->set('newPassword', 'new-password-123') + ->set('newPassword_confirmation', 'new-password-123') + ->call('updatePassword') + ->assertHasErrors(['currentPassword']); + } + + public function test_password_confirmation_must_match(): void + { + $user = User::factory()->create([ + 'password' => Hash::make('correct-password'), + ]); + + Livewire::actingAs($user) + ->test(Settings::class) + ->set('currentPassword', 'correct-password') + ->set('newPassword', 'new-password-123') + ->set('newPassword_confirmation', 'different-password') + ->call('updatePassword') + ->assertHasErrors(['newPassword']); + } + + public function test_new_password_must_be_at_least_8_characters(): void + { + $user = User::factory()->create([ + 'password' => Hash::make('correct-password'), + ]); + + Livewire::actingAs($user) + ->test(Settings::class) + ->set('currentPassword', 'correct-password') + ->set('newPassword', 'short') + ->set('newPassword_confirmation', 'short') + ->call('updatePassword') + ->assertHasErrors(['newPassword' => 'min']); + } + + // --- Delete Account --- + + public function test_user_can_delete_account(): void + { + $user = User::factory()->create([ + 'password' => Hash::make('my-password'), + ]); + + Livewire::actingAs($user) + ->test(Settings::class) + ->set('deleteConfirmPassword', 'my-password') + ->call('deleteAccount') + ->assertHasNoErrors() + ->assertRedirect(route('welcome')); + + $this->assertDatabaseMissing('users', ['id' => $user->id]); + } + + public function test_delete_account_with_wrong_password_keeps_user(): void + { + $user = User::factory()->create([ + 'password' => Hash::make('my-password'), + ]); + + Livewire::actingAs($user) + ->test(Settings::class) + ->set('deleteConfirmPassword', 'wrong-password') + ->call('deleteAccount') + ->assertHasErrors(['deleteConfirmPassword']); + + $this->assertDatabaseHas('users', ['id' => $user->id]); + } + + public function test_delete_account_removes_licenses(): void + { + $user = User::factory()->create([ + 'password' => Hash::make('my-password'), + ]); + + License::factory()->count(2)->create(['user_id' => $user->id]); + + Livewire::actingAs($user) + ->test(Settings::class) + ->set('deleteConfirmPassword', 'my-password') + ->call('deleteAccount') + ->assertRedirect(route('welcome')); + + $this->assertDatabaseMissing('users', ['id' => $user->id]); + $this->assertDatabaseMissing('licenses', ['user_id' => $user->id]); + } + + // --- GitHub user password hint --- + + public function test_github_user_without_password_sees_hint(): void + { + $user = User::factory()->create(['github_id' => '12345']); + + // Bypass the hashed cast to set an empty password + DB::table('users') + ->where('id', $user->id) + ->update(['password' => '']); + + $user->refresh(); + + Livewire::actingAs($user) + ->test(Settings::class) + ->assertSee('Your account uses GitHub for authentication'); + } + + public function test_regular_user_does_not_see_github_hint(): void + { + $user = User::factory()->create([ + 'github_id' => null, + 'password' => Hash::make('password'), + ]); + + Livewire::actingAs($user) + ->test(Settings::class) + ->assertDontSee('Your account uses GitHub for authentication'); + } + + // --- Tabs --- + + public function test_settings_page_has_account_and_notifications_tabs(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Settings::class) + ->assertSee('Account') + ->assertSee('Notifications'); + } + + public function test_tab_defaults_to_account(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Settings::class) + ->assertSet('tab', 'account'); + } + + // --- Email notification preference --- + + public function test_email_notification_toggle_is_shown(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Settings::class) + ->set('tab', 'notifications') + ->assertSee('Email notifications') + ->assertSee('Receive email notifications about your account activity.'); + } + + public function test_email_notification_toggle_defaults_to_enabled(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Settings::class) + ->assertSet('receivesNotificationEmails', true); + } + + public function test_user_can_disable_email_notifications(): void + { + $user = User::factory()->create(['receives_notification_emails' => true]); + + Livewire::actingAs($user) + ->test(Settings::class) + ->set('tab', 'notifications') + ->assertSet('receivesNotificationEmails', true) + ->set('receivesNotificationEmails', false) + ->assertSet('receivesNotificationEmails', false); + + $this->assertFalse($user->fresh()->receives_notification_emails); + } + + public function test_user_can_enable_email_notifications(): void + { + $user = User::factory()->create(['receives_notification_emails' => false]); + + Livewire::actingAs($user) + ->test(Settings::class) + ->set('tab', 'notifications') + ->assertSet('receivesNotificationEmails', false) + ->set('receivesNotificationEmails', true) + ->assertSet('receivesNotificationEmails', true); + + $this->assertTrue($user->fresh()->receives_notification_emails); + } + + // --- New plugin notification preference --- + + public function test_new_plugin_notification_toggle_is_shown(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Settings::class) + ->set('tab', 'notifications') + ->assertSee('New plugin notifications') + ->assertSee('Get notified when new plugins are added to the directory.'); + } + + public function test_new_plugin_notification_toggle_defaults_to_enabled(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Settings::class) + ->assertSet('receivesNewPluginNotifications', true); + } + + public function test_user_can_disable_new_plugin_notifications(): void + { + $user = User::factory()->create(['receives_new_plugin_notifications' => true]); + + Livewire::actingAs($user) + ->test(Settings::class) + ->set('tab', 'notifications') + ->assertSet('receivesNewPluginNotifications', true) + ->set('receivesNewPluginNotifications', false) + ->assertSet('receivesNewPluginNotifications', false); + + $this->assertFalse($user->fresh()->receives_new_plugin_notifications); + } + + public function test_user_can_enable_new_plugin_notifications(): void + { + $user = User::factory()->create(['receives_new_plugin_notifications' => false]); + + Livewire::actingAs($user) + ->test(Settings::class) + ->set('tab', 'notifications') + ->assertSet('receivesNewPluginNotifications', false) + ->set('receivesNewPluginNotifications', true) + ->assertSet('receivesNewPluginNotifications', true); + + $this->assertTrue($user->fresh()->receives_new_plugin_notifications); + } +} diff --git a/tests/Feature/Livewire/Customer/Step1PagesTest.php b/tests/Feature/Livewire/Customer/Step1PagesTest.php new file mode 100644 index 00000000..0b5f8eba --- /dev/null +++ b/tests/Feature/Livewire/Customer/Step1PagesTest.php @@ -0,0 +1,176 @@ +create(); + + $response = $this->withoutVite()->actingAs($user)->get('/dashboard/integrations'); + + $response->assertStatus(200); + } + + public function test_integrations_page_requires_authentication(): void + { + $response = $this->withoutVite()->get('/dashboard/integrations'); + + $response->assertRedirect('/login'); + } + + public function test_integrations_component_renders_heading(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(Integrations::class) + ->assertSee('Integrations') + ->assertSee('Connect your accounts') + ->assertStatus(200); + } + + // --- Showcase Create --- + + public function test_showcase_create_page_renders_successfully(): void + { + $user = User::factory()->create(); + + $response = $this->withoutVite()->actingAs($user)->get('/dashboard/showcase/create'); + + $response->assertStatus(200); + } + + public function test_showcase_create_page_requires_authentication(): void + { + $response = $this->withoutVite()->get('/dashboard/showcase/create'); + + $response->assertRedirect('/login'); + } + + public function test_showcase_create_component_shows_guidelines(): void + { + $user = User::factory()->create(); + + Livewire::actingAs($user) + ->test(ShowcaseCreate::class) + ->assertSee('Submit Your App to the Showcase') + ->assertSee('Showcase Guidelines') + ->assertStatus(200); + } + + // --- Showcase Edit --- + + public function test_showcase_edit_page_renders_for_owner(): void + { + $user = User::factory()->create(); + $showcase = Showcase::factory()->create(['user_id' => $user->id]); + + $response = $this->withoutVite()->actingAs($user)->get("/dashboard/showcase/{$showcase->id}/edit"); + + $response->assertStatus(200); + } + + public function test_showcase_edit_page_returns_403_for_non_owner(): void + { + $user = User::factory()->create(); + $otherUser = User::factory()->create(); + $showcase = Showcase::factory()->create(['user_id' => $otherUser->id]); + + $response = $this->withoutVite()->actingAs($user)->get("/dashboard/showcase/{$showcase->id}/edit"); + + $response->assertStatus(403); + } + + public function test_showcase_edit_page_requires_authentication(): void + { + $showcase = Showcase::factory()->create(); + + $response = $this->withoutVite()->get("/dashboard/showcase/{$showcase->id}/edit"); + + $response->assertRedirect('/login'); + } + + public function test_showcase_edit_component_shows_status_badge(): void + { + $user = User::factory()->create(); + $showcase = Showcase::factory()->approved()->create(['user_id' => $user->id]); + + Livewire::actingAs($user) + ->test(ShowcaseEdit::class, ['showcase' => $showcase]) + ->assertSee('Edit Your Submission') + ->assertSee('Approved') + ->assertStatus(200); + } + + // --- Wall of Love Create --- + + public function test_wall_of_love_page_renders_for_eligible_user(): void + { + $user = User::factory()->create(); + License::factory()->create([ + 'user_id' => $user->id, + 'created_at' => '2025-01-15', + ]); + + $response = $this->withoutVite()->actingAs($user)->get('/dashboard/wall-of-love/create'); + + $response->assertStatus(200); + } + + public function test_wall_of_love_page_returns_404_for_ineligible_user(): void + { + $user = User::factory()->create(); + + $response = $this->withoutVite()->actingAs($user)->get('/dashboard/wall-of-love/create'); + + $response->assertStatus(404); + } + + public function test_wall_of_love_page_redirects_if_already_submitted(): void + { + $user = User::factory()->create(); + License::factory()->create([ + 'user_id' => $user->id, + 'created_at' => '2025-01-15', + ]); + WallOfLoveSubmission::factory()->create(['user_id' => $user->id]); + + Livewire::actingAs($user) + ->test(WallOfLoveCreate::class) + ->assertRedirect(route('dashboard')); + } + + public function test_wall_of_love_page_requires_authentication(): void + { + $response = $this->withoutVite()->get('/dashboard/wall-of-love/create'); + + $response->assertRedirect('/login'); + } +} diff --git a/tests/Feature/Livewire/OrderSuccessTest.php b/tests/Feature/Livewire/OrderSuccessTest.php new file mode 100644 index 00000000..86593e94 --- /dev/null +++ b/tests/Feature/Livewire/OrderSuccessTest.php @@ -0,0 +1,220 @@ +mockStripeClient(); + } + + #[Test] + public function it_renders_successfully() + { + $response = $this->withoutVite()->get('/order/cs_test_123'); + + $response->assertStatus(200); + } + + #[Test] + public function it_displays_loading_state_when_no_license_key_is_available() + { + Livewire::test(OrderSuccess::class, ['checkoutSessionId' => 'cs_test_123']) + ->assertSet('email', null) + ->assertSet('licenseKey', null) + ->assertSee('License registration in progress') + ->assertSee('check your email'); + } + + #[Test] + public function it_displays_license_key_when_available_in_database() + { + $user = User::factory()->create([ + 'email' => 'test@example.com', + 'stripe_id' => 'cus_test123', + ]); + + $subscription = Cashier::$subscriptionModel::factory() + ->for($user, 'user') + ->create([ + 'stripe_id' => 'sub_test123', + ]); + + $subscriptionItem = Cashier::$subscriptionItemModel::factory() + ->for($subscription, 'subscription') + ->create([ + 'stripe_id' => 'si_test123', + 'stripe_price' => Subscription::Max->stripePriceId(), + ]); + + $license = License::factory() + ->for($user, 'user') + ->for($subscriptionItem, 'subscriptionItem') + ->create([ + 'key' => 'db-license-key-12345', + 'policy_name' => 'max', + ]); + + Livewire::test(OrderSuccess::class, ['checkoutSessionId' => 'cs_test_123']) + ->assertSet('email', 'test@example.com') + ->assertSet('licenseKey', 'db-license-key-12345') + ->assertSee('db-license-key-12345') + ->assertSee('test@example.com') + ->assertDontSee('License registration in progress'); + } + + #[Test] + public function it_polls_for_updates_from_database() + { + $component = Livewire::test(OrderSuccess::class, ['checkoutSessionId' => 'cs_test_123']) + ->assertSet('licenseKey', null) + ->assertSee('License registration in progress') + ->assertSeeHtml('wire:poll.2s="loadData"'); + + $user = User::factory()->create([ + 'email' => 'test@example.com', + 'stripe_id' => 'cus_test123', + ]); + + $subscription = Cashier::$subscriptionModel::factory() + ->for($user, 'user') + ->create([ + 'stripe_id' => 'sub_test123', + ]); + + $subscriptionItem = Cashier::$subscriptionItemModel::factory() + ->for($subscription, 'subscription') + ->create([ + 'stripe_id' => 'si_test123', + 'stripe_price' => Subscription::Max->stripePriceId(), + ]); + + $license = License::factory() + ->for($user, 'user') + ->for($subscriptionItem, 'subscriptionItem') + ->create([ + 'key' => 'db-polled-license-key', + 'policy_name' => 'max', + ]); + + $component->call('loadData') + ->assertSet('licenseKey', 'db-polled-license-key') + ->assertSee('db-polled-license-key') + ->assertDontSee('License registration in progress'); + } + + #[Test] + public function it_redirects_to_mobile_route_when_checkout_session_is_not_found() + { + $mockStripeClient = $this->createMock(StripeClient::class); + + $mockStripeClient->checkout = new class {}; + + $mockStripeClient->checkout->sessions = new class + { + public function retrieve() + { + throw new InvalidRequestException('No such checkout.session'); + } + + public function allLineItems() + { + throw new InvalidRequestException('No such checkout.session'); + } + }; + + $this->app->bind(StripeClient::class, function ($app, $parameters) use ($mockStripeClient) { + return $mockStripeClient; + }); + + Livewire::test(OrderSuccess::class, ['checkoutSessionId' => 'not_a_real_checkout_session']) + ->assertRedirect('/mobile'); + } + + private function mockStripeClient(): void + { + $mockCheckoutSession = CheckoutSession::constructFrom([ + 'id' => 'cs_test_123', + 'customer' => 'cus_test123', + 'customer_details' => [ + 'email' => 'test@example.com', + ], + 'subscription' => 'sub_test123', + ]); + + $mockCheckoutSessionLineItems = Collection::constructFrom([ + 'object' => 'list', + 'data' => [ + LineItem::constructFrom([ + 'id' => 'li_1RFKPpAyFo6rlwXqAHI9wA95', + 'object' => 'item', + 'description' => 'Max', + 'price' => [ + 'id' => Subscription::Max->stripePriceId(), + 'object' => 'price', + 'product' => 'prod_S9Z5CgycbP7P4y', + ], + ]), + ], + ]); + + $mockStripeClient = $this->createMock(StripeClient::class); + + $mockStripeClient->checkout = new class($mockCheckoutSession) + { + private $mockCheckoutSession; + + public function __construct($mockCheckoutSession) + { + $this->mockCheckoutSession = $mockCheckoutSession; + } + }; + + $mockStripeClient->checkout->sessions = new class($mockCheckoutSession, $mockCheckoutSessionLineItems) + { + private $mockCheckoutSession; + + private $mockCheckoutSessionLineItems; + + public function __construct($mockCheckoutSession, $mockCheckoutSessionLineItems) + { + $this->mockCheckoutSession = $mockCheckoutSession; + $this->mockCheckoutSessionLineItems = $mockCheckoutSessionLineItems; + } + + public function retrieve() + { + return $this->mockCheckoutSession; + } + + public function allLineItems() + { + return $this->mockCheckoutSessionLineItems; + } + }; + + $this->app->bind(StripeClient::class, function ($app, $parameters) use ($mockStripeClient) { + return $mockStripeClient; + }); + } +} diff --git a/tests/Feature/Livewire/PurchaseModalTest.php b/tests/Feature/Livewire/PurchaseModalTest.php new file mode 100644 index 00000000..f1da549e --- /dev/null +++ b/tests/Feature/Livewire/PurchaseModalTest.php @@ -0,0 +1,73 @@ +call('setPlan', 'mini') + ->assertSet('selectedPlan', 'mini'); + } + + #[Test] + public function purchase_modal_can_be_closed() + { + Livewire::test(PurchaseModal::class) + ->set('showModal', true) + ->set('email', 'test@example.com') + ->set('selectedPlan', 'mini') + ->call('closeModal') + ->assertSet('showModal', false) + ->assertSet('email', '') + ->assertSet('selectedPlan', null); + } + + #[Test] + public function purchase_modal_validates_email() + { + Livewire::test(PurchaseModal::class) + ->set('email', 'invalid-email') + ->call('submit') + ->assertHasErrors(['email' => 'email']); + } + + #[Test] + public function purchase_modal_requires_email() + { + Livewire::test(PurchaseModal::class) + ->set('email', '') + ->call('submit') + ->assertHasErrors(['email' => 'required']); + } + + #[Test] + public function test_submit_action() + { + Livewire::test(PurchaseModal::class) + ->call('setPlan', 'mini') + ->set('email', 'valid@example.com') + ->call('submit') + ->assertDispatched('purchase-request-submitted', [ + 'email' => 'valid@example.com', + 'plan' => 'mini', + ]); + } + + #[Test] + public function purchase_modal_closes_after_emitting_event() + { + Livewire::test(PurchaseModal::class) + ->call('setPlan', 'mini') + ->set('email', 'valid@example.com') + ->call('submit') + ->assertSet('showModal', false); + } +} diff --git a/tests/Feature/MaxSubscriberPayoutTest.php b/tests/Feature/MaxSubscriberPayoutTest.php new file mode 100644 index 00000000..fd07266a --- /dev/null +++ b/tests/Feature/MaxSubscriberPayoutTest.php @@ -0,0 +1,193 @@ + self::MAX_PRICE_ID]); + } + + private function createStripeInvoice(string $cartId, string $customerId): Invoice + { + $invoice = Invoice::constructFrom([ + 'id' => 'in_test_'.uniqid(), + 'billing_reason' => Invoice::BILLING_REASON_MANUAL, + 'customer' => $customerId, + 'payment_intent' => 'pi_test_'.uniqid(), + 'currency' => 'usd', + 'metadata' => ['cart_id' => $cartId], + 'lines' => [], + ]); + + return $invoice; + } + + private function createSubscription(User $user, string $priceId): Subscription + { + return Subscription::factory() + ->for($user) + ->active() + ->create(['stripe_price' => $priceId]); + } + + #[Test] + public function max_subscriber_gets_zero_platform_fee_for_third_party_plugin(): void + { + $buyer = User::factory()->create(['stripe_id' => 'cus_test_buyer_'.uniqid()]); + $this->createSubscription($buyer, self::MAX_PRICE_ID); + + $developerAccount = DeveloperAccount::factory()->create(); + $plugin = Plugin::factory()->approved()->paid()->create([ + 'is_active' => true, + 'is_official' => false, + 'user_id' => $developerAccount->user_id, + 'developer_account_id' => $developerAccount->id, + ]); + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]); + + $cart = Cart::factory()->for($buyer)->create(); + CartItem::create([ + 'cart_id' => $cart->id, + 'plugin_id' => $plugin->id, + 'plugin_price_id' => $plugin->prices->first()->id, + 'price_at_addition' => 2999, + ]); + + $invoice = $this->createStripeInvoice($cart->id, $buyer->stripe_id); + $job = new HandleInvoicePaidJob($invoice); + $job->handle(); + + $payout = PluginPayout::first(); + $this->assertNotNull($payout); + $this->assertEquals(2999, $payout->gross_amount); + $this->assertEquals(0, $payout->platform_fee); + $this->assertEquals(2999, $payout->developer_amount); + $this->assertEquals(PayoutStatus::Pending, $payout->status); + } + + #[Test] + public function non_max_subscriber_gets_normal_platform_fee_for_third_party_plugin(): void + { + $buyer = User::factory()->create(['stripe_id' => 'cus_test_buyer_'.uniqid()]); + $this->createSubscription($buyer, self::PRO_PRICE_ID); + + $developerAccount = DeveloperAccount::factory()->create(); + $plugin = Plugin::factory()->approved()->paid()->create([ + 'is_active' => true, + 'is_official' => false, + 'user_id' => $developerAccount->user_id, + 'developer_account_id' => $developerAccount->id, + ]); + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]); + + $cart = Cart::factory()->for($buyer)->create(); + CartItem::create([ + 'cart_id' => $cart->id, + 'plugin_id' => $plugin->id, + 'plugin_price_id' => $plugin->prices->first()->id, + 'price_at_addition' => 2999, + ]); + + $invoice = $this->createStripeInvoice($cart->id, $buyer->stripe_id); + $job = new HandleInvoicePaidJob($invoice); + $job->handle(); + + $payout = PluginPayout::first(); + $this->assertNotNull($payout); + $this->assertEquals(2999, $payout->gross_amount); + $this->assertEquals(900, $payout->platform_fee); + $this->assertEquals(2099, $payout->developer_amount); + } + + #[Test] + public function non_subscriber_gets_normal_platform_fee_for_third_party_plugin(): void + { + $buyer = User::factory()->create(['stripe_id' => 'cus_test_buyer_'.uniqid()]); + + $developerAccount = DeveloperAccount::factory()->create(); + $plugin = Plugin::factory()->approved()->paid()->create([ + 'is_active' => true, + 'is_official' => false, + 'user_id' => $developerAccount->user_id, + 'developer_account_id' => $developerAccount->id, + ]); + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]); + + $cart = Cart::factory()->for($buyer)->create(); + CartItem::create([ + 'cart_id' => $cart->id, + 'plugin_id' => $plugin->id, + 'plugin_price_id' => $plugin->prices->first()->id, + 'price_at_addition' => 2999, + ]); + + $invoice = $this->createStripeInvoice($cart->id, $buyer->stripe_id); + $job = new HandleInvoicePaidJob($invoice); + $job->handle(); + + $payout = PluginPayout::first(); + $this->assertNotNull($payout); + $this->assertEquals(2999, $payout->gross_amount); + $this->assertEquals(900, $payout->platform_fee); + $this->assertEquals(2099, $payout->developer_amount); + } + + #[Test] + public function max_subscriber_gets_normal_platform_fee_for_official_plugin(): void + { + $buyer = User::factory()->create(['stripe_id' => 'cus_test_buyer_'.uniqid()]); + $this->createSubscription($buyer, self::MAX_PRICE_ID); + + $developerAccount = DeveloperAccount::factory()->create(); + $plugin = Plugin::factory()->approved()->paid()->create([ + 'is_active' => true, + 'is_official' => true, + 'user_id' => $developerAccount->user_id, + 'developer_account_id' => $developerAccount->id, + ]); + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]); + + $cart = Cart::factory()->for($buyer)->create(); + CartItem::create([ + 'cart_id' => $cart->id, + 'plugin_id' => $plugin->id, + 'plugin_price_id' => $plugin->prices->first()->id, + 'price_at_addition' => 2999, + ]); + + $invoice = $this->createStripeInvoice($cart->id, $buyer->stripe_id); + $job = new HandleInvoicePaidJob($invoice); + $job->handle(); + + $payout = PluginPayout::first(); + $this->assertNotNull($payout); + $this->assertEquals(2999, $payout->gross_amount); + $this->assertEquals(900, $payout->platform_fee); + $this->assertEquals(2099, $payout->developer_amount); + } +} diff --git a/tests/Feature/McpSecurityTest.php b/tests/Feature/McpSecurityTest.php new file mode 100644 index 00000000..9d7af19a --- /dev/null +++ b/tests/Feature/McpSecurityTest.php @@ -0,0 +1,71 @@ +getJson('/api/mcp/search?q=test&platform=..'); + + $response->assertStatus(422); + $response->assertJsonValidationErrors(['platform']); + } + + public function test_search_rejects_path_traversal_in_version(): void + { + $response = $this->getJson('/api/mcp/search?q=test&version=..'); + + $response->assertStatus(422); + $response->assertJsonValidationErrors(['version']); + } + + public function test_search_rejects_excessive_limit(): void + { + $response = $this->getJson('/api/mcp/search?q=test&limit=1200'); + + $response->assertStatus(422); + $response->assertJsonValidationErrors(['limit']); + } + + public function test_search_accepts_valid_parameters(): void + { + $response = $this->getJson('/api/mcp/search?q=camera&platform=mobile&version=2&limit=10'); + + $response->assertStatus(200); + $response->assertJsonStructure(['results']); + } + + public function test_search_allows_null_platform_and_version(): void + { + $response = $this->getJson('/api/mcp/search?q=camera'); + + $response->assertStatus(200); + $response->assertJsonStructure(['results']); + } + + public function test_page_api_rejects_path_traversal(): void + { + $response = $this->getJson('/api/mcp/page/../../../etc/passwd'); + + $response->assertStatus(404); + } + + public function test_apis_endpoint_rejects_invalid_platform(): void + { + $response = $this->getJson('/api/mcp/apis/../1'); + + $response->assertStatus(200); + $response->assertJson(['apis' => []]); + } + + public function test_navigation_endpoint_rejects_invalid_version(): void + { + $response = $this->getJson('/api/mcp/navigation/mobile/..'); + + $response->assertStatus(200); + $response->assertJson(['navigation' => []]); + } +} diff --git a/tests/Feature/MobilePricingTest.php b/tests/Feature/MobilePricingTest.php new file mode 100644 index 00000000..7a10d8b2 --- /dev/null +++ b/tests/Feature/MobilePricingTest.php @@ -0,0 +1,451 @@ + self::PRO_PRICE_ID, + 'subscriptions.plans.max.stripe_price_id' => self::MAX_PRICE_ID, + ]); + } + + #[Test] + public function authenticated_users_without_subscription_see_checkout_button() + { + $user = User::factory()->create(); + Auth::login($user); + + $component = Livewire::test(MobilePricing::class); + $component->assertSeeHtml([ + 'wire:click="createCheckoutSession(\'max\')"', + ]); + $component->assertDontSeeHtml([ + '@click="$dispatch(\'open-purchase-modal\', { plan: \'max\' })"', + ]); + } + + #[Test] + public function guest_users_see_purchase_modal_component() + { + Auth::logout(); + + Livewire::test(MobilePricing::class) + ->assertSeeLivewire('purchase-modal') + ->assertSeeHtml([ + '@click="$dispatch(\'open-purchase-modal\', { plan: \'max\' })"', + ]) + ->assertDontSeeHtml([ + 'wire:click="createCheckoutSession(\'max\')"', + ]); + } + + #[Test] + public function authenticated_users_do_not_see_purchase_modal_component() + { + Auth::login(User::factory()->create()); + + Livewire::test(MobilePricing::class) + ->assertDontSeeLivewire('purchase-modal'); + } + + #[Test] + public function it_validates_email_before_creating_user() + { + Livewire::test(MobilePricing::class) + ->call('handlePurchaseRequest', ['email' => 'invalid-email']) + ->assertHasErrors('email'); + } + + #[Test] + public function default_interval_is_month() + { + Livewire::test(MobilePricing::class) + ->assertSet('interval', 'month'); + } + + #[Test] + public function interval_can_be_set_to_year() + { + Livewire::test(MobilePricing::class) + ->set('interval', 'year') + ->assertSet('interval', 'year'); + } + + #[Test] + public function existing_subscriber_sees_upgrade_button() + { + $user = User::factory()->create(['stripe_id' => 'cus_'.uniqid()]); + Auth::login($user); + + $subscription = Cashier::$subscriptionModel::factory() + ->for($user) + ->active() + ->create(['stripe_price' => self::PRO_PRICE_ID]); + + Cashier::$subscriptionItemModel::factory() + ->for($subscription, 'subscription') + ->create(['stripe_price' => self::PRO_PRICE_ID]); + + Livewire::test(MobilePricing::class) + ->assertSee('Upgrade to Ultra') + ->assertDontSeeHtml('wire:click="createCheckoutSession(\'max\')"'); + } + + #[Test] + public function ultra_subscriber_sees_already_on_ultra_message() + { + $user = User::factory()->create(['stripe_id' => 'cus_'.uniqid()]); + Auth::login($user); + + $subscription = Cashier::$subscriptionModel::factory() + ->for($user) + ->active() + ->create([ + 'stripe_price' => self::MAX_PRICE_ID, + 'is_comped' => false, + ]); + + Cashier::$subscriptionItemModel::factory() + ->for($subscription, 'subscription') + ->create(['stripe_price' => self::MAX_PRICE_ID]); + + Livewire::test(MobilePricing::class) + ->assertSee('on Ultra', escape: false) + ->assertDontSee('Upgrade to Ultra') + ->assertDontSeeHtml('wire:click="createCheckoutSession(\'max\')"'); + } + + #[Test] + public function comped_max_subscriber_sees_checkout_button() + { + $user = User::factory()->create(['stripe_id' => 'cus_'.uniqid()]); + Auth::login($user); + + $subscription = Cashier::$subscriptionModel::factory() + ->for($user) + ->active() + ->create([ + 'stripe_price' => self::MAX_PRICE_ID, + 'is_comped' => true, + ]); + + Cashier::$subscriptionItemModel::factory() + ->for($subscription, 'subscription') + ->create(['stripe_price' => self::MAX_PRICE_ID]); + + Livewire::test(MobilePricing::class) + ->assertSeeHtml('wire:click="createCheckoutSession(\'max\')"') + ->assertDontSee('Upgrade to Ultra') + ->assertDontSee('on Ultra', escape: false); + } + + #[Test] + public function comped_ultra_price_subscriber_sees_checkout_button() + { + $compedPriceId = 'price_test_comped_ultra'; + + config([ + 'subscriptions.plans.max.stripe_price_id_comped' => $compedPriceId, + ]); + + $user = User::factory()->create(['stripe_id' => 'cus_'.uniqid()]); + Auth::login($user); + + $subscription = Cashier::$subscriptionModel::factory() + ->for($user) + ->active() + ->create([ + 'stripe_price' => $compedPriceId, + ]); + + Cashier::$subscriptionItemModel::factory() + ->for($subscription, 'subscription') + ->create(['stripe_price' => $compedPriceId]); + + Livewire::test(MobilePricing::class) + ->assertSeeHtml('wire:click="createCheckoutSession(\'max\')"') + ->assertDontSee('Upgrade to Ultra') + ->assertDontSee('on Ultra', escape: false); + } + + #[Test] + public function upgrade_modal_shows_confirm_button_for_existing_subscriber() + { + $user = User::factory()->create(['stripe_id' => 'cus_'.uniqid()]); + Auth::login($user); + + $subscription = Cashier::$subscriptionModel::factory() + ->for($user) + ->active() + ->create(['stripe_price' => self::PRO_PRICE_ID]); + + Cashier::$subscriptionItemModel::factory() + ->for($subscription, 'subscription') + ->create(['stripe_price' => self::PRO_PRICE_ID]); + + Livewire::test(MobilePricing::class) + ->assertSeeHtml('wire:click="upgradeSubscription"') + ->assertSee('Confirm upgrade'); + } + + #[Test] + public function upgrade_modal_not_shown_for_users_without_subscription() + { + $user = User::factory()->create(); + Auth::login($user); + + Livewire::test(MobilePricing::class) + ->assertDontSeeHtml('wire:click="upgradeSubscription"') + ->assertDontSee('Confirm upgrade'); + } + + #[Test] + public function eap_customer_sees_eap_offer_badge_on_annual_toggle() + { + $user = User::factory()->create(); + License::factory()->eapEligible()->withoutSubscriptionItem()->for($user)->create(); + Auth::login($user); + + Livewire::test(MobilePricing::class) + ->assertSee('EAP offer') + ->assertDontSee('Save 16%'); + } + + #[Test] + public function non_eap_customer_sees_save_badge_on_annual_toggle() + { + $user = User::factory()->create(); + Auth::login($user); + + Livewire::test(MobilePricing::class) + ->assertSee('Save 16%') + ->assertDontSee('EAP offer'); + } + + #[Test] + public function guest_sees_save_badge_on_annual_toggle() + { + Auth::logout(); + + Livewire::test(MobilePricing::class) + ->assertSee('Save 16%') + ->assertDontSee('EAP offer'); + } + + #[Test] + public function eap_customer_sees_strikethrough_and_discounted_price() + { + $user = User::factory()->create(); + License::factory()->eapEligible()->withoutSubscriptionItem()->for($user)->create(); + Auth::login($user); + + $eapPrice = config('subscriptions.plans.max.eap_price_yearly'); + $regularPrice = config('subscriptions.plans.max.price_yearly'); + $discount = (int) round((1 - $eapPrice / $regularPrice) * 100); + + Livewire::test(MobilePricing::class) + ->assertSee('$'.$regularPrice.'/yr') + ->assertSee($discount.'% off') + ->assertSee('Early Access discount'); + } + + #[Test] + public function non_eap_customer_does_not_see_eap_pricing() + { + $user = User::factory()->create(); + License::factory()->afterEap()->withoutSubscriptionItem()->for($user)->create(); + Auth::login($user); + + Livewire::test(MobilePricing::class) + ->assertDontSee('Early Access discount') + ->assertDontSee('EAP offer') + ->assertSee('Save 16%'); + } + + #[Test] + public function eap_upgrade_modal_shows_eap_discount_applied() + { + $user = User::factory()->create(['stripe_id' => 'cus_'.uniqid()]); + License::factory()->eapEligible()->withoutSubscriptionItem()->for($user)->create(); + Auth::login($user); + + $subscription = Cashier::$subscriptionModel::factory() + ->for($user) + ->active() + ->create(['stripe_price' => self::PRO_PRICE_ID]); + + Cashier::$subscriptionItemModel::factory() + ->for($subscription, 'subscription') + ->create(['stripe_price' => self::PRO_PRICE_ID]); + + Livewire::test(MobilePricing::class) + ->assertSee('EAP discount applied'); + } + + #[Test] + public function upgrade_button_triggers_preview_for_existing_subscriber() + { + $user = User::factory()->create(['stripe_id' => 'cus_'.uniqid()]); + Auth::login($user); + + $subscription = Cashier::$subscriptionModel::factory() + ->for($user) + ->active() + ->create(['stripe_price' => self::PRO_PRICE_ID]); + + Cashier::$subscriptionItemModel::factory() + ->for($subscription, 'subscription') + ->create(['stripe_price' => self::PRO_PRICE_ID]); + + Livewire::test(MobilePricing::class) + ->assertSeeHtml('wire:click="previewUpgrade"'); + } + + #[Test] + public function upgrade_modal_shows_proration_breakdown_when_preview_loaded() + { + $user = User::factory()->create(['stripe_id' => 'cus_'.uniqid()]); + Auth::login($user); + + $subscription = Cashier::$subscriptionModel::factory() + ->for($user) + ->active() + ->create(['stripe_price' => self::PRO_PRICE_ID]); + + Cashier::$subscriptionItemModel::factory() + ->for($subscription, 'subscription') + ->create(['stripe_price' => self::PRO_PRICE_ID]); + + Livewire::test(MobilePricing::class) + ->set('upgradePreview', [ + 'amount_due' => '$28.50', + 'raw_amount_due' => 2850, + 'new_charge' => '$35.00', + 'is_prorated' => false, + 'credit' => '$6.50', + 'remaining_credit' => null, + ]) + ->assertSee('Due today') + ->assertSee('$28.50') + ->assertSee('$6.50') + ->assertSee('$35.00') + ->assertSee('Credit for unused') + ->assertDontSee('pro-rated') + ->assertDontSee('credited to your next invoice'); + } + + #[Test] + public function upgrade_modal_shows_prorated_label_when_charge_is_prorated() + { + $user = User::factory()->create(['stripe_id' => 'cus_'.uniqid()]); + Auth::login($user); + + $subscription = Cashier::$subscriptionModel::factory() + ->for($user) + ->active() + ->create(['stripe_price' => self::PRO_PRICE_ID]); + + Cashier::$subscriptionItemModel::factory() + ->for($subscription, 'subscription') + ->create(['stripe_price' => self::PRO_PRICE_ID]); + + Livewire::test(MobilePricing::class) + ->set('upgradePreview', [ + 'amount_due' => '$200.00', + 'raw_amount_due' => 20000, + 'new_charge' => '$250.00', + 'is_prorated' => true, + 'credit' => '$50.00', + 'remaining_credit' => null, + ]) + ->assertSee('New plan (Ultra)') + ->assertSee('pro-rated') + ->assertSee('$250.00') + ->assertSee('Credit for unused') + ->assertSee('$50.00') + ->assertSee('$200.00') + ->assertDontSee('credited to your next invoice'); + } + + #[Test] + public function upgrade_modal_shows_remaining_credit_note_when_credit_exceeds_charge() + { + $user = User::factory()->create(['stripe_id' => 'cus_'.uniqid()]); + Auth::login($user); + + $subscription = Cashier::$subscriptionModel::factory() + ->for($user) + ->active() + ->create(['stripe_price' => self::PRO_PRICE_ID]); + + Cashier::$subscriptionItemModel::factory() + ->for($subscription, 'subscription') + ->create(['stripe_price' => self::PRO_PRICE_ID]); + + Livewire::test(MobilePricing::class) + ->set('upgradePreview', [ + 'amount_due' => '$0.00', + 'raw_amount_due' => 0, + 'new_charge' => '$35.00', + 'is_prorated' => false, + 'credit' => '$50.00', + 'remaining_credit' => '$15.00', + ]) + ->assertSee('$0.00') + ->assertSee('$35.00') + ->assertSee('$50.00') + ->assertSee('$15.00 will be credited to your next invoice'); + } + + #[Test] + public function upgrade_modal_shows_fallback_when_preview_is_null() + { + $user = User::factory()->create(['stripe_id' => 'cus_'.uniqid()]); + Auth::login($user); + + $subscription = Cashier::$subscriptionModel::factory() + ->for($user) + ->active() + ->create(['stripe_price' => self::PRO_PRICE_ID]); + + Cashier::$subscriptionItemModel::factory() + ->for($subscription, 'subscription') + ->create(['stripe_price' => self::PRO_PRICE_ID]); + + Livewire::test(MobilePricing::class) + ->set('upgradePreview', null) + ->assertSee('Unable to load pricing preview') + ->assertSee('Confirm upgrade'); + } + + #[Test] + public function non_subscriber_does_not_see_preview_upgrade_button() + { + $user = User::factory()->create(); + Auth::login($user); + + Livewire::test(MobilePricing::class) + ->assertDontSeeHtml('wire:click="previewUpgrade"'); + } +} diff --git a/tests/Feature/MobileRepoAccessTest.php b/tests/Feature/MobileRepoAccessTest.php new file mode 100644 index 00000000..d5508678 --- /dev/null +++ b/tests/Feature/MobileRepoAccessTest.php @@ -0,0 +1,235 @@ + self::MAX_PRICE_ID]); + + Cache::flush(); + } + + // ======================================== + // hasMobileRepoAccess() Unit Tests + // ======================================== + + public function test_user_with_active_max_license_has_mobile_repo_access(): void + { + $user = User::factory()->create(); + License::factory()->create([ + 'user_id' => $user->id, + 'policy_name' => 'max', + 'expires_at' => now()->addDays(30), + 'is_suspended' => false, + ]); + + $this->assertTrue($user->hasMobileRepoAccess()); + } + + public function test_ultra_subscriber_before_cutoff_has_mobile_repo_access(): void + { + $user = User::factory()->create(['stripe_id' => 'cus_'.uniqid()]); + + CashierSubscription::factory()->for($user)->active()->create([ + 'stripe_price' => self::MAX_PRICE_ID, + 'created_at' => '2026-01-15 00:00:00', + ]); + + $this->assertTrue($user->hasMobileRepoAccess()); + } + + public function test_ultra_subscriber_after_cutoff_does_not_have_mobile_repo_access(): void + { + $user = User::factory()->create(['stripe_id' => 'cus_'.uniqid()]); + + CashierSubscription::factory()->for($user)->active()->create([ + 'stripe_price' => self::MAX_PRICE_ID, + 'created_at' => '2026-02-01 00:00:00', + ]); + + $this->assertFalse($user->hasMobileRepoAccess()); + } + + public function test_ultra_subscriber_on_cutoff_date_does_not_have_mobile_repo_access(): void + { + $user = User::factory()->create(['stripe_id' => 'cus_'.uniqid()]); + + CashierSubscription::factory()->for($user)->active()->create([ + 'stripe_price' => self::MAX_PRICE_ID, + 'created_at' => '2026-02-01 12:00:00', + ]); + + $this->assertFalse($user->hasMobileRepoAccess()); + } + + public function test_user_without_license_or_subscription_does_not_have_mobile_repo_access(): void + { + $user = User::factory()->create(); + + $this->assertFalse($user->hasMobileRepoAccess()); + } + + public function test_user_with_inactive_subscription_does_not_have_mobile_repo_access(): void + { + $user = User::factory()->create(['stripe_id' => 'cus_'.uniqid()]); + + CashierSubscription::factory()->for($user)->canceled()->create([ + 'stripe_price' => self::MAX_PRICE_ID, + 'created_at' => '2025-12-01 00:00:00', + ]); + + $this->assertFalse($user->hasMobileRepoAccess()); + } + + // ======================================== + // Integrations Page Visibility Tests + // ======================================== + + public function test_ultra_subscriber_before_cutoff_sees_mobile_repo_banner(): void + { + Http::fake(['api.github.com/*' => Http::response([], 404)]); + + $user = User::factory()->create(['stripe_id' => 'cus_'.uniqid()]); + + CashierSubscription::factory()->for($user)->active()->create([ + 'stripe_price' => self::MAX_PRICE_ID, + 'created_at' => '2026-01-15 00:00:00', + ]); + + $response = $this->actingAs($user)->get('/dashboard/integrations'); + + $response->assertStatus(200); + $response->assertSee('nativephp/mobile'); + $response->assertSee('Repo Access'); + } + + public function test_ultra_subscriber_after_cutoff_does_not_see_mobile_repo_banner(): void + { + Http::fake(['api.github.com/*' => Http::response([], 404)]); + + $user = User::factory()->create(['stripe_id' => 'cus_'.uniqid()]); + + CashierSubscription::factory()->for($user)->active()->create([ + 'stripe_price' => self::MAX_PRICE_ID, + 'created_at' => '2026-02-15 00:00:00', + ]); + + $response = $this->actingAs($user)->get('/dashboard/integrations'); + + $response->assertStatus(200); + $response->assertDontSee('nativephp/mobile Repo Access', false); + } + + // ======================================== + // Request Access Controller Tests + // ======================================== + + public function test_ultra_subscriber_after_cutoff_cannot_request_repo_access(): void + { + $user = User::factory()->create([ + 'stripe_id' => 'cus_'.uniqid(), + 'github_username' => 'testuser', + 'github_id' => '123456', + ]); + + CashierSubscription::factory()->for($user)->active()->create([ + 'stripe_price' => self::MAX_PRICE_ID, + 'created_at' => '2026-02-15 00:00:00', + ]); + + $response = $this->actingAs($user) + ->post('/dashboard/github/request-access'); + + $response->assertRedirect(); + $response->assertSessionHas('error'); + } + + public function test_ultra_subscriber_before_cutoff_can_request_repo_access(): void + { + Http::fake([ + 'api.github.com/repos/nativephp/mobile/collaborators/testuser' => Http::response([], 201), + ]); + + $user = User::factory()->create([ + 'stripe_id' => 'cus_'.uniqid(), + 'github_username' => 'testuser', + 'github_id' => '123456', + ]); + + CashierSubscription::factory()->for($user)->active()->create([ + 'stripe_price' => self::MAX_PRICE_ID, + 'created_at' => '2026-01-15 00:00:00', + ]); + + $response = $this->actingAs($user) + ->post('/dashboard/github/request-access'); + + $response->assertRedirect(); + $response->assertSessionHas('success'); + $this->assertNotNull($user->fresh()->mobile_repo_access_granted_at); + } + + // ======================================== + // Cleanup Command Tests + // ======================================== + + public function test_cleanup_command_removes_access_for_post_cutoff_ultra_subscriber(): void + { + Http::fake([ + 'api.github.com/repos/nativephp/mobile/collaborators/testuser' => Http::response([], 204), + ]); + + $user = User::factory()->create([ + 'stripe_id' => 'cus_'.uniqid(), + 'github_username' => 'testuser', + 'github_id' => '123456', + 'mobile_repo_access_granted_at' => now()->subDays(10), + ]); + + CashierSubscription::factory()->for($user)->active()->create([ + 'stripe_price' => self::MAX_PRICE_ID, + 'created_at' => '2026-02-15 00:00:00', + ]); + + $this->artisan('github:remove-expired-access') + ->assertExitCode(0); + + $this->assertNull($user->fresh()->mobile_repo_access_granted_at); + } + + public function test_cleanup_command_retains_access_for_pre_cutoff_ultra_subscriber(): void + { + $user = User::factory()->create([ + 'stripe_id' => 'cus_'.uniqid(), + 'github_username' => 'testuser', + 'github_id' => '123456', + 'mobile_repo_access_granted_at' => now()->subDays(10), + ]); + + CashierSubscription::factory()->for($user)->active()->create([ + 'stripe_price' => self::MAX_PRICE_ID, + 'created_at' => '2026-01-15 00:00:00', + ]); + + $this->artisan('github:remove-expired-access') + ->assertExitCode(0); + + $this->assertNotNull($user->fresh()->mobile_repo_access_granted_at); + } +} diff --git a/tests/Feature/MobileRouteTest.php b/tests/Feature/MobileRouteTest.php new file mode 100644 index 00000000..d02166a7 --- /dev/null +++ b/tests/Feature/MobileRouteTest.php @@ -0,0 +1,31 @@ +withoutVite() + ->get(route('pricing')) + ->assertOk() + ->assertSeeLivewire('mobile-pricing'); + } +} diff --git a/tests/Feature/NavigationMobileMenuBreakpointTest.php b/tests/Feature/NavigationMobileMenuBreakpointTest.php new file mode 100644 index 00000000..6278a678 --- /dev/null +++ b/tests/Feature/NavigationMobileMenuBreakpointTest.php @@ -0,0 +1,32 @@ +get('/'); + + $response->assertStatus(200); + + // The mobile menu should not have xl:hidden — it's visible on all breakpoints + $response->assertDontSee('class="relative z-40 xl:hidden"', false); + $response->assertSee('class="relative z-40"', false); + } + + public function test_resize_handler_does_not_auto_close_mobile_menu(): void + { + $response = $this->get('/'); + + $response->assertStatus(200); + + // The old breakpoint-based auto-close should be removed + $response->assertDontSee("window.matchMedia('(min-width: 80rem)').matches", false); + } +} diff --git a/tests/Feature/Notifications/NewPluginAvailableTest.php b/tests/Feature/Notifications/NewPluginAvailableTest.php new file mode 100644 index 00000000..d6eb1b3c --- /dev/null +++ b/tests/Feature/Notifications/NewPluginAvailableTest.php @@ -0,0 +1,124 @@ +mock(PluginSyncService::class, function ($mock): void { + $mock->shouldReceive('sync')->andReturn(true); + }); + } + + public function test_notification_is_sent_to_opted_in_users_on_first_approval(): void + { + Notification::fake(); + + $author = User::factory()->create(); + $optedIn = User::factory()->create(['receives_new_plugin_notifications' => true]); + $optedOut = User::factory()->create(['receives_new_plugin_notifications' => false]); + + $plugin = Plugin::factory()->pending()->for($author)->create(); + $admin = User::factory()->create(); + + $plugin->approve($admin->id); + + Notification::assertSentTo($optedIn, NewPluginAvailable::class); + Notification::assertNotSentTo($optedOut, NewPluginAvailable::class); + Notification::assertNotSentTo($author, NewPluginAvailable::class); + } + + public function test_notification_is_not_sent_on_re_approval(): void + { + Notification::fake(); + + $author = User::factory()->create(); + $optedIn = User::factory()->create(['receives_new_plugin_notifications' => true]); + + $plugin = Plugin::factory()->pending()->for($author)->create([ + 'approved_at' => now()->subDay(), + ]); + $admin = User::factory()->create(); + + $plugin->approve($admin->id); + + Notification::assertNotSentTo($optedIn, NewPluginAvailable::class); + } + + public function test_via_returns_empty_array_when_user_opted_out(): void + { + $user = User::factory()->create(['receives_new_plugin_notifications' => false]); + $plugin = Plugin::factory()->for($user)->create(); + + $notification = new NewPluginAvailable($plugin); + + $this->assertEmpty($notification->via($user)); + } + + public function test_via_returns_mail_and_database_when_user_opted_in(): void + { + $user = User::factory()->create(['receives_new_plugin_notifications' => true]); + $plugin = Plugin::factory()->for($user)->create(); + + $notification = new NewPluginAvailable($plugin); + + $this->assertEquals(['mail', 'database'], $notification->via($user)); + } + + public function test_mail_contains_plugin_name(): void + { + $user = User::factory()->create(); + $plugin = Plugin::factory()->for($user)->create(['name' => 'acme/awesome-plugin']); + + $notification = new NewPluginAvailable($plugin); + $mail = $notification->toMail($user); + + $this->assertStringContainsString('acme/awesome-plugin', $mail->subject); + } + + public function test_mail_action_links_to_plugin_page(): void + { + $user = User::factory()->create(); + $plugin = Plugin::factory()->for($user)->create(['name' => 'acme/awesome-plugin']); + + $notification = new NewPluginAvailable($plugin); + $mail = $notification->toMail($user); + + $this->assertEquals(route('plugins.show', ['vendor' => 'acme', 'package' => 'awesome-plugin']), $mail->actionUrl); + } + + public function test_database_notification_contains_plugin_data(): void + { + $user = User::factory()->create(); + $plugin = Plugin::factory()->for($user)->create(['name' => 'acme/awesome-plugin']); + + $notification = new NewPluginAvailable($plugin); + $data = $notification->toArray($user); + + $this->assertEquals($plugin->id, $data['plugin_id']); + $this->assertEquals('acme/awesome-plugin', $data['plugin_name']); + $this->assertStringContainsString('acme/awesome-plugin', $data['title']); + $this->assertEquals(route('plugins.show', ['vendor' => 'acme', 'package' => 'awesome-plugin']), $data['action_url']); + $this->assertEquals('View Plugin', $data['action_label']); + } + + public function test_new_users_receive_new_plugin_notifications_by_default(): void + { + $user = User::factory()->create(); + + $this->assertTrue($user->receives_new_plugin_notifications); + } +} diff --git a/tests/Feature/Notifications/PluginApprovedTest.php b/tests/Feature/Notifications/PluginApprovedTest.php new file mode 100644 index 00000000..58d7d4be --- /dev/null +++ b/tests/Feature/Notifications/PluginApprovedTest.php @@ -0,0 +1,58 @@ +create(); + $plugin = Plugin::factory()->for($user)->create(['name' => 'acme/awesome-plugin']); + + $notification = new PluginApproved($plugin); + $mail = $notification->toMail($user); + + $this->assertEquals(route('plugins.show', ['vendor' => 'acme', 'package' => 'awesome-plugin']), $mail->actionUrl); + } + + public function test_database_notification_contains_action_url(): void + { + $user = User::factory()->create(); + $plugin = Plugin::factory()->for($user)->create(['name' => 'acme/awesome-plugin']); + + $notification = new PluginApproved($plugin); + $data = $notification->toArray($user); + + $this->assertEquals(route('plugins.show', ['vendor' => 'acme', 'package' => 'awesome-plugin']), $data['action_url']); + $this->assertEquals('View Plugin', $data['action_label']); + } + + public function test_mail_contains_plugin_name(): void + { + $user = User::factory()->create(); + $plugin = Plugin::factory()->for($user)->create(['name' => 'acme/awesome-plugin']); + + $notification = new PluginApproved($plugin); + $mail = $notification->toMail($user); + + $this->assertStringContainsString('acme/awesome-plugin', $mail->render()->toHtml()); + } + + public function test_via_returns_mail_and_database(): void + { + $user = User::factory()->create(); + $plugin = Plugin::factory()->for($user)->create(); + + $notification = new PluginApproved($plugin); + + $this->assertEquals(['mail', 'database'], $notification->via($user)); + } +} diff --git a/tests/Feature/Notifications/PluginRejectedTest.php b/tests/Feature/Notifications/PluginRejectedTest.php new file mode 100644 index 00000000..76b74e27 --- /dev/null +++ b/tests/Feature/Notifications/PluginRejectedTest.php @@ -0,0 +1,75 @@ +create(); + $plugin = Plugin::factory()->for($user)->create(['name' => 'acme/awesome-plugin']); + + $notification = new PluginRejected($plugin); + $mail = $notification->toMail($user); + + $this->assertEquals(route('customer.plugins.show', ['vendor' => 'acme', 'package' => 'awesome-plugin']), $mail->actionUrl); + } + + public function test_database_notification_contains_action_url(): void + { + $user = User::factory()->create(); + $plugin = Plugin::factory()->for($user)->create(['name' => 'acme/awesome-plugin']); + + $notification = new PluginRejected($plugin); + $data = $notification->toArray($user); + + $this->assertEquals(route('customer.plugins.show', ['vendor' => 'acme', 'package' => 'awesome-plugin']), $data['action_url']); + $this->assertEquals('View Plugin', $data['action_label']); + } + + public function test_mail_contains_rejection_reason(): void + { + $user = User::factory()->create(); + $plugin = Plugin::factory()->for($user)->create([ + 'name' => 'acme/awesome-plugin', + 'rejection_reason' => 'Missing license file', + ]); + + $notification = new PluginRejected($plugin); + $rendered = $notification->toMail($user)->render()->toHtml(); + + $this->assertStringContainsString('Missing license file', $rendered); + } + + public function test_database_notification_contains_rejection_reason(): void + { + $user = User::factory()->create(); + $plugin = Plugin::factory()->for($user)->create([ + 'name' => 'acme/awesome-plugin', + 'rejection_reason' => 'Missing license file', + ]); + + $notification = new PluginRejected($plugin); + $data = $notification->toArray($user); + + $this->assertEquals('Missing license file', $data['rejection_reason']); + } + + public function test_via_returns_mail_and_database(): void + { + $user = User::factory()->create(); + $plugin = Plugin::factory()->for($user)->create(); + + $notification = new PluginRejected($plugin); + + $this->assertEquals(['mail', 'database'], $notification->via($user)); + } +} diff --git a/tests/Feature/Notifications/PluginReviewChecksIncompleteTest.php b/tests/Feature/Notifications/PluginReviewChecksIncompleteTest.php new file mode 100644 index 00000000..432d709d --- /dev/null +++ b/tests/Feature/Notifications/PluginReviewChecksIncompleteTest.php @@ -0,0 +1,120 @@ +create(); + + $plugin = Plugin::factory()->create([ + 'user_id' => $user->id, + 'name' => 'acme/test-plugin', + 'webhook_installed' => true, + 'review_checks' => [ + 'has_license_file' => true, + 'has_release_version' => true, + 'release_version' => 'v1.0.0', + 'supports_ios' => true, + 'supports_android' => true, + 'supports_js' => false, + 'requires_mobile_sdk' => false, + 'mobile_sdk_constraint' => null, + 'has_ios_min_version' => true, + 'ios_min_version' => '18.0', + 'has_android_min_version' => false, + 'android_min_version' => null, + ], + ]); + + $notification = new PluginReviewChecksIncomplete($plugin); + $rendered = $notification->toMail($user)->render()->toHtml(); + + // Passing checks shown + $this->assertStringContainsString('License file', $rendered); + $this->assertStringContainsString('Release version', $rendered); + $this->assertStringContainsString('Webhook configured', $rendered); + $this->assertStringContainsString('iOS native code', $rendered); + $this->assertStringContainsString('Android native code', $rendered); + $this->assertStringContainsString('iOS min_version', $rendered); + + // Failing checks shown with doc links + $this->assertStringContainsString('JavaScript library', $rendered); + $this->assertStringContainsString('nativephp/mobile', $rendered); + $this->assertStringContainsString('Android', $rendered); + $this->assertStringContainsString('creating-plugins', $rendered); + $this->assertStringContainsString('advanced-configuration', $rendered); + } + + public function test_email_shows_all_failing_when_no_checks_pass(): void + { + $user = User::factory()->create(); + + $plugin = Plugin::factory()->create([ + 'user_id' => $user->id, + 'name' => 'acme/empty-plugin', + 'webhook_installed' => false, + 'review_checks' => [ + 'has_license_file' => false, + 'has_release_version' => false, + 'release_version' => null, + 'supports_ios' => false, + 'supports_android' => false, + 'supports_js' => false, + 'requires_mobile_sdk' => false, + 'mobile_sdk_constraint' => null, + 'has_ios_min_version' => false, + 'ios_min_version' => null, + 'has_android_min_version' => false, + 'android_min_version' => null, + ], + ]); + + $notification = new PluginReviewChecksIncomplete($plugin); + $rendered = $notification->toMail($user)->render()->toHtml(); + + $this->assertStringContainsString('bridge-functions', $rendered); + $this->assertStringContainsString('creating-plugins', $rendered); + $this->assertStringContainsString('best-practices', $rendered); + $this->assertStringContainsString('advanced-configuration', $rendered); + } + + public function test_email_subject_includes_plugin_name(): void + { + $user = User::factory()->create(); + + $plugin = Plugin::factory()->create([ + 'user_id' => $user->id, + 'name' => 'acme/cool-plugin', + 'webhook_installed' => true, + 'review_checks' => [ + 'has_license_file' => true, + 'has_release_version' => true, + 'release_version' => 'v2.0.0', + 'supports_ios' => true, + 'supports_android' => true, + 'supports_js' => true, + 'requires_mobile_sdk' => true, + 'mobile_sdk_constraint' => '^3.0', + 'has_ios_min_version' => true, + 'ios_min_version' => '18.0', + 'has_android_min_version' => true, + 'android_min_version' => '33', + ], + ]); + + $notification = new PluginReviewChecksIncomplete($plugin); + $mail = $notification->toMail($user); + + $this->assertEquals('Action Required: acme/cool-plugin — Review Checks', $mail->subject); + } +} diff --git a/tests/Feature/Notifications/PluginSaleCompletedTest.php b/tests/Feature/Notifications/PluginSaleCompletedTest.php new file mode 100644 index 00000000..4bda8c39 --- /dev/null +++ b/tests/Feature/Notifications/PluginSaleCompletedTest.php @@ -0,0 +1,196 @@ +create(); + $developerAccount = DeveloperAccount::factory() + ->withAcceptedTerms() + ->create(['user_id' => $developer->id]); + + $plugin = Plugin::factory()->paid()->create([ + 'user_id' => $developer->id, + 'developer_account_id' => $developerAccount->id, + 'name' => 'acme/camera-plugin', + ]); + + $license = PluginLicense::factory()->create([ + 'plugin_id' => $plugin->id, + 'stripe_invoice_id' => 'in_test_123', + 'price_paid' => 2900, + ]); + + $payout = PluginPayout::create([ + 'plugin_license_id' => $license->id, + 'developer_account_id' => $developerAccount->id, + 'gross_amount' => 2900, + 'platform_fee' => 870, + 'developer_amount' => 2030, + 'status' => PayoutStatus::Pending, + ]); + + $notification = new PluginSaleCompleted(collect([$payout->load('pluginLicense.plugin')])); + $rendered = $notification->toMail($developer)->render()->toHtml(); + + $this->assertStringContainsString('acme/camera-plugin', $rendered); + $this->assertStringContainsString('$20.30', $rendered); + $this->assertStringContainsString('Total payout: $20.30', $rendered); + + $mail = $notification->toMail($developer); + $this->assertEquals("You've made a sale!", $mail->subject); + } + + public function test_email_lists_multiple_plugins_with_correct_total(): void + { + $developer = User::factory()->create(); + $developerAccount = DeveloperAccount::factory() + ->withAcceptedTerms() + ->create(['user_id' => $developer->id]); + + $plugin1 = Plugin::factory()->paid()->create([ + 'user_id' => $developer->id, + 'developer_account_id' => $developerAccount->id, + 'name' => 'acme/camera-plugin', + ]); + + $plugin2 = Plugin::factory()->paid()->create([ + 'user_id' => $developer->id, + 'developer_account_id' => $developerAccount->id, + 'name' => 'acme/gps-plugin', + ]); + + $license1 = PluginLicense::factory()->create([ + 'plugin_id' => $plugin1->id, + 'stripe_invoice_id' => 'in_test_456', + 'price_paid' => 2900, + ]); + + $license2 = PluginLicense::factory()->create([ + 'plugin_id' => $plugin2->id, + 'stripe_invoice_id' => 'in_test_456', + 'price_paid' => 4900, + ]); + + $payout1 = PluginPayout::create([ + 'plugin_license_id' => $license1->id, + 'developer_account_id' => $developerAccount->id, + 'gross_amount' => 2900, + 'platform_fee' => 870, + 'developer_amount' => 2030, + 'status' => PayoutStatus::Pending, + ]); + + $payout2 = PluginPayout::create([ + 'plugin_license_id' => $license2->id, + 'developer_account_id' => $developerAccount->id, + 'gross_amount' => 4900, + 'platform_fee' => 1470, + 'developer_amount' => 3430, + 'status' => PayoutStatus::Pending, + ]); + + $payout1->load('pluginLicense.plugin'); + $payout2->load('pluginLicense.plugin'); + $payouts = collect([$payout1, $payout2]); + + $notification = new PluginSaleCompleted($payouts); + $rendered = $notification->toMail($developer)->render()->toHtml(); + + $this->assertStringContainsString('acme/camera-plugin', $rendered); + $this->assertStringContainsString('$20.30', $rendered); + $this->assertStringContainsString('acme/gps-plugin', $rendered); + $this->assertStringContainsString('$34.30', $rendered); + $this->assertStringContainsString('Total payout: $54.60', $rendered); + } + + public function test_email_does_not_contain_buyer_information(): void + { + $buyer = User::factory()->create([ + 'name' => 'BuyerFirstName BuyerLastName', + 'email' => 'buyer@example.com', + ]); + + $developer = User::factory()->create(); + $developerAccount = DeveloperAccount::factory() + ->withAcceptedTerms() + ->create(['user_id' => $developer->id]); + + $plugin = Plugin::factory()->paid()->create([ + 'user_id' => $developer->id, + 'developer_account_id' => $developerAccount->id, + 'name' => 'acme/test-plugin', + ]); + + $license = PluginLicense::factory()->create([ + 'user_id' => $buyer->id, + 'plugin_id' => $plugin->id, + 'stripe_invoice_id' => 'in_test_789', + 'price_paid' => 2900, + ]); + + $payout = PluginPayout::create([ + 'plugin_license_id' => $license->id, + 'developer_account_id' => $developerAccount->id, + 'gross_amount' => 2900, + 'platform_fee' => 870, + 'developer_amount' => 2030, + 'status' => PayoutStatus::Pending, + ]); + + $notification = new PluginSaleCompleted(collect([$payout->load('pluginLicense.plugin')])); + $rendered = $notification->toMail($developer)->render()->toHtml(); + + $this->assertStringNotContainsString('BuyerFirstName', $rendered); + $this->assertStringNotContainsString('BuyerLastName', $rendered); + $this->assertStringNotContainsString('buyer@example.com', $rendered); + } + + public function test_toarray_contains_payout_ids_and_total(): void + { + $developer = User::factory()->create(); + $developerAccount = DeveloperAccount::factory() + ->withAcceptedTerms() + ->create(['user_id' => $developer->id]); + + $plugin = Plugin::factory()->paid()->create([ + 'user_id' => $developer->id, + 'developer_account_id' => $developerAccount->id, + ]); + + $license = PluginLicense::factory()->create([ + 'plugin_id' => $plugin->id, + 'stripe_invoice_id' => 'in_test_arr', + 'price_paid' => 2900, + ]); + + $payout = PluginPayout::create([ + 'plugin_license_id' => $license->id, + 'developer_account_id' => $developerAccount->id, + 'gross_amount' => 2900, + 'platform_fee' => 870, + 'developer_amount' => 2030, + 'status' => PayoutStatus::Pending, + ]); + + $notification = new PluginSaleCompleted(collect([$payout])); + $array = $notification->toArray($developer); + + $this->assertEquals([$payout->id], $array['payout_ids']); + $this->assertEquals(2030, $array['total_developer_amount']); + } +} diff --git a/tests/Feature/OpenCollectiveWebhookTest.php b/tests/Feature/OpenCollectiveWebhookTest.php new file mode 100644 index 00000000..7c185c7c --- /dev/null +++ b/tests/Feature/OpenCollectiveWebhookTest.php @@ -0,0 +1,208 @@ +post('/opencollective/contribution'); + + $this->assertNotEquals(404, $response->getStatusCode()); + } + + #[Test] + public function opencollective_webhook_route_is_excluded_from_csrf_verification(): void + { + $reflection = new \ReflectionClass(VerifyCsrfToken::class); + $property = $reflection->getProperty('except'); + $exceptPaths = $property->getValue(resolve(VerifyCsrfToken::class)); + + $this->assertContains('opencollective/contribution', $exceptPaths); + } + + #[Test] + public function it_stores_donation_for_order_processed_webhook(): void + { + $payload = $this->getOrderProcessedPayload(); + + $response = $this->postJson('/opencollective/contribution', $payload); + + $response->assertStatus(200); + $response->assertJson(['status' => 'success']); + + $this->assertDatabaseHas('opencollective_donations', [ + 'webhook_id' => 335409, + 'order_id' => 51763, + 'order_idv2' => '88rzownx-l9e50pxj-z836ymvb-dgk7j43a', + 'amount' => 2000, + 'currency' => 'USD', + 'interval' => null, + 'from_collective_id' => 54797, + 'from_collective_name' => 'Testing User', + 'from_collective_slug' => 'sudharaka', + ]); + } + + #[Test] + public function it_does_not_store_duplicate_orders(): void + { + $payload = $this->getOrderProcessedPayload(); + + // First webhook + $this->postJson('/opencollective/contribution', $payload); + + // Second webhook with same order + $response = $this->postJson('/opencollective/contribution', $payload); + + $response->assertStatus(200); + $this->assertDatabaseCount('opencollective_donations', 1); + } + + #[Test] + public function it_handles_missing_order_id_gracefully(): void + { + $payload = [ + 'id' => 335409, + 'type' => 'order.processed', + 'data' => [ + 'order' => [], + ], + ]; + + $response = $this->postJson('/opencollective/contribution', $payload); + + $response->assertStatus(200); + $this->assertDatabaseCount('opencollective_donations', 0); + } + + #[Test] + public function it_verifies_webhook_signature_when_secret_is_configured(): void + { + Config::set('services.opencollective.webhook_secret', 'test-secret'); + + $payload = $this->getOrderProcessedPayload(); + + $response = $this->postJson('/opencollective/contribution', $payload); + + $response->assertStatus(401); + } + + #[Test] + public function it_accepts_valid_webhook_signature(): void + { + Config::set('services.opencollective.webhook_secret', 'test-secret'); + + $payload = $this->getOrderProcessedPayload(); + + $payloadJson = json_encode($payload); + $signature = hash_hmac('sha256', $payloadJson, 'test-secret'); + + $response = $this->postJson('/opencollective/contribution', $payload, [ + 'X-OpenCollective-Signature' => $signature, + ]); + + $response->assertStatus(200); + } + + #[Test] + public function it_handles_unhandled_webhook_types(): void + { + $payload = [ + 'id' => 12345, + 'type' => 'some.other.event', + 'data' => [], + ]; + + $response = $this->postJson('/opencollective/contribution', $payload); + + $response->assertStatus(200); + $response->assertJson(['status' => 'success']); + } + + #[Test] + public function donation_can_be_marked_as_claimed(): void + { + $donation = OpenCollectiveDonation::factory()->create(); + $user = User::factory()->create(); + + $this->assertFalse($donation->isClaimed()); + + $donation->markAsClaimed($user); + + $this->assertTrue($donation->fresh()->isClaimed()); + $this->assertEquals($user->id, $donation->fresh()->user_id); + $this->assertNotNull($donation->fresh()->claimed_at); + } + + protected function getOrderProcessedPayload(): array + { + return [ + 'createdAt' => '2025-12-04T16:20:34.260Z', + 'id' => 335409, + 'type' => 'order.processed', + 'CollectiveId' => 20206, + 'data' => [ + 'firstPayment' => true, + 'order' => [ + 'idV2' => '88rzownx-l9e50pxj-z836ymvb-dgk7j43a', + 'id' => 51763, + 'totalAmount' => 2000, + 'currency' => 'USD', + 'description' => 'Financial contribution to BackYourStack', + 'tags' => null, + 'interval' => null, + 'createdAt' => '2025-12-04T16:20:31.861Z', + 'quantity' => 1, + 'FromCollectiveId' => 54797, + 'TierId' => null, + 'formattedAmount' => '$20.00', + 'formattedAmountWithInterval' => '$20.00', + ], + 'host' => [ + 'idV2' => '8a47byg9-nxozdp80-xm6mjlv0-3rek5w8k', + 'id' => 11004, + 'type' => 'ORGANIZATION', + 'slug' => 'opensource', + 'name' => 'Open Source Collective', + ], + 'collective' => [ + 'idV2' => 'rvedj9wr-oz3a56d3-d35p7blg-8x4m0ykn', + 'id' => 20206, + 'type' => 'COLLECTIVE', + 'slug' => 'backyourstack', + 'name' => 'BackYourStack', + ], + 'fromCollective' => [ + 'idV2' => 'eeng0kzd-yvor4pz7-37gqbma8-37xlw95j', + 'id' => 54797, + 'type' => 'USER', + 'slug' => 'sudharaka', + 'name' => 'Testing User', + 'twitterHandle' => null, + 'githubHandle' => 'SudharakaP', + 'repositoryUrl' => 'https://github.com/test', + ], + ], + ]; + } +} diff --git a/tests/Feature/PluginDirectoryTest.php b/tests/Feature/PluginDirectoryTest.php new file mode 100644 index 00000000..ed9f60ea --- /dev/null +++ b/tests/Feature/PluginDirectoryTest.php @@ -0,0 +1,34 @@ +approved()->count(13)->create(); + + Livewire::test(PluginDirectory::class) + ->assertViewHas('plugins', function ($plugins) { + return $plugins->count() === 12 + && $plugins->lastPage() === 2; + }); + } +} diff --git a/tests/Feature/PluginIsOfficialTest.php b/tests/Feature/PluginIsOfficialTest.php new file mode 100644 index 00000000..f1a8e4f0 --- /dev/null +++ b/tests/Feature/PluginIsOfficialTest.php @@ -0,0 +1,53 @@ +create(['name' => 'nativephp/mobile-camera']); + + $this->assertTrue($plugin->fresh()->is_official); + } + + #[Test] + public function plugin_with_other_namespace_is_not_marked_as_official(): void + { + $plugin = Plugin::factory()->create(['name' => 'acme/some-plugin']); + + $this->assertFalse($plugin->fresh()->is_official); + } + + #[Test] + public function updating_plugin_name_to_nativephp_namespace_sets_official(): void + { + $plugin = Plugin::factory()->create(['name' => 'acme/some-plugin']); + + $this->assertFalse($plugin->fresh()->is_official); + + $plugin->update(['name' => 'nativephp/some-plugin']); + + $this->assertTrue($plugin->fresh()->is_official); + } + + #[Test] + public function updating_plugin_name_away_from_nativephp_namespace_unsets_official(): void + { + $plugin = Plugin::factory()->create(['name' => 'nativephp/some-plugin']); + + $this->assertTrue($plugin->fresh()->is_official); + + $plugin->update(['name' => 'acme/some-plugin']); + + $this->assertFalse($plugin->fresh()->is_official); + } +} diff --git a/tests/Feature/PluginShowMobileVersionTest.php b/tests/Feature/PluginShowMobileVersionTest.php new file mode 100644 index 00000000..c28408ba --- /dev/null +++ b/tests/Feature/PluginShowMobileVersionTest.php @@ -0,0 +1,44 @@ +approved()->create([ + 'mobile_min_version' => '^3.0.0', + ]); + + $this->get(route('plugins.show', $plugin->routeParams())) + ->assertStatus(200) + ->assertSee('NativePHP Mobile') + ->assertSee('^3.0.0'); + } + + public function test_plugin_show_displays_dash_when_mobile_min_version_is_null(): void + { + $plugin = Plugin::factory()->approved()->create([ + 'mobile_min_version' => null, + ]); + + $this->get(route('plugins.show', $plugin->routeParams())) + ->assertStatus(200) + ->assertSee('NativePHP Mobile'); + } +} diff --git a/tests/Feature/PluginSubmissionNotesTest.php b/tests/Feature/PluginSubmissionNotesTest.php new file mode 100644 index 00000000..d01ebfa6 --- /dev/null +++ b/tests/Feature/PluginSubmissionNotesTest.php @@ -0,0 +1,236 @@ + $extraComposerData + */ + private function fakeGitHubForPlugin(string $repoSlug, array $extraComposerData = []): void + { + $base = "https://api.github.com/repos/{$repoSlug}"; + $composerJson = json_encode(array_merge([ + 'name' => $repoSlug, + 'description' => "A test plugin: {$repoSlug}", + 'require' => [ + 'php' => '^8.1', + 'nativephp/mobile' => '^3.0.0', + ], + ], $extraComposerData)); + + Http::fake([ + "{$base}/contents/README.md*" => Http::response([ + 'content' => base64_encode("# {$repoSlug}"), + 'encoding' => 'base64', + ]), + "{$base}/contents/composer.json*" => Http::response([ + 'content' => base64_encode($composerJson), + 'encoding' => 'base64', + ]), + "{$base}/contents/nativephp.json*" => Http::response([], 404), + "{$base}/contents/LICENSE*" => Http::response([], 404), + "{$base}/releases/latest" => Http::response([], 404), + "{$base}/tags*" => Http::response([]), + "{$base}/hooks" => Http::response(['id' => 1]), + "https://raw.githubusercontent.com/{$repoSlug}/*" => Http::response('', 404), + $base => Http::response(['default_branch' => 'main']), + "{$base}/git/trees/main*" => Http::response([ + 'tree' => [ + ['path' => 'src/ServiceProvider.php', 'type' => 'blob'], + ], + ]), + "{$base}/readme" => Http::response([ + 'content' => base64_encode("# {$repoSlug}"), + 'encoding' => 'base64', + ]), + ]); + } + + private function createUserWithGitHub(): User + { + $user = User::factory()->create([ + 'github_id' => '12345', + 'github_token' => encrypt('fake-token'), + ]); + DeveloperAccount::factory()->withAcceptedTerms()->create([ + 'user_id' => $user->id, + ]); + + return $user; + } + + /** @test */ + public function submitting_a_plugin_saves_notes(): void + { + Notification::fake(); + $user = $this->createUserWithGitHub(); + $repoSlug = 'acme/notes-plugin'; + $this->fakeGitHubForPlugin($repoSlug); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('repository', $repoSlug) + ->set('pluginType', 'free') + ->set('supportChannel', 'help@example.com') + ->set('notes', 'Please review this quickly, we have a launch deadline.') + ->call('submitPlugin') + ->assertRedirect(); + + $plugin = $user->plugins()->where('repository_url', "https://github.com/{$repoSlug}")->first(); + + $this->assertNotNull($plugin); + $this->assertEquals('Please review this quickly, we have a launch deadline.', $plugin->notes); + } + + /** @test */ + public function submitting_a_plugin_without_notes_stores_null(): void + { + Notification::fake(); + $user = $this->createUserWithGitHub(); + $repoSlug = 'acme/no-notes-plugin'; + $this->fakeGitHubForPlugin($repoSlug); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('repository', $repoSlug) + ->set('pluginType', 'free') + ->set('supportChannel', 'help@example.com') + ->call('submitPlugin') + ->assertRedirect(); + + $plugin = $user->plugins()->where('repository_url', "https://github.com/{$repoSlug}")->first(); + + $this->assertNotNull($plugin); + $this->assertNull($plugin->notes); + } + + /** @test */ + public function submitting_a_plugin_saves_support_channel_email(): void + { + Notification::fake(); + $user = $this->createUserWithGitHub(); + $repoSlug = 'acme/support-email-plugin'; + $this->fakeGitHubForPlugin($repoSlug); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('repository', $repoSlug) + ->set('pluginType', 'free') + ->set('supportChannel', 'help@example.com') + ->call('submitPlugin') + ->assertRedirect(); + + $plugin = $user->plugins()->where('repository_url', "https://github.com/{$repoSlug}")->first(); + + $this->assertNotNull($plugin); + $this->assertEquals('help@example.com', $plugin->support_channel); + } + + /** @test */ + public function submitting_a_plugin_saves_support_channel_url(): void + { + Notification::fake(); + $user = $this->createUserWithGitHub(); + $repoSlug = 'acme/support-url-plugin'; + $this->fakeGitHubForPlugin($repoSlug); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('repository', $repoSlug) + ->set('pluginType', 'free') + ->set('supportChannel', 'https://example.com/support') + ->call('submitPlugin') + ->assertRedirect(); + + $plugin = $user->plugins()->where('repository_url', "https://github.com/{$repoSlug}")->first(); + + $this->assertNotNull($plugin); + $this->assertEquals('https://example.com/support', $plugin->support_channel); + } + + /** @test */ + public function submitting_a_plugin_without_support_channel_fails_validation(): void + { + Notification::fake(); + $user = $this->createUserWithGitHub(); + $repoSlug = 'acme/no-support-plugin'; + $this->fakeGitHubForPlugin($repoSlug); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('repository', $repoSlug) + ->set('pluginType', 'free') + ->call('submitPlugin') + ->assertHasErrors(['supportChannel' => 'required']); + + $plugin = $user->plugins()->where('repository_url', "https://github.com/{$repoSlug}")->first(); + + $this->assertNull($plugin); + } + + /** @test */ + public function submitting_a_plugin_with_invalid_support_channel_fails_validation(): void + { + Notification::fake(); + $user = $this->createUserWithGitHub(); + $repoSlug = 'acme/invalid-support-plugin'; + $this->fakeGitHubForPlugin($repoSlug); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('repository', $repoSlug) + ->set('pluginType', 'free') + ->set('supportChannel', 'not-an-email-or-url') + ->call('submitPlugin') + ->assertHasErrors('supportChannel'); + + $plugin = $user->plugins()->where('repository_url', "https://github.com/{$repoSlug}")->first(); + + $this->assertNull($plugin); + } + + /** @test */ + public function notes_and_support_channel_fields_are_hidden_until_repository_selected(): void + { + $user = User::factory()->create([ + 'github_id' => '12345', + ]); + + Livewire::actingAs($user) + ->test(Create::class) + ->assertDontSee('Support Channel') + ->assertDontSee('Any notes for the review team') + ->set('repository', 'acme/my-plugin') + ->assertSee('Support Channel') + ->assertSee('Notes'); + } + + /** @test */ + public function plugin_factory_with_notes_state_works(): void + { + $plugin = Plugin::factory()->withNotes('Custom note')->create(); + + $this->assertEquals('Custom note', $plugin->notes); + } + + /** @test */ + public function plugin_factory_with_support_channel_state_works(): void + { + $plugin = Plugin::factory()->withSupportChannel('support@myplugin.dev')->create(); + + $this->assertEquals('support@myplugin.dev', $plugin->support_channel); + } +} diff --git a/tests/Feature/PluginSyncServiceTest.php b/tests/Feature/PluginSyncServiceTest.php new file mode 100644 index 00000000..8c24b11e --- /dev/null +++ b/tests/Feature/PluginSyncServiceTest.php @@ -0,0 +1,80 @@ + 'acme/test-plugin', + 'require' => [ + 'nativephp/mobile' => '^3.0.0', + ], + ]); + + Http::fake([ + 'api.github.com/repos/acme/test-plugin/contents/composer.json' => Http::response([ + 'content' => base64_encode($composerJson), + ]), + 'api.github.com/repos/acme/test-plugin/contents/nativephp.json' => Http::response([], 404), + 'raw.githubusercontent.com/*' => Http::response('', 404), + 'api.github.com/repos/acme/test-plugin/releases/latest' => Http::response([], 404), + 'api.github.com/repos/acme/test-plugin/tags*' => Http::response([]), + 'api.github.com/repos/acme/test-plugin/contents/LICENSE*' => Http::response([], 404), + ]); + + $plugin = Plugin::factory()->create([ + 'name' => 'acme/test-plugin', + 'repository_url' => 'https://github.com/acme/test-plugin', + 'mobile_min_version' => null, + ]); + + $service = new PluginSyncService; + $result = $service->sync($plugin); + + $this->assertTrue($result); + $this->assertEquals('^3.0.0', $plugin->fresh()->mobile_min_version); + } + + public function test_sync_sets_mobile_min_version_to_null_when_not_in_composer_data(): void + { + $composerJson = json_encode([ + 'name' => 'acme/test-plugin', + 'require' => [ + 'php' => '^8.2', + ], + ]); + + Http::fake([ + 'api.github.com/repos/acme/test-plugin/contents/composer.json' => Http::response([ + 'content' => base64_encode($composerJson), + ]), + 'api.github.com/repos/acme/test-plugin/contents/nativephp.json' => Http::response([], 404), + 'raw.githubusercontent.com/*' => Http::response('', 404), + 'api.github.com/repos/acme/test-plugin/releases/latest' => Http::response([], 404), + 'api.github.com/repos/acme/test-plugin/tags*' => Http::response([]), + 'api.github.com/repos/acme/test-plugin/contents/LICENSE*' => Http::response([], 404), + ]); + + $plugin = Plugin::factory()->create([ + 'name' => 'acme/test-plugin', + 'repository_url' => 'https://github.com/acme/test-plugin', + 'mobile_min_version' => '^2.0.0', + ]); + + $service = new PluginSyncService; + $result = $service->sync($plugin); + + $this->assertTrue($result); + $this->assertNull($plugin->fresh()->mobile_min_version); + } +} diff --git a/tests/Feature/PluginTableOfContentsTest.php b/tests/Feature/PluginTableOfContentsTest.php new file mode 100644 index 00000000..73566de5 --- /dev/null +++ b/tests/Feature/PluginTableOfContentsTest.php @@ -0,0 +1,43 @@ +approved()->create([ + 'readme_html' => '

Installation

Steps here.

Usage

More content.

', + ]); + + $this->get(route('plugins.show', $plugin->routeParams())) + ->assertStatus(200) + ->assertSee('On this page'); + } + + public function test_toc_component_not_rendered_when_no_readme(): void + { + $plugin = Plugin::factory()->approved()->create([ + 'readme_html' => null, + ]); + + $this->get(route('plugins.show', $plugin->routeParams())) + ->assertStatus(200) + ->assertDontSee('On this page'); + } +} diff --git a/tests/Feature/ProductPageTest.php b/tests/Feature/ProductPageTest.php new file mode 100644 index 00000000..090dfbcd --- /dev/null +++ b/tests/Feature/ProductPageTest.php @@ -0,0 +1,87 @@ +first(); + + $this + ->get(route('products.show', $product)) + ->assertRedirect(route('course')); + } + + #[Test] + public function non_masterclass_product_page_loads_normally(): void + { + $product = Product::factory()->active()->create([ + 'slug' => 'plugin-dev-kit', + ]); + + ProductPrice::factory()->for($product)->create(); + + $this + ->withoutVite() + ->get(route('products.show', $product)) + ->assertStatus(200) + ->assertSee($product->name); + } + + #[Test] + public function course_page_shows_purchase_form_for_guests(): void + { + $this + ->withoutVite() + ->get(route('course')) + ->assertStatus(200) + ->assertSee('Get Early Bird Access') + ->assertDontSee('You Own This Course'); + } + + #[Test] + public function course_page_shows_purchase_form_for_users_without_purchase(): void + { + $user = User::factory()->create(); + + $this + ->withoutVite() + ->actingAs($user) + ->get(route('course')) + ->assertStatus(200) + ->assertSee('Get Early Bird Access') + ->assertDontSee('You Own This Course'); + } + + #[Test] + public function course_page_shows_owned_state_for_purchasers(): void + { + $user = User::factory()->create(); + $product = Product::where('slug', 'nativephp-masterclass')->first(); + + ProductLicense::factory()->create([ + 'user_id' => $user->id, + 'product_id' => $product->id, + ]); + + $this + ->withoutVite() + ->actingAs($user) + ->get(route('course')) + ->assertStatus(200) + ->assertSee('You Own This Course') + ->assertDontSee('Get Early Bird Access'); + } +} diff --git a/tests/Feature/PurchaseHistoryTest.php b/tests/Feature/PurchaseHistoryTest.php new file mode 100644 index 00000000..a0213a0b --- /dev/null +++ b/tests/Feature/PurchaseHistoryTest.php @@ -0,0 +1,124 @@ +get('/dashboard/purchase-history'); + + $response->assertRedirect('/login'); + } + + public function test_purchase_history_shows_product_licenses(): void + { + $user = User::factory()->create(); + $product = Product::factory()->create(['name' => 'Plugin Dev Kit']); + + ProductLicense::factory()->create([ + 'user_id' => $user->id, + 'product_id' => $product->id, + 'price_paid' => 4900, + 'currency' => 'USD', + 'purchased_at' => now()->subDay(), + ]); + + $response = $this->actingAs($user)->get('/dashboard/purchase-history'); + + $response->assertStatus(200); + $response->assertSee('Plugin Dev Kit'); + $response->assertSee('$49.00'); + } + + public function test_purchase_history_shows_multiple_product_types(): void + { + $user = User::factory()->create(); + + $devKit = Product::factory()->create(['name' => 'Plugin Dev Kit']); + $masterclass = Product::factory()->create(['name' => 'The NativePHP Masterclass']); + + ProductLicense::factory()->create([ + 'user_id' => $user->id, + 'product_id' => $devKit->id, + 'price_paid' => 4900, + 'purchased_at' => now()->subDays(2), + ]); + + ProductLicense::factory()->create([ + 'user_id' => $user->id, + 'product_id' => $masterclass->id, + 'price_paid' => 10100, + 'purchased_at' => now()->subDay(), + ]); + + $response = $this->actingAs($user)->get('/dashboard/purchase-history'); + + $response->assertStatus(200); + $response->assertSee('Plugin Dev Kit'); + $response->assertSee('The NativePHP Masterclass'); + } + + public function test_dashboard_total_purchases_includes_product_licenses(): void + { + $user = User::factory()->create(); + $product = Product::factory()->create(); + + ProductLicense::factory()->create([ + 'user_id' => $user->id, + 'product_id' => $product->id, + ]); + + Livewire::actingAs($user) + ->test(Dashboard::class) + ->assertOk(); + + $this->assertEquals(1, $user->productLicenses()->count()); + } + + public function test_purchase_history_shows_empty_state_when_no_purchases(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->get('/dashboard/purchase-history'); + + $response->assertStatus(200); + $response->assertSee('No purchases yet'); + } + + public function test_product_purchases_show_on_page(): void + { + $user = User::factory()->create(); + $product = Product::factory()->create(['name' => 'Plugin Dev Kit']); + + ProductLicense::factory()->create([ + 'user_id' => $user->id, + 'product_id' => $product->id, + 'purchased_at' => now(), + ]); + + $response = $this->actingAs($user)->get('/dashboard/purchase-history'); + + $response->assertStatus(200); + $response->assertSee('Plugin Dev Kit'); + } +} diff --git a/tests/Feature/ResendLeadNotificationsTest.php b/tests/Feature/ResendLeadNotificationsTest.php new file mode 100644 index 00000000..934d6fde --- /dev/null +++ b/tests/Feature/ResendLeadNotificationsTest.php @@ -0,0 +1,60 @@ +create(['created_at' => '2025-01-01 12:00:00']); + $matchingLead = Lead::factory()->create(['created_at' => '2025-03-15 09:00:00']); + $newerLead = Lead::factory()->create(['created_at' => '2025-03-16 14:00:00']); + + $this->artisan('app:resend-lead-notifications', ['date' => '2025-03-15']) + ->expectsOutputToContain('Found 2 lead(s)') + ->expectsOutputToContain('Done.') + ->assertExitCode(0); + + Notification::assertSentOnDemand( + NewLeadSubmitted::class, + function ($notification, $channels, $notifiable) use ($matchingLead) { + return $notifiable->routes['mail'] === 'sales@nativephp.com' + && $notification->lead->is($matchingLead); + } + ); + + Notification::assertSentOnDemand( + NewLeadSubmitted::class, + function ($notification, $channels, $notifiable) use ($newerLead) { + return $notifiable->routes['mail'] === 'sales@nativephp.com' + && $notification->lead->is($newerLead); + } + ); + + Notification::assertNotSentTo($oldLead, NewLeadSubmitted::class); + } + + #[Test] + public function it_shows_message_when_no_leads_are_found(): void + { + Notification::fake(); + + $this->artisan('app:resend-lead-notifications', ['date' => '2099-01-01']) + ->expectsOutputToContain('No leads found') + ->assertExitCode(0); + + Notification::assertNothingSent(); + } +} diff --git a/tests/Feature/SatisSync/SatisSyncTest.php b/tests/Feature/SatisSync/SatisSyncTest.php new file mode 100644 index 00000000..f3d99360 --- /dev/null +++ b/tests/Feature/SatisSync/SatisSyncTest.php @@ -0,0 +1,153 @@ +admin = User::factory()->create(['email' => 'admin@test.com']); + config(['filament.users' => ['admin@test.com']]); + } + + public function test_approval_does_not_dispatch_sync_plugin_releases(): void + { + Bus::fake([SyncPluginReleases::class]); + + $plugin = Plugin::factory()->paid()->pending()->create(); + + $plugin->approve($this->admin->id); + + Bus::assertNotDispatched(SyncPluginReleases::class); + } + + public function test_filament_sync_to_satis_action_dispatches_job(): void + { + Bus::fake([SyncPluginReleases::class]); + + $plugin = Plugin::factory()->paid()->approved()->create(); + + Livewire::actingAs($this->admin) + ->test(EditPlugin::class, ['record' => $plugin->getRouteKey()]) + ->callAction('syncToSatis') + ->assertNotified(); + + Bus::assertDispatched(SyncPluginReleases::class, function ($job) use ($plugin) { + return $job->plugin->is($plugin); + }); + } + + public function test_sync_to_satis_action_hidden_for_free_plugins(): void + { + $plugin = Plugin::factory()->free()->approved()->create(); + + Livewire::actingAs($this->admin) + ->test(EditPlugin::class, ['record' => $plugin->getRouteKey()]) + ->assertActionHidden('syncToSatis'); + } + + public function test_sync_to_satis_action_visible_for_paid_plugins(): void + { + $plugin = Plugin::factory()->paid()->approved()->create(); + + Livewire::actingAs($this->admin) + ->test(EditPlugin::class, ['record' => $plugin->getRouteKey()]) + ->assertActionVisible('syncToSatis'); + } + + public function test_satis_synced_at_is_stamped_after_successful_build(): void + { + $plugin = Plugin::factory()->paid()->approved()->create(); + + $this->assertNull($plugin->satis_synced_at); + + $satisService = $this->mock(SatisService::class); + $satisService->shouldReceive('build') + ->once() + ->andReturn(['success' => true, 'job_id' => 'test-123']); + + $job = new SyncPluginReleases($plugin, triggerSatisBuild: true); + $job->handle($satisService); + + $plugin->refresh(); + + $this->assertNotNull($plugin->satis_synced_at); + } + + public function test_satis_synced_at_is_not_stamped_after_failed_build(): void + { + $plugin = Plugin::factory()->paid()->approved()->create(); + + $this->assertNull($plugin->satis_synced_at); + + $satisService = $this->mock(SatisService::class); + $satisService->shouldReceive('build') + ->once() + ->andReturn(['success' => false, 'error' => 'Build failed']); + + $job = new SyncPluginReleases($plugin, triggerSatisBuild: true); + $job->handle($satisService); + + $plugin->refresh(); + + $this->assertNull($plugin->satis_synced_at); + } + + public function test_is_satis_synced_returns_false_when_never_synced(): void + { + $plugin = Plugin::factory()->paid()->create(); + + $this->assertFalse($plugin->isSatisSynced()); + } + + public function test_is_satis_synced_returns_true_when_synced(): void + { + $plugin = Plugin::factory()->paid()->create([ + 'satis_synced_at' => now(), + ]); + + $this->assertTrue($plugin->isSatisSynced()); + } + + public function test_build_all_only_includes_paid_plugins(): void + { + Http::fake(['*' => Http::response(['job_id' => 'test-123', 'message' => 'Build started'], 200)]); + + config(['services.satis.url' => 'https://satis.test', 'services.satis.api_key' => 'test-key']); + + $paidPlugin = Plugin::factory()->paid()->approved()->create(); + Plugin::factory()->free()->approved()->create(); + + $service = new SatisService; + $result = $service->buildAll(); + + $this->assertTrue($result['success']); + $this->assertEquals(1, $result['plugins_count']); + + Http::assertSent(function ($request) use ($paidPlugin) { + $data = $request->data(); + $plugins = $data['plugins'] ?? []; + + return count($plugins) === 1 + && $plugins[0]['name'] === $paidPlugin->name + && $data['full_build'] === true; + }); + } +} diff --git a/tests/Feature/SendMaxToUltraAnnouncementTest.php b/tests/Feature/SendMaxToUltraAnnouncementTest.php new file mode 100644 index 00000000..293fd282 --- /dev/null +++ b/tests/Feature/SendMaxToUltraAnnouncementTest.php @@ -0,0 +1,255 @@ + self::COMPED_ULTRA_PRICE_ID]); + } + + private function createPaidMaxSubscription(User $user, ?string $priceId = null): \Laravel\Cashier\Subscription + { + $user->update(['stripe_id' => 'cus_'.uniqid()]); + + $price = $priceId ?? Subscription::Max->stripePriceId(); + + $subscription = \Laravel\Cashier\Subscription::factory() + ->for($user) + ->active() + ->create([ + 'stripe_price' => $price, + 'is_comped' => false, + ]); + + SubscriptionItem::factory() + ->for($subscription, 'subscription') + ->create([ + 'stripe_price' => $price, + 'quantity' => 1, + ]); + + return $subscription; + } + + private function createCompedMaxSubscription(User $user): \Laravel\Cashier\Subscription + { + $user->update(['stripe_id' => 'cus_'.uniqid()]); + + $subscription = \Laravel\Cashier\Subscription::factory() + ->for($user) + ->active() + ->create([ + 'stripe_price' => Subscription::Max->stripePriceId(), + 'is_comped' => true, + ]); + + SubscriptionItem::factory() + ->for($subscription, 'subscription') + ->create([ + 'stripe_price' => Subscription::Max->stripePriceId(), + 'quantity' => 1, + ]); + + return $subscription; + } + + private function createProSubscription(User $user): \Laravel\Cashier\Subscription + { + $user->update(['stripe_id' => 'cus_'.uniqid()]); + + $subscription = \Laravel\Cashier\Subscription::factory() + ->for($user) + ->active() + ->create([ + 'stripe_price' => Subscription::Pro->stripePriceId(), + 'is_comped' => false, + ]); + + SubscriptionItem::factory() + ->for($subscription, 'subscription') + ->create([ + 'stripe_price' => Subscription::Pro->stripePriceId(), + 'quantity' => 1, + ]); + + return $subscription; + } + + private function createCompedUltraSubscription(User $user): \Laravel\Cashier\Subscription + { + $user->update(['stripe_id' => 'cus_'.uniqid()]); + + $subscription = \Laravel\Cashier\Subscription::factory() + ->for($user) + ->active() + ->create([ + 'stripe_price' => self::COMPED_ULTRA_PRICE_ID, + ]); + + SubscriptionItem::factory() + ->for($subscription, 'subscription') + ->create([ + 'stripe_price' => self::COMPED_ULTRA_PRICE_ID, + 'quantity' => 1, + ]); + + return $subscription; + } + + public function test_sends_to_paying_max_subscriber(): void + { + Notification::fake(); + + $user = User::factory()->create(); + $this->createPaidMaxSubscription($user); + + $this->artisan('ultra:send-announcement') + ->expectsOutputToContain('Found 1 paying Max subscriber(s)') + ->expectsOutputToContain('Sent: 1 email(s)') + ->assertSuccessful(); + + Notification::assertSentTo($user, MaxToUltraAnnouncement::class); + } + + public function test_sends_to_monthly_max_subscriber(): void + { + Notification::fake(); + + $monthlyPriceId = config('subscriptions.plans.max.stripe_price_id_monthly'); + + if (! $monthlyPriceId) { + $this->markTestSkipped('Monthly Max price ID not configured'); + } + + $user = User::factory()->create(); + $this->createPaidMaxSubscription($user, $monthlyPriceId); + + $this->artisan('ultra:send-announcement') + ->expectsOutputToContain('Sent: 1 email(s)') + ->assertSuccessful(); + + Notification::assertSentTo($user, MaxToUltraAnnouncement::class); + } + + public function test_skips_comped_max_subscriber(): void + { + Notification::fake(); + + $user = User::factory()->create(); + $this->createCompedMaxSubscription($user); + + $this->artisan('ultra:send-announcement') + ->expectsOutputToContain('Found 0 paying Max subscriber(s)') + ->assertSuccessful(); + + Notification::assertNotSentTo($user, MaxToUltraAnnouncement::class); + } + + public function test_skips_comped_ultra_subscriber(): void + { + Notification::fake(); + + $user = User::factory()->create(); + $this->createCompedUltraSubscription($user); + + $this->artisan('ultra:send-announcement') + ->expectsOutputToContain('Found 0 paying Max subscriber(s)') + ->assertSuccessful(); + + Notification::assertNotSentTo($user, MaxToUltraAnnouncement::class); + } + + public function test_skips_pro_subscriber(): void + { + Notification::fake(); + + $user = User::factory()->create(); + $this->createProSubscription($user); + + $this->artisan('ultra:send-announcement') + ->expectsOutputToContain('Found 0 paying Max subscriber(s)') + ->assertSuccessful(); + + Notification::assertNotSentTo($user, MaxToUltraAnnouncement::class); + } + + public function test_dry_run_does_not_send(): void + { + Notification::fake(); + + $user = User::factory()->create(); + $this->createPaidMaxSubscription($user); + + $this->artisan('ultra:send-announcement --dry-run') + ->expectsOutputToContain('DRY RUN') + ->expectsOutputToContain("Would send to: {$user->email}") + ->expectsOutputToContain('Would send: 1 email(s)') + ->assertSuccessful(); + + Notification::assertNotSentTo($user, MaxToUltraAnnouncement::class); + } + + public function test_notification_has_correct_subject(): void + { + $user = User::factory()->create(['name' => 'Jane Doe']); + + $notification = new MaxToUltraAnnouncement; + $mail = $notification->toMail($user); + + $this->assertEquals('Your Max Plan is Now NativePHP Ultra', $mail->subject); + } + + public function test_notification_greeting_uses_first_name(): void + { + $user = User::factory()->create(['name' => 'Jane Doe']); + + $notification = new MaxToUltraAnnouncement; + $mail = $notification->toMail($user); + + $this->assertEquals('Hi Jane,', $mail->greeting); + } + + public function test_notification_greeting_fallback_when_no_name(): void + { + $user = User::factory()->create(['name' => null]); + + $notification = new MaxToUltraAnnouncement; + $mail = $notification->toMail($user); + + $this->assertEquals('Hi there,', $mail->greeting); + } + + public function test_notification_contains_ultra_benefits(): void + { + $user = User::factory()->create(['name' => 'Test']); + + $notification = new MaxToUltraAnnouncement; + $mail = $notification->toMail($user); + + $rendered = $mail->render()->__toString(); + + $this->assertStringContainsString('Teams', $rendered); + $this->assertStringContainsString('Free official plugins', $rendered); + $this->assertStringContainsString('Plugin Dev Kit', $rendered); + $this->assertStringContainsString('Priority support', $rendered); + $this->assertStringContainsString('Early access', $rendered); + $this->assertStringContainsString('Exclusive content', $rendered); + $this->assertStringContainsString('Shape the roadmap', $rendered); + } +} diff --git a/tests/Feature/SendUltraLicenseHolderPromotionTest.php b/tests/Feature/SendUltraLicenseHolderPromotionTest.php new file mode 100644 index 00000000..50d4155a --- /dev/null +++ b/tests/Feature/SendUltraLicenseHolderPromotionTest.php @@ -0,0 +1,267 @@ +for($user) + ->withoutSubscriptionItem() + ->state(['policy_name' => $policyName]) + ->create(); + } + + private function createActiveSubscription(User $user, string $priceId): \Laravel\Cashier\Subscription + { + $user->update(['stripe_id' => 'cus_'.uniqid()]); + + $subscription = \Laravel\Cashier\Subscription::factory() + ->for($user) + ->active() + ->create([ + 'stripe_price' => $priceId, + 'is_comped' => false, + ]); + + SubscriptionItem::factory() + ->for($subscription, 'subscription') + ->create([ + 'stripe_price' => $priceId, + 'quantity' => 1, + ]); + + return $subscription; + } + + public function test_sends_to_legacy_mini_license_holder(): void + { + Notification::fake(); + + $user = User::factory()->create(); + $this->createLegacyLicense($user, 'mini'); + + $this->artisan('ultra:send-license-holder-promo') + ->expectsOutputToContain('Found 1 eligible license holder(s)') + ->expectsOutputToContain('Sent: 1 email(s)') + ->assertSuccessful(); + + Notification::assertSentTo($user, UltraLicenseHolderPromotion::class); + } + + public function test_sends_to_legacy_pro_license_holder(): void + { + Notification::fake(); + + $user = User::factory()->create(); + $this->createLegacyLicense($user, 'pro'); + + $this->artisan('ultra:send-license-holder-promo') + ->expectsOutputToContain('Found 1 eligible license holder(s)') + ->assertSuccessful(); + + Notification::assertSentTo($user, UltraLicenseHolderPromotion::class); + } + + public function test_sends_to_legacy_max_license_holder(): void + { + Notification::fake(); + + $user = User::factory()->create(); + $this->createLegacyLicense($user, 'max'); + + $this->artisan('ultra:send-license-holder-promo') + ->expectsOutputToContain('Found 1 eligible license holder(s)') + ->assertSuccessful(); + + Notification::assertSentTo($user, UltraLicenseHolderPromotion::class); + } + + public function test_skips_user_with_active_subscription(): void + { + Notification::fake(); + + $user = User::factory()->create(); + $this->createLegacyLicense($user, 'pro'); + $this->createActiveSubscription($user, Subscription::Pro->stripePriceId()); + + $this->artisan('ultra:send-license-holder-promo') + ->expectsOutputToContain("Skipping {$user->email} - already has active subscription") + ->expectsOutputToContain('Found 0 eligible license holder(s)') + ->assertSuccessful(); + + Notification::assertNotSentTo($user, UltraLicenseHolderPromotion::class); + } + + public function test_skips_license_tied_to_subscription_item(): void + { + Notification::fake(); + + $user = User::factory()->create(['stripe_id' => 'cus_'.uniqid()]); + + // License with a subscription_item_id (not legacy) + License::factory() + ->for($user) + ->state(['policy_name' => 'pro']) + ->create(); + + $this->artisan('ultra:send-license-holder-promo') + ->expectsOutputToContain('Found 0 eligible license holder(s)') + ->assertSuccessful(); + + Notification::assertNotSentTo($user, UltraLicenseHolderPromotion::class); + } + + public function test_sends_only_one_email_per_user_with_multiple_licenses(): void + { + Notification::fake(); + + $user = User::factory()->create(); + $this->createLegacyLicense($user, 'mini'); + $this->createLegacyLicense($user, 'pro'); + + $this->artisan('ultra:send-license-holder-promo') + ->expectsOutputToContain('Found 1 eligible license holder(s)') + ->expectsOutputToContain('Sent: 1 email(s)') + ->assertSuccessful(); + + Notification::assertSentToTimes($user, UltraLicenseHolderPromotion::class, 1); + } + + public function test_dry_run_does_not_send(): void + { + Notification::fake(); + + $user = User::factory()->create(); + $this->createLegacyLicense($user, 'mini'); + + $this->artisan('ultra:send-license-holder-promo --dry-run') + ->expectsOutputToContain('DRY RUN') + ->expectsOutputToContain("Would send to: {$user->email}") + ->expectsOutputToContain('Would send: 1 email(s)') + ->assertSuccessful(); + + Notification::assertNotSentTo($user, UltraLicenseHolderPromotion::class); + } + + public function test_notification_has_correct_subject(): void + { + $user = User::factory()->create(['name' => 'Jane Doe']); + + $notification = new UltraLicenseHolderPromotion('Mini'); + $mail = $notification->toMail($user); + + $this->assertEquals('Unlock More with NativePHP Ultra', $mail->subject); + } + + public function test_notification_greeting_uses_first_name(): void + { + $user = User::factory()->create(['name' => 'Jane Doe']); + + $notification = new UltraLicenseHolderPromotion('Mini'); + $mail = $notification->toMail($user); + + $this->assertEquals('Hi Jane,', $mail->greeting); + } + + public function test_notification_greeting_fallback_when_no_name(): void + { + $user = User::factory()->create(['name' => null]); + + $notification = new UltraLicenseHolderPromotion('Mini'); + $mail = $notification->toMail($user); + + $this->assertEquals('Hi there,', $mail->greeting); + } + + public function test_notification_contains_plan_name(): void + { + $user = User::factory()->create(['name' => 'Test']); + + $notification = new UltraLicenseHolderPromotion('Pro'); + $mail = $notification->toMail($user); + + $rendered = $mail->render()->__toString(); + + $this->assertStringContainsString('Pro', $rendered); + } + + public function test_notification_contains_ultra_benefits(): void + { + $user = User::factory()->create(['name' => 'Test']); + + $notification = new UltraLicenseHolderPromotion('Mini'); + $mail = $notification->toMail($user); + + $rendered = $mail->render()->__toString(); + + $this->assertStringContainsString('Teams', $rendered); + $this->assertStringContainsString('Free official plugins', $rendered); + $this->assertStringContainsString('Plugin Dev Kit', $rendered); + $this->assertStringContainsString('Priority support', $rendered); + $this->assertStringContainsString('Early access', $rendered); + $this->assertStringContainsString('Exclusive content', $rendered); + $this->assertStringContainsString('Shape the roadmap', $rendered); + $this->assertStringContainsString('monthly billing', $rendered); + } + + public function test_personalizes_plan_name_for_mini(): void + { + Notification::fake(); + + $user = User::factory()->create(); + $this->createLegacyLicense($user, 'mini'); + + $this->artisan('ultra:send-license-holder-promo') + ->expectsOutputToContain('(Mini)') + ->assertSuccessful(); + + Notification::assertSentTo($user, UltraLicenseHolderPromotion::class, function ($notification) { + return $notification->planName === 'Mini'; + }); + } + + public function test_personalizes_plan_name_for_pro(): void + { + Notification::fake(); + + $user = User::factory()->create(); + $this->createLegacyLicense($user, 'pro'); + + $this->artisan('ultra:send-license-holder-promo') + ->expectsOutputToContain('(Pro)') + ->assertSuccessful(); + + Notification::assertSentTo($user, UltraLicenseHolderPromotion::class, function ($notification) { + return $notification->planName === 'Pro'; + }); + } + + public function test_personalizes_plan_name_for_max(): void + { + Notification::fake(); + + $user = User::factory()->create(); + $this->createLegacyLicense($user, 'max'); + + $this->artisan('ultra:send-license-holder-promo') + ->expectsOutputToContain('(Ultra)') + ->assertSuccessful(); + + Notification::assertSentTo($user, UltraLicenseHolderPromotion::class, function ($notification) { + return $notification->planName === 'Ultra'; + }); + } +} diff --git a/tests/Feature/SendUltraUpgradePromotionTest.php b/tests/Feature/SendUltraUpgradePromotionTest.php new file mode 100644 index 00000000..05a406b9 --- /dev/null +++ b/tests/Feature/SendUltraUpgradePromotionTest.php @@ -0,0 +1,254 @@ + self::COMPED_ULTRA_PRICE_ID]); + } + + private function createSubscription(User $user, string $priceId, bool $isComped = false): \Laravel\Cashier\Subscription + { + $user->update(['stripe_id' => 'cus_'.uniqid()]); + + $subscription = \Laravel\Cashier\Subscription::factory() + ->for($user) + ->active() + ->create([ + 'stripe_price' => $priceId, + 'is_comped' => $isComped, + ]); + + SubscriptionItem::factory() + ->for($subscription, 'subscription') + ->create([ + 'stripe_price' => $priceId, + 'quantity' => 1, + ]); + + return $subscription; + } + + public function test_sends_to_mini_subscriber(): void + { + Notification::fake(); + + $user = User::factory()->create(); + $this->createSubscription($user, Subscription::Mini->stripePriceId()); + + $this->artisan('ultra:send-upgrade-promo') + ->expectsOutputToContain('Found 1 eligible subscriber(s)') + ->expectsOutputToContain('Sent: 1 email(s)') + ->assertSuccessful(); + + Notification::assertSentTo($user, UltraUpgradePromotion::class); + } + + public function test_sends_to_pro_subscriber(): void + { + Notification::fake(); + + $user = User::factory()->create(); + $this->createSubscription($user, Subscription::Pro->stripePriceId()); + + $this->artisan('ultra:send-upgrade-promo') + ->expectsOutputToContain('Found 1 eligible subscriber(s)') + ->expectsOutputToContain('Sent: 1 email(s)') + ->assertSuccessful(); + + Notification::assertSentTo($user, UltraUpgradePromotion::class); + } + + public function test_skips_max_subscriber(): void + { + Notification::fake(); + + $user = User::factory()->create(); + $this->createSubscription($user, Subscription::Max->stripePriceId()); + + $this->artisan('ultra:send-upgrade-promo') + ->expectsOutputToContain('Found 0 eligible subscriber(s)') + ->assertSuccessful(); + + Notification::assertNotSentTo($user, UltraUpgradePromotion::class); + } + + public function test_skips_comped_ultra_subscriber(): void + { + Notification::fake(); + + $user = User::factory()->create(); + $this->createSubscription($user, self::COMPED_ULTRA_PRICE_ID); + + $this->artisan('ultra:send-upgrade-promo') + ->expectsOutputToContain('Found 0 eligible subscriber(s)') + ->assertSuccessful(); + + Notification::assertNotSentTo($user, UltraUpgradePromotion::class); + } + + public function test_skips_comped_mini_subscriber(): void + { + Notification::fake(); + + $user = User::factory()->create(); + $this->createSubscription($user, Subscription::Mini->stripePriceId(), isComped: true); + + $this->artisan('ultra:send-upgrade-promo') + ->expectsOutputToContain('Found 0 eligible subscriber(s)') + ->assertSuccessful(); + + Notification::assertNotSentTo($user, UltraUpgradePromotion::class); + } + + public function test_skips_comped_pro_subscriber(): void + { + Notification::fake(); + + $user = User::factory()->create(); + $this->createSubscription($user, Subscription::Pro->stripePriceId(), isComped: true); + + $this->artisan('ultra:send-upgrade-promo') + ->expectsOutputToContain('Found 0 eligible subscriber(s)') + ->assertSuccessful(); + + Notification::assertNotSentTo($user, UltraUpgradePromotion::class); + } + + public function test_dry_run_does_not_send(): void + { + Notification::fake(); + + $user = User::factory()->create(); + $this->createSubscription($user, Subscription::Mini->stripePriceId()); + + $this->artisan('ultra:send-upgrade-promo --dry-run') + ->expectsOutputToContain('DRY RUN') + ->expectsOutputToContain("Would send to: {$user->email}") + ->expectsOutputToContain('Would send: 1 email(s)') + ->assertSuccessful(); + + Notification::assertNotSentTo($user, UltraUpgradePromotion::class); + } + + public function test_notification_has_correct_subject(): void + { + $user = User::factory()->create(['name' => 'Jane Doe']); + + $notification = new UltraUpgradePromotion('Mini'); + $mail = $notification->toMail($user); + + $this->assertEquals('Unlock More with NativePHP Ultra', $mail->subject); + } + + public function test_notification_greeting_uses_first_name(): void + { + $user = User::factory()->create(['name' => 'Jane Doe']); + + $notification = new UltraUpgradePromotion('Mini'); + $mail = $notification->toMail($user); + + $this->assertEquals('Hi Jane,', $mail->greeting); + } + + public function test_notification_greeting_fallback_when_no_name(): void + { + $user = User::factory()->create(['name' => null]); + + $notification = new UltraUpgradePromotion('Mini'); + $mail = $notification->toMail($user); + + $this->assertEquals('Hi there,', $mail->greeting); + } + + public function test_notification_contains_current_plan_name(): void + { + $user = User::factory()->create(['name' => 'Test']); + + $notification = new UltraUpgradePromotion('Mini'); + $mail = $notification->toMail($user); + + $rendered = $mail->render()->__toString(); + + $this->assertStringContainsString('Mini', $rendered); + } + + public function test_notification_contains_ultra_benefits(): void + { + $user = User::factory()->create(['name' => 'Test']); + + $notification = new UltraUpgradePromotion('Pro'); + $mail = $notification->toMail($user); + + $rendered = $mail->render()->__toString(); + + $this->assertStringContainsString('Teams', $rendered); + $this->assertStringContainsString('Free official plugins', $rendered); + $this->assertStringContainsString('Plugin Dev Kit', $rendered); + $this->assertStringContainsString('Priority support', $rendered); + $this->assertStringContainsString('Early access', $rendered); + $this->assertStringContainsString('Exclusive content', $rendered); + $this->assertStringContainsString('Shape the roadmap', $rendered); + $this->assertStringContainsString('monthly billing', $rendered); + } + + public function test_notification_mentions_prorated_billing(): void + { + $user = User::factory()->create(['name' => 'Test']); + + $notification = new UltraUpgradePromotion('Mini'); + $mail = $notification->toMail($user); + + $rendered = $mail->render()->__toString(); + + $this->assertStringContainsString('prorated', $rendered); + } + + public function test_personalizes_plan_name_for_mini(): void + { + Notification::fake(); + + $user = User::factory()->create(); + $this->createSubscription($user, Subscription::Mini->stripePriceId()); + + $this->artisan('ultra:send-upgrade-promo') + ->expectsOutputToContain('(Mini)') + ->assertSuccessful(); + + Notification::assertSentTo($user, UltraUpgradePromotion::class, function ($notification) { + return $notification->currentPlanName === 'Mini'; + }); + } + + public function test_personalizes_plan_name_for_pro(): void + { + Notification::fake(); + + $user = User::factory()->create(); + $this->createSubscription($user, Subscription::Pro->stripePriceId()); + + $this->artisan('ultra:send-upgrade-promo') + ->expectsOutputToContain('(Pro)') + ->assertSuccessful(); + + Notification::assertSentTo($user, UltraUpgradePromotion::class, function ($notification) { + return $notification->currentPlanName === 'Pro'; + }); + } +} diff --git a/tests/Feature/StripePurchaseHandlingTest.php b/tests/Feature/StripePurchaseHandlingTest.php new file mode 100644 index 00000000..174840cf --- /dev/null +++ b/tests/Feature/StripePurchaseHandlingTest.php @@ -0,0 +1,289 @@ +set('cashier.webhook.secret', null); + + Http::fake([ + 'https://api.anystack.sh/v1/contacts' => Http::response(['data' => ['id' => 'contact-123']], 200), + 'https://api.anystack.sh/v1/products/*/licenses' => Http::response(['data' => ['key' => 'test-license-key-12345']], 200), + ]); + } + + #[Test] + public function a_user_is_not_created_when_a_stripe_customer_is_created() + { + Bus::fake(); + + $payload = [ + 'id' => 'evt_test_webhook', + 'type' => 'customer.created', + 'data' => [ + 'object' => [ + 'id' => 'cus_test123', + 'name' => 'Test Customer', + 'email' => 'test@example.com', + ], + ], + ]; + + $this->postJson('/stripe/webhook', $payload); + + Bus::assertNotDispatched(CreateUserFromStripeCustomer::class); + } + + #[Test] + public function a_user_is_created_when_a_stripe_customer_subscription_is_created_and_a_matching_user_doesnt_exist() + { + Bus::fake(); + + $this->mockStripeClient(); + + $payload = [ + 'id' => 'evt_test_webhook', + 'type' => 'customer.subscription.created', + 'data' => [ + 'object' => [ + 'id' => 'sub_test123', + 'customer' => 'cus_test123', + 'status' => 'active', + 'items' => [ + 'object' => 'list', + 'data' => [ + [ + 'id' => 'si_test', + 'price' => [ + 'id' => Subscription::Max->stripePriceId(), + 'product' => 'prod_test', + ], + 'quantity' => 1, + ], + ], + ], + ], + ], + ]; + + $this->postJson('/stripe/webhook', $payload); + + Bus::assertDispatched(CreateUserFromStripeCustomer::class); + } + + #[Test] + public function a_user_is_not_created_when_a_stripe_customer_subscription_is_created_if_a_matching_user_already_exists() + { + Bus::fake(); + + $user = User::factory()->create([ + 'stripe_id' => 'cus_test123', + 'name' => 'John Doe', + 'email' => 'john@example.com', + ]); + + $this->mockStripeClient($user); + + $payload = [ + 'id' => 'evt_test_webhook', + 'type' => 'customer.subscription.created', + 'data' => [ + 'object' => [ + 'id' => 'sub_test123', + 'customer' => $user->stripe_id, + 'status' => 'active', + 'items' => [ + 'object' => 'list', + 'data' => [ + [ + 'id' => 'si_test', + 'price' => [ + 'id' => Subscription::Max->stripePriceId(), + 'product' => 'prod_test', + ], + 'quantity' => 1, + ], + ], + ], + ], + ], + ]; + + $this->postJson('/stripe/webhook', $payload); + + Bus::assertNotDispatched(CreateUserFromStripeCustomer::class); + } + + #[Test] + public function a_license_is_not_created_when_a_stripe_subscription_is_created() + { + Bus::fake([CreateAnystackLicenseJob::class]); + + $user = User::factory()->create([ + 'stripe_id' => 'cus_test123', + 'name' => 'John Doe', + 'email' => 'john@example.com', + ]); + + $this->mockStripeClient($user); + + $payload = [ + 'id' => 'evt_test_webhook', + 'type' => 'customer.subscription.created', + 'data' => [ + 'object' => [ + 'id' => 'sub_test123', + 'customer' => 'cus_test123', + 'status' => 'active', + 'items' => [ + 'object' => 'list', + 'data' => [ + [ + 'id' => 'si_test', + 'price' => [ + 'id' => Subscription::Max->stripePriceId(), + 'product' => 'prod_test', + ], + 'quantity' => 1, + ], + ], + ], + ], + ], + ]; + + $this->postJson('/stripe/webhook', $payload); + + Bus::assertNotDispatched(CreateAnystackLicenseJob::class); + + $user->refresh(); + + $this->assertNotEmpty($user->subscriptions); + $this->assertNotEmpty($user->subscriptions->first()->items); + } + + #[Test] + public function a_license_is_not_created_when_a_stripe_invoice_is_paid() + { + Bus::fake([CreateAnystackLicenseJob::class]); + + $user = User::factory()->create([ + 'stripe_id' => 'cus_test123', + 'name' => 'John Doe', + 'email' => 'john@example.com', + ]); + + \Laravel\Cashier\Subscription::factory() + ->for($user, 'user') + ->create([ + 'stripe_id' => 'sub_test123', + 'stripe_status' => 'incomplete', // the subscription is incomplete at the time this webhook is sent + 'stripe_price' => Subscription::Max->stripePriceId(), + 'quantity' => 1, + ]); + SubscriptionItem::factory() + ->for($user->subscriptions->first(), 'subscription') + ->create([ + 'stripe_id' => 'si_test', + 'stripe_price' => Subscription::Max->stripePriceId(), + 'quantity' => 1, + ]); + + $this->mockStripeClient($user); + + $payload = [ + 'id' => 'evt_test_webhook', + 'type' => 'invoice.paid', + 'data' => [ + 'object' => [ + 'id' => 'in_test', + 'object' => 'invoice', + 'billing_reason' => 'subscription_create', + 'customer' => 'cus_test123', + 'paid' => true, + 'status' => 'paid', + 'lines' => [ + 'object' => 'list', + 'data' => [ + [ + 'id' => 'il_test', + 'price' => [ + 'id' => Subscription::Max->stripePriceId(), + 'object' => 'price', + 'product' => 'prod_test', + ], + 'quantity' => 1, + 'subscription' => 'sub_test123', + 'subscription_item' => 'si_test', + 'type' => 'subscription', + ], + ], + ], + 'subscription' => 'sub_test123', + ], + ], + ]; + + $this->postJson('/stripe/webhook', $payload); + + Bus::assertNotDispatched(CreateAnystackLicenseJob::class); + } + + protected function mockStripeClient(?User $user = null): void + { + $mockStripeClient = $this->createMock(StripeClient::class); + $mockStripeClient->customers = new class($user) + { + private $user; + + public function __construct($user) + { + $this->user = $user; + } + + public function retrieve() + { + return Customer::constructFrom([ + 'id' => $this->user?->stripe_id ?: 'cus_test123', + 'name' => $this->user?->name ?: 'Test Customer', + 'email' => $this->user?->email ?: 'test@example.com', + ]); + } + }; + + $mockStripeClient->subscriptions = new class + { + public function retrieve($subscriptionId) + { + return \Stripe\Subscription::constructFrom([ + 'id' => $subscriptionId, + 'metadata' => [], // No renewal metadata for normal tests + 'current_period_end' => now()->addYear()->timestamp, + ]); + } + }; + + $this->app->bind(StripeClient::class, function ($app, $parameters) use ($mockStripeClient) { + return $mockStripeClient; + }); + } +} diff --git a/tests/Feature/StripeWebhookRouteTest.php b/tests/Feature/StripeWebhookRouteTest.php new file mode 100644 index 00000000..1085bba7 --- /dev/null +++ b/tests/Feature/StripeWebhookRouteTest.php @@ -0,0 +1,28 @@ +post('/stripe/webhook'); + + $this->assertNotEquals(404, $response->getStatusCode()); + } + + #[Test] + public function stripe_webhook_route_is_excluded_from_csrf_verification() + { + $reflection = new \ReflectionClass(VerifyCsrfToken::class); + $property = $reflection->getProperty('except'); + $exceptPaths = $property->getValue(resolve(VerifyCsrfToken::class)); + + $this->assertContains('stripe/webhook', $exceptPaths); + } +} diff --git a/tests/Feature/SupportTicketTest.php b/tests/Feature/SupportTicketTest.php new file mode 100644 index 00000000..1c3372a8 --- /dev/null +++ b/tests/Feature/SupportTicketTest.php @@ -0,0 +1,1251 @@ + self::MAX_PRICE_ID]); + } + + private function createUltraUser(): User + { + $user = User::factory()->create(); + License::factory()->max()->active()->create(['user_id' => $user->id]); + Subscription::factory()->for($user)->active()->create([ + 'stripe_price' => self::MAX_PRICE_ID, + ]); + + return $user; + } + + #[Test] + public function guests_cannot_access_create_ticket_page(): void + { + $this->get(route('customer.support.tickets.create')) + ->assertRedirect(); + } + + #[Test] + public function non_ultra_users_cannot_access_create_ticket_page(): void + { + $user = User::factory()->create(); + + $this->actingAs($user) + ->get(route('customer.support.tickets.create')) + ->assertForbidden(); + } + + #[Test] + public function ultra_users_can_access_create_ticket_page(): void + { + $user = $this->createUltraUser(); + + $this->actingAs($user) + ->get(route('customer.support.tickets.create')) + ->assertOk() + ->assertSeeLivewire(Create::class); + } + + #[Test] + public function wizard_starts_at_step_1(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Create::class) + ->assertSet('currentStep', 1) + ->assertSee('Which product is this about?'); + } + + #[Test] + public function a_product_must_be_selected(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Create::class) + ->call('nextStep') + ->assertHasErrors('selectedProduct') + ->assertSet('currentStep', 1); + } + + #[Test] + public function product_must_be_a_valid_value(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('selectedProduct', 'invalid') + ->call('nextStep') + ->assertHasErrors('selectedProduct') + ->assertSet('currentStep', 1); + } + + #[Test] + public function selecting_mobile_advances_to_step_2(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('selectedProduct', 'mobile') + ->call('nextStep') + ->assertSet('currentStep', 2); + } + + #[Test] + public function mobile_selection_shows_area_type_and_bug_fields_on_step_2(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('selectedProduct', 'mobile') + ->call('nextStep') + ->assertSet('currentStep', 2) + ->assertSee('What is the issue related to?') + ->assertSee('Bug report details') + ->assertDontSee('Describe your issue'); + } + + #[Test] + public function desktop_selection_shows_bug_fields_but_not_area_on_step_2(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('selectedProduct', 'desktop') + ->call('nextStep') + ->assertSet('currentStep', 2) + ->assertSee('Bug report details') + ->assertDontSee('Describe your issue') + ->assertDontSee('Which area?'); + } + + #[Test] + public function bifrost_selection_shows_issue_type_on_step_2(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('selectedProduct', 'bifrost') + ->call('nextStep') + ->assertSet('currentStep', 2) + ->assertSee('Issue type') + ->assertSee('Describe your issue') + ->assertDontSee('Bug report details'); + } + + #[Test] + public function nativephp_com_selection_shows_issue_type_on_step_2(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('selectedProduct', 'nativephp.com') + ->call('nextStep') + ->assertSet('currentStep', 2) + ->assertSee('Issue type') + ->assertSee('Describe your issue') + ->assertDontSee('Bug report details'); + } + + #[Test] + public function bug_report_fields_are_required_when_shown(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('selectedProduct', 'desktop') + ->call('nextStep') + ->assertSet('currentStep', 2) + ->call('nextStep') + ->assertHasErrors(['tryingToDo', 'whatHappened', 'reproductionSteps', 'environment']) + ->assertSet('currentStep', 2); + } + + #[Test] + public function mobile_area_type_is_required_when_mobile_selected(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('selectedProduct', 'mobile') + ->call('nextStep') + ->assertSet('currentStep', 2) + ->set('tryingToDo', 'Test') + ->set('whatHappened', 'Test') + ->set('reproductionSteps', 'Test') + ->set('environment', 'Test') + ->call('nextStep') + ->assertHasErrors('mobileAreaType') + ->assertSet('currentStep', 2); + } + + #[Test] + public function mobile_area_is_required_when_plugin_type_selected(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('selectedProduct', 'mobile') + ->call('nextStep') + ->assertSet('currentStep', 2) + ->set('mobileAreaType', 'plugin') + ->set('tryingToDo', 'Test') + ->set('whatHappened', 'Test') + ->set('reproductionSteps', 'Test') + ->set('environment', 'Test') + ->call('nextStep') + ->assertHasErrors('mobileArea') + ->assertSet('currentStep', 2); + } + + #[Test] + public function mobile_core_type_does_not_require_mobile_area(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('selectedProduct', 'mobile') + ->call('nextStep') + ->assertSet('currentStep', 2) + ->set('mobileAreaType', 'core') + ->set('tryingToDo', 'Test') + ->set('whatHappened', 'Test') + ->set('reproductionSteps', 'Test') + ->set('environment', 'Test') + ->call('nextStep') + ->assertHasNoErrors('mobileArea') + ->assertSet('currentStep', 3); + } + + #[Test] + public function issue_type_is_required_when_bifrost_selected(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('selectedProduct', 'bifrost') + ->call('nextStep') + ->assertSet('currentStep', 2) + ->set('subject', 'Test subject') + ->set('message', 'Test message') + ->call('nextStep') + ->assertHasErrors('issueType') + ->assertSet('currentStep', 2); + } + + #[Test] + public function issue_type_must_be_valid_value(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('selectedProduct', 'bifrost') + ->call('nextStep') + ->set('issueType', 'invalid_type') + ->set('subject', 'Test subject') + ->set('message', 'Test message') + ->call('nextStep') + ->assertHasErrors('issueType') + ->assertSet('currentStep', 2); + } + + #[Test] + public function subject_is_required_on_step_2(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('selectedProduct', 'bifrost') + ->call('nextStep') + ->set('issueType', 'bug') + ->set('message', 'Some message') + ->call('nextStep') + ->assertHasErrors('subject') + ->assertSet('currentStep', 2); + } + + #[Test] + public function message_is_required_on_step_2(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('selectedProduct', 'bifrost') + ->call('nextStep') + ->set('issueType', 'bug') + ->set('subject', 'Some subject') + ->call('nextStep') + ->assertHasErrors('message') + ->assertSet('currentStep', 2); + } + + #[Test] + public function subject_cannot_exceed_255_characters(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('selectedProduct', 'bifrost') + ->call('nextStep') + ->set('issueType', 'bug') + ->set('subject', str_repeat('a', 256)) + ->set('message', 'Some message') + ->call('nextStep') + ->assertHasErrors('subject') + ->assertSet('currentStep', 2); + } + + #[Test] + public function message_cannot_exceed_5000_characters(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('selectedProduct', 'bifrost') + ->call('nextStep') + ->set('issueType', 'bug') + ->set('subject', 'Some subject') + ->set('message', str_repeat('a', 5001)) + ->call('nextStep') + ->assertHasErrors('message') + ->assertSet('currentStep', 2); + } + + #[Test] + public function full_desktop_submission_creates_ticket_with_bug_report_data(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('selectedProduct', 'desktop') + ->call('nextStep') + ->set('tryingToDo', 'Build an app') + ->set('whatHappened', 'It crashed') + ->set('reproductionSteps', '1. Open app 2. Click button') + ->set('environment', 'macOS 14, Electron 28') + ->call('nextStep') + ->assertSet('currentStep', 3) + ->call('submit') + ->assertRedirect(); + + $ticket = SupportTicket::where('user_id', $user->id)->first(); + + $this->assertNotNull($ticket); + $this->assertEquals('desktop', $ticket->product); + $this->assertEquals('Build an app', $ticket->subject); + $this->assertStringContainsString('Build an app', $ticket->message); + $this->assertStringContainsString('It crashed', $ticket->message); + $this->assertStringContainsString('1. Open app 2. Click button', $ticket->message); + $this->assertStringContainsString('macOS 14, Electron 28', $ticket->message); + $this->assertNull($ticket->issue_type); + $this->assertEquals('Build an app', $ticket->metadata['trying_to_do']); + $this->assertEquals('It crashed', $ticket->metadata['what_happened']); + } + + #[Test] + public function bifrost_submission_stores_product_and_issue_type(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('selectedProduct', 'bifrost') + ->call('nextStep') + ->set('issueType', 'feature_request') + ->set('subject', 'Feature request') + ->set('message', 'Please add this feature.') + ->call('nextStep') + ->assertSet('currentStep', 3) + ->call('submit') + ->assertRedirect(); + + $ticket = SupportTicket::where('subject', 'Feature request')->first(); + + $this->assertNotNull($ticket); + $this->assertEquals('bifrost', $ticket->product); + $this->assertEquals('feature_request', $ticket->issue_type); + $this->assertNull($ticket->metadata); + } + + #[Test] + public function submission_redirects_to_show_page(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('selectedProduct', 'nativephp.com') + ->call('nextStep') + ->set('issueType', 'other') + ->set('subject', 'Redirect test') + ->set('message', 'Testing redirect after creation.') + ->call('nextStep') + ->assertSet('currentStep', 3) + ->call('submit') + ->assertRedirect(); + + $ticket = SupportTicket::where('subject', 'Redirect test')->first(); + + $this->assertNotNull($ticket); + $this->assertNotEmpty(route('customer.support.tickets.show', $ticket)); + } + + #[Test] + public function step_3_shows_full_summary_including_environment_and_reproduction_steps(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('selectedProduct', 'desktop') + ->call('nextStep') + ->set('tryingToDo', 'Build an app') + ->set('whatHappened', 'It crashed') + ->set('reproductionSteps', '1. Open app 2. Click button') + ->set('environment', 'macOS 14, PHP 8.4') + ->call('nextStep') + ->assertSet('currentStep', 3) + ->assertSee('Review your request') + ->assertSee('Desktop') + ->assertSee('Build an app') + ->assertSee('It crashed') + ->assertSee('1. Open app 2. Click button') + ->assertSee('macOS 14, PHP 8.4'); + } + + #[Test] + public function support_page_shows_priority_support_for_ultra_users(): void + { + $user = $this->createUltraUser(); + + $this->actingAs($user) + ->get(route('support.index')) + ->assertOk() + ->assertSee('Priority Support') + ->assertSee('Submit a Ticket'); + } + + #[Test] + public function support_page_shows_ultra_upsell_for_non_ultra_users(): void + { + $user = User::factory()->create(); + + License::factory()->pro()->active()->create(['user_id' => $user->id]); + + $this->actingAs($user) + ->get(route('support.index')) + ->assertOk() + ->assertSee('Priority Support') + ->assertSee('Learn about Ultra') + ->assertDontSee('Submit a Ticket'); + } + + #[Test] + public function support_page_shows_ultra_upsell_for_guests(): void + { + $this->get(route('support.index')) + ->assertOk() + ->assertSee('Priority Support') + ->assertSee('Learn about Ultra') + ->assertDontSee('Submit a Ticket'); + } + + #[Test] + public function changing_product_resets_all_step_2_fields(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('selectedProduct', 'mobile') + ->call('nextStep') + ->set('mobileAreaType', 'plugin') + ->set('mobileArea', 'jump') + ->set('tryingToDo', 'Something') + ->set('whatHappened', 'Something else') + ->set('reproductionSteps', 'Steps here') + ->set('environment', 'macOS') + ->call('previousStep') + ->set('selectedProduct', 'nativephp.com') + ->assertSet('mobileAreaType', '') + ->assertSet('mobileArea', '') + ->assertSet('tryingToDo', '') + ->assertSet('whatHappened', '') + ->assertSet('reproductionSteps', '') + ->assertSet('environment', '') + ->assertSet('subject', '') + ->assertSet('message', '') + ->assertSet('issueType', ''); + } + + #[Test] + public function ticket_index_shows_create_button_link(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Index::class) + ->assertSee(route('customer.support.tickets.create')); + } + + #[Test] + public function back_button_returns_to_previous_step(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('selectedProduct', 'desktop') + ->call('nextStep') + ->assertSet('currentStep', 2) + ->call('previousStep') + ->assertSet('currentStep', 1); + } + + #[Test] + public function plugin_type_shows_official_plugins_in_select(): void + { + $user = $this->createUltraUser(); + + Plugin::factory()->create([ + 'name' => 'nativephp/mobile-camera', + 'is_official' => true, + 'user_id' => $user->id, + ]); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('selectedProduct', 'mobile') + ->call('nextStep') + ->set('mobileAreaType', 'plugin') + ->assertSee('nativephp/mobile-camera') + ->assertSee('Jump'); + } + + #[Test] + public function mobile_plugin_submission_stores_area_in_metadata(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('selectedProduct', 'mobile') + ->call('nextStep') + ->set('mobileAreaType', 'plugin') + ->set('mobileArea', 'jump') + ->set('tryingToDo', 'Navigate between screens') + ->set('whatHappened', 'App froze') + ->set('reproductionSteps', '1. Open app 2. Navigate') + ->set('environment', 'iOS 17, iPhone 15') + ->call('nextStep') + ->assertSet('currentStep', 3) + ->call('submit') + ->assertRedirect(); + + $ticket = SupportTicket::where('user_id', $user->id)->first(); + + $this->assertNotNull($ticket); + $this->assertEquals('mobile', $ticket->product); + $this->assertEquals('Navigate between screens', $ticket->subject); + $this->assertEquals('plugin', $ticket->metadata['mobile_area_type']); + $this->assertEquals('jump', $ticket->metadata['mobile_area']); + $this->assertEquals('Navigate between screens', $ticket->metadata['trying_to_do']); + } + + #[Test] + public function mobile_core_submission_stores_area_type_without_area(): void + { + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('selectedProduct', 'mobile') + ->call('nextStep') + ->set('mobileAreaType', 'core') + ->set('tryingToDo', 'Build an app') + ->set('whatHappened', 'It crashed') + ->set('reproductionSteps', '1. Run build') + ->set('environment', 'iOS 17') + ->call('nextStep') + ->assertSet('currentStep', 3) + ->call('submit') + ->assertRedirect(); + + $ticket = SupportTicket::where('user_id', $user->id)->first(); + + $this->assertNotNull($ticket); + $this->assertEquals('mobile', $ticket->product); + $this->assertEquals('Build an app', $ticket->subject); + $this->assertEquals('core', $ticket->metadata['mobile_area_type']); + $this->assertArrayNotHasKey('mobile_area', $ticket->metadata); + } + + #[Test] + public function submitting_a_ticket_sends_notification_to_support_email(): void + { + Notification::fake(); + + $user = $this->createUltraUser(); + + Livewire::actingAs($user) + ->test(Create::class) + ->set('selectedProduct', 'bifrost') + ->call('nextStep') + ->set('issueType', 'bug') + ->set('subject', 'Notification test') + ->set('message', 'Testing notification dispatch.') + ->call('nextStep') + ->assertSet('currentStep', 3) + ->call('submit') + ->assertRedirect(); + + Notification::assertSentOnDemand( + SupportTicketSubmitted::class, + function (SupportTicketSubmitted $notification, array $channels, object $notifiable) { + return $notifiable->routes['mail'] === 'support@nativephp.com' + && $notification->ticket->subject === 'Notification test'; + } + ); + } + + #[Test] + public function support_ticket_email_includes_customer_details_with_obfuscated_email(): void + { + $user = User::factory()->create([ + 'name' => 'Jane Smith', + 'email' => 'jane@example.com', + ]); + + $ticket = SupportTicket::factory()->create([ + 'user_id' => $user->id, + 'subject' => 'Test ticket', + 'product' => 'bifrost', + 'issue_type' => 'bug', + 'message' => 'Test message', + ]); + + $notification = new SupportTicketSubmitted($ticket); + $mailMessage = $notification->toMail($user); + $rendered = $mailMessage->render()->toHtml(); + + $this->assertStringContainsString('Jane Smith', $rendered); + $this->assertStringContainsString('ja**@ex*****.com', $rendered); + $this->assertStringNotContainsString('jane@example.com', $rendered); + } + + #[Test] + public function authenticated_ultra_user_can_reply_to_their_open_ticket(): void + { + $user = $this->createUltraUser(); + $ticket = SupportTicket::factory()->create(['user_id' => $user->id]); + + Livewire::actingAs($user) + ->test(Show::class, ['supportTicket' => $ticket]) + ->set('replyMessage', 'This is my reply.') + ->call('reply') + ->assertHasNoErrors(); + + $this->assertDatabaseHas('replies', [ + 'support_ticket_id' => $ticket->id, + 'user_id' => $user->id, + 'message' => 'This is my reply.', + 'note' => false, + ]); + } + + #[Test] + public function user_reply_sends_notification_to_support_email(): void + { + Notification::fake(); + + $user = $this->createUltraUser(); + $ticket = SupportTicket::factory()->create(['user_id' => $user->id]); + + Livewire::actingAs($user) + ->test(Show::class, ['supportTicket' => $ticket]) + ->set('replyMessage', 'I have more info.') + ->call('reply') + ->assertHasNoErrors(); + + Notification::assertSentOnDemand( + SupportTicketUserReplied::class, + function (SupportTicketUserReplied $notification, array $channels, object $notifiable) use ($ticket) { + return $notifiable->routes['mail'] === 'support@nativephp.com' + && $notification->ticket->is($ticket) + && $notification->reply->message === 'I have more info.'; + } + ); + } + + #[Test] + public function ultra_user_cannot_reply_to_a_closed_ticket(): void + { + $user = $this->createUltraUser(); + $ticket = SupportTicket::factory()->create([ + 'user_id' => $user->id, + 'status' => Status::CLOSED, + ]); + + Livewire::actingAs($user) + ->test(Show::class, ['supportTicket' => $ticket]) + ->set('replyMessage', 'This should fail.') + ->call('reply') + ->assertForbidden(); + } + + #[Test] + public function ultra_user_cannot_view_another_users_ticket(): void + { + $user = $this->createUltraUser(); + $otherUser = $this->createUltraUser(); + $ticket = SupportTicket::factory()->create(['user_id' => $otherUser->id]); + + Livewire::actingAs($user) + ->test(Show::class, ['supportTicket' => $ticket]) + ->assertNotFound(); + } + + #[Test] + public function non_ultra_user_cannot_view_ticket(): void + { + $user = User::factory()->create(); + $ticket = SupportTicket::factory()->create(['user_id' => $user->id]); + + Livewire::actingAs($user) + ->test(Show::class, ['supportTicket' => $ticket]) + ->assertForbidden(); + } + + #[Test] + public function reply_is_rate_limited_to_10_per_minute(): void + { + $user = $this->createUltraUser(); + $ticket = SupportTicket::factory()->create(['user_id' => $user->id]); + + $component = Livewire::actingAs($user) + ->test(Show::class, ['supportTicket' => $ticket]); + + for ($i = 1; $i <= 10; $i++) { + $component + ->set('replyMessage', "Reply {$i}") + ->call('reply') + ->assertHasNoErrors(); + } + + $component + ->set('replyMessage', 'One too many') + ->call('reply') + ->assertHasErrors('replyMessage'); + + $this->assertDatabaseCount('replies', 10); + } + + #[Test] + public function reply_message_is_required(): void + { + $user = $this->createUltraUser(); + $ticket = SupportTicket::factory()->create(['user_id' => $user->id]); + + Livewire::actingAs($user) + ->test(Show::class, ['supportTicket' => $ticket]) + ->set('replyMessage', '') + ->call('reply') + ->assertHasErrors('replyMessage'); + } + + #[Test] + public function ticket_show_page_displays_inline_reply_form_for_open_ticket(): void + { + $user = $this->createUltraUser(); + $ticket = SupportTicket::factory()->create(['user_id' => $user->id]); + + Livewire::actingAs($user) + ->test(Show::class, ['supportTicket' => $ticket]) + ->assertSee('Add a reply'); + } + + #[Test] + public function ticket_show_page_hides_reply_form_for_closed_ticket(): void + { + $user = $this->createUltraUser(); + $ticket = SupportTicket::factory()->create([ + 'user_id' => $user->id, + 'status' => Status::CLOSED, + ]); + + Livewire::actingAs($user) + ->test(Show::class, ['supportTicket' => $ticket]) + ->assertDontSee('Add a reply'); + } + + #[Test] + public function ticket_show_page_hides_internal_notes_from_ticket_owner(): void + { + $user = $this->createUltraUser(); + $admin = User::factory()->create(); + $ticket = SupportTicket::factory()->create(['user_id' => $user->id]); + + Reply::factory()->create([ + 'support_ticket_id' => $ticket->id, + 'user_id' => $admin->id, + 'message' => 'Visible staff reply', + 'note' => false, + ]); + + Reply::factory()->create([ + 'support_ticket_id' => $ticket->id, + 'user_id' => $admin->id, + 'message' => 'Secret internal note', + 'note' => true, + ]); + + Livewire::actingAs($user) + ->test(Show::class, ['supportTicket' => $ticket]) + ->assertSee('Visible staff reply') + ->assertDontSee('Secret internal note'); + } + + #[Test] + public function admin_reply_sends_notification_to_ticket_owner(): void + { + Notification::fake(); + + $user = User::factory()->create(); + $admin = User::factory()->create(); + $ticket = SupportTicket::factory()->create(['user_id' => $user->id]); + + $reply = Reply::factory()->create([ + 'support_ticket_id' => $ticket->id, + 'user_id' => $admin->id, + 'message' => 'We are looking into this.', + 'note' => false, + ]); + + $ticket->user->notify(new SupportTicketReplied($ticket, $reply)); + + Notification::assertSentTo( + $user, + SupportTicketReplied::class, + function (SupportTicketReplied $notification) use ($ticket, $reply) { + return $notification->ticket->is($ticket) + && $notification->reply->is($reply); + } + ); + } + + #[Test] + public function internal_note_reply_does_not_send_notification_to_ticket_owner(): void + { + Notification::fake(); + + $user = User::factory()->create(); + $admin = User::factory()->create(); + $ticket = SupportTicket::factory()->create(['user_id' => $user->id]); + + Reply::factory()->create([ + 'support_ticket_id' => $ticket->id, + 'user_id' => $admin->id, + 'message' => 'Internal note only.', + 'note' => true, + ]); + + // The RepliesRelationManager skips notification for notes, + // so we verify no notification was sent. + Notification::assertNotSentTo($user, SupportTicketReplied::class); + } + + #[Test] + public function ticket_owner_does_not_receive_notification_for_own_reply(): void + { + Notification::fake(); + + $admin = User::factory()->create(['is_admin' => true]); + $ticket = SupportTicket::factory()->create(['user_id' => $admin->id]); + + Livewire::actingAs($admin) + ->test(TicketRepliesWidget::class, ['record' => $ticket]) + ->set('newMessage', 'Replying to my own ticket.') + ->call('sendReply'); + + $this->assertDatabaseHas('replies', [ + 'support_ticket_id' => $ticket->id, + 'message' => 'Replying to my own ticket.', + ]); + + Notification::assertNotSentTo($admin, SupportTicketReplied::class); + } + + #[Test] + public function support_ticket_replied_notification_contains_correct_mail_content(): void + { + $user = User::factory()->create(['name' => 'Jane Doe']); + $ticket = SupportTicket::factory()->create([ + 'user_id' => $user->id, + 'subject' => 'Login issue', + ]); + $reply = Reply::factory()->create([ + 'support_ticket_id' => $ticket->id, + 'message' => 'We have fixed the login issue.', + ]); + + $notification = new SupportTicketReplied($ticket, $reply); + $mail = $notification->toMail($user); + $rendered = $mail->render()->toHtml(); + + $this->assertStringContainsString($ticket->mask, $mail->subject); + $this->assertStringNotContainsString('Login issue', $mail->subject); + $this->assertStringContainsString('Hi Jane', $mail->greeting); + $this->assertStringContainsString('log in to your dashboard', $rendered); + $this->assertStringContainsString('do not reply to this email', $rendered); + $this->assertStringNotContainsString('We have fixed the login issue', $rendered); + } + + #[Test] + public function ticket_show_page_displays_submission_details_section(): void + { + $user = $this->createUltraUser(); + $ticket = SupportTicket::factory()->create([ + 'user_id' => $user->id, + 'product' => 'mobile', + 'message' => 'Original submission message', + 'metadata' => [ + 'trying_to_do' => 'Build an app', + 'what_happened' => 'It crashed', + ], + ]); + + Livewire::actingAs($user) + ->test(Show::class, ['supportTicket' => $ticket]) + ->assertSee('Submission Details') + ->assertSee('Mobile') + ->assertDontSee('Original submission message') + ->assertSee('Build an app') + ->assertSee('It crashed'); + } + + #[Test] + public function ticket_show_page_hides_original_message_for_desktop_tickets(): void + { + $user = $this->createUltraUser(); + $ticket = SupportTicket::factory()->create([ + 'user_id' => $user->id, + 'product' => 'desktop', + 'message' => 'Auto-generated bug report message', + 'metadata' => [ + 'trying_to_do' => 'Run the app', + ], + ]); + + Livewire::actingAs($user) + ->test(Show::class, ['supportTicket' => $ticket]) + ->assertDontSee('Original Message') + ->assertSee('Run the app'); + } + + #[Test] + public function ticket_show_page_shows_original_message_for_non_bug_report_tickets(): void + { + $user = $this->createUltraUser(); + $ticket = SupportTicket::factory()->create([ + 'user_id' => $user->id, + 'product' => 'nativephp.com', + 'message' => 'I have a billing question.', + ]); + + Livewire::actingAs($user) + ->test(Show::class, ['supportTicket' => $ticket]) + ->assertSee('Original Message') + ->assertSee('I have a billing question.'); + } + + #[Test] + public function ultra_user_can_close_their_ticket(): void + { + $user = $this->createUltraUser(); + $ticket = SupportTicket::factory()->create(['user_id' => $user->id]); + + Livewire::actingAs($user) + ->test(Show::class, ['supportTicket' => $ticket]) + ->call('closeTicket') + ->assertHasNoErrors(); + + $this->assertEquals(Status::CLOSED, $ticket->fresh()->status); + } + + #[Test] + public function closing_ticket_creates_system_message(): void + { + $user = $this->createUltraUser(); + $ticket = SupportTicket::factory()->create(['user_id' => $user->id]); + + Livewire::actingAs($user) + ->test(Show::class, ['supportTicket' => $ticket]) + ->call('closeTicket') + ->assertSee($user->name.' closed this ticket.'); + + $this->assertDatabaseHas('replies', [ + 'support_ticket_id' => $ticket->id, + 'user_id' => null, + 'message' => $user->name.' closed this ticket.', + ]); + } + + #[Test] + public function reopening_ticket_creates_system_message(): void + { + $user = $this->createUltraUser(); + $ticket = SupportTicket::factory()->create([ + 'user_id' => $user->id, + 'status' => Status::CLOSED, + ]); + + Livewire::actingAs($user) + ->test(Show::class, ['supportTicket' => $ticket]) + ->call('reopenTicket') + ->assertSee($user->name.' reopened this ticket.'); + + $this->assertDatabaseHas('replies', [ + 'support_ticket_id' => $ticket->id, + 'user_id' => null, + 'message' => $user->name.' reopened this ticket.', + ]); + } + + #[Test] + public function ultra_user_can_reopen_their_closed_ticket(): void + { + $user = $this->createUltraUser(); + $ticket = SupportTicket::factory()->create([ + 'user_id' => $user->id, + 'status' => Status::CLOSED, + ]); + + Livewire::actingAs($user) + ->test(Show::class, ['supportTicket' => $ticket]) + ->call('reopenTicket') + ->assertHasNoErrors(); + + $this->assertEquals(Status::OPEN, $ticket->fresh()->status); + } + + #[Test] + public function ultra_user_cannot_reopen_an_already_open_ticket(): void + { + $user = $this->createUltraUser(); + $ticket = SupportTicket::factory()->create(['user_id' => $user->id]); + + Livewire::actingAs($user) + ->test(Show::class, ['supportTicket' => $ticket]) + ->call('reopenTicket') + ->assertForbidden(); + } + + #[Test] + public function ultra_user_cannot_reopen_another_users_ticket(): void + { + $user = $this->createUltraUser(); + $otherUser = $this->createUltraUser(); + $ticket = SupportTicket::factory()->create([ + 'user_id' => $otherUser->id, + 'status' => Status::CLOSED, + ]); + + Livewire::actingAs($user) + ->test(Show::class, ['supportTicket' => $ticket]) + ->assertNotFound(); + } + + #[Test] + public function admin_can_pin_an_internal_note(): void + { + $admin = User::factory()->create(['is_admin' => true]); + $ticket = SupportTicket::factory()->create(); + $note = Reply::factory()->create([ + 'support_ticket_id' => $ticket->id, + 'user_id' => $admin->id, + 'message' => 'Important context', + 'note' => true, + 'pinned' => false, + ]); + + Livewire::actingAs($admin) + ->test(TicketRepliesWidget::class, ['record' => $ticket]) + ->call('togglePin', $note->id); + + $this->assertTrue($note->fresh()->pinned); + } + + #[Test] + public function admin_can_unpin_a_pinned_note(): void + { + $admin = User::factory()->create(['is_admin' => true]); + $ticket = SupportTicket::factory()->create(); + $note = Reply::factory()->create([ + 'support_ticket_id' => $ticket->id, + 'user_id' => $admin->id, + 'message' => 'Pinned context', + 'note' => true, + 'pinned' => true, + ]); + + Livewire::actingAs($admin) + ->test(TicketRepliesWidget::class, ['record' => $ticket]) + ->call('togglePin', $note->id); + + $this->assertFalse($note->fresh()->pinned); + } + + #[Test] + public function pinning_a_note_unpins_the_previously_pinned_note(): void + { + $admin = User::factory()->create(['is_admin' => true]); + $ticket = SupportTicket::factory()->create(); + $firstNote = Reply::factory()->create([ + 'support_ticket_id' => $ticket->id, + 'user_id' => $admin->id, + 'message' => 'First note', + 'note' => true, + 'pinned' => true, + ]); + $secondNote = Reply::factory()->create([ + 'support_ticket_id' => $ticket->id, + 'user_id' => $admin->id, + 'message' => 'Second note', + 'note' => true, + 'pinned' => false, + ]); + + Livewire::actingAs($admin) + ->test(TicketRepliesWidget::class, ['record' => $ticket]) + ->call('togglePin', $secondNote->id); + + $this->assertFalse($firstNote->fresh()->pinned); + $this->assertTrue($secondNote->fresh()->pinned); + } + + #[Test] + public function only_internal_notes_can_be_pinned(): void + { + $admin = User::factory()->create(['is_admin' => true]); + $ticket = SupportTicket::factory()->create(); + $reply = Reply::factory()->create([ + 'support_ticket_id' => $ticket->id, + 'user_id' => $admin->id, + 'message' => 'Regular reply', + 'note' => false, + ]); + + $this->expectException(ModelNotFoundException::class); + + Livewire::actingAs($admin) + ->test(TicketRepliesWidget::class, ['record' => $ticket]) + ->call('togglePin', $reply->id); + } + + #[Test] + public function admin_view_page_shows_user_email_when_name_is_null(): void + { + $admin = User::factory()->create(['email' => 'admin@test.com']); + config(['filament.users' => ['admin@test.com']]); + + $namelessUser = User::factory()->create(['name' => null]); + $ticket = SupportTicket::factory()->create(['user_id' => $namelessUser->id]); + + Livewire::actingAs($admin) + ->test(ViewSupportTicket::class, ['record' => $ticket->getRouteKey()]) + ->assertOk() + ->assertSee($namelessUser->email); + } + + #[Test] + public function admin_view_page_shows_name_and_email_when_user_has_name(): void + { + $admin = User::factory()->create(['email' => 'admin@test.com']); + config(['filament.users' => ['admin@test.com']]); + + $namedUser = User::factory()->create(['name' => 'Jane Doe']); + $ticket = SupportTicket::factory()->create(['user_id' => $namedUser->id]); + + Livewire::actingAs($admin) + ->test(ViewSupportTicket::class, ['record' => $ticket->getRouteKey()]) + ->assertOk() + ->assertSee('Jane Doe') + ->assertSee($namedUser->email); + } + + #[Test] + public function guests_cannot_access_ticket_index(): void + { + $this->get(route('customer.support.tickets')) + ->assertRedirect(); + } + + #[Test] + public function non_ultra_users_cannot_access_ticket_index(): void + { + $user = User::factory()->create(); + + $this->actingAs($user) + ->get(route('customer.support.tickets')) + ->assertForbidden(); + } + + #[Test] + public function ultra_users_can_access_ticket_index(): void + { + $user = $this->createUltraUser(); + + $this->actingAs($user) + ->get(route('customer.support.tickets')) + ->assertOk() + ->assertSeeLivewire(Index::class); + } +} diff --git a/tests/Feature/SuppressMailNotificationListenerTest.php b/tests/Feature/SuppressMailNotificationListenerTest.php new file mode 100644 index 00000000..62716b40 --- /dev/null +++ b/tests/Feature/SuppressMailNotificationListenerTest.php @@ -0,0 +1,74 @@ +create(['receives_notification_emails' => true]); + + $event = new NotificationSending( + $user, + new PluginApproved( + plugin: Plugin::factory()->for($user)->create(), + ), + 'mail', + ); + + $listener = new SuppressMailNotificationListener; + + $this->assertTrue($listener->handle($event)); + } + + public function test_suppresses_mail_when_user_has_opted_out(): void + { + $user = User::factory()->create(['receives_notification_emails' => false]); + + $event = new NotificationSending( + $user, + new PluginApproved( + plugin: Plugin::factory()->for($user)->create(), + ), + 'mail', + ); + + $listener = new SuppressMailNotificationListener; + + $this->assertFalse($listener->handle($event)); + } + + public function test_allows_non_mail_channels_regardless_of_preference(): void + { + $user = User::factory()->create(['receives_notification_emails' => false]); + + $event = new NotificationSending( + $user, + new PluginApproved( + plugin: Plugin::factory()->for($user)->create(), + ), + 'database', + ); + + $listener = new SuppressMailNotificationListener; + + $this->assertTrue($listener->handle($event)); + } + + public function test_new_users_receive_notifications_by_default(): void + { + $user = User::factory()->create(); + + $this->assertTrue($user->receives_notification_emails); + } +} diff --git a/tests/Feature/TeamManagementTest.php b/tests/Feature/TeamManagementTest.php new file mode 100644 index 00000000..49307e71 --- /dev/null +++ b/tests/Feature/TeamManagementTest.php @@ -0,0 +1,968 @@ + self::MAX_PRICE_ID]); + } + + private function createUltraUser(): User + { + $user = User::factory()->create(); + License::factory()->max()->active()->create(['user_id' => $user->id]); + Subscription::factory()->for($user)->active()->create([ + 'stripe_price' => self::MAX_PRICE_ID, + ]); + + return $user; + } + + private function createTeamWithOwner(): array + { + $owner = $this->createUltraUser(); + $team = Team::factory()->create(['user_id' => $owner->id, 'name' => 'Test Team']); + + return [$owner, $team]; + } + + // ======================================== + // Team Creation Tests + // ======================================== + + public function test_ultra_subscriber_can_create_team(): void + { + $user = $this->createUltraUser(); + + $response = $this->actingAs($user) + ->post(route('customer.team.store'), ['name' => 'My Team']); + + $response->assertRedirect(route('customer.team.index')); + $response->assertSessionHas('success'); + + $this->assertDatabaseHas('teams', [ + 'user_id' => $user->id, + 'name' => 'My Team', + 'is_suspended' => false, + ]); + } + + public function test_non_ultra_user_cannot_create_team(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user) + ->post(route('customer.team.store'), ['name' => 'My Team']); + + $response->assertSessionHas('error'); + $this->assertDatabaseMissing('teams', ['user_id' => $user->id]); + } + + public function test_cannot_create_duplicate_team(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + + $response = $this->actingAs($owner) + ->post(route('customer.team.store'), ['name' => 'Another Team']); + + $response->assertSessionHas('error'); + $this->assertCount(1, Team::where('user_id', $owner->id)->get()); + } + + public function test_team_name_is_required(): void + { + $user = $this->createUltraUser(); + + $response = $this->actingAs($user) + ->post(route('customer.team.store'), ['name' => '']); + + $response->assertSessionHasErrors('name'); + } + + // ======================================== + // Invitation Tests + // ======================================== + + public function test_owner_can_invite_member_by_email(): void + { + Notification::fake(); + + [$owner, $team] = $this->createTeamWithOwner(); + + $response = $this->actingAs($owner) + ->post(route('customer.team.invite'), ['email' => 'member@example.com']); + + $response->assertSessionHas('success'); + + $this->assertDatabaseHas('team_users', [ + 'team_id' => $team->id, + 'email' => 'member@example.com', + 'status' => TeamUserStatus::Pending->value, + ]); + + Notification::assertSentOnDemand(TeamInvitation::class); + } + + public function test_cannot_invite_duplicate_email(): void + { + Notification::fake(); + + [$owner, $team] = $this->createTeamWithOwner(); + + TeamUser::factory()->create([ + 'team_id' => $team->id, + 'email' => 'member@example.com', + 'status' => TeamUserStatus::Active, + ]); + + $response = $this->actingAs($owner) + ->post(route('customer.team.invite'), ['email' => 'member@example.com']); + + $response->assertSessionHas('error'); + } + + public function test_can_reinvite_previously_removed_member(): void + { + Notification::fake(); + + [$owner, $team] = $this->createTeamWithOwner(); + + // Create a removed team user (previously invited then cancelled/removed) + $removed = TeamUser::factory()->create([ + 'team_id' => $team->id, + 'email' => 'member@example.com', + 'status' => TeamUserStatus::Removed, + 'user_id' => null, + ]); + + $response = $this->actingAs($owner) + ->post(route('customer.team.invite'), ['email' => 'member@example.com']); + + $response->assertSessionHas('success'); + + $this->assertDatabaseHas('team_users', [ + 'id' => $removed->id, + 'team_id' => $team->id, + 'email' => 'member@example.com', + 'status' => TeamUserStatus::Pending->value, + ]); + + // Should reuse the existing record, not create a new one + $this->assertEquals(1, TeamUser::where('team_id', $team->id)->where('email', 'member@example.com')->count()); + + Notification::assertSentOnDemand(TeamInvitation::class); + } + + public function test_cannot_invite_when_team_suspended(): void + { + Notification::fake(); + + $owner = $this->createUltraUser(); + $team = Team::factory()->suspended()->create(['user_id' => $owner->id]); + + $response = $this->actingAs($owner) + ->post(route('customer.team.invite'), ['email' => 'member@example.com']); + + $response->assertSessionHas('error'); + Notification::assertNothingSent(); + } + + public function test_owner_cannot_invite_themselves(): void + { + Notification::fake(); + + [$owner, $team] = $this->createTeamWithOwner(); + + $response = $this->actingAs($owner) + ->post(route('customer.team.invite'), ['email' => $owner->email]); + + $response->assertSessionHas('error', 'You cannot invite yourself to your own team.'); + Notification::assertNothingSent(); + $this->assertDatabaseMissing('team_users', [ + 'team_id' => $team->id, + 'email' => $owner->email, + ]); + } + + public function test_cannot_invite_beyond_seat_limit(): void + { + Notification::fake(); + + [$owner, $team] = $this->createTeamWithOwner(); + + // Create 4 active members to fill all seats (owner occupies 1 of 5 included seats) + TeamUser::factory()->count(4)->active()->create(['team_id' => $team->id]); + + $response = $this->actingAs($owner) + ->post(route('customer.team.invite'), ['email' => 'extra@example.com']); + + $response->assertSessionHas('show_add_seats', true) + ->assertSessionHas('error'); + Notification::assertNothingSent(); + } + + // ======================================== + // Member Removal Tests + // ======================================== + + public function test_owner_can_remove_member(): void + { + Notification::fake(); + Queue::fake(); + + [$owner, $team] = $this->createTeamWithOwner(); + + $member = TeamUser::factory()->active()->create([ + 'team_id' => $team->id, + 'email' => 'member@example.com', + ]); + + $response = $this->actingAs($owner) + ->delete(route('customer.team.users.remove', $member)); + + $response->assertSessionHas('success'); + + $member->refresh(); + $this->assertEquals(TeamUserStatus::Removed, $member->status); + + Notification::assertSentOnDemand(TeamUserRemoved::class); + Queue::assertPushed(RevokeTeamUserAccessJob::class); + } + + public function test_non_owner_cannot_remove_member(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + $otherUser = User::factory()->create(); + + $member = TeamUser::factory()->active()->create(['team_id' => $team->id]); + + $response = $this->actingAs($otherUser) + ->delete(route('customer.team.users.remove', $member)); + + $response->assertSessionHas('error'); + } + + // ======================================== + // Invitation Acceptance Tests + // ======================================== + + public function test_authenticated_user_can_accept_invitation(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + + $member = User::factory()->create(['email' => 'member@example.com']); + $teamUser = TeamUser::factory()->create([ + 'team_id' => $team->id, + 'email' => 'member@example.com', + 'invitation_token' => 'test-token-123', + ]); + + $response = $this->actingAs($member) + ->get(route('team.invitation.accept', 'test-token-123')); + + $response->assertRedirect(route('dashboard')); + $response->assertSessionHas('success'); + + $teamUser->refresh(); + $this->assertEquals(TeamUserStatus::Active, $teamUser->status); + $this->assertEquals($member->id, $teamUser->user_id); + $this->assertNull($teamUser->invitation_token); + } + + public function test_email_mismatch_rejects_acceptance(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + + $otherUser = User::factory()->create(['email' => 'other@example.com']); + $teamUser = TeamUser::factory()->create([ + 'team_id' => $team->id, + 'email' => 'member@example.com', + 'invitation_token' => 'test-token-456', + ]); + + $response = $this->actingAs($otherUser) + ->get(route('team.invitation.accept', 'test-token-456')); + + $response->assertRedirect(route('dashboard')); + $response->assertSessionHas('error'); + + $teamUser->refresh(); + $this->assertEquals(TeamUserStatus::Pending, $teamUser->status); + } + + public function test_unauthenticated_user_stores_token_in_session(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + + TeamUser::factory()->create([ + 'team_id' => $team->id, + 'email' => 'newuser@example.com', + 'invitation_token' => 'test-token-789', + ]); + + $response = $this->get(route('team.invitation.accept', 'test-token-789')); + + $response->assertRedirect(route('customer.login')); + $response->assertSessionHas('pending_team_invitation_token', 'test-token-789'); + } + + public function test_invitation_accepted_after_registration(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + + $email = 'testuser@gmail.com'; + + $teamUser = TeamUser::factory()->create([ + 'team_id' => $team->id, + 'email' => $email, + 'invitation_token' => 'test-token-abc', + ]); + + $response = $this->withSession(['pending_team_invitation_token' => 'test-token-abc']) + ->post(route('customer.register'), [ + 'name' => 'New User', + 'email' => $email, + 'password' => 'password123', + 'password_confirmation' => 'password123', + ]); + + $teamUser->refresh(); + $this->assertEquals(TeamUserStatus::Active, $teamUser->status); + $this->assertNotNull($teamUser->user_id); + } + + public function test_invitation_accepted_after_login(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + + $member = User::factory()->create([ + 'email' => 'member@example.com', + 'password' => bcrypt('password123'), + ]); + + $teamUser = TeamUser::factory()->create([ + 'team_id' => $team->id, + 'email' => 'member@example.com', + 'invitation_token' => 'test-token-def', + ]); + + $response = $this->withSession(['pending_team_invitation_token' => 'test-token-def']) + ->post(route('customer.login'), [ + 'email' => 'member@example.com', + 'password' => 'password123', + ]); + + $teamUser->refresh(); + $this->assertEquals(TeamUserStatus::Active, $teamUser->status); + $this->assertEquals($member->id, $teamUser->user_id); + } + + // ======================================== + // Access Tests + // ======================================== + + public function test_team_owner_is_ultra_team_member(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + + $this->assertTrue($owner->isUltraTeamMember()); + } + + public function test_team_owner_gets_official_plugin_access(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + + $officialPlugin = Plugin::factory()->paid()->approved()->create([ + 'is_official' => true, + ]); + + $this->assertTrue($owner->hasPluginAccess($officialPlugin)); + } + + public function test_suspended_team_owner_is_not_ultra_team_member(): void + { + $owner = $this->createUltraUser(); + $team = Team::factory()->suspended()->create(['user_id' => $owner->id]); + + $this->assertFalse($owner->isUltraTeamMember()); + } + + public function test_team_member_gets_official_plugin_access(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + + $member = User::factory()->create(); + TeamUser::factory()->active()->create([ + 'team_id' => $team->id, + 'user_id' => $member->id, + 'email' => $member->email, + ]); + + $officialPlugin = Plugin::factory()->paid()->approved()->create([ + 'is_official' => true, + ]); + + $this->assertTrue($member->hasPluginAccess($officialPlugin)); + } + + public function test_team_member_does_not_get_third_party_plugin_access(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + + $member = User::factory()->create(); + TeamUser::factory()->active()->create([ + 'team_id' => $team->id, + 'user_id' => $member->id, + 'email' => $member->email, + ]); + + $thirdPartyPlugin = Plugin::factory()->paid()->approved()->create([ + 'is_official' => false, + ]); + + $this->assertFalse($member->hasPluginAccess($thirdPartyPlugin)); + } + + public function test_team_member_without_own_subscription_does_not_get_subscriber_pricing(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + + $member = User::factory()->create(); + TeamUser::factory()->active()->create([ + 'team_id' => $team->id, + 'user_id' => $member->id, + 'email' => $member->email, + ]); + + $tiers = $member->getEligiblePriceTiers(); + + $this->assertNotContains(PriceTier::Subscriber, $tiers); + } + + public function test_non_team_member_does_not_get_subscriber_pricing(): void + { + $user = User::factory()->create(); + + $tiers = $user->getEligiblePriceTiers(); + + $this->assertNotContains(PriceTier::Subscriber, $tiers); + } + + public function test_team_member_gets_product_access_via_team(): void + { + $owner = $this->createUltraUser(); + $team = Team::factory()->create(['user_id' => $owner->id]); + + $product = Product::factory()->create(['slug' => 'plugin-dev-kit']); + ProductLicense::factory()->create([ + 'user_id' => $owner->id, + 'product_id' => $product->id, + ]); + + $member = User::factory()->create(); + TeamUser::factory()->active()->create([ + 'team_id' => $team->id, + 'user_id' => $member->id, + 'email' => $member->email, + ]); + + $this->assertTrue($member->hasProductLicense($product)); + } + + // ======================================== + // Suspension Tests + // ======================================== + + public function test_team_suspended_on_subscription_cancel(): void + { + Queue::fake(); + + [$owner, $team] = $this->createTeamWithOwner(); + + $event = new WebhookReceived([ + 'type' => 'customer.subscription.deleted', + 'data' => [ + 'object' => [ + 'customer' => $owner->stripe_id ?? 'cus_test', + ], + ], + ]); + + // We can't fully simulate Stripe, but we can test the job dispatch + // by calling SuspendTeamJob directly + $job = new SuspendTeamJob($owner->id); + $job->handle(); + + $team->refresh(); + $this->assertTrue($team->is_suspended); + } + + public function test_team_unsuspended_on_resubscribe(): void + { + $owner = $this->createUltraUser(); + $team = Team::factory()->suspended()->create(['user_id' => $owner->id]); + + $job = new UnsuspendTeamJob($owner->id); + $job->handle(); + + $team->refresh(); + $this->assertFalse($team->is_suspended); + } + + public function test_suspended_team_member_loses_access(): void + { + $owner = $this->createUltraUser(); + $team = Team::factory()->suspended()->create(['user_id' => $owner->id]); + + $member = User::factory()->create(); + TeamUser::factory()->active()->create([ + 'team_id' => $team->id, + 'user_id' => $member->id, + 'email' => $member->email, + ]); + + $this->assertFalse($member->isUltraTeamMember()); + } + + public function test_removed_member_loses_plugin_access(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + + $member = User::factory()->create(); + $teamUser = TeamUser::factory()->active()->create([ + 'team_id' => $team->id, + 'user_id' => $member->id, + 'email' => $member->email, + ]); + + $officialPlugin = Plugin::factory()->paid()->approved()->create([ + 'is_official' => true, + ]); + + $this->assertTrue($member->hasPluginAccess($officialPlugin)); + + $teamUser->remove(); + + // Clear cached state + $member->refresh(); + + $this->assertFalse($member->hasPluginAccess($officialPlugin)); + } + + // ======================================== + // API Plugin Access Tests + // ======================================== + + public function test_api_returns_team_based_plugin_access(): void + { + config(['services.bifrost.api_key' => 'test-api-key']); + + [$owner, $team] = $this->createTeamWithOwner(); + + $member = User::factory()->create([ + 'plugin_license_key' => 'test-key-123', + ]); + TeamUser::factory()->active()->create([ + 'team_id' => $team->id, + 'user_id' => $member->id, + 'email' => $member->email, + ]); + + $officialPlugin = Plugin::factory()->paid()->approved()->create([ + 'is_official' => true, + 'name' => 'nativephp/test-plugin', + ]); + + $response = $this->withHeaders([ + 'X-API-Key' => 'test-api-key', + 'PHP_AUTH_USER' => $member->email, + 'PHP_AUTH_PW' => 'test-key-123', + ])->getJson('/api/plugins/access'); + + $response->assertOk(); + $response->assertJsonFragment([ + 'name' => 'nativephp/test-plugin', + 'access' => 'team', + ]); + } + + // ======================================== + // GitHub Dev Kit Access Tests + // ======================================== + + public function test_ultra_team_member_can_request_claude_plugins_access(): void + { + Http::fake(['github.com/*' => Http::response([], 201)]); + + [$owner, $team] = $this->createTeamWithOwner(); + + $member = User::factory()->create(['github_username' => 'testuser']); + TeamUser::factory()->active()->create([ + 'team_id' => $team->id, + 'user_id' => $member->id, + 'email' => $member->email, + ]); + + $response = $this->actingAs($member) + ->post(route('github.request-claude-plugins-access')); + + $response->assertSessionHas('success'); + $this->assertNotNull($member->fresh()->claude_plugins_repo_access_granted_at); + } + + public function test_team_owner_can_request_claude_plugins_access(): void + { + Http::fake(['github.com/*' => Http::response([], 201)]); + + [$owner, $team] = $this->createTeamWithOwner(); + $owner->update(['github_username' => 'owneruser']); + + $response = $this->actingAs($owner) + ->post(route('github.request-claude-plugins-access')); + + $response->assertSessionHas('success'); + $this->assertNotNull($owner->fresh()->claude_plugins_repo_access_granted_at); + } + + public function test_non_team_member_without_product_license_cannot_request_claude_plugins_access(): void + { + $user = User::factory()->create(['github_username' => 'someuser']); + + $response = $this->actingAs($user) + ->post(route('github.request-claude-plugins-access')); + + $response->assertSessionHas('error'); + $this->assertNull($user->fresh()->claude_plugins_repo_access_granted_at); + } + + // ======================================== + // Resend Invitation Tests + // ======================================== + + public function test_owner_can_resend_invitation(): void + { + Notification::fake(); + + [$owner, $team] = $this->createTeamWithOwner(); + + $invitation = TeamUser::factory()->create([ + 'team_id' => $team->id, + 'email' => 'pending@example.com', + ]); + + $response = $this->actingAs($owner) + ->post(route('customer.team.users.resend', $invitation)); + + $response->assertSessionHas('success'); + Notification::assertSentOnDemand(TeamInvitation::class); + } + + // ======================================== + // View Tests + // ======================================== + + public function test_team_page_accessible_for_authenticated_user(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->get(route('customer.team.index')); + + $response->assertOk(); + } + + public function test_team_page_shows_create_form_for_ultra_user(): void + { + $user = $this->createUltraUser(); + + $response = $this->actingAs($user)->get(route('customer.team.index')); + + $response->assertOk(); + $response->assertSee('Create a Team'); + } + + public function test_team_page_shows_view_plans_for_non_ultra(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->get(route('customer.team.index')); + + $response->assertOk(); + $response->assertSee('View Plans'); + } + + // ======================================== + // Team Name Update Tests + // ======================================== + + public function test_owner_can_update_team_name(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + + $response = $this->actingAs($owner) + ->patch(route('customer.team.update'), ['name' => 'New Team Name']); + + $response->assertRedirect(); + $response->assertSessionHas('success'); + + $this->assertDatabaseHas('teams', [ + 'id' => $team->id, + 'name' => 'New Team Name', + ]); + } + + public function test_non_owner_cannot_update_team_name(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user) + ->patch(route('customer.team.update'), ['name' => 'Hacked']); + + $response->assertSessionHas('error'); + } + + public function test_team_name_update_requires_name(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + + $response = $this->actingAs($owner) + ->patch(route('customer.team.update'), ['name' => '']); + + $response->assertSessionHasErrors('name'); + } + + public function test_team_page_shows_team_name_as_heading_for_owner(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + + $response = $this->actingAs($owner)->get(route('customer.team.index')); + + $response->assertOk(); + $response->assertSee($team->name); + } + + // ======================================== + // Team Detail Page Tests + // ======================================== + + public function test_team_member_can_view_team_detail_page(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + + $member = User::factory()->create(); + TeamUser::factory()->active()->create([ + 'team_id' => $team->id, + 'user_id' => $member->id, + 'email' => $member->email, + ]); + + $response = $this->actingAs($member) + ->get(route('customer.team.show', $team)); + + $response->assertOk(); + $response->assertSee($team->name); + $response->assertSee('Team Membership'); + } + + public function test_non_member_cannot_view_team_detail_page(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + $otherUser = User::factory()->create(); + + $response = $this->actingAs($otherUser) + ->get(route('customer.team.show', $team)); + + $response->assertForbidden(); + } + + public function test_team_owner_is_redirected_from_show_to_index(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + + $response = $this->actingAs($owner) + ->get(route('customer.team.show', $team)); + + $response->assertRedirect(route('customer.team.index')); + } + + public function test_removed_member_cannot_view_team_detail_page(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + + $member = User::factory()->create(); + TeamUser::factory()->create([ + 'team_id' => $team->id, + 'user_id' => $member->id, + 'email' => $member->email, + 'status' => TeamUserStatus::Removed, + ]); + + $response = $this->actingAs($member) + ->get(route('customer.team.show', $team)); + + $response->assertForbidden(); + } + + public function test_team_detail_page_shows_official_plugins(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + + $member = User::factory()->create(); + TeamUser::factory()->active()->create([ + 'team_id' => $team->id, + 'user_id' => $member->id, + 'email' => $member->email, + ]); + + $plugin = Plugin::factory()->approved()->paid()->create([ + 'name' => 'nativephp/official-test', + 'is_active' => true, + 'is_official' => true, + ]); + + $response = $this->actingAs($member) + ->get(route('customer.team.show', $team)); + + $response->assertOk(); + $response->assertSee('nativephp/official-test'); + $response->assertSee('Accessible Plugins'); + } + + public function test_team_detail_page_shows_owner_purchased_plugins(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + + $member = User::factory()->create(); + TeamUser::factory()->active()->create([ + 'team_id' => $team->id, + 'user_id' => $member->id, + 'email' => $member->email, + ]); + + $plugin = Plugin::factory()->approved()->paid()->create([ + 'name' => 'acme/shared-plugin', + 'is_active' => true, + ]); + + PluginLicense::factory()->create([ + 'user_id' => $owner->id, + 'plugin_id' => $plugin->id, + ]); + + $response = $this->actingAs($member) + ->get(route('customer.team.show', $team)); + + $response->assertOk(); + $response->assertSee('acme/shared-plugin'); + $response->assertSee('Accessible Plugins'); + } + + // ======================================== + // Seat Validation Tests + // ======================================== + + public function test_cannot_add_zero_seats(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + + Livewire::actingAs($owner) + ->test(TeamManager::class, ['team' => $team]) + ->call('addSeats', 0); + + $this->assertEquals(0, $team->fresh()->extra_seats); + } + + public function test_cannot_add_negative_seats(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + + Livewire::actingAs($owner) + ->test(TeamManager::class, ['team' => $team]) + ->call('addSeats', -1); + + $this->assertEquals(0, $team->fresh()->extra_seats); + } + + public function test_cannot_add_more_than_fifty_seats(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + + Livewire::actingAs($owner) + ->test(TeamManager::class, ['team' => $team]) + ->call('addSeats', 51); + + $this->assertEquals(0, $team->fresh()->extra_seats); + } + + public function test_cannot_remove_zero_seats(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + $team->update(['extra_seats' => 5]); + + Livewire::actingAs($owner) + ->test(TeamManager::class, ['team' => $team]) + ->call('removeSeats', 0); + + $this->assertEquals(5, $team->fresh()->extra_seats); + } + + public function test_cannot_remove_negative_seats(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + $team->update(['extra_seats' => 5]); + + Livewire::actingAs($owner) + ->test(TeamManager::class, ['team' => $team]) + ->call('removeSeats', -1); + + $this->assertEquals(5, $team->fresh()->extra_seats); + } + + public function test_cannot_remove_more_than_fifty_seats(): void + { + [$owner, $team] = $this->createTeamWithOwner(); + $team->update(['extra_seats' => 60]); + + Livewire::actingAs($owner) + ->test(TeamManager::class, ['team' => $team]) + ->call('removeSeats', 51); + + $this->assertEquals(60, $team->fresh()->extra_seats); + } +} diff --git a/tests/Feature/TierBasedPricingTest.php b/tests/Feature/TierBasedPricingTest.php new file mode 100644 index 00000000..95145bdb --- /dev/null +++ b/tests/Feature/TierBasedPricingTest.php @@ -0,0 +1,977 @@ + self::MINI_PRICE_ID]); + config(['subscriptions.plans.max.stripe_price_id' => self::MAX_PRICE_ID]); + } + + /** + * Create an active subscription for a user. + */ + private function createSubscription(User $user, string $priceId): Subscription + { + return Subscription::factory() + ->for($user) + ->active() + ->create(['stripe_price' => $priceId]); + } + + // ======================================== + // User Tier Eligibility Tests + // ======================================== + + #[Test] + public function user_without_license_only_qualifies_for_regular_tier(): void + { + $user = User::factory()->create(); + + $tiers = $user->getEligiblePriceTiers(); + + $this->assertCount(1, $tiers); + $this->assertContains(PriceTier::Regular, $tiers); + $this->assertNotContains(PriceTier::Subscriber, $tiers); + $this->assertNotContains(PriceTier::Eap, $tiers); + } + + #[Test] + public function user_with_active_pro_subscription_qualifies_for_subscriber_tier(): void + { + $user = User::factory()->create(); + $this->createSubscription($user, self::PRO_PRICE_ID); + + $tiers = $user->getEligiblePriceTiers(); + + $this->assertTrue($user->subscribed()); + $this->assertFalse($user->isEapCustomer()); + $this->assertContains(PriceTier::Regular, $tiers); + $this->assertContains(PriceTier::Subscriber, $tiers); + $this->assertNotContains(PriceTier::Eap, $tiers); + } + + #[Test] + public function user_with_active_max_subscription_qualifies_for_subscriber_tier(): void + { + $user = User::factory()->create(); + $this->createSubscription($user, self::MAX_PRICE_ID); + + $tiers = $user->getEligiblePriceTiers(); + + $this->assertTrue($user->subscribed()); + $this->assertFalse($user->isEapCustomer()); + $this->assertContains(PriceTier::Subscriber, $tiers); + } + + #[Test] + public function user_with_mini_subscription_qualifies_for_subscriber_tier(): void + { + $user = User::factory()->create(); + $this->createSubscription($user, self::MINI_PRICE_ID); + + $this->assertTrue($user->subscribed()); + + $tiers = $user->getEligiblePriceTiers(); + + $this->assertContains(PriceTier::Subscriber, $tiers); + } + + #[Test] + public function user_with_canceled_subscription_does_not_qualify_for_subscriber_tier(): void + { + $user = User::factory()->create(); + Subscription::factory() + ->for($user) + ->canceled() + ->create(['stripe_price' => self::PRO_PRICE_ID]); + + $this->assertFalse($user->subscribed()); + + $tiers = $user->getEligiblePriceTiers(); + + $this->assertNotContains(PriceTier::Subscriber, $tiers); + } + + #[Test] + public function user_with_past_due_subscription_does_not_qualify_for_subscriber_tier(): void + { + $user = User::factory()->create(); + Subscription::factory() + ->for($user) + ->pastDue() + ->create(['stripe_price' => self::PRO_PRICE_ID]); + + $this->assertFalse($user->subscribed()); + + $tiers = $user->getEligiblePriceTiers(); + + $this->assertNotContains(PriceTier::Subscriber, $tiers); + } + + #[Test] + public function user_with_eap_license_qualifies_for_eap_tier(): void + { + $user = User::factory()->create(); + License::factory() + ->for($user) + ->mini() + ->active() + ->eapEligible() + ->withoutSubscriptionItem() + ->create(); + + $this->assertTrue($user->isEapCustomer()); + + $tiers = $user->getEligiblePriceTiers(); + + $this->assertContains(PriceTier::Eap, $tiers); + } + + #[Test] + public function user_with_subscription_and_eap_license_qualifies_for_both_tiers(): void + { + $user = User::factory()->create(); + $this->createSubscription($user, self::PRO_PRICE_ID); + // EAP eligibility is still determined by licenses table + License::factory() + ->for($user) + ->mini() + ->active() + ->eapEligible() + ->withoutSubscriptionItem() + ->create(); + + $tiers = $user->getEligiblePriceTiers(); + + $this->assertTrue($user->subscribed()); + $this->assertTrue($user->isEapCustomer()); + $this->assertContains(PriceTier::Regular, $tiers); + $this->assertContains(PriceTier::Subscriber, $tiers); + $this->assertContains(PriceTier::Eap, $tiers); + $this->assertCount(3, $tiers); + } + + // ======================================== + // Ultra Subscription Access Tests + // ======================================== + + #[Test] + public function user_with_max_subscription_has_active_ultra_subscription(): void + { + $user = User::factory()->create(); + $this->createSubscription($user, self::MAX_PRICE_ID); + + $this->assertTrue($user->hasActiveUltraSubscription()); + } + + #[Test] + public function user_with_pro_subscription_does_not_have_active_ultra_subscription(): void + { + $user = User::factory()->create(); + $this->createSubscription($user, self::PRO_PRICE_ID); + + $this->assertFalse($user->hasActiveUltraSubscription()); + } + + #[Test] + public function user_with_mini_subscription_does_not_have_active_ultra_subscription(): void + { + $user = User::factory()->create(); + $this->createSubscription($user, self::MINI_PRICE_ID); + + $this->assertFalse($user->hasActiveUltraSubscription()); + } + + #[Test] + public function user_without_subscription_does_not_have_active_ultra_subscription(): void + { + $user = User::factory()->create(); + + $this->assertFalse($user->hasActiveUltraSubscription()); + } + + // ======================================== + // Plugin Tier Pricing Tests + // ======================================== + + #[Test] + public function guest_user_sees_regular_plugin_price(): void + { + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true]); + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]); + PluginPrice::factory()->subscriber()->amount(1999)->create(['plugin_id' => $plugin->id]); + PluginPrice::factory()->eap()->amount(999)->create(['plugin_id' => $plugin->id]); + + $bestPrice = $plugin->getBestPriceForUser(null); + + $this->assertNotNull($bestPrice); + $this->assertEquals(2999, $bestPrice->amount); + $this->assertEquals(PriceTier::Regular, $bestPrice->tier); + } + + #[Test] + public function user_without_license_sees_regular_plugin_price(): void + { + $user = User::factory()->create(); + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true]); + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]); + PluginPrice::factory()->subscriber()->amount(1999)->create(['plugin_id' => $plugin->id]); + PluginPrice::factory()->eap()->amount(999)->create(['plugin_id' => $plugin->id]); + + $bestPrice = $plugin->getBestPriceForUser($user); + + $this->assertNotNull($bestPrice); + $this->assertEquals(2999, $bestPrice->amount); + $this->assertEquals(PriceTier::Regular, $bestPrice->tier); + } + + #[Test] + public function subscriber_sees_subscriber_plugin_price_for_official_plugin(): void + { + $user = User::factory()->create(); + $this->createSubscription($user, self::PRO_PRICE_ID); + + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true, 'is_official' => true]); + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]); + PluginPrice::factory()->subscriber()->amount(1999)->create(['plugin_id' => $plugin->id]); + PluginPrice::factory()->eap()->amount(999)->create(['plugin_id' => $plugin->id]); + + $bestPrice = $plugin->getBestPriceForUser($user); + + $this->assertNotNull($bestPrice); + $this->assertEquals(1999, $bestPrice->amount); + $this->assertEquals(PriceTier::Subscriber, $bestPrice->tier); + } + + #[Test] + public function eap_customer_sees_eap_plugin_price_for_official_plugin(): void + { + $user = User::factory()->create(); + License::factory() + ->for($user) + ->mini() + ->active() + ->eapEligible() + ->withoutSubscriptionItem() + ->create(); + + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true, 'is_official' => true]); + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]); + PluginPrice::factory()->subscriber()->amount(1999)->create(['plugin_id' => $plugin->id]); + PluginPrice::factory()->eap()->amount(999)->create(['plugin_id' => $plugin->id]); + + $bestPrice = $plugin->getBestPriceForUser($user); + + $this->assertNotNull($bestPrice); + $this->assertEquals(999, $bestPrice->amount); + $this->assertEquals(PriceTier::Eap, $bestPrice->tier); + } + + #[Test] + public function user_qualifying_for_multiple_tiers_sees_lowest_official_plugin_price(): void + { + $user = User::factory()->create(); + $this->createSubscription($user, self::PRO_PRICE_ID); + License::factory() + ->for($user) + ->pro() + ->active() + ->eapEligible() + ->withoutSubscriptionItem() + ->create(); + + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true, 'is_official' => true]); + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]); + PluginPrice::factory()->subscriber()->amount(1999)->create(['plugin_id' => $plugin->id]); + PluginPrice::factory()->eap()->amount(999)->create(['plugin_id' => $plugin->id]); + + $bestPrice = $plugin->getBestPriceForUser($user); + + $this->assertNotNull($bestPrice); + $this->assertEquals(999, $bestPrice->amount); + } + + #[Test] + public function subscriber_sees_subscriber_price_when_it_is_lower_than_eap_for_official_plugin(): void + { + $user = User::factory()->create(); + $this->createSubscription($user, self::PRO_PRICE_ID); + License::factory() + ->for($user) + ->pro() + ->active() + ->eapEligible() + ->withoutSubscriptionItem() + ->create(); + + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true, 'is_official' => true]); + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]); + PluginPrice::factory()->subscriber()->amount(500)->create(['plugin_id' => $plugin->id]); + PluginPrice::factory()->eap()->amount(999)->create(['plugin_id' => $plugin->id]); + + $bestPrice = $plugin->getBestPriceForUser($user); + + $this->assertEquals(500, $bestPrice->amount); + $this->assertEquals(PriceTier::Subscriber, $bestPrice->tier); + } + + #[Test] + public function plugin_falls_back_to_regular_price_when_user_tier_not_available(): void + { + $user = User::factory()->create(); + $this->createSubscription($user, self::PRO_PRICE_ID); + + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true]); + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]); + + $bestPrice = $plugin->getBestPriceForUser($user); + + $this->assertNotNull($bestPrice); + $this->assertEquals(2999, $bestPrice->amount); + $this->assertEquals(PriceTier::Regular, $bestPrice->tier); + } + + #[Test] + public function plugin_inactive_prices_are_not_returned(): void + { + $user = User::factory()->create(); + + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true]); + PluginPrice::factory()->regular()->amount(2999)->inactive()->create(['plugin_id' => $plugin->id]); + PluginPrice::factory()->regular()->amount(3999)->create(['plugin_id' => $plugin->id]); + + $bestPrice = $plugin->getBestPriceForUser($user); + + $this->assertNotNull($bestPrice); + $this->assertEquals(3999, $bestPrice->amount); + } + + #[Test] + public function get_regular_price_returns_regular_tier_price(): void + { + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true]); + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]); + PluginPrice::factory()->subscriber()->amount(1999)->create(['plugin_id' => $plugin->id]); + + $regularPrice = $plugin->getRegularPrice(); + + $this->assertNotNull($regularPrice); + $this->assertEquals(2999, $regularPrice->amount); + $this->assertEquals(PriceTier::Regular, $regularPrice->tier); + } + + // ======================================== + // Bundle Tier Pricing Tests + // ======================================== + + #[Test] + public function guest_user_sees_regular_bundle_price(): void + { + $bundle = PluginBundle::factory()->active()->create(); + BundlePrice::factory()->regular()->amount(9999)->create(['plugin_bundle_id' => $bundle->id]); + BundlePrice::factory()->subscriber()->amount(7999)->create(['plugin_bundle_id' => $bundle->id]); + BundlePrice::factory()->eap()->amount(4999)->create(['plugin_bundle_id' => $bundle->id]); + + $bestPrice = $bundle->getBestPriceForUser(null); + + $this->assertNotNull($bestPrice); + $this->assertEquals(9999, $bestPrice->amount); + $this->assertEquals(PriceTier::Regular, $bestPrice->tier); + } + + #[Test] + public function user_without_license_sees_regular_bundle_price(): void + { + $user = User::factory()->create(); + $bundle = PluginBundle::factory()->active()->create(); + BundlePrice::factory()->regular()->amount(9999)->create(['plugin_bundle_id' => $bundle->id]); + BundlePrice::factory()->subscriber()->amount(7999)->create(['plugin_bundle_id' => $bundle->id]); + BundlePrice::factory()->eap()->amount(4999)->create(['plugin_bundle_id' => $bundle->id]); + + $bestPrice = $bundle->getBestPriceForUser($user); + + $this->assertNotNull($bestPrice); + $this->assertEquals(9999, $bestPrice->amount); + $this->assertEquals(PriceTier::Regular, $bestPrice->tier); + } + + #[Test] + public function subscriber_sees_subscriber_bundle_price(): void + { + $user = User::factory()->create(); + $this->createSubscription($user, self::MAX_PRICE_ID); + + $bundle = PluginBundle::factory()->active()->create(); + BundlePrice::factory()->regular()->amount(9999)->create(['plugin_bundle_id' => $bundle->id]); + BundlePrice::factory()->subscriber()->amount(7999)->create(['plugin_bundle_id' => $bundle->id]); + BundlePrice::factory()->eap()->amount(4999)->create(['plugin_bundle_id' => $bundle->id]); + + $bestPrice = $bundle->getBestPriceForUser($user); + + $this->assertNotNull($bestPrice); + $this->assertEquals(7999, $bestPrice->amount); + $this->assertEquals(PriceTier::Subscriber, $bestPrice->tier); + } + + #[Test] + public function eap_customer_sees_eap_bundle_price(): void + { + $user = User::factory()->create(); + License::factory() + ->for($user) + ->mini() + ->active() + ->eapEligible() + ->withoutSubscriptionItem() + ->create(); + + $bundle = PluginBundle::factory()->active()->create(); + BundlePrice::factory()->regular()->amount(9999)->create(['plugin_bundle_id' => $bundle->id]); + BundlePrice::factory()->subscriber()->amount(7999)->create(['plugin_bundle_id' => $bundle->id]); + BundlePrice::factory()->eap()->amount(4999)->create(['plugin_bundle_id' => $bundle->id]); + + $bestPrice = $bundle->getBestPriceForUser($user); + + $this->assertNotNull($bestPrice); + $this->assertEquals(4999, $bestPrice->amount); + $this->assertEquals(PriceTier::Eap, $bestPrice->tier); + } + + #[Test] + public function user_qualifying_for_multiple_tiers_sees_lowest_bundle_price(): void + { + $user = User::factory()->create(); + $this->createSubscription($user, self::MAX_PRICE_ID); + License::factory() + ->for($user) + ->mini() + ->active() + ->eapEligible() + ->withoutSubscriptionItem() + ->create(); + + $bundle = PluginBundle::factory()->active()->create(); + BundlePrice::factory()->regular()->amount(9999)->create(['plugin_bundle_id' => $bundle->id]); + BundlePrice::factory()->subscriber()->amount(7999)->create(['plugin_bundle_id' => $bundle->id]); + BundlePrice::factory()->eap()->amount(4999)->create(['plugin_bundle_id' => $bundle->id]); + + $bestPrice = $bundle->getBestPriceForUser($user); + + $this->assertNotNull($bestPrice); + $this->assertEquals(4999, $bestPrice->amount); + } + + #[Test] + public function bundle_falls_back_to_legacy_price_when_no_tier_prices_exist(): void + { + $user = User::factory()->create(); + $bundle = PluginBundle::factory()->active()->create(['price' => 14999]); + + $bestPrice = $bundle->getBestPriceForUser($user); + + $this->assertNull($bestPrice); + + $regularPrice = $bundle->getRegularPrice(); + $this->assertNull($regularPrice); + + $this->assertEquals('$149.99', $bundle->formatted_price); + } + + #[Test] + public function get_regular_bundle_price_returns_regular_tier_price(): void + { + $bundle = PluginBundle::factory()->active()->create(); + BundlePrice::factory()->regular()->amount(9999)->create(['plugin_bundle_id' => $bundle->id]); + BundlePrice::factory()->subscriber()->amount(7999)->create(['plugin_bundle_id' => $bundle->id]); + + $regularPrice = $bundle->getRegularPrice(); + + $this->assertNotNull($regularPrice); + $this->assertEquals(9999, $regularPrice->amount); + $this->assertEquals(PriceTier::Regular, $regularPrice->tier); + } + + // ======================================== + // Cart Tier Pricing Tests + // ======================================== + + #[Test] + public function cart_adds_plugin_at_regular_price_for_user_without_license(): void + { + $user = User::factory()->create(); + $cart = Cart::factory()->for($user)->create(); + + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true]); + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]); + PluginPrice::factory()->subscriber()->amount(1999)->create(['plugin_id' => $plugin->id]); + + $cartService = new CartService; + $item = $cartService->addPlugin($cart, $plugin); + + $this->assertEquals(2999, $item->price_at_addition); + $this->assertEquals(PriceTier::Regular, $item->pluginPrice->tier); + } + + #[Test] + public function cart_adds_official_plugin_at_subscriber_price_for_pro_user(): void + { + $user = User::factory()->create(); + $this->createSubscription($user, self::PRO_PRICE_ID); + $cart = Cart::factory()->for($user)->create(); + + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true, 'is_official' => true]); + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]); + PluginPrice::factory()->subscriber()->amount(1999)->create(['plugin_id' => $plugin->id]); + + $cartService = new CartService; + $item = $cartService->addPlugin($cart, $plugin); + + $this->assertEquals(1999, $item->price_at_addition); + $this->assertEquals(PriceTier::Subscriber, $item->pluginPrice->tier); + } + + #[Test] + public function cart_adds_official_plugin_at_eap_price_for_eap_customer(): void + { + $user = User::factory()->create(); + License::factory() + ->for($user) + ->mini() + ->active() + ->eapEligible() + ->withoutSubscriptionItem() + ->create(); + $cart = Cart::factory()->for($user)->create(); + + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true, 'is_official' => true]); + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]); + PluginPrice::factory()->subscriber()->amount(1999)->create(['plugin_id' => $plugin->id]); + PluginPrice::factory()->eap()->amount(999)->create(['plugin_id' => $plugin->id]); + + $cartService = new CartService; + $item = $cartService->addPlugin($cart, $plugin); + + $this->assertEquals(999, $item->price_at_addition); + $this->assertEquals(PriceTier::Eap, $item->pluginPrice->tier); + } + + #[Test] + public function cart_adds_bundle_at_regular_price_for_user_without_license(): void + { + $user = User::factory()->create(); + $cart = Cart::factory()->for($user)->create(); + + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true]); + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]); + + $bundle = PluginBundle::factory()->active()->create(); + $bundle->plugins()->attach($plugin->id); + BundlePrice::factory()->regular()->amount(9999)->create(['plugin_bundle_id' => $bundle->id]); + BundlePrice::factory()->subscriber()->amount(7999)->create(['plugin_bundle_id' => $bundle->id]); + + $cartService = new CartService; + $item = $cartService->addBundle($cart, $bundle); + + $this->assertEquals(9999, $item->bundle_price_at_addition); + } + + #[Test] + public function cart_adds_bundle_at_subscriber_price_for_max_user(): void + { + $user = User::factory()->create(); + $this->createSubscription($user, self::MAX_PRICE_ID); + $cart = Cart::factory()->for($user)->create(); + + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true]); + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]); + + $bundle = PluginBundle::factory()->active()->create(); + $bundle->plugins()->attach($plugin->id); + BundlePrice::factory()->regular()->amount(9999)->create(['plugin_bundle_id' => $bundle->id]); + BundlePrice::factory()->subscriber()->amount(7999)->create(['plugin_bundle_id' => $bundle->id]); + + $cartService = new CartService; + $item = $cartService->addBundle($cart, $bundle); + + $this->assertEquals(7999, $item->bundle_price_at_addition); + } + + #[Test] + public function cart_adds_bundle_at_eap_price_for_eap_customer(): void + { + $user = User::factory()->create(); + License::factory() + ->for($user) + ->mini() + ->active() + ->eapEligible() + ->withoutSubscriptionItem() + ->create(); + $cart = Cart::factory()->for($user)->create(); + + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true]); + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]); + + $bundle = PluginBundle::factory()->active()->create(); + $bundle->plugins()->attach($plugin->id); + BundlePrice::factory()->regular()->amount(9999)->create(['plugin_bundle_id' => $bundle->id]); + BundlePrice::factory()->subscriber()->amount(7999)->create(['plugin_bundle_id' => $bundle->id]); + BundlePrice::factory()->eap()->amount(4999)->create(['plugin_bundle_id' => $bundle->id]); + + $cartService = new CartService; + $item = $cartService->addBundle($cart, $bundle); + + $this->assertEquals(4999, $item->bundle_price_at_addition); + } + + #[Test] + public function cart_refresh_prices_updates_to_current_tier_price_for_official_plugin(): void + { + $user = User::factory()->create(); + $cart = Cart::factory()->for($user)->create(); + + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true, 'is_official' => true]); + $regularPrice = PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]); + + $cartService = new CartService; + $item = $cartService->addPlugin($cart, $plugin); + $this->assertEquals(2999, $item->price_at_addition); + + $this->createSubscription($user, self::PRO_PRICE_ID); + + PluginPrice::factory()->subscriber()->amount(1999)->create(['plugin_id' => $plugin->id]); + + $user->refresh(); + $changes = $cartService->refreshPrices($cart->fresh()); + + $this->assertCount(1, $changes); + $this->assertEquals('price_changed', $changes[0]['type']); + $this->assertEquals(2999, $changes[0]['old_price']); + $this->assertEquals(1999, $changes[0]['new_price']); + + $item->refresh(); + $this->assertEquals(1999, $item->price_at_addition); + } + + #[Test] + public function cart_exchange_for_bundle_uses_correct_tier_price(): void + { + $user = User::factory()->create(); + $this->createSubscription($user, self::PRO_PRICE_ID); + $cart = Cart::factory()->for($user)->create(); + + $plugin1 = Plugin::factory()->approved()->paid()->create(['is_active' => true]); + $plugin2 = Plugin::factory()->approved()->paid()->create(['is_active' => true]); + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin1->id]); + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin2->id]); + + $bundle = PluginBundle::factory()->active()->create(); + $bundle->plugins()->attach([$plugin1->id, $plugin2->id]); + BundlePrice::factory()->regular()->amount(4999)->create(['plugin_bundle_id' => $bundle->id]); + BundlePrice::factory()->subscriber()->amount(3999)->create(['plugin_bundle_id' => $bundle->id]); + + $cartService = new CartService; + $cartService->addPlugin($cart, $plugin1); + $cartService->addPlugin($cart, $plugin2); + + $this->assertEquals(2, $cart->items()->count()); + + $bundleItem = $cartService->exchangeForBundle($cart->fresh(), $bundle); + + $this->assertEquals(1, $cart->fresh()->items()->count()); + $this->assertEquals(3999, $bundleItem->bundle_price_at_addition); + } + + // ======================================== + // Edge Case Tests + // ======================================== + + #[Test] + public function subscriber_who_cancels_subscription_sees_regular_price_for_official_plugin(): void + { + $user = User::factory()->create(); + $subscription = $this->createSubscription($user, self::PRO_PRICE_ID); + + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true, 'is_official' => true]); + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]); + PluginPrice::factory()->subscriber()->amount(1999)->create(['plugin_id' => $plugin->id]); + + $this->assertEquals(1999, $plugin->getBestPriceForUser($user)->amount); + + $subscription->update(['stripe_status' => 'canceled', 'ends_at' => now()]); + $user->refresh(); + + $this->assertEquals(2999, $plugin->getBestPriceForUser($user)->amount); + } + + #[Test] + public function user_with_only_eap_tier_price_available_sees_that_price_for_official_plugin(): void + { + $user = User::factory()->create(); + License::factory() + ->for($user) + ->mini() + ->active() + ->eapEligible() + ->withoutSubscriptionItem() + ->create(); + + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true, 'is_official' => true]); + PluginPrice::factory()->eap()->amount(999)->create(['plugin_id' => $plugin->id]); + + $bestPrice = $plugin->getBestPriceForUser($user); + + $this->assertNotNull($bestPrice); + $this->assertEquals(999, $bestPrice->amount); + } + + #[Test] + public function regular_user_gets_null_when_no_regular_tier_price_exists(): void + { + $user = User::factory()->create(); + + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true]); + PluginPrice::factory()->subscriber()->amount(1999)->create(['plugin_id' => $plugin->id]); + + $bestPrice = $plugin->getBestPriceForUser($user); + + $this->assertNull($bestPrice); + $this->assertFalse($plugin->hasAccessiblePriceFor($user)); + } + + #[Test] + public function subscriber_can_access_official_plugin_with_only_subscriber_tier_price(): void + { + $user = User::factory()->create(); + $this->createSubscription($user, self::PRO_PRICE_ID); + + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true, 'is_official' => true]); + PluginPrice::factory()->subscriber()->amount(1999)->create(['plugin_id' => $plugin->id]); + + $bestPrice = $plugin->getBestPriceForUser($user); + + $this->assertNotNull($bestPrice); + $this->assertEquals(1999, $bestPrice->amount); + $this->assertTrue($plugin->hasAccessiblePriceFor($user)); + } + + #[Test] + public function bundle_without_regular_tier_price_is_not_accessible_to_regular_user(): void + { + $user = User::factory()->create(); + + $bundle = PluginBundle::factory()->active()->create(); + BundlePrice::factory()->subscriber()->amount(7999)->create(['plugin_bundle_id' => $bundle->id]); + + $bestPrice = $bundle->getBestPriceForUser($user); + + $this->assertNull($bestPrice); + $this->assertFalse($bundle->hasAccessiblePriceFor($user)); + } + + #[Test] + public function subscriber_can_access_bundle_with_only_subscriber_tier_price(): void + { + $user = User::factory()->create(); + $this->createSubscription($user, self::MAX_PRICE_ID); + + $bundle = PluginBundle::factory()->active()->create(); + BundlePrice::factory()->subscriber()->amount(7999)->create(['plugin_bundle_id' => $bundle->id]); + + $bestPrice = $bundle->getBestPriceForUser($user); + + $this->assertNotNull($bestPrice); + $this->assertEquals(7999, $bestPrice->amount); + $this->assertTrue($bundle->hasAccessiblePriceFor($user)); + } + + #[Test] + public function free_plugin_is_always_accessible(): void + { + $user = User::factory()->create(); + + $plugin = Plugin::factory()->approved()->free()->create(['is_active' => true]); + + $this->assertTrue($plugin->isFree()); + $this->assertNull($plugin->getBestPriceForUser($user)); + } + + // ======================================== + // Access Control Tests + // ======================================== + + #[Test] + public function inaccessible_paid_plugin_returns_404(): void + { + $user = User::factory()->create(); + + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true]); + PluginPrice::factory()->subscriber()->amount(1999)->create(['plugin_id' => $plugin->id]); + + $this->actingAs($user) + ->get(route('plugins.show', $plugin->routeParams())) + ->assertNotFound(); + } + + #[Test] + public function accessible_paid_official_plugin_returns_200(): void + { + $user = User::factory()->create(); + $this->createSubscription($user, self::PRO_PRICE_ID); + + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true, 'is_official' => true]); + PluginPrice::factory()->subscriber()->amount(1999)->create(['plugin_id' => $plugin->id]); + + $this->actingAs($user) + ->get(route('plugins.show', $plugin->routeParams())) + ->assertOk(); + } + + #[Test] + public function inaccessible_bundle_returns_404(): void + { + $user = User::factory()->create(); + + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true]); + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]); + + $bundle = PluginBundle::factory()->active()->create(); + $bundle->plugins()->attach($plugin->id); + BundlePrice::factory()->subscriber()->amount(7999)->create(['plugin_bundle_id' => $bundle->id]); + + $this->actingAs($user) + ->get(route('bundles.show', $bundle)) + ->assertNotFound(); + } + + #[Test] + public function accessible_bundle_returns_200(): void + { + $user = User::factory()->create(); + $this->createSubscription($user, self::MAX_PRICE_ID); + + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true]); + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]); + + $bundle = PluginBundle::factory()->active()->create(); + $bundle->plugins()->attach($plugin->id); + BundlePrice::factory()->subscriber()->amount(7999)->create(['plugin_bundle_id' => $bundle->id]); + + $this->actingAs($user) + ->get(route('bundles.show', $bundle)) + ->assertOk(); + } + + // ======================================== + // Third-Party Plugin Pricing Tests + // ======================================== + + #[Test] + public function third_party_plugin_always_returns_regular_price_for_subscriber(): void + { + $user = User::factory()->create(); + $this->createSubscription($user, self::PRO_PRICE_ID); + + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true, 'is_official' => false]); + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]); + PluginPrice::factory()->subscriber()->amount(1999)->create(['plugin_id' => $plugin->id]); + + $bestPrice = $plugin->getBestPriceForUser($user); + + $this->assertNotNull($bestPrice); + $this->assertEquals(2999, $bestPrice->amount); + $this->assertEquals(PriceTier::Regular, $bestPrice->tier); + } + + #[Test] + public function third_party_plugin_always_returns_regular_price_for_eap_customer(): void + { + $user = User::factory()->create(); + License::factory() + ->for($user) + ->mini() + ->active() + ->eapEligible() + ->withoutSubscriptionItem() + ->create(); + + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true, 'is_official' => false]); + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]); + PluginPrice::factory()->eap()->amount(999)->create(['plugin_id' => $plugin->id]); + + $bestPrice = $plugin->getBestPriceForUser($user); + + $this->assertNotNull($bestPrice); + $this->assertEquals(2999, $bestPrice->amount); + $this->assertEquals(PriceTier::Regular, $bestPrice->tier); + } + + #[Test] + public function third_party_plugin_always_returns_regular_price_for_max_subscriber(): void + { + $user = User::factory()->create(); + $this->createSubscription($user, self::MAX_PRICE_ID); + + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true, 'is_official' => false]); + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]); + PluginPrice::factory()->subscriber()->amount(1999)->create(['plugin_id' => $plugin->id]); + PluginPrice::factory()->eap()->amount(999)->create(['plugin_id' => $plugin->id]); + + $bestPrice = $plugin->getBestPriceForUser($user); + + $this->assertNotNull($bestPrice); + $this->assertEquals(2999, $bestPrice->amount); + $this->assertEquals(PriceTier::Regular, $bestPrice->tier); + } + + #[Test] + public function cart_adds_third_party_plugin_at_regular_price_for_subscriber(): void + { + $user = User::factory()->create(); + $this->createSubscription($user, self::PRO_PRICE_ID); + $cart = Cart::factory()->for($user)->create(); + + $plugin = Plugin::factory()->approved()->paid()->create(['is_active' => true, 'is_official' => false]); + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]); + PluginPrice::factory()->subscriber()->amount(1999)->create(['plugin_id' => $plugin->id]); + + $cartService = new CartService; + $item = $cartService->addPlugin($cart, $plugin); + + $this->assertEquals(2999, $item->price_at_addition); + $this->assertEquals(PriceTier::Regular, $item->pluginPrice->tier); + } +} diff --git a/tests/Feature/UltraBenefitsPageTest.php b/tests/Feature/UltraBenefitsPageTest.php new file mode 100644 index 00000000..950b1792 --- /dev/null +++ b/tests/Feature/UltraBenefitsPageTest.php @@ -0,0 +1,96 @@ +create([ + 'stripe_id' => 'cus_'.uniqid(), + ]); + + $subscription = \Laravel\Cashier\Subscription::factory() + ->for($user) + ->active() + ->create([ + 'stripe_price' => Subscription::Max->stripePriceId(), + 'is_comped' => false, + ]); + + SubscriptionItem::factory() + ->for($subscription, 'subscription') + ->create([ + 'stripe_price' => Subscription::Max->stripePriceId(), + 'quantity' => 1, + ]); + + return $user; + } + + public function test_ultra_subscriber_can_access_benefits_page(): void + { + $user = $this->createUltraSubscriber(); + + $response = $this->actingAs($user)->get(route('customer.ultra.index')); + + $response->assertStatus(200); + } + + public function test_non_ultra_user_is_redirected_to_pricing(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->get(route('customer.ultra.index')); + + $response->assertRedirect(route('pricing')); + } + + public function test_guest_is_redirected_to_login(): void + { + $response = $this->get(route('customer.ultra.index')); + + $response->assertRedirect(route('customer.login')); + } + + public function test_benefits_page_displays_expected_content(): void + { + $user = $this->createUltraSubscriber(); + + $response = $this->actingAs($user)->get(route('customer.ultra.index')); + + $response->assertSee('Ultra'); + $response->assertSee('Your premium subscription benefits'); + $response->assertSee('All first-party plugins'); + $response->assertSee('Claude Code Plugin Dev Kit'); + $response->assertSee('Teams'); + $response->assertSee('Premium support'); + $response->assertSee('Up to 90% Marketplace revenue'); + $response->assertSee('Exclusive discounts'); + $response->assertSee('Direct repo access on GitHub'); + $response->assertSee('Shape the roadmap'); + } + + public function test_benefits_page_uses_config_values(): void + { + $user = $this->createUltraSubscriber(); + + $response = $this->actingAs($user)->get(route('customer.ultra.index')); + + $includedSeats = config('subscriptions.plans.max.included_seats'); + $extraSeatMonthly = config('subscriptions.plans.max.extra_seat_price_monthly'); + $extraSeatYearly = config('subscriptions.plans.max.extra_seat_price_yearly'); + + $response->assertSee("{$includedSeats} seats included"); + $response->assertSee("\${$extraSeatMonthly}/mo"); + $response->assertSee("\${$extraSeatYearly}/mo on annual"); + } +} diff --git a/tests/Feature/UltraClaudeCodeAccessTest.php b/tests/Feature/UltraClaudeCodeAccessTest.php new file mode 100644 index 00000000..7b75d72c --- /dev/null +++ b/tests/Feature/UltraClaudeCodeAccessTest.php @@ -0,0 +1,192 @@ + self::MAX_PRICE_ID]); + } + + private function createUltraUser(): User + { + $user = User::factory()->create(['stripe_id' => 'cus_'.uniqid()]); + + CashierSubscription::factory()->for($user)->active()->create([ + 'stripe_price' => self::MAX_PRICE_ID, + ]); + + return $user; + } + + // ======================================== + // Banner Visibility Tests + // ======================================== + + public function test_ultra_subscriber_sees_claude_plugins_banner(): void + { + Http::fake(['github.com/*' => Http::response([], 200)]); + + $user = $this->createUltraUser(); + + $response = $this->actingAs($user)->get(route('customer.integrations')); + + $response->assertStatus(200); + $response->assertSee('Repo Access'); + } + + public function test_plugin_dev_kit_license_holder_sees_claude_plugins_banner(): void + { + Http::fake(['github.com/*' => Http::response([], 200)]); + + $user = User::factory()->create(); + $product = Product::factory()->create(['slug' => 'plugin-dev-kit']); + ProductLicense::factory()->create([ + 'user_id' => $user->id, + 'product_id' => $product->id, + ]); + + $response = $this->actingAs($user)->get(route('customer.integrations')); + + $response->assertStatus(200); + $response->assertSee('Repo Access'); + } + + public function test_non_ultra_non_licensed_user_does_not_see_claude_plugins_banner(): void + { + $user = User::factory()->create(); + + $response = $this->actingAs($user)->get(route('customer.integrations')); + + $response->assertStatus(200); + $response->assertDontSee('Repo Access'); + } + + // ======================================== + // Request Access Tests + // ======================================== + + public function test_ultra_subscriber_can_request_claude_plugins_access(): void + { + Http::fake(['github.com/*' => Http::response([], 201)]); + + $user = $this->createUltraUser(); + $user->update(['github_username' => 'ultrauser']); + + $response = $this->actingAs($user) + ->post(route('github.request-claude-plugins-access')); + + $response->assertSessionHas('success'); + $this->assertNotNull($user->fresh()->claude_plugins_repo_access_granted_at); + } + + public function test_non_ultra_non_licensed_user_cannot_request_claude_plugins_access(): void + { + $user = User::factory()->create(['github_username' => 'someuser']); + + $response = $this->actingAs($user) + ->post(route('github.request-claude-plugins-access')); + + $response->assertSessionHas('error'); + $this->assertNull($user->fresh()->claude_plugins_repo_access_granted_at); + } + + // ======================================== + // Subscription Revocation Tests + // ======================================== + + public function test_revoke_job_dispatched_when_subscription_deleted(): void + { + Queue::fake(); + + $user = $this->createUltraUser(); + + $event = new WebhookReceived([ + 'type' => 'customer.subscription.deleted', + 'data' => [ + 'object' => [ + 'customer' => $user->stripe_id, + ], + ], + ]); + + $listener = new StripeWebhookReceivedListener; + $listener->handle($event); + + Queue::assertPushed(RevokeTeamUserAccessJob::class, function ($job) use ($user) { + return $job->userId === $user->id; + }); + } + + public function test_revoke_job_dispatched_when_subscription_canceled(): void + { + Queue::fake(); + + $user = $this->createUltraUser(); + + $event = new WebhookReceived([ + 'type' => 'customer.subscription.updated', + 'data' => [ + 'object' => [ + 'customer' => $user->stripe_id, + 'status' => 'canceled', + ], + 'previous_attributes' => [ + 'status' => 'active', + ], + ], + ]); + + $listener = new StripeWebhookReceivedListener; + $listener->handle($event); + + Queue::assertPushed(RevokeTeamUserAccessJob::class, function ($job) use ($user) { + return $job->userId === $user->id; + }); + } + + public function test_revoke_job_not_dispatched_when_subscription_reactivated(): void + { + Queue::fake(); + + $user = $this->createUltraUser(); + + $event = new WebhookReceived([ + 'type' => 'customer.subscription.updated', + 'data' => [ + 'object' => [ + 'customer' => $user->stripe_id, + 'status' => 'active', + ], + 'previous_attributes' => [ + 'status' => 'canceled', + ], + ], + ]); + + $listener = new StripeWebhookReceivedListener; + $listener->handle($event); + + Queue::assertNotPushed(RevokeTeamUserAccessJob::class); + } +} diff --git a/tests/Feature/UltraPluginAccessTest.php b/tests/Feature/UltraPluginAccessTest.php new file mode 100644 index 00000000..cd1304d1 --- /dev/null +++ b/tests/Feature/UltraPluginAccessTest.php @@ -0,0 +1,807 @@ + self::COMPED_ULTRA_PRICE_ID]); + } + + private function createCompedUltraSubscription(User $user): \Laravel\Cashier\Subscription + { + $user->update(['stripe_id' => 'cus_'.uniqid()]); + + $subscription = \Laravel\Cashier\Subscription::factory() + ->for($user) + ->active() + ->create([ + 'stripe_price' => self::COMPED_ULTRA_PRICE_ID, + ]); + + SubscriptionItem::factory() + ->for($subscription, 'subscription') + ->create([ + 'stripe_price' => self::COMPED_ULTRA_PRICE_ID, + 'quantity' => 1, + ]); + + return $subscription; + } + + private function createPaidMaxSubscription(User $user): \Laravel\Cashier\Subscription + { + $user->update(['stripe_id' => 'cus_'.uniqid()]); + + $subscription = \Laravel\Cashier\Subscription::factory() + ->for($user) + ->active() + ->create([ + 'stripe_price' => Subscription::Max->stripePriceId(), + 'is_comped' => false, + ]); + + SubscriptionItem::factory() + ->for($subscription, 'subscription') + ->create([ + 'stripe_price' => Subscription::Max->stripePriceId(), + 'quantity' => 1, + ]); + + return $subscription; + } + + private function createCompedMaxSubscription(User $user): \Laravel\Cashier\Subscription + { + $user->update(['stripe_id' => 'cus_'.uniqid()]); + + $subscription = \Laravel\Cashier\Subscription::factory() + ->for($user) + ->active() + ->create([ + 'stripe_price' => Subscription::Max->stripePriceId(), + 'is_comped' => true, + ]); + + SubscriptionItem::factory() + ->for($subscription, 'subscription') + ->create([ + 'stripe_price' => Subscription::Max->stripePriceId(), + 'quantity' => 1, + ]); + + return $subscription; + } + + private function createOfficialPlugin(): Plugin + { + $plugin = Plugin::factory()->approved()->paid()->create([ + 'name' => 'nativephp/test-plugin', + 'is_active' => true, + 'is_official' => true, + ]); + + PluginPrice::factory()->regular()->amount(2999)->create(['plugin_id' => $plugin->id]); + PluginPrice::factory()->subscriber()->amount(1999)->create(['plugin_id' => $plugin->id]); + + return $plugin; + } + + private function createThirdPartyPlugin(): Plugin + { + $plugin = Plugin::factory()->approved()->paid()->create([ + 'name' => 'vendor/third-party-plugin', + 'is_active' => true, + 'is_official' => false, + ]); + + PluginPrice::factory()->regular()->amount(4900)->create(['plugin_id' => $plugin->id]); + PluginPrice::factory()->subscriber()->amount(2900)->create(['plugin_id' => $plugin->id]); + + return $plugin; + } + + // ---- Phase 1: Official plugin pricing for Ultra ---- + + public function test_ultra_user_gets_subscriber_price_for_official_plugin(): void + { + $user = User::factory()->create(); + $this->createPaidMaxSubscription($user); + $plugin = $this->createOfficialPlugin(); + + $bestPrice = $plugin->getBestPriceForUser($user); + + $this->assertNotNull($bestPrice); + $this->assertEquals(1999, $bestPrice->amount); + } + + public function test_ultra_user_gets_regular_price_for_third_party_plugin(): void + { + $user = User::factory()->create(); + $this->createPaidMaxSubscription($user); + $plugin = $this->createThirdPartyPlugin(); + + $bestPrice = $plugin->getBestPriceForUser($user); + + $this->assertNotNull($bestPrice); + $this->assertEquals(4900, $bestPrice->amount); + } + + public function test_comped_ultra_user_does_not_get_free_official_plugin(): void + { + $user = User::factory()->create(); + $this->createCompedMaxSubscription($user); + $plugin = $this->createOfficialPlugin(); + + $bestPrice = $plugin->getBestPriceForUser($user); + + $this->assertNotNull($bestPrice); + $this->assertGreaterThan(0, $bestPrice->amount); + } + + public function test_non_subscriber_gets_regular_price_for_official_plugin(): void + { + $user = User::factory()->create(); + $plugin = $this->createOfficialPlugin(); + + $bestPrice = $plugin->getBestPriceForUser($user); + + $this->assertNotNull($bestPrice); + $this->assertEquals(2999, $bestPrice->amount); + } + + public function test_guest_gets_regular_price_for_official_plugin(): void + { + $plugin = $this->createOfficialPlugin(); + + $bestPrice = $plugin->getBestPriceForUser(null); + + $this->assertNotNull($bestPrice); + $this->assertEquals(2999, $bestPrice->amount); + } + + // ---- Phase 2: Cart captures real price for Ultra users ---- + + public function test_ultra_user_cart_captures_real_price_for_official_plugin(): void + { + $user = User::factory()->create(); + $this->createPaidMaxSubscription($user); + $plugin = $this->createOfficialPlugin(); + + $this->actingAs($user); + + $cartService = resolve(CartService::class); + $cart = $cartService->getCart($user); + $cartService->addPlugin($cart, $plugin); + + // Verify the price was captured at the subscriber rate, not $0 + $cartItem = $cart->items()->first(); + $this->assertEquals(1999, $cartItem->price_at_addition); + } + + // ---- Phase 3: Team plugin access ---- + + public function test_team_member_has_plugin_access_via_owner_license(): void + { + $owner = User::factory()->create(); + $this->createPaidMaxSubscription($owner); + + $team = Team::factory()->create(['user_id' => $owner->id]); + $member = User::factory()->create(); + + TeamUser::create([ + 'team_id' => $team->id, + 'user_id' => $member->id, + 'email' => $member->email, + 'status' => 'active', + 'role' => 'member', + 'accepted_at' => now(), + ]); + + $plugin = Plugin::factory()->approved()->paid()->create([ + 'name' => 'nativephp/licensed-plugin', + 'type' => PluginType::Paid, + 'is_active' => true, + 'is_official' => true, + ]); + + $this->assertTrue($member->hasPluginAccess($plugin)); + } + + public function test_team_member_loses_access_when_removed(): void + { + $owner = User::factory()->create(); + $this->createPaidMaxSubscription($owner); + + $team = Team::factory()->create(['user_id' => $owner->id]); + $member = User::factory()->create(); + + $teamUser = TeamUser::create([ + 'team_id' => $team->id, + 'user_id' => $member->id, + 'email' => $member->email, + 'status' => 'active', + 'role' => 'member', + 'accepted_at' => now(), + ]); + + $plugin = Plugin::factory()->approved()->paid()->create([ + 'name' => 'nativephp/licensed-plugin', + 'type' => PluginType::Paid, + 'is_active' => true, + 'is_official' => true, + ]); + + $this->assertTrue($member->hasPluginAccess($plugin)); + + // Remove from team + $teamUser->delete(); + + $this->assertFalse($member->hasPluginAccess($plugin)); + } + + public function test_non_team_member_does_not_get_team_plugin_access(): void + { + $owner = User::factory()->create(); + $this->createPaidMaxSubscription($owner); + + Team::factory()->create(['user_id' => $owner->id]); + $nonMember = User::factory()->create(); + + $plugin = Plugin::factory()->approved()->paid()->create([ + 'name' => 'nativephp/licensed-plugin', + 'type' => PluginType::Paid, + 'is_active' => true, + 'is_official' => true, + ]); + + PluginLicense::factory()->create([ + 'user_id' => $owner->id, + 'plugin_id' => $plugin->id, + 'expires_at' => null, + ]); + + $this->assertFalse($nonMember->hasPluginAccess($plugin)); + } + + public function test_pending_team_member_does_not_get_plugin_access(): void + { + $owner = User::factory()->create(); + $this->createPaidMaxSubscription($owner); + + $team = Team::factory()->create(['user_id' => $owner->id]); + $member = User::factory()->create(); + + TeamUser::create([ + 'team_id' => $team->id, + 'user_id' => $member->id, + 'email' => $member->email, + 'status' => 'pending', + 'role' => 'member', + 'invited_at' => now(), + ]); + + $plugin = Plugin::factory()->approved()->paid()->create([ + 'name' => 'nativephp/licensed-plugin', + 'type' => PluginType::Paid, + 'is_active' => true, + 'is_official' => true, + ]); + + PluginLicense::factory()->create([ + 'user_id' => $owner->id, + 'plugin_id' => $plugin->id, + 'expires_at' => null, + ]); + + $this->assertFalse($member->hasPluginAccess($plugin)); + } + + public function test_satis_api_includes_team_plugins(): void + { + $owner = User::factory()->create([ + 'plugin_license_key' => 'owner-key', + ]); + $this->createPaidMaxSubscription($owner); + + $team = Team::factory()->create(['user_id' => $owner->id]); + $member = User::factory()->create([ + 'plugin_license_key' => 'member-key', + ]); + + TeamUser::create([ + 'team_id' => $team->id, + 'user_id' => $member->id, + 'email' => $member->email, + 'status' => 'active', + 'role' => 'member', + 'accepted_at' => now(), + ]); + + $plugin = Plugin::factory()->create([ + 'name' => 'nativephp/team-plugin', + 'type' => PluginType::Paid, + 'status' => PluginStatus::Approved, + 'is_active' => true, + 'is_official' => true, + ]); + + PluginLicense::factory()->create([ + 'user_id' => $owner->id, + 'plugin_id' => $plugin->id, + 'expires_at' => null, + ]); + + $response = $this->withHeaders([ + 'X-API-Key' => config('services.bifrost.api_key'), + 'Authorization' => 'Basic '.base64_encode("{$member->email}:member-key"), + ])->getJson('/api/plugins/access'); + + $response->assertStatus(200); + + $plugins = $response->json('plugins'); + $pluginNames = array_column($plugins, 'name'); + + $this->assertContains('nativephp/team-plugin', $pluginNames); + + $teamPlugin = collect($plugins)->firstWhere('name', 'nativephp/team-plugin'); + $this->assertEquals('team', $teamPlugin['access']); + } + + public function test_satis_check_access_returns_true_for_team_member(): void + { + $owner = User::factory()->create([ + 'plugin_license_key' => 'owner-key', + ]); + $this->createPaidMaxSubscription($owner); + + $team = Team::factory()->create(['user_id' => $owner->id]); + $member = User::factory()->create([ + 'plugin_license_key' => 'member-key', + ]); + + TeamUser::create([ + 'team_id' => $team->id, + 'user_id' => $member->id, + 'email' => $member->email, + 'status' => 'active', + 'role' => 'member', + 'accepted_at' => now(), + ]); + + $plugin = Plugin::factory()->create([ + 'name' => 'nativephp/team-plugin', + 'type' => PluginType::Paid, + 'status' => PluginStatus::Approved, + 'is_active' => true, + 'is_official' => true, + ]); + + PluginLicense::factory()->create([ + 'user_id' => $owner->id, + 'plugin_id' => $plugin->id, + 'expires_at' => null, + ]); + + $response = $this->withHeaders([ + 'X-API-Key' => config('services.bifrost.api_key'), + 'Authorization' => 'Basic '.base64_encode("{$member->email}:member-key"), + ])->getJson('/api/plugins/access/nativephp/team-plugin'); + + $response->assertStatus(200) + ->assertJson([ + 'has_access' => true, + ]); + } + + // ---- Phase 4: Team Plugins moved to dedicated team page ---- + + public function test_purchased_plugins_page_does_not_show_team_plugins_for_team_member(): void + { + $owner = User::factory()->create(); + $this->createPaidMaxSubscription($owner); + + $team = Team::factory()->create(['user_id' => $owner->id]); + $member = User::factory()->create(); + + TeamUser::create([ + 'team_id' => $team->id, + 'user_id' => $member->id, + 'email' => $member->email, + 'status' => 'active', + 'role' => 'member', + 'accepted_at' => now(), + ]); + + $plugin = Plugin::factory()->approved()->paid()->create([ + 'name' => 'nativephp/shared-plugin', + 'is_active' => true, + ]); + + PluginLicense::factory()->create([ + 'user_id' => $owner->id, + 'plugin_id' => $plugin->id, + 'expires_at' => null, + ]); + + $this->actingAs($member); + $response = $this->get(route('customer.purchased-plugins.index')); + + $response->assertStatus(200); + $response->assertDontSee('Team Plugins'); + } + + public function test_purchased_plugins_page_does_not_show_team_plugins_for_non_member(): void + { + $user = User::factory()->create(); + $this->actingAs($user); + + $response = $this->get(route('customer.purchased-plugins.index')); + + $response->assertStatus(200); + $response->assertDontSee('Team Plugins'); + } + + // ---- Phase 5: Team suspension (cancelled/defaulted subscription) ---- + + public function test_team_member_has_access_to_owner_purchased_third_party_plugin(): void + { + $owner = User::factory()->create(); + $this->createPaidMaxSubscription($owner); + + $team = Team::factory()->create(['user_id' => $owner->id]); + $member = User::factory()->create(); + + TeamUser::create([ + 'team_id' => $team->id, + 'user_id' => $member->id, + 'email' => $member->email, + 'status' => 'active', + 'role' => 'member', + 'accepted_at' => now(), + ]); + + $plugin = $this->createThirdPartyPlugin(); + + PluginLicense::factory()->create([ + 'user_id' => $owner->id, + 'plugin_id' => $plugin->id, + 'expires_at' => null, + ]); + + $this->assertTrue($member->hasPluginAccess($plugin)); + } + + public function test_team_member_loses_plugin_access_when_team_suspended(): void + { + $owner = User::factory()->create(); + $this->createPaidMaxSubscription($owner); + + $team = Team::factory()->create(['user_id' => $owner->id]); + $member = User::factory()->create(); + + TeamUser::create([ + 'team_id' => $team->id, + 'user_id' => $member->id, + 'email' => $member->email, + 'status' => 'active', + 'role' => 'member', + 'accepted_at' => now(), + ]); + + $officialPlugin = $this->createOfficialPlugin(); + $thirdPartyPlugin = $this->createThirdPartyPlugin(); + + PluginLicense::factory()->create([ + 'user_id' => $owner->id, + 'plugin_id' => $thirdPartyPlugin->id, + 'expires_at' => null, + ]); + + // Before suspension, member has access + $this->assertTrue($member->hasPluginAccess($officialPlugin)); + $this->assertTrue($member->hasPluginAccess($thirdPartyPlugin)); + + // Suspend the team + $team->suspend(); + + // After suspension, member loses team-granted access + $this->assertFalse($member->hasPluginAccess($officialPlugin)); + $this->assertFalse($member->hasPluginAccess($thirdPartyPlugin)); + } + + public function test_team_member_keeps_own_license_when_team_suspended(): void + { + $owner = User::factory()->create(); + $this->createPaidMaxSubscription($owner); + + $team = Team::factory()->create(['user_id' => $owner->id]); + $member = User::factory()->create(); + + TeamUser::create([ + 'team_id' => $team->id, + 'user_id' => $member->id, + 'email' => $member->email, + 'status' => 'active', + 'role' => 'member', + 'accepted_at' => now(), + ]); + + $plugin = $this->createThirdPartyPlugin(); + + // Member bought this plugin themselves + PluginLicense::factory()->create([ + 'user_id' => $member->id, + 'plugin_id' => $plugin->id, + 'expires_at' => null, + ]); + + // Suspend the team + $team->suspend(); + + // Member keeps access via their own license + $this->assertTrue($member->hasPluginAccess($plugin)); + } + + public function test_satis_api_excludes_team_plugins_when_team_suspended(): void + { + $owner = User::factory()->create([ + 'plugin_license_key' => 'owner-key', + ]); + $this->createPaidMaxSubscription($owner); + + $team = Team::factory()->create(['user_id' => $owner->id]); + $member = User::factory()->create([ + 'plugin_license_key' => 'member-key', + ]); + + TeamUser::create([ + 'team_id' => $team->id, + 'user_id' => $member->id, + 'email' => $member->email, + 'status' => 'active', + 'role' => 'member', + 'accepted_at' => now(), + ]); + + $plugin = Plugin::factory()->create([ + 'name' => 'nativephp/team-plugin', + 'type' => PluginType::Paid, + 'status' => PluginStatus::Approved, + 'is_active' => true, + 'is_official' => true, + ]); + + PluginLicense::factory()->create([ + 'user_id' => $owner->id, + 'plugin_id' => $plugin->id, + 'expires_at' => null, + ]); + + // Suspend the team + $team->suspend(); + + $response = $this->withHeaders([ + 'X-API-Key' => config('services.bifrost.api_key'), + 'Authorization' => 'Basic '.base64_encode("{$member->email}:member-key"), + ])->getJson('/api/plugins/access'); + + $response->assertStatus(200); + + $pluginNames = array_column($response->json('plugins'), 'name'); + $this->assertNotContains('nativephp/team-plugin', $pluginNames); + } + + // ---- Phase 6: Team member pricing ---- + + public function test_team_member_without_own_subscription_sees_regular_price(): void + { + $owner = User::factory()->create(); + $this->createPaidMaxSubscription($owner); + + $team = Team::factory()->create(['user_id' => $owner->id]); + $member = User::factory()->create(); + + TeamUser::create([ + 'team_id' => $team->id, + 'user_id' => $member->id, + 'email' => $member->email, + 'status' => 'active', + 'role' => 'member', + 'accepted_at' => now(), + ]); + + $plugin = $this->createThirdPartyPlugin(); + + $bestPrice = $plugin->getBestPriceForUser($member); + + $this->assertNotNull($bestPrice); + $this->assertEquals(4900, $bestPrice->amount); + } + + public function test_team_member_with_own_subscription_sees_regular_price_for_third_party_plugin(): void + { + $owner = User::factory()->create(); + $this->createPaidMaxSubscription($owner); + + $team = Team::factory()->create(['user_id' => $owner->id]); + $member = User::factory()->create(); + $this->createPaidMaxSubscription($member); + + TeamUser::create([ + 'team_id' => $team->id, + 'user_id' => $member->id, + 'email' => $member->email, + 'status' => 'active', + 'role' => 'member', + 'accepted_at' => now(), + ]); + + $plugin = $this->createThirdPartyPlugin(); + + $bestPrice = $plugin->getBestPriceForUser($member); + + $this->assertNotNull($bestPrice); + $this->assertEquals(4900, $bestPrice->amount); + } + + // ---- Ultra subscribers without a team ---- + + public function test_ultra_subscriber_without_team_has_access_to_official_plugin(): void + { + $user = User::factory()->create(); + $this->createPaidMaxSubscription($user); + $plugin = $this->createOfficialPlugin(); + + // User has Ultra subscription but no team created + $this->assertNull($user->ownedTeam); + $this->assertTrue($user->hasPluginAccess($plugin)); + } + + public function test_ultra_subscriber_without_team_does_not_have_access_to_third_party_plugin(): void + { + $user = User::factory()->create(); + $this->createPaidMaxSubscription($user); + $plugin = $this->createThirdPartyPlugin(); + + $this->assertFalse($user->hasPluginAccess($plugin)); + } + + public function test_comped_ultra_subscriber_without_team_has_access_to_official_plugin(): void + { + $user = User::factory()->create(); + $this->createCompedUltraSubscription($user); + $plugin = $this->createOfficialPlugin(); + + $this->assertNull($user->ownedTeam); + $this->assertTrue($user->hasPluginAccess($plugin)); + } + + public function test_legacy_comped_max_without_team_does_not_have_access_to_official_plugin(): void + { + $user = User::factory()->create(); + $this->createCompedMaxSubscription($user); + $plugin = $this->createOfficialPlugin(); + + $this->assertFalse($user->hasPluginAccess($plugin)); + } + + public function test_satis_api_includes_official_plugins_for_ultra_subscriber_without_team(): void + { + $user = User::factory()->create([ + 'plugin_license_key' => 'ultra-no-team-key', + ]); + $this->createPaidMaxSubscription($user); + + $plugin = Plugin::factory()->create([ + 'name' => 'nativephp/secure-storage', + 'type' => PluginType::Paid, + 'status' => PluginStatus::Approved, + 'is_active' => true, + 'is_official' => true, + ]); + + $response = $this->withHeaders([ + 'X-API-Key' => config('services.bifrost.api_key'), + 'Authorization' => 'Basic '.base64_encode("{$user->email}:ultra-no-team-key"), + ])->getJson('/api/plugins/access'); + + $response->assertStatus(200); + + $pluginNames = array_column($response->json('plugins'), 'name'); + $this->assertContains('nativephp/secure-storage', $pluginNames); + } + + public function test_satis_check_access_returns_true_for_ultra_subscriber_without_team(): void + { + $user = User::factory()->create([ + 'plugin_license_key' => 'ultra-no-team-key', + ]); + $this->createPaidMaxSubscription($user); + + Plugin::factory()->create([ + 'name' => 'nativephp/secure-storage', + 'type' => PluginType::Paid, + 'status' => PluginStatus::Approved, + 'is_active' => true, + 'is_official' => true, + ]); + + $response = $this->withHeaders([ + 'X-API-Key' => config('services.bifrost.api_key'), + 'Authorization' => 'Basic '.base64_encode("{$user->email}:ultra-no-team-key"), + ])->getJson('/api/plugins/access/nativephp/secure-storage'); + + $response->assertStatus(200) + ->assertJson([ + 'has_access' => true, + ]); + } + + // ---- Comped Ultra subscriptions ---- + + public function test_comped_ultra_user_has_active_ultra_subscription(): void + { + $user = User::factory()->create(); + $this->createCompedUltraSubscription($user); + + $this->assertTrue($user->hasActiveUltraSubscription()); + } + + public function test_comped_ultra_user_has_ultra_access(): void + { + $user = User::factory()->create(); + $this->createCompedUltraSubscription($user); + + $this->assertTrue($user->hasUltraAccess()); + } + + public function test_comped_ultra_user_gets_subscriber_price_for_official_plugin(): void + { + $user = User::factory()->create(); + $this->createCompedUltraSubscription($user); + $plugin = $this->createOfficialPlugin(); + + $bestPrice = $plugin->getBestPriceForUser($user); + + $this->assertNotNull($bestPrice); + $this->assertEquals(1999, $bestPrice->amount); + } + + public function test_legacy_comped_max_does_not_have_active_ultra_subscription(): void + { + $user = User::factory()->create(); + $this->createCompedMaxSubscription($user); + + $this->assertFalse($user->hasActiveUltraSubscription()); + } + + public function test_legacy_comped_max_does_not_have_ultra_access(): void + { + $user = User::factory()->create(); + $this->createCompedMaxSubscription($user); + + $this->assertFalse($user->hasUltraAccess()); + } +} diff --git a/tests/Feature/UserModelTest.php b/tests/Feature/UserModelTest.php new file mode 100644 index 00000000..857336e5 --- /dev/null +++ b/tests/Feature/UserModelTest.php @@ -0,0 +1,40 @@ +create(['name' => 'John Doe']); + + $this->assertSame('John Doe', $user->getFilamentName()); + } + + public function test_get_filament_name_returns_display_name_when_name_is_null(): void + { + $user = User::factory()->create(['name' => null, 'display_name' => 'Custom Name']); + + $this->assertSame('Custom Name', $user->getFilamentName()); + } + + public function test_get_filament_name_returns_email_when_name_and_display_name_are_null(): void + { + $user = User::factory()->create(['name' => null, 'display_name' => null]); + + $this->assertSame($user->email, $user->getFilamentName()); + } + + public function test_get_filament_name_always_returns_string(): void + { + $user = User::factory()->create(['name' => null, 'display_name' => null]); + + $this->assertIsString($user->getFilamentName()); + } +} diff --git a/tests/Feature/WallOfLoveEditTest.php b/tests/Feature/WallOfLoveEditTest.php new file mode 100644 index 00000000..0a4ad7fb --- /dev/null +++ b/tests/Feature/WallOfLoveEditTest.php @@ -0,0 +1,155 @@ +create(); + $submission = WallOfLoveSubmission::factory()->create(['user_id' => $user->id]); + + $response = $this->actingAs($user)->get("/dashboard/wall-of-love/{$submission->id}/edit"); + + $response->assertStatus(200); + $response->assertSee('Edit Your Listing'); + } + + public function test_non_owner_gets_403(): void + { + $owner = User::factory()->create(); + $otherUser = User::factory()->create(); + $submission = WallOfLoveSubmission::factory()->create(['user_id' => $owner->id]); + + $response = $this->actingAs($otherUser)->get("/dashboard/wall-of-love/{$submission->id}/edit"); + + $response->assertStatus(403); + } + + public function test_guest_cannot_access_edit_page(): void + { + $submission = WallOfLoveSubmission::factory()->create(); + + $response = $this->get("/dashboard/wall-of-love/{$submission->id}/edit"); + + $response->assertRedirect('/login'); + } + + public function test_owner_can_update_company_name(): void + { + $user = User::factory()->create(); + $submission = WallOfLoveSubmission::factory()->create([ + 'user_id' => $user->id, + 'company' => 'Old Company', + ]); + + Livewire::actingAs($user) + ->test(WallOfLoveSubmissionForm::class, ['submission' => $submission]) + ->assertSet('isEditing', true) + ->assertSet('company', 'Old Company') + ->set('company', 'New Company') + ->call('submit'); + + $this->assertDatabaseHas('wall_of_love_submissions', [ + 'id' => $submission->id, + 'company' => 'New Company', + ]); + } + + public function test_owner_can_update_photo(): void + { + Storage::fake('public'); + + $user = User::factory()->create(); + $submission = WallOfLoveSubmission::factory()->create([ + 'user_id' => $user->id, + 'photo_path' => null, + ]); + + Livewire::actingAs($user) + ->test(WallOfLoveSubmissionForm::class, ['submission' => $submission]) + ->set('photo', UploadedFile::fake()->image('avatar.jpg')) + ->call('submit'); + + $submission->refresh(); + $this->assertNotNull($submission->photo_path); + Storage::disk('public')->assertExists($submission->photo_path); + } + + public function test_approval_status_is_not_reset_on_edit(): void + { + $user = User::factory()->create(); + $submission = WallOfLoveSubmission::factory()->approved()->create([ + 'user_id' => $user->id, + 'company' => 'Old Company', + ]); + + $originalApprovedAt = $submission->approved_at; + $originalApprovedBy = $submission->approved_by; + + Livewire::actingAs($user) + ->test(WallOfLoveSubmissionForm::class, ['submission' => $submission]) + ->set('company', 'Updated Company') + ->call('submit'); + + $submission->refresh(); + $this->assertEquals('Updated Company', $submission->company); + $this->assertEquals($originalApprovedAt->toDateTimeString(), $submission->approved_at->toDateTimeString()); + $this->assertEquals($originalApprovedBy, $submission->approved_by); + } + + public function test_edit_mode_only_shows_company_and_photo_fields(): void + { + $user = User::factory()->create(); + $submission = WallOfLoveSubmission::factory()->create(['user_id' => $user->id]); + + Livewire::actingAs($user) + ->test(WallOfLoveSubmissionForm::class, ['submission' => $submission]) + ->assertSee('Company') + ->assertSee('Photo') + ->assertDontSee('Name *') + ->assertDontSee('Website or Social Media URL') + ->assertDontSee('Your story or testimonial'); + } + + public function test_owner_can_remove_existing_photo(): void + { + Storage::fake('public'); + + $user = User::factory()->create(); + $submission = WallOfLoveSubmission::factory()->create([ + 'user_id' => $user->id, + 'photo_path' => 'wall-of-love-photos/old-photo.jpg', + ]); + + Livewire::actingAs($user) + ->test(WallOfLoveSubmissionForm::class, ['submission' => $submission]) + ->assertSet('existingPhoto', 'wall-of-love-photos/old-photo.jpg') + ->call('removeExistingPhoto') + ->assertSet('existingPhoto', null) + ->call('submit'); + + $submission->refresh(); + $this->assertNull($submission->photo_path); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 2932d4a6..fe1ffc2f 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -6,5 +6,5 @@ abstract class TestCase extends BaseTestCase { - use CreatesApplication; + // } diff --git a/tests/Unit/BladeMarkdownPreprocessorTest.php b/tests/Unit/BladeMarkdownPreprocessorTest.php new file mode 100644 index 00000000..b070b739 --- /dev/null +++ b/tests/Unit/BladeMarkdownPreprocessorTest.php @@ -0,0 +1,87 @@ + + +Some more text. +MD; + + $result = BladeMarkdownPreprocessor::process($markdown); + + // The component should be rendered (replaced with SVG content) + $this->assertStringNotContainsString('assertStringContainsString('svg', $result); + } + + public function test_it_preserves_code_blocks(): void + { + $markdown = <<<'MD' +# Example + +```php + // This should NOT be rendered +``` + + +MD; + + $result = BladeMarkdownPreprocessor::process($markdown); + + // Code block content should be preserved as-is + $this->assertStringContainsString('', $result); + // But the actual component outside code block should be rendered + $this->assertStringNotContainsString('` syntax for components. + + +MD; + + $result = BladeMarkdownPreprocessor::process($markdown); + + // Inline code should be preserved + $this->assertStringContainsString('``', $result); + // But the actual component should be rendered + $this->assertStringNotContainsString('assertEquals($markdown, $result); + } + + public function test_it_passes_data_to_blade_components(): void + { + $markdown = '{{ $name }}'; + + $result = BladeMarkdownPreprocessor::process($markdown, ['name' => 'John']); + + $this->assertStringContainsString('John', $result); + } +} diff --git a/tests/Unit/DocsVersionServiceTest.php b/tests/Unit/DocsVersionServiceTest.php new file mode 100644 index 00000000..bd010d3b --- /dev/null +++ b/tests/Unit/DocsVersionServiceTest.php @@ -0,0 +1,108 @@ +docsVersionService = app(DocsVersionService::class); + } + + public static function platformDataProvider(): array + { + return [ + 'platform=mobile' => ['mobile'], + 'platform=desktop' => ['desktop'], + ]; + } + + #[Test] + #[DataProvider('platformDataProvider')] + public function it_points_to_the_latest_version_when_page_exists_in_latest( + string $platform, + ): void { + $latestVersion = $platform === 'mobile' ? config('docs.latest_versions.mobile') : config('docs.latest_versions.desktop'); + + $url = $this->docsVersionService->determineCanonicalUrl( + platform: $platform, + page: 'getting-started/introduction', + ); + + $expected = route('docs.show', [ + 'platform' => $platform, + 'version' => $latestVersion, + 'page' => 'getting-started/introduction', + ]); + + $this->assertEquals($expected, $url); + } + + #[Test] + public function it_remaps_mobile_apis_pages_to_plugins_core_when_the_page_exists_there_in_latest(): void + { + $url = $this->docsVersionService->determineCanonicalUrl( + platform: 'mobile', + page: 'apis/camera', + ); + + $expected = route('docs.show', [ + 'platform' => 'mobile', + 'version' => config('docs.latest_versions.mobile'), + 'page' => 'plugins/core/camera', + ]); + + $this->assertEquals($expected, $url); + } + + #[Test] + public function it_points_to_the_latest_version_of_getting_started_introduction_when_page_does_not_exist_in_latest(): void + { + // concepts/ci-cd only exists in version 1 of mobile documentation + $url = $this->docsVersionService->determineCanonicalUrl( + platform: 'mobile', + page: 'concepts/ci-cd', + ); + + $expected = route('docs.show', [ + 'platform' => 'mobile', + 'version' => '3', + 'page' => 'getting-started/introduction', + ]); + + $this->assertEquals($expected, $url); + } + + #[Test] + #[DataProvider('platformDataProvider')] + public function it_points_to_the_latest_version_of_getting_started_introduction_when_page_does_not_exist( + string $platform, + ): void { + $latestVersion = $platform === 'mobile' ? config('docs.latest_versions.mobile') : config('docs.latest_versions.desktop'); + + $url = $this->docsVersionService->determineCanonicalUrl( + platform: $platform, + page: 'non-existent-page', + ); + + $expected = route('docs.show', [ + 'platform' => $platform, + 'version' => $latestVersion, + 'page' => 'getting-started/introduction', + ]); + + $this->assertEquals($expected, $url); + } +} diff --git a/tests/Unit/ExampleTest.php b/tests/Unit/ExampleTest.php deleted file mode 100644 index 5773b0ce..00000000 --- a/tests/Unit/ExampleTest.php +++ /dev/null @@ -1,16 +0,0 @@ -assertTrue(true); - } -} diff --git a/tests/Unit/PluginPayoutTest.php b/tests/Unit/PluginPayoutTest.php new file mode 100644 index 00000000..e82435b5 --- /dev/null +++ b/tests/Unit/PluginPayoutTest.php @@ -0,0 +1,56 @@ +assertEquals(3000, $split['platform_fee']); + $this->assertEquals(7000, $split['developer_amount']); + } + + #[Test] + public function calculate_split_with_zero_platform_fee(): void + { + $split = PluginPayout::calculateSplit(10000, 0); + + $this->assertEquals(0, $split['platform_fee']); + $this->assertEquals(10000, $split['developer_amount']); + } + + #[Test] + public function calculate_split_with_custom_platform_fee(): void + { + $split = PluginPayout::calculateSplit(10000, 15); + + $this->assertEquals(1500, $split['platform_fee']); + $this->assertEquals(8500, $split['developer_amount']); + } + + #[Test] + public function calculate_split_with_zero_amount(): void + { + $split = PluginPayout::calculateSplit(0); + + $this->assertEquals(0, $split['platform_fee']); + $this->assertEquals(0, $split['developer_amount']); + } + + #[Test] + public function calculate_split_rounds_platform_fee(): void + { + // 2999 * 30% = 899.7 -> rounds to 900 + $split = PluginPayout::calculateSplit(2999); + + $this->assertEquals(900, $split['platform_fee']); + $this->assertEquals(2099, $split['developer_amount']); + } +} diff --git a/tests/Unit/StripeConnectCountriesTest.php b/tests/Unit/StripeConnectCountriesTest.php new file mode 100644 index 00000000..a8d1fb30 --- /dev/null +++ b/tests/Unit/StripeConnectCountriesTest.php @@ -0,0 +1,145 @@ +assertCount(108, $countries); + $this->assertArrayHasKey('US', $countries); + $this->assertArrayHasKey('GB', $countries); + $this->assertArrayHasKey('DE', $countries); + } + + /** @test */ + public function is_supported_returns_true_for_valid_country(): void + { + $this->assertTrue(StripeConnectCountries::isSupported('US')); + $this->assertTrue(StripeConnectCountries::isSupported('GB')); + $this->assertTrue(StripeConnectCountries::isSupported('FR')); + } + + /** @test */ + public function is_supported_returns_false_for_invalid_country(): void + { + $this->assertFalse(StripeConnectCountries::isSupported('XX')); + $this->assertFalse(StripeConnectCountries::isSupported('ZZ')); + } + + /** @test */ + public function is_supported_is_case_insensitive(): void + { + $this->assertTrue(StripeConnectCountries::isSupported('us')); + $this->assertTrue(StripeConnectCountries::isSupported('gb')); + } + + /** @test */ + public function default_currency_returns_correct_currency(): void + { + $this->assertEquals('USD', StripeConnectCountries::defaultCurrency('US')); + $this->assertEquals('GBP', StripeConnectCountries::defaultCurrency('GB')); + $this->assertEquals('EUR', StripeConnectCountries::defaultCurrency('DE')); + $this->assertEquals('AUD', StripeConnectCountries::defaultCurrency('AU')); + $this->assertEquals('JPY', StripeConnectCountries::defaultCurrency('JP')); + } + + /** @test */ + public function default_currency_returns_null_for_unsupported_country(): void + { + $this->assertNull(StripeConnectCountries::defaultCurrency('XX')); + } + + /** @test */ + public function available_currencies_returns_array(): void + { + $currencies = StripeConnectCountries::availableCurrencies('US'); + + $this->assertIsArray($currencies); + $this->assertContains('USD', $currencies); + } + + /** @test */ + public function available_currencies_returns_multiple_for_european_countries(): void + { + $currencies = StripeConnectCountries::availableCurrencies('DE'); + + $this->assertContains('EUR', $currencies); + $this->assertContains('GBP', $currencies); + $this->assertContains('USD', $currencies); + } + + /** @test */ + public function available_currencies_returns_empty_for_unsupported_country(): void + { + $this->assertEmpty(StripeConnectCountries::availableCurrencies('XX')); + } + + /** @test */ + public function is_valid_currency_for_country_validates_correctly(): void + { + $this->assertTrue(StripeConnectCountries::isValidCurrencyForCountry('US', 'USD')); + $this->assertTrue(StripeConnectCountries::isValidCurrencyForCountry('DE', 'EUR')); + $this->assertTrue(StripeConnectCountries::isValidCurrencyForCountry('DE', 'USD')); + $this->assertFalse(StripeConnectCountries::isValidCurrencyForCountry('US', 'EUR')); + $this->assertFalse(StripeConnectCountries::isValidCurrencyForCountry('JP', 'USD')); + } + + /** @test */ + public function supported_country_codes_returns_array_of_codes(): void + { + $codes = StripeConnectCountries::supportedCountryCodes(); + + $this->assertContains('US', $codes); + $this->assertContains('GB', $codes); + $this->assertCount(108, $codes); + } + + /** @test */ + public function currency_name_returns_correct_name(): void + { + $this->assertEquals('US Dollar', StripeConnectCountries::currencyName('USD')); + $this->assertEquals('Euro', StripeConnectCountries::currencyName('EUR')); + $this->assertEquals('British Pound', StripeConnectCountries::currencyName('GBP')); + $this->assertEquals('Japanese Yen', StripeConnectCountries::currencyName('JPY')); + } + + /** @test */ + public function currency_name_returns_code_for_unknown_currency(): void + { + $this->assertEquals('ZZZ', StripeConnectCountries::currencyName('ZZZ')); + } + + /** @test */ + public function every_used_currency_has_a_name(): void + { + $countries = StripeConnectCountries::all(); + $currencies = collect($countries)->pluck('currencies')->flatten()->unique(); + + foreach ($currencies as $code) { + $name = StripeConnectCountries::currencyName($code); + $this->assertNotEquals($code, $name, "Currency {$code} is missing a name in CURRENCY_NAMES"); + } + } + + /** @test */ + public function each_country_has_required_keys(): void + { + foreach (StripeConnectCountries::all() as $code => $details) { + $this->assertArrayHasKey('name', $details, "Country {$code} missing 'name'"); + $this->assertArrayHasKey('flag', $details, "Country {$code} missing 'flag'"); + $this->assertArrayHasKey('default_currency', $details, "Country {$code} missing 'default_currency'"); + $this->assertArrayHasKey('currencies', $details, "Country {$code} missing 'currencies'"); + $this->assertNotEmpty($details['currencies'], "Country {$code} has empty currencies"); + $this->assertContains($details['default_currency'], $details['currencies'], "Country {$code} default currency not in currencies list"); + $this->assertEquals(2, strlen($code), "Country code {$code} is not 2 characters"); + $this->assertEquals(3, strlen($details['default_currency']), "Country {$code} default currency is not 3 characters"); + } + } +} diff --git a/tests/Unit/SubscriptionStripePriceIdTest.php b/tests/Unit/SubscriptionStripePriceIdTest.php new file mode 100644 index 00000000..42fdc2b0 --- /dev/null +++ b/tests/Unit/SubscriptionStripePriceIdTest.php @@ -0,0 +1,70 @@ +assertEquals(self::YEARLY_PRICE, Subscription::Max->stripePriceId()); + } + + #[Test] + public function monthly_returns_monthly_price(): void + { + $this->assertEquals(self::MONTHLY_PRICE, Subscription::Max->stripePriceId(interval: 'month')); + } + + #[Test] + public function eap_yearly_returns_eap_price(): void + { + $this->assertEquals(self::EAP_PRICE, Subscription::Max->stripePriceId(forceEap: true)); + } + + #[Test] + public function eap_monthly_returns_monthly_price_not_eap(): void + { + $this->assertEquals( + self::MONTHLY_PRICE, + Subscription::Max->stripePriceId(forceEap: true, interval: 'month') + ); + } + + #[Test] + public function discounted_returns_discounted_price(): void + { + $this->assertEquals(self::DISCOUNTED_PRICE, Subscription::Max->stripePriceId(discounted: true)); + } + + #[Test] + public function monthly_falls_back_to_default_when_no_monthly_price(): void + { + Config::set('subscriptions.plans.max.stripe_price_id_monthly', null); + + $this->assertEquals(self::YEARLY_PRICE, Subscription::Max->stripePriceId(interval: 'month')); + } +} diff --git a/vite.config.js b/vite.config.js index 67bdbe16..ddc1aa68 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,20 +1,20 @@ -import {defineConfig} from 'vite'; -import laravel from 'laravel-vite-plugin'; +import { defineConfig } from 'vite' +import laravel from 'laravel-vite-plugin' +import tailwindcss from '@tailwindcss/vite' export default defineConfig({ - - server: { - cors: { - origin: [ - /^https?:\/\/(?:(?:[^:]+\.)?localhost|127\.0\.0\.1|\[::1\])(?::\d+)?$/, - /^https?:\/\/.*\.test(:\d+)?$/, // Valet / Herd (SCHEME://*.test:PORT) - ], - }, - }, plugins: [ laravel({ - input: ['resources/css/app.css', 'resources/js/app.js'], + input: [ + 'resources/css/app.css', + 'resources/js/app.js', + 'resources/css/docsearch.css', + ], refresh: true, }), + tailwindcss(), ], -}); + server: { + cors: true, + }, +})