commit 2312629d6a445754440637ec98a9aabad2a9e63d
Author: Syarif Ubaidillah <165665831+rif746@users.noreply.github.com>
Date: Thu Jul 2 08:38:43 2026 +0700
Initial commit
diff --git a/.agents/skills/ai-sdk-development/SKILL.md b/.agents/skills/ai-sdk-development/SKILL.md
new file mode 100644
index 0000000..717d626
--- /dev/null
+++ b/.agents/skills/ai-sdk-development/SKILL.md
@@ -0,0 +1,436 @@
+---
+name: ai-sdk-development
+description: TRIGGER when working with ai-sdk which is Laravel official first-party AI SDK. Activate when building, editing AI agents, chatbots, text generation, image generation, audio/TTS, transcription/STT, embeddings, RAG, vector stores, reranking, structured output, streaming, conversation memory, tools, queueing, broadcasting, and provider failover across OpenAI, Anthropic, Gemini, Azure, Groq, xAI, DeepSeek, Mistral, Ollama, ElevenLabs, Cohere, Jina, and VoyageAI. Invoke when the user references ai-sdk, the `Laravel\Ai\` namespace, or this project's AI features — not for other AI packages used directly.
+license: MIT
+metadata:
+ author: laravel
+---
+
+# Developing with the Laravel AI SDK
+
+The Laravel AI SDK (`laravel/ai`) is the official AI package for Laravel, providing a unified API for agents, images, audio, transcription, embeddings, reranking, vector stores, and file management across multiple AI providers.
+
+## Searching the Documentation
+
+This package is new. Always search the documentation before implementing any feature. Never guess at APIs — the documentation is the single source of truth.
+
+- Use broad, simple queries that match the documentation section headings below.
+- Do not add package names to queries — package information is shared automatically. Use `test agent fake`, not `laravel ai test agent fake`.
+- Run multiple queries at once — the most relevant results are returned first.
+
+### Documentation Sections
+
+Use these section headings as query terms for accurate results:
+
+- Introduction, Installation, Configuration, Provider Support
+- Agents: Prompting, Conversation Context, Structured Output, Attachments, Streaming, Broadcasting, Queueing, Tools, Provider Tools, Middleware, Anonymous Agents, Agent Configuration
+- Images
+- Audio (TTS)
+- Transcription (STT)
+- Embeddings: Querying Embeddings, Caching Embeddings
+- Reranking
+- Files
+- Vector Stores: Adding Files to Stores
+- Failover
+- Testing: Agents, Images, Audio, Transcriptions, Embeddings, Reranking, Files, Vector Stores
+- Events
+
+## Decision Workflow
+
+Determine the right entry point before writing code:
+
+Text generation or chat? → Agent class with `Promptable` trait
+Chat with conversation history? → Agent + `Conversational` interface (manual) or `RemembersConversations` trait (automatic)
+Structured JSON output? → Agent + `HasStructuredOutput` interface
+Image generation? → `Image::of()->generate()`
+Audio synthesis? → `Audio::of()->generate()`
+Transcription? → `Transcription::fromPath()->generate()`
+Embeddings? → `Embeddings::for()->generate()`
+Reranking? → `Reranking::of()->rerank()`
+File storage? → `Document::fromPath()->put()`
+Vector stores? → `Stores::create()`
+
+## Basic Usage Examples
+
+### Agents
+
+```php
+use Laravel\Ai\Contracts\Agent;
+use Laravel\Ai\Enums\Lab;
+use Laravel\Ai\Promptable;
+
+class SalesCoach implements Agent
+{
+ use Promptable;
+
+ public function instructions(): string
+ {
+ return 'You are a sales coach.';
+ }
+}
+
+// Prompting
+$response = (new SalesCoach)->prompt('Analyze this transcript...');
+echo $response->text;
+
+// Container resolution with dependency injection
+$agent = SalesCoach::make(user: $user);
+
+// Override provider, model, or timeout per-prompt
+$response = (new SalesCoach)->prompt(
+ 'Analyze this transcript...',
+ provider: Lab::Anthropic,
+ model: 'claude-haiku-4-5-20251001',
+ timeout: 120,
+);
+
+// Streaming (returns SSE response from a route)
+return (new SalesCoach)->stream('Analyze this transcript...');
+
+// Queueing
+(new SalesCoach)->queue('Analyze this transcript...')
+ ->then(fn ($response) => /* ... */);
+
+// Anonymous agents
+use function Laravel\Ai\{agent};
+
+$response = agent(instructions: 'You are a helpful assistant.')->prompt('Hello');
+```
+
+### Conversation Context
+
+Manual conversation history via the `Conversational` interface:
+
+```php
+use Laravel\Ai\Contracts\Agent;
+use Laravel\Ai\Contracts\Conversational;
+use Laravel\Ai\Messages\Message;
+use Laravel\Ai\Promptable;
+
+class SalesCoach implements Agent, Conversational
+{
+ use Promptable;
+
+ public function __construct(public User $user) {}
+
+ public function instructions(): string { return 'You are a sales coach.'; }
+
+ public function messages(): iterable
+ {
+ return History::where('user_id', $this->user->id)
+ ->latest()->limit(50)->get()->reverse()
+ ->map(fn ($m) => new Message($m->role, $m->content))
+ ->all();
+ }
+}
+```
+
+Automatic conversation persistence via the `RemembersConversations` trait:
+
+```php
+use Laravel\Ai\Concerns\RemembersConversations;
+use Laravel\Ai\Contracts\Agent;
+use Laravel\Ai\Contracts\Conversational;
+use Laravel\Ai\Promptable;
+
+class SalesCoach implements Agent, Conversational
+{
+ use Promptable, RemembersConversations;
+
+ public function instructions(): string { return 'You are a sales coach.'; }
+}
+
+// Start a new conversation
+$response = (new SalesCoach)->forUser($user)->prompt('Hello!');
+$conversationId = $response->conversationId;
+
+// Continue an existing conversation
+$response = (new SalesCoach)->continue($conversationId, as: $user)->prompt('Tell me more.');
+```
+
+### Structured Output
+
+```php
+use Illuminate\Contracts\JsonSchema\JsonSchema;
+use Laravel\Ai\Contracts\Agent;
+use Laravel\Ai\Contracts\HasStructuredOutput;
+use Laravel\Ai\Promptable;
+
+class Reviewer implements Agent, HasStructuredOutput
+{
+ use Promptable;
+
+ public function instructions(): string { return 'Review and score content.'; }
+
+ public function schema(JsonSchema $schema): array
+ {
+ return [
+ 'feedback' => $schema->string()->required(),
+ 'score' => $schema->integer()->min(1)->max(10)->required(),
+ ];
+ }
+}
+
+$response = (new Reviewer)->prompt('Review this...');
+echo $response['score']; // Access like an array
+```
+
+### Images
+
+```php
+use Laravel\Ai\Image;
+
+$image = Image::of('A sunset over mountains')
+ ->landscape()
+ ->quality('high')
+ ->generate();
+
+$path = $image->store(); // Store to default disk
+```
+
+### Audio
+
+```php
+use Laravel\Ai\Audio;
+
+$audio = Audio::of('Hello from Laravel.')
+ ->female()
+ ->instructions('Speak warmly')
+ ->generate();
+
+$path = $audio->store();
+```
+
+### Transcription
+
+```php
+use Laravel\Ai\Transcription;
+
+$transcript = Transcription::fromStorage('audio.mp3')
+ ->diarize()
+ ->generate();
+
+echo (string) $transcript;
+```
+
+### Embeddings
+
+```php
+use Laravel\Ai\Embeddings;
+use Illuminate\Support\Str;
+
+$response = Embeddings::for(['Text one', 'Text two'])
+ ->dimensions(1536)
+ ->cache()
+ ->generate();
+
+// Single string via Stringable
+$embedding = Str::of('Napa Valley has great wine.')->toEmbeddings();
+```
+
+### Reranking
+
+```php
+use Laravel\Ai\Reranking;
+
+$response = Reranking::of(['Django is Python.', 'Laravel is PHP.', 'React is JS.'])
+ ->limit(5)
+ ->rerank('PHP frameworks');
+
+$response->first()->document; // "Laravel is PHP."
+```
+
+### Files and Vector Stores
+
+```php
+use Laravel\Ai\Files\Document;
+use Laravel\Ai\Stores;
+
+// Store a file with the provider
+$file = Document::fromPath('/path/to/doc.pdf')->put();
+
+// Create a vector store and add files
+$store = Stores::create('Knowledge Base');
+$store->add($file->id);
+$store->add(Document::fromStorage('manual.pdf')); // Store + add in one step
+```
+
+## Agent Configuration
+
+### PHP Attributes
+
+```php
+use Laravel\Ai\Attributes\{Provider, Model, MaxSteps, MaxTokens, Temperature, Timeout};
+use Laravel\Ai\Enums\Lab;
+
+#[Provider(Lab::Anthropic)]
+#[Model('claude-haiku-4-5-20251001')]
+#[MaxSteps(10)]
+#[MaxTokens(4096)]
+#[Temperature(0.7)]
+#[Timeout(120)]
+class MyAgent implements Agent
+{
+ use Promptable;
+ // ...
+}
+```
+
+The `#[UseCheapestModel]` and `#[UseSmartestModel]` attributes are also available for automatic model selection.
+
+### Tools
+
+Implement the `HasTools` interface and scaffold tools with `php artisan make:tool`:
+
+```php
+use Laravel\Ai\Contracts\HasTools;
+
+class MyAgent implements Agent, HasTools
+{
+ use Promptable;
+
+ public function tools(): iterable
+ {
+ return [new MyCustomTool];
+ }
+}
+```
+
+### Provider Tools
+
+```php
+use Laravel\Ai\Providers\Tools\{WebSearch, WebFetch, FileSearch};
+
+public function tools(): iterable
+{
+ return [
+ (new WebSearch)->max(5)->allow(['laravel.com']),
+ new WebFetch,
+ new FileSearch(stores: ['store_id']),
+ ];
+}
+```
+
+### Conversation Memory
+
+```php
+use Laravel\Ai\Concerns\RemembersConversations;
+use Laravel\Ai\Contracts\Conversational;
+
+class ChatBot implements Agent, Conversational
+{
+ use Promptable, RemembersConversations;
+ // ...
+}
+
+$response = (new ChatBot)->forUser($user)->prompt('Hello!');
+$response = (new ChatBot)->continue($conversationId, as: $user)->prompt('More...');
+```
+
+### Failover
+
+```php
+$response = (new MyAgent)->prompt('Hello', provider: [Lab::OpenAI, Lab::Anthropic]);
+```
+
+## Testing and Faking
+
+Each capability supports `fake()` with assertions:
+
+```php
+use App\Ai\Agents\SalesCoach;
+use Laravel\Ai\{Image, Audio, Transcription, Embeddings, Reranking, Files, Stores};
+
+// Agents
+SalesCoach::fake(['Response 1', 'Response 2']);
+SalesCoach::assertPrompted('query');
+SalesCoach::assertNotPrompted('query');
+SalesCoach::assertNeverPrompted();
+SalesCoach::fake()->preventStrayPrompts();
+
+// Images
+Image::fake();
+Image::assertGenerated(fn ($prompt) => $prompt->contains('sunset'));
+Image::assertNothingGenerated();
+
+// Audio
+Audio::fake();
+Audio::assertGenerated(fn ($prompt) => $prompt->contains('Hello'));
+
+// Transcription
+Transcription::fake(['Transcribed text.']);
+Transcription::assertGenerated(fn ($prompt) => $prompt->isDiarized());
+
+// Embeddings
+Embeddings::fake();
+Embeddings::assertGenerated(fn ($prompt) => $prompt->contains('Laravel'));
+
+// Reranking
+Reranking::fake();
+Reranking::assertReranked(fn ($prompt) => $prompt->contains('PHP'));
+
+// Files
+Files::fake();
+Files::assertStored(fn ($file) => $file->mimeType() === 'text/plain');
+
+// Stores
+Stores::fake();
+Stores::assertCreated('Knowledge Base');
+$store = Stores::get('id');
+$store->assertAdded('file_id');
+```
+
+## Key Patterns
+
+- Namespace: `Laravel\Ai\`
+- Package: `composer require laravel/ai`
+- Agent pattern: Implement the `Agent` interface and use the `Promptable` trait
+- Optional interfaces: `HasTools`, `HasMiddleware`, `HasStructuredOutput`, `Conversational`
+- Entry-point classes: `Image`, `Audio`, `Transcription`, `Embeddings`, `Reranking`, `Stores`
+- Provider enum: `Laravel\Ai\Enums\Lab` (prefer over plain strings)
+- Artisan commands: `php artisan make:agent`, `php artisan make:tool`
+- Global helper: `agent()` for anonymous agents
+
+## Common Pitfalls
+
+### Wrong Namespace
+
+The namespace is `Laravel\Ai`, not `Illuminate\Ai` or `Laravel\AI`.
+
+```php
+// Correct
+use Laravel\Ai\Image;
+use Laravel\Ai\Contracts\Agent;
+use Laravel\Ai\Promptable;
+
+// Wrong — these do not exist
+use Illuminate\Ai\Image;
+use Laravel\AI\Agent;
+```
+
+### Unsupported Provider Capability
+
+Calling a capability not supported by a provider throws a `LogicException`. Refer to the provider support table below.
+
+## Provider Support
+
+| Feature | Providers |
+| ---------- | --------------------------------------------------------------- |
+| Text | OpenAI, Anthropic, Gemini, Azure, Groq, xAI, DeepSeek, Mistral, Ollama |
+| Images | OpenAI, Gemini, xAI |
+| TTS | OpenAI, ElevenLabs |
+| STT | OpenAI, ElevenLabs, Mistral |
+| Embeddings | OpenAI, Gemini, Azure, Cohere, Mistral, Jina, VoyageAI |
+| Reranking | Cohere, Jina |
+| Files | OpenAI, Anthropic, Gemini |
+
+Use the `Laravel\Ai\Enums\Lab` enum to reference providers in code instead of plain strings:
+
+```php
+use Laravel\Ai\Enums\Lab;
+
+Lab::Anthropic;
+Lab::OpenAI;
+Lab::Gemini;
+// ...
+```
diff --git a/.agents/skills/laravel-backup/SKILL.md b/.agents/skills/laravel-backup/SKILL.md
new file mode 100644
index 0000000..4a8a3a4
--- /dev/null
+++ b/.agents/skills/laravel-backup/SKILL.md
@@ -0,0 +1,230 @@
+---
+name: laravel-backup
+description: "Configure and extend spatie/laravel-backup for database and file backups, cleanup strategies, health monitoring, and notifications. Activates when working with backup configuration, scheduling backups, creating custom cleanup strategies or health checks, customizing notifications, or when the user mentions backups, backup monitoring, backup cleanup, or spatie/laravel-backup."
+license: MIT
+metadata:
+ author: spatie
+---
+
+# Laravel Backup
+
+## When to Apply
+
+Activate this skill when:
+
+- Configuring backup sources, destinations, or notifications
+- Scheduling backup, cleanup, or monitor commands
+- Creating custom cleanup strategies or health checks
+- Customizing backup notifications
+- Troubleshooting backup failures
+
+## Key Commands
+
+```bash
+
+# Run a backup
+
+php artisan backup:run
+
+# Backup only the database
+
+php artisan backup:run --only-db
+
+# Backup specific database connections
+
+php artisan backup:run --db-name=mysql --db-name=pgsql
+
+# Backup only files (no database)
+
+php artisan backup:run --only-files
+
+# Backup to a specific disk
+
+php artisan backup:run --only-to-disk=s3
+
+# Custom filename
+
+php artisan backup:run --filename=my-backup.zip
+
+# Clean old backups
+
+php artisan backup:clean
+
+# List all backups
+
+php artisan backup:list
+
+# Monitor backup health
+
+php artisan backup:monitor
+
+# Use an alternative config key
+
+php artisan backup:run --config=backup_secondary
+```
+
+## Scheduling
+
+Add to `routes/console.php` or `app/Console/Kernel.php`:
+
+```php
+use Illuminate\Support\Facades\Schedule;
+
+Schedule::command('backup:clean')->daily()->at('01:00');
+Schedule::command('backup:run')->daily()->at('01:30');
+Schedule::command('backup:monitor')->daily()->at('10:00');
+```
+
+## Configuration
+
+Published to `config/backup.php` with sections: `backup` (source files/databases, destination disks, encryption), `notifications` (mail, Slack, Discord), `monitor_backups` (health checks), and `cleanup` (retention strategy).
+
+### Database Dump Customization
+
+Customize dumps per connection in `config/database.php`:
+
+```php
+'mysql' => [
+ // ...
+ 'dump' => [
+ 'exclude_tables' => ['telescope_entries', 'telescope_monitoring'],
+ 'useSingleTransaction' => true,
+ ],
+],
+```
+
+Enable dump compression:
+
+```php
+// config/backup.php
+'database_dump_compressor' => \Spatie\DbDumper\Compressors\GzipCompressor::class,
+```
+
+### Multiple Backup Destinations
+
+```php
+// config/backup.php
+'destination' => [
+ 'disks' => ['local', 's3'],
+],
+```
+
+### Encryption
+
+```php
+// config/backup.php
+'password' => env('BACKUP_ARCHIVE_PASSWORD'),
+'encryption' => 'default', // Uses ZipArchive::EM_AES_256
+```
+
+## Custom Cleanup Strategy
+
+Extend `Spatie\Backup\Tasks\Cleanup\CleanupStrategy` and implement `deleteOldBackups`:
+
+```php
+use Spatie\Backup\BackupDestination\BackupCollection;
+use Spatie\Backup\Tasks\Cleanup\CleanupStrategy;
+
+class MyCleanupStrategy extends CleanupStrategy
+{
+ public function deleteOldBackups(BackupCollection $backups): void
+ {
+ $backups
+ ->reject(fn ($backup) => $backup->date()->gt(now()->subMonth()))
+ ->each(fn ($backup) => $backup->delete());
+ }
+}
+```
+
+Register in `config/backup.php`:
+
+```php
+'cleanup' => [
+ 'strategy' => \App\Backup\MyCleanupStrategy::class,
+],
+```
+
+## Custom Health Check
+
+Extend `Spatie\Backup\Tasks\Monitor\HealthCheck` and implement `checkHealth`:
+
+```php
+use Spatie\Backup\BackupDestination\BackupDestination;
+use Spatie\Backup\Tasks\Monitor\HealthCheck;
+
+class MinimumBackupCount extends HealthCheck
+{
+ protected int $minimumCount;
+
+ public function __construct(int $minimumCount = 3)
+ {
+ $this->minimumCount = $minimumCount;
+ }
+
+ public function checkHealth(BackupDestination $backupDestination): void
+ {
+ $this->failIf(
+ $backupDestination->backups()->count() < $this->minimumCount,
+ "Expected at least {$this->minimumCount} backups."
+ );
+ }
+}
+```
+
+Register in `config/backup.php`:
+
+```php
+'health_checks' => [
+ \App\Backup\MinimumBackupCount::class => 5,
+],
+```
+
+## Custom Notification
+
+Extend `Spatie\Backup\Notifications\BaseNotification`:
+
+```php
+use Illuminate\Notifications\Messages\MailMessage;
+use Spatie\Backup\Notifications\BaseNotification;
+
+class CustomBackupFailedNotification extends BaseNotification
+{
+ public function toMail(): MailMessage
+ {
+ return (new MailMessage)
+ ->error()
+ ->subject("Backup failed for {$this->applicationName()}")
+ ->line($this->event->exception->getMessage());
+ }
+}
+```
+
+Map it in `config/backup.php`:
+
+```php
+'notifications' => [
+ 'notifications' => [
+ \App\Notifications\CustomBackupFailedNotification::class => ['mail'],
+ ],
+],
+```
+
+## Events
+
+All events are in `Spatie\Backup\Events`:
+
+- `BackupWasSuccessful` - Backup completed successfully
+- `BackupHasFailed` - Backup failed (includes exception and optional backup destination)
+- `BackupManifestWasCreated` - File manifest created before zipping
+- `BackupZipWasCreated` - Zip archive created (used for encryption hook)
+- `DumpingDatabase` - Database dump in progress
+- `CleanupWasSuccessful` / `CleanupHasFailed` - Cleanup result
+- `HealthyBackupWasFound` / `UnhealthyBackupWasFound` - Monitor result
+
+## Common Pitfalls
+
+- Forgetting to schedule `backup:clean` alongside `backup:run`, causing disk space to fill up
+- Not excluding `vendor` and `node_modules` from file backups (excluded by default)
+- Setting `only-db` and `only-files` together (mutually exclusive)
+- Missing the `ext-zip` PHP extension
+- Not configuring the notification `mail.to` address after publishing config
diff --git a/.agents/skills/laravel-best-practices/SKILL.md b/.agents/skills/laravel-best-practices/SKILL.md
new file mode 100644
index 0000000..965e267
--- /dev/null
+++ b/.agents/skills/laravel-best-practices/SKILL.md
@@ -0,0 +1,190 @@
+---
+name: laravel-best-practices
+description: "Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns."
+license: MIT
+metadata:
+ author: laravel
+---
+
+# Laravel Best Practices
+
+Best practices for Laravel, prioritized by impact. Each rule teaches what to do and why. For exact API syntax, verify with `search-docs`.
+
+## Consistency First
+
+Before applying any rule, check what the application already does. Laravel offers multiple valid approaches — the best choice is the one the codebase already uses, even if another pattern would be theoretically better. Inconsistency is worse than a suboptimal pattern.
+
+Check sibling files, related controllers, models, or tests for established patterns. If one exists, follow it — don't introduce a second way. These rules are defaults for when no pattern exists yet, not overrides.
+
+## Quick Reference
+
+### 1. Database Performance → `rules/db-performance.md`
+
+- Eager load with `with()` to prevent N+1 queries
+- Enable `Model::preventLazyLoading()` in development
+- Select only needed columns, avoid `SELECT *`
+- `chunk()` / `chunkById()` for large datasets
+- Index columns used in `WHERE`, `ORDER BY`, `JOIN`
+- `withCount()` instead of loading relations to count
+- `cursor()` for memory-efficient read-only iteration
+- Never query in Blade templates
+
+### 2. Advanced Query Patterns → `rules/advanced-queries.md`
+
+- `addSelect()` subqueries over eager-loading entire has-many for a single value
+- Dynamic relationships via subquery FK + `belongsTo`
+- Conditional aggregates (`CASE WHEN` in `selectRaw`) over multiple count queries
+- `setRelation()` to prevent circular N+1 queries
+- `whereIn` + `pluck()` over `whereHas` for better index usage
+- Two simple queries can beat one complex query
+- Compound indexes matching `orderBy` column order
+- Correlated subqueries in `orderBy` for has-many sorting (avoid joins)
+
+### 3. Security → `rules/security.md`
+
+- Define `$fillable` or `$guarded` on every model, authorize every action via policies or gates
+- No raw SQL with user input — use Eloquent or query builder
+- `{{ }}` for output escaping, `@csrf` on all POST/PUT/DELETE forms, `throttle` on auth and API routes
+- Validate MIME type, extension, and size for file uploads
+- Never commit `.env`, use `config()` for secrets, `encrypted` cast for sensitive DB fields
+
+### 4. Caching → `rules/caching.md`
+
+- `Cache::remember()` over manual get/put
+- `Cache::flexible()` for stale-while-revalidate on high-traffic data
+- `Cache::memo()` to avoid redundant cache hits within a request
+- Cache tags to invalidate related groups
+- `Cache::add()` for atomic conditional writes
+- `once()` to memoize per-request or per-object lifetime
+- `Cache::lock()` / `lockForUpdate()` for race conditions
+- Failover cache stores in production
+
+### 5. Eloquent Patterns → `rules/eloquent.md`
+
+- Correct relationship types with return type hints
+- Local scopes for reusable query constraints
+- Global scopes sparingly — document their existence
+- Attribute casts in the `casts()` method
+- Cast date columns, use Carbon instances in templates
+- `whereBelongsTo($model)` for cleaner queries
+- Never hardcode table names — use `(new Model)->getTable()` or Eloquent queries
+
+### 6. Validation & Forms → `rules/validation.md`
+
+- Form Request classes, not inline validation
+- Array notation `['required', 'email']` for new code; follow existing convention
+- `$request->validated()` only — never `$request->all()`
+- `Rule::when()` for conditional validation
+- `after()` instead of `withValidator()`
+
+### 7. Configuration → `rules/config.md`
+
+- `env()` only inside config files
+- `App::environment()` or `app()->isProduction()`
+- Config, lang files, and constants over hardcoded text
+
+### 8. Testing Patterns → `rules/testing.md`
+
+- `LazilyRefreshDatabase` over `RefreshDatabase` for speed
+- `assertModelExists()` over raw `assertDatabaseHas()`
+- Factory states and sequences over manual overrides
+- Use fakes (`Event::fake()`, `Exceptions::fake()`, etc.) — but always after factory setup, not before
+- `recycle()` to share relationship instances across factories
+
+### 9. Queue & Job Patterns → `rules/queue-jobs.md`
+
+- `retry_after` must exceed job `timeout`; use exponential backoff `[1, 5, 10]`
+- `ShouldBeUnique` to prevent duplicates; `ShouldBeUniqueUntilProcessing` for early lock release
+- Always implement `failed()`; with `retryUntil()`, set `$tries = 0`
+- `RateLimited` middleware for external API calls; `Bus::batch()` for related jobs
+- Horizon for complex multi-queue scenarios
+
+### 10. Routing & Controllers → `rules/routing.md`
+
+- Implicit route model binding
+- Scoped bindings for nested resources
+- `Route::resource()` or `apiResource()`
+- Methods under 10 lines — extract to actions/services
+- Type-hint Form Requests for auto-validation
+
+### 11. HTTP Client → `rules/http-client.md`
+
+- Explicit `timeout` and `connectTimeout` on every request
+- `retry()` with exponential backoff for external APIs
+- Check response status or use `throw()`
+- `Http::pool()` for concurrent independent requests
+- `Http::fake()` and `preventStrayRequests()` in tests
+
+### 12. Events, Notifications & Mail → `rules/events-notifications.md`, `rules/mail.md`
+
+- Event discovery over manual registration; `event:cache` in production
+- `ShouldDispatchAfterCommit` / `afterCommit()` inside transactions
+- Queue notifications and mailables with `ShouldQueue`
+- On-demand notifications for non-user recipients
+- `HasLocalePreference` on notifiable models
+- `assertQueued()` not `assertSent()` for queued mailables
+- Markdown mailables for transactional emails
+
+### 13. Error Handling → `rules/error-handling.md`
+
+- `report()`/`render()` on exception classes or in `bootstrap/app.php` — follow existing pattern
+- `ShouldntReport` for exceptions that should never log
+- Throttle high-volume exceptions to protect log sinks
+- `dontReportDuplicates()` for multi-catch scenarios
+- Force JSON rendering for API routes
+- Structured context via `context()` on exception classes
+
+### 14. Task Scheduling → `rules/scheduling.md`
+
+- `withoutOverlapping()` on variable-duration tasks
+- `onOneServer()` on multi-server deployments
+- `runInBackground()` for concurrent long tasks
+- `environments()` to restrict to appropriate environments
+- `takeUntilTimeout()` for time-bounded processing
+- Schedule groups for shared configuration
+
+### 15. Architecture → `rules/architecture.md`
+
+- Single-purpose Action classes; dependency injection over `app()` helper
+- Prefer official Laravel packages and follow conventions, don't override defaults
+- Default to `ORDER BY id DESC` or `created_at DESC`; `mb_*` for UTF-8 safety
+- `defer()` for post-response work; `Context` for request-scoped data; `Concurrency::run()` for parallel execution
+
+### 16. Migrations → `rules/migrations.md`
+
+- Generate migrations with `php artisan make:migration`
+- `constrained()` for foreign keys
+- Never modify migrations that have run in production
+- Add indexes in the migration, not as an afterthought
+- Mirror column defaults in model `$attributes`
+- Reversible `down()` by default; forward-fix migrations for intentionally irreversible changes
+- One concern per migration — never mix DDL and DML
+
+### 17. Collections → `rules/collections.md`
+
+- Higher-order messages for simple collection operations
+- `cursor()` vs. `lazy()` — choose based on relationship needs
+- `lazyById()` when updating records while iterating
+- `toQuery()` for bulk operations on collections
+
+### 18. Blade & Views → `rules/blade-views.md`
+
+- `$attributes->merge()` in component templates
+- Blade components over `@include`; `@pushOnce` for per-component scripts
+- View Composers for shared view data
+- `@aware` for deeply nested component props
+
+### 19. Conventions & Style → `rules/style.md`
+
+- Follow Laravel naming conventions for all entities
+- Prefer Laravel helpers (`Str`, `Arr`, `Number`, `Uri`, `Str::of()`, `$request->string()`) over raw PHP functions
+- No JS/CSS in Blade, no HTML in PHP classes
+- Code should be readable; comments only for config files
+
+## How to Apply
+
+Always use a sub-agent to read rule files and explore this skill's content.
+
+1. Identify the file type and select relevant sections (e.g., migration → §16, controller → §1, §3, §5, §6, §10)
+2. Check sibling files for existing patterns — follow those first per Consistency First
+3. Verify API syntax with `search-docs` for the installed Laravel version
diff --git a/.agents/skills/laravel-best-practices/rules/advanced-queries.md b/.agents/skills/laravel-best-practices/rules/advanced-queries.md
new file mode 100644
index 0000000..f12876e
--- /dev/null
+++ b/.agents/skills/laravel-best-practices/rules/advanced-queries.md
@@ -0,0 +1,106 @@
+# Advanced Query Patterns
+
+## Use `addSelect()` Subqueries for Single Values from Has-Many
+
+Instead of eager-loading an entire has-many relationship for a single value (like the latest timestamp), use a correlated subquery via `addSelect()`. This pulls the value directly in the main SQL query — zero extra queries.
+
+```php
+public function scopeWithLastLoginAt($query): void
+{
+ $query->addSelect([
+ 'last_login_at' => Login::select('created_at')
+ ->whereColumn('user_id', 'users.id')
+ ->latest()
+ ->take(1),
+ ])->withCasts(['last_login_at' => 'datetime']);
+}
+```
+
+## Create Dynamic Relationships via Subquery FK
+
+Extend the `addSelect()` pattern to fetch a foreign key via subquery, then define a `belongsTo` relationship on that virtual attribute. This provides a fully-hydrated related model without loading the entire collection.
+
+```php
+public function lastLogin(): BelongsTo
+{
+ return $this->belongsTo(Login::class);
+}
+
+public function scopeWithLastLogin($query): void
+{
+ $query->addSelect([
+ 'last_login_id' => Login::select('id')
+ ->whereColumn('user_id', 'users.id')
+ ->latest()
+ ->take(1),
+ ])->with('lastLogin');
+}
+```
+
+## Use Conditional Aggregates Instead of Multiple Count Queries
+
+Replace N separate `count()` queries with a single query using `CASE WHEN` inside `selectRaw()`. Use `toBase()` to skip model hydration when you only need scalar values.
+
+```php
+$statuses = Feature::toBase()
+ ->selectRaw("count(case when status = 'Requested' then 1 end) as requested")
+ ->selectRaw("count(case when status = 'Planned' then 1 end) as planned")
+ ->selectRaw("count(case when status = 'Completed' then 1 end) as completed")
+ ->first();
+```
+
+## Use `setRelation()` to Prevent Circular N+1
+
+When a parent model is eager-loaded with its children, and the view also needs `$child->parent`, use `setRelation()` to inject the already-loaded parent rather than letting Eloquent fire N additional queries.
+
+```php
+$feature->load('comments.user');
+$feature->comments->each->setRelation('feature', $feature);
+```
+
+## Prefer `whereIn` + Subquery Over `whereHas`
+
+`whereHas()` emits a correlated `EXISTS` subquery that re-executes per row. Using `whereIn()` with a `select('id')` subquery lets the database use an index lookup instead, without loading data into PHP memory.
+
+Incorrect (correlated EXISTS re-executes per row):
+
+```php
+$query->whereHas('company', fn ($q) => $q->where('name', 'like', $term));
+```
+
+Correct (index-friendly subquery, no PHP memory overhead):
+
+```php
+$query->whereIn('company_id', Company::where('name', 'like', $term)->select('id'));
+```
+
+## Sometimes Two Simple Queries Beat One Complex Query
+
+Running a small, targeted secondary query and passing its results via `whereIn` is often faster than a single complex correlated subquery or join. The additional round-trip is worthwhile when the secondary query is highly selective and uses its own index.
+
+## Use Compound Indexes Matching `orderBy` Column Order
+
+When ordering by multiple columns, create a single compound index in the same column order as the `ORDER BY` clause. Individual single-column indexes cannot combine for multi-column sorts — the database will filesort without a compound index.
+
+```php
+// Migration
+$table->index(['last_name', 'first_name']);
+
+// Query — column order must match the index
+User::query()->orderBy('last_name')->orderBy('first_name')->paginate();
+```
+
+## Use Correlated Subqueries for Has-Many Ordering
+
+When sorting by a value from a has-many relationship, avoid joins (they duplicate rows). Use a correlated subquery inside `orderBy()` instead, paired with an `addSelect` scope for eager loading.
+
+```php
+public function scopeOrderByLastLogin($query): void
+{
+ $query->orderByDesc(Login::select('created_at')
+ ->whereColumn('user_id', 'users.id')
+ ->latest()
+ ->take(1)
+ );
+}
+```
diff --git a/.agents/skills/laravel-best-practices/rules/architecture.md b/.agents/skills/laravel-best-practices/rules/architecture.md
new file mode 100644
index 0000000..51c6e65
--- /dev/null
+++ b/.agents/skills/laravel-best-practices/rules/architecture.md
@@ -0,0 +1,202 @@
+# Architecture Best Practices
+
+## Single-Purpose Action Classes
+
+Extract discrete business operations into invokable Action classes.
+
+```php
+class CreateOrderAction
+{
+ public function __construct(private InventoryService $inventory) {}
+
+ public function execute(array $data): Order
+ {
+ $order = Order::create($data);
+ $this->inventory->reserve($order);
+
+ return $order;
+ }
+}
+```
+
+## Use Dependency Injection
+
+Always use constructor injection. Avoid `app()` or `resolve()` inside classes.
+
+Incorrect:
+```php
+class OrderController extends Controller
+{
+ public function store(StoreOrderRequest $request)
+ {
+ $service = app(OrderService::class);
+
+ return $service->create($request->validated());
+ }
+}
+```
+
+Correct:
+```php
+class OrderController extends Controller
+{
+ public function __construct(private OrderService $service) {}
+
+ public function store(StoreOrderRequest $request)
+ {
+ return $this->service->create($request->validated());
+ }
+}
+```
+
+## Code to Interfaces
+
+Depend on contracts at system boundaries (payment gateways, notification channels, external APIs) for testability and swappability.
+
+Incorrect (concrete dependency):
+```php
+class OrderService
+{
+ public function __construct(private StripeGateway $gateway) {}
+}
+```
+
+Correct (interface dependency):
+```php
+interface PaymentGateway
+{
+ public function charge(int $amount, string $customerId): PaymentResult;
+}
+
+class OrderService
+{
+ public function __construct(private PaymentGateway $gateway) {}
+}
+```
+
+Bind in a service provider:
+
+```php
+$this->app->bind(PaymentGateway::class, StripeGateway::class);
+```
+
+## Default Sort by Descending
+
+When no explicit order is specified, sort by `id` or `created_at` descending. Without an explicit `ORDER BY`, row order is undefined.
+
+Incorrect:
+```php
+$posts = Post::paginate();
+```
+
+Correct:
+```php
+$posts = Post::latest()->paginate();
+```
+
+## Use Atomic Locks for Race Conditions
+
+Prevent race conditions with `Cache::lock()` or `lockForUpdate()`.
+
+```php
+Cache::lock('order-processing-'.$order->id, 10)->block(5, function () use ($order) {
+ $order->process();
+});
+
+// Or at query level
+$product = Product::where('id', $id)->lockForUpdate()->first();
+```
+
+## Use `mb_*` String Functions
+
+When no Laravel helper exists, prefer `mb_strlen`, `mb_strtolower`, etc. for UTF-8 safety. Standard PHP string functions count bytes, not characters.
+
+Incorrect:
+```php
+strlen('José'); // 5 (bytes, not characters)
+strtolower('MÜNCHEN'); // 'mÜnchen' — fails on multibyte
+```
+
+Correct:
+```php
+mb_strlen('José'); // 4 (characters)
+mb_strtolower('MÜNCHEN'); // 'münchen'
+
+// Prefer Laravel's Str helpers when available
+Str::length('José'); // 4
+Str::lower('MÜNCHEN'); // 'münchen'
+```
+
+## Use `defer()` for Post-Response Work
+
+For lightweight tasks that don't need to survive a crash (logging, analytics, cleanup), use `defer()` instead of dispatching a job. The callback runs after the HTTP response is sent — no queue overhead.
+
+Incorrect (job overhead for trivial work):
+```php
+dispatch(new LogPageView($page));
+```
+
+Correct (runs after response, same process):
+```php
+defer(fn () => PageView::create(['page_id' => $page->id, 'user_id' => auth()->id()]));
+```
+
+Use jobs when the work must survive process crashes or needs retry logic. Use `defer()` for fire-and-forget work.
+
+## Use `Context` for Request-Scoped Data
+
+The `Context` facade passes data through the entire request lifecycle — middleware, controllers, jobs, logs — without passing arguments manually.
+
+```php
+// In middleware
+Context::add('tenant_id', $request->header('X-Tenant-ID'));
+
+// Anywhere later — controllers, jobs, log context
+$tenantId = Context::get('tenant_id');
+```
+
+Context data automatically propagates to queued jobs and is included in log entries. Use `Context::addHidden()` for sensitive data that should be available in queued jobs but excluded from log context. If data must not leave the current process, do not store it in `Context`.
+
+## Use `Concurrency::run()` for Parallel Execution
+
+Run independent operations in parallel using child processes — no async libraries needed.
+
+```php
+use Illuminate\Support\Facades\Concurrency;
+
+[$users, $orders] = Concurrency::run([
+ fn () => User::count(),
+ fn () => Order::where('status', 'pending')->count(),
+]);
+```
+
+Each closure runs in a separate process with full Laravel access. Use for independent database queries, API calls, or computations that would otherwise run sequentially.
+
+## Convention Over Configuration
+
+Follow Laravel conventions. Don't override defaults unnecessarily.
+
+Incorrect:
+```php
+class Customer extends Model
+{
+ protected $table = 'Customer';
+ protected $primaryKey = 'customer_id';
+
+ public function roles(): BelongsToMany
+ {
+ return $this->belongsToMany(Role::class, 'role_customer', 'customer_id', 'role_id');
+ }
+}
+```
+
+Correct:
+```php
+class Customer extends Model
+{
+ public function roles(): BelongsToMany
+ {
+ return $this->belongsToMany(Role::class);
+ }
+}
+```
diff --git a/.agents/skills/laravel-best-practices/rules/blade-views.md b/.agents/skills/laravel-best-practices/rules/blade-views.md
new file mode 100644
index 0000000..5f0b3a1
--- /dev/null
+++ b/.agents/skills/laravel-best-practices/rules/blade-views.md
@@ -0,0 +1,36 @@
+# Blade & Views Best Practices
+
+## Use `$attributes->merge()` in Component Templates
+
+Hardcoding classes prevents consumers from adding their own. `merge()` combines class attributes cleanly.
+
+```blade
+
merge(['class' => 'alert alert-'.$type]) }}>
+ {{ $message }}
+
+```
+
+## Use `@pushOnce` for Per-Component Scripts
+
+If a component renders inside a `@foreach`, `@push` inserts the script N times. `@pushOnce` guarantees it's included exactly once.
+
+## Prefer Blade Components Over `@include`
+
+`@include` shares all parent variables implicitly (hidden coupling). Components have explicit props, attribute bags, and slots.
+
+## Use View Composers for Shared View Data
+
+If every controller rendering a sidebar must pass `$categories`, that's duplicated code. A View Composer centralizes it.
+
+## Use Blade Fragments for Partial Re-Renders (htmx/Turbo)
+
+A single view can return either the full page or just a fragment, keeping routing clean.
+
+```php
+return view('dashboard', compact('users'))
+ ->fragmentIf($request->hasHeader('HX-Request'), 'user-list');
+```
+
+## Use `@aware` for Deeply Nested Component Props
+
+Avoids re-passing parent props through every level of nested components.
diff --git a/.agents/skills/laravel-best-practices/rules/caching.md b/.agents/skills/laravel-best-practices/rules/caching.md
new file mode 100644
index 0000000..67408d6
--- /dev/null
+++ b/.agents/skills/laravel-best-practices/rules/caching.md
@@ -0,0 +1,70 @@
+# Caching Best Practices
+
+## Use `Cache::remember()` Instead of Manual Get/Put
+
+Cleaner cache-aside pattern that removes boilerplate. use `Cache::lock()` for race conditions.
+
+Incorrect:
+```php
+$val = Cache::get('stats');
+if (! $val) {
+ $val = $this->computeStats();
+ Cache::put('stats', $val, 60);
+}
+```
+
+Correct:
+```php
+$val = Cache::remember('stats', 60, fn () => $this->computeStats());
+```
+
+## Use `Cache::flexible()` for Stale-While-Revalidate
+
+On high-traffic keys, one user always gets a slow response when the cache expires. `flexible()` serves slightly stale data while refreshing in the background.
+
+Incorrect: `Cache::remember('users', 300, fn () => User::all());`
+
+Correct: `Cache::flexible('users', [300, 600], fn () => User::all());` — fresh for 5 min, stale-but-served up to 10 min, refreshes via deferred function.
+
+## Use `Cache::memo()` to Avoid Redundant Hits Within a Request
+
+If the same cache key is read multiple times per request (e.g., a service called from multiple places), `memo()` stores the resolved value in memory.
+
+`Cache::memo()->get('settings');` — 5 calls = 1 Redis round-trip instead of 5.
+
+## Use Cache Tags to Invalidate Related Groups
+
+Without tags, invalidating a group of entries requires tracking every key. Tags let you flush atomically. Only works with `redis`, `memcached`, `dynamodb` — not `file` or `database`.
+
+```php
+Cache::tags(['user-1'])->flush();
+```
+
+## Use `Cache::add()` for Atomic Conditional Writes
+
+`add()` only writes if the key does not exist — atomic, no race condition between checking and writing.
+
+Incorrect: `if (! Cache::has('lock')) { Cache::put('lock', true, 10); }`
+
+Correct: `Cache::add('lock', true, 10);`
+
+## Use `once()` for Per-Request Memoization
+
+`once()` memoizes a function's return value for the lifetime of the object (or request for closures). Unlike `Cache::memo()`, it doesn't hit the cache store at all — pure in-memory.
+
+```php
+public function roles(): Collection
+{
+ return once(fn () => $this->loadRoles());
+}
+```
+
+Multiple calls return the cached result without re-executing. Use `once()` for expensive computations called multiple times per request. Use `Cache::memo()` when you also want cross-request caching.
+
+## Configure Failover Cache Stores in Production
+
+If Redis goes down, the app falls back to a secondary store automatically.
+
+```php
+'failover' => ['driver' => 'failover', 'stores' => ['redis', 'database']],
+```
diff --git a/.agents/skills/laravel-best-practices/rules/collections.md b/.agents/skills/laravel-best-practices/rules/collections.md
new file mode 100644
index 0000000..18e8d9e
--- /dev/null
+++ b/.agents/skills/laravel-best-practices/rules/collections.md
@@ -0,0 +1,44 @@
+# Collection Best Practices
+
+## Use Higher-Order Messages for Simple Operations
+
+Incorrect:
+```php
+$users->each(function (User $user) {
+ $user->markAsVip();
+});
+```
+
+Correct: `$users->each->markAsVip();`
+
+Works with `each`, `map`, `sum`, `filter`, `reject`, `contains`, etc.
+
+## Choose `cursor()` vs. `lazy()` Correctly
+
+- `cursor()` — one model in memory, but cannot eager-load relationships (N+1 risk).
+- `lazy()` — chunked pagination returning a flat LazyCollection, supports eager loading.
+
+Incorrect: `User::with('roles')->cursor()` — eager loading silently ignored.
+
+Correct: `User::with('roles')->lazy()` for relationship access; `User::cursor()` for attribute-only work.
+
+## Use `lazyById()` When Updating Records While Iterating
+
+`lazy()` uses offset pagination — updating records during iteration can skip or double-process. `lazyById()` uses `id > last_id`, safe against mutation.
+
+## Use `toQuery()` for Bulk Operations on Collections
+
+Avoids manual `whereIn` construction.
+
+Incorrect: `User::whereIn('id', $users->pluck('id'))->update([...]);`
+
+Correct: `$users->toQuery()->update([...]);`
+
+## Use `#[CollectedBy]` for Custom Collection Classes
+
+More declarative than overriding `newCollection()`.
+
+```php
+#[CollectedBy(UserCollection::class)]
+class User extends Model {}
+```
diff --git a/.agents/skills/laravel-best-practices/rules/config.md b/.agents/skills/laravel-best-practices/rules/config.md
new file mode 100644
index 0000000..9bea727
--- /dev/null
+++ b/.agents/skills/laravel-best-practices/rules/config.md
@@ -0,0 +1,73 @@
+# Configuration Best Practices
+
+## `env()` Only in Config Files
+
+Direct `env()` calls may return `null` when config is cached.
+
+Incorrect:
+```php
+$key = env('API_KEY');
+```
+
+Correct:
+```php
+// config/services.php
+'key' => env('API_KEY'),
+
+// Application code
+$key = config('services.key');
+```
+
+## Use Encrypted Env or External Secrets
+
+Never store production secrets in plain `.env` files in version control.
+
+Incorrect:
+```bash
+
+# .env committed to repo or shared in Slack
+
+STRIPE_SECRET=sk_live_abc123
+AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI
+```
+
+Correct:
+```bash
+php artisan env:encrypt --env=production --readable
+php artisan env:decrypt --env=production
+```
+
+For cloud deployments, prefer the platform's native secret store (AWS Secrets Manager, Vault, etc.) and inject at runtime.
+
+## Use `App::environment()` for Environment Checks
+
+Incorrect:
+```php
+if (env('APP_ENV') === 'production') {
+```
+
+Correct:
+```php
+if (app()->isProduction()) {
+// or
+if (App::environment('production')) {
+```
+
+## Use Constants and Language Files
+
+Use class constants instead of hardcoded magic strings for model states, types, and statuses.
+
+```php
+// Incorrect
+return $this->type === 'normal';
+
+// Correct
+return $this->type === self::TYPE_NORMAL;
+```
+
+If the application already uses language files for localization, use `__()` for user-facing strings too. Do not introduce language files purely for English-only apps — simple string literals are fine there.
+
+```php
+// Only when lang files already exist in the project
+return back()->with('message', __('app.article_added'));
+```
diff --git a/.agents/skills/laravel-best-practices/rules/db-performance.md b/.agents/skills/laravel-best-practices/rules/db-performance.md
new file mode 100644
index 0000000..c49ba16
--- /dev/null
+++ b/.agents/skills/laravel-best-practices/rules/db-performance.md
@@ -0,0 +1,192 @@
+# Database Performance Best Practices
+
+## Always Eager Load Relationships
+
+Lazy loading causes N+1 query problems — one query per loop iteration. Always use `with()` to load relationships upfront.
+
+Incorrect (N+1 — executes 1 + N queries):
+```php
+$posts = Post::all();
+foreach ($posts as $post) {
+ echo $post->author->name;
+}
+```
+
+Correct (2 queries total):
+```php
+$posts = Post::with('author')->get();
+foreach ($posts as $post) {
+ echo $post->author->name;
+}
+```
+
+Constrain eager loads to select only needed columns (always include the foreign key):
+
+```php
+$users = User::with(['posts' => function ($query) {
+ $query->select('id', 'user_id', 'title')
+ ->where('published', true)
+ ->latest()
+ ->limit(10);
+}])->get();
+```
+
+## Prevent Lazy Loading in Development
+
+Enable this in `AppServiceProvider::boot()` to catch N+1 issues during development.
+
+```php
+public function boot(): void
+{
+ Model::preventLazyLoading(! app()->isProduction());
+}
+```
+
+Throws `LazyLoadingViolationException` when a relationship is accessed without being eager-loaded.
+
+## Select Only Needed Columns
+
+Avoid `SELECT *` — especially when tables have large text or JSON columns.
+
+Incorrect:
+```php
+$posts = Post::with('author')->get();
+```
+
+Correct:
+```php
+$posts = Post::select('id', 'title', 'user_id', 'created_at')
+ ->with(['author:id,name,avatar'])
+ ->get();
+```
+
+When selecting columns on eager-loaded relationships, always include the foreign key column or the relationship won't match.
+
+## Chunk Large Datasets
+
+Never load thousands of records at once. Use chunking for batch processing.
+
+Incorrect:
+```php
+$users = User::all();
+foreach ($users as $user) {
+ $user->notify(new WeeklyDigest);
+}
+```
+
+Correct:
+```php
+User::where('subscribed', true)->chunk(200, function ($users) {
+ foreach ($users as $user) {
+ $user->notify(new WeeklyDigest);
+ }
+});
+```
+
+Use `chunkById()` when modifying records during iteration — standard `chunk()` uses OFFSET which shifts when rows change:
+
+```php
+User::where('active', false)->chunkById(200, function ($users) {
+ $users->each->delete();
+});
+```
+
+## Add Database Indexes
+
+Index columns that appear in `WHERE`, `ORDER BY`, `JOIN`, and `GROUP BY` clauses.
+
+Incorrect:
+```php
+Schema::create('orders', function (Blueprint $table) {
+ $table->id();
+ $table->foreignId('user_id')->constrained();
+ $table->string('status');
+ $table->timestamps();
+});
+```
+
+Correct:
+```php
+Schema::create('orders', function (Blueprint $table) {
+ $table->id();
+ $table->foreignId('user_id')->index()->constrained();
+ $table->string('status')->index();
+ $table->timestamps();
+ $table->index(['status', 'created_at']);
+});
+```
+
+Add composite indexes for common query patterns (e.g., `WHERE status = ? ORDER BY created_at`).
+
+## Use `withCount()` for Counting Relations
+
+Never load entire collections just to count them.
+
+Incorrect:
+```php
+$posts = Post::all();
+foreach ($posts as $post) {
+ echo $post->comments->count();
+}
+```
+
+Correct:
+```php
+$posts = Post::withCount('comments')->get();
+foreach ($posts as $post) {
+ echo $post->comments_count;
+}
+```
+
+Conditional counting:
+
+```php
+$posts = Post::withCount([
+ 'comments',
+ 'comments as approved_comments_count' => function ($query) {
+ $query->where('approved', true);
+ },
+])->get();
+```
+
+## Use `cursor()` for Memory-Efficient Iteration
+
+For read-only iteration over large result sets, `cursor()` loads one record at a time via a PHP generator.
+
+Incorrect:
+```php
+$users = User::where('active', true)->get();
+```
+
+Correct:
+```php
+foreach (User::where('active', true)->cursor() as $user) {
+ ProcessUser::dispatch($user->id);
+}
+```
+
+Use `cursor()` for read-only iteration. Use `chunk()` / `chunkById()` when modifying records.
+
+## No Queries in Blade Templates
+
+Never execute queries in Blade templates. Pass data from controllers.
+
+Incorrect:
+```blade
+@foreach (User::all() as $user)
+ {{ $user->profile->name }}
+@endforeach
+```
+
+Correct:
+```php
+// Controller
+$users = User::with('profile')->get();
+return view('users.index', compact('users'));
+```
+
+```blade
+@foreach ($users as $user)
+ {{ $user->profile->name }}
+@endforeach
+```
diff --git a/.agents/skills/laravel-best-practices/rules/eloquent.md b/.agents/skills/laravel-best-practices/rules/eloquent.md
new file mode 100644
index 0000000..413d5da
--- /dev/null
+++ b/.agents/skills/laravel-best-practices/rules/eloquent.md
@@ -0,0 +1,148 @@
+# Eloquent Best Practices
+
+## Use Correct Relationship Types
+
+Use `hasMany`, `belongsTo`, `morphMany`, etc. with proper return type hints.
+
+```php
+public function comments(): HasMany
+{
+ return $this->hasMany(Comment::class);
+}
+
+public function author(): BelongsTo
+{
+ return $this->belongsTo(User::class, 'user_id');
+}
+```
+
+## Use Local Scopes for Reusable Queries
+
+Extract reusable query constraints into local scopes to avoid duplication.
+
+Incorrect:
+```php
+$active = User::where('verified', true)->whereNotNull('activated_at')->get();
+$articles = Article::whereHas('user', function ($q) {
+ $q->where('verified', true)->whereNotNull('activated_at');
+})->get();
+```
+
+Correct:
+```php
+public function scopeActive(Builder $query): Builder
+{
+ return $query->where('verified', true)->whereNotNull('activated_at');
+}
+
+// Usage
+$active = User::active()->get();
+$articles = Article::whereHas('user', fn ($q) => $q->active())->get();
+```
+
+## Apply Global Scopes Sparingly
+
+Global scopes silently modify every query on the model, making debugging difficult. Prefer local scopes and reserve global scopes for truly universal constraints like soft deletes or multi-tenancy.
+
+Incorrect (global scope for a conditional filter):
+```php
+class PublishedScope implements Scope
+{
+ public function apply(Builder $builder, Model $model): void
+ {
+ $builder->where('published', true);
+ }
+}
+// Now admin panels, reports, and background jobs all silently skip drafts
+```
+
+Correct (local scope you opt into):
+```php
+public function scopePublished(Builder $query): Builder
+{
+ return $query->where('published', true);
+}
+
+Post::published()->paginate(); // Explicit
+Post::paginate(); // Admin sees all
+```
+
+## Define Attribute Casts
+
+Use the `casts()` method (or `$casts` property following project convention) for automatic type conversion.
+
+```php
+protected function casts(): array
+{
+ return [
+ 'is_active' => 'boolean',
+ 'metadata' => 'array',
+ 'total' => 'decimal:2',
+ ];
+}
+```
+
+## Cast Date Columns Properly
+
+Always cast date columns. Use Carbon instances in templates instead of formatting strings manually.
+
+Incorrect:
+```blade
+{{ Carbon::createFromFormat('Y-d-m H-i', $order->ordered_at)->toDateString() }}
+```
+
+Correct:
+```php
+protected function casts(): array
+{
+ return [
+ 'ordered_at' => 'datetime',
+ ];
+}
+```
+
+```blade
+{{ $order->ordered_at->toDateString() }}
+{{ $order->ordered_at->format('m-d') }}
+```
+
+## Use `whereBelongsTo()` for Relationship Queries
+
+Cleaner than manually specifying foreign keys.
+
+Incorrect:
+```php
+Post::where('user_id', $user->id)->get();
+```
+
+Correct:
+```php
+Post::whereBelongsTo($user)->get();
+Post::whereBelongsTo($user, 'author')->get();
+```
+
+## Avoid Hardcoded Table Names in Queries
+
+Never use string literals for table names in raw queries, joins, or subqueries. Hardcoded table names make it impossible to find all places a model is used and break refactoring (e.g., renaming a table requires hunting through every raw string).
+
+Incorrect:
+```php
+DB::table('users')->where('active', true)->get();
+
+$query->join('companies', 'companies.id', '=', 'users.company_id');
+
+DB::select('SELECT * FROM orders WHERE status = ?', ['pending']);
+```
+
+Correct — reference the model's table:
+```php
+DB::table((new User)->getTable())->where('active', true)->get();
+
+// Even better — use Eloquent or the query builder instead of raw SQL
+User::where('active', true)->get();
+Order::where('status', 'pending')->get();
+```
+
+Prefer Eloquent queries and relationships over `DB::table()` whenever possible — they already reference the model's table. When `DB::table()` or raw joins are unavoidable, always use `(new Model)->getTable()` to keep the reference traceable.
+
+**Exception — migrations:** In migrations, hardcoded table names via `DB::table('settings')` are acceptable and preferred. Models change over time but migrations are frozen snapshots — referencing a model that is later renamed or deleted would break the migration.
diff --git a/.agents/skills/laravel-best-practices/rules/error-handling.md b/.agents/skills/laravel-best-practices/rules/error-handling.md
new file mode 100644
index 0000000..4b14866
--- /dev/null
+++ b/.agents/skills/laravel-best-practices/rules/error-handling.md
@@ -0,0 +1,72 @@
+# Error Handling Best Practices
+
+## Exception Reporting and Rendering
+
+There are two valid approaches — choose one and apply it consistently across the project.
+
+**Co-location on the exception class** — keeps behavior alongside the exception definition, easier to find:
+
+```php
+class InvalidOrderException extends Exception
+{
+ public function report(): void { /* custom reporting */ }
+
+ public function render(Request $request): Response
+ {
+ return response()->view('errors.invalid-order', status: 422);
+ }
+}
+```
+
+**Centralized in `bootstrap/app.php`** — all exception handling in one place, easier to see the full picture:
+
+```php
+->withExceptions(function (Exceptions $exceptions) {
+ $exceptions->report(function (InvalidOrderException $e) { /* ... */ });
+ $exceptions->render(function (InvalidOrderException $e, Request $request) {
+ return response()->view('errors.invalid-order', status: 422);
+ });
+})
+```
+
+Check the existing codebase and follow whichever pattern is already established.
+
+## Use `ShouldntReport` for Exceptions That Should Never Log
+
+More discoverable than listing classes in `dontReport()`.
+
+```php
+class PodcastProcessingException extends Exception implements ShouldntReport {}
+```
+
+## Throttle High-Volume Exceptions
+
+A single failing integration can flood error tracking. Use `throttle()` to rate-limit per exception type.
+
+## Enable `dontReportDuplicates()`
+
+Prevents the same exception instance from being logged multiple times when `report($e)` is called in multiple catch blocks.
+
+## Force JSON Error Rendering for API Routes
+
+Laravel auto-detects `Accept: application/json` but API clients may not set it. Explicitly declare JSON rendering for API routes.
+
+```php
+$exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) {
+ return $request->is('api/*') || $request->expectsJson();
+});
+```
+
+## Add Context to Exception Classes
+
+Attach structured data to exceptions at the source via a `context()` method — Laravel includes it automatically in the log entry.
+
+```php
+class InvalidOrderException extends Exception
+{
+ public function context(): array
+ {
+ return ['order_id' => $this->orderId];
+ }
+}
+```
diff --git a/.agents/skills/laravel-best-practices/rules/events-notifications.md b/.agents/skills/laravel-best-practices/rules/events-notifications.md
new file mode 100644
index 0000000..82e329e
--- /dev/null
+++ b/.agents/skills/laravel-best-practices/rules/events-notifications.md
@@ -0,0 +1,52 @@
+# Events & Notifications Best Practices
+
+## Rely on Event Discovery
+
+Laravel auto-discovers listeners by reading `handle(EventType $event)` type-hints. No manual registration needed in `AppServiceProvider`.
+
+## Run `event:cache` in Production Deploy
+
+Event discovery scans the filesystem per-request in dev. Cache it in production: `php artisan optimize` or `php artisan event:cache`.
+
+## Use `ShouldDispatchAfterCommit` Inside Transactions
+
+Without it, a queued listener may process before the DB transaction commits, reading data that doesn't exist yet.
+
+```php
+class OrderShipped implements ShouldDispatchAfterCommit {}
+```
+
+## Always Queue Notifications
+
+Notifications often hit external APIs (email, SMS, Slack). Without `ShouldQueue`, they block the HTTP response.
+
+```php
+class InvoicePaid extends Notification implements ShouldQueue
+{
+ use Queueable;
+}
+```
+
+## Use `afterCommit()` on Notifications in Transactions
+
+Same race condition as events — call `afterCommit()` to delay dispatch until the transaction commits.
+
+```php
+$user->notify((new InvoicePaid($invoice))->afterCommit());
+```
+
+## Route Notification Channels to Dedicated Queues
+
+Mail and database notifications have different priorities. Use `viaQueues()` to route them to separate queues.
+
+## Use On-Demand Notifications for Non-User Recipients
+
+Avoid creating dummy models to send notifications to arbitrary addresses.
+
+```php
+Notification::route('mail', 'admin@example.com')->notify(new SystemAlert());
+```
+
+## Implement `HasLocalePreference` on Notifiable Models
+
+Laravel automatically uses the user's preferred locale for all notifications and mailables — no per-call `locale()` needed.
diff --git a/.agents/skills/laravel-best-practices/rules/http-client.md b/.agents/skills/laravel-best-practices/rules/http-client.md
new file mode 100644
index 0000000..8e2f16e
--- /dev/null
+++ b/.agents/skills/laravel-best-practices/rules/http-client.md
@@ -0,0 +1,160 @@
+# HTTP Client Best Practices
+
+## Always Set Explicit Timeouts
+
+The default timeout is 30 seconds — too long for most API calls. Always set explicit `timeout` and `connectTimeout` to fail fast.
+
+Incorrect:
+```php
+$response = Http::get('https://api.example.com/users');
+```
+
+Correct:
+```php
+$response = Http::timeout(5)
+ ->connectTimeout(3)
+ ->get('https://api.example.com/users');
+```
+
+For service-specific clients, define timeouts in a macro:
+
+```php
+Http::macro('github', function () {
+ return Http::baseUrl('https://api.github.com')
+ ->timeout(10)
+ ->connectTimeout(3)
+ ->withToken(config('services.github.token'));
+});
+
+$response = Http::github()->get('/repos/laravel/framework');
+```
+
+## Use Retry with Backoff for External APIs
+
+External APIs have transient failures. Use `retry()` with increasing delays.
+
+Incorrect:
+```php
+$response = Http::post('https://api.stripe.com/v1/charges', $data);
+
+if ($response->failed()) {
+ throw new PaymentFailedException('Charge failed');
+}
+```
+
+Correct:
+```php
+$response = Http::retry([100, 500, 1000])
+ ->timeout(10)
+ ->post('https://api.stripe.com/v1/charges', $data);
+```
+
+Only retry on specific errors:
+
+```php
+$response = Http::retry(3, 100, function (Throwable $exception, PendingRequest $request) {
+ return $exception instanceof ConnectionException
+ || ($exception instanceof RequestException && $exception->response->serverError());
+})->post('https://api.example.com/data');
+```
+
+## Handle Errors Explicitly
+
+The HTTP Client does not throw on 4xx/5xx by default. Always check status or use `throw()`.
+
+Incorrect:
+```php
+$response = Http::get('https://api.example.com/users/1');
+$user = $response->json(); // Could be an error body
+```
+
+Correct:
+```php
+$response = Http::timeout(5)
+ ->get('https://api.example.com/users/1')
+ ->throw();
+
+$user = $response->json();
+```
+
+For graceful degradation:
+
+```php
+$response = Http::get('https://api.example.com/users/1');
+
+if ($response->successful()) {
+ return $response->json();
+}
+
+if ($response->notFound()) {
+ return null;
+}
+
+$response->throw();
+```
+
+## Use Request Pooling for Concurrent Requests
+
+When making multiple independent API calls, use `Http::pool()` instead of sequential calls.
+
+Incorrect:
+```php
+$users = Http::get('https://api.example.com/users')->json();
+$posts = Http::get('https://api.example.com/posts')->json();
+$comments = Http::get('https://api.example.com/comments')->json();
+```
+
+Correct:
+```php
+use Illuminate\Http\Client\Pool;
+
+$responses = Http::pool(fn (Pool $pool) => [
+ $pool->as('users')->get('https://api.example.com/users'),
+ $pool->as('posts')->get('https://api.example.com/posts'),
+ $pool->as('comments')->get('https://api.example.com/comments'),
+]);
+
+$users = $responses['users']->json();
+$posts = $responses['posts']->json();
+```
+
+## Fake HTTP Calls in Tests
+
+Never make real HTTP requests in tests. Use `Http::fake()` and `preventStrayRequests()`.
+
+Incorrect:
+```php
+it('syncs user from API', function () {
+ $service = new UserSyncService;
+ $service->sync(1); // Hits the real API
+});
+```
+
+Correct:
+```php
+it('syncs user from API', function () {
+ Http::preventStrayRequests();
+
+ Http::fake([
+ 'api.example.com/users/1' => Http::response([
+ 'name' => 'John Doe',
+ 'email' => 'john@example.com',
+ ]),
+ ]);
+
+ $service = new UserSyncService;
+ $service->sync(1);
+
+ Http::assertSent(function (Request $request) {
+ return $request->url() === 'https://api.example.com/users/1';
+ });
+});
+```
+
+Test failure scenarios too:
+
+```php
+Http::fake([
+ 'api.example.com/*' => Http::failedConnection(),
+]);
+```
diff --git a/.agents/skills/laravel-best-practices/rules/mail.md b/.agents/skills/laravel-best-practices/rules/mail.md
new file mode 100644
index 0000000..7c71733
--- /dev/null
+++ b/.agents/skills/laravel-best-practices/rules/mail.md
@@ -0,0 +1,27 @@
+# Mail Best Practices
+
+## Implement `ShouldQueue` on the Mailable Class
+
+Makes queueing the default regardless of how the mailable is dispatched. No need to remember `Mail::queue()` at every call site — `Mail::send()` also queues it.
+
+## Use `afterCommit()` on Mailables Inside Transactions
+
+A queued mailable dispatched inside a transaction may process before the commit. Use `$this->afterCommit()` in the constructor.
+
+## Use `assertQueued()` Not `assertSent()` for Queued Mailables
+
+`Mail::assertSent()` only catches synchronous mail. Queued mailables fail `assertSent` with a "Did you mean to use assertQueued()?" hint.
+
+Incorrect: `Mail::assertSent(OrderShipped::class);` when mailable implements `ShouldQueue`.
+
+Correct: `Mail::assertQueued(OrderShipped::class);`
+
+## Use Markdown Mailables for Transactional Emails
+
+Markdown mailables auto-generate both HTML and plain-text versions, use responsive components, and allow global style customization. Generate with `--markdown` flag.
+
+## Separate Content Tests from Sending Tests
+
+Content tests: instantiate the mailable directly, call `assertSeeInHtml()`.
+Sending tests: use `Mail::fake()` and `assertSent()`/`assertQueued()`.
+Don't mix them — it conflates concerns and makes tests brittle.
diff --git a/.agents/skills/laravel-best-practices/rules/migrations.md b/.agents/skills/laravel-best-practices/rules/migrations.md
new file mode 100644
index 0000000..df6f5f3
--- /dev/null
+++ b/.agents/skills/laravel-best-practices/rules/migrations.md
@@ -0,0 +1,121 @@
+# Migration Best Practices
+
+## Generate Migrations with Artisan
+
+Always use `php artisan make:migration` for consistent naming and timestamps.
+
+Incorrect (manually created file):
+```php
+// database/migrations/posts_migration.php ← wrong naming, no timestamp
+```
+
+Correct (Artisan-generated):
+```bash
+php artisan make:migration create_posts_table
+php artisan make:migration add_slug_to_posts_table
+```
+
+## Use `constrained()` for Foreign Keys
+
+Automatic naming and referential integrity.
+
+```php
+$table->foreignId('user_id')->constrained()->cascadeOnDelete();
+
+// Non-standard names
+$table->foreignId('author_id')->constrained('users');
+```
+
+## Never Modify Deployed Migrations
+
+Once a migration has run in production, treat it as immutable. Create a new migration to change the table.
+
+Incorrect (editing a deployed migration):
+```php
+// 2024_01_01_create_posts_table.php — already in production
+$table->string('slug')->unique(); // ← added after deployment
+```
+
+Correct (new migration to alter):
+```php
+// 2024_03_15_add_slug_to_posts_table.php
+Schema::table('posts', function (Blueprint $table) {
+ $table->string('slug')->unique()->after('title');
+});
+```
+
+## Add Indexes in the Migration
+
+Add indexes when creating the table, not as an afterthought. Columns used in `WHERE`, `ORDER BY`, and `JOIN` clauses need indexes.
+
+Incorrect:
+```php
+Schema::create('orders', function (Blueprint $table) {
+ $table->id();
+ $table->foreignId('user_id')->constrained();
+ $table->string('status');
+ $table->timestamps();
+});
+```
+
+Correct:
+```php
+Schema::create('orders', function (Blueprint $table) {
+ $table->id();
+ $table->foreignId('user_id')->constrained()->index();
+ $table->string('status')->index();
+ $table->timestamp('shipped_at')->nullable()->index();
+ $table->timestamps();
+});
+```
+
+## Mirror Defaults in Model `$attributes`
+
+When a column has a database default, mirror it in the model so new instances have correct values before saving.
+
+```php
+// Migration
+$table->string('status')->default('pending');
+
+// Model
+protected $attributes = [
+ 'status' => 'pending',
+];
+```
+
+## Write Reversible `down()` Methods by Default
+
+Implement `down()` for schema changes that can be safely reversed so `migrate:rollback` works in CI and failed deployments.
+
+```php
+public function down(): void
+{
+ Schema::table('posts', function (Blueprint $table) {
+ $table->dropColumn('slug');
+ });
+}
+```
+
+For intentionally irreversible migrations (e.g., destructive data backfills), leave a clear comment and require a forward fix migration instead of pretending rollback is supported.
+
+## Keep Migrations Focused
+
+One concern per migration. Never mix DDL (schema changes) and DML (data manipulation).
+
+Incorrect (partial failure creates unrecoverable state):
+```php
+public function up(): void
+{
+ Schema::create('settings', function (Blueprint $table) { ... });
+ DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']);
+}
+```
+
+Correct (separate migrations):
+```php
+// Migration 1: create_settings_table
+Schema::create('settings', function (Blueprint $table) { ... });
+
+// Migration 2: seed_default_settings
+DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']);
+```
diff --git a/.agents/skills/laravel-best-practices/rules/queue-jobs.md b/.agents/skills/laravel-best-practices/rules/queue-jobs.md
new file mode 100644
index 0000000..c41915e
--- /dev/null
+++ b/.agents/skills/laravel-best-practices/rules/queue-jobs.md
@@ -0,0 +1,144 @@
+# Queue & Job Best Practices
+
+## Set `retry_after` Greater Than `timeout`
+
+If `retry_after` is shorter than the job's `timeout`, the queue worker re-dispatches the job while it's still running, causing duplicate execution.
+
+Incorrect (`retry_after` ≤ `timeout`):
+```php
+class ProcessReport implements ShouldQueue
+{
+ public $timeout = 120;
+}
+
+// config/queue.php — retry_after: 90 ← job retried while still running!
+```
+
+Correct (`retry_after` > `timeout`):
+```php
+class ProcessReport implements ShouldQueue
+{
+ public $timeout = 120;
+}
+
+// config/queue.php — retry_after: 180 ← safely longer than any job timeout
+```
+
+## Use Exponential Backoff
+
+Use progressively longer delays between retries to avoid hammering failing services.
+
+Incorrect (fixed retry interval):
+```php
+class SyncWithStripe implements ShouldQueue
+{
+ public $tries = 3;
+ // Default: retries immediately, overwhelming the API
+}
+```
+
+Correct (exponential backoff):
+```php
+class SyncWithStripe implements ShouldQueue
+{
+ public $tries = 3;
+ public $backoff = [1, 5, 10];
+}
+```
+
+## Implement `ShouldBeUnique`
+
+Prevent duplicate job processing.
+
+```php
+class GenerateInvoice implements ShouldQueue, ShouldBeUnique
+{
+ public function uniqueId(): string
+ {
+ return $this->order->id;
+ }
+
+ public $uniqueFor = 3600;
+}
+```
+
+## Always Implement `failed()`
+
+Handle errors explicitly — don't rely on silent failure.
+
+```php
+public function failed(?Throwable $exception): void
+{
+ $this->podcast->update(['status' => 'failed']);
+ Log::error('Processing failed', ['id' => $this->podcast->id, 'error' => $exception->getMessage()]);
+}
+```
+
+## Rate Limit External API Calls in Jobs
+
+Use `RateLimited` middleware to throttle jobs calling third-party APIs.
+
+```php
+public function middleware(): array
+{
+ return [new RateLimited('external-api')];
+}
+```
+
+## Batch Related Jobs
+
+Use `Bus::batch()` when jobs should succeed or fail together.
+
+```php
+Bus::batch([
+ new ImportCsvChunk($chunk1),
+ new ImportCsvChunk($chunk2),
+])
+->then(fn (Batch $batch) => Notification::send($user, new ImportComplete))
+->catch(fn (Batch $batch, Throwable $e) => Log::error('Batch failed'))
+->dispatch();
+```
+
+## `retryUntil()` Needs `$tries = 0`
+
+When using time-based retry limits, set `$tries = 0` to avoid premature failure.
+
+```php
+public $tries = 0;
+
+public function retryUntil(): \DateTimeInterface
+{
+ return now()->addHours(4);
+}
+```
+
+## Use `ShouldBeUniqueUntilProcessing` for Early Lock Release
+
+`ShouldBeUnique` holds the lock until the job completes. `ShouldBeUniqueUntilProcessing` releases it when processing starts, allowing new instances to queue.
+
+```php
+class UpdateSearchIndex implements ShouldQueue, ShouldBeUniqueUntilProcessing
+{
+ // Lock releases when processing begins, not when it finishes
+}
+```
+
+## Use Horizon for Complex Queue Scenarios
+
+Use Laravel Horizon when you need monitoring, auto-scaling, failure tracking, or multiple queues with different priorities.
+
+```php
+// config/horizon.php
+'environments' => [
+ 'production' => [
+ 'supervisor-1' => [
+ 'connection' => 'redis',
+ 'queue' => ['high', 'default', 'low'],
+ 'balance' => 'auto',
+ 'minProcesses' => 1,
+ 'maxProcesses' => 10,
+ 'tries' => 3,
+ ],
+ ],
+],
+```
diff --git a/.agents/skills/laravel-best-practices/rules/routing.md b/.agents/skills/laravel-best-practices/rules/routing.md
new file mode 100644
index 0000000..b6e3086
--- /dev/null
+++ b/.agents/skills/laravel-best-practices/rules/routing.md
@@ -0,0 +1,99 @@
+# Routing & Controllers Best Practices
+
+## Use Implicit Route Model Binding
+
+Let Laravel resolve models automatically from route parameters.
+
+Incorrect:
+```php
+public function show(int $id)
+{
+ $post = Post::findOrFail($id);
+}
+```
+
+Correct:
+```php
+public function show(Post $post)
+{
+ return view('posts.show', ['post' => $post]);
+}
+```
+
+## Use Scoped Bindings for Nested Resources
+
+Enforce parent-child relationships automatically.
+
+```php
+Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) {
+ // $post is automatically scoped to $user
+})->scopeBindings();
+```
+
+## Use Resource Controllers
+
+Use `Route::resource()` or `apiResource()` for RESTful endpoints.
+
+```php
+Route::resource('posts', PostController::class);
+// In routes/api.php — the /api prefix is applied automatically
+Route::apiResource('posts', Api\PostController::class);
+```
+
+## Keep Controllers Thin
+
+Aim for under 10 lines per method. Extract business logic to action or service classes.
+
+Incorrect:
+```php
+public function store(Request $request)
+{
+ $validated = $request->validate([...]);
+ if ($request->hasFile('image')) {
+ $request->file('image')->move(public_path('images'));
+ }
+ $post = Post::create($validated);
+ $post->tags()->sync($validated['tags']);
+ event(new PostCreated($post));
+ return redirect()->route('posts.show', $post);
+}
+```
+
+Correct:
+```php
+public function store(StorePostRequest $request, CreatePostAction $create)
+{
+ $post = $create->execute($request->validated());
+
+ return redirect()->route('posts.show', $post);
+}
+```
+
+## Type-Hint Form Requests
+
+Type-hinting Form Requests triggers automatic validation and authorization before the method executes.
+
+Incorrect:
+```php
+public function store(Request $request): RedirectResponse
+{
+ $validated = $request->validate([
+ 'title' => ['required', 'max:255'],
+ 'body' => ['required'],
+ ]);
+
+ Post::create($validated);
+
+ return redirect()->route('posts.index');
+}
+```
+
+Correct:
+```php
+public function store(StorePostRequest $request): RedirectResponse
+{
+ Post::create($request->validated());
+
+ return redirect()->route('posts.index');
+}
+```
diff --git a/.agents/skills/laravel-best-practices/rules/scheduling.md b/.agents/skills/laravel-best-practices/rules/scheduling.md
new file mode 100644
index 0000000..a984794
--- /dev/null
+++ b/.agents/skills/laravel-best-practices/rules/scheduling.md
@@ -0,0 +1,39 @@
+# Task Scheduling Best Practices
+
+## Use `withoutOverlapping()` on Variable-Duration Tasks
+
+Without it, a long-running task spawns a second instance on the next tick, causing double-processing or resource exhaustion.
+
+## Use `onOneServer()` on Multi-Server Deployments
+
+Without it, every server runs the same task simultaneously. Requires a shared cache driver (Redis, database, Memcached).
+
+## Use `runInBackground()` for Concurrent Long Tasks
+
+By default, tasks at the same tick run sequentially. A slow first task delays all subsequent ones. `runInBackground()` runs them as separate processes.
+
+## Use `environments()` to Restrict Tasks
+
+Prevent accidental execution of production-only tasks (billing, reporting) on staging.
+
+```php
+Schedule::command('billing:charge')->monthly()->environments(['production']);
+```
+
+## Use `takeUntilTimeout()` for Time-Bounded Processing
+
+A task running every 15 minutes that processes an unbounded cursor can overlap with the next run. Bound execution time.
+
+## Use Schedule Groups for Shared Configuration
+
+Avoid repeating `->onOneServer()->timezone('America/New_York')` across many tasks.
+
+```php
+Schedule::daily()
+ ->onOneServer()
+ ->timezone('America/New_York')
+ ->group(function () {
+ Schedule::command('emails:send --force');
+ Schedule::command('emails:prune');
+ });
+```
diff --git a/.agents/skills/laravel-best-practices/rules/security.md b/.agents/skills/laravel-best-practices/rules/security.md
new file mode 100644
index 0000000..2d7200c
--- /dev/null
+++ b/.agents/skills/laravel-best-practices/rules/security.md
@@ -0,0 +1,198 @@
+# Security Best Practices
+
+## Mass Assignment Protection
+
+Every model must define `$fillable` (whitelist) or `$guarded` (blacklist).
+
+Incorrect:
+```php
+class User extends Model
+{
+ protected $guarded = []; // All fields are mass assignable
+}
+```
+
+Correct:
+```php
+class User extends Model
+{
+ protected $fillable = [
+ 'name',
+ 'email',
+ 'password',
+ ];
+}
+```
+
+Never use `$guarded = []` on models that accept user input.
+
+## Authorize Every Action
+
+Use policies or gates in controllers. Never skip authorization.
+
+Incorrect:
+```php
+public function update(UpdatePostRequest $request, Post $post)
+{
+ $post->update($request->validated());
+}
+```
+
+Correct:
+```php
+public function update(UpdatePostRequest $request, Post $post)
+{
+ Gate::authorize('update', $post);
+
+ $post->update($request->validated());
+}
+```
+
+Or via Form Request:
+
+```php
+public function authorize(): bool
+{
+ return $this->user()->can('update', $this->route('post'));
+}
+```
+
+## Prevent SQL Injection
+
+Always use parameter binding. Never interpolate user input into queries.
+
+Incorrect:
+```php
+DB::select("SELECT * FROM users WHERE name = '{$request->name}'");
+```
+
+Correct:
+```php
+User::where('name', $request->name)->get();
+
+// Raw expressions with bindings
+User::whereRaw('LOWER(name) = ?', [strtolower($request->name)])->get();
+```
+
+## Escape Output to Prevent XSS
+
+Use `{{ }}` for HTML escaping. Only use `{!! !!}` for trusted, pre-sanitized content.
+
+Incorrect:
+```blade
+{!! $user->bio !!}
+```
+
+Correct:
+```blade
+{{ $user->bio }}
+```
+
+## CSRF Protection
+
+Include `@csrf` in all POST/PUT/DELETE Blade forms. In Inertia apps, the `@csrf` directive is automatically applied.
+
+Incorrect:
+```blade
+
+```
+
+Correct:
+```blade
+
+```
+
+## Rate Limit Auth and API Routes
+
+Apply `throttle` middleware to authentication and API routes.
+
+```php
+RateLimiter::for('login', function (Request $request) {
+ return Limit::perMinute(5)->by($request->ip());
+});
+
+Route::post('/login', LoginController::class)->middleware('throttle:login');
+```
+
+## Validate File Uploads
+
+Validate extension, MIME type, and size. The `mimes` rule checks extensions; use `mimetypes` for actual MIME type validation. Never trust client-provided filenames.
+
+```php
+public function rules(): array
+{
+ return [
+ 'avatar' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'],
+ ];
+}
+```
+
+Store with generated filenames:
+
+```php
+$path = $request->file('avatar')->store('avatars', 'public');
+```
+
+## Keep Secrets Out of Code
+
+Never commit `.env`. Access secrets via `config()` only.
+
+Incorrect:
+```php
+$key = env('API_KEY');
+```
+
+Correct:
+```php
+// config/services.php
+'api_key' => env('API_KEY'),
+
+// In application code
+$key = config('services.api_key');
+```
+
+## Audit Dependencies
+
+Run `composer audit` periodically to check for known vulnerabilities in dependencies. Automate this in CI to catch issues before deployment.
+
+```bash
+composer audit
+```
+
+## Encrypt Sensitive Database Fields
+
+Use `encrypted` cast for API keys/tokens and mark the attribute as `hidden`.
+
+Incorrect:
+```php
+class Integration extends Model
+{
+ protected function casts(): array
+ {
+ return [
+ 'api_key' => 'string',
+ ];
+ }
+}
+```
+
+Correct:
+```php
+class Integration extends Model
+{
+ protected $hidden = ['api_key', 'api_secret'];
+
+ protected function casts(): array
+ {
+ return [
+ 'api_key' => 'encrypted',
+ 'api_secret' => 'encrypted',
+ ];
+ }
+}
+```
diff --git a/.agents/skills/laravel-best-practices/rules/style.md b/.agents/skills/laravel-best-practices/rules/style.md
new file mode 100644
index 0000000..64d1730
--- /dev/null
+++ b/.agents/skills/laravel-best-practices/rules/style.md
@@ -0,0 +1,125 @@
+# Conventions & Style
+
+## Follow Laravel Naming Conventions
+
+| What | Convention | Good | Bad |
+|------|-----------|------|-----|
+| Controller | singular | `ArticleController` | `ArticlesController` |
+| Model | singular | `User` | `Users` |
+| Table | plural, snake_case | `article_comments` | `articleComments` |
+| Pivot table | singular alphabetical | `article_user` | `user_article` |
+| Column | snake_case, no model name | `meta_title` | `article_meta_title` |
+| Foreign key | singular model + `_id` | `article_id` | `articles_id` |
+| Route | plural | `articles/1` | `article/1` |
+| Route name | snake_case with dots | `users.show_active` | `users.show-active` |
+| Method | camelCase | `getAll` | `get_all` |
+| Variable | camelCase | `$articlesWithAuthor` | `$articles_with_author` |
+| Collection | descriptive, plural | `$activeUsers` | `$data` |
+| Object | descriptive, singular | `$activeUser` | `$users` |
+| View | kebab-case | `show-filtered.blade.php` | `showFiltered.blade.php` |
+| Config | snake_case | `google_calendar.php` | `googleCalendar.php` |
+| Enum | singular | `UserType` | `UserTypes` |
+
+## Prefer Shorter Readable Syntax
+
+| Verbose | Shorter |
+|---------|---------|
+| `Session::get('cart')` | `session('cart')` |
+| `$request->session()->get('cart')` | `session('cart')` |
+| `$request->input('name')` | `$request->name` |
+| `return Redirect::back()` | `return back()` |
+| `Carbon::now()` | `now()` |
+| `App::make('Class')` | `app('Class')` |
+| `->where('column', '=', 1)` | `->where('column', 1)` |
+| `->orderBy('created_at', 'desc')` | `->latest()` |
+| `->orderBy('created_at', 'asc')` | `->oldest()` |
+| `->first()->name` | `->value('name')` |
+
+## Use Laravel String & Array Helpers
+
+Laravel provides `Str`, `Arr`, `Number`, and `Uri` helper classes that are more readable, chainable, and UTF-8 safe than raw PHP functions. Always prefer them.
+
+Strings — use `Str` and fluent `Str::of()` over raw PHP:
+```php
+// Incorrect
+$slug = strtolower(str_replace(' ', '-', $title));
+$short = substr($text, 0, 100) . '...';
+$class = substr(strrchr('App\Models\User', '\'), 1);
+
+// Correct
+$slug = Str::slug($title);
+$short = Str::limit($text, 100);
+$class = class_basename('App\Models\User');
+```
+
+Fluent strings — chain operations for complex transformations:
+```php
+// Incorrect
+$result = strtolower(trim(str_replace('_', '-', $input)));
+
+// Correct
+$result = Str::of($input)->trim()->replace('_', '-')->lower();
+```
+
+Key `Str` methods to prefer: `Str::slug()`, `Str::limit()`, `Str::contains()`, `Str::before()`, `Str::after()`, `Str::between()`, `Str::camel()`, `Str::snake()`, `Str::kebab()`, `Str::headline()`, `Str::squish()`, `Str::mask()`, `Str::uuid()`, `Str::ulid()`, `Str::random()`, `Str::is()`.
+
+Arrays — use `Arr` over raw PHP:
+```php
+// Incorrect
+$name = isset($array['user']['name']) ? $array['user']['name'] : 'default';
+
+// Correct
+$name = Arr::get($array, 'user.name', 'default');
+```
+
+Key `Arr` methods: `Arr::get()`, `Arr::has()`, `Arr::only()`, `Arr::except()`, `Arr::first()`, `Arr::flatten()`, `Arr::pluck()`, `Arr::where()`, `Arr::wrap()`.
+
+Numbers — use `Number` for display formatting:
+```php
+Number::format(1000000); // "1,000,000"
+Number::currency(1500, 'USD'); // "$1,500.00"
+Number::abbreviate(1000000); // "1M"
+Number::fileSize(1024 * 1024); // "1 MB"
+Number::percentage(75.5); // "75.5%"
+```
+
+URIs — use `Uri` for URL manipulation:
+```php
+$uri = Uri::of('https://example.com/search')
+ ->withQuery(['q' => 'laravel', 'page' => 1]);
+```
+
+Use `$request->string('name')` to get a fluent `Stringable` directly from request input for immediate chaining.
+
+Use `search-docs` for the full list of available methods — these helpers are extensive.
+
+## No Inline JS/CSS in Blade
+
+Do not put JS or CSS in Blade templates. Do not put HTML in PHP classes.
+
+Incorrect:
+```blade
+let article = `{{ json_encode($article) }}`;
+```
+
+Correct:
+```blade
+{{ $article->name }}
+```
+
+Pass data to JS via data attributes or use a dedicated PHP-to-JS package.
+
+## No Unnecessary Comments
+
+Code should be readable on its own. Use descriptive method and variable names instead of comments. The only exception is config files, where descriptive comments are expected.
+
+Incorrect:
+```php
+// Check if there are any joins
+if (count((array) $builder->getQuery()->joins) > 0)
+```
+
+Correct:
+```php
+if ($this->hasJoins())
+```
diff --git a/.agents/skills/laravel-best-practices/rules/testing.md b/.agents/skills/laravel-best-practices/rules/testing.md
new file mode 100644
index 0000000..4fbf12f
--- /dev/null
+++ b/.agents/skills/laravel-best-practices/rules/testing.md
@@ -0,0 +1,43 @@
+# Testing Best Practices
+
+## Use `LazilyRefreshDatabase` Over `RefreshDatabase`
+
+`RefreshDatabase` migrates once per process and wraps each test in a rolled-back transaction. `LazilyRefreshDatabase` skips even that first migration if the schema is already up to date.
+
+## Use Model Assertions Over Raw Database Assertions
+
+Incorrect: `$this->assertDatabaseHas('users', ['id' => $user->id]);`
+
+Correct: `$this->assertModelExists($user);`
+
+More expressive, type-safe, and fails with clearer messages.
+
+## Use Factory States and Sequences
+
+Named states make tests self-documenting. Sequences eliminate repetitive setup.
+
+Incorrect: `User::factory()->create(['email_verified_at' => null]);`
+
+Correct: `User::factory()->unverified()->create();`
+
+## Use `Exceptions::fake()` to Assert Exception Reporting
+
+Instead of `withoutExceptionHandling()`, use `Exceptions::fake()` to assert the correct exception was reported while the request completes normally.
+
+## Call `Event::fake()` After Factory Setup
+
+Model factories rely on model events (e.g., `creating` to generate UUIDs). Calling `Event::fake()` before factory calls silences those events, producing broken models.
+
+Incorrect: `Event::fake(); $user = User::factory()->create();`
+
+Correct: `$user = User::factory()->create(); Event::fake();`
+
+## Use `recycle()` to Share Relationship Instances Across Factories
+
+Without `recycle()`, nested factories create separate instances of the same conceptual entity.
+
+```php
+Ticket::factory()
+ ->recycle(Airline::factory()->create())
+ ->create();
+```
diff --git a/.agents/skills/laravel-best-practices/rules/validation.md b/.agents/skills/laravel-best-practices/rules/validation.md
new file mode 100644
index 0000000..5fde106
--- /dev/null
+++ b/.agents/skills/laravel-best-practices/rules/validation.md
@@ -0,0 +1,75 @@
+# Validation & Forms Best Practices
+
+## Use Form Request Classes
+
+Extract validation from controllers into dedicated Form Request classes.
+
+Incorrect:
+```php
+public function store(Request $request)
+{
+ $request->validate([
+ 'title' => 'required|max:255',
+ 'body' => 'required',
+ ]);
+}
+```
+
+Correct:
+```php
+public function store(StorePostRequest $request)
+{
+ Post::create($request->validated());
+}
+```
+
+## Array vs. String Notation for Rules
+
+Array syntax is more readable and composes cleanly with `Rule::` objects. Prefer it in new code, but check existing Form Requests first and match whatever notation the project already uses.
+
+```php
+// Preferred for new code
+'email' => ['required', 'email', Rule::unique('users')],
+
+// Follow existing convention if the project uses string notation
+'email' => 'required|email|unique:users',
+```
+
+## Always Use `validated()`
+
+Get only validated data. Never use `$request->all()` for mass operations.
+
+Incorrect:
+```php
+Post::create($request->all());
+```
+
+Correct:
+```php
+Post::create($request->validated());
+```
+
+## Use `Rule::when()` for Conditional Validation
+
+```php
+'company_name' => [
+ Rule::when($this->account_type === 'business', ['required', 'string', 'max:255']),
+],
+```
+
+## Use the `after()` Method for Custom Validation
+
+Use `after()` instead of `withValidator()` for custom validation logic that depends on multiple fields.
+
+```php
+public function after(): array
+{
+ return [
+ function (Validator $validator) {
+ if ($this->quantity > Product::find($this->product_id)?->stock) {
+ $validator->errors()->add('quantity', 'Not enough stock.');
+ }
+ },
+ ];
+}
+```
diff --git a/.agents/skills/laravel-permission-development/SKILL.md b/.agents/skills/laravel-permission-development/SKILL.md
new file mode 100644
index 0000000..0e3bbd8
--- /dev/null
+++ b/.agents/skills/laravel-permission-development/SKILL.md
@@ -0,0 +1,277 @@
+---
+name: laravel-permission-development
+description: Build and work with Spatie Laravel Permission features, including roles, permissions, middleware, policies, teams, and Blade directives.
+---
+
+# Laravel Permission Development
+
+## When to use this skill
+
+Use this skill when working with authorization, roles, permissions, access control, middleware guards, or Blade permission directives using spatie/laravel-permission.
+
+## Core Concepts
+
+- **Users have Roles, Roles have Permissions, Apps check Permissions** (not Roles).
+- Direct permissions on users are an anti-pattern; assign permissions to roles instead.
+- Use `$user->can('permission-name')` for all authorization checks (supports Super Admin via Gate).
+- The `HasRoles` trait (which includes `HasPermissions`) is added to User models.
+
+## Setup
+
+Add the `HasRoles` trait to your User model:
+
+```php
+use Spatie\Permission\Traits\HasRoles;
+
+class User extends Authenticatable
+{
+ use HasRoles;
+}
+```
+
+## Creating Roles and Permissions
+
+```php
+use Spatie\Permission\Models\Role;
+use Spatie\Permission\Models\Permission;
+
+$role = Role::create(['name' => 'writer']);
+$permission = Permission::create(['name' => 'edit articles']);
+
+// findOrCreate is idempotent (safe for seeders)
+$role = Role::findOrCreate('writer', 'web');
+$permission = Permission::findOrCreate('edit articles', 'web');
+```
+
+## Assigning Roles and Permissions
+
+```php
+// Assign roles to users
+$user->assignRole('writer');
+$user->assignRole('writer', 'admin');
+$user->assignRole(['writer', 'admin']);
+$user->syncRoles(['writer', 'admin']); // replaces all
+$user->removeRole('writer');
+
+// Assign permissions to roles (preferred)
+$role->givePermissionTo('edit articles');
+$role->givePermissionTo(['edit articles', 'delete articles']);
+$role->syncPermissions(['edit articles', 'delete articles']);
+$role->revokePermissionTo('edit articles');
+
+// Reverse assignment
+$permission->assignRole('writer');
+$permission->syncRoles(['writer', 'editor']);
+$permission->removeRole('writer');
+```
+
+## Checking Roles and Permissions
+
+```php
+// Permission checks (preferred - supports Super Admin via Gate)
+$user->can('edit articles');
+$user->canAny(['edit articles', 'delete articles']);
+
+// Direct package methods (bypass Gate, no Super Admin support)
+$user->hasPermissionTo('edit articles');
+$user->hasAnyPermission(['edit articles', 'publish articles']);
+$user->hasAllPermissions(['edit articles', 'publish articles']);
+$user->hasDirectPermission('edit articles');
+
+// Role checks
+$user->hasRole('writer');
+$user->hasAnyRole(['writer', 'editor']);
+$user->hasAllRoles(['writer', 'editor']);
+$user->hasExactRoles(['writer', 'editor']);
+
+// Get assigned roles and permissions
+$user->getRoleNames(); // Collection of role name strings
+$user->getPermissionNames(); // Collection of permission name strings
+$user->getDirectPermissions(); // Direct permissions only
+$user->getPermissionsViaRoles(); // Inherited via roles
+$user->getAllPermissions(); // Both direct and inherited
+```
+
+## Query Scopes
+
+```php
+$users = User::role('writer')->get();
+$users = User::withoutRole('writer')->get();
+$users = User::permission('edit articles')->get();
+$users = User::withoutPermission('edit articles')->get();
+```
+
+## Middleware
+
+Register middleware aliases in `bootstrap/app.php`:
+
+```php
+->withMiddleware(function (Middleware $middleware) {
+ $middleware->alias([
+ 'role' => \Spatie\Permission\Middleware\RoleMiddleware::class,
+ 'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class,
+ 'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class,
+ ]);
+})
+```
+
+Use in routes (pipe `|` for OR logic):
+
+```php
+Route::middleware(['permission:edit articles'])->group(function () { ... });
+Route::middleware(['role:manager|writer'])->group(function () { ... });
+Route::middleware(['role_or_permission:manager|edit articles'])->group(function () { ... });
+
+// With specific guard
+Route::middleware(['role:manager,api'])->group(function () { ... });
+```
+
+For single permissions, Laravel's built-in `can` middleware also works:
+
+```php
+Route::middleware(['can:edit articles'])->group(function () { ... });
+```
+
+## Blade Directives
+
+Prefer `@can` (permission-based) over `@role` (role-based):
+
+```blade
+@can('edit articles')
+ {{-- User can edit articles (supports Super Admin) --}}
+@endcan
+
+@canany(['edit articles', 'delete articles'])
+ {{-- User can do at least one --}}
+@endcanany
+
+@role('admin')
+ {{-- Only use for super-admin type checks --}}
+@endrole
+
+@hasanyrole('writer|admin')
+ {{-- Has writer or admin --}}
+@endhasanyrole
+```
+
+## Super Admin
+
+Use `Gate::before` in `AppServiceProvider::boot()`:
+
+```php
+use Illuminate\Support\Facades\Gate;
+
+public function boot(): void
+{
+ Gate::before(function ($user, $ability) {
+ return $user->hasRole('Super Admin') ? true : null;
+ });
+}
+```
+
+This makes `$user->can()` and `@can` always return true for Super Admins. Must return `null` (not `false`) to allow normal checks for other users.
+
+## Policies
+
+Use `$user->can()` inside policy methods to check permissions:
+
+```php
+class PostPolicy
+{
+ public function update(User $user, Post $post): bool
+ {
+ if ($user->can('edit all posts')) {
+ return true;
+ }
+
+ return $user->can('edit own posts') && $user->id === $post->user_id;
+ }
+}
+```
+
+## Enums
+
+```php
+enum RolesEnum: string
+{
+ case WRITER = 'writer';
+ case EDITOR = 'editor';
+}
+
+enum PermissionsEnum: string
+{
+ case EDIT_POSTS = 'edit posts';
+ case DELETE_POSTS = 'delete posts';
+}
+
+// Creation requires ->value
+Permission::findOrCreate(PermissionsEnum::EDIT_POSTS->value, 'web');
+
+// Most methods accept enums directly
+$user->assignRole(RolesEnum::WRITER);
+$user->hasRole(RolesEnum::WRITER);
+$role->givePermissionTo(PermissionsEnum::EDIT_POSTS);
+$user->hasPermissionTo(PermissionsEnum::EDIT_POSTS);
+```
+
+## Seeding
+
+Always flush the permission cache when seeding:
+
+```php
+class RolesAndPermissionsSeeder extends Seeder
+{
+ public function run(): void
+ {
+ // Reset cache
+ app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
+
+ // Create permissions
+ Permission::findOrCreate('edit articles', 'web');
+ Permission::findOrCreate('delete articles', 'web');
+
+ // Create roles and assign permissions
+ Role::findOrCreate('writer', 'web')
+ ->givePermissionTo(['edit articles']);
+
+ Role::findOrCreate('admin', 'web')
+ ->givePermissionTo(Permission::all());
+ }
+}
+```
+
+## Teams (Multi-Tenancy)
+
+Enable in `config/permission.php` before running migrations:
+
+```php
+'teams' => true,
+```
+
+Set the active team in middleware:
+
+```php
+setPermissionsTeamId($teamId);
+```
+
+When switching teams, unset cached relations:
+
+```php
+$user->unsetRelation('roles')->unsetRelation('permissions');
+```
+
+## Events
+
+Enable in `config/permission.php`:
+
+```php
+'events_enabled' => true,
+```
+
+Available events: `RoleAttachedEvent`, `RoleDetachedEvent`, `PermissionAttachedEvent`, `PermissionDetachedEvent` in the `Spatie\Permission\Events` namespace.
+
+## Performance
+
+- Permissions are cached automatically. The cache is flushed when roles/permissions change via package methods.
+- After direct DB operations, flush manually: `app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions()`
+- For bulk seeding, use `Permission::insert()` for speed, but flush the cache afterward.
diff --git a/.agents/skills/pest-testing/SKILL.md b/.agents/skills/pest-testing/SKILL.md
new file mode 100644
index 0000000..ab27161
--- /dev/null
+++ b/.agents/skills/pest-testing/SKILL.md
@@ -0,0 +1,166 @@
+---
+name: pest-testing
+description: "Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code."
+license: MIT
+metadata:
+ author: laravel
+---
+
+# Pest Testing 4
+
+## Documentation
+
+Use `search-docs` for detailed Pest 4 patterns and documentation.
+
+## Basic Usage
+
+### Creating Tests
+
+All tests must be written using Pest. Use `php artisan make:test --pest {name}`.
+
+The `{name}` argument should include only the path and test name, but should not include the test suite.
+- Incorrect: `php artisan make:test --pest Feature/SomeFeatureTest` will generate `tests/Feature/Feature/SomeFeatureTest.php`
+- Correct: `php artisan make:test --pest SomeControllerTest` will generate `tests/Feature/SomeControllerTest.php`
+- Incorrect: `php artisan make:test --pest --unit Unit/SomeServiceTest` will generate `tests/Unit/Unit/SomeServiceTest.php`
+- Correct: `php artisan make:test --pest --unit SomeServiceTest` will generate `tests/Unit/SomeServiceTest.php`
+
+### Test Organization
+
+- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories.
+- Browser tests: `tests/Browser/` directory.
+- Do NOT remove tests without approval - these are core application code.
+
+### Basic Test Structure
+
+Pest supports both `test()` and `it()` functions. Before writing new tests, check existing test files in the same directory to match the project's convention. Use `test()` if existing tests use `test()`, or `it()` if they use `it()`.
+
+
+```php
+it('is true', function () {
+ expect(true)->toBeTrue();
+});
+```
+
+### Running Tests
+
+- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`.
+- Run all tests: `php artisan test --compact`.
+- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`.
+
+## Assertions
+
+Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
+
+
+```php
+it('returns all', function () {
+ $this->postJson('/api/docs', [])->assertSuccessful();
+});
+```
+
+| Use | Instead of |
+|-----|------------|
+| `assertSuccessful()` | `assertStatus(200)` |
+| `assertNotFound()` | `assertStatus(404)` |
+| `assertForbidden()` | `assertStatus(403)` |
+
+## Mocking
+
+Import mock function before use: `use function Pest\Laravel\mock;`
+
+## Datasets
+
+Use datasets for repetitive tests (validation rules, etc.):
+
+
+```php
+it('has emails', function (string $email) {
+ expect($email)->not->toBeEmpty();
+})->with([
+ 'james' => 'james@laravel.com',
+ 'taylor' => 'taylor@laravel.com',
+]);
+```
+
+## Pest 4 Features
+
+| Feature | Purpose |
+|---------|---------|
+| Browser Testing | Full integration tests in real browsers |
+| Smoke Testing | Validate multiple pages quickly |
+| Visual Regression | Compare screenshots for visual changes |
+| Test Sharding | Parallel CI runs |
+| Architecture Testing | Enforce code conventions |
+
+### Browser Test Example
+
+Browser tests run in real browsers for full integration testing:
+
+- Browser tests live in `tests/Browser/`.
+- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories.
+- Use `RefreshDatabase` for clean state per test.
+- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures.
+- Test on multiple browsers (Chrome, Firefox, Safari) if requested.
+- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested.
+- Switch color schemes (light/dark mode) when appropriate.
+- Take screenshots or pause tests for debugging.
+
+
+```php
+it('may reset the password', function () {
+ Notification::fake();
+
+ $this->actingAs(User::factory()->create());
+
+ $page = visit('/sign-in');
+
+ $page->assertSee('Sign In')
+ ->assertNoJavaScriptErrors()
+ ->click('Forgot Password?')
+ ->fill('email', 'nuno@laravel.com')
+ ->click('Send Reset Link')
+ ->assertSee('We have emailed your password reset link!');
+
+ Notification::assertSent(ResetPassword::class);
+});
+```
+
+### Smoke Testing
+
+Quickly validate multiple pages have no JavaScript errors:
+
+
+```php
+$pages = visit(['/', '/about', '/contact']);
+
+$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs();
+```
+
+### Visual Regression Testing
+
+Capture and compare screenshots to detect visual changes.
+
+### Test Sharding
+
+Split tests across parallel processes for faster CI runs.
+
+### Architecture Testing
+
+Pest 4 includes architecture testing (from Pest 3):
+
+
+```php
+arch('controllers')
+ ->expect('App\Http\Controllers')
+ ->toExtendNothing()
+ ->toHaveSuffix('Controller');
+```
+
+## Common Pitfalls
+
+- Not importing `use function Pest\Laravel\mock;` before using mock
+- Using `assertStatus(200)` instead of `assertSuccessful()`
+- Forgetting datasets for repetitive validation tests
+- Deleting tests without approval
+- Forgetting `assertNoJavaScriptErrors()` in browser tests
+- Prefixing `Feature/` or `Unit/` in `{name}` when using `make:test`
diff --git a/.ai/mcp/mcp.json b/.ai/mcp/mcp.json
new file mode 100644
index 0000000..40b4d24
--- /dev/null
+++ b/.ai/mcp/mcp.json
@@ -0,0 +1,11 @@
+{
+ "mcpServers": {
+ "laravel-boost": {
+ "command": "php",
+ "args": [
+ "artisan",
+ "boost:mcp"
+ ]
+ }
+ }
+}
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..a186cd2
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,18 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = lf
+indent_size = 4
+indent_style = space
+insert_final_newline = true
+trim_trailing_whitespace = true
+
+[*.md]
+trim_trailing_whitespace = false
+
+[*.{yml,yaml}]
+indent_size = 2
+
+[compose.yaml]
+indent_size = 4
diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..c0660ea
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,65 @@
+APP_NAME=Laravel
+APP_ENV=local
+APP_KEY=
+APP_DEBUG=true
+APP_URL=http://localhost
+
+APP_LOCALE=en
+APP_FALLBACK_LOCALE=en
+APP_FAKER_LOCALE=en_US
+
+APP_MAINTENANCE_DRIVER=file
+# APP_MAINTENANCE_STORE=database
+
+# PHP_CLI_SERVER_WORKERS=4
+
+BCRYPT_ROUNDS=12
+
+LOG_CHANNEL=stack
+LOG_STACK=single
+LOG_DEPRECATIONS_CHANNEL=null
+LOG_LEVEL=debug
+
+DB_CONNECTION=sqlite
+# DB_HOST=127.0.0.1
+# DB_PORT=3306
+# DB_DATABASE=laravel
+# DB_USERNAME=root
+# DB_PASSWORD=
+
+SESSION_DRIVER=database
+SESSION_LIFETIME=120
+SESSION_ENCRYPT=false
+SESSION_PATH=/
+SESSION_DOMAIN=null
+
+BROADCAST_CONNECTION=log
+FILESYSTEM_DISK=local
+QUEUE_CONNECTION=database
+
+CACHE_STORE=database
+# CACHE_PREFIX=
+
+MEMCACHED_HOST=127.0.0.1
+
+REDIS_CLIENT=phpredis
+REDIS_HOST=127.0.0.1
+REDIS_PASSWORD=null
+REDIS_PORT=6379
+
+MAIL_MAILER=log
+MAIL_SCHEME=null
+MAIL_HOST=127.0.0.1
+MAIL_PORT=2525
+MAIL_USERNAME=null
+MAIL_PASSWORD=null
+MAIL_FROM_ADDRESS="hello@example.com"
+MAIL_FROM_NAME="${APP_NAME}"
+
+AWS_ACCESS_KEY_ID=
+AWS_SECRET_ACCESS_KEY=
+AWS_DEFAULT_REGION=us-east-1
+AWS_BUCKET=
+AWS_USE_PATH_STYLE_ENDPOINT=false
+
+VITE_APP_NAME="${APP_NAME}"
diff --git a/.gemini/settings.json b/.gemini/settings.json
new file mode 100644
index 0000000..8c6715a
--- /dev/null
+++ b/.gemini/settings.json
@@ -0,0 +1,11 @@
+{
+ "mcpServers": {
+ "laravel-boost": {
+ "command": "php",
+ "args": [
+ "artisan",
+ "boost:mcp"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/.gitattributes b/.gitattributes
new file mode 100644
index 0000000..fcb21d3
--- /dev/null
+++ b/.gitattributes
@@ -0,0 +1,11 @@
+* text=auto eol=lf
+
+*.blade.php diff=html
+*.css diff=css
+*.html diff=html
+*.md diff=markdown
+*.php diff=php
+
+/.github export-ignore
+CHANGELOG.md export-ignore
+.styleci.yml export-ignore
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..0eeb578
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,25 @@
+*.log
+.DS_Store
+.env
+.env.backup
+.env.production
+.phpactor.json
+.phpunit.result.cache
+/.fleet
+/.idea
+/.nova
+/.phpunit.cache
+/.vscode
+/.zed
+/auth.json
+/node_modules
+/public/build
+/public/hot
+/public/storage
+/storage/*.key
+/storage/pail
+/vendor
+_ide_helper.php
+Homestead.json
+Homestead.yaml
+Thumbs.db
diff --git a/.junie/skills/ai-sdk-development/SKILL.md b/.junie/skills/ai-sdk-development/SKILL.md
new file mode 100644
index 0000000..717d626
--- /dev/null
+++ b/.junie/skills/ai-sdk-development/SKILL.md
@@ -0,0 +1,436 @@
+---
+name: ai-sdk-development
+description: TRIGGER when working with ai-sdk which is Laravel official first-party AI SDK. Activate when building, editing AI agents, chatbots, text generation, image generation, audio/TTS, transcription/STT, embeddings, RAG, vector stores, reranking, structured output, streaming, conversation memory, tools, queueing, broadcasting, and provider failover across OpenAI, Anthropic, Gemini, Azure, Groq, xAI, DeepSeek, Mistral, Ollama, ElevenLabs, Cohere, Jina, and VoyageAI. Invoke when the user references ai-sdk, the `Laravel\Ai\` namespace, or this project's AI features — not for other AI packages used directly.
+license: MIT
+metadata:
+ author: laravel
+---
+
+# Developing with the Laravel AI SDK
+
+The Laravel AI SDK (`laravel/ai`) is the official AI package for Laravel, providing a unified API for agents, images, audio, transcription, embeddings, reranking, vector stores, and file management across multiple AI providers.
+
+## Searching the Documentation
+
+This package is new. Always search the documentation before implementing any feature. Never guess at APIs — the documentation is the single source of truth.
+
+- Use broad, simple queries that match the documentation section headings below.
+- Do not add package names to queries — package information is shared automatically. Use `test agent fake`, not `laravel ai test agent fake`.
+- Run multiple queries at once — the most relevant results are returned first.
+
+### Documentation Sections
+
+Use these section headings as query terms for accurate results:
+
+- Introduction, Installation, Configuration, Provider Support
+- Agents: Prompting, Conversation Context, Structured Output, Attachments, Streaming, Broadcasting, Queueing, Tools, Provider Tools, Middleware, Anonymous Agents, Agent Configuration
+- Images
+- Audio (TTS)
+- Transcription (STT)
+- Embeddings: Querying Embeddings, Caching Embeddings
+- Reranking
+- Files
+- Vector Stores: Adding Files to Stores
+- Failover
+- Testing: Agents, Images, Audio, Transcriptions, Embeddings, Reranking, Files, Vector Stores
+- Events
+
+## Decision Workflow
+
+Determine the right entry point before writing code:
+
+Text generation or chat? → Agent class with `Promptable` trait
+Chat with conversation history? → Agent + `Conversational` interface (manual) or `RemembersConversations` trait (automatic)
+Structured JSON output? → Agent + `HasStructuredOutput` interface
+Image generation? → `Image::of()->generate()`
+Audio synthesis? → `Audio::of()->generate()`
+Transcription? → `Transcription::fromPath()->generate()`
+Embeddings? → `Embeddings::for()->generate()`
+Reranking? → `Reranking::of()->rerank()`
+File storage? → `Document::fromPath()->put()`
+Vector stores? → `Stores::create()`
+
+## Basic Usage Examples
+
+### Agents
+
+```php
+use Laravel\Ai\Contracts\Agent;
+use Laravel\Ai\Enums\Lab;
+use Laravel\Ai\Promptable;
+
+class SalesCoach implements Agent
+{
+ use Promptable;
+
+ public function instructions(): string
+ {
+ return 'You are a sales coach.';
+ }
+}
+
+// Prompting
+$response = (new SalesCoach)->prompt('Analyze this transcript...');
+echo $response->text;
+
+// Container resolution with dependency injection
+$agent = SalesCoach::make(user: $user);
+
+// Override provider, model, or timeout per-prompt
+$response = (new SalesCoach)->prompt(
+ 'Analyze this transcript...',
+ provider: Lab::Anthropic,
+ model: 'claude-haiku-4-5-20251001',
+ timeout: 120,
+);
+
+// Streaming (returns SSE response from a route)
+return (new SalesCoach)->stream('Analyze this transcript...');
+
+// Queueing
+(new SalesCoach)->queue('Analyze this transcript...')
+ ->then(fn ($response) => /* ... */);
+
+// Anonymous agents
+use function Laravel\Ai\{agent};
+
+$response = agent(instructions: 'You are a helpful assistant.')->prompt('Hello');
+```
+
+### Conversation Context
+
+Manual conversation history via the `Conversational` interface:
+
+```php
+use Laravel\Ai\Contracts\Agent;
+use Laravel\Ai\Contracts\Conversational;
+use Laravel\Ai\Messages\Message;
+use Laravel\Ai\Promptable;
+
+class SalesCoach implements Agent, Conversational
+{
+ use Promptable;
+
+ public function __construct(public User $user) {}
+
+ public function instructions(): string { return 'You are a sales coach.'; }
+
+ public function messages(): iterable
+ {
+ return History::where('user_id', $this->user->id)
+ ->latest()->limit(50)->get()->reverse()
+ ->map(fn ($m) => new Message($m->role, $m->content))
+ ->all();
+ }
+}
+```
+
+Automatic conversation persistence via the `RemembersConversations` trait:
+
+```php
+use Laravel\Ai\Concerns\RemembersConversations;
+use Laravel\Ai\Contracts\Agent;
+use Laravel\Ai\Contracts\Conversational;
+use Laravel\Ai\Promptable;
+
+class SalesCoach implements Agent, Conversational
+{
+ use Promptable, RemembersConversations;
+
+ public function instructions(): string { return 'You are a sales coach.'; }
+}
+
+// Start a new conversation
+$response = (new SalesCoach)->forUser($user)->prompt('Hello!');
+$conversationId = $response->conversationId;
+
+// Continue an existing conversation
+$response = (new SalesCoach)->continue($conversationId, as: $user)->prompt('Tell me more.');
+```
+
+### Structured Output
+
+```php
+use Illuminate\Contracts\JsonSchema\JsonSchema;
+use Laravel\Ai\Contracts\Agent;
+use Laravel\Ai\Contracts\HasStructuredOutput;
+use Laravel\Ai\Promptable;
+
+class Reviewer implements Agent, HasStructuredOutput
+{
+ use Promptable;
+
+ public function instructions(): string { return 'Review and score content.'; }
+
+ public function schema(JsonSchema $schema): array
+ {
+ return [
+ 'feedback' => $schema->string()->required(),
+ 'score' => $schema->integer()->min(1)->max(10)->required(),
+ ];
+ }
+}
+
+$response = (new Reviewer)->prompt('Review this...');
+echo $response['score']; // Access like an array
+```
+
+### Images
+
+```php
+use Laravel\Ai\Image;
+
+$image = Image::of('A sunset over mountains')
+ ->landscape()
+ ->quality('high')
+ ->generate();
+
+$path = $image->store(); // Store to default disk
+```
+
+### Audio
+
+```php
+use Laravel\Ai\Audio;
+
+$audio = Audio::of('Hello from Laravel.')
+ ->female()
+ ->instructions('Speak warmly')
+ ->generate();
+
+$path = $audio->store();
+```
+
+### Transcription
+
+```php
+use Laravel\Ai\Transcription;
+
+$transcript = Transcription::fromStorage('audio.mp3')
+ ->diarize()
+ ->generate();
+
+echo (string) $transcript;
+```
+
+### Embeddings
+
+```php
+use Laravel\Ai\Embeddings;
+use Illuminate\Support\Str;
+
+$response = Embeddings::for(['Text one', 'Text two'])
+ ->dimensions(1536)
+ ->cache()
+ ->generate();
+
+// Single string via Stringable
+$embedding = Str::of('Napa Valley has great wine.')->toEmbeddings();
+```
+
+### Reranking
+
+```php
+use Laravel\Ai\Reranking;
+
+$response = Reranking::of(['Django is Python.', 'Laravel is PHP.', 'React is JS.'])
+ ->limit(5)
+ ->rerank('PHP frameworks');
+
+$response->first()->document; // "Laravel is PHP."
+```
+
+### Files and Vector Stores
+
+```php
+use Laravel\Ai\Files\Document;
+use Laravel\Ai\Stores;
+
+// Store a file with the provider
+$file = Document::fromPath('/path/to/doc.pdf')->put();
+
+// Create a vector store and add files
+$store = Stores::create('Knowledge Base');
+$store->add($file->id);
+$store->add(Document::fromStorage('manual.pdf')); // Store + add in one step
+```
+
+## Agent Configuration
+
+### PHP Attributes
+
+```php
+use Laravel\Ai\Attributes\{Provider, Model, MaxSteps, MaxTokens, Temperature, Timeout};
+use Laravel\Ai\Enums\Lab;
+
+#[Provider(Lab::Anthropic)]
+#[Model('claude-haiku-4-5-20251001')]
+#[MaxSteps(10)]
+#[MaxTokens(4096)]
+#[Temperature(0.7)]
+#[Timeout(120)]
+class MyAgent implements Agent
+{
+ use Promptable;
+ // ...
+}
+```
+
+The `#[UseCheapestModel]` and `#[UseSmartestModel]` attributes are also available for automatic model selection.
+
+### Tools
+
+Implement the `HasTools` interface and scaffold tools with `php artisan make:tool`:
+
+```php
+use Laravel\Ai\Contracts\HasTools;
+
+class MyAgent implements Agent, HasTools
+{
+ use Promptable;
+
+ public function tools(): iterable
+ {
+ return [new MyCustomTool];
+ }
+}
+```
+
+### Provider Tools
+
+```php
+use Laravel\Ai\Providers\Tools\{WebSearch, WebFetch, FileSearch};
+
+public function tools(): iterable
+{
+ return [
+ (new WebSearch)->max(5)->allow(['laravel.com']),
+ new WebFetch,
+ new FileSearch(stores: ['store_id']),
+ ];
+}
+```
+
+### Conversation Memory
+
+```php
+use Laravel\Ai\Concerns\RemembersConversations;
+use Laravel\Ai\Contracts\Conversational;
+
+class ChatBot implements Agent, Conversational
+{
+ use Promptable, RemembersConversations;
+ // ...
+}
+
+$response = (new ChatBot)->forUser($user)->prompt('Hello!');
+$response = (new ChatBot)->continue($conversationId, as: $user)->prompt('More...');
+```
+
+### Failover
+
+```php
+$response = (new MyAgent)->prompt('Hello', provider: [Lab::OpenAI, Lab::Anthropic]);
+```
+
+## Testing and Faking
+
+Each capability supports `fake()` with assertions:
+
+```php
+use App\Ai\Agents\SalesCoach;
+use Laravel\Ai\{Image, Audio, Transcription, Embeddings, Reranking, Files, Stores};
+
+// Agents
+SalesCoach::fake(['Response 1', 'Response 2']);
+SalesCoach::assertPrompted('query');
+SalesCoach::assertNotPrompted('query');
+SalesCoach::assertNeverPrompted();
+SalesCoach::fake()->preventStrayPrompts();
+
+// Images
+Image::fake();
+Image::assertGenerated(fn ($prompt) => $prompt->contains('sunset'));
+Image::assertNothingGenerated();
+
+// Audio
+Audio::fake();
+Audio::assertGenerated(fn ($prompt) => $prompt->contains('Hello'));
+
+// Transcription
+Transcription::fake(['Transcribed text.']);
+Transcription::assertGenerated(fn ($prompt) => $prompt->isDiarized());
+
+// Embeddings
+Embeddings::fake();
+Embeddings::assertGenerated(fn ($prompt) => $prompt->contains('Laravel'));
+
+// Reranking
+Reranking::fake();
+Reranking::assertReranked(fn ($prompt) => $prompt->contains('PHP'));
+
+// Files
+Files::fake();
+Files::assertStored(fn ($file) => $file->mimeType() === 'text/plain');
+
+// Stores
+Stores::fake();
+Stores::assertCreated('Knowledge Base');
+$store = Stores::get('id');
+$store->assertAdded('file_id');
+```
+
+## Key Patterns
+
+- Namespace: `Laravel\Ai\`
+- Package: `composer require laravel/ai`
+- Agent pattern: Implement the `Agent` interface and use the `Promptable` trait
+- Optional interfaces: `HasTools`, `HasMiddleware`, `HasStructuredOutput`, `Conversational`
+- Entry-point classes: `Image`, `Audio`, `Transcription`, `Embeddings`, `Reranking`, `Stores`
+- Provider enum: `Laravel\Ai\Enums\Lab` (prefer over plain strings)
+- Artisan commands: `php artisan make:agent`, `php artisan make:tool`
+- Global helper: `agent()` for anonymous agents
+
+## Common Pitfalls
+
+### Wrong Namespace
+
+The namespace is `Laravel\Ai`, not `Illuminate\Ai` or `Laravel\AI`.
+
+```php
+// Correct
+use Laravel\Ai\Image;
+use Laravel\Ai\Contracts\Agent;
+use Laravel\Ai\Promptable;
+
+// Wrong — these do not exist
+use Illuminate\Ai\Image;
+use Laravel\AI\Agent;
+```
+
+### Unsupported Provider Capability
+
+Calling a capability not supported by a provider throws a `LogicException`. Refer to the provider support table below.
+
+## Provider Support
+
+| Feature | Providers |
+| ---------- | --------------------------------------------------------------- |
+| Text | OpenAI, Anthropic, Gemini, Azure, Groq, xAI, DeepSeek, Mistral, Ollama |
+| Images | OpenAI, Gemini, xAI |
+| TTS | OpenAI, ElevenLabs |
+| STT | OpenAI, ElevenLabs, Mistral |
+| Embeddings | OpenAI, Gemini, Azure, Cohere, Mistral, Jina, VoyageAI |
+| Reranking | Cohere, Jina |
+| Files | OpenAI, Anthropic, Gemini |
+
+Use the `Laravel\Ai\Enums\Lab` enum to reference providers in code instead of plain strings:
+
+```php
+use Laravel\Ai\Enums\Lab;
+
+Lab::Anthropic;
+Lab::OpenAI;
+Lab::Gemini;
+// ...
+```
diff --git a/.junie/skills/laravel-backup/SKILL.md b/.junie/skills/laravel-backup/SKILL.md
new file mode 100644
index 0000000..4a8a3a4
--- /dev/null
+++ b/.junie/skills/laravel-backup/SKILL.md
@@ -0,0 +1,230 @@
+---
+name: laravel-backup
+description: "Configure and extend spatie/laravel-backup for database and file backups, cleanup strategies, health monitoring, and notifications. Activates when working with backup configuration, scheduling backups, creating custom cleanup strategies or health checks, customizing notifications, or when the user mentions backups, backup monitoring, backup cleanup, or spatie/laravel-backup."
+license: MIT
+metadata:
+ author: spatie
+---
+
+# Laravel Backup
+
+## When to Apply
+
+Activate this skill when:
+
+- Configuring backup sources, destinations, or notifications
+- Scheduling backup, cleanup, or monitor commands
+- Creating custom cleanup strategies or health checks
+- Customizing backup notifications
+- Troubleshooting backup failures
+
+## Key Commands
+
+```bash
+
+# Run a backup
+
+php artisan backup:run
+
+# Backup only the database
+
+php artisan backup:run --only-db
+
+# Backup specific database connections
+
+php artisan backup:run --db-name=mysql --db-name=pgsql
+
+# Backup only files (no database)
+
+php artisan backup:run --only-files
+
+# Backup to a specific disk
+
+php artisan backup:run --only-to-disk=s3
+
+# Custom filename
+
+php artisan backup:run --filename=my-backup.zip
+
+# Clean old backups
+
+php artisan backup:clean
+
+# List all backups
+
+php artisan backup:list
+
+# Monitor backup health
+
+php artisan backup:monitor
+
+# Use an alternative config key
+
+php artisan backup:run --config=backup_secondary
+```
+
+## Scheduling
+
+Add to `routes/console.php` or `app/Console/Kernel.php`:
+
+```php
+use Illuminate\Support\Facades\Schedule;
+
+Schedule::command('backup:clean')->daily()->at('01:00');
+Schedule::command('backup:run')->daily()->at('01:30');
+Schedule::command('backup:monitor')->daily()->at('10:00');
+```
+
+## Configuration
+
+Published to `config/backup.php` with sections: `backup` (source files/databases, destination disks, encryption), `notifications` (mail, Slack, Discord), `monitor_backups` (health checks), and `cleanup` (retention strategy).
+
+### Database Dump Customization
+
+Customize dumps per connection in `config/database.php`:
+
+```php
+'mysql' => [
+ // ...
+ 'dump' => [
+ 'exclude_tables' => ['telescope_entries', 'telescope_monitoring'],
+ 'useSingleTransaction' => true,
+ ],
+],
+```
+
+Enable dump compression:
+
+```php
+// config/backup.php
+'database_dump_compressor' => \Spatie\DbDumper\Compressors\GzipCompressor::class,
+```
+
+### Multiple Backup Destinations
+
+```php
+// config/backup.php
+'destination' => [
+ 'disks' => ['local', 's3'],
+],
+```
+
+### Encryption
+
+```php
+// config/backup.php
+'password' => env('BACKUP_ARCHIVE_PASSWORD'),
+'encryption' => 'default', // Uses ZipArchive::EM_AES_256
+```
+
+## Custom Cleanup Strategy
+
+Extend `Spatie\Backup\Tasks\Cleanup\CleanupStrategy` and implement `deleteOldBackups`:
+
+```php
+use Spatie\Backup\BackupDestination\BackupCollection;
+use Spatie\Backup\Tasks\Cleanup\CleanupStrategy;
+
+class MyCleanupStrategy extends CleanupStrategy
+{
+ public function deleteOldBackups(BackupCollection $backups): void
+ {
+ $backups
+ ->reject(fn ($backup) => $backup->date()->gt(now()->subMonth()))
+ ->each(fn ($backup) => $backup->delete());
+ }
+}
+```
+
+Register in `config/backup.php`:
+
+```php
+'cleanup' => [
+ 'strategy' => \App\Backup\MyCleanupStrategy::class,
+],
+```
+
+## Custom Health Check
+
+Extend `Spatie\Backup\Tasks\Monitor\HealthCheck` and implement `checkHealth`:
+
+```php
+use Spatie\Backup\BackupDestination\BackupDestination;
+use Spatie\Backup\Tasks\Monitor\HealthCheck;
+
+class MinimumBackupCount extends HealthCheck
+{
+ protected int $minimumCount;
+
+ public function __construct(int $minimumCount = 3)
+ {
+ $this->minimumCount = $minimumCount;
+ }
+
+ public function checkHealth(BackupDestination $backupDestination): void
+ {
+ $this->failIf(
+ $backupDestination->backups()->count() < $this->minimumCount,
+ "Expected at least {$this->minimumCount} backups."
+ );
+ }
+}
+```
+
+Register in `config/backup.php`:
+
+```php
+'health_checks' => [
+ \App\Backup\MinimumBackupCount::class => 5,
+],
+```
+
+## Custom Notification
+
+Extend `Spatie\Backup\Notifications\BaseNotification`:
+
+```php
+use Illuminate\Notifications\Messages\MailMessage;
+use Spatie\Backup\Notifications\BaseNotification;
+
+class CustomBackupFailedNotification extends BaseNotification
+{
+ public function toMail(): MailMessage
+ {
+ return (new MailMessage)
+ ->error()
+ ->subject("Backup failed for {$this->applicationName()}")
+ ->line($this->event->exception->getMessage());
+ }
+}
+```
+
+Map it in `config/backup.php`:
+
+```php
+'notifications' => [
+ 'notifications' => [
+ \App\Notifications\CustomBackupFailedNotification::class => ['mail'],
+ ],
+],
+```
+
+## Events
+
+All events are in `Spatie\Backup\Events`:
+
+- `BackupWasSuccessful` - Backup completed successfully
+- `BackupHasFailed` - Backup failed (includes exception and optional backup destination)
+- `BackupManifestWasCreated` - File manifest created before zipping
+- `BackupZipWasCreated` - Zip archive created (used for encryption hook)
+- `DumpingDatabase` - Database dump in progress
+- `CleanupWasSuccessful` / `CleanupHasFailed` - Cleanup result
+- `HealthyBackupWasFound` / `UnhealthyBackupWasFound` - Monitor result
+
+## Common Pitfalls
+
+- Forgetting to schedule `backup:clean` alongside `backup:run`, causing disk space to fill up
+- Not excluding `vendor` and `node_modules` from file backups (excluded by default)
+- Setting `only-db` and `only-files` together (mutually exclusive)
+- Missing the `ext-zip` PHP extension
+- Not configuring the notification `mail.to` address after publishing config
diff --git a/.junie/skills/laravel-best-practices/SKILL.md b/.junie/skills/laravel-best-practices/SKILL.md
new file mode 100644
index 0000000..965e267
--- /dev/null
+++ b/.junie/skills/laravel-best-practices/SKILL.md
@@ -0,0 +1,190 @@
+---
+name: laravel-best-practices
+description: "Apply this skill whenever writing, reviewing, or refactoring Laravel PHP code. This includes creating or modifying controllers, models, migrations, form requests, policies, jobs, scheduled commands, service classes, and Eloquent queries. Triggers for N+1 and query performance issues, caching strategies, authorization and security patterns, validation, error handling, queue and job configuration, route definitions, and architectural decisions. Also use for Laravel code reviews and refactoring existing Laravel code to follow best practices. Covers any task involving Laravel backend PHP code patterns."
+license: MIT
+metadata:
+ author: laravel
+---
+
+# Laravel Best Practices
+
+Best practices for Laravel, prioritized by impact. Each rule teaches what to do and why. For exact API syntax, verify with `search-docs`.
+
+## Consistency First
+
+Before applying any rule, check what the application already does. Laravel offers multiple valid approaches — the best choice is the one the codebase already uses, even if another pattern would be theoretically better. Inconsistency is worse than a suboptimal pattern.
+
+Check sibling files, related controllers, models, or tests for established patterns. If one exists, follow it — don't introduce a second way. These rules are defaults for when no pattern exists yet, not overrides.
+
+## Quick Reference
+
+### 1. Database Performance → `rules/db-performance.md`
+
+- Eager load with `with()` to prevent N+1 queries
+- Enable `Model::preventLazyLoading()` in development
+- Select only needed columns, avoid `SELECT *`
+- `chunk()` / `chunkById()` for large datasets
+- Index columns used in `WHERE`, `ORDER BY`, `JOIN`
+- `withCount()` instead of loading relations to count
+- `cursor()` for memory-efficient read-only iteration
+- Never query in Blade templates
+
+### 2. Advanced Query Patterns → `rules/advanced-queries.md`
+
+- `addSelect()` subqueries over eager-loading entire has-many for a single value
+- Dynamic relationships via subquery FK + `belongsTo`
+- Conditional aggregates (`CASE WHEN` in `selectRaw`) over multiple count queries
+- `setRelation()` to prevent circular N+1 queries
+- `whereIn` + `pluck()` over `whereHas` for better index usage
+- Two simple queries can beat one complex query
+- Compound indexes matching `orderBy` column order
+- Correlated subqueries in `orderBy` for has-many sorting (avoid joins)
+
+### 3. Security → `rules/security.md`
+
+- Define `$fillable` or `$guarded` on every model, authorize every action via policies or gates
+- No raw SQL with user input — use Eloquent or query builder
+- `{{ }}` for output escaping, `@csrf` on all POST/PUT/DELETE forms, `throttle` on auth and API routes
+- Validate MIME type, extension, and size for file uploads
+- Never commit `.env`, use `config()` for secrets, `encrypted` cast for sensitive DB fields
+
+### 4. Caching → `rules/caching.md`
+
+- `Cache::remember()` over manual get/put
+- `Cache::flexible()` for stale-while-revalidate on high-traffic data
+- `Cache::memo()` to avoid redundant cache hits within a request
+- Cache tags to invalidate related groups
+- `Cache::add()` for atomic conditional writes
+- `once()` to memoize per-request or per-object lifetime
+- `Cache::lock()` / `lockForUpdate()` for race conditions
+- Failover cache stores in production
+
+### 5. Eloquent Patterns → `rules/eloquent.md`
+
+- Correct relationship types with return type hints
+- Local scopes for reusable query constraints
+- Global scopes sparingly — document their existence
+- Attribute casts in the `casts()` method
+- Cast date columns, use Carbon instances in templates
+- `whereBelongsTo($model)` for cleaner queries
+- Never hardcode table names — use `(new Model)->getTable()` or Eloquent queries
+
+### 6. Validation & Forms → `rules/validation.md`
+
+- Form Request classes, not inline validation
+- Array notation `['required', 'email']` for new code; follow existing convention
+- `$request->validated()` only — never `$request->all()`
+- `Rule::when()` for conditional validation
+- `after()` instead of `withValidator()`
+
+### 7. Configuration → `rules/config.md`
+
+- `env()` only inside config files
+- `App::environment()` or `app()->isProduction()`
+- Config, lang files, and constants over hardcoded text
+
+### 8. Testing Patterns → `rules/testing.md`
+
+- `LazilyRefreshDatabase` over `RefreshDatabase` for speed
+- `assertModelExists()` over raw `assertDatabaseHas()`
+- Factory states and sequences over manual overrides
+- Use fakes (`Event::fake()`, `Exceptions::fake()`, etc.) — but always after factory setup, not before
+- `recycle()` to share relationship instances across factories
+
+### 9. Queue & Job Patterns → `rules/queue-jobs.md`
+
+- `retry_after` must exceed job `timeout`; use exponential backoff `[1, 5, 10]`
+- `ShouldBeUnique` to prevent duplicates; `ShouldBeUniqueUntilProcessing` for early lock release
+- Always implement `failed()`; with `retryUntil()`, set `$tries = 0`
+- `RateLimited` middleware for external API calls; `Bus::batch()` for related jobs
+- Horizon for complex multi-queue scenarios
+
+### 10. Routing & Controllers → `rules/routing.md`
+
+- Implicit route model binding
+- Scoped bindings for nested resources
+- `Route::resource()` or `apiResource()`
+- Methods under 10 lines — extract to actions/services
+- Type-hint Form Requests for auto-validation
+
+### 11. HTTP Client → `rules/http-client.md`
+
+- Explicit `timeout` and `connectTimeout` on every request
+- `retry()` with exponential backoff for external APIs
+- Check response status or use `throw()`
+- `Http::pool()` for concurrent independent requests
+- `Http::fake()` and `preventStrayRequests()` in tests
+
+### 12. Events, Notifications & Mail → `rules/events-notifications.md`, `rules/mail.md`
+
+- Event discovery over manual registration; `event:cache` in production
+- `ShouldDispatchAfterCommit` / `afterCommit()` inside transactions
+- Queue notifications and mailables with `ShouldQueue`
+- On-demand notifications for non-user recipients
+- `HasLocalePreference` on notifiable models
+- `assertQueued()` not `assertSent()` for queued mailables
+- Markdown mailables for transactional emails
+
+### 13. Error Handling → `rules/error-handling.md`
+
+- `report()`/`render()` on exception classes or in `bootstrap/app.php` — follow existing pattern
+- `ShouldntReport` for exceptions that should never log
+- Throttle high-volume exceptions to protect log sinks
+- `dontReportDuplicates()` for multi-catch scenarios
+- Force JSON rendering for API routes
+- Structured context via `context()` on exception classes
+
+### 14. Task Scheduling → `rules/scheduling.md`
+
+- `withoutOverlapping()` on variable-duration tasks
+- `onOneServer()` on multi-server deployments
+- `runInBackground()` for concurrent long tasks
+- `environments()` to restrict to appropriate environments
+- `takeUntilTimeout()` for time-bounded processing
+- Schedule groups for shared configuration
+
+### 15. Architecture → `rules/architecture.md`
+
+- Single-purpose Action classes; dependency injection over `app()` helper
+- Prefer official Laravel packages and follow conventions, don't override defaults
+- Default to `ORDER BY id DESC` or `created_at DESC`; `mb_*` for UTF-8 safety
+- `defer()` for post-response work; `Context` for request-scoped data; `Concurrency::run()` for parallel execution
+
+### 16. Migrations → `rules/migrations.md`
+
+- Generate migrations with `php artisan make:migration`
+- `constrained()` for foreign keys
+- Never modify migrations that have run in production
+- Add indexes in the migration, not as an afterthought
+- Mirror column defaults in model `$attributes`
+- Reversible `down()` by default; forward-fix migrations for intentionally irreversible changes
+- One concern per migration — never mix DDL and DML
+
+### 17. Collections → `rules/collections.md`
+
+- Higher-order messages for simple collection operations
+- `cursor()` vs. `lazy()` — choose based on relationship needs
+- `lazyById()` when updating records while iterating
+- `toQuery()` for bulk operations on collections
+
+### 18. Blade & Views → `rules/blade-views.md`
+
+- `$attributes->merge()` in component templates
+- Blade components over `@include`; `@pushOnce` for per-component scripts
+- View Composers for shared view data
+- `@aware` for deeply nested component props
+
+### 19. Conventions & Style → `rules/style.md`
+
+- Follow Laravel naming conventions for all entities
+- Prefer Laravel helpers (`Str`, `Arr`, `Number`, `Uri`, `Str::of()`, `$request->string()`) over raw PHP functions
+- No JS/CSS in Blade, no HTML in PHP classes
+- Code should be readable; comments only for config files
+
+## How to Apply
+
+Always use a sub-agent to read rule files and explore this skill's content.
+
+1. Identify the file type and select relevant sections (e.g., migration → §16, controller → §1, §3, §5, §6, §10)
+2. Check sibling files for existing patterns — follow those first per Consistency First
+3. Verify API syntax with `search-docs` for the installed Laravel version
diff --git a/.junie/skills/laravel-best-practices/rules/advanced-queries.md b/.junie/skills/laravel-best-practices/rules/advanced-queries.md
new file mode 100644
index 0000000..f12876e
--- /dev/null
+++ b/.junie/skills/laravel-best-practices/rules/advanced-queries.md
@@ -0,0 +1,106 @@
+# Advanced Query Patterns
+
+## Use `addSelect()` Subqueries for Single Values from Has-Many
+
+Instead of eager-loading an entire has-many relationship for a single value (like the latest timestamp), use a correlated subquery via `addSelect()`. This pulls the value directly in the main SQL query — zero extra queries.
+
+```php
+public function scopeWithLastLoginAt($query): void
+{
+ $query->addSelect([
+ 'last_login_at' => Login::select('created_at')
+ ->whereColumn('user_id', 'users.id')
+ ->latest()
+ ->take(1),
+ ])->withCasts(['last_login_at' => 'datetime']);
+}
+```
+
+## Create Dynamic Relationships via Subquery FK
+
+Extend the `addSelect()` pattern to fetch a foreign key via subquery, then define a `belongsTo` relationship on that virtual attribute. This provides a fully-hydrated related model without loading the entire collection.
+
+```php
+public function lastLogin(): BelongsTo
+{
+ return $this->belongsTo(Login::class);
+}
+
+public function scopeWithLastLogin($query): void
+{
+ $query->addSelect([
+ 'last_login_id' => Login::select('id')
+ ->whereColumn('user_id', 'users.id')
+ ->latest()
+ ->take(1),
+ ])->with('lastLogin');
+}
+```
+
+## Use Conditional Aggregates Instead of Multiple Count Queries
+
+Replace N separate `count()` queries with a single query using `CASE WHEN` inside `selectRaw()`. Use `toBase()` to skip model hydration when you only need scalar values.
+
+```php
+$statuses = Feature::toBase()
+ ->selectRaw("count(case when status = 'Requested' then 1 end) as requested")
+ ->selectRaw("count(case when status = 'Planned' then 1 end) as planned")
+ ->selectRaw("count(case when status = 'Completed' then 1 end) as completed")
+ ->first();
+```
+
+## Use `setRelation()` to Prevent Circular N+1
+
+When a parent model is eager-loaded with its children, and the view also needs `$child->parent`, use `setRelation()` to inject the already-loaded parent rather than letting Eloquent fire N additional queries.
+
+```php
+$feature->load('comments.user');
+$feature->comments->each->setRelation('feature', $feature);
+```
+
+## Prefer `whereIn` + Subquery Over `whereHas`
+
+`whereHas()` emits a correlated `EXISTS` subquery that re-executes per row. Using `whereIn()` with a `select('id')` subquery lets the database use an index lookup instead, without loading data into PHP memory.
+
+Incorrect (correlated EXISTS re-executes per row):
+
+```php
+$query->whereHas('company', fn ($q) => $q->where('name', 'like', $term));
+```
+
+Correct (index-friendly subquery, no PHP memory overhead):
+
+```php
+$query->whereIn('company_id', Company::where('name', 'like', $term)->select('id'));
+```
+
+## Sometimes Two Simple Queries Beat One Complex Query
+
+Running a small, targeted secondary query and passing its results via `whereIn` is often faster than a single complex correlated subquery or join. The additional round-trip is worthwhile when the secondary query is highly selective and uses its own index.
+
+## Use Compound Indexes Matching `orderBy` Column Order
+
+When ordering by multiple columns, create a single compound index in the same column order as the `ORDER BY` clause. Individual single-column indexes cannot combine for multi-column sorts — the database will filesort without a compound index.
+
+```php
+// Migration
+$table->index(['last_name', 'first_name']);
+
+// Query — column order must match the index
+User::query()->orderBy('last_name')->orderBy('first_name')->paginate();
+```
+
+## Use Correlated Subqueries for Has-Many Ordering
+
+When sorting by a value from a has-many relationship, avoid joins (they duplicate rows). Use a correlated subquery inside `orderBy()` instead, paired with an `addSelect` scope for eager loading.
+
+```php
+public function scopeOrderByLastLogin($query): void
+{
+ $query->orderByDesc(Login::select('created_at')
+ ->whereColumn('user_id', 'users.id')
+ ->latest()
+ ->take(1)
+ );
+}
+```
diff --git a/.junie/skills/laravel-best-practices/rules/architecture.md b/.junie/skills/laravel-best-practices/rules/architecture.md
new file mode 100644
index 0000000..51c6e65
--- /dev/null
+++ b/.junie/skills/laravel-best-practices/rules/architecture.md
@@ -0,0 +1,202 @@
+# Architecture Best Practices
+
+## Single-Purpose Action Classes
+
+Extract discrete business operations into invokable Action classes.
+
+```php
+class CreateOrderAction
+{
+ public function __construct(private InventoryService $inventory) {}
+
+ public function execute(array $data): Order
+ {
+ $order = Order::create($data);
+ $this->inventory->reserve($order);
+
+ return $order;
+ }
+}
+```
+
+## Use Dependency Injection
+
+Always use constructor injection. Avoid `app()` or `resolve()` inside classes.
+
+Incorrect:
+```php
+class OrderController extends Controller
+{
+ public function store(StoreOrderRequest $request)
+ {
+ $service = app(OrderService::class);
+
+ return $service->create($request->validated());
+ }
+}
+```
+
+Correct:
+```php
+class OrderController extends Controller
+{
+ public function __construct(private OrderService $service) {}
+
+ public function store(StoreOrderRequest $request)
+ {
+ return $this->service->create($request->validated());
+ }
+}
+```
+
+## Code to Interfaces
+
+Depend on contracts at system boundaries (payment gateways, notification channels, external APIs) for testability and swappability.
+
+Incorrect (concrete dependency):
+```php
+class OrderService
+{
+ public function __construct(private StripeGateway $gateway) {}
+}
+```
+
+Correct (interface dependency):
+```php
+interface PaymentGateway
+{
+ public function charge(int $amount, string $customerId): PaymentResult;
+}
+
+class OrderService
+{
+ public function __construct(private PaymentGateway $gateway) {}
+}
+```
+
+Bind in a service provider:
+
+```php
+$this->app->bind(PaymentGateway::class, StripeGateway::class);
+```
+
+## Default Sort by Descending
+
+When no explicit order is specified, sort by `id` or `created_at` descending. Without an explicit `ORDER BY`, row order is undefined.
+
+Incorrect:
+```php
+$posts = Post::paginate();
+```
+
+Correct:
+```php
+$posts = Post::latest()->paginate();
+```
+
+## Use Atomic Locks for Race Conditions
+
+Prevent race conditions with `Cache::lock()` or `lockForUpdate()`.
+
+```php
+Cache::lock('order-processing-'.$order->id, 10)->block(5, function () use ($order) {
+ $order->process();
+});
+
+// Or at query level
+$product = Product::where('id', $id)->lockForUpdate()->first();
+```
+
+## Use `mb_*` String Functions
+
+When no Laravel helper exists, prefer `mb_strlen`, `mb_strtolower`, etc. for UTF-8 safety. Standard PHP string functions count bytes, not characters.
+
+Incorrect:
+```php
+strlen('José'); // 5 (bytes, not characters)
+strtolower('MÜNCHEN'); // 'mÜnchen' — fails on multibyte
+```
+
+Correct:
+```php
+mb_strlen('José'); // 4 (characters)
+mb_strtolower('MÜNCHEN'); // 'münchen'
+
+// Prefer Laravel's Str helpers when available
+Str::length('José'); // 4
+Str::lower('MÜNCHEN'); // 'münchen'
+```
+
+## Use `defer()` for Post-Response Work
+
+For lightweight tasks that don't need to survive a crash (logging, analytics, cleanup), use `defer()` instead of dispatching a job. The callback runs after the HTTP response is sent — no queue overhead.
+
+Incorrect (job overhead for trivial work):
+```php
+dispatch(new LogPageView($page));
+```
+
+Correct (runs after response, same process):
+```php
+defer(fn () => PageView::create(['page_id' => $page->id, 'user_id' => auth()->id()]));
+```
+
+Use jobs when the work must survive process crashes or needs retry logic. Use `defer()` for fire-and-forget work.
+
+## Use `Context` for Request-Scoped Data
+
+The `Context` facade passes data through the entire request lifecycle — middleware, controllers, jobs, logs — without passing arguments manually.
+
+```php
+// In middleware
+Context::add('tenant_id', $request->header('X-Tenant-ID'));
+
+// Anywhere later — controllers, jobs, log context
+$tenantId = Context::get('tenant_id');
+```
+
+Context data automatically propagates to queued jobs and is included in log entries. Use `Context::addHidden()` for sensitive data that should be available in queued jobs but excluded from log context. If data must not leave the current process, do not store it in `Context`.
+
+## Use `Concurrency::run()` for Parallel Execution
+
+Run independent operations in parallel using child processes — no async libraries needed.
+
+```php
+use Illuminate\Support\Facades\Concurrency;
+
+[$users, $orders] = Concurrency::run([
+ fn () => User::count(),
+ fn () => Order::where('status', 'pending')->count(),
+]);
+```
+
+Each closure runs in a separate process with full Laravel access. Use for independent database queries, API calls, or computations that would otherwise run sequentially.
+
+## Convention Over Configuration
+
+Follow Laravel conventions. Don't override defaults unnecessarily.
+
+Incorrect:
+```php
+class Customer extends Model
+{
+ protected $table = 'Customer';
+ protected $primaryKey = 'customer_id';
+
+ public function roles(): BelongsToMany
+ {
+ return $this->belongsToMany(Role::class, 'role_customer', 'customer_id', 'role_id');
+ }
+}
+```
+
+Correct:
+```php
+class Customer extends Model
+{
+ public function roles(): BelongsToMany
+ {
+ return $this->belongsToMany(Role::class);
+ }
+}
+```
diff --git a/.junie/skills/laravel-best-practices/rules/blade-views.md b/.junie/skills/laravel-best-practices/rules/blade-views.md
new file mode 100644
index 0000000..5f0b3a1
--- /dev/null
+++ b/.junie/skills/laravel-best-practices/rules/blade-views.md
@@ -0,0 +1,36 @@
+# Blade & Views Best Practices
+
+## Use `$attributes->merge()` in Component Templates
+
+Hardcoding classes prevents consumers from adding their own. `merge()` combines class attributes cleanly.
+
+```blade
+merge(['class' => 'alert alert-'.$type]) }}>
+ {{ $message }}
+
+```
+
+## Use `@pushOnce` for Per-Component Scripts
+
+If a component renders inside a `@foreach`, `@push` inserts the script N times. `@pushOnce` guarantees it's included exactly once.
+
+## Prefer Blade Components Over `@include`
+
+`@include` shares all parent variables implicitly (hidden coupling). Components have explicit props, attribute bags, and slots.
+
+## Use View Composers for Shared View Data
+
+If every controller rendering a sidebar must pass `$categories`, that's duplicated code. A View Composer centralizes it.
+
+## Use Blade Fragments for Partial Re-Renders (htmx/Turbo)
+
+A single view can return either the full page or just a fragment, keeping routing clean.
+
+```php
+return view('dashboard', compact('users'))
+ ->fragmentIf($request->hasHeader('HX-Request'), 'user-list');
+```
+
+## Use `@aware` for Deeply Nested Component Props
+
+Avoids re-passing parent props through every level of nested components.
diff --git a/.junie/skills/laravel-best-practices/rules/caching.md b/.junie/skills/laravel-best-practices/rules/caching.md
new file mode 100644
index 0000000..67408d6
--- /dev/null
+++ b/.junie/skills/laravel-best-practices/rules/caching.md
@@ -0,0 +1,70 @@
+# Caching Best Practices
+
+## Use `Cache::remember()` Instead of Manual Get/Put
+
+Cleaner cache-aside pattern that removes boilerplate. use `Cache::lock()` for race conditions.
+
+Incorrect:
+```php
+$val = Cache::get('stats');
+if (! $val) {
+ $val = $this->computeStats();
+ Cache::put('stats', $val, 60);
+}
+```
+
+Correct:
+```php
+$val = Cache::remember('stats', 60, fn () => $this->computeStats());
+```
+
+## Use `Cache::flexible()` for Stale-While-Revalidate
+
+On high-traffic keys, one user always gets a slow response when the cache expires. `flexible()` serves slightly stale data while refreshing in the background.
+
+Incorrect: `Cache::remember('users', 300, fn () => User::all());`
+
+Correct: `Cache::flexible('users', [300, 600], fn () => User::all());` — fresh for 5 min, stale-but-served up to 10 min, refreshes via deferred function.
+
+## Use `Cache::memo()` to Avoid Redundant Hits Within a Request
+
+If the same cache key is read multiple times per request (e.g., a service called from multiple places), `memo()` stores the resolved value in memory.
+
+`Cache::memo()->get('settings');` — 5 calls = 1 Redis round-trip instead of 5.
+
+## Use Cache Tags to Invalidate Related Groups
+
+Without tags, invalidating a group of entries requires tracking every key. Tags let you flush atomically. Only works with `redis`, `memcached`, `dynamodb` — not `file` or `database`.
+
+```php
+Cache::tags(['user-1'])->flush();
+```
+
+## Use `Cache::add()` for Atomic Conditional Writes
+
+`add()` only writes if the key does not exist — atomic, no race condition between checking and writing.
+
+Incorrect: `if (! Cache::has('lock')) { Cache::put('lock', true, 10); }`
+
+Correct: `Cache::add('lock', true, 10);`
+
+## Use `once()` for Per-Request Memoization
+
+`once()` memoizes a function's return value for the lifetime of the object (or request for closures). Unlike `Cache::memo()`, it doesn't hit the cache store at all — pure in-memory.
+
+```php
+public function roles(): Collection
+{
+ return once(fn () => $this->loadRoles());
+}
+```
+
+Multiple calls return the cached result without re-executing. Use `once()` for expensive computations called multiple times per request. Use `Cache::memo()` when you also want cross-request caching.
+
+## Configure Failover Cache Stores in Production
+
+If Redis goes down, the app falls back to a secondary store automatically.
+
+```php
+'failover' => ['driver' => 'failover', 'stores' => ['redis', 'database']],
+```
diff --git a/.junie/skills/laravel-best-practices/rules/collections.md b/.junie/skills/laravel-best-practices/rules/collections.md
new file mode 100644
index 0000000..18e8d9e
--- /dev/null
+++ b/.junie/skills/laravel-best-practices/rules/collections.md
@@ -0,0 +1,44 @@
+# Collection Best Practices
+
+## Use Higher-Order Messages for Simple Operations
+
+Incorrect:
+```php
+$users->each(function (User $user) {
+ $user->markAsVip();
+});
+```
+
+Correct: `$users->each->markAsVip();`
+
+Works with `each`, `map`, `sum`, `filter`, `reject`, `contains`, etc.
+
+## Choose `cursor()` vs. `lazy()` Correctly
+
+- `cursor()` — one model in memory, but cannot eager-load relationships (N+1 risk).
+- `lazy()` — chunked pagination returning a flat LazyCollection, supports eager loading.
+
+Incorrect: `User::with('roles')->cursor()` — eager loading silently ignored.
+
+Correct: `User::with('roles')->lazy()` for relationship access; `User::cursor()` for attribute-only work.
+
+## Use `lazyById()` When Updating Records While Iterating
+
+`lazy()` uses offset pagination — updating records during iteration can skip or double-process. `lazyById()` uses `id > last_id`, safe against mutation.
+
+## Use `toQuery()` for Bulk Operations on Collections
+
+Avoids manual `whereIn` construction.
+
+Incorrect: `User::whereIn('id', $users->pluck('id'))->update([...]);`
+
+Correct: `$users->toQuery()->update([...]);`
+
+## Use `#[CollectedBy]` for Custom Collection Classes
+
+More declarative than overriding `newCollection()`.
+
+```php
+#[CollectedBy(UserCollection::class)]
+class User extends Model {}
+```
diff --git a/.junie/skills/laravel-best-practices/rules/config.md b/.junie/skills/laravel-best-practices/rules/config.md
new file mode 100644
index 0000000..9bea727
--- /dev/null
+++ b/.junie/skills/laravel-best-practices/rules/config.md
@@ -0,0 +1,73 @@
+# Configuration Best Practices
+
+## `env()` Only in Config Files
+
+Direct `env()` calls may return `null` when config is cached.
+
+Incorrect:
+```php
+$key = env('API_KEY');
+```
+
+Correct:
+```php
+// config/services.php
+'key' => env('API_KEY'),
+
+// Application code
+$key = config('services.key');
+```
+
+## Use Encrypted Env or External Secrets
+
+Never store production secrets in plain `.env` files in version control.
+
+Incorrect:
+```bash
+
+# .env committed to repo or shared in Slack
+
+STRIPE_SECRET=sk_live_abc123
+AWS_SECRET_ACCESS_KEY=wJalrXUtnFEMI
+```
+
+Correct:
+```bash
+php artisan env:encrypt --env=production --readable
+php artisan env:decrypt --env=production
+```
+
+For cloud deployments, prefer the platform's native secret store (AWS Secrets Manager, Vault, etc.) and inject at runtime.
+
+## Use `App::environment()` for Environment Checks
+
+Incorrect:
+```php
+if (env('APP_ENV') === 'production') {
+```
+
+Correct:
+```php
+if (app()->isProduction()) {
+// or
+if (App::environment('production')) {
+```
+
+## Use Constants and Language Files
+
+Use class constants instead of hardcoded magic strings for model states, types, and statuses.
+
+```php
+// Incorrect
+return $this->type === 'normal';
+
+// Correct
+return $this->type === self::TYPE_NORMAL;
+```
+
+If the application already uses language files for localization, use `__()` for user-facing strings too. Do not introduce language files purely for English-only apps — simple string literals are fine there.
+
+```php
+// Only when lang files already exist in the project
+return back()->with('message', __('app.article_added'));
+```
diff --git a/.junie/skills/laravel-best-practices/rules/db-performance.md b/.junie/skills/laravel-best-practices/rules/db-performance.md
new file mode 100644
index 0000000..c49ba16
--- /dev/null
+++ b/.junie/skills/laravel-best-practices/rules/db-performance.md
@@ -0,0 +1,192 @@
+# Database Performance Best Practices
+
+## Always Eager Load Relationships
+
+Lazy loading causes N+1 query problems — one query per loop iteration. Always use `with()` to load relationships upfront.
+
+Incorrect (N+1 — executes 1 + N queries):
+```php
+$posts = Post::all();
+foreach ($posts as $post) {
+ echo $post->author->name;
+}
+```
+
+Correct (2 queries total):
+```php
+$posts = Post::with('author')->get();
+foreach ($posts as $post) {
+ echo $post->author->name;
+}
+```
+
+Constrain eager loads to select only needed columns (always include the foreign key):
+
+```php
+$users = User::with(['posts' => function ($query) {
+ $query->select('id', 'user_id', 'title')
+ ->where('published', true)
+ ->latest()
+ ->limit(10);
+}])->get();
+```
+
+## Prevent Lazy Loading in Development
+
+Enable this in `AppServiceProvider::boot()` to catch N+1 issues during development.
+
+```php
+public function boot(): void
+{
+ Model::preventLazyLoading(! app()->isProduction());
+}
+```
+
+Throws `LazyLoadingViolationException` when a relationship is accessed without being eager-loaded.
+
+## Select Only Needed Columns
+
+Avoid `SELECT *` — especially when tables have large text or JSON columns.
+
+Incorrect:
+```php
+$posts = Post::with('author')->get();
+```
+
+Correct:
+```php
+$posts = Post::select('id', 'title', 'user_id', 'created_at')
+ ->with(['author:id,name,avatar'])
+ ->get();
+```
+
+When selecting columns on eager-loaded relationships, always include the foreign key column or the relationship won't match.
+
+## Chunk Large Datasets
+
+Never load thousands of records at once. Use chunking for batch processing.
+
+Incorrect:
+```php
+$users = User::all();
+foreach ($users as $user) {
+ $user->notify(new WeeklyDigest);
+}
+```
+
+Correct:
+```php
+User::where('subscribed', true)->chunk(200, function ($users) {
+ foreach ($users as $user) {
+ $user->notify(new WeeklyDigest);
+ }
+});
+```
+
+Use `chunkById()` when modifying records during iteration — standard `chunk()` uses OFFSET which shifts when rows change:
+
+```php
+User::where('active', false)->chunkById(200, function ($users) {
+ $users->each->delete();
+});
+```
+
+## Add Database Indexes
+
+Index columns that appear in `WHERE`, `ORDER BY`, `JOIN`, and `GROUP BY` clauses.
+
+Incorrect:
+```php
+Schema::create('orders', function (Blueprint $table) {
+ $table->id();
+ $table->foreignId('user_id')->constrained();
+ $table->string('status');
+ $table->timestamps();
+});
+```
+
+Correct:
+```php
+Schema::create('orders', function (Blueprint $table) {
+ $table->id();
+ $table->foreignId('user_id')->index()->constrained();
+ $table->string('status')->index();
+ $table->timestamps();
+ $table->index(['status', 'created_at']);
+});
+```
+
+Add composite indexes for common query patterns (e.g., `WHERE status = ? ORDER BY created_at`).
+
+## Use `withCount()` for Counting Relations
+
+Never load entire collections just to count them.
+
+Incorrect:
+```php
+$posts = Post::all();
+foreach ($posts as $post) {
+ echo $post->comments->count();
+}
+```
+
+Correct:
+```php
+$posts = Post::withCount('comments')->get();
+foreach ($posts as $post) {
+ echo $post->comments_count;
+}
+```
+
+Conditional counting:
+
+```php
+$posts = Post::withCount([
+ 'comments',
+ 'comments as approved_comments_count' => function ($query) {
+ $query->where('approved', true);
+ },
+])->get();
+```
+
+## Use `cursor()` for Memory-Efficient Iteration
+
+For read-only iteration over large result sets, `cursor()` loads one record at a time via a PHP generator.
+
+Incorrect:
+```php
+$users = User::where('active', true)->get();
+```
+
+Correct:
+```php
+foreach (User::where('active', true)->cursor() as $user) {
+ ProcessUser::dispatch($user->id);
+}
+```
+
+Use `cursor()` for read-only iteration. Use `chunk()` / `chunkById()` when modifying records.
+
+## No Queries in Blade Templates
+
+Never execute queries in Blade templates. Pass data from controllers.
+
+Incorrect:
+```blade
+@foreach (User::all() as $user)
+ {{ $user->profile->name }}
+@endforeach
+```
+
+Correct:
+```php
+// Controller
+$users = User::with('profile')->get();
+return view('users.index', compact('users'));
+```
+
+```blade
+@foreach ($users as $user)
+ {{ $user->profile->name }}
+@endforeach
+```
diff --git a/.junie/skills/laravel-best-practices/rules/eloquent.md b/.junie/skills/laravel-best-practices/rules/eloquent.md
new file mode 100644
index 0000000..413d5da
--- /dev/null
+++ b/.junie/skills/laravel-best-practices/rules/eloquent.md
@@ -0,0 +1,148 @@
+# Eloquent Best Practices
+
+## Use Correct Relationship Types
+
+Use `hasMany`, `belongsTo`, `morphMany`, etc. with proper return type hints.
+
+```php
+public function comments(): HasMany
+{
+ return $this->hasMany(Comment::class);
+}
+
+public function author(): BelongsTo
+{
+ return $this->belongsTo(User::class, 'user_id');
+}
+```
+
+## Use Local Scopes for Reusable Queries
+
+Extract reusable query constraints into local scopes to avoid duplication.
+
+Incorrect:
+```php
+$active = User::where('verified', true)->whereNotNull('activated_at')->get();
+$articles = Article::whereHas('user', function ($q) {
+ $q->where('verified', true)->whereNotNull('activated_at');
+})->get();
+```
+
+Correct:
+```php
+public function scopeActive(Builder $query): Builder
+{
+ return $query->where('verified', true)->whereNotNull('activated_at');
+}
+
+// Usage
+$active = User::active()->get();
+$articles = Article::whereHas('user', fn ($q) => $q->active())->get();
+```
+
+## Apply Global Scopes Sparingly
+
+Global scopes silently modify every query on the model, making debugging difficult. Prefer local scopes and reserve global scopes for truly universal constraints like soft deletes or multi-tenancy.
+
+Incorrect (global scope for a conditional filter):
+```php
+class PublishedScope implements Scope
+{
+ public function apply(Builder $builder, Model $model): void
+ {
+ $builder->where('published', true);
+ }
+}
+// Now admin panels, reports, and background jobs all silently skip drafts
+```
+
+Correct (local scope you opt into):
+```php
+public function scopePublished(Builder $query): Builder
+{
+ return $query->where('published', true);
+}
+
+Post::published()->paginate(); // Explicit
+Post::paginate(); // Admin sees all
+```
+
+## Define Attribute Casts
+
+Use the `casts()` method (or `$casts` property following project convention) for automatic type conversion.
+
+```php
+protected function casts(): array
+{
+ return [
+ 'is_active' => 'boolean',
+ 'metadata' => 'array',
+ 'total' => 'decimal:2',
+ ];
+}
+```
+
+## Cast Date Columns Properly
+
+Always cast date columns. Use Carbon instances in templates instead of formatting strings manually.
+
+Incorrect:
+```blade
+{{ Carbon::createFromFormat('Y-d-m H-i', $order->ordered_at)->toDateString() }}
+```
+
+Correct:
+```php
+protected function casts(): array
+{
+ return [
+ 'ordered_at' => 'datetime',
+ ];
+}
+```
+
+```blade
+{{ $order->ordered_at->toDateString() }}
+{{ $order->ordered_at->format('m-d') }}
+```
+
+## Use `whereBelongsTo()` for Relationship Queries
+
+Cleaner than manually specifying foreign keys.
+
+Incorrect:
+```php
+Post::where('user_id', $user->id)->get();
+```
+
+Correct:
+```php
+Post::whereBelongsTo($user)->get();
+Post::whereBelongsTo($user, 'author')->get();
+```
+
+## Avoid Hardcoded Table Names in Queries
+
+Never use string literals for table names in raw queries, joins, or subqueries. Hardcoded table names make it impossible to find all places a model is used and break refactoring (e.g., renaming a table requires hunting through every raw string).
+
+Incorrect:
+```php
+DB::table('users')->where('active', true)->get();
+
+$query->join('companies', 'companies.id', '=', 'users.company_id');
+
+DB::select('SELECT * FROM orders WHERE status = ?', ['pending']);
+```
+
+Correct — reference the model's table:
+```php
+DB::table((new User)->getTable())->where('active', true)->get();
+
+// Even better — use Eloquent or the query builder instead of raw SQL
+User::where('active', true)->get();
+Order::where('status', 'pending')->get();
+```
+
+Prefer Eloquent queries and relationships over `DB::table()` whenever possible — they already reference the model's table. When `DB::table()` or raw joins are unavoidable, always use `(new Model)->getTable()` to keep the reference traceable.
+
+**Exception — migrations:** In migrations, hardcoded table names via `DB::table('settings')` are acceptable and preferred. Models change over time but migrations are frozen snapshots — referencing a model that is later renamed or deleted would break the migration.
diff --git a/.junie/skills/laravel-best-practices/rules/error-handling.md b/.junie/skills/laravel-best-practices/rules/error-handling.md
new file mode 100644
index 0000000..4b14866
--- /dev/null
+++ b/.junie/skills/laravel-best-practices/rules/error-handling.md
@@ -0,0 +1,72 @@
+# Error Handling Best Practices
+
+## Exception Reporting and Rendering
+
+There are two valid approaches — choose one and apply it consistently across the project.
+
+**Co-location on the exception class** — keeps behavior alongside the exception definition, easier to find:
+
+```php
+class InvalidOrderException extends Exception
+{
+ public function report(): void { /* custom reporting */ }
+
+ public function render(Request $request): Response
+ {
+ return response()->view('errors.invalid-order', status: 422);
+ }
+}
+```
+
+**Centralized in `bootstrap/app.php`** — all exception handling in one place, easier to see the full picture:
+
+```php
+->withExceptions(function (Exceptions $exceptions) {
+ $exceptions->report(function (InvalidOrderException $e) { /* ... */ });
+ $exceptions->render(function (InvalidOrderException $e, Request $request) {
+ return response()->view('errors.invalid-order', status: 422);
+ });
+})
+```
+
+Check the existing codebase and follow whichever pattern is already established.
+
+## Use `ShouldntReport` for Exceptions That Should Never Log
+
+More discoverable than listing classes in `dontReport()`.
+
+```php
+class PodcastProcessingException extends Exception implements ShouldntReport {}
+```
+
+## Throttle High-Volume Exceptions
+
+A single failing integration can flood error tracking. Use `throttle()` to rate-limit per exception type.
+
+## Enable `dontReportDuplicates()`
+
+Prevents the same exception instance from being logged multiple times when `report($e)` is called in multiple catch blocks.
+
+## Force JSON Error Rendering for API Routes
+
+Laravel auto-detects `Accept: application/json` but API clients may not set it. Explicitly declare JSON rendering for API routes.
+
+```php
+$exceptions->shouldRenderJsonWhen(function (Request $request, Throwable $e) {
+ return $request->is('api/*') || $request->expectsJson();
+});
+```
+
+## Add Context to Exception Classes
+
+Attach structured data to exceptions at the source via a `context()` method — Laravel includes it automatically in the log entry.
+
+```php
+class InvalidOrderException extends Exception
+{
+ public function context(): array
+ {
+ return ['order_id' => $this->orderId];
+ }
+}
+```
diff --git a/.junie/skills/laravel-best-practices/rules/events-notifications.md b/.junie/skills/laravel-best-practices/rules/events-notifications.md
new file mode 100644
index 0000000..82e329e
--- /dev/null
+++ b/.junie/skills/laravel-best-practices/rules/events-notifications.md
@@ -0,0 +1,52 @@
+# Events & Notifications Best Practices
+
+## Rely on Event Discovery
+
+Laravel auto-discovers listeners by reading `handle(EventType $event)` type-hints. No manual registration needed in `AppServiceProvider`.
+
+## Run `event:cache` in Production Deploy
+
+Event discovery scans the filesystem per-request in dev. Cache it in production: `php artisan optimize` or `php artisan event:cache`.
+
+## Use `ShouldDispatchAfterCommit` Inside Transactions
+
+Without it, a queued listener may process before the DB transaction commits, reading data that doesn't exist yet.
+
+```php
+class OrderShipped implements ShouldDispatchAfterCommit {}
+```
+
+## Always Queue Notifications
+
+Notifications often hit external APIs (email, SMS, Slack). Without `ShouldQueue`, they block the HTTP response.
+
+```php
+class InvoicePaid extends Notification implements ShouldQueue
+{
+ use Queueable;
+}
+```
+
+## Use `afterCommit()` on Notifications in Transactions
+
+Same race condition as events — call `afterCommit()` to delay dispatch until the transaction commits.
+
+```php
+$user->notify((new InvoicePaid($invoice))->afterCommit());
+```
+
+## Route Notification Channels to Dedicated Queues
+
+Mail and database notifications have different priorities. Use `viaQueues()` to route them to separate queues.
+
+## Use On-Demand Notifications for Non-User Recipients
+
+Avoid creating dummy models to send notifications to arbitrary addresses.
+
+```php
+Notification::route('mail', 'admin@example.com')->notify(new SystemAlert());
+```
+
+## Implement `HasLocalePreference` on Notifiable Models
+
+Laravel automatically uses the user's preferred locale for all notifications and mailables — no per-call `locale()` needed.
diff --git a/.junie/skills/laravel-best-practices/rules/http-client.md b/.junie/skills/laravel-best-practices/rules/http-client.md
new file mode 100644
index 0000000..8e2f16e
--- /dev/null
+++ b/.junie/skills/laravel-best-practices/rules/http-client.md
@@ -0,0 +1,160 @@
+# HTTP Client Best Practices
+
+## Always Set Explicit Timeouts
+
+The default timeout is 30 seconds — too long for most API calls. Always set explicit `timeout` and `connectTimeout` to fail fast.
+
+Incorrect:
+```php
+$response = Http::get('https://api.example.com/users');
+```
+
+Correct:
+```php
+$response = Http::timeout(5)
+ ->connectTimeout(3)
+ ->get('https://api.example.com/users');
+```
+
+For service-specific clients, define timeouts in a macro:
+
+```php
+Http::macro('github', function () {
+ return Http::baseUrl('https://api.github.com')
+ ->timeout(10)
+ ->connectTimeout(3)
+ ->withToken(config('services.github.token'));
+});
+
+$response = Http::github()->get('/repos/laravel/framework');
+```
+
+## Use Retry with Backoff for External APIs
+
+External APIs have transient failures. Use `retry()` with increasing delays.
+
+Incorrect:
+```php
+$response = Http::post('https://api.stripe.com/v1/charges', $data);
+
+if ($response->failed()) {
+ throw new PaymentFailedException('Charge failed');
+}
+```
+
+Correct:
+```php
+$response = Http::retry([100, 500, 1000])
+ ->timeout(10)
+ ->post('https://api.stripe.com/v1/charges', $data);
+```
+
+Only retry on specific errors:
+
+```php
+$response = Http::retry(3, 100, function (Throwable $exception, PendingRequest $request) {
+ return $exception instanceof ConnectionException
+ || ($exception instanceof RequestException && $exception->response->serverError());
+})->post('https://api.example.com/data');
+```
+
+## Handle Errors Explicitly
+
+The HTTP Client does not throw on 4xx/5xx by default. Always check status or use `throw()`.
+
+Incorrect:
+```php
+$response = Http::get('https://api.example.com/users/1');
+$user = $response->json(); // Could be an error body
+```
+
+Correct:
+```php
+$response = Http::timeout(5)
+ ->get('https://api.example.com/users/1')
+ ->throw();
+
+$user = $response->json();
+```
+
+For graceful degradation:
+
+```php
+$response = Http::get('https://api.example.com/users/1');
+
+if ($response->successful()) {
+ return $response->json();
+}
+
+if ($response->notFound()) {
+ return null;
+}
+
+$response->throw();
+```
+
+## Use Request Pooling for Concurrent Requests
+
+When making multiple independent API calls, use `Http::pool()` instead of sequential calls.
+
+Incorrect:
+```php
+$users = Http::get('https://api.example.com/users')->json();
+$posts = Http::get('https://api.example.com/posts')->json();
+$comments = Http::get('https://api.example.com/comments')->json();
+```
+
+Correct:
+```php
+use Illuminate\Http\Client\Pool;
+
+$responses = Http::pool(fn (Pool $pool) => [
+ $pool->as('users')->get('https://api.example.com/users'),
+ $pool->as('posts')->get('https://api.example.com/posts'),
+ $pool->as('comments')->get('https://api.example.com/comments'),
+]);
+
+$users = $responses['users']->json();
+$posts = $responses['posts']->json();
+```
+
+## Fake HTTP Calls in Tests
+
+Never make real HTTP requests in tests. Use `Http::fake()` and `preventStrayRequests()`.
+
+Incorrect:
+```php
+it('syncs user from API', function () {
+ $service = new UserSyncService;
+ $service->sync(1); // Hits the real API
+});
+```
+
+Correct:
+```php
+it('syncs user from API', function () {
+ Http::preventStrayRequests();
+
+ Http::fake([
+ 'api.example.com/users/1' => Http::response([
+ 'name' => 'John Doe',
+ 'email' => 'john@example.com',
+ ]),
+ ]);
+
+ $service = new UserSyncService;
+ $service->sync(1);
+
+ Http::assertSent(function (Request $request) {
+ return $request->url() === 'https://api.example.com/users/1';
+ });
+});
+```
+
+Test failure scenarios too:
+
+```php
+Http::fake([
+ 'api.example.com/*' => Http::failedConnection(),
+]);
+```
diff --git a/.junie/skills/laravel-best-practices/rules/mail.md b/.junie/skills/laravel-best-practices/rules/mail.md
new file mode 100644
index 0000000..7c71733
--- /dev/null
+++ b/.junie/skills/laravel-best-practices/rules/mail.md
@@ -0,0 +1,27 @@
+# Mail Best Practices
+
+## Implement `ShouldQueue` on the Mailable Class
+
+Makes queueing the default regardless of how the mailable is dispatched. No need to remember `Mail::queue()` at every call site — `Mail::send()` also queues it.
+
+## Use `afterCommit()` on Mailables Inside Transactions
+
+A queued mailable dispatched inside a transaction may process before the commit. Use `$this->afterCommit()` in the constructor.
+
+## Use `assertQueued()` Not `assertSent()` for Queued Mailables
+
+`Mail::assertSent()` only catches synchronous mail. Queued mailables fail `assertSent` with a "Did you mean to use assertQueued()?" hint.
+
+Incorrect: `Mail::assertSent(OrderShipped::class);` when mailable implements `ShouldQueue`.
+
+Correct: `Mail::assertQueued(OrderShipped::class);`
+
+## Use Markdown Mailables for Transactional Emails
+
+Markdown mailables auto-generate both HTML and plain-text versions, use responsive components, and allow global style customization. Generate with `--markdown` flag.
+
+## Separate Content Tests from Sending Tests
+
+Content tests: instantiate the mailable directly, call `assertSeeInHtml()`.
+Sending tests: use `Mail::fake()` and `assertSent()`/`assertQueued()`.
+Don't mix them — it conflates concerns and makes tests brittle.
diff --git a/.junie/skills/laravel-best-practices/rules/migrations.md b/.junie/skills/laravel-best-practices/rules/migrations.md
new file mode 100644
index 0000000..df6f5f3
--- /dev/null
+++ b/.junie/skills/laravel-best-practices/rules/migrations.md
@@ -0,0 +1,121 @@
+# Migration Best Practices
+
+## Generate Migrations with Artisan
+
+Always use `php artisan make:migration` for consistent naming and timestamps.
+
+Incorrect (manually created file):
+```php
+// database/migrations/posts_migration.php ← wrong naming, no timestamp
+```
+
+Correct (Artisan-generated):
+```bash
+php artisan make:migration create_posts_table
+php artisan make:migration add_slug_to_posts_table
+```
+
+## Use `constrained()` for Foreign Keys
+
+Automatic naming and referential integrity.
+
+```php
+$table->foreignId('user_id')->constrained()->cascadeOnDelete();
+
+// Non-standard names
+$table->foreignId('author_id')->constrained('users');
+```
+
+## Never Modify Deployed Migrations
+
+Once a migration has run in production, treat it as immutable. Create a new migration to change the table.
+
+Incorrect (editing a deployed migration):
+```php
+// 2024_01_01_create_posts_table.php — already in production
+$table->string('slug')->unique(); // ← added after deployment
+```
+
+Correct (new migration to alter):
+```php
+// 2024_03_15_add_slug_to_posts_table.php
+Schema::table('posts', function (Blueprint $table) {
+ $table->string('slug')->unique()->after('title');
+});
+```
+
+## Add Indexes in the Migration
+
+Add indexes when creating the table, not as an afterthought. Columns used in `WHERE`, `ORDER BY`, and `JOIN` clauses need indexes.
+
+Incorrect:
+```php
+Schema::create('orders', function (Blueprint $table) {
+ $table->id();
+ $table->foreignId('user_id')->constrained();
+ $table->string('status');
+ $table->timestamps();
+});
+```
+
+Correct:
+```php
+Schema::create('orders', function (Blueprint $table) {
+ $table->id();
+ $table->foreignId('user_id')->constrained()->index();
+ $table->string('status')->index();
+ $table->timestamp('shipped_at')->nullable()->index();
+ $table->timestamps();
+});
+```
+
+## Mirror Defaults in Model `$attributes`
+
+When a column has a database default, mirror it in the model so new instances have correct values before saving.
+
+```php
+// Migration
+$table->string('status')->default('pending');
+
+// Model
+protected $attributes = [
+ 'status' => 'pending',
+];
+```
+
+## Write Reversible `down()` Methods by Default
+
+Implement `down()` for schema changes that can be safely reversed so `migrate:rollback` works in CI and failed deployments.
+
+```php
+public function down(): void
+{
+ Schema::table('posts', function (Blueprint $table) {
+ $table->dropColumn('slug');
+ });
+}
+```
+
+For intentionally irreversible migrations (e.g., destructive data backfills), leave a clear comment and require a forward fix migration instead of pretending rollback is supported.
+
+## Keep Migrations Focused
+
+One concern per migration. Never mix DDL (schema changes) and DML (data manipulation).
+
+Incorrect (partial failure creates unrecoverable state):
+```php
+public function up(): void
+{
+ Schema::create('settings', function (Blueprint $table) { ... });
+ DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']);
+}
+```
+
+Correct (separate migrations):
+```php
+// Migration 1: create_settings_table
+Schema::create('settings', function (Blueprint $table) { ... });
+
+// Migration 2: seed_default_settings
+DB::table('settings')->insert(['key' => 'version', 'value' => '1.0']);
+```
diff --git a/.junie/skills/laravel-best-practices/rules/queue-jobs.md b/.junie/skills/laravel-best-practices/rules/queue-jobs.md
new file mode 100644
index 0000000..c41915e
--- /dev/null
+++ b/.junie/skills/laravel-best-practices/rules/queue-jobs.md
@@ -0,0 +1,144 @@
+# Queue & Job Best Practices
+
+## Set `retry_after` Greater Than `timeout`
+
+If `retry_after` is shorter than the job's `timeout`, the queue worker re-dispatches the job while it's still running, causing duplicate execution.
+
+Incorrect (`retry_after` ≤ `timeout`):
+```php
+class ProcessReport implements ShouldQueue
+{
+ public $timeout = 120;
+}
+
+// config/queue.php — retry_after: 90 ← job retried while still running!
+```
+
+Correct (`retry_after` > `timeout`):
+```php
+class ProcessReport implements ShouldQueue
+{
+ public $timeout = 120;
+}
+
+// config/queue.php — retry_after: 180 ← safely longer than any job timeout
+```
+
+## Use Exponential Backoff
+
+Use progressively longer delays between retries to avoid hammering failing services.
+
+Incorrect (fixed retry interval):
+```php
+class SyncWithStripe implements ShouldQueue
+{
+ public $tries = 3;
+ // Default: retries immediately, overwhelming the API
+}
+```
+
+Correct (exponential backoff):
+```php
+class SyncWithStripe implements ShouldQueue
+{
+ public $tries = 3;
+ public $backoff = [1, 5, 10];
+}
+```
+
+## Implement `ShouldBeUnique`
+
+Prevent duplicate job processing.
+
+```php
+class GenerateInvoice implements ShouldQueue, ShouldBeUnique
+{
+ public function uniqueId(): string
+ {
+ return $this->order->id;
+ }
+
+ public $uniqueFor = 3600;
+}
+```
+
+## Always Implement `failed()`
+
+Handle errors explicitly — don't rely on silent failure.
+
+```php
+public function failed(?Throwable $exception): void
+{
+ $this->podcast->update(['status' => 'failed']);
+ Log::error('Processing failed', ['id' => $this->podcast->id, 'error' => $exception->getMessage()]);
+}
+```
+
+## Rate Limit External API Calls in Jobs
+
+Use `RateLimited` middleware to throttle jobs calling third-party APIs.
+
+```php
+public function middleware(): array
+{
+ return [new RateLimited('external-api')];
+}
+```
+
+## Batch Related Jobs
+
+Use `Bus::batch()` when jobs should succeed or fail together.
+
+```php
+Bus::batch([
+ new ImportCsvChunk($chunk1),
+ new ImportCsvChunk($chunk2),
+])
+->then(fn (Batch $batch) => Notification::send($user, new ImportComplete))
+->catch(fn (Batch $batch, Throwable $e) => Log::error('Batch failed'))
+->dispatch();
+```
+
+## `retryUntil()` Needs `$tries = 0`
+
+When using time-based retry limits, set `$tries = 0` to avoid premature failure.
+
+```php
+public $tries = 0;
+
+public function retryUntil(): \DateTimeInterface
+{
+ return now()->addHours(4);
+}
+```
+
+## Use `ShouldBeUniqueUntilProcessing` for Early Lock Release
+
+`ShouldBeUnique` holds the lock until the job completes. `ShouldBeUniqueUntilProcessing` releases it when processing starts, allowing new instances to queue.
+
+```php
+class UpdateSearchIndex implements ShouldQueue, ShouldBeUniqueUntilProcessing
+{
+ // Lock releases when processing begins, not when it finishes
+}
+```
+
+## Use Horizon for Complex Queue Scenarios
+
+Use Laravel Horizon when you need monitoring, auto-scaling, failure tracking, or multiple queues with different priorities.
+
+```php
+// config/horizon.php
+'environments' => [
+ 'production' => [
+ 'supervisor-1' => [
+ 'connection' => 'redis',
+ 'queue' => ['high', 'default', 'low'],
+ 'balance' => 'auto',
+ 'minProcesses' => 1,
+ 'maxProcesses' => 10,
+ 'tries' => 3,
+ ],
+ ],
+],
+```
diff --git a/.junie/skills/laravel-best-practices/rules/routing.md b/.junie/skills/laravel-best-practices/rules/routing.md
new file mode 100644
index 0000000..b6e3086
--- /dev/null
+++ b/.junie/skills/laravel-best-practices/rules/routing.md
@@ -0,0 +1,99 @@
+# Routing & Controllers Best Practices
+
+## Use Implicit Route Model Binding
+
+Let Laravel resolve models automatically from route parameters.
+
+Incorrect:
+```php
+public function show(int $id)
+{
+ $post = Post::findOrFail($id);
+}
+```
+
+Correct:
+```php
+public function show(Post $post)
+{
+ return view('posts.show', ['post' => $post]);
+}
+```
+
+## Use Scoped Bindings for Nested Resources
+
+Enforce parent-child relationships automatically.
+
+```php
+Route::get('/users/{user}/posts/{post}', function (User $user, Post $post) {
+ // $post is automatically scoped to $user
+})->scopeBindings();
+```
+
+## Use Resource Controllers
+
+Use `Route::resource()` or `apiResource()` for RESTful endpoints.
+
+```php
+Route::resource('posts', PostController::class);
+// In routes/api.php — the /api prefix is applied automatically
+Route::apiResource('posts', Api\PostController::class);
+```
+
+## Keep Controllers Thin
+
+Aim for under 10 lines per method. Extract business logic to action or service classes.
+
+Incorrect:
+```php
+public function store(Request $request)
+{
+ $validated = $request->validate([...]);
+ if ($request->hasFile('image')) {
+ $request->file('image')->move(public_path('images'));
+ }
+ $post = Post::create($validated);
+ $post->tags()->sync($validated['tags']);
+ event(new PostCreated($post));
+ return redirect()->route('posts.show', $post);
+}
+```
+
+Correct:
+```php
+public function store(StorePostRequest $request, CreatePostAction $create)
+{
+ $post = $create->execute($request->validated());
+
+ return redirect()->route('posts.show', $post);
+}
+```
+
+## Type-Hint Form Requests
+
+Type-hinting Form Requests triggers automatic validation and authorization before the method executes.
+
+Incorrect:
+```php
+public function store(Request $request): RedirectResponse
+{
+ $validated = $request->validate([
+ 'title' => ['required', 'max:255'],
+ 'body' => ['required'],
+ ]);
+
+ Post::create($validated);
+
+ return redirect()->route('posts.index');
+}
+```
+
+Correct:
+```php
+public function store(StorePostRequest $request): RedirectResponse
+{
+ Post::create($request->validated());
+
+ return redirect()->route('posts.index');
+}
+```
diff --git a/.junie/skills/laravel-best-practices/rules/scheduling.md b/.junie/skills/laravel-best-practices/rules/scheduling.md
new file mode 100644
index 0000000..a984794
--- /dev/null
+++ b/.junie/skills/laravel-best-practices/rules/scheduling.md
@@ -0,0 +1,39 @@
+# Task Scheduling Best Practices
+
+## Use `withoutOverlapping()` on Variable-Duration Tasks
+
+Without it, a long-running task spawns a second instance on the next tick, causing double-processing or resource exhaustion.
+
+## Use `onOneServer()` on Multi-Server Deployments
+
+Without it, every server runs the same task simultaneously. Requires a shared cache driver (Redis, database, Memcached).
+
+## Use `runInBackground()` for Concurrent Long Tasks
+
+By default, tasks at the same tick run sequentially. A slow first task delays all subsequent ones. `runInBackground()` runs them as separate processes.
+
+## Use `environments()` to Restrict Tasks
+
+Prevent accidental execution of production-only tasks (billing, reporting) on staging.
+
+```php
+Schedule::command('billing:charge')->monthly()->environments(['production']);
+```
+
+## Use `takeUntilTimeout()` for Time-Bounded Processing
+
+A task running every 15 minutes that processes an unbounded cursor can overlap with the next run. Bound execution time.
+
+## Use Schedule Groups for Shared Configuration
+
+Avoid repeating `->onOneServer()->timezone('America/New_York')` across many tasks.
+
+```php
+Schedule::daily()
+ ->onOneServer()
+ ->timezone('America/New_York')
+ ->group(function () {
+ Schedule::command('emails:send --force');
+ Schedule::command('emails:prune');
+ });
+```
diff --git a/.junie/skills/laravel-best-practices/rules/security.md b/.junie/skills/laravel-best-practices/rules/security.md
new file mode 100644
index 0000000..2d7200c
--- /dev/null
+++ b/.junie/skills/laravel-best-practices/rules/security.md
@@ -0,0 +1,198 @@
+# Security Best Practices
+
+## Mass Assignment Protection
+
+Every model must define `$fillable` (whitelist) or `$guarded` (blacklist).
+
+Incorrect:
+```php
+class User extends Model
+{
+ protected $guarded = []; // All fields are mass assignable
+}
+```
+
+Correct:
+```php
+class User extends Model
+{
+ protected $fillable = [
+ 'name',
+ 'email',
+ 'password',
+ ];
+}
+```
+
+Never use `$guarded = []` on models that accept user input.
+
+## Authorize Every Action
+
+Use policies or gates in controllers. Never skip authorization.
+
+Incorrect:
+```php
+public function update(UpdatePostRequest $request, Post $post)
+{
+ $post->update($request->validated());
+}
+```
+
+Correct:
+```php
+public function update(UpdatePostRequest $request, Post $post)
+{
+ Gate::authorize('update', $post);
+
+ $post->update($request->validated());
+}
+```
+
+Or via Form Request:
+
+```php
+public function authorize(): bool
+{
+ return $this->user()->can('update', $this->route('post'));
+}
+```
+
+## Prevent SQL Injection
+
+Always use parameter binding. Never interpolate user input into queries.
+
+Incorrect:
+```php
+DB::select("SELECT * FROM users WHERE name = '{$request->name}'");
+```
+
+Correct:
+```php
+User::where('name', $request->name)->get();
+
+// Raw expressions with bindings
+User::whereRaw('LOWER(name) = ?', [strtolower($request->name)])->get();
+```
+
+## Escape Output to Prevent XSS
+
+Use `{{ }}` for HTML escaping. Only use `{!! !!}` for trusted, pre-sanitized content.
+
+Incorrect:
+```blade
+{!! $user->bio !!}
+```
+
+Correct:
+```blade
+{{ $user->bio }}
+```
+
+## CSRF Protection
+
+Include `@csrf` in all POST/PUT/DELETE Blade forms. In Inertia apps, the `@csrf` directive is automatically applied.
+
+Incorrect:
+```blade
+
+```
+
+Correct:
+```blade
+
+```
+
+## Rate Limit Auth and API Routes
+
+Apply `throttle` middleware to authentication and API routes.
+
+```php
+RateLimiter::for('login', function (Request $request) {
+ return Limit::perMinute(5)->by($request->ip());
+});
+
+Route::post('/login', LoginController::class)->middleware('throttle:login');
+```
+
+## Validate File Uploads
+
+Validate extension, MIME type, and size. The `mimes` rule checks extensions; use `mimetypes` for actual MIME type validation. Never trust client-provided filenames.
+
+```php
+public function rules(): array
+{
+ return [
+ 'avatar' => ['required', 'image', 'mimes:jpg,jpeg,png,webp', 'max:2048'],
+ ];
+}
+```
+
+Store with generated filenames:
+
+```php
+$path = $request->file('avatar')->store('avatars', 'public');
+```
+
+## Keep Secrets Out of Code
+
+Never commit `.env`. Access secrets via `config()` only.
+
+Incorrect:
+```php
+$key = env('API_KEY');
+```
+
+Correct:
+```php
+// config/services.php
+'api_key' => env('API_KEY'),
+
+// In application code
+$key = config('services.api_key');
+```
+
+## Audit Dependencies
+
+Run `composer audit` periodically to check for known vulnerabilities in dependencies. Automate this in CI to catch issues before deployment.
+
+```bash
+composer audit
+```
+
+## Encrypt Sensitive Database Fields
+
+Use `encrypted` cast for API keys/tokens and mark the attribute as `hidden`.
+
+Incorrect:
+```php
+class Integration extends Model
+{
+ protected function casts(): array
+ {
+ return [
+ 'api_key' => 'string',
+ ];
+ }
+}
+```
+
+Correct:
+```php
+class Integration extends Model
+{
+ protected $hidden = ['api_key', 'api_secret'];
+
+ protected function casts(): array
+ {
+ return [
+ 'api_key' => 'encrypted',
+ 'api_secret' => 'encrypted',
+ ];
+ }
+}
+```
diff --git a/.junie/skills/laravel-best-practices/rules/style.md b/.junie/skills/laravel-best-practices/rules/style.md
new file mode 100644
index 0000000..64d1730
--- /dev/null
+++ b/.junie/skills/laravel-best-practices/rules/style.md
@@ -0,0 +1,125 @@
+# Conventions & Style
+
+## Follow Laravel Naming Conventions
+
+| What | Convention | Good | Bad |
+|------|-----------|------|-----|
+| Controller | singular | `ArticleController` | `ArticlesController` |
+| Model | singular | `User` | `Users` |
+| Table | plural, snake_case | `article_comments` | `articleComments` |
+| Pivot table | singular alphabetical | `article_user` | `user_article` |
+| Column | snake_case, no model name | `meta_title` | `article_meta_title` |
+| Foreign key | singular model + `_id` | `article_id` | `articles_id` |
+| Route | plural | `articles/1` | `article/1` |
+| Route name | snake_case with dots | `users.show_active` | `users.show-active` |
+| Method | camelCase | `getAll` | `get_all` |
+| Variable | camelCase | `$articlesWithAuthor` | `$articles_with_author` |
+| Collection | descriptive, plural | `$activeUsers` | `$data` |
+| Object | descriptive, singular | `$activeUser` | `$users` |
+| View | kebab-case | `show-filtered.blade.php` | `showFiltered.blade.php` |
+| Config | snake_case | `google_calendar.php` | `googleCalendar.php` |
+| Enum | singular | `UserType` | `UserTypes` |
+
+## Prefer Shorter Readable Syntax
+
+| Verbose | Shorter |
+|---------|---------|
+| `Session::get('cart')` | `session('cart')` |
+| `$request->session()->get('cart')` | `session('cart')` |
+| `$request->input('name')` | `$request->name` |
+| `return Redirect::back()` | `return back()` |
+| `Carbon::now()` | `now()` |
+| `App::make('Class')` | `app('Class')` |
+| `->where('column', '=', 1)` | `->where('column', 1)` |
+| `->orderBy('created_at', 'desc')` | `->latest()` |
+| `->orderBy('created_at', 'asc')` | `->oldest()` |
+| `->first()->name` | `->value('name')` |
+
+## Use Laravel String & Array Helpers
+
+Laravel provides `Str`, `Arr`, `Number`, and `Uri` helper classes that are more readable, chainable, and UTF-8 safe than raw PHP functions. Always prefer them.
+
+Strings — use `Str` and fluent `Str::of()` over raw PHP:
+```php
+// Incorrect
+$slug = strtolower(str_replace(' ', '-', $title));
+$short = substr($text, 0, 100) . '...';
+$class = substr(strrchr('App\Models\User', '\'), 1);
+
+// Correct
+$slug = Str::slug($title);
+$short = Str::limit($text, 100);
+$class = class_basename('App\Models\User');
+```
+
+Fluent strings — chain operations for complex transformations:
+```php
+// Incorrect
+$result = strtolower(trim(str_replace('_', '-', $input)));
+
+// Correct
+$result = Str::of($input)->trim()->replace('_', '-')->lower();
+```
+
+Key `Str` methods to prefer: `Str::slug()`, `Str::limit()`, `Str::contains()`, `Str::before()`, `Str::after()`, `Str::between()`, `Str::camel()`, `Str::snake()`, `Str::kebab()`, `Str::headline()`, `Str::squish()`, `Str::mask()`, `Str::uuid()`, `Str::ulid()`, `Str::random()`, `Str::is()`.
+
+Arrays — use `Arr` over raw PHP:
+```php
+// Incorrect
+$name = isset($array['user']['name']) ? $array['user']['name'] : 'default';
+
+// Correct
+$name = Arr::get($array, 'user.name', 'default');
+```
+
+Key `Arr` methods: `Arr::get()`, `Arr::has()`, `Arr::only()`, `Arr::except()`, `Arr::first()`, `Arr::flatten()`, `Arr::pluck()`, `Arr::where()`, `Arr::wrap()`.
+
+Numbers — use `Number` for display formatting:
+```php
+Number::format(1000000); // "1,000,000"
+Number::currency(1500, 'USD'); // "$1,500.00"
+Number::abbreviate(1000000); // "1M"
+Number::fileSize(1024 * 1024); // "1 MB"
+Number::percentage(75.5); // "75.5%"
+```
+
+URIs — use `Uri` for URL manipulation:
+```php
+$uri = Uri::of('https://example.com/search')
+ ->withQuery(['q' => 'laravel', 'page' => 1]);
+```
+
+Use `$request->string('name')` to get a fluent `Stringable` directly from request input for immediate chaining.
+
+Use `search-docs` for the full list of available methods — these helpers are extensive.
+
+## No Inline JS/CSS in Blade
+
+Do not put JS or CSS in Blade templates. Do not put HTML in PHP classes.
+
+Incorrect:
+```blade
+let article = `{{ json_encode($article) }}`;
+```
+
+Correct:
+```blade
+{{ $article->name }}
+```
+
+Pass data to JS via data attributes or use a dedicated PHP-to-JS package.
+
+## No Unnecessary Comments
+
+Code should be readable on its own. Use descriptive method and variable names instead of comments. The only exception is config files, where descriptive comments are expected.
+
+Incorrect:
+```php
+// Check if there are any joins
+if (count((array) $builder->getQuery()->joins) > 0)
+```
+
+Correct:
+```php
+if ($this->hasJoins())
+```
diff --git a/.junie/skills/laravel-best-practices/rules/testing.md b/.junie/skills/laravel-best-practices/rules/testing.md
new file mode 100644
index 0000000..4fbf12f
--- /dev/null
+++ b/.junie/skills/laravel-best-practices/rules/testing.md
@@ -0,0 +1,43 @@
+# Testing Best Practices
+
+## Use `LazilyRefreshDatabase` Over `RefreshDatabase`
+
+`RefreshDatabase` migrates once per process and wraps each test in a rolled-back transaction. `LazilyRefreshDatabase` skips even that first migration if the schema is already up to date.
+
+## Use Model Assertions Over Raw Database Assertions
+
+Incorrect: `$this->assertDatabaseHas('users', ['id' => $user->id]);`
+
+Correct: `$this->assertModelExists($user);`
+
+More expressive, type-safe, and fails with clearer messages.
+
+## Use Factory States and Sequences
+
+Named states make tests self-documenting. Sequences eliminate repetitive setup.
+
+Incorrect: `User::factory()->create(['email_verified_at' => null]);`
+
+Correct: `User::factory()->unverified()->create();`
+
+## Use `Exceptions::fake()` to Assert Exception Reporting
+
+Instead of `withoutExceptionHandling()`, use `Exceptions::fake()` to assert the correct exception was reported while the request completes normally.
+
+## Call `Event::fake()` After Factory Setup
+
+Model factories rely on model events (e.g., `creating` to generate UUIDs). Calling `Event::fake()` before factory calls silences those events, producing broken models.
+
+Incorrect: `Event::fake(); $user = User::factory()->create();`
+
+Correct: `$user = User::factory()->create(); Event::fake();`
+
+## Use `recycle()` to Share Relationship Instances Across Factories
+
+Without `recycle()`, nested factories create separate instances of the same conceptual entity.
+
+```php
+Ticket::factory()
+ ->recycle(Airline::factory()->create())
+ ->create();
+```
diff --git a/.junie/skills/laravel-best-practices/rules/validation.md b/.junie/skills/laravel-best-practices/rules/validation.md
new file mode 100644
index 0000000..5fde106
--- /dev/null
+++ b/.junie/skills/laravel-best-practices/rules/validation.md
@@ -0,0 +1,75 @@
+# Validation & Forms Best Practices
+
+## Use Form Request Classes
+
+Extract validation from controllers into dedicated Form Request classes.
+
+Incorrect:
+```php
+public function store(Request $request)
+{
+ $request->validate([
+ 'title' => 'required|max:255',
+ 'body' => 'required',
+ ]);
+}
+```
+
+Correct:
+```php
+public function store(StorePostRequest $request)
+{
+ Post::create($request->validated());
+}
+```
+
+## Array vs. String Notation for Rules
+
+Array syntax is more readable and composes cleanly with `Rule::` objects. Prefer it in new code, but check existing Form Requests first and match whatever notation the project already uses.
+
+```php
+// Preferred for new code
+'email' => ['required', 'email', Rule::unique('users')],
+
+// Follow existing convention if the project uses string notation
+'email' => 'required|email|unique:users',
+```
+
+## Always Use `validated()`
+
+Get only validated data. Never use `$request->all()` for mass operations.
+
+Incorrect:
+```php
+Post::create($request->all());
+```
+
+Correct:
+```php
+Post::create($request->validated());
+```
+
+## Use `Rule::when()` for Conditional Validation
+
+```php
+'company_name' => [
+ Rule::when($this->account_type === 'business', ['required', 'string', 'max:255']),
+],
+```
+
+## Use the `after()` Method for Custom Validation
+
+Use `after()` instead of `withValidator()` for custom validation logic that depends on multiple fields.
+
+```php
+public function after(): array
+{
+ return [
+ function (Validator $validator) {
+ if ($this->quantity > Product::find($this->product_id)?->stock) {
+ $validator->errors()->add('quantity', 'Not enough stock.');
+ }
+ },
+ ];
+}
+```
diff --git a/.junie/skills/laravel-permission-development/SKILL.md b/.junie/skills/laravel-permission-development/SKILL.md
new file mode 100644
index 0000000..0e3bbd8
--- /dev/null
+++ b/.junie/skills/laravel-permission-development/SKILL.md
@@ -0,0 +1,277 @@
+---
+name: laravel-permission-development
+description: Build and work with Spatie Laravel Permission features, including roles, permissions, middleware, policies, teams, and Blade directives.
+---
+
+# Laravel Permission Development
+
+## When to use this skill
+
+Use this skill when working with authorization, roles, permissions, access control, middleware guards, or Blade permission directives using spatie/laravel-permission.
+
+## Core Concepts
+
+- **Users have Roles, Roles have Permissions, Apps check Permissions** (not Roles).
+- Direct permissions on users are an anti-pattern; assign permissions to roles instead.
+- Use `$user->can('permission-name')` for all authorization checks (supports Super Admin via Gate).
+- The `HasRoles` trait (which includes `HasPermissions`) is added to User models.
+
+## Setup
+
+Add the `HasRoles` trait to your User model:
+
+```php
+use Spatie\Permission\Traits\HasRoles;
+
+class User extends Authenticatable
+{
+ use HasRoles;
+}
+```
+
+## Creating Roles and Permissions
+
+```php
+use Spatie\Permission\Models\Role;
+use Spatie\Permission\Models\Permission;
+
+$role = Role::create(['name' => 'writer']);
+$permission = Permission::create(['name' => 'edit articles']);
+
+// findOrCreate is idempotent (safe for seeders)
+$role = Role::findOrCreate('writer', 'web');
+$permission = Permission::findOrCreate('edit articles', 'web');
+```
+
+## Assigning Roles and Permissions
+
+```php
+// Assign roles to users
+$user->assignRole('writer');
+$user->assignRole('writer', 'admin');
+$user->assignRole(['writer', 'admin']);
+$user->syncRoles(['writer', 'admin']); // replaces all
+$user->removeRole('writer');
+
+// Assign permissions to roles (preferred)
+$role->givePermissionTo('edit articles');
+$role->givePermissionTo(['edit articles', 'delete articles']);
+$role->syncPermissions(['edit articles', 'delete articles']);
+$role->revokePermissionTo('edit articles');
+
+// Reverse assignment
+$permission->assignRole('writer');
+$permission->syncRoles(['writer', 'editor']);
+$permission->removeRole('writer');
+```
+
+## Checking Roles and Permissions
+
+```php
+// Permission checks (preferred - supports Super Admin via Gate)
+$user->can('edit articles');
+$user->canAny(['edit articles', 'delete articles']);
+
+// Direct package methods (bypass Gate, no Super Admin support)
+$user->hasPermissionTo('edit articles');
+$user->hasAnyPermission(['edit articles', 'publish articles']);
+$user->hasAllPermissions(['edit articles', 'publish articles']);
+$user->hasDirectPermission('edit articles');
+
+// Role checks
+$user->hasRole('writer');
+$user->hasAnyRole(['writer', 'editor']);
+$user->hasAllRoles(['writer', 'editor']);
+$user->hasExactRoles(['writer', 'editor']);
+
+// Get assigned roles and permissions
+$user->getRoleNames(); // Collection of role name strings
+$user->getPermissionNames(); // Collection of permission name strings
+$user->getDirectPermissions(); // Direct permissions only
+$user->getPermissionsViaRoles(); // Inherited via roles
+$user->getAllPermissions(); // Both direct and inherited
+```
+
+## Query Scopes
+
+```php
+$users = User::role('writer')->get();
+$users = User::withoutRole('writer')->get();
+$users = User::permission('edit articles')->get();
+$users = User::withoutPermission('edit articles')->get();
+```
+
+## Middleware
+
+Register middleware aliases in `bootstrap/app.php`:
+
+```php
+->withMiddleware(function (Middleware $middleware) {
+ $middleware->alias([
+ 'role' => \Spatie\Permission\Middleware\RoleMiddleware::class,
+ 'permission' => \Spatie\Permission\Middleware\PermissionMiddleware::class,
+ 'role_or_permission' => \Spatie\Permission\Middleware\RoleOrPermissionMiddleware::class,
+ ]);
+})
+```
+
+Use in routes (pipe `|` for OR logic):
+
+```php
+Route::middleware(['permission:edit articles'])->group(function () { ... });
+Route::middleware(['role:manager|writer'])->group(function () { ... });
+Route::middleware(['role_or_permission:manager|edit articles'])->group(function () { ... });
+
+// With specific guard
+Route::middleware(['role:manager,api'])->group(function () { ... });
+```
+
+For single permissions, Laravel's built-in `can` middleware also works:
+
+```php
+Route::middleware(['can:edit articles'])->group(function () { ... });
+```
+
+## Blade Directives
+
+Prefer `@can` (permission-based) over `@role` (role-based):
+
+```blade
+@can('edit articles')
+ {{-- User can edit articles (supports Super Admin) --}}
+@endcan
+
+@canany(['edit articles', 'delete articles'])
+ {{-- User can do at least one --}}
+@endcanany
+
+@role('admin')
+ {{-- Only use for super-admin type checks --}}
+@endrole
+
+@hasanyrole('writer|admin')
+ {{-- Has writer or admin --}}
+@endhasanyrole
+```
+
+## Super Admin
+
+Use `Gate::before` in `AppServiceProvider::boot()`:
+
+```php
+use Illuminate\Support\Facades\Gate;
+
+public function boot(): void
+{
+ Gate::before(function ($user, $ability) {
+ return $user->hasRole('Super Admin') ? true : null;
+ });
+}
+```
+
+This makes `$user->can()` and `@can` always return true for Super Admins. Must return `null` (not `false`) to allow normal checks for other users.
+
+## Policies
+
+Use `$user->can()` inside policy methods to check permissions:
+
+```php
+class PostPolicy
+{
+ public function update(User $user, Post $post): bool
+ {
+ if ($user->can('edit all posts')) {
+ return true;
+ }
+
+ return $user->can('edit own posts') && $user->id === $post->user_id;
+ }
+}
+```
+
+## Enums
+
+```php
+enum RolesEnum: string
+{
+ case WRITER = 'writer';
+ case EDITOR = 'editor';
+}
+
+enum PermissionsEnum: string
+{
+ case EDIT_POSTS = 'edit posts';
+ case DELETE_POSTS = 'delete posts';
+}
+
+// Creation requires ->value
+Permission::findOrCreate(PermissionsEnum::EDIT_POSTS->value, 'web');
+
+// Most methods accept enums directly
+$user->assignRole(RolesEnum::WRITER);
+$user->hasRole(RolesEnum::WRITER);
+$role->givePermissionTo(PermissionsEnum::EDIT_POSTS);
+$user->hasPermissionTo(PermissionsEnum::EDIT_POSTS);
+```
+
+## Seeding
+
+Always flush the permission cache when seeding:
+
+```php
+class RolesAndPermissionsSeeder extends Seeder
+{
+ public function run(): void
+ {
+ // Reset cache
+ app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions();
+
+ // Create permissions
+ Permission::findOrCreate('edit articles', 'web');
+ Permission::findOrCreate('delete articles', 'web');
+
+ // Create roles and assign permissions
+ Role::findOrCreate('writer', 'web')
+ ->givePermissionTo(['edit articles']);
+
+ Role::findOrCreate('admin', 'web')
+ ->givePermissionTo(Permission::all());
+ }
+}
+```
+
+## Teams (Multi-Tenancy)
+
+Enable in `config/permission.php` before running migrations:
+
+```php
+'teams' => true,
+```
+
+Set the active team in middleware:
+
+```php
+setPermissionsTeamId($teamId);
+```
+
+When switching teams, unset cached relations:
+
+```php
+$user->unsetRelation('roles')->unsetRelation('permissions');
+```
+
+## Events
+
+Enable in `config/permission.php`:
+
+```php
+'events_enabled' => true,
+```
+
+Available events: `RoleAttachedEvent`, `RoleDetachedEvent`, `PermissionAttachedEvent`, `PermissionDetachedEvent` in the `Spatie\Permission\Events` namespace.
+
+## Performance
+
+- Permissions are cached automatically. The cache is flushed when roles/permissions change via package methods.
+- After direct DB operations, flush manually: `app()[\Spatie\Permission\PermissionRegistrar::class]->forgetCachedPermissions()`
+- For bulk seeding, use `Permission::insert()` for speed, but flush the cache afterward.
diff --git a/.junie/skills/pest-testing/SKILL.md b/.junie/skills/pest-testing/SKILL.md
new file mode 100644
index 0000000..ab27161
--- /dev/null
+++ b/.junie/skills/pest-testing/SKILL.md
@@ -0,0 +1,166 @@
+---
+name: pest-testing
+description: "Use this skill for Pest PHP testing in Laravel projects only. Trigger whenever any test is being written, edited, fixed, or refactored — including fixing tests that broke after a code change, adding assertions, converting PHPUnit to Pest, adding datasets, and TDD workflows. Always activate when the user asks how to write something in Pest, mentions test files or directories (tests/Feature, tests/Unit, tests/Browser), or needs browser testing, smoke testing multiple pages for JS errors, or architecture tests. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, and all Pest 4 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code."
+license: MIT
+metadata:
+ author: laravel
+---
+
+# Pest Testing 4
+
+## Documentation
+
+Use `search-docs` for detailed Pest 4 patterns and documentation.
+
+## Basic Usage
+
+### Creating Tests
+
+All tests must be written using Pest. Use `php artisan make:test --pest {name}`.
+
+The `{name}` argument should include only the path and test name, but should not include the test suite.
+- Incorrect: `php artisan make:test --pest Feature/SomeFeatureTest` will generate `tests/Feature/Feature/SomeFeatureTest.php`
+- Correct: `php artisan make:test --pest SomeControllerTest` will generate `tests/Feature/SomeControllerTest.php`
+- Incorrect: `php artisan make:test --pest --unit Unit/SomeServiceTest` will generate `tests/Unit/Unit/SomeServiceTest.php`
+- Correct: `php artisan make:test --pest --unit SomeServiceTest` will generate `tests/Unit/SomeServiceTest.php`
+
+### Test Organization
+
+- Unit/Feature tests: `tests/Feature` and `tests/Unit` directories.
+- Browser tests: `tests/Browser/` directory.
+- Do NOT remove tests without approval - these are core application code.
+
+### Basic Test Structure
+
+Pest supports both `test()` and `it()` functions. Before writing new tests, check existing test files in the same directory to match the project's convention. Use `test()` if existing tests use `test()`, or `it()` if they use `it()`.
+
+
+```php
+it('is true', function () {
+ expect(true)->toBeTrue();
+});
+```
+
+### Running Tests
+
+- Run minimal tests with filter before finalizing: `php artisan test --compact --filter=testName`.
+- Run all tests: `php artisan test --compact`.
+- Run file: `php artisan test --compact tests/Feature/ExampleTest.php`.
+
+## Assertions
+
+Use specific assertions (`assertSuccessful()`, `assertNotFound()`) instead of `assertStatus()`:
+
+
+```php
+it('returns all', function () {
+ $this->postJson('/api/docs', [])->assertSuccessful();
+});
+```
+
+| Use | Instead of |
+|-----|------------|
+| `assertSuccessful()` | `assertStatus(200)` |
+| `assertNotFound()` | `assertStatus(404)` |
+| `assertForbidden()` | `assertStatus(403)` |
+
+## Mocking
+
+Import mock function before use: `use function Pest\Laravel\mock;`
+
+## Datasets
+
+Use datasets for repetitive tests (validation rules, etc.):
+
+
+```php
+it('has emails', function (string $email) {
+ expect($email)->not->toBeEmpty();
+})->with([
+ 'james' => 'james@laravel.com',
+ 'taylor' => 'taylor@laravel.com',
+]);
+```
+
+## Pest 4 Features
+
+| Feature | Purpose |
+|---------|---------|
+| Browser Testing | Full integration tests in real browsers |
+| Smoke Testing | Validate multiple pages quickly |
+| Visual Regression | Compare screenshots for visual changes |
+| Test Sharding | Parallel CI runs |
+| Architecture Testing | Enforce code conventions |
+
+### Browser Test Example
+
+Browser tests run in real browsers for full integration testing:
+
+- Browser tests live in `tests/Browser/`.
+- Use Laravel features like `Event::fake()`, `assertAuthenticated()`, and model factories.
+- Use `RefreshDatabase` for clean state per test.
+- Interact with page: click, type, scroll, select, submit, drag-and-drop, touch gestures.
+- Test on multiple browsers (Chrome, Firefox, Safari) if requested.
+- Test on different devices/viewports (iPhone 14 Pro, tablets) if requested.
+- Switch color schemes (light/dark mode) when appropriate.
+- Take screenshots or pause tests for debugging.
+
+
+```php
+it('may reset the password', function () {
+ Notification::fake();
+
+ $this->actingAs(User::factory()->create());
+
+ $page = visit('/sign-in');
+
+ $page->assertSee('Sign In')
+ ->assertNoJavaScriptErrors()
+ ->click('Forgot Password?')
+ ->fill('email', 'nuno@laravel.com')
+ ->click('Send Reset Link')
+ ->assertSee('We have emailed your password reset link!');
+
+ Notification::assertSent(ResetPassword::class);
+});
+```
+
+### Smoke Testing
+
+Quickly validate multiple pages have no JavaScript errors:
+
+
+```php
+$pages = visit(['/', '/about', '/contact']);
+
+$pages->assertNoJavaScriptErrors()->assertNoConsoleLogs();
+```
+
+### Visual Regression Testing
+
+Capture and compare screenshots to detect visual changes.
+
+### Test Sharding
+
+Split tests across parallel processes for faster CI runs.
+
+### Architecture Testing
+
+Pest 4 includes architecture testing (from Pest 3):
+
+
+```php
+arch('controllers')
+ ->expect('App\Http\Controllers')
+ ->toExtendNothing()
+ ->toHaveSuffix('Controller');
+```
+
+## Common Pitfalls
+
+- Not importing `use function Pest\Laravel\mock;` before using mock
+- Using `assertStatus(200)` instead of `assertSuccessful()`
+- Forgetting datasets for repetitive validation tests
+- Deleting tests without approval
+- Forgetting `assertNoJavaScriptErrors()` in browser tests
+- Prefixing `Feature/` or `Unit/` in `{name}` when using `make:test`
diff --git a/.phpantom.toml b/.phpantom.toml
new file mode 100644
index 0000000..d88c891
--- /dev/null
+++ b/.phpantom.toml
@@ -0,0 +1 @@
+# $schema: https://github.com/AJenbo/phpantom_lsp/raw/main/config-schema.json
diff --git a/README.id.md b/README.id.md
new file mode 100644
index 0000000..55bbe31
--- /dev/null
+++ b/README.id.md
@@ -0,0 +1,601 @@
+# Arsitektur Antigravity: Laravel Modular Monolith
+
+[](https://laravel.com)
+[](https://www.php.net)
+[](https://vitejs.dev)
+[](https://pestphp.com)
+
+Selamat datang di repositori ini. Aplikasi ini dibangun menggunakan arsitektur **Pragmatic Domain-Driven Design (DDD)** yang ketat. Kami menyebutnya sebagai arsitektur "Antigravity" karena ia mencegah basis kode menjadi terlalu kompleks saat aplikasi berkembang pesat (scaling).
+
+---
+
+## 1. Stack Teknis
+
+- **Backend:** PHP 8.4+ & Laravel 13.0
+- **Frontend:** Vite, AlpineJS, Livewire, Tailwind CSS
+- **Database:** SQLite (default), MySQL, atau PostgreSQL
+- **Testing:** Pest PHP
+- **Package Managers:** Composer (PHP), NPM (JS)
+
+---
+
+## 2. Persyaratan
+
+- **PHP:** ^8.4
+- **Node.js:** Disarankan LTS terbaru
+- **Composer:** ^2.0
+- **Ekstensi:** `ext-zip`, `ext-pdo_sqlite` (jika menggunakan SQLite)
+
+---
+
+## 3. Setup & Instalasi
+
+Proyek ini menyediakan skrip setup terpadu di `composer.json`.
+
+```bash
+# 1. Clone repositori
+git clone
+cd laravel-base
+
+# 2. Jalankan setup otomatis
+# Ini akan menginstal dependensi PHP/JS, membuat file .env, generate key, dan menjalankan migrasi
+composer setup
+```
+
+---
+
+## 4. Menjalankan Aplikasi
+
+Gunakan perintah pengembangan untuk menjalankan server, listener antrean, log, dan Vite secara bersamaan:
+
+```bash
+composer dev
+```
+
+Aplikasi akan tersedia di `http://localhost:8000`.
+
+---
+
+## 5. Skrip & Perintah
+
+Tersedia via Composer:
+- `composer setup`: Bootstrap awal proyek.
+- `composer dev`: Memulai lingkungan pengembangan (Server + Queue + Logs + Vite).
+- `composer test`: Menjalankan suite pengujian (test suite).
+- `php artisan domain:make`: Generator kustom untuk arsitektur DDD (lihat Bagian 12).
+
+Tersedia via NPM:
+- `npm run dev`: Memulai dev server Vite.
+- `npm run build`: Build aset untuk produksi.
+
+---
+
+## 6. Struktur Proyek
+
+```text
+.
+├── app/
+│ ├── Attributes/ <-- Atribut PHP 8 (SEO, Layout)
+│ ├── Domains/ <-- Logika Bisnis Inti (The Vault)
+│ ├── Http/ <-- Gateway Web/API (Controllers, Requests, DataTables)
+│ ├── Livewire/ <-- Komponen & Form Livewire
+│ ├── Providers/ <-- Service Providers
+│ └── UI/ <-- Logika spesifik UI (Actions, Enums)
+├── bootstrap/ <-- Logika bootstrap aplikasi
+├── config/ <-- File konfigurasi Laravel
+├── database/ <-- Migrasi, factory, dan seeder
+├── public/ <-- Entry point server web (index.php) dan aset statis
+├── resources/
+│ ├── lang/ <-- File lokalisasi
+│ ├── views/ <-- Template Blade & komponen Livewire
+│ └── css/js/ <-- Aset sumber frontend
+├── routes/ <-- Rute Web, API, dan Konsol
+├── tests/ <-- Pest test suite
+├── storage/ <-- Log, unggahan file, dan cache
+└── vite.config.js <-- Konfigurasi Vite
+```
+
+---
+
+## 7. Filosofi Inti
+
+Arsitektur ini memberlakukan batasan fisik yang keras antara **Delivery** (bagaimana pengguna berinteraksi dengan aplikasi) dan **Business Logic** (apa yang sebenarnya dilakukan aplikasi).
+
+* **The Gateway (Lapisan HTTP):** Controllers, komponen Livewire, dan Volt murni bertindak sebagai pengatur lalu lintas. Mereka menangani sesi web, cookie, pengalihan (redirect), dan validasi form.
+* **The Domain (The Vault):** Actions, DTOs, dan Models di dalam `app/Domains/` menangani aturan bisnis yang sebenarnya, mutasi database, dan pemanggilan API eksternal.
+
+> **Aturan Emas:** Domain harus benar-benar terisolasi dari lapisan web. Anda tidak diperbolehkan menggunakan helper `request()`, `session()`, atau melempar eksepsi HTTP di dalam direktori `app/Domains/`.
+
+---
+
+## 8. Struktur Direktori
+
+Aplikasi ini dibagi berdasarkan **Konsep Bisnis**, bukan fitur teknis.
+
+```text
+app/
+├── Attributes/ <-- Atribut PHP 8 (misal, #[Seo], #[LayoutData])
+├── Console/
+│ ├── Commands/ <-- Perintah Artisan kustom (DomainMakeCommand, CleanOrphanedFiles)
+│ └── stubs/ <-- Stub pembuatan kode kustom
+├── Http/ <-- The Gateway (Lapisan HTTP)
+│ ├── Controllers/
+│ │ ├── Api/
+│ │ │ └── V1/ <-- Kontroler API berversi
+│ │ └── Web/
+│ │ ├── Auth/ <-- Kontroler autentikasi
+│ │ ├── Identity/ <-- Kontroler manajemen pengguna & peran
+│ │ └── Account/ <-- Kontroler manajemen profil
+│ ├── DataTables/ <-- Konfigurasi DataTable Livewire
+│ ├── Ingestion/ <-- Kelas Impor/Ingesti Excel
+│ ├── Middleware/ <-- HandlePreferredLanguage, HandleSeoSetting, dll.
+│ ├── Requests/
+│ │ ├── Api/ <-- Form request untuk API
+│ │ └── Web/ <-- Form request untuk Web
+│ └── Resources/ <-- Resource API (LookupResource, SuccessResource, dll.)
+├── Livewire/
+│ ├── Concerns/ <-- Trait Livewire yang digunakan bersama (WithModal, WithToast)
+│ └── Forms/ <-- Objek Form Livewire
+├── Providers/ <-- AppServiceProvider, EventServiceProvider, UiServiceProvider
+├── UI/
+│ ├── Actions/ <-- Actions di lapisan UI (SetSeoMetadata, ApplyLayoutMetadata)
+│ ├── Enums/ <-- Enum spesifik UI (FileType, InputType)
+│ └── Support/ <-- Kelas pembantu UI (LayoutState, StyledExport)
+└── Domains/
+ ├── Identity/ <-- Konsep Bisnis: Autentikasi & Pengguna
+ │ ├── Actions/ <-- Mutasi yang dikelompokkan berdasarkan kapabilitas
+ │ ├── DTOs/ <-- Data Transfer Objects yang dikelompokkan berdasarkan kapabilitas
+ │ ├── Enums/
+ │ ├── Events/ <-- Fakta masa lalu
+ │ ├── Exports/
+ │ ├── Integration/ <-- Mapper sistem eksternal
+ │ ├── Listeners/ <-- Handler kata kerja aktif
+ │ ├── Models/ <-- User, Role, Permission
+ │ ├── Notifications/
+ │ ├── Policies/
+ │ ├── Providers/
+ │ ├── Queries/ <-- Bacaan Kompleks
+ │ └── Scopes/ <-- Global Scopes Eloquent
+ ├── Account/ <-- Konsep Bisnis: Profil & Tagihan
+ │ ├── Actions/
+ │ ├── DTOs/
+ │ ├── Enums/
+ │ ├── Listeners/
+ │ ├── Models/
+ │ └── Providers/
+ └── System/ <-- Konsep Bisnis: Infrastruktur Lintas Domain
+ ├── Actions/
+ ├── Casts/ <-- Casts Eloquent kustom
+ ├── DTOs/
+ ├── Enums/
+ ├── Events/
+ ├── Helpers/ <-- Helper spesifik domain (asset.php)
+ ├── Jobs/ <-- Background job spesifik domain
+ ├── Listeners/
+ ├── Mail/ <-- Mailable spesifik domain
+ ├── Models/ <-- File, SystemSettings, Backup
+ ├── Policies/
+ ├── Providers/ <-- SystemServiceProvider
+ ├── Queries/ <-- GetSystemSettings, GetModelAuditLog
+ ├── Support/
+ ├── Traits/ <-- Trait spesifik domain (HasFile)
+ └── ...
+```
+
+---
+
+## 9. Aturan Pelaksanaan (Rules of Engagement)
+
+### DTOs (Data Transfer Objects)
+
+Semua data yang tidak dapat dipercaya dari Gateway harus dikemas ke dalam DTO *readonly* dengan pengetikan yang ketat (strictly typed) sebelum memasuki Domain.
+
+* DTO hanya berisi data yang ditujukan untuk mengubah state.
+* Jangan masukkan model Eloquent ke dalam DTO. Lewatkan Model sebagai parameter terpisah ke Action.
+
+### Actions (Sang Eksekutor)
+
+Actions adalah satu-satunya tempat di mana mutasi database (`create`, `update`, `delete`, `syncRoles`) diizinkan.
+
+* Actions harus memiliki tanggung jawab tunggal (single responsibility).
+* Gunakan `DB::transaction()` di dalam Actions saat beberapa penulisan database (misalnya, membuat pengguna dan menetapkan peran Spatie) harus berhasil atau gagal secara bersamaan.
+* Gunakan **Action Composition** (menyuntikkan satu Action ke Action lainnya melalui konstruktor) untuk menggunakan kembali logika tanpa menduplikasi kode.
+
+### Events & Listeners
+
+Gunakan Arsitektur Event-Driven untuk semua efek samping (side effects) seperti email, logging, dan pemrosesan latar belakang (background processing).
+
+* Gateway mendispatch Events untuk fakta sesi yang tidak memutasi (misalnya, `UserLoggedIn`).
+* Actions mendispatch Events segera setelah mutasi status berhasil (misalnya, `UserWasProvisioned`).
+* Listeners menangani reaksi di luar siklus hidup HTTP utama dan merupakan **satu-satunya** tempat di mana pemanggilan `Notification::send()`, `Mail::send()`, atau logging dilakukan sebagai respons terhadap Event Domain.
+* Events dan Listeners harus **dikelompokkan di bawah folder kapabilitas yang sama** dengan Action yang mendispatch mereka (misalnya, `Events/Onboarding/`, `Listeners/Onboarding/`).
+
+---
+
+## 10. Konvensi Penamaan
+
+Arsitektur ini menggunakan bahasa penamaan yang ketat dan disengaja. Setiap nama harus mengomunikasikan **Niat Bisnis (Business Intent)**, bukan operasi database.
+
+### 10.1 Folder Domain (`app/Domains/{Name}/`)
+
+Nama domain adalah **Konsep Bisnis**, bukan lapisan teknis. Harus berupa kata benda tunggal yang mendeskripsikan *bounded context*.
+
+| ✅ Benar | ❌ Salah | Alasan |
+|---|---|---|
+| `Identity` | `Users` | Identity mencakup autentikasi, peran, dan siklus hidup pengguna — bukan sekadar nama tabel. |
+| `Account` | `Profile` | Account memiliki seluruh permukaan akun pengguna, bukan hanya satu model. |
+| `System` | `Utils` / `Helpers` | System adalah konteks bisnis nyata untuk infrastruktur lintas-potong (cross-cutting). |
+
+### 10.2 Folder Kapabilitas (Subdirektori Action / DTO / Event / Listener)
+
+Subdirektori di dalam `Actions/`, `DTOs/`, `Events/`, and `Listeners/` harus dinamai berdasarkan **Kapabilitas Bisnis**, bukan kata benda database.
+
+| ✅ Benar | ❌ Salah | Alasan |
+|---|---|---|
+| `Onboarding/` | `Users/` | Mendeskripsikan tahap siklus hidup, bukan tabel DB. |
+| `AccessControl/` | `Roles/` | Mendeskripsikan kapabilitas, bukan nama resource. |
+| `Governance/` | `Admin/` | Mendeskripsikan niat kepatuhan/pengawasan. |
+| `Passwords/` | `Auth/` | Ruang lingkup yang sempit dan presisi. |
+| `Registration/` | `Signup/` | Menggunakan bahasa formal sistem. |
+
+**Aturan:** Jika nama folder juga merupakan nama Model Eloquent yang valid, maka penamaan tersebut salah.
+
+### 10.3 Nama Kelas Action
+
+Actions harus dinamai berdasarkan **Niat Bisnis yang spesifik** yang mereka penuhi. Gunakan pola kata kerja aktif + kata benda bisnis.
+
+| ✅ Benar | ❌ Salah | Alasan |
+|---|---|---|
+| `ProvisionNewUser` | `CreateUser` | Mendeskripsikan *siapa* yang memicu dan *mengapa*. |
+| `SuspendUser` | `DeleteUser` | Mengungkapkan konsekuensi bisnis (pencabutan lunak, bukan penghancuran). |
+| `UpdateUserRole` | `SaveRole` | Eksplisit mengenai subjek dan properti yang diubah. |
+| `RegisterUser` | `StoreUser` | Bahasa domain, bukan bahasa HTTP verb. |
+| `SendPasswordResetLink` | `ResetPassword` | Mencerminkan efek samping sebenarnya yang dipicu. |
+
+Nama-nama CRUD (`CreateCategory`, `UpdateSetting`) hanya dapat diterima untuk tabel pencarian sepele (lookup tables) yang **tidak memiliki efek samping**.
+
+### 10.4 Nama Kelas DTO
+
+DTO dinamai berdasarkan Action yang mereka layani, dengan akhiran `DTO`.
+
+| Action | DTO |
+|---|---|
+| `ProvisionNewUser` | `ProvisionUserDTO` |
+| `UpdateUser` | `UpdateUserDTO` |
+| `CreateSystemRole` | `CreateRoleDTO` |
+
+### 10.5 Nama Kelas Event
+
+Events adalah **fakta masa lalu (past-tense)** mengenai sesuatu yang telah terjadi dalam domain. Nama kelas harus secara gramatikal menyatakan kebenaran yang sudah selesai.
+
+| ✅ Benar | ❌ Salah | Alasan |
+|---|---|---|
+| `UserWasProvisioned` | `UserProvisioned` | Bentuk *past-tense* eksplisit menghilangkan ambiguitas. |
+| `UserWasSuspended` | `UserSuspended` | Membaca sebagai state, bukan sebagai fakta yang sudah selesai. |
+| `UserLoggedIn` | `LoginEvent` | Pola kata benda + kata kerja; menghindari akhiran `Event`. |
+| `UserEmailVerified` | `EmailVerification` | Mendeskripsikan tindakan yang telah selesai. |
+
+**Aturan:** Jangan pernah mengakhiri Events dengan `Event` (misal, `UserRegisteredEvent` itu salah). Namespace `Events\` sudah mengomunikasikan jenisnya.
+
+### 10.6 Nama Kelas Listener
+
+Listeners mendeskripsikan **reaksi aktif** terhadap sebuah event menggunakan frasa kata kerja imperatif.
+
+| ✅ Benar | ❌ Salah | Alasan |
+|---|---|---|
+| `SendSignInActivityNotification` | `UserLoggedInListener` | Mendeskripsikan apa yang *dilakukan* listener, bukan apa yang diresponsnya. |
+| `DispatchWelcomeNotification` | `WelcomeListener` | Kata kerja imperatif membuat niat menjadi sangat jelas. |
+
+**Aturan:** Jangan pernah mengakhiri Listeners dengan `Listener` di nama kelasnya. Namespace `Listeners\` sudah mengomunikasikan jenisnya.
+
+---
+
+## 11. Prompt Agen AI (Instruksi Sistem)
+
+**Untuk Pengembang:** Salin dan tempel blok di bawah ini ke obrolan agen AI Anda atau instruksi sistem sebelum memintanya untuk menulis atau merefaktor kode di repositori ini.
+
+```text
+You are an autonomous Senior Laravel Architect specializing in Pragmatic Domain-Driven Design (DDD) and Event-Driven Architecture. You must strictly obey the "Antigravity" rules of this repository.
+
+### 1. The HTTP Gateway (Delivery Layer)
+- Lives in `app/Http/Controllers/`, `app/Http/DataTables/`, or `app/Livewire/`.
+- Responsibilities: HTTP validation, rate limiting, session management (`Auth::login`, `session()->regenerate()`), and redirects.
+- HARD RESTRICTION: The Gateway MUST NEVER call `Model::create()`, `Model::update()`, or `Hash::make()`. It must map validated data into a DTO and pass it to a Domain Action.
+
+### 2. The Domain (Business Logic)
+- Lives inside `app/Domains/{Concept}/`.
+- Responsibilities: DTO definitions, Actions (database writes), Models, and Events.
+- HARD RESTRICTION: The Domain MUST NEVER read from the `request()` helper, manipulate cookies/sessions, or throw HTTP-specific exceptions (like `ValidationException`).
+
+### 3. Execution & Workflow
+- DTOs: Must be strictly typed readonly classes.
+- Actions: Must represent a specific Business Intent (e.g., `ProvisionNewUser` not `SaveUser`). Actions that perform multiple database writes must wrap them in a `DB::transaction()`.
+- Action Composition: Inject Actions into other Actions via the constructor to reuse logic (e.g., injecting `AccessControl\UpdateUserRole` into `Onboarding\ProvisionNewUser`).
+- Events: Use Events to decouple side effects (Notifications, Activity Logs). Events must be **past-tense** (e.g., `UserWasProvisioned` not `UserProvisioned`).
+
+### 4. Naming Rules
+- **Domains**: Singular Business Concepts, never database nouns (✅ `Identity` ❌ `Users`).
+- **Capability Folders**: Name subdirectories after business capabilities, never after models (✅ `Onboarding/`, `AccessControl/`, `Governance/` ❌ `Users/`, `Roles/`).
+- **Actions**: Active verb + business noun (✅ `ProvisionNewUser` ❌ `CreateUser`).
+- **Events**: Past-tense completed facts, no `Event` suffix (✅ `UserWasProvisioned` ❌ `UserProvisionedEvent`).
+- **Listeners**: Imperative active-verb phrases, no `Listener` suffix (✅ `SendSignInActivityNotification` ❌ `UserLoggedInListener`).
+
+### 5. File Generation Rules
+* NEVER use standard Laravel generators (e.g., `php artisan make:model`) for Domain classes.
+* ALWAYS use the custom `domain:make` command to create Domain files.
+* Example: `php artisan domain:make action Identity Onboarding/ProvisionNewUser`
+* Supported types: `model`, `action`, `dto`, `enum`, `event`, `listener`, `notification`, `policy`, `query`, `provider`, `export`, `mapper`, `scope`, `trait`, `mailable`.
+* Examples for the Integration layer:
+ * `php artisan domain:make export Identity UserExport --model=User`
+ * `php artisan domain:make mapper Identity User` → generates `Integration/Mappers/UserDataMapper.php`
+* Excel ingestion (Import) classes live in the **Gateway layer** at `app/Http/Ingestion/Excel/` — do NOT generate them with `domain:make`.
+* Queries: For complex database reads (e.g., massive filtering or reporting), create a Query class in app/Domains/{Concept}/Queries/. Queries are read-only, do not use transactions, do not mutate state, and do not dispatch events.
+
+### 6. Testing Strategy
+* Always write tests using Pest PHP.
+* When generating new features (Actions/DTOs/Models), create corresponding tests in `tests/Feature/` or `tests/Unit/`.
+* Ensure 100% adherence to Architectural tests (check `tests/Architecture/` for existing rules).
+* When writing tests, use `Event::fake()`, `Queue::fake()`, `Notification::fake()` to isolate side effects.
+* For dependency-injected services, use `$this->mock()` to define expected behaviors.
+
+Write modern PHP 8.4+ code with strict typing. Ensure all PSR-4 namespaces perfectly match the directory structure.
+```
+
+---
+
+## 12. Alat Pengembangan & Generator
+
+Untuk mempertahankan struktur folder yang ketat dari arsitektur Antigravity, **jangan gunakan perintah `make:` standar (seperti `make:model`) untuk file Domain.** Gunakan perintah kustom `domain:make` untuk menghasilkan kelas di namespace `app/Domains/` yang benar.
+
+### Perintah `domain:make`
+
+**Signature:**
+
+```bash
+php artisan domain:make {type} {domain} {name} [options]
+
+```
+
+**Argumen:**
+
+* `type`: Jenis file yang akan dibuat. Didukung: `model`, `action`, `dto`, `enum`, `event`, `listener`, `notification`, `policy`, `scope`, `trait`, `query`, `provider`, `export`, `mapper`, `mailable`.
+* `domain`: Folder Domain target (misal, `Identity`, `Account`, `System`).
+* `name`: Nama kelas. Mendukung pengelompokan sub-direktori (misal, `Management/ProvisionNewUser`).
+
+**Opsi:**
+
+* `--factory`: Menghasilkan factory database terkait (Hanya Models).
+* `--migration`: Menghasilkan file migrasi database (Hanya Models).
+* `--model=`: Mengaitkan kelas ekspor dengan model Eloquent (Hanya Exports).
+
+### Menyesuaikan Generator (Stubs)
+
+Semua template `domain:make` disimpan sebagai file `.stub` di `app/Console/stubs/domain-make/`. Anda dapat dengan bebas mengedit stub ini untuk menyesuaikan *boilerplate* default dengan kebutuhan spesifik proyek Anda (misalnya, mengubah metode default di DTO).
+
+---
+
+## 13. Manajemen File Universal (Domain System)
+
+Penanganan file (unggahan, lampiran, pemangkasan gambar, dan penghapusan) adalah kemampuan yang dibagikan secara universal. Untuk mencegah setiap domain menulis logika penyimpanan filenya sendiri, semua file fisik dikelola oleh mesin terpusat di dalam domain **`System`**.
+
+### Mesin Polimorfik
+
+Kami tidak menambahkan path file secara langsung ke tabel bisnis (misalnya, tidak ada kolom `avatar_path` di tabel `users`). Alih-alih, kami menggunakan tabel `files` terpusat dan model `System\Models\File`.
+
+* File dilampirkan secara **polimorfik** ke entitas apa pun di dalam aplikasi.
+* Tabel `files` menyertakan kolom string `relation_name` bertipe ketat (misalnya, `'avatar'`) untuk mencegah terjadinya tabrakan antar jenis file yang dilampirkan ke model yang sama.
+
+### Trait Konsumen
+
+Ketika Domain Model (seperti `User` atau `Invoice`) perlu menerima lampiran file, model tersebut akan menarik trait `HasFile`. Trait ini menyediakan pembuat relasi terisolasi.
+
+```php
+namespace App\Domains\Identity\Models;
+
+use Illuminate\Database\Eloquent\Model;
+use App\Domains\System\Traits\HasFile;
+use Illuminate\Database\Eloquent\Relations\MorphOne;
+
+class User extends Model
+{
+ use HasFile;
+
+ // String 'avatar' secara sempurna mengisolasi file ini di dalam database
+ public function avatar(): MorphOne
+ {
+ return $this->singleFile('avatar');
+ }
+}
+
+```
+
+### File Action (Metadata via DTO)
+
+Karena objek `Illuminate\Http\UploadedFile` milik Laravel merupakan objek yang kompleks, kami melewatkannya bersama dengan `FileDTO` yang diketik secara ketat (strictly-typed) yang berisi metadata (model target, disk, dan nama relasi). Ini memastikan Gateway tetap bersih sementara Domain menerima semua konteks yang diperlukan.
+
+* **`UploadAndAttachFile`**: Action dasar. Ini menyimpan file fisik ke disk dan membuat rekaman database polimorfik.
+* **`ReplaceSingleFile`**: Digunakan untuk penggantian 1-ke-1 (seperti mengganti avatar). Ia menghapus file lama secara aman sebelum mendelegasikan unggahan baru kembali ke Action dasar.
+
+**Contoh Gateway:**
+
+```php
+$action->execute(
+ newFile: $request->file('photo'),
+ dto: new FileDTO(
+ modelType: $user->getMorphClass(),
+ modelId: $user->id,
+ relationName: 'avatar',
+ disk: 'local',
+ directory: 'avatars',
+ options: [],
+ uploaderId: auth()->id(),
+ )
+);
+
+```
+
+### Helper Asset Sistem
+
+Untuk menghindari polusi namespace global Laravel dengan file `app/helpers.php` sampah, kami mengelola file helper yang terikat ketat pada domain di `app/Domains/System/Helpers/asset.php`. File ini di-autoload via `composer.json` dan menyediakan fungsi `asset_static()` untuk menyelesaikan (resolve) file publik dan privat di view Blade kita secara elegan.
+
+---
+
+## 14. Pengaturan Global & State Aplikasi
+
+Pengaturan yang mendikte *state* (status) *runtime* aplikasi (Zona Waktu, Lokalisasi, Tag SEO) dikelola oleh domain `System` untuk memastikan performa tinggi dan kesadaran konteks.
+
+* **Memoization & Singletons:** Query `GetSystemSettings` didaftarkan sebagai Singleton di `SystemServiceProvider`. Ia mengambil data dari database/cache *sekali* saja dan menyimpannya di memori lokal PHP selama durasi request berjalan.
+* **Contextual Middlewares:** Kami menggunakan *middleware* terdedikasi (`HandlePreferredLanguage`, `HandlePreferredTimezone`) untuk memeriksa preferensi pengguna yang terautentikasi secara dinamis, lalu menggunakan (fallback ke) pengaturan global jika tidak ada preferensi yang ditemukan.
+* **View Composers:** Variabel *layout* global (seperti Logo dan Nama Web) disuntikkan ke global via View Composers di dalam Service Provider, untuk mencegah penggunaan direktif `@inject` yang berulang.
+
+---
+
+## 15. Pencatatan Audit (Audit Logging) & Pelacakan
+
+Semua mutasi database yang penting dilacak untuk mempertahankan buku besar (ledger) historis yang patuh (compliant).
+
+* **Model Auditing:** Kami memanfaatkan Eloquent event untuk secara otomatis mencatat perubahan baris.
+* **Complex Relation Auditing:** Melacak perubahan hubungan many-to-many (seperti `syncPermissions` milik Spatie) akan melewati (bypass) standar Eloquent event. Oleh karena itu, kami secara eksplisit mendispatch Custom Audit Event langsung di dalam Domain Action terkait (misalnya, `UpdateRolePermissions`). Ini menjamin state "Sebelum" dan "Sesudah" terekam dengan rapi di dalam satu baris transaksional tunggal.
+
+---
+
+## 16. UI Dinamis & Interoperabilitas Livewire
+
+Saat membangun antarmuka berbasis data (seperti formulir pengaturan dinamis), kami memanfaatkan pola **Renderable Enum** dikombinasikan dengan komponen dinamis bawaan Laravel.
+
+* Enum bertindak sebagai **Penyedia Metadata** (mengembalikan nama string dari komponen Blade target).
+* Kami menggunakan `` di dalam file Blade untuk menukar elemen UI secara dinamis.
+* **KRITIS:** Enum tidak boleh sama sekali mengembalikan string HTML mentah yang dikompilasi melalui *facade* `Blade`. Melakukan hal tersebut akan merusak mesin DOM-diffing milik Livewire dan memutuskan binding `wire:model`.
+
+---
+
+## 17. Ekspor & Impor Excel
+
+Komponen Laravel `resources/views/components/datatables/⚡excel-manager.blade.php` (didaftarkan sebagai ``) menyediakan mekanisme berbasis *queue* (antrean) yang dapat digunakan kembali untuk mengimpor dan mengekspor file Excel pada halaman DataTable apa pun. Ini adalah **komponen Laravel file tunggal terpadu** — logika kelas PHP dan templat Blade berdampingan pada file yang sama, mengikuti konvensi penamaan `⚡` yang digunakan di seluruh komponen Laravel dalam proyek ini.
+
+### Gambaran Arsitektur
+
+Komponen ini bergantung pada tiga lapisan yang berkolaborasi:
+
+1. **Komponen Laravel** (`⚡excel-manager.blade.php`) — Menangani *state* UI, unggahan file melalui `WithFilePond`, validasi, dan Livewire *event listeners*. Semua *props* diamankan menggunakan `#[Locked]` untuk mencegah perusakan (tampering) di sisi klien.
+2. **Dekorator `StyledExport`** (`App\UI\Support\Excel\StyledExport`) — *Wrapper* lapisan UI yang memperkaya *Export* domain apa pun dengan penataan gaya (styling) visual yang terstandar (baris *header* yang dibekukan, orientasi *landscape*, tepi yang tipis, penyejajaran tengah, dan kolom *auto-sized*) tanpa mencemari kelas *Export* domain dengan logika presentasi.
+3. **Pipeline Pemberitahuan Event-Driven** — Setelah file ekspor ditulis ke *disk*, tugas antrean `NotifyExportReady` mendispatch event `ExportCompleted`, yang ditangani oleh *listener* `SendExportReportEmail` untuk mengirimkan file melalui email.
+
+### Cara Kerjanya
+
+* **Impor:** Pengguna mengunggah file `.xlsx` melalui modal FilePond. File disimpan ke `local/excel/import/{resourceName}` dan sebuah instansiasi impor baru dikonstruksi menggunakan UUID (`$importId`) dan ID pengguna yang terautentikasi (`$initiatorId`) sebelum didispatch ke antrean menggunakan `Excel::queueImport()`. Kelas Ingesti (Ingestion) berada di `app/Http/Ingestion/` (Lapisan Gateway) dan mengimplementasikan `WithChunkReading` (ukuran *chunk*: 200 baris) agar tetap berada di dalam batas memori *shared-hosting*. Sebuah toast peringatan (`ui.excel.import.success`) dimunculkan seketika setelah masuk antrean.
+
+* **Ekspor:** Sebuah Livewire browser event (`export-excel`) — didispatch oleh tombol Export DataTable — memicu metode `export()` melalui atribut `#[On('export-excel')]`. Kelas Export domain dibungkus (wrapped) di dalam `StyledExport` lalu didispatch menggunakan `Excel::queue()`. Job chain menambahkan `NotifyExportReady`, yang kemudian mendispatch event `ExportCompleted`, yang pada akhirnya ditangani oleh listener `SendExportReportEmail` untuk mengirim file tersebut sebagai lampiran email ke pengguna. Sebuah toast sukses (`ui.excel.export.success`) dimunculkan dengan segera setelah masuk antrean.
+
+### Properti Komponen (Props)
+
+| Prop | Tipe | Deskripsi |
+| --- | --- | --- |
+| `importClass` | `string` | Nama kelas kualifikasi penuh (fully-qualified) dari Gateway Ingestion class (contoh, `App\Http\Ingestion\Excel\Identity\UserImport`). |
+| `exportClass` | `string` | Nama kelas kualifikasi penuh dari domain Export (contoh, `App\Domains\Identity\Exports\UserExport`). |
+| `resourceName` | `string` | Sebuah kata (slug) yang digunakan untuk menamai file impor yang disimpan serta nama file ekspor yang bertanda waktu (contoh, `user`). |
+
+### Penggunaan
+
+Sematkan komponen di dalam halaman DataTable (Blade view) mana pun. Semua props harus disediakan berupa string nama kelas PHP yang terkualifikasi secara penuh:
+
+```blade
+
+```
+
+Tombol Export milik DataTable harus mendispatch event Livewire `export-excel`, sedangkan tombol Import harus membuka Bootstrap modal `#excel-import-modal`:
+
+```php
+// Di dalam file builder html() DataTable Anda:
+Button::make('excel')
+ ->action("Livewire.dispatch('export-excel')"),
+
+Button::make('excel')
+ ->action("$('#excel-import-modal').modal('show')"),
+```
+
+### Menghasilkan Kelas Export & Mapper
+
+Gunakan perintah `domain:make` untuk membuat kelas lapisan Export dan Integration:
+
+```bash
+# Menghasilkan kelas Export domain
+php artisan domain:make export Identity UserExport --model=User
+
+# Menghasilkan Integration Mapper (akan meng-append akhiran DataMapper secara otomatis)
+php artisan domain:make mapper Identity User
+# → app/Domains/Identity/Integration/Mappers/UserDataMapper.php
+```
+
+Kelas Export Domain wajib mengimplementasikan `FromQuery & WithHeadings & WithMapping & WithColumnFormatting`. Dekorator `StyledExport` akan menerapkan semua format visual secara otomatis saat diproses antrean — **jangan** mengimplementasikan `WithStyles` secara langsung di kelas Export domain.
+
+> **Lapisan Gateway:** Kelas Excel Ingestion (Import) berada di dalam `app/Http/Ingestion/Excel/` dan **tidak** dihasilkan oleh generator `domain:make`. Buatlah secara manual atau gunakan `make:class` sebagai kelas PHP standar yang mengimplementasikan `ToCollection`, `WithHeadingRow`, dan `WithChunkReading`.
+
+### Pipeline Pemberitahuan
+
+Alur pemberitahuan ekspor mengikuti rantai *event-driven* berbasis antrean secara ketat:
+
+```
+Excel::queue(StyledExport, $path)
+ └─> NotifyExportReady (Job) [app/Domains/System/Jobs/]
+ └─> ExportCompleted::dispatch (Event) [app/Domains/System/Events/]
+ └─> SendExportReportEmail (Listener) [app/Domains/System/Listeners/]
+ └─> ExcelExportEmail (Mailable) [app/Domains/System/Mail/]
+```
+
+Pemberitahuan impor akan dikirim langsung oleh kelas Import domain itu sendiri sesaat setelah penyelesaian, dengan menggunakan `ExcelImportEmail` dari namespace `App\Domains\System\Mail\` yang sama.
+
+### Mailables
+
+Kedua kelas Mailable berada pada **domain System**, bukan di namespace utama `App\Mail\`:
+
+* **`App\Domains\System\Mail\ExcelImportEmail`** — Dikirim saat unggahan *queued import* telah selesai. Menggunakan terjemahan dari `domains/system.notifications.excel.import_email.*`.
+* **`App\Domains\Identity\Mail\Registration\WelcomeEmail`** — Contoh mailable spesifik domain.
+* **`App\Domains\System\Mail\ExcelExportEmail`** — Dikirim saat *queued export* siap, dilampirkan menggunakan file dari disk `local`. Menggunakan terjemahan dari `domains/system.notifications.excel.export_email.*`.
+
+### Kunci Terjemahan (Translation Keys)
+
+| File | Kunci (Key) | Tujuan |
+| --- | --- | --- |
+| `lang/{locale}/ui.php` | `ui.excel.import.file_label` | Label unggahan FilePond di dalam modal impor. |
+| `lang/{locale}/ui.php` | `ui.excel.import.success` | Pesan Toast muncul setelah impor masuk antrean. |
+| `lang/{locale}/ui.php` | `ui.excel.export.success` | Pesan Toast muncul setelah ekspor masuk antrean. |
+| `lang/{locale}/domains/system.php` | `notifications.excel.import_email.*` | Isi (body) email untuk pemberitahuan selesai impor. |
+| `lang/{locale}/domains/system.php` | `notifications.excel.export_email.*` | Isi (body) email untuk pemberitahuan ekspor sudah siap. |
+
+---
+
+## 18. Testing
+
+Proyek ini menggunakan **Pest PHP** untuk pengujian.
+
+```bash
+composer test
+```
+
+Tes terletak di direktori `tests/` dan mengikuti konvensi standar Laravel (Feature dan Unit).
+
+---
+
+## 19. Variabel Lingkungan (Environment Variables)
+
+Variabel utama yang digunakan di `.env`:
+
+- `APP_NAME`: Nama aplikasi.
+- `APP_ENV`: Lingkungan aplikasi (`local`, `production`, dll.).
+- `APP_KEY`: Kunci enkripsi aplikasi.
+- `DB_CONNECTION`: Driver database (`sqlite`, `mysql`, `pgsql`).
+- `QUEUE_CONNECTION`: Driver antrean (default: `database`).
+- `MAIL_MAILER`: Driver email (default: `log`).
+
+Lihat `.env.example` untuk daftar lengkap opsi yang tersedia.
+
+---
+
+## 20. Lisensi
+
+Proyek ini dilisensikan di bawah **Lisensi MIT**.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e104974
--- /dev/null
+++ b/README.md
@@ -0,0 +1,599 @@
+# Antigravity Architecture: Laravel Modular Monolith
+
+[](https://laravel.com)
+[](https://www.php.net)
+[](https://vitejs.dev)
+[](https://pestphp.com)
+
+Welcome to the repository. This application is built using a strict **Pragmatic Domain-Driven Design (DDD)** architecture. We refer to this as the "Antigravity" architecture because it prevents the codebase from collapsing under its own weight as it scales.
+
+---
+
+## 1. Technical Stack
+
+- **Backend:** PHP 8.4+ & Laravel 13.0
+- **Frontend:** Vite, AlpineJS, Livewire, Tailwind CSS
+- **Database:** SQLite (default), MySQL, or PostgreSQL
+- **Testing:** Pest PHP
+- **Package Managers:** Composer (PHP), NPM (JS)
+
+---
+
+## 2. Requirements
+
+- **PHP:** ^8.4
+- **Node.js:** Latest LTS recommended
+- **Composer:** ^2.0
+- **Extensions:** `ext-zip`, `ext-pdo_sqlite` (if using SQLite)
+
+---
+
+## 3. Setup & Installation
+
+The project includes a unified setup script in `composer.json`.
+
+```bash
+# 1. Clone the repository
+git clone
+cd laravel-base
+
+# 2. Run the automated setup
+# This installs PHP/JS deps, creates .env, generates key, and runs migrations
+composer setup
+```
+
+---
+
+## 4. Running the Application
+
+Use the pre-configured development command which runs the server, queue listener, logs, and Vite concurrently:
+
+```bash
+composer dev
+```
+
+The application will be available at `http://localhost:8000`.
+
+---
+
+## 5. Scripts & Commands
+
+Available via Composer:
+- `composer setup`: Initial project bootstrap.
+- `composer dev`: Start development environment (Server + Queue + Logs + Vite).
+- `composer test`: Run the test suite.
+- `php artisan domain:make`: Custom generator for the DDD architecture (see Section 12).
+
+Available via NPM:
+- `npm run dev`: Start Vite dev server.
+- `npm run build`: Build assets for production.
+
+---
+
+## 6. Project Structure
+
+```text
+.
+├── app/
+│ ├── Attributes/ <-- PHP 8 Attributes (SEO, Layout)
+│ ├── Domains/ <-- Core Business Logic (The Vault)
+│ ├── Http/ <-- Web/API Gateway (Controllers, Requests, DataTables)
+│ ├── Livewire/ <-- Livewire Components & Forms
+│ ├── Providers/ <-- Service Providers
+│ └── UI/ <-- UI-specific logic (Actions, Enums)
+├── bootstrap/ <-- App bootstrap logic
+├── config/ <-- Laravel configuration files
+├── database/ <-- Migrations, factories, and seeders
+├── public/ <-- Web server entry point (index.php) and static assets
+├── resources/
+│ ├── lang/ <-- Localization files
+│ ├── views/ <-- Blade templates & Livewire components
+│ └── css/js/ <-- Frontend source assets
+├── routes/ <-- Web, API, and Console routes
+├── tests/ <-- Pest test suite
+├── storage/ <-- Logs, file uploads, and cache
+└── vite.config.js <-- Vite configuration
+```
+
+---
+
+## 7. The Core Philosophy
+
+This architecture enforces a hard, physical boundary between **Delivery** (how the user interacts with the app) and **Business Logic** (what the app actually does).
+
+* **The Gateway (HTTP Layer):** Controllers, Livewire, and Volt components are purely traffic cops. They handle web sessions, cookies, redirects, and form validation.
+* **The Domain (The Vault):** Actions, DTOs, and Models inside `app/Domains/` handle the actual business rules, database mutations, and external API calls.
+
+> **The Golden Rule:** The Domain must remain completely ignorant of the web. You must never use `request()`, `session()`, or throw HTTP exceptions inside the `app/Domains/` directory.
+
+---
+
+## 8. Directory Structure
+
+The application is divided by **Business Concepts**, not technical features.
+
+```text
+app/
+├── Attributes/ <-- PHP 8 Attributes (e.g., #[Seo], #[LayoutData])
+├── Console/
+│ ├── Commands/ <-- Custom Artisan commands (DomainMakeCommand, CleanOrphanedFiles)
+│ └── stubs/ <-- Custom code generation stubs
+├── Http/ <-- The Gateway (HTTP Layer)
+│ ├── Controllers/
+│ │ ├── Api/
+│ │ │ └── V1/ <-- Versioned API controllers
+│ │ └── Web/
+│ │ ├── Auth/ <-- Authentication controllers
+│ │ ├── Identity/ <-- User & role management controllers
+│ │ └── Account/ <-- Profile management controllers
+│ ├── DataTables/ <-- Livewire DataTable configurations
+│ ├── Ingestion/ <-- Excel Import/Ingestion classes
+│ ├── Middleware/ <-- HandlePreferredLanguage, HandleSeoSetting, etc.
+│ ├── Requests/
+│ │ ├── Api/ <-- API form requests
+│ │ └── Web/ <-- Web form requests
+│ └── Resources/ <-- API resources (LookupResource, SuccessResource, etc.)
+├── Livewire/
+│ ├── Concerns/ <-- Shared Livewire traits (WithModal, WithToast)
+│ └── Forms/ <-- Livewire Form Objects
+├── Providers/ <-- AppServiceProvider, EventServiceProvider, UiServiceProvider
+├── UI/
+│ ├── Actions/ <-- UI-layer actions (SetSeoMetadata, ApplyLayoutMetadata)
+│ ├── Enums/ <-- UI-specific enums (FileType, InputType)
+│ └── Support/ <-- UI helper classes (LayoutState, StyledExport)
+└── Domains/
+ ├── Identity/ <-- Business Concept: Authentication & Users
+ │ ├── Actions/ <-- Capability-grouped mutations
+ │ ├── DTOs/ <-- Capability-grouped Data Transfer Objects
+ │ ├── Enums/
+ │ ├── Events/ <-- Past-tense truths
+ │ ├── Exports/
+ │ ├── Integration/ <-- External system mappers
+ │ ├── Listeners/ <-- Active-verb handlers
+ │ ├── Models/ <-- User, Role, Permission
+ │ ├── Notifications/
+ │ ├── Policies/
+ │ ├── Providers/
+ │ ├── Queries/ <-- Complex Reads
+ │ └── Scopes/ <-- Eloquent Global Scopes
+ ├── Account/ <-- Business Concept: Profiles & Billing
+ │ ├── Actions/
+ │ ├── DTOs/
+ │ ├── Enums/
+ │ ├── Listeners/
+ │ ├── Models/
+ │ └── Providers/
+ └── System/ <-- Business Concept: Cross-cutting Infrastructure
+ ├── Actions/
+ ├── Casts/ <-- Custom Eloquent casts
+ ├── DTOs/
+ ├── Enums/
+ ├── Events/
+ ├── Helpers/ <-- Domain-specific helpers (asset.php)
+ ├── Jobs/ <-- Domain-specific background jobs
+ ├── Listeners/
+ ├── Mail/ <-- Domain-specific mailables
+ ├── Models/ <-- File, SystemSettings, Backup
+ ├── Policies/
+ ├── Providers/ <-- SystemServiceProvider
+ ├── Queries/ <-- GetSystemSettings, GetModelAuditLog
+ ├── Support/
+ ├── Traits/ <-- Domain-specific traits (HasFile)
+ └── ...
+
+```
+
+---
+
+## 9. The Rules of Engagement
+
+### DTOs (Data Transfer Objects)
+
+All untrusted data from the Gateway must be packed into a strictly typed, readonly DTO before entering the Domain.
+
+* DTOs only contain data meant to change state.
+* Do not put Eloquent models inside DTOs. Pass the Model as a separate parameter to the Action.
+
+### Actions (The Executors)
+
+Actions are the only place database mutations (`create`, `update`, `delete`, `syncRoles`) are allowed.
+
+* Actions must have a single responsibility.
+* Use `DB::transaction()` inside Actions when multiple database writes (e.g., creating a user and assigning a Spatie role) must succeed or fail together.
+* Use **Action Composition** (injecting one Action into another via the constructor) to reuse logic without duplicating code.
+
+### Events & Listeners
+
+Use Event-Driven Architecture for all side effects (emails, logging, background processing).
+
+* The Gateway dispatches Events for non-mutating session facts (e.g., `UserLoggedIn`).
+* Actions dispatch Events immediately after a successful state mutation (e.g., `UserWasProvisioned`).
+* Listeners handle the reaction outside the main HTTP lifecycle and are the **only** place `Notification::send()`, `Mail::send()`, or logging calls are made in response to a Domain Event.
+* Events and Listeners must be **grouped under the same capability folder** as the Action that dispatches them (e.g., `Events/Onboarding/`, `Listeners/Onboarding/`).
+
+---
+
+## 10. Naming Conventions
+
+This architecture uses a strict, intentional naming language. Every name must communicate **Business Intent**, not database operations.
+
+### 10.1 Domain Folders (`app/Domains/{Name}/`)
+
+Domain names are **Business Concepts**, not technical layers. They must be a singular noun that describes a bounded context.
+
+| ✅ Correct | ❌ Wrong | Why |
+|---|---|---|
+| `Identity` | `Users` | Identity covers auth, roles, and user lifecycle — not just a table |
+| `Account` | `Profile` | Account owns the full user account surface, not one model |
+| `System` | `Utils` / `Helpers` | System is a real bounded context for cross-cutting infrastructure |
+
+### 10.2 Capability Folders (Action / DTO / Event / Listener subdirectories)
+
+Subdirectories inside `Actions/`, `DTOs/`, `Events/`, and `Listeners/` must be named after **Business Capabilities**, not database nouns.
+
+| ✅ Correct | ❌ Wrong | Why |
+|---|---|---|
+| `Onboarding/` | `Users/` | Describes the lifecycle stage, not the DB table |
+| `AccessControl/` | `Roles/` | Describes the capability, not the resource |
+| `Governance/` | `Admin/` | Describes the compliance intent |
+| `Passwords/` | `Auth/` | Narrow, precise scope |
+| `Registration/` | `Signup/` | Uses the system's formal language |
+
+**Rule:** If a folder name is also a valid Eloquent Model name, it is wrong.
+
+### 10.3 Action Class Names
+
+Actions must be named after the **specific Business Intent** they fulfill. Use an active verb + business noun pattern.
+
+| ✅ Correct | ❌ Wrong | Why |
+|---|---|---|
+| `ProvisionNewUser` | `CreateUser` | Describes *who* triggers it and *why* |
+| `SuspendUser` | `DeleteUser` | Reveals the business consequence (soft revoke, not destroy) |
+| `UpdateUserRole` | `SaveRole` | Explicit about the subject and property being changed |
+| `RegisterUser` | `StoreUser` | Domain language, not HTTP verb language |
+| `SendPasswordResetLink` | `ResetPassword` | Reflects the actual side effect triggered |
+
+CRUD names (`CreateCategory`, `UpdateSetting`) are only acceptable for trivial lookup tables with **no side effects**.
+
+### 10.4 DTO Class Names
+
+DTOs are named after the Action they serve, with a `DTO` suffix.
+
+| Action | DTO |
+|---|---|
+| `ProvisionNewUser` | `ProvisionUserDTO` |
+| `UpdateUser` | `UpdateUserDTO` |
+| `CreateSystemRole` | `CreateRoleDTO` |
+
+### 10.5 Event Class Names
+
+Events are **past-tense facts** about something that already happened in the domain. The class name must be grammatically a completed truth.
+
+| ✅ Correct | ❌ Wrong | Why |
+|---|---|---|
+| `UserWasProvisioned` | `UserProvisioned` | Explicit past-tense removes ambiguity |
+| `UserWasSuspended` | `UserSuspended` | Reads as a state, not a completed fact |
+| `UserLoggedIn` | `LoginEvent` | Noun + verb pattern; avoids the `Event` suffix |
+| `UserEmailVerified` | `EmailVerification` | Describes the completed action |
+
+**Rule:** Never suffix Events with `Event` (e.g., `UserRegisteredEvent` is wrong). The namespace `Events\` already communicates the type.
+
+### 10.6 Listener Class Names
+
+Listeners describe the **active reaction** to an event using an imperative verb phrase.
+
+| ✅ Correct | ❌ Wrong | Why |
+|---|---|---|
+| `SendSignInActivityNotification` | `UserLoggedInListener` | Describes what the listener *does*, not what it reacts to |
+| `DispatchWelcomeNotification` | `WelcomeListener` | Imperative verb makes the intent crystal clear |
+
+**Rule:** Never suffix Listeners with `Listener` in the class name. The namespace `Listeners\` already communicates the type.
+
+---
+
+## 11. AI Agent Prompt (System Instructions)
+
+**For Developers:** Copy and paste the block below into your AI agent's chat or system instructions before asking it to write or refactor code in this repository.
+
+```text
+You are an autonomous Senior Laravel Architect specializing in Pragmatic Domain-Driven Design (DDD) and Event-Driven Architecture. You must strictly obey the "Antigravity" rules of this repository.
+
+### 1. The HTTP Gateway (Delivery Layer)
+- Lives in `app/Http/Controllers/`, `app/Http/DataTables/`, or `app/Livewire/`.
+- Responsibilities: HTTP validation, rate limiting, session management (`Auth::login`, `session()->regenerate()`), and redirects.
+- HARD RESTRICTION: The Gateway MUST NEVER call `Model::create()`, `Model::update()`, or `Hash::make()`. It must map validated data into a DTO and pass it to a Domain Action.
+
+### 2. The Domain (Business Logic)
+- Lives inside `app/Domains/{Concept}/`.
+- Responsibilities: DTO definitions, Actions (database writes), Models, and Events.
+- HARD RESTRICTION: The Domain MUST NEVER read from the `request()` helper, manipulate cookies/sessions, or throw HTTP-specific exceptions (like `ValidationException`).
+
+### 3. Execution & Workflow
+- DTOs: Must be strictly typed readonly classes.
+- Actions: Must represent a specific Business Intent (e.g., `ProvisionNewUser` not `SaveUser`). Actions that perform multiple database writes must wrap them in a `DB::transaction()`.
+- Action Composition: Inject Actions into other Actions via the constructor to reuse logic (e.g., injecting `AccessControl\UpdateUserRole` into `Onboarding\ProvisionNewUser`).
+- Events: Use Events to decouple side effects (Notifications, Activity Logs). Events must be **past-tense** (e.g., `UserWasProvisioned` not `UserProvisioned`).
+
+### 4. Naming Rules
+- **Domains**: Singular Business Concepts, never database nouns (✅ `Identity` ❌ `Users`).
+- **Capability Folders**: Name subdirectories after business capabilities, never after models (✅ `Onboarding/`, `AccessControl/`, `Governance/` ❌ `Users/`, `Roles/`).
+- **Actions**: Active verb + business noun (✅ `ProvisionNewUser` ❌ `CreateUser`).
+- **Events**: Past-tense completed facts, no `Event` suffix (✅ `UserWasProvisioned` ❌ `UserProvisionedEvent`).
+- **Listeners**: Imperative active-verb phrases, no `Listener` suffix (✅ `SendSignInActivityNotification` ❌ `UserLoggedInListener`).
+
+### 5. File Generation Rules
+- NEVER use standard Laravel generators (e.g., `php artisan make:model`) for Domain classes.
+- ALWAYS use the custom `domain:make` command to create Domain files.
+- Example: `php artisan domain:make action Identity Onboarding/ProvisionNewUser`
+- Supported types: `model`, `action`, `dto`, `enum`, `event`, `listener`, `notification`, `policy`, `query`, `provider`, `export`, `mapper`, `scope`, `trait`, `mailable`.
+- Examples for the Integration layer:
+ - `php artisan domain:make export Identity UserExport --model=User`
+ - `php artisan domain:make mapper Identity User` → generates `Integration/Mappers/UserDataMapper.php`
+- Excel ingestion (Import) classes live in the **Gateway layer** at `app/Http/Ingestion/Excel/` — do NOT generate them with `domain:make`.
+- Queries: For complex database reads (e.g., massive filtering or reporting), create a Query class in app/Domains/{Concept}/Queries/. Queries are read-only, do not use transactions, do not mutate state, and do not dispatch events.
+
+### 6. Testing Strategy
+- Always write tests using Pest PHP.
+- When generating new features (Actions/DTOs/Models), create corresponding tests in `tests/Feature/` or `tests/Unit/`.
+- Ensure 100% adherence to Architectural tests (check `tests/Architecture/` for existing rules).
+- When writing tests, use `Event::fake()`, `Queue::fake()`, `Notification::fake()` to isolate side effects.
+- For dependency-injected services, use `$this->mock()` to define expected behaviors.
+
+Write modern PHP 8.4+ code with strict typing. Ensure all PSR-4 namespaces perfectly match the directory structure.
+
+```
+
+---
+
+## 12. Development Tools & Generators
+
+To maintain the strict folder structure of the Antigravity architecture, **do not use standard `make:` commands (like `make:model`) for Domain files.** Use the custom `domain:make` command to generate classes in the correct `app/Domains/` namespaces.
+
+### The `domain:make` Command
+
+**Signature:**
+
+```bash
+php artisan domain:make {type} {domain} {name} [options]
+
+```
+
+**Arguments:**
+
+* `type`: The file type to generate. Supported: `model`, `action`, `dto`, `enum`, `event`, `listener`, `notification`, `policy`, `scope`, `trait`, `query`, `provider`, `export`, `mapper`, `mailable`.
+* `domain`: The target Domain folder (e.g., `Identity`, `Account`, `System`).
+* `name`: The class name. Supports sub-directory grouping (e.g., `Management/ProvisionNewUser`).
+
+**Options:**
+
+* `--factory`: Generates an associated database factory (Models only).
+* `--migration`: Generates a database migration file (Models only).
+* `--model=`: Associates the export class with an Eloquent model (Exports only).
+
+### Customizing Generators (Stubs)
+
+All `domain:make` templates are stored as `.stub` files in `app/Console/stubs/domain-make/`. You can freely edit these stubs to customize the default boilerplate for your project's specific needs (e.g., changing the default methods in a Repository or adjusting the strict typing in a DTO).
+
+---
+
+## 13. Universal File Management (The System Domain)
+
+File handling (uploads, attachments, image cropping, and deletions) is a universally shared capability. To prevent every domain from writing its own file storage logic, all physical files are managed by a centralized engine within the **`System`** domain.
+
+### The Polymorphic Engine
+
+We do not add file paths directly to business tables (e.g., no `avatar_path` column on the `users` table). Instead, we use a central `files` table and the `System\Models\File` model.
+
+* Files are attached **polymorphically** to any entity in the application.
+* The `files` table includes a strictly typed `relation_name` string column (e.g., `'avatar'`) to prevent multiple file types attached to the same model from colliding.
+
+### The Consumer Trait
+
+When a Domain Model (like `User` or `Invoice`) needs to accept a file attachment, it pulls in the `HasFile` trait. This trait provides isolated relationship builders.
+
+```php
+namespace App\Domains\Identity\Models;
+
+use Illuminate\Database\Eloquent\Model;
+use App\Domains\System\Traits\HasFile;
+use Illuminate\Database\Eloquent\Relations\MorphOne;
+
+class User extends Model
+{
+ use HasFile;
+
+ // The string 'avatar' perfectly isolates this file in the database
+ public function avatar(): MorphOne
+ {
+ return $this->singleFile('avatar');
+ }
+}
+
+```
+
+### File Actions (Metadata via DTO)
+
+Because Laravel's `Illuminate\Http\UploadedFile` is a complex object, we pass it alongside a strictly-typed `FileDTO` that contains the metadata (target model, disk, and relation name). This ensures the Gateway remains clean while the Domain receives all necessary context.
+
+* **`UploadAndAttachFile`**: The base action. It stores the physical file to the disk and creates the polymorphic database record.
+* **`ReplaceSingleFile`**: Used for 1-to-1 replacements (like changing an avatar). It safely deletes the old file before delegating the new upload back to the base action.
+
+**Gateway Example:**
+
+```php
+$action->execute(
+ newFile: $request->file('photo'),
+ dto: new FileDTO(
+ modelType: $user->getMorphClass(),
+ modelId: $user->id,
+ relationName: 'avatar',
+ disk: 'local',
+ directory: 'avatars',
+ options: [],
+ uploaderId: auth()->id(),
+ )
+);
+
+```
+
+### System Asset Helper
+
+To avoid polluting Laravel's global namespace with a junk `app/helpers.php` file, we maintain a strictly domain-bound helper file at `app/Domains/System/Helpers/asset.php`. It is autoloaded via `composer.json` and provides the `asset_static()` function to elegantly resolve public and private files in our Blade views.
+
+---
+
+## 14. Global Settings & Application State
+
+Settings that dictate the runtime state of the application (Timezones, Localization, SEO tags) are managed by the `System` domain to ensure high performance and context awareness.
+
+* **Memoization & Singletons:** The `GetSystemSettings` query is registered as a Singleton in the `SystemServiceProvider`. It fetches data from the database/cache *once* and stores it in local PHP memory for the duration of the request.
+* **Contextual Middlewares:** We use dedicated middlewares (`HandlePreferredLanguage`, `HandlePreferredTimezone`) to dynamically check the authenticated user's preferences, falling back to the global settings if no preference exists.
+* **View Composers:** Global layout variables (like Logos and Web Names) are injected globally via View Composers in the Service Provider, preventing repetitive `@inject` directives.
+
+---
+
+## 15. Audit Logging & Tracking
+
+All critical database mutations are tracked to maintain a compliant historical ledger.
+
+* **Model Auditing:** We utilize Eloquent events to automatically log row changes.
+* **Complex Relation Auditing:** Tracking many-to-many relationship changes (like Spatie `syncPermissions`) bypasses standard Eloquent events. Therefore, we explicitly dispatch Custom Audit Events directly inside the relevant Domain Action (e.g., `UpdateRolePermissions`). This guarantees the "Before" and "After" state is captured cleanly in a single transactional row.
+
+---
+
+## 16. Dynamic UIs & Livewire Interoperability
+
+When building data-driven interfaces (like dynamic settings forms), we utilize the **Renderable Enum** pattern combined with Laravel's native dynamic components.
+
+* Enums act as **Metadata Providers** (returning the string name of the target Blade component).
+* We use `` in the Blade file to swap UI elements.
+* **CRITICAL:** Enums must never return raw HTML strings compiled via the `Blade` facade. Doing so breaks Livewire's DOM-diffing engine and severs `wire:model` bindings.
+
+---
+
+## 17. Excel Import & Export
+
+The Laravel component `resources/views/components/datatables/⚡excel-manager.blade.php` (registered as ``) provides a reusable, queue-backed mechanism for importing and exporting Excel files in any DataTable page. It is a **unified single-file Laravel component** — PHP class logic and Blade template co-exist in the same file, following the `⚡` naming convention used across all Laravel components in this project.
+
+### Architecture Overview
+
+The component relies on three collaborating layers:
+
+1. **The Laravel Component** (`⚡excel-manager.blade.php`) — Handles UI state, file uploads via `WithFilePond`, validation, and Livewire event listeners. All props are secured with `#[Locked]` to prevent client-side tampering.
+2. **The `StyledExport` Decorator** (`App\UI\Support\Excel\StyledExport`) — A UI-layer wrapper that enriches any domain Export with standardized visual styling (frozen header row, landscape orientation, thin borders, centered alignment, and auto-sized columns) without polluting domain Export classes with presentation logic.
+3. **The Event-Driven Notification Pipeline** — After the export file is written to disk, a queued `NotifyExportReady` job dispatches the `ExportCompleted` event, which is handled by the `SendExportReportEmail` listener to deliver the file via email.
+
+### How It Works
+
+* **Import:** The user uploads an `.xlsx` file via the FilePond modal. The file is stored to `local/excel/import/{resourceName}` and a new import instance is constructed with a UUID (`$importId`) and the authenticated user's ID (`$initiatorId`) before being dispatched to the queue via `Excel::queueImport()`. The Ingestion class lives in `app/Http/Ingestion/` (Gateway layer) and implements `WithChunkReading` (chunk size: 200 rows) to stay within shared-hosting memory limits. A success toast (`ui.excel.import.success`) is shown immediately upon queuing.
+
+* **Export:** A Livewire browser event (`export-excel`) — dispatched by the DataTable's Export button — triggers the `export()` method via `#[On('export-excel')]`. The domain Export is wrapped in `StyledExport` and queued via `Excel::queue()`. The job chain appends `NotifyExportReady`, which dispatches `ExportCompleted`, which is handled by `SendExportReportEmail` to send the file as an email attachment to the authenticated user. A success toast (`ui.excel.export.success`) is shown immediately upon queuing.
+
+### Component Props
+
+| Prop | Type | Description |
+| --- | --- | --- |
+| `importClass` | `string` | Fully-qualified class name of the Gateway Ingestion class (e.g., `App\Http\Ingestion\Excel\Identity\UserImport`). |
+| `exportClass` | `string` | Fully-qualified class name of the domain Export (e.g., `App\Domains\Identity\Exports\UserExport`). |
+| `resourceName` | `string` | A slug used to name the stored import file and the timestamped export file (e.g., `user`). |
+
+### Usage
+
+Embed the component in any DataTable page view. All props must be provided as fully-qualified PHP class name strings:
+
+```blade
+
+```
+
+The DataTable's Export button should dispatch the `export-excel` Livewire event, and the Import button should open the `#excel-import-modal` Bootstrap modal:
+
+```php
+// In your DataTable html() builder:
+Button::make('excel')
+ ->action("Livewire.dispatch('export-excel')"),
+
+Button::make('excel')
+ ->action("$('#excel-import-modal').modal('show')"),
+```
+
+### Generating Export & Mapper Classes
+
+Use the `domain:make` command to create Export and Integration layer classes:
+
+```bash
+# Generate a domain Export class
+php artisan domain:make export Identity UserExport --model=User
+
+# Generate an Integration Mapper (auto-appends DataMapper suffix)
+php artisan domain:make mapper Identity User
+# → app/Domains/Identity/Integration/Mappers/UserDataMapper.php
+```
+
+Domain Export classes must implement `FromQuery & WithHeadings & WithMapping & WithColumnFormatting`. The `StyledExport` decorator will apply all visual styling automatically at queue time — do **not** implement `WithStyles` directly on domain Exports.
+
+> **Gateway Layer:** Excel Ingestion (Import) classes live in `app/Http/Ingestion/Excel/` and are **not** generated by `domain:make`. Create them manually or with `make:class` as standard PHP classes implementing `ToCollection`, `WithHeadingRow`, and `WithChunkReading`.
+
+### The Notification Pipeline
+
+The export notification flow follows a strict, fully-queued event-driven chain:
+
+```
+Excel::queue(StyledExport, $path)
+ └─> NotifyExportReady (Job) [app/Domains/System/Jobs/]
+ └─> ExportCompleted::dispatch (Event) [app/Domains/System/Events/]
+ └─> SendExportReportEmail (Listener) [app/Domains/System/Listeners/]
+ └─> ExcelExportEmail (Mailable) [app/Domains/System/Mail/]
+```
+
+The import notification is sent by the domain Import class itself upon completion, using `ExcelImportEmail` from the same `App\Domains\System\Mail\` namespace.
+
+### Mailables
+
+Both Mailable classes live in the **System domain**, not the root `App\Mail\` namespace:
+
+* **`App\Domains\System\Mail\ExcelImportEmail`** — Sent when a queued import finishes. Uses `domains/system.notifications.excel.import_email.*` translations.
+* **`App\Domains\Identity\Mail\Registration\WelcomeEmail`** — Example of a domain-specific mailable.
+* **`App\Domains\System\Mail\ExcelExportEmail`** — Sent when a queued export is ready, with the file attached from the `local` disk. Uses `domains/system.notifications.excel.export_email.*` translations.
+
+### Translation Keys
+
+| File | Key | Purpose |
+| --- | --- | --- |
+| `lang/{locale}/ui.php` | `ui.excel.import.file_label` | FilePond upload label inside the import modal. |
+| `lang/{locale}/ui.php` | `ui.excel.import.success` | Toast shown after import is queued. |
+| `lang/{locale}/ui.php` | `ui.excel.export.success` | Toast shown after export is queued. |
+| `lang/{locale}/domains/system.php` | `notifications.excel.import_email.*` | Email body for the import completion notification. |
+| `lang/{locale}/domains/system.php` | `notifications.excel.export_email.*` | Email body for the export ready notification. |
+
+---
+
+## 18. Testing
+
+We use [Pest PHP](https://pestphp.com) for our test suite. Please refer to [TESTING.md](TESTING.md) for detailed instructions on running, structuring, and writing tests.
+
+---
+
+## 19. Environment Variables
+
+Key variables used in `.env`:
+
+- `APP_NAME`: Name of the application.
+- `APP_ENV`: Application environment (`local`, `production`, etc.).
+- `APP_KEY`: Application encryption key.
+- `DB_CONNECTION`: Database driver (`sqlite`, `mysql`, `pgsql`).
+- `QUEUE_CONNECTION`: Queue driver (default: `database`).
+- `MAIL_MAILER`: Mail driver (default: `log`).
+
+See `.env.example` for the full list of available options.
+
+---
+
+## 20. License
+
+This project is licensed under the **MIT License**.
+
+
diff --git a/TESTING.md b/TESTING.md
new file mode 100644
index 0000000..43013ef
--- /dev/null
+++ b/TESTING.md
@@ -0,0 +1,125 @@
+# Testing Guide
+
+We use [Pest PHP](https://pestphp.com) for our test suite, providing a clean, expressive syntax for testing our Modular Monolith.
+
+## 1. Running Tests
+
+The project includes several pre-configured commands to run the test suite:
+
+- **Run all tests:** `composer test`
+- **Run Feature tests:** `php artisan test --testsuite=Feature`
+- **Run Unit tests:** `php artisan test --testsuite=Unit`
+- **Run Architecture tests:** `php artisan test --testsuite=Architecture`
+
+You can also run specific tests by passing arguments to Artisan:
+
+```bash
+# Run a specific file
+php artisan test tests/Feature/Identity/LoginTest.php
+
+# Run a specific test method
+php artisan test --filter=test_user_can_login
+```
+
+## 2. Test Directory Structure
+
+Our tests follow a strict directory structure that mirrors the application's domain-based architecture:
+
+- `tests/Feature/`: High-level application features. These tests interact with the application like a user (HTTP requests, Livewire components, form submissions).
+- `tests/Unit/`: Low-level logic, helpers, and domain-specific logic. These tests should be fast and have zero external dependencies.
+- `tests/Architecture/`: Ensures the integrity of our Modular Monolith (e.g., verifying that the Domain layer does not depend on the HTTP layer).
+
+## 3. Writing Tests
+
+### Base Class
+All tests extend `Tests\TestCase`. This base class sets up the application environment and includes shared traits.
+
+### Database Interaction
+By default, Feature tests use the `RefreshDatabase` trait (configured in `tests/Pest.php`). This ensures a clean database state for every test.
+
+### Architectural Rules
+We use architecture tests to prevent "spaghetti code." For example:
+- **Layer Isolation:** The `Domains/` directory must not import anything from `Http/` or `Livewire/`.
+- **Debugging:** Ensure `dd()` or `dump()` calls are not committed to the repository.
+
+### Writing Domain Tests
+When testing a new Domain feature, create a corresponding test file in the `tests/Feature/` or `tests/Unit/` directory.
+
+Example: If you create `app/Domains/Identity/Actions/CreateUser.php`, create a test at `tests/Unit/Domains/Identity/Actions/CreateUserTest.php`.
+
+## 4. Best Practices
+
+- **Test Behavior, Not Implementation:** Focus on what the code does, not how it does it.
+- **Keep Tests Fast:** Unit tests should run in milliseconds. If a test is slow, it might be a candidate for a unit test rather than a feature test.
+- **Use Factories:** Utilize Laravel model factories to generate test data instead of manually creating records.
+- **Mock External Services:** Use dependency injection and interfaces to mock external API calls or email services in your tests.
+
+## 5. Advanced Testing
+
+### Testing Events
+To assert that events are dispatched, use Laravel's `Event` facade:
+
+```php
+use App\Domains\Identity\Events\UserLoggedIn;
+use Illuminate\Support\Facades\Event;
+
+it('dispatches the user logged in event', function () {
+ Event::fake();
+
+ // Perform action
+ $this->post('/login', [...]);
+
+ Event::assertDispatched(UserLoggedIn::class);
+});
+```
+
+### Testing Jobs
+To assert that jobs are pushed to the queue, use the `Queue` facade:
+
+```php
+use App\Domains\System\Jobs\NotifyExportReady;
+use Illuminate\Support\Facades\Queue;
+
+it('queues the export notification job', function () {
+ Queue::fake();
+
+ // Perform action that triggers the job
+ // ...
+
+ Queue::assertPushed(NotifyExportReady::class);
+});
+```
+
+### Testing Notifications
+To test notifications, use the `Notification` facade:
+
+```php
+use App\Domains\Identity\Notifications\WelcomeNotification;
+use Illuminate\Support\Facades\Notification;
+
+it('sends a welcome notification', function () {
+ Notification::fake();
+
+ // Perform action
+ // ...
+
+ Notification::assertSentTo($user, WelcomeNotification::class);
+});
+```
+
+### Mocking
+For services or classes that are dependency-injected, use `mock()` or `spy()`:
+
+```php
+use App\Domains\System\Integration\ExternalApiService;
+
+it('uses the external api service', function () {
+ $this->mock(ExternalApiService::class, function ($mock) {
+ $mock->shouldReceive('call')
+ ->once()
+ ->andReturn(['status' => 'success']);
+ });
+
+ // Run code that uses ExternalApiService
+});
+```
diff --git a/app/Attributes/LayoutData.php b/app/Attributes/LayoutData.php
new file mode 100644
index 0000000..70166eb
--- /dev/null
+++ b/app/Attributes/LayoutData.php
@@ -0,0 +1,15 @@
+info('Starting file reconciliation...');
+
+ $stats = $action->execute(
+ disk: $this->option('disk'),
+ directory: $this->option('directory')
+ );
+
+ $this->info('Reconciliation complete.');
+ $this->line("- DB Orphans Removed: {$stats['db_orphans_removed']}");
+ $this->line("- Disk Stranded Files Removed: {$stats['disk_orphans_removed']}");
+
+ return Command::SUCCESS;
+ }
+}
diff --git a/app/Console/Commands/DomainMakeCommand.php b/app/Console/Commands/DomainMakeCommand.php
new file mode 100644
index 0000000..144be3b
--- /dev/null
+++ b/app/Console/Commands/DomainMakeCommand.php
@@ -0,0 +1,205 @@
+ type → subdirectory */
+ protected array $types = [
+ 'model' => 'Models',
+ 'action' => 'Actions',
+ 'dto' => 'DTOs',
+ 'enum' => 'Enums',
+ 'event' => 'Events',
+ 'listener' => 'Listeners',
+ 'notification' => 'Notifications',
+ 'policy' => 'Policies',
+ 'scope' => 'Scopes',
+ 'trait' => 'Traits',
+ 'query' => 'Queries',
+ 'provider' => 'Providers',
+ 'export' => 'Exports',
+ // Integration layer — files live under Integration//
+ 'mapper' => 'Integration/Mappers',
+ 'mailable' => 'Mail',
+ ];
+
+ public function __construct(protected Filesystem $files)
+ {
+ parent::__construct();
+ }
+
+ public function handle(): int
+ {
+ $type = strtolower($this->argument('type'));
+ $domain = ucfirst($this->argument('domain'));
+ $name = $this->argument('name'); // may contain sub-path, e.g. Backup/DeleteBackup
+
+ if (! isset($this->types[$type])) {
+ $this->components->error("Unknown type [{$type}]. Supported: ".implode(', ', array_keys($this->types)));
+
+ return self::FAILURE;
+ }
+
+ $subDir = $this->types[$type];
+ $className = class_basename(str_replace('/', '\\', $name));
+ $subPath = str_contains($name, '/') ? dirname($name) : null;
+
+ // ── Integration / Mapper special handling ─────────────────────────────
+ // Automatically append the 'DataMapper' suffix when the developer omits it,
+ // keeping the class name consistent with the DataPayloadMapper contract.
+ if ($type === 'mapper' && ! str_ends_with($className, 'DataMapper')) {
+ $className .= 'DataMapper';
+ }
+
+ // The $subDir for 'mapper' already encodes the full Integration/Mappers
+ // nested path, so we must not double-nest an additional subPath beneath it.
+ // Extra sub-path segments are intentionally ignored for the mapper type.
+ $relativeDir = ($type === 'mapper')
+ ? "Domains/{$domain}/{$subDir}"
+ : "Domains/{$domain}/{$subDir}".($subPath ? "/{$subPath}" : '');
+ // ─────────────────────────────────────────────────────────────────────
+
+ $namespace = 'App\\'.str_replace('/', '\\', $relativeDir);
+ $path = app_path("{$relativeDir}/{$className}.php");
+
+ if ($this->files->exists($path)) {
+ $this->components->error("File already exists: [{$path}]");
+
+ return self::FAILURE;
+ }
+
+ $this->files->ensureDirectoryExists(dirname($path));
+ $this->files->put($path, $this->buildStub($type, $namespace, $className, $domain));
+
+ $this->components->info("File [{$path}] created successfully.");
+
+ if ($type === 'model' && $this->option('factory')) {
+ $this->createFactory($domain, $className, $namespace);
+ }
+
+ if ($type === 'model' && $this->option('migration')) {
+ $table = Str::snake(Str::pluralStudly($className));
+ $this->call('make:migration', ['name' => "create_{$table}_table"]);
+ }
+
+ return self::SUCCESS;
+ }
+
+ // ─── Stubs ────────────────────────────────────────────────────────────────
+
+ protected function buildStub(string $type, string $namespace, string $name, string $domain): string
+ {
+ $stubPath = app_path("Console/stubs/domain-make/{$type}.stub");
+
+ if (! $this->files->exists($stubPath)) {
+ $this->components->error("Stub file not found: [{$stubPath}]");
+
+ return '';
+ }
+
+ $stub = $this->files->get($stubPath);
+
+ $replacements = [
+ '{{ namespace }}' => $namespace,
+ '{{ class }}' => $name,
+ ];
+
+ if ($type === 'model') {
+ $replacements['{{ factoryImport }}'] = $this->option('factory')
+ ? "\nuse Database\\Factories\\{$domain}\\{$name}Factory;\nuse Illuminate\\Database\\Eloquent\\Factories\\Factory;"
+ : '';
+ $replacements['{{ factoryMethod }}'] = $this->option('factory')
+ ? "\n protected static function newFactory(): Factory\n {\n return {$name}Factory::new();\n }"
+ : '';
+ }
+
+ if ($type === 'export') {
+ $modelOption = $this->option('model');
+ $modelImport = '';
+ $queryBody = ' // return YourModel::query();';
+
+ if ($modelOption) {
+ $modelData = $this->resolveModel($modelOption, $domain);
+ $modelImport = "use {$modelData['full']};\n";
+ $modelClass = $modelData['class'];
+ $queryBody = <<with('profile') // CRITICAL: Eager load any relations used in map()
+ // ->when(isset(\$this->filters['status']), fn(Builder \$q) => \$q->where('status', \$this->filters['status']))
+ ;
+PHP;
+ }
+
+ $replacements['{{ modelImport }}'] = $modelImport;
+ $replacements['{{ queryBody }}'] = $queryBody;
+ }
+
+ return str_replace(array_keys($replacements), array_values($replacements), $stub);
+ }
+
+ protected function resolveModel(string $modelOption, string $domain): array
+ {
+ if (Str::contains($modelOption, ['\\', '/'])) {
+ if (Str::startsWith(str_replace('/', '\\', $modelOption), 'App\\')) {
+ $full = str_replace('/', '\\', $modelOption);
+ $class = class_basename($full);
+ } else {
+ $parts = explode('/', str_replace('\\', '/', $modelOption));
+ $targetDomain = ucfirst($parts[0]);
+ $targetName = implode('\\', array_map('ucfirst', array_slice($parts, 1)));
+ $full = "App\\Domains\\{$targetDomain}\\Models\\{$targetName}";
+ $class = class_basename($full);
+ }
+ } else {
+ $full = "App\\Domains\\{$domain}\\Models\\".ucfirst($modelOption);
+ $class = ucfirst($modelOption);
+ }
+
+ return [
+ 'full' => $full,
+ 'class' => $class,
+ ];
+ }
+
+ protected function createFactory(string $domain, string $name, string $modelNamespace): void
+ {
+ $factoryNamespace = "Database\\Factories\\{$domain}";
+ $factoryPath = database_path("factories/{$domain}/{$name}Factory.php");
+
+ $this->files->ensureDirectoryExists(dirname($factoryPath));
+
+ if ($this->files->exists($factoryPath)) {
+ $this->components->warn("Factory already exists: [{$factoryPath}]");
+
+ return;
+ }
+
+ $stubPath = app_path('Console/stubs/domain-make/factory.stub');
+ $stub = $this->files->exists($stubPath) ? $this->files->get($stubPath) : '';
+
+ $stub = str_replace(
+ ['{{ factoryNamespace }}', '{{ modelNamespace }}', '{{ class }}'],
+ [$factoryNamespace, $modelNamespace, $name],
+ $stub
+ );
+
+ $this->files->put($factoryPath, $stub);
+
+ $this->components->info("Factory [database/factories/{$domain}/{$name}Factory.php] created successfully.");
+ }
+}
diff --git a/app/Console/stubs/domain-make/action.stub b/app/Console/stubs/domain-make/action.stub
new file mode 100644
index 0000000..1dd130c
--- /dev/null
+++ b/app/Console/stubs/domain-make/action.stub
@@ -0,0 +1,11 @@
+id,
+ // To format as a true Excel date, pass a native PHP DateTime object or an Excel timestamp
+ // \PhpOffice\PhpSpreadsheet\Shared\Date::dateTimeToExcel($row->created_at),
+ $row->created_at?->format('Y-m-d H:i'),
+ // $row->amount,
+ ];
+ }
+
+ /**
+ * Apply Excel formatting to specific columns (Dates, Currency, Percentages).
+ */
+ public function columnFormats(): array
+ {
+ return [
+ // Example: Format Column B as a true Date
+ // 'B' => NumberFormat::FORMAT_DATE_YYYYMMDD,
+
+ // Example: Format Column C as Currency
+ // 'C' => NumberFormat::FORMAT_CURRENCY_USD_SIMPLE,
+ ];
+ }
+
+ /**
+ * Apply visual styling to specific rows or columns.
+ */
+ public function styles(Worksheet $sheet)
+ {
+ return [
+ // Make the first row (the headings) bold
+ 1 => ['font' => ['bold' => true]],
+ ];
+ }
+}
\ No newline at end of file
diff --git a/app/Console/stubs/domain-make/factory.stub b/app/Console/stubs/domain-make/factory.stub
new file mode 100644
index 0000000..00fa3c4
--- /dev/null
+++ b/app/Console/stubs/domain-make/factory.stub
@@ -0,0 +1,19 @@
+
+ */
+class {{ class }}Factory extends Factory
+{
+ protected $model = {{ class }}::class;
+
+ public function definition(): array
+ {
+ return [];
+ }
+}
\ No newline at end of file
diff --git a/app/Console/stubs/domain-make/listener.stub b/app/Console/stubs/domain-make/listener.stub
new file mode 100644
index 0000000..bcfe51f
--- /dev/null
+++ b/app/Console/stubs/domain-make/listener.stub
@@ -0,0 +1,11 @@
+
+ */
+ public function attachments(): array
+ {
+ return [];
+ }
+}
diff --git a/app/Console/stubs/domain-make/mapper.stub b/app/Console/stubs/domain-make/mapper.stub
new file mode 100644
index 0000000..c8cb4a8
--- /dev/null
+++ b/app/Console/stubs/domain-make/mapper.stub
@@ -0,0 +1,31 @@
+line('');
+ }
+
+ public function toArray(object $notifiable): array
+ {
+ return [];
+ }
+}
\ No newline at end of file
diff --git a/app/Console/stubs/domain-make/policy.stub b/app/Console/stubs/domain-make/policy.stub
new file mode 100644
index 0000000..a703565
--- /dev/null
+++ b/app/Console/stubs/domain-make/policy.stub
@@ -0,0 +1,33 @@
+ $dto->userId],
+ [
+ 'gender' => $dto->gender,
+ 'date_of_birth' => $dto->dateOfBirth,
+ 'phone_number' => $dto->phoneNumber,
+ ]
+ );
+ }
+}
diff --git a/app/Domains/Account/DTOs/Profile/UpdateProfileDTO.php b/app/Domains/Account/DTOs/Profile/UpdateProfileDTO.php
new file mode 100644
index 0000000..00ea6ae
--- /dev/null
+++ b/app/Domains/Account/DTOs/Profile/UpdateProfileDTO.php
@@ -0,0 +1,15 @@
+value}");
+ }
+
+ public static function fromLabel($value): self
+ {
+ return match ($value) {
+ self::MALE->label() => self::MALE,
+ self::FEMALE->label() => self::FEMALE,
+ };
+ }
+}
diff --git a/app/Domains/Account/Listeners/SyncImportedUserProfile.php b/app/Domains/Account/Listeners/SyncImportedUserProfile.php
new file mode 100644
index 0000000..eb20587
--- /dev/null
+++ b/app/Domains/Account/Listeners/SyncImportedUserProfile.php
@@ -0,0 +1,25 @@
+updateProfile->execute(new UpdateProfileDTO(
+ userId: $event->userId,
+ gender: GenderOption::fromLabel($event->gender),
+ dateOfBirth: $event->dateOfBirth,
+ phoneNumber: $event->phoneNumber,
+ ));
+ }
+}
diff --git a/app/Domains/Account/Models/Profile.php b/app/Domains/Account/Models/Profile.php
new file mode 100644
index 0000000..bac9043
--- /dev/null
+++ b/app/Domains/Account/Models/Profile.php
@@ -0,0 +1,37 @@
+ */
+ use HasFactory;
+
+ protected static function newFactory(): Factory
+ {
+ return ProfileFactory::new();
+ }
+
+ protected function casts(): array
+ {
+ return [
+ 'gender' => GenderOption::class,
+ 'date_of_birth' => 'date',
+ ];
+ }
+
+ public function user(): BelongsTo
+ {
+ return $this->belongsTo(User::class);
+ }
+}
diff --git a/app/Domains/Account/Providers/AccountServiceProvider.php b/app/Domains/Account/Providers/AccountServiceProvider.php
new file mode 100644
index 0000000..f790107
--- /dev/null
+++ b/app/Domains/Account/Providers/AccountServiceProvider.php
@@ -0,0 +1,22 @@
+ [
+ SyncImportedUserProfile::class,
+ ]
+ ];
+
+ public function boot(): void
+ {
+ //
+ }
+}
diff --git a/app/Domains/Identity/Actions/AccessControl/CreateSystemRole.php b/app/Domains/Identity/Actions/AccessControl/CreateSystemRole.php
new file mode 100644
index 0000000..a64ce78
--- /dev/null
+++ b/app/Domains/Identity/Actions/AccessControl/CreateSystemRole.php
@@ -0,0 +1,26 @@
+ $dto->name,
+ ]);
+ $role->syncPermissions($dto->permissions);
+
+ return true;
+ });
+ }
+}
diff --git a/app/Domains/Identity/Actions/AccessControl/RemoveSystemRole.php b/app/Domains/Identity/Actions/AccessControl/RemoveSystemRole.php
new file mode 100644
index 0000000..3ba33f2
--- /dev/null
+++ b/app/Domains/Identity/Actions/AccessControl/RemoveSystemRole.php
@@ -0,0 +1,26 @@
+name, [RoleType::ADMIN->value, RoleType::SYSTEM_ADMIN->value])) {
+ throw new Exception(__('domains/identity/messages.exceptions.cannot_remove_system_role'));
+ }
+
+ if ($role->users()->exists()) {
+ throw new Exception(__('domains/identity/messages.exceptions.role_has_users'));
+ }
+ }
+}
diff --git a/app/Domains/Identity/Actions/AccessControl/UpdateSystemRole.php b/app/Domains/Identity/Actions/AccessControl/UpdateSystemRole.php
new file mode 100644
index 0000000..be197f4
--- /dev/null
+++ b/app/Domains/Identity/Actions/AccessControl/UpdateSystemRole.php
@@ -0,0 +1,41 @@
+permissions->pluck('name')->toArray();
+ DB::transaction(function () use ($role, $dto) {
+ $role->syncPermissions($dto->permissions);
+ });
+
+ $role->load('permissions');
+ $newPermissions = $role->permissions->pluck('name')->toArray();
+
+ if ($newPermissions !== $oldPermissions) {
+ /** @var Auditable $role */
+
+ // Tell Owen-It exactly what to log
+ $role->auditEvent = 'permissions_synced';
+ $role->isCustomEvent = true;
+ $role->auditCustomOld = ['permissions' => implode(', ', $oldPermissions)];
+ $role->auditCustomNew = ['permissions' => implode(', ', $newPermissions)];
+
+ event(new AuditCustom($role));
+ }
+
+ return true;
+ }
+}
diff --git a/app/Domains/Identity/Actions/AccessControl/UpdateUserRole.php b/app/Domains/Identity/Actions/AccessControl/UpdateUserRole.php
new file mode 100644
index 0000000..f671faa
--- /dev/null
+++ b/app/Domains/Identity/Actions/AccessControl/UpdateUserRole.php
@@ -0,0 +1,13 @@
+syncRoles($roles);
+ }
+}
diff --git a/app/Domains/Identity/Actions/Governance/ActivateUserStatus.php b/app/Domains/Identity/Actions/Governance/ActivateUserStatus.php
new file mode 100644
index 0000000..9dd63d1
--- /dev/null
+++ b/app/Domains/Identity/Actions/Governance/ActivateUserStatus.php
@@ -0,0 +1,27 @@
+status->isActive()) {
+ throw new Exception(__('domains/identity/messages.exceptions.user_already_active'));
+ }
+
+ $this->updateUserStatus->execute($user, UserStatus::ACTIVE);
+
+ UserWasActivated::dispatch(email: $user->email);
+ }
+}
diff --git a/app/Domains/Identity/Actions/Governance/PurgeUser.php b/app/Domains/Identity/Actions/Governance/PurgeUser.php
new file mode 100644
index 0000000..86a733b
--- /dev/null
+++ b/app/Domains/Identity/Actions/Governance/PurgeUser.php
@@ -0,0 +1,24 @@
+hasRole([RoleType::SYSTEM_ADMIN, RoleType::ADMIN])) {
+ throw new Exception(__('domains/identity/messages.exceptions.user_cannot_be_purged'));
+ }
+
+ $user->delete();
+ UserWasPurged::dispatch(email: $user->email, user_id: $user->id, model: User::class);
+ }
+}
diff --git a/app/Domains/Identity/Actions/Governance/RemoveUser.php b/app/Domains/Identity/Actions/Governance/RemoveUser.php
new file mode 100644
index 0000000..674dbed
--- /dev/null
+++ b/app/Domains/Identity/Actions/Governance/RemoveUser.php
@@ -0,0 +1,22 @@
+status->isActive()) {
+ $this->suspendUser->execute($user);
+ } else {
+ $this->purgeUser->execute($user);
+ }
+ }
+}
diff --git a/app/Domains/Identity/Actions/Governance/SuspendUser.php b/app/Domains/Identity/Actions/Governance/SuspendUser.php
new file mode 100644
index 0000000..9458af3
--- /dev/null
+++ b/app/Domains/Identity/Actions/Governance/SuspendUser.php
@@ -0,0 +1,36 @@
+hasRole([RoleType::SYSTEM_ADMIN, RoleType::ADMIN])) {
+ throw new Exception(__('domains/identity/messages.exceptions.user_cannot_be_suspended'));
+ }
+
+ if (! $user->status->isActive()) {
+ throw new Exception(__('domains/identity/messages.exceptions.user_already_suspended'));
+ }
+
+ DB::table('sessions')
+ ->where('user_id', $user->id)
+ ->delete();
+
+ $this->action->execute($user, UserStatus::INACTIVE);
+ UserWasSuspended::dispatch(email: $user->email);
+ }
+}
diff --git a/app/Domains/Identity/Actions/Governance/UpdateUserStatus.php b/app/Domains/Identity/Actions/Governance/UpdateUserStatus.php
new file mode 100644
index 0000000..5611f6a
--- /dev/null
+++ b/app/Domains/Identity/Actions/Governance/UpdateUserStatus.php
@@ -0,0 +1,27 @@
+status === $status) {
+ throw new Exception(__('domains/identity/messages.exceptions.user_already_status', ['status' => $status->value]));
+ }
+
+ if ($user->hasRole([RoleType::SYSTEM_ADMIN, RoleType::ADMIN])) {
+ throw new Exception(__('domains/identity/messages.exceptions.user_cannot_be_edited'));
+ }
+
+ $user->update(['status' => $status]);
+ }
+}
diff --git a/app/Domains/Identity/Actions/IdentityMaintenance/UpdateUserAvatar.php b/app/Domains/Identity/Actions/IdentityMaintenance/UpdateUserAvatar.php
new file mode 100644
index 0000000..ba13f3f
--- /dev/null
+++ b/app/Domains/Identity/Actions/IdentityMaintenance/UpdateUserAvatar.php
@@ -0,0 +1,30 @@
+replaceSingleFile->execute(
+ newFile: $file,
+ dto: new FileDTO(
+ modelType: $user->getMorphClass(),
+ modelId: $user->id,
+ relationName: 'avatar',
+ disk: 'local',
+ directory: 'avatars',
+ options: [],
+ uploaderId: $user->id,
+ )
+ );
+ }
+}
diff --git a/app/Domains/Identity/Actions/IdentityMaintenance/UpdateUserIdentity.php b/app/Domains/Identity/Actions/IdentityMaintenance/UpdateUserIdentity.php
new file mode 100644
index 0000000..4193592
--- /dev/null
+++ b/app/Domains/Identity/Actions/IdentityMaintenance/UpdateUserIdentity.php
@@ -0,0 +1,33 @@
+fill([
+ 'name' => $dto->name,
+ 'email' => $dto->email,
+ ]);
+
+ if ($user->isDirty('email')) {
+ $user->email_verified_at = null;
+ $user->sendEmailVerificationNotification();
+ }
+
+ $user->save();
+
+ return $user;
+ }
+}
diff --git a/app/Domains/Identity/Actions/IdentityMaintenance/UpdateUserSettings.php b/app/Domains/Identity/Actions/IdentityMaintenance/UpdateUserSettings.php
new file mode 100644
index 0000000..407720a
--- /dev/null
+++ b/app/Domains/Identity/Actions/IdentityMaintenance/UpdateUserSettings.php
@@ -0,0 +1,22 @@
+settings ?? [];
+
+ foreach (UserSettingKey::cases() as $key) {
+ if (array_key_exists($key->value, $newSettings)) {
+ $currentSettings[$key->value] = $newSettings[$key->value];
+ }
+ }
+
+ $user->update(['settings' => $currentSettings]);
+ }
+}
diff --git a/app/Domains/Identity/Actions/Onboarding/ProvisionNewUser.php b/app/Domains/Identity/Actions/Onboarding/ProvisionNewUser.php
new file mode 100644
index 0000000..202b28b
--- /dev/null
+++ b/app/Domains/Identity/Actions/Onboarding/ProvisionNewUser.php
@@ -0,0 +1,36 @@
+ $dto->name,
+ 'email' => $dto->email,
+ 'password' => $dto->password,
+ ]);
+ $this->updateUserRole->execute($user, [$dto->role]);
+
+ return $user;
+ });
+
+ UserWasProvisioned::dispatch($user);
+
+ return $user;
+ }
+}
diff --git a/app/Domains/Identity/Actions/Onboarding/RegisterSelfServiceUser.php b/app/Domains/Identity/Actions/Onboarding/RegisterSelfServiceUser.php
new file mode 100644
index 0000000..8ca5b49
--- /dev/null
+++ b/app/Domains/Identity/Actions/Onboarding/RegisterSelfServiceUser.php
@@ -0,0 +1,50 @@
+ $dto->name,
+ 'email' => $dto->email,
+ 'password' => $dto->password,
+ ]);
+ $this->updateUserRole->execute($user, [RoleType::USER]);
+
+ return $user;
+ });
+
+ // Framework event: triggers SendEmailVerificationNotification listener.
+ event(new Registered($user));
+
+ // Domain event: for application-level listeners.
+ UserWasRegistered::dispatch($user, $dto);
+
+ return $user;
+ }
+}
diff --git a/app/Domains/Identity/Actions/Onboarding/ResendVerificationEmail.php b/app/Domains/Identity/Actions/Onboarding/ResendVerificationEmail.php
new file mode 100644
index 0000000..e6f9176
--- /dev/null
+++ b/app/Domains/Identity/Actions/Onboarding/ResendVerificationEmail.php
@@ -0,0 +1,19 @@
+sendEmailVerificationNotification();
+ }
+}
diff --git a/app/Domains/Identity/Actions/Onboarding/VerifyUserEmail.php b/app/Domains/Identity/Actions/Onboarding/VerifyUserEmail.php
new file mode 100644
index 0000000..ff06344
--- /dev/null
+++ b/app/Domains/Identity/Actions/Onboarding/VerifyUserEmail.php
@@ -0,0 +1,40 @@
+hasVerifiedEmail()) {
+ return false;
+ }
+
+ $user->markEmailAsVerified();
+
+ // Framework event: preserves Laravel's Verified contract.
+ event(new Verified($user));
+
+ // Domain event: for application-level listeners.
+ UserEmailWasVerified::dispatch($user);
+
+ return true;
+ }
+}
diff --git a/app/Domains/Identity/Actions/Passwords/ResetUserPassword.php b/app/Domains/Identity/Actions/Passwords/ResetUserPassword.php
new file mode 100644
index 0000000..fd4ac1d
--- /dev/null
+++ b/app/Domains/Identity/Actions/Passwords/ResetUserPassword.php
@@ -0,0 +1,37 @@
+ $dto->token,
+ 'email' => $dto->email,
+ 'password' => $dto->password,
+ 'password_confirmation' => $dto->password_confirmation,
+ ],
+ function (User $user) use ($dto) {
+ $user->forceFill([
+ 'password' => $dto->password,
+ 'remember_token' => Str::random(60),
+ ])->save();
+
+ // Framework event: triggers session token invalidation etc.
+ event(new PasswordReset($user));
+
+ // Domain event: for application-level listeners (audit log, security alert).
+ UserPasswordReset::dispatch($user);
+ }
+ );
+ }
+}
diff --git a/app/Domains/Identity/Actions/Passwords/SendPasswordResetLink.php b/app/Domains/Identity/Actions/Passwords/SendPasswordResetLink.php
new file mode 100644
index 0000000..bb13e53
--- /dev/null
+++ b/app/Domains/Identity/Actions/Passwords/SendPasswordResetLink.php
@@ -0,0 +1,14 @@
+ $dto->email]);
+ }
+}
diff --git a/app/Domains/Identity/Actions/Passwords/UpdatePassword.php b/app/Domains/Identity/Actions/Passwords/UpdatePassword.php
new file mode 100644
index 0000000..6618e01
--- /dev/null
+++ b/app/Domains/Identity/Actions/Passwords/UpdatePassword.php
@@ -0,0 +1,16 @@
+update([
+ 'password' => $dto->new_password,
+ ]);
+ }
+}
diff --git a/app/Domains/Identity/DTOs/AccessControl/CreateRoleDTO.php b/app/Domains/Identity/DTOs/AccessControl/CreateRoleDTO.php
new file mode 100644
index 0000000..4a87050
--- /dev/null
+++ b/app/Domains/Identity/DTOs/AccessControl/CreateRoleDTO.php
@@ -0,0 +1,12 @@
+ 'dashboard.index',
+ 'description' => 'permissions.dashboard.index',
+ 'group' => 'dashboard',
+ 'guard_name' => 'web',
+ 'roles' => [self::SYSTEM_ADMIN, self::ADMIN, self::USER],
+ ],
+ [
+ 'name' => 'dashboard.admin',
+ 'description' => 'permissions.dashboard.admin',
+ 'group' => 'dashboard',
+ 'guard_name' => 'web',
+ 'roles' => [self::SYSTEM_ADMIN, self::ADMIN],
+ ],
+ [
+ 'name' => 'dashboard.user',
+ 'description' => 'permissions.dashboard.user',
+ 'group' => 'dashboard',
+ 'guard_name' => 'web',
+ 'roles' => [self::USER],
+ ],
+
+ ...self::generatePolicy('role', [self::SYSTEM_ADMIN, self::ADMIN]),
+ ...self::generatePolicy('user', [self::SYSTEM_ADMIN, self::ADMIN]),
+
+ // System Setting
+ [
+ 'name' => 'system-setting.manage',
+ 'description' => 'permissions.system-setting.manage',
+ 'group' => 'system-setting',
+ 'guard_name' => 'web',
+ 'roles' => [self::SYSTEM_ADMIN, self::ADMIN],
+ ],
+
+ // System Backup
+ [
+ 'name' => 'system-backup.manage',
+ 'description' => 'permissions.system-backup.manage',
+ 'group' => 'system-backup',
+ 'guard_name' => 'web',
+ 'roles' => [self::SYSTEM_ADMIN, self::ADMIN],
+ ],
+ ];
+ }
+
+ private static function generatePolicy(string $modelName, array $roles = []): array
+ {
+ $policy = ['viewAny', 'view', 'create', 'update', 'delete'];
+ $permissions = [];
+ foreach ($policy as $policyName) {
+ $permissions[] = [
+ 'name' => "{$modelName}.{$policyName}",
+ 'description' => "permissions.{$modelName}.{$policyName}",
+ 'group' => $modelName,
+ 'guard_name' => 'web',
+ 'roles' => $roles,
+ ];
+ }
+
+ return $permissions;
+ }
+}
diff --git a/app/Domains/Identity/Enums/UserSettingKey.php b/app/Domains/Identity/Enums/UserSettingKey.php
new file mode 100644
index 0000000..d38e90d
--- /dev/null
+++ b/app/Domains/Identity/Enums/UserSettingKey.php
@@ -0,0 +1,73 @@
+value}");
+ }
+
+ public static function effect(string $settingKey, string|int $value): void
+ {
+ match ($settingKey) {
+ self::LANGUAGE->value => app()->setLocale($value),
+ self::TIMEZONE->value => date_default_timezone_set($value),
+ default => null,
+ };
+ }
+
+ public function default(): mixed
+ {
+ return match ($this) {
+ self::NOTIFICATION => 0,
+ self::LANGUAGE => 'en',
+ self::TIMEZONE => 'UTC',
+ };
+ }
+
+ public function type(): string
+ {
+ return match ($this) {
+ self::NOTIFICATION,
+ self::LANGUAGE,
+ self::TIMEZONE => 'option',
+ };
+ }
+
+ public function options(): array
+ {
+ return match ($this) {
+ self::LANGUAGE => [
+ 'en' => __('domains/account/enum.user_settings.options.language.en'),
+ 'id' => __('domains/account/enum.user_settings.options.language.id'),
+ ],
+ self::TIMEZONE => [
+ 'UTC' => 'UTC',
+ 'Asia/Jakarta' => 'Asia/Jakarta',
+ 'Asia/Makassar' => 'Asia/Makassar',
+ 'Asia/Jayapura' => 'Asia/Jayapura',
+ ],
+ self::NOTIFICATION => [
+ 1 => __('domains/account/enum.user_settings.options.notification.on'),
+ 0 => __('domains/account/enum.user_settings.options.notification.off'),
+ ],
+ default => [],
+ };
+ }
+
+ public function validation(): array
+ {
+ return [
+ 'required',
+ Rule::in(array_keys($this->options())),
+ ];
+ }
+}
diff --git a/app/Domains/Identity/Enums/UserStatus.php b/app/Domains/Identity/Enums/UserStatus.php
new file mode 100644
index 0000000..7794e8d
--- /dev/null
+++ b/app/Domains/Identity/Enums/UserStatus.php
@@ -0,0 +1,27 @@
+value);
+ }
+
+ public function badgeVariant(): string
+ {
+ return match ($this) {
+ self::ACTIVE => 'success',
+ self::INACTIVE => 'danger',
+ };
+ }
+
+ public function isActive(): bool
+ {
+ return $this == self::ACTIVE;
+ }
+}
diff --git a/app/Domains/Identity/Events/Authentication/UserLoggedIn.php b/app/Domains/Identity/Events/Authentication/UserLoggedIn.php
new file mode 100644
index 0000000..13302e7
--- /dev/null
+++ b/app/Domains/Identity/Events/Authentication/UserLoggedIn.php
@@ -0,0 +1,26 @@
+with('profile');
+ }
+
+ public function headings(): array
+ {
+ return [
+ 'No',
+ 'Name',
+ 'Email',
+ 'Gender',
+ 'Date Of Birth',
+ 'Phone',
+ ];
+ }
+
+ public function map(mixed $row): array
+ {
+ $gender = $row->profile?->gender?->label() ?: '-';
+ $dateOfBirth = $row->profile?->date_of_birth ?: '-';
+ $phone = $row->profile?->phone_number ?: '-';
+
+ return [
+ '=ROW()-1',
+ $row->name,
+ $row->email,
+ $gender,
+ $dateOfBirth,
+ $phone,
+ ];
+ }
+
+ /**
+ * Apply Excel formatting to specific columns (Dates, Currency, Percentages).
+ */
+ public function columnFormats(): array
+ {
+ return [
+ 'A' => NumberFormat::FORMAT_NUMBER,
+ 'B' => NumberFormat::FORMAT_TEXT,
+ 'C' => NumberFormat::FORMAT_TEXT,
+ 'D' => NumberFormat::FORMAT_TEXT,
+ 'E' => NumberFormat::FORMAT_DATE_DDMMYYYY,
+ 'F' => NumberFormat::FORMAT_NUMBER,
+ ];
+ }
+}
diff --git a/app/Domains/Identity/Integration/Mappers/UserDataMapper.php b/app/Domains/Identity/Integration/Mappers/UserDataMapper.php
new file mode 100644
index 0000000..df57154
--- /dev/null
+++ b/app/Domains/Identity/Integration/Mappers/UserDataMapper.php
@@ -0,0 +1,82 @@
+ trim((string) $rawData['name']),
+ 'email' => trim((string) $rawData['email']),
+ 'role' => trim((string) ($rawData['role'] ?? 'user')),
+ 'gender' => isset($rawData['gender']) ? trim((string) $rawData['gender']) : null,
+ 'date_of_birth' => isset($rawData['date_of_birth']) ? trim((string) $rawData['date_of_birth']) : null,
+ 'phone_number' => isset($rawData['phone']) ? trim((string) $rawData['phone']) : null,
+ ];
+ }
+
+ /**
+ * Coordinate and execute internal domain mutations using the transformed payload.
+ *
+ * @throws Throwable
+ */
+ public function updateOrCreateDomainState(array $payload, ?Model $model = null): void
+ {
+ /** @var User|null $user */
+ $user = $model;
+
+ if ($user) {
+ $this->updateUser->execute(
+ $user,
+ new UpdateUserIdentityDTO(name: $payload['name'], email: $payload['email'])
+ );
+ } else {
+ $user = $this->provisionNewUser->execute(
+ new ProvisionUserDTO(
+ name: $payload['name'],
+ email: $payload['email'],
+ password: Str::random(8),
+ role: RoleType::USER->value
+ )
+ );
+ }
+
+ // Handle cross-domain profile write details
+ UserImportWasProcessed::dispatch(
+ userId: $user->id,
+ email: $payload['email'],
+ gender: $payload['gender'],
+ dateOfBirth: $payload['date_of_birth'],
+ phoneNumber: $payload['phone_number']
+ );
+ }
+}
diff --git a/app/Domains/Identity/Listeners/Authentication/SendSignInActivityNotification.php b/app/Domains/Identity/Listeners/Authentication/SendSignInActivityNotification.php
new file mode 100644
index 0000000..6d067e3
--- /dev/null
+++ b/app/Domains/Identity/Listeners/Authentication/SendSignInActivityNotification.php
@@ -0,0 +1,46 @@
+
+ */
+ public array $backoff = [10, 30, 60];
+
+ /**
+ * Handle the event.
+ *
+ * Sends a sign-in activity notification (email + database) to the user.
+ * Runs asynchronously via the queue — fully decoupled from the HTTP session.
+ */
+ public function handle(UserLoggedIn $event): void
+ {
+ $event->user->notify(
+ new SignInActivity($event->ipAddress, $event->userAgent),
+ );
+ }
+
+ /**
+ * Handle a job failure.
+ */
+ public function failed(UserLoggedIn $event, Throwable $exception): void
+ {
+ report($exception);
+ }
+}
diff --git a/app/Domains/Identity/Listeners/Governance/SendUserActivatedNotification.php b/app/Domains/Identity/Listeners/Governance/SendUserActivatedNotification.php
new file mode 100644
index 0000000..a848a4a
--- /dev/null
+++ b/app/Domains/Identity/Listeners/Governance/SendUserActivatedNotification.php
@@ -0,0 +1,15 @@
+email)->send(new UserActivatedEmail);
+ }
+}
diff --git a/app/Domains/Identity/Listeners/Governance/SendUserPurgedNotification.php b/app/Domains/Identity/Listeners/Governance/SendUserPurgedNotification.php
new file mode 100644
index 0000000..75545d4
--- /dev/null
+++ b/app/Domains/Identity/Listeners/Governance/SendUserPurgedNotification.php
@@ -0,0 +1,18 @@
+email)->send(new UserPurgedEmail);
+ }
+}
diff --git a/app/Domains/Identity/Listeners/Governance/SendUserSuspendedNotification.php b/app/Domains/Identity/Listeners/Governance/SendUserSuspendedNotification.php
new file mode 100644
index 0000000..176a8d2
--- /dev/null
+++ b/app/Domains/Identity/Listeners/Governance/SendUserSuspendedNotification.php
@@ -0,0 +1,18 @@
+email)->send(new UserSuspendedEmail);
+ }
+}
diff --git a/app/Domains/Identity/Mail/Governance/UserActivatedEmail.php b/app/Domains/Identity/Mail/Governance/UserActivatedEmail.php
new file mode 100644
index 0000000..255abd9
--- /dev/null
+++ b/app/Domains/Identity/Mail/Governance/UserActivatedEmail.php
@@ -0,0 +1,61 @@
+line(__('domains/identity/notifications.governance.user_activated.intro'))
+ ->line(__('domains/identity/notifications.governance.user_activated.body'))
+ ->line(__('domains/identity/notifications.governance.user_activated.outro'))
+ ->render();
+
+ return new Content(
+ htmlString: $message,
+ );
+ }
+
+ /**
+ * Get the attachments for the message.
+ *
+ * @return array
+ */
+ public function attachments(): array
+ {
+ return [];
+ }
+}
diff --git a/app/Domains/Identity/Mail/Governance/UserPurgedEmail.php b/app/Domains/Identity/Mail/Governance/UserPurgedEmail.php
new file mode 100644
index 0000000..a77be20
--- /dev/null
+++ b/app/Domains/Identity/Mail/Governance/UserPurgedEmail.php
@@ -0,0 +1,61 @@
+line(__('domains/identity/notifications.governance.user_purged.intro'))
+ ->line(__('domains/identity/notifications.governance.user_purged.body'))
+ ->line(__('domains/identity/notifications.governance.user_purged.outro'))
+ ->render();
+
+ return new Content(
+ htmlString: $message,
+ );
+ }
+
+ /**
+ * Get the attachments for the message.
+ *
+ * @return array
+ */
+ public function attachments(): array
+ {
+ return [];
+ }
+}
diff --git a/app/Domains/Identity/Mail/Governance/UserSuspendedEmail.php b/app/Domains/Identity/Mail/Governance/UserSuspendedEmail.php
new file mode 100644
index 0000000..42015be
--- /dev/null
+++ b/app/Domains/Identity/Mail/Governance/UserSuspendedEmail.php
@@ -0,0 +1,61 @@
+line(__('domains/identity/notifications.governance.user_suspended.intro'))
+ ->line(__('domains/identity/notifications.governance.user_suspended.body'))
+ ->line(__('domains/identity/notifications.governance.user_suspended.outro'))
+ ->render();
+
+ return new Content(
+ htmlString: $message,
+ );
+ }
+
+ /**
+ * Get the attachments for the message.
+ *
+ * @return array
+ */
+ public function attachments(): array
+ {
+ return [];
+ }
+}
diff --git a/app/Domains/Identity/Models/Permission.php b/app/Domains/Identity/Models/Permission.php
new file mode 100644
index 0000000..374e466
--- /dev/null
+++ b/app/Domains/Identity/Models/Permission.php
@@ -0,0 +1,12 @@
+ */
+ use HasFactory;
+
+ use HasFile;
+ use HasRoles;
+ use HasUlids;
+ use HasApiTokens;
+ use Notifiable;
+ use \OwenIt\Auditing\Auditable;
+
+ /**
+ * Cast attributes
+ *
+ * @var array
+ */
+ protected $casts = [
+ 'email_verified_at' => 'datetime',
+ 'password' => 'hashed',
+ 'settings' => 'collection',
+ 'status' => UserStatus::class,
+ ];
+
+ /**
+ * Attributes to include in the Audit.
+ */
+ protected array $auditInclude = [
+ 'name',
+ 'email',
+ 'status',
+ ];
+
+ protected static function newFactory(): Factory
+ {
+ return UserFactory::new();
+ }
+
+ public function sendPasswordResetNotification($token): void
+ {
+ // This overrides the default CanResetPassword trait method
+ $this->notify(new ResetPasswordNotification($token));
+ }
+
+ public function sendEmailVerificationNotification(): void
+ {
+ $this->notify(new VerifyEmailNotification);
+ }
+
+ public function uniqueIds(): array
+ {
+ return ['ulid'];
+ }
+
+ public function getRoleNameAttribute()
+ {
+ return $this->roles->first()->name ?? '-';
+ }
+
+ public function avatar(): MorphOne
+ {
+ return $this->hasSingleFile('avatar');
+ }
+
+ public function profile(): HasOne
+ {
+ return $this->hasOne(Profile::class);
+ }
+}
diff --git a/app/Domains/Identity/Notifications/ResetPasswordNotification.php b/app/Domains/Identity/Notifications/ResetPasswordNotification.php
new file mode 100644
index 0000000..c3da566
--- /dev/null
+++ b/app/Domains/Identity/Notifications/ResetPasswordNotification.php
@@ -0,0 +1,39 @@
+ $this->token,
+ 'email' => $notifiable->getEmailForPasswordReset(),
+ ]));
+ return (new MailMessage)
+ ->subject(__('domains/auth/notifications.reset_password.subject'))
+ ->line(__('domains/auth/notifications.reset_password.intro'))
+ ->action(__('domains/auth/notifications.reset_password.action'), $url)
+ ->line(__('domains/auth/notifications.reset_password.outro'));
+ }
+
+ public function toArray(object $notifiable): array
+ {
+ return [];
+ }
+}
diff --git a/app/Domains/Identity/Notifications/SignInActivity.php b/app/Domains/Identity/Notifications/SignInActivity.php
new file mode 100644
index 0000000..4315783
--- /dev/null
+++ b/app/Domains/Identity/Notifications/SignInActivity.php
@@ -0,0 +1,66 @@
+ip = $ip ?? 'Unknown';
+ $this->userAgent = $userAgent ?? 'Unknown';
+ }
+
+ /**
+ * Get the notification's delivery channels.
+ *
+ * @return array
+ */
+ public function via(object $notifiable): array
+ {
+ return ['mail', 'database'];
+ }
+
+ /**
+ * Get the mail representation of the notification.
+ */
+ public function toMail(object $notifiable): MailMessage
+ {
+ return (new MailMessage)
+ ->subject(__('domains/auth/notifications.sign_in_activity.subject'))
+ ->greeting(__('domains/auth/notifications.sign_in_activity.greeting', ['name' => $notifiable->name ?? '']))
+ ->line(__('domains/auth/notifications.sign_in_activity.intro', ['app' => config('app.name')]))
+ ->line(__('domains/auth/notifications.sign_in_activity.time', ['time' => now()->toDayDateTimeString()]))
+ ->line(__('domains/auth/notifications.sign_in_activity.ip', ['ip' => $this->ip]))
+ ->line(__('domains/auth/notifications.sign_in_activity.browser', ['browser' => $this->userAgent]))
+ ->line(__('domains/auth/notifications.sign_in_activity.outro'))
+ ->action(__('domains/auth/notifications.sign_in_activity.action'), url('/dashboard'));
+ }
+
+ /**
+ * Get the array representation of the notification.
+ *
+ * @return array
+ */
+ public function toArray(object $notifiable): array
+ {
+ return [
+ 'title' => __('domains/auth/notifications.sign_in_activity.title'),
+ 'message' => __('domains/auth/notifications.sign_in_activity.message', ['ip' => $this->ip]),
+ 'url' => url('/dashboard'),
+ ];
+ }
+}
diff --git a/app/Domains/Identity/Notifications/VerifyEmailNotification.php b/app/Domains/Identity/Notifications/VerifyEmailNotification.php
new file mode 100644
index 0000000..55f34b7
--- /dev/null
+++ b/app/Domains/Identity/Notifications/VerifyEmailNotification.php
@@ -0,0 +1,34 @@
+verificationUrl($notifiable);
+
+ return (new MailMessage)
+ ->subject(__('domains/auth/notifications.verify_email.subject'))
+ ->line(__('domains/auth/notifications.verify_email.intro'))
+ ->action(__('domains/auth/notifications.verify_email.action'), $url)
+ ->line(__('domains/auth/notifications.verify_email.outro'));
+ }
+
+ public function toArray(object $notifiable): array
+ {
+ return [];
+ }
+}
diff --git a/app/Domains/Identity/Policies/RolePolicy.php b/app/Domains/Identity/Policies/RolePolicy.php
new file mode 100644
index 0000000..e08d16f
--- /dev/null
+++ b/app/Domains/Identity/Policies/RolePolicy.php
@@ -0,0 +1,49 @@
+can('role.viewAny');
+ }
+
+ /**
+ * Determine whether the user can view the model.
+ */
+ public function view(User $user, Role $model): bool
+ {
+ return $user->can('role.view');
+ }
+
+ /**
+ * Determine whether the user can create models.
+ */
+ public function create(User $user): bool
+ {
+ return $user->can('role.create');
+ }
+
+ /**
+ * Determine whether the user can update the model.
+ */
+ public function update(User $user, Role $model): bool
+ {
+ return $user->can('role.update');
+ }
+
+ /**
+ * Determine whether the user can delete the model.
+ */
+ public function delete(User $user, Role $model): bool
+ {
+ return $user->can('role.delete');
+ }
+}
diff --git a/app/Domains/Identity/Policies/UserPolicy.php b/app/Domains/Identity/Policies/UserPolicy.php
new file mode 100644
index 0000000..41f2248
--- /dev/null
+++ b/app/Domains/Identity/Policies/UserPolicy.php
@@ -0,0 +1,57 @@
+hasPermissionTo('user.viewAny');
+ }
+
+ /**
+ * Determine whether the user can view the model.
+ */
+ public function view(User $user, User $model): bool
+ {
+ return $user->hasPermissionTo('user.view');
+ }
+
+ /**
+ * Determine whether the user can create models.
+ */
+ public function create(User $user): bool
+ {
+ return $user->hasPermissionTo('user.create');
+ }
+
+ /**
+ * Determine whether the user can update the model.
+ */
+ public function update(User $user, User $model): bool
+ {
+ if ($model->hasRole(RoleType::SYSTEM_ADMIN)) {
+ return false;
+ }
+
+ return $user->hasPermissionTo('user.update');
+ }
+
+ /**
+ * Determine whether the user can delete the model.
+ */
+ public function delete(User $user, User $model): bool
+ {
+ if ($model->hasRole(RoleType::SYSTEM_ADMIN)) {
+ return false;
+ }
+
+ return $user->hasPermissionTo('user.delete');
+ }
+}
diff --git a/app/Domains/Identity/Providers/IdentityServiceProvider.php b/app/Domains/Identity/Providers/IdentityServiceProvider.php
new file mode 100644
index 0000000..b44cb47
--- /dev/null
+++ b/app/Domains/Identity/Providers/IdentityServiceProvider.php
@@ -0,0 +1,45 @@
+ [
+ SendSignInActivityNotification::class,
+ ],
+ UserWasActivated::class => [
+ SendUserActivatedNotification::class,
+ ],
+ UserWasPurged::class => [
+ SendUserPurgedNotification::class,
+ ],
+ UserWasSuspended::class => [
+ SendUserSuspendedNotification::class,
+ ]
+ ];
+
+ public function register(): void
+ {
+ $this->app->singleton(GetAuthenticatedUserContext::class, fn () => new GetAuthenticatedUserContext);
+ }
+
+ public function boot(): void
+ {
+ File::resolveRelationUsing('uploader', fn (File $file) => $file->belongsTo(User::class, 'uploader_id'));
+ }
+}
diff --git a/app/Domains/Identity/Queries/Dashboard/GetMonthlyNewUsers.php b/app/Domains/Identity/Queries/Dashboard/GetMonthlyNewUsers.php
new file mode 100644
index 0000000..2597ec8
--- /dev/null
+++ b/app/Domains/Identity/Queries/Dashboard/GetMonthlyNewUsers.php
@@ -0,0 +1,43 @@
+subMonth();
+
+ $newUser = User::whereMonth('created_at', $now->month)
+ ->whereYear('created_at', $now->year)
+ ->count();
+
+ $newUserLastMonth = User::whereMonth('created_at', $lastMonth->month)
+ ->whereYear('created_at', $lastMonth->year)
+ ->count();
+
+ if ($newUserLastMonth > 0) {
+ $growthRate = ($newUser - $newUserLastMonth) / $newUserLastMonth;
+ $growthRate = $growthRate * 100;
+ $growthRate = number_format($growthRate, 2, '.', '');
+ if ($growthRate > 0) {
+ $growthRate = "+{$growthRate}%";
+ } else {
+ $growthRate = "{$growthRate}%";
+ }
+ } else {
+ $growthRate = '+100%';
+ }
+
+ return [
+ 'new_users' => $newUser,
+ 'growth_rate' => $growthRate,
+ ];
+ }
+}
diff --git a/app/Domains/Identity/Queries/Dashboard/GetRoleDistributions.php b/app/Domains/Identity/Queries/Dashboard/GetRoleDistributions.php
new file mode 100644
index 0000000..b4ebbc9
--- /dev/null
+++ b/app/Domains/Identity/Queries/Dashboard/GetRoleDistributions.php
@@ -0,0 +1,23 @@
+withCount('users')
+ ->get();
+
+ $series = $roles->map(fn ($role) => $role->users_count);
+ $categories = $roles->map(fn ($role) => $role->name);
+
+ return [
+ 'series' => $series,
+ 'categories' => $categories,
+ ];
+ }
+}
diff --git a/app/Domains/Identity/Queries/Dashboard/GetTotalUsers.php b/app/Domains/Identity/Queries/Dashboard/GetTotalUsers.php
new file mode 100644
index 0000000..5299d2b
--- /dev/null
+++ b/app/Domains/Identity/Queries/Dashboard/GetTotalUsers.php
@@ -0,0 +1,35 @@
+subMonth();
+
+ $currentTotalUsers = User::count();
+ $totalUsersLastMonth = User::whereMonth('created_at', $lastMonth->month)
+ ->whereYear('created_at', $lastMonth->year)
+ ->count();
+
+ if ($totalUsersLastMonth > 0) {
+ $growthRate = ($currentTotalUsers - $totalUsersLastMonth) / $totalUsersLastMonth;
+ $growthRate = $growthRate * 100;
+ $growthRate = number_format($growthRate, 2, '.', '');
+ $growthRate = "+{$growthRate}%";
+ } else {
+ $growthRate = '+100%';
+ }
+
+ return [
+ 'total_users' => $currentTotalUsers,
+ 'growth_rate' => $growthRate,
+ ];
+ }
+}
diff --git a/app/Domains/Identity/Queries/Dashboard/GetUserGrowthTrends.php b/app/Domains/Identity/Queries/Dashboard/GetUserGrowthTrends.php
new file mode 100644
index 0000000..d306758
--- /dev/null
+++ b/app/Domains/Identity/Queries/Dashboard/GetUserGrowthTrends.php
@@ -0,0 +1,38 @@
+, series: array}
+ */
+ public function fetch(): array
+ {
+ $year = now()->year;
+ $records = User::query()
+ ->selectRaw('created_at')
+ ->whereYear('created_at', $year)
+ ->get()
+ ->groupBy(fn ($user) => $user->created_at->month)
+ ->map(fn ($group) => $group->count())
+ ->toArray();
+
+ $categories = [];
+ $series = [];
+
+ // Ensure all 12 months are populated, filling gaps with 0
+ for ($i = 1; $i <= 12; $i++) {
+ $categories[] = Carbon::create()->month($i)->translatedFormat('F');
+ $series[] = $records[$i] ?? 0;
+ }
+
+ return [
+ 'categories' => $categories,
+ 'series' => $series,
+ ];
+ }
+}
diff --git a/app/Domains/Identity/Queries/Dashboard/GetUserVerificationRates.php b/app/Domains/Identity/Queries/Dashboard/GetUserVerificationRates.php
new file mode 100644
index 0000000..6a44702
--- /dev/null
+++ b/app/Domains/Identity/Queries/Dashboard/GetUserVerificationRates.php
@@ -0,0 +1,31 @@
+selectRaw('COUNT(id) as total')
+ ->selectRaw('SUM(CASE WHEN email_verified_at IS NOT NULL THEN 1 ELSE 0 END) as verified')
+ ->first();
+
+ $userTotal = (int) $stats->total;
+ $userVerified = (int) ($stats->verified ?? 0);
+ $userUnverified = $userTotal - $userVerified;
+ $verificationRate = $userTotal > 0 ? round($userVerified / $userTotal * 100) : 0;
+
+ return [
+ 'verified' => $userVerified,
+ 'unverified' => $userUnverified,
+ 'verification_rate' => $verificationRate,
+ ];
+ }
+}
diff --git a/app/Domains/Identity/Queries/GetAuthenticatedUserContext.php b/app/Domains/Identity/Queries/GetAuthenticatedUserContext.php
new file mode 100644
index 0000000..a117842
--- /dev/null
+++ b/app/Domains/Identity/Queries/GetAuthenticatedUserContext.php
@@ -0,0 +1,29 @@
+cache !== null) {
+ return $this->cache['user'];
+ }
+
+ $user = Auth::user();
+ $user?->loadMissing(['avatar', 'profile']);
+ $this->cache['user'] = $user;
+
+ return $this->cache['user'];
+ }
+
+ public function refresh(): void
+ {
+ $this->cache = null;
+ }
+}
diff --git a/app/Domains/Identity/Queries/Lookup/RoleLookup.php b/app/Domains/Identity/Queries/Lookup/RoleLookup.php
new file mode 100644
index 0000000..86a81ba
--- /dev/null
+++ b/app/Domains/Identity/Queries/Lookup/RoleLookup.php
@@ -0,0 +1,17 @@
+select('name')
+ ->where('name', 'like', "%{$search}%")
+ ->get();
+ }
+}
diff --git a/app/Domains/System/Actions/Backup/DeleteBackup.php b/app/Domains/System/Actions/Backup/DeleteBackup.php
new file mode 100644
index 0000000..3169ecc
--- /dev/null
+++ b/app/Domains/System/Actions/Backup/DeleteBackup.php
@@ -0,0 +1,16 @@
+disk)->delete($backup->path);
+
+ $backup->delete();
+ }
+}
diff --git a/app/Domains/System/Actions/Backup/SyncBackupCatalog.php b/app/Domains/System/Actions/Backup/SyncBackupCatalog.php
new file mode 100644
index 0000000..f02d3c8
--- /dev/null
+++ b/app/Domains/System/Actions/Backup/SyncBackupCatalog.php
@@ -0,0 +1,45 @@
+files($backupDirectory);
+ $physicalFilePaths = [];
+
+ foreach ($files as $file) {
+ if (Str::endsWith($file, '.zip')) {
+ // Ensure we just store the relative filename if that's your convention
+ $filename = basename($file);
+ $filename = $backupDirectory.' '.basename($filename);
+ $physicalFilePaths[] = $file;
+
+ // Restore the 'disappeared' record using the file's raw metadata
+ Backup::updateOrCreate(
+ ['file_name' => $filename],
+ [
+ 'path' => $file,
+ 'size' => $disk->size($file), // Handled by your ByteUsage cast
+ 'type' => 'full', // Or parse filename if you differentiate types
+ 'disk' => $diskName,
+ ]
+ );
+ }
+ }
+
+ // Clean up ghost records (in DB, but physical file was deleted)
+ Backup::whereNotIn('path', $physicalFilePaths)->delete();
+ }
+}
diff --git a/app/Domains/System/Actions/Backup/SystemBackup.php b/app/Domains/System/Actions/Backup/SystemBackup.php
new file mode 100644
index 0000000..4992d93
--- /dev/null
+++ b/app/Domains/System/Actions/Backup/SystemBackup.php
@@ -0,0 +1,42 @@
+allFiles($backupName);
+ if (empty($files)) {
+ throw new Exception(__('domains/system/messages.backup.verification_error'));
+ }
+
+ $latestFile = collect($files)->last();
+ $fileName = $backupName.' '.basename($latestFile);
+ $sizeInBytes = Storage::disk($disk)->size($latestFile);
+
+ return Backup::create([
+ 'file_name' => $fileName,
+ 'disk' => $disk,
+ 'path' => $latestFile,
+ 'size' => $sizeInBytes,
+ 'type' => 'full',
+ ]);
+ }
+}
diff --git a/app/Domains/System/Actions/Backup/SystemRestore.php b/app/Domains/System/Actions/Backup/SystemRestore.php
new file mode 100644
index 0000000..dd81959
--- /dev/null
+++ b/app/Domains/System/Actions/Backup/SystemRestore.php
@@ -0,0 +1,69 @@
+disk)->path($backup->path);
+ $restorePath = storage_path('app/restore-temp');
+ if (! file_exists($backupPath)) {
+ throw new Exception(__('domains/system/messages.backup.file_not_found'));
+ }
+
+ $zip = new ZipArchive;
+ if ($zip->open($backupPath) === true) {
+ $zip->extractTo($restorePath);
+ $zip->close();
+
+ $this->restoreDatabase($restorePath);
+ $this->restoreFiles($restorePath);
+
+ File::deleteDirectory($restorePath);
+
+ $this->syncBackupCatalog->execute();
+ }
+ }
+
+ private function restoreDatabase(string $tempPath): void
+ {
+ $dbDump = $tempPath.'/db-dumps/mysql-'.config('database.connections.mysql.database').'.sql';
+ if (file_exists($dbDump)) {
+ $restoreSql = Process::run([
+ 'mysql',
+ '-u', config('database.connections.mysql.username'),
+ '-p'.config('database.connections.mysql.password'),
+ '-h'.config('database.connections.mysql.host'),
+ config('database.connections.mysql.database'),
+ '-e', "source {$dbDump}",
+ ]);
+
+ if ($restoreSql->failed()) {
+ throw new RuntimeException(__('domains/system/messages.backup.restored_error').' '.$restoreSql->errorOutput());
+ }
+ }
+ }
+
+ private function restoreFiles(string $tempPath): void
+ {
+ $uploadsPath = $tempPath.'/storage';
+
+ if (file_exists($uploadsPath)) {
+ File::copyDirectory($uploadsPath, storage_path());
+ }
+ }
+}
diff --git a/app/Domains/System/Actions/Backup/UploadBackupFile.php b/app/Domains/System/Actions/Backup/UploadBackupFile.php
new file mode 100644
index 0000000..1ee4c04
--- /dev/null
+++ b/app/Domains/System/Actions/Backup/UploadBackupFile.php
@@ -0,0 +1,21 @@
+storeAs($backupDirectory, $file->getClientOriginalName(), ['disk' => $diskName]);
+ $this->syncBackupCatalog->execute();
+ }
+}
diff --git a/app/Domains/System/Actions/Files/PruneOrphanedFiles.php b/app/Domains/System/Actions/Files/PruneOrphanedFiles.php
new file mode 100644
index 0000000..bd9093f
--- /dev/null
+++ b/app/Domains/System/Actions/Files/PruneOrphanedFiles.php
@@ -0,0 +1,57 @@
+ 0, 'disk_orphans_removed' => 0];
+
+ // --- SWEEP 1: Clean up orphaned Database Records ---
+ // Find files where the parent model (fileable) has disappeared
+ $orphanedRecords = File::whereDoesntHaveMorph('fileable', '*')->get();
+
+ foreach ($orphanedRecords as $record) {
+ // Because we set up the self-cleaning booted() method earlier,
+ // calling delete() here will automatically remove the physical file too!
+ $record->delete();
+ $stats['db_orphans_removed']++;
+ }
+
+ // --- SWEEP 2: Clean up stranded Physical Files ---
+ // Get all files physically sitting on the disk in the target directory
+ $physicalFiles = Storage::disk($disk)->allFiles($directory);
+
+ // Get all file paths currently tracked in the database
+ $trackedFiles = File::pluck('path')->toArray();
+
+ $settingFiles = collect(SystemSettingKey::cases())
+ ->filter(fn ($key) => $key->inputType() === InputType::FILE)
+ ->map(fn ($key) => $this->settingQuery->get($key))
+ ->filter()
+ ->toArray();
+
+ $trackedFiles = array_merge($trackedFiles, $settingFiles);
+ $trackedFiles[] = 'images/logo.svg';
+
+ // Compare the two arrays to find files on disk that the DB knows nothing about
+ $strandedFiles = array_diff($physicalFiles, $trackedFiles);
+ $strandedFiles = array_filter($strandedFiles, fn ($file) => basename($file) !== '.gitignore');
+
+ if (! empty($strandedFiles)) {
+ Storage::disk($disk)->delete($strandedFiles);
+ $stats['disk_orphans_removed'] = count($strandedFiles);
+ }
+
+ return $stats;
+ }
+}
diff --git a/app/Domains/System/Actions/Files/RemoveModelFile.php b/app/Domains/System/Actions/Files/RemoveModelFile.php
new file mode 100644
index 0000000..cc3463a
--- /dev/null
+++ b/app/Domains/System/Actions/Files/RemoveModelFile.php
@@ -0,0 +1,19 @@
+where('fileable_type', $model)
+ ->get();
+
+ foreach ($files as $file) {
+ $file->delete();
+ }
+ }
+}
diff --git a/app/Domains/System/Actions/Files/ReplaceSingleFile.php b/app/Domains/System/Actions/Files/ReplaceSingleFile.php
new file mode 100644
index 0000000..7a203aa
--- /dev/null
+++ b/app/Domains/System/Actions/Files/ReplaceSingleFile.php
@@ -0,0 +1,29 @@
+modelType)
+ ->where('fileable_id', $dto->modelId)
+ ->first();
+ $oldFile?->delete(); // The physical file is destroyed via the model's booted() event
+
+ // 2. Delegate to the base upload action, passing the relation name down the chain
+ return $this->uploadAction->execute(
+ uploadedFile: $newFile,
+ dto: $dto,
+ );
+ }
+}
diff --git a/app/Domains/System/Actions/Files/UploadAndAttachFile.php b/app/Domains/System/Actions/Files/UploadAndAttachFile.php
new file mode 100644
index 0000000..b215622
--- /dev/null
+++ b/app/Domains/System/Actions/Files/UploadAndAttachFile.php
@@ -0,0 +1,34 @@
+ $dto->modelType,
+ 'fileable_id' => $dto->modelId,
+ 'relation_name' => $dto->relationName,
+ 'name' => $uploadedFile->getClientOriginalName(),
+ 'mime_type' => $uploadedFile->getMimeType(),
+ 'size' => $uploadedFile->getSize(),
+ 'disk' => $dto->disk,
+ 'options' => $dto->options,
+ 'uploader_id' => $dto->uploaderId,
+ ];
+
+ $metadata['path'] = $uploadedFile->store($dto->directory, $dto->disk);
+
+ // We dynamically call the relation method on the model (e.g., $targetModel->avatar())
+ // This ensures Laravel hooks up the polymorphic type and ID automatically.
+ return File::create($metadata);
+ }
+}
diff --git a/app/Domains/System/Actions/Integration/RunGenericImportPipeline.php b/app/Domains/System/Actions/Integration/RunGenericImportPipeline.php
new file mode 100644
index 0000000..7081a06
--- /dev/null
+++ b/app/Domains/System/Actions/Integration/RunGenericImportPipeline.php
@@ -0,0 +1,59 @@
+> $rows Raw spreadsheet rows from the current chunk
+ * @param DataPayloadMapper $mapper The specific structural mapping engine
+ * @param class-string $modelClass The Eloquent model class to query against
+ */
+ public function execute(Collection $rows, DataPayloadMapper $mapper, string $modelClass): void
+ {
+ $lookupKey = $mapper->getLookupKey();
+
+ $lookupValues = $rows->pluck($lookupKey)
+ ->filter()
+ ->map(fn ($val) => trim((string) $val))
+ ->toArray();
+
+ $existingRecords = $modelClass::query()
+ ->whereIn(column: $lookupKey, values: $lookupValues)
+ ->get()
+ ->keyBy($lookupKey);
+
+ foreach ($rows->toArray() as $row) {
+ if (empty($row[$lookupKey])) {
+ continue;
+ }
+
+ try {
+ DB::transaction(function () use ($row, $lookupKey, $mapper, $existingRecords) {
+ $lookupValue = trim((string) $row[$lookupKey]);
+
+ // Match against our pre-fetched in-memory cache
+ $matchedModel = $existingRecords->get($lookupValue);
+
+ // Normalize data columns via the mapper specification
+ $normalizedPayload = $mapper->transform($row);
+
+ // Delegate execution to the underlying actions
+ $mapper->updateOrCreateDomainState(payload: $normalizedPayload, model: $matchedModel);
+ });
+ } catch (Throwable $e) {
+ Log::error('Generic Import failure on Row processing: '.$e->getMessage(), [
+ 'row' => $row,
+ 'lookupKey' => $lookupKey,
+ ]);
+ }
+ }
+ }
+}
diff --git a/app/Domains/System/Actions/Settings/ResolveIpTimezone.php b/app/Domains/System/Actions/Settings/ResolveIpTimezone.php
new file mode 100644
index 0000000..0800907
--- /dev/null
+++ b/app/Domains/System/Actions/Settings/ResolveIpTimezone.php
@@ -0,0 +1,37 @@
+getSystemSettings->get(SystemSettingKey::TIMEZONE);
+
+ // Localhost fallback
+ if (in_array($ip, ['127.0.0.1', '::1', 'localhost'])) {
+ return $systemTZ;
+ }
+
+ // Cache the result for 24 hours (86,400 seconds)
+ return Cache::remember("timezone_ip_{$ip}", 86400, function () use ($ip, $systemTZ) {
+ try {
+ // Quick 2-second timeout so the app doesn't hang if the API is down
+ $response = Http::timeout(2)->get("http://ip-api.com/json/{$ip}?fields=timezone");
+
+ return $response->json('timezone') ?? $systemTZ;
+ } catch (Exception $e) {
+ // If API fails, safely fallback to UTC
+ return $systemTZ;
+ }
+ });
+ }
+}
diff --git a/app/Domains/System/Actions/Settings/UpdateSettings.php b/app/Domains/System/Actions/Settings/UpdateSettings.php
new file mode 100644
index 0000000..fd2f34f
--- /dev/null
+++ b/app/Domains/System/Actions/Settings/UpdateSettings.php
@@ -0,0 +1,38 @@
+value;
+
+ if ($dto->key->isImage()) {
+ $currentSettings = SystemSettings::where('key', $dto->key->value)->value('value');
+
+ if ($currentSettings) {
+ remove_file($currentSettings);
+ }
+
+ if ($value instanceof TemporaryUploadedFile) {
+ $value = upload_file(
+ file: $value,
+ path: '/system/settings/'.$dto->key->value,
+ disk: 'public'
+ );
+ }
+ }
+
+ SystemSettings::updateOrCreate(
+ ['key' => $dto->key->value],
+ ['value' => $value],
+ );
+
+ cache()->forget('system_settings');
+ }
+}
diff --git a/app/Domains/System/Casts/ByteHumanReadable.php b/app/Domains/System/Casts/ByteHumanReadable.php
new file mode 100644
index 0000000..7b56712
--- /dev/null
+++ b/app/Domains/System/Casts/ByteHumanReadable.php
@@ -0,0 +1,37 @@
+bytes;
+ }
+
+ if (is_numeric($value)) {
+ return (int) $value;
+ }
+
+ throw new InvalidArgumentException("The {$key} attribute must be an integer or an instance of ByteUsage.");
+ }
+}
diff --git a/app/Domains/System/DTOs/DeleteBackupDTO.php b/app/Domains/System/DTOs/DeleteBackupDTO.php
new file mode 100644
index 0000000..823b3a7
--- /dev/null
+++ b/app/Domains/System/DTOs/DeleteBackupDTO.php
@@ -0,0 +1,10 @@
+value));
+ }
+
+ public static function section(): array
+ {
+ return [
+ [
+ __('domains/system/pages.settings.sections.web') => [
+ self::WEB_NAME,
+ self::WEB_DESCRIPTION,
+ self::WEB_ADDRESS,
+ self::WEB_PHONE,
+ self::WEB_EMAIL,
+ self::WEB_LOGO,
+ self::WEB_FAVICON,
+ ],
+ ],
+ [
+ __('domains/system/pages.settings.sections.general') => [
+ self::DEFAULT_LANGUAGE,
+ self::TIMEZONE,
+ ],
+ __('domains/system/pages.settings.sections.webmaster') => [
+ self::GOOGLE_TAG_MANAGER_ID,
+ self::GOOGLE_WEBMASTER_ID,
+ ],
+ ],
+ ];
+ }
+
+ public function inputType(): InputType
+ {
+ return match ($this) {
+ self::WEB_LOGO, self::WEB_FAVICON => InputType::FILE,
+ self::DEFAULT_LANGUAGE, self::TIMEZONE => InputType::SELECT,
+ self::WEB_DESCRIPTION => InputType::TEXTAREA,
+ default => InputType::TEXTLINE
+ };
+ }
+
+ public function inputAttributes(): array
+ {
+ $options = match ($this) {
+ self::WEB_LOGO, self::WEB_FAVICON => [
+ 'allow-image-crop' => true,
+ 'allow-image-resize' => true,
+ 'allow-image-transform' => true,
+ 'image-crop-aspect-ratio' => '1:1',
+ 'image-resize-target-width' => '500',
+ 'image-resize-target-height' => '500',
+ ],
+ self::TIMEZONE, self::DEFAULT_LANGUAGE => ['options' => $this->options()],
+ default => [],
+ };
+ $options['label'] = $this->label();
+
+ return $options;
+ }
+
+ public function validation(): array
+ {
+ return match ($this) {
+ self::WEB_LOGO, self::WEB_FAVICON => ['required', 'file', 'mimetypes:'.implode(',', FileType::IMAGE->mimeType()), 'max:1024'],
+ default => ['required', 'string'],
+ };
+ }
+
+ public function default(): ?string
+ {
+ return match ($this) {
+ self::DEFAULT_LANGUAGE => 'en',
+ self::TIMEZONE => 'UTC',
+ self::WEB_NAME => 'Acme Inc',
+ self::WEB_ADDRESS => '123 Main St, Anytown, USA',
+ self::WEB_PHONE => '+1234567890',
+ self::WEB_EMAIL => 'acme@web.io',
+ default => null
+ };
+ }
+
+ public function options(): array
+ {
+ return match ($this) {
+ self::DEFAULT_LANGUAGE => [
+ 'en' => 'English',
+ 'id' => 'Indonesian',
+ ],
+ self::TIMEZONE => [
+ 'UTC' => 'UTC',
+ 'Asia/Jakarta' => 'Asia/Jakarta',
+ 'Asia/Makassar' => 'Asia/Makassar',
+ 'Asia/Jayapura' => 'Asia/Jayapura',
+ ],
+ default => [],
+ };
+ }
+
+ public function isImage(): bool
+ {
+ return in_array($this, [
+ self::WEB_LOGO,
+ self::WEB_FAVICON,
+ ]);
+ }
+}
diff --git a/app/Domains/System/Events/ExportCompleted.php b/app/Domains/System/Events/ExportCompleted.php
new file mode 100644
index 0000000..33f5b50
--- /dev/null
+++ b/app/Domains/System/Events/ExportCompleted.php
@@ -0,0 +1,19 @@
+addMinutes(5);
+ }
+
+ if (Storage::disk('local')->exists($path)) {
+ return url(Storage::temporaryUrl($path, $time, $options));
+ }
+
+ if (Storage::disk('public')->exists($path)) {
+ return url(Storage::url($path));
+ }
+
+ return false;
+ }
+}
+
+if (! function_exists('upload_file')) {
+ function upload_file(UploadedFile|TemporaryUploadedFile $file, string $path, string $disk = 'local'): bool|string
+ {
+ return $file->store($path, $disk);
+ }
+}
+
+if (! function_exists('remove_file')) {
+ function remove_file(string $path): void
+ {
+ if (Storage::disk('local')->exists($path)) {
+ Storage::disk('local')->delete($path);
+ }
+ if (Storage::disk('public')->exists($path)) {
+ Storage::disk('public')->delete($path);
+ }
+ }
+}
diff --git a/app/Domains/System/Jobs/Excel/NotifyExportReady.php b/app/Domains/System/Jobs/Excel/NotifyExportReady.php
new file mode 100644
index 0000000..8f80e02
--- /dev/null
+++ b/app/Domains/System/Jobs/Excel/NotifyExportReady.php
@@ -0,0 +1,35 @@
+recipientEmail,
+ $this->filePath,
+ $this->downloadName,
+ );
+ }
+}
diff --git a/app/Domains/System/Jobs/Excel/NotifyImportComplete.php b/app/Domains/System/Jobs/Excel/NotifyImportComplete.php
new file mode 100644
index 0000000..dd9d13e
--- /dev/null
+++ b/app/Domains/System/Jobs/Excel/NotifyImportComplete.php
@@ -0,0 +1,28 @@
+recipientEmail);
+ }
+}
diff --git a/app/Domains/System/Listeners/Excel/SendExportReportEmail.php b/app/Domains/System/Listeners/Excel/SendExportReportEmail.php
new file mode 100644
index 0000000..5cfa250
--- /dev/null
+++ b/app/Domains/System/Listeners/Excel/SendExportReportEmail.php
@@ -0,0 +1,17 @@
+recipientEmail)->send(
+ new ExcelExportEmail($event->filePath, $event->downloadName)
+ );
+ }
+}
diff --git a/app/Domains/System/Listeners/Excel/SendImportReportEmail.php b/app/Domains/System/Listeners/Excel/SendImportReportEmail.php
new file mode 100644
index 0000000..a7f797b
--- /dev/null
+++ b/app/Domains/System/Listeners/Excel/SendImportReportEmail.php
@@ -0,0 +1,16 @@
+recipientEmail)->send(
+ new ExcelImportEmail
+ );
+ }
+}
diff --git a/app/Domains/System/Listeners/Files/RemoveUserFiles.php b/app/Domains/System/Listeners/Files/RemoveUserFiles.php
new file mode 100644
index 0000000..62a706a
--- /dev/null
+++ b/app/Domains/System/Listeners/Files/RemoveUserFiles.php
@@ -0,0 +1,20 @@
+removeModelFile->execute(
+ model: $event->model,
+ id: $event->user_id
+ );
+ }
+}
diff --git a/app/Domains/System/Mail/Excel/ExcelExportEmail.php b/app/Domains/System/Mail/Excel/ExcelExportEmail.php
new file mode 100644
index 0000000..c76bdf7
--- /dev/null
+++ b/app/Domains/System/Mail/Excel/ExcelExportEmail.php
@@ -0,0 +1,63 @@
+subject(__('domains/system/notifications.excel.export_email.subject'))
+ ->line(__('domains/system/notifications.excel.export_email.intro'))
+ ->line(__('domains/system/notifications.excel.export_email.body'))
+ ->line(__('domains/system/notifications.excel.export_email.outro'))
+ ->render();
+
+ return new Content(
+ htmlString: $message,
+ );
+ }
+
+ /**
+ * Get the attachments for the message.
+ *
+ * @return array
+ */
+ public function attachments(): array
+ {
+ return [
+ Attachment::fromStorageDisk('local', $this->filePath)->as($this->downloadName),
+ ];
+ }
+}
diff --git a/app/Domains/System/Mail/Excel/ExcelImportEmail.php b/app/Domains/System/Mail/Excel/ExcelImportEmail.php
new file mode 100644
index 0000000..037b56b
--- /dev/null
+++ b/app/Domains/System/Mail/Excel/ExcelImportEmail.php
@@ -0,0 +1,61 @@
+subject(__('domains/system/notifications.excel.import_email.subject'))
+ ->line(__('domains/system/notifications.excel.import_email.intro'))
+ ->line(__('domains/system/notifications.excel.import_email.body'))
+ ->line(__('domains/system/notifications.excel.import_email.outro'))
+ ->render();
+
+ return new Content(
+ htmlString: $message,
+ );
+ }
+
+ /**
+ * Get the attachments for the message.
+ *
+ * @return array
+ */
+ public function attachments(): array
+ {
+ return [];
+ }
+}
diff --git a/app/Domains/System/Models/Backup.php b/app/Domains/System/Models/Backup.php
new file mode 100644
index 0000000..04004ff
--- /dev/null
+++ b/app/Domains/System/Models/Backup.php
@@ -0,0 +1,17 @@
+ ByteHumanReadable::class,
+ ];
+}
diff --git a/app/Domains/System/Models/File.php b/app/Domains/System/Models/File.php
new file mode 100644
index 0000000..6491f65
--- /dev/null
+++ b/app/Domains/System/Models/File.php
@@ -0,0 +1,64 @@
+ ByteHumanReadable::class,
+ 'options' => 'array',
+ ];
+
+ protected static function newFactory(): Factory
+ {
+ return FileFactory::new();
+ }
+
+ protected static function booted(): void
+ {
+ // Whenever this model is deleted from the DB, delete the physical file
+ static::deleted(function (File $file) {
+ if ($file->path) {
+ // Ensure the disk matches where you saved it (e.g., 'public' or 's3')
+ Storage::disk($file->disk)->delete($file->path);
+ }
+ });
+ }
+
+ /**
+ * @throws Exception
+ */
+ public function getUrlAttribute(): string
+ {
+ return asset_static($this->path);
+ }
+
+ public function fileable(): MorphTo
+ {
+ return $this->morphTo();
+ }
+}
diff --git a/app/Domains/System/Models/SystemSettings.php b/app/Domains/System/Models/SystemSettings.php
new file mode 100644
index 0000000..7f43838
--- /dev/null
+++ b/app/Domains/System/Models/SystemSettings.php
@@ -0,0 +1,30 @@
+attributes['key']);
+ if ($key->inputType() == InputType::FILE && isset($this->attributes['value'])) {
+ return asset_static($this->attributes['value']);
+ } elseif ($key->inputType() == InputType::SELECT) {
+ return $key->options()[$this->attributes['value']];
+ }
+
+ return $this->attributes['value'];
+ }
+}
diff --git a/app/Domains/System/Policies/SystemSettingPolicy.php b/app/Domains/System/Policies/SystemSettingPolicy.php
new file mode 100644
index 0000000..262b9b4
--- /dev/null
+++ b/app/Domains/System/Policies/SystemSettingPolicy.php
@@ -0,0 +1,13 @@
+can('system-setting.manage');
+ }
+}
diff --git a/app/Domains/System/Providers/SystemServiceProvider.php b/app/Domains/System/Providers/SystemServiceProvider.php
new file mode 100644
index 0000000..374f613
--- /dev/null
+++ b/app/Domains/System/Providers/SystemServiceProvider.php
@@ -0,0 +1,59 @@
+ [
+ SendExportReportEmail::class,
+ ],
+ ImportCompleted::class => [
+ SendImportReportEmail::class,
+ ],
+ UserWasPurged::class => [
+ RemoveUserFiles::class,
+ ]
+ ];
+
+ public function register(): void
+ {
+ // Tell Laravel: "Whenever someone asks for GetSystemSettings,
+ // give them the exact same object instance for the entire request."
+ $this->app->singleton(GetSystemSettings::class, function ($app) {
+ return new GetSystemSettings;
+ });
+ }
+
+ /** @noinspection PhpInconsistentReturnPointsInspection */
+ public function boot(GetSystemSettings $getSystemSettings): void
+ {
+ View::composer(['components.layouts.*'], function ($view) use ($getSystemSettings) {
+ $view->with('logo', $getSystemSettings->get(SystemSettingKey::WEB_LOGO));
+ $view->with('favicon', $getSystemSettings->get(SystemSettingKey::WEB_FAVICON));
+ });
+
+ Audit::creating(function (Audit $model) {
+ if (empty($model->old_values) && empty($model->new_values)) {
+ return false;
+ }
+ });
+
+ Carbon::macro('toUserTz', fn () => $this->copy()
+ ->tz(config('app.display_timezone', 'UTC')));
+ }
+}
diff --git a/app/Domains/System/Queries/GetModelAuditLog.php b/app/Domains/System/Queries/GetModelAuditLog.php
new file mode 100644
index 0000000..90cf81f
--- /dev/null
+++ b/app/Domains/System/Queries/GetModelAuditLog.php
@@ -0,0 +1,18 @@
+audits()
+ ->with('user')
+ ->latest()
+ ->limit(5)
+ ->get();
+ }
+}
diff --git a/app/Domains/System/Queries/GetSystemSettings.php b/app/Domains/System/Queries/GetSystemSettings.php
new file mode 100644
index 0000000..c83e2e0
--- /dev/null
+++ b/app/Domains/System/Queries/GetSystemSettings.php
@@ -0,0 +1,44 @@
+fetch()[$setting->value] ?? $setting->default();
+ }
+
+ public function fetch(): array
+ {
+ if ($this->settings !== null) {
+ return $this->settings;
+ }
+
+ $this->settings = Cache::rememberForever('system_settings', function () {
+ $settings = SystemSettings::pluck('value', 'key')->toArray();
+ $finalSettings = [];
+ foreach (SystemSettingKey::cases() as $key) {
+ $finalSettings[$key->value] = $settings[$key->value] ?? $key->default();
+ }
+
+ return $finalSettings;
+ });
+
+ return $this->settings;
+ }
+
+ /**
+ * Clears the local memory. Crucial for long-running processes like Laravel Octane.
+ */
+ public function flushMemory(): void
+ {
+ $this->settings = null;
+ }
+}
diff --git a/app/Domains/System/Support/Integration/DataPayloadMapper.php b/app/Domains/System/Support/Integration/DataPayloadMapper.php
new file mode 100644
index 0000000..d49fc2a
--- /dev/null
+++ b/app/Domains/System/Support/Integration/DataPayloadMapper.php
@@ -0,0 +1,23 @@
+bytes < 0) {
+ throw new InvalidArgumentException('Bytes cannot be negative.');
+ }
+ }
+
+ /**
+ * Format the bytes into a human-readable string.
+ */
+ public function format(int $precision = 2): string
+ {
+ if ($this->bytes === 0) {
+ return '0 B';
+ }
+
+ $units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB'];
+ $base = 1024;
+
+ // Calculate the magnitude (0 for B, 1 for KB, etc.)
+ $class = min((int) log($this->bytes, $base), count($units) - 1);
+
+ // Calculate the formatted value
+ $value = round($this->bytes / pow($base, $class), $precision);
+
+ return sprintf('%s %s', $value, $units[$class]);
+ }
+
+ /**
+ * Automatically format when echoed in Blade (e.g., {{ $model->bandwidth }}).
+ */
+ public function __toString(): string
+ {
+ return $this->format();
+ }
+}
diff --git a/app/Domains/System/Traits/Model/HasFile.php b/app/Domains/System/Traits/Model/HasFile.php
new file mode 100644
index 0000000..82733d7
--- /dev/null
+++ b/app/Domains/System/Traits/Model/HasFile.php
@@ -0,0 +1,26 @@
+morphOne(File::class, 'fileable')
+ ->where('relation_name', $relation);
+ }
+
+ public function hasMultiFile(string $relation): MorphMany
+ {
+ return $this->morphMany(File::class, 'fileable')
+ ->where('relation_name', $relation);
+ }
+}
diff --git a/app/Http/Controllers/Api/V1/Lookup/RoleLookupController.php b/app/Http/Controllers/Api/V1/Lookup/RoleLookupController.php
new file mode 100644
index 0000000..c8a7797
--- /dev/null
+++ b/app/Http/Controllers/Api/V1/Lookup/RoleLookupController.php
@@ -0,0 +1,25 @@
+fetch($request->input('search'))
+ ->map(fn($res) => (object) [
+ 'id' => $res->name,
+ 'text' => $res->name,
+ ]);
+
+ return LookupResource::collection($result);
+ }
+}
diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php
new file mode 100644
index 0000000..e7f7c94
--- /dev/null
+++ b/app/Http/Controllers/Controller.php
@@ -0,0 +1,10 @@
+ 'dashboard',
+ 'domains/account/seo.profile.title' => '',
+ ],
+ )]
+ public function __invoke(Request $request)
+ {
+ return view('pages.account.profile.index');
+ }
+}
diff --git a/app/Http/Controllers/Web/Auth/VerifyEmailController.php b/app/Http/Controllers/Web/Auth/VerifyEmailController.php
new file mode 100644
index 0000000..c19babe
--- /dev/null
+++ b/app/Http/Controllers/Web/Auth/VerifyEmailController.php
@@ -0,0 +1,27 @@
+execute($request->user());
+
+ return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
+ }
+}
diff --git a/app/Http/Controllers/Web/Identity/RoleController.php b/app/Http/Controllers/Web/Identity/RoleController.php
new file mode 100644
index 0000000..2e86346
--- /dev/null
+++ b/app/Http/Controllers/Web/Identity/RoleController.php
@@ -0,0 +1,31 @@
+ 'dashboard',
+ 'domains/identity/seo.role.title' => '',
+ ],
+ )]
+ #[Seo(
+ title: 'domains/identity/seo.role.title',
+ description: 'domains/identity/seo.role.description',
+ keywords: 'domains/identity/seo.role.keywords'
+ )]
+ public function __invoke(RoleDataTable $dataTable)
+ {
+ return $dataTable->render('pages.identity.roles.index');
+ }
+}
diff --git a/app/Http/Controllers/Web/Identity/UserController.php b/app/Http/Controllers/Web/Identity/UserController.php
new file mode 100644
index 0000000..5a88eac
--- /dev/null
+++ b/app/Http/Controllers/Web/Identity/UserController.php
@@ -0,0 +1,32 @@
+ 'dashboard',
+ 'domains/identity/seo.user.title' => '',
+ ],
+ )]
+ #[Seo(
+ title: 'domains/identity/seo.user.title',
+ description: 'domains/identity/seo.user.description',
+ keywords: 'domains/identity/seo.user.keywords'
+ )]
+ public function __invoke(UserDataTable $userDataTable)
+ {
+
+ return $userDataTable->render('pages.identity.users.index');
+ }
+}
diff --git a/app/Http/DataTables/Identity/RoleDataTable.php b/app/Http/DataTables/Identity/RoleDataTable.php
new file mode 100644
index 0000000..0127f05
--- /dev/null
+++ b/app/Http/DataTables/Identity/RoleDataTable.php
@@ -0,0 +1,136 @@
+ $query Results from query() method.
+ */
+ public function dataTable(QueryBuilder $query): EloquentDataTable
+ {
+ return (new EloquentDataTable($query))
+ ->addColumn(
+ 'action',
+ fn ($role) => view('components.datatables.action-button', [
+ 'log' => true,
+ 'view' => [
+ 'modal' => 'role-view-modal',
+ 'permission' => auth()->user()->can('view', $role),
+ ],
+ 'edit' => [
+ 'modal' => 'role-form-modal',
+ 'permission' => $role->name == RoleType::SYSTEM_ADMIN->value
+ ? false
+ : auth()->user()->can('update', $role),
+ ],
+ 'delete' => [
+ 'url' => null,
+ 'title' => __('ui.button.delete'),
+ 'permission' => $role->name == RoleType::SYSTEM_ADMIN->value
+ ? false
+ : auth()->user()->can('delete', $role),
+ 'message' => __('ui.confirmation.delete', ['resource' => __('resources.role')]),
+ 'success_message' => __('ui.crud.success.deleted', ['resource' => __('resources.role')]),
+ ],
+ 'table_name' => 'role-table',
+ 'id' => $role->ulid,
+ ])
+ )
+ ->addIndexColumn();
+ }
+
+ /**
+ * Get the query source of dataTable.
+ *
+ * @return QueryBuilder
+ */
+ public function query(Role $model): QueryBuilder
+ {
+ return $model->withCount('permissions')->newQuery();
+ }
+
+ /**
+ * Optional method if you want to use the html builder.
+ */
+ public function html(): HtmlBuilder
+ {
+ return $this->builder()
+ ->setTableId('role-table')
+ ->columns($this->getColumns())
+ ->minifiedAjax()
+ ->orderBy(-1)
+ ->layout([
+ 'topStart' => [
+ 'className' => 'col-md-auto me-auto d-flex flex-sm-row flex-column justify-content-center justify-content-md-start align-items-center align-items-md-start gap-1',
+ 'features' => ['buttons', 'pageLength'],
+ ],
+ 'topEnd' => [
+ 'className' => 'col-md-auto ms-auto d-flex flex-sm-row flex-column justify-content-center justify-content-md-end align-items-center align-items-md-start gap-1',
+ 'features' => ['search'],
+ ],
+
+ 'bottomStart' => 'info',
+ 'bottomEnd' => 'paging',
+ ])
+ ->parameters([
+ 'language' => [
+ 'search' => '',
+ 'searchPlaceholder' => __('ui.button.lookup'),
+ ],
+ ])
+ ->buttons([
+ Button::make('add')
+ ->action('$("#role-form-modal").modal("show");')
+ ->text(svg('tabler-plus', ['width' => 16, 'height' => 16])->toHtml())
+ ->addClass('btn-sm'),
+ Button::make('reload')
+ ->text(svg('tabler-reload', ['width' => 16, 'height' => 16])->toHtml())
+ ->addClass('btn-sm'),
+ ]);
+ }
+
+ /**
+ * Get the dataTable columns definition.
+ */
+ public function getColumns(): array
+ {
+ return [
+ Column::computed('DT_RowIndex')
+ ->title('#')
+ ->searchable(false)
+ ->orderable(false),
+ Column::make('name')
+ ->title(__('domains/identity/field.role.name')),
+ Column::computed('permissions_count')
+ ->title(__('domains/identity/field.role.permission_count')),
+ Column::computed('guard_name')
+ ->title(__('domains/identity/field.role.guard_name')),
+ Column::computed('action')
+ ->title(__('ui.label.actions'))
+ ->exportable(false)
+ ->printable(false)
+ ->width(60)
+ ->addClass('text-center'),
+ ];
+ }
+
+ /**
+ * Get the filename for export.
+ */
+ protected function filename(): string
+ {
+ return 'Role_'.date('YmdHis');
+ }
+}
diff --git a/app/Http/DataTables/Identity/UserDataTable.php b/app/Http/DataTables/Identity/UserDataTable.php
new file mode 100644
index 0000000..228da29
--- /dev/null
+++ b/app/Http/DataTables/Identity/UserDataTable.php
@@ -0,0 +1,183 @@
+ $query Results from query() method.
+ */
+ public function dataTable(QueryBuilder $query): EloquentDataTable
+ {
+ return (new EloquentDataTable($query))
+ ->editColumn('status', fn ($model) => view('components.badge', [
+ 'label' => $model->status->label(),
+ 'variant' => $model->status->badgeVariant(),
+ ]))
+ ->addColumn(
+ 'action',
+ fn ($user) => view('components.datatables.action-button', [
+ 'log' => true,
+ 'view' => [
+ 'url' => route('users.view', ['user_id' => $user->ulid]),
+ 'permission' => auth()->user()->can('view', $user),
+ ],
+ 'edit' => [
+ 'modal' => 'user-form-modal',
+ 'permission' => auth()->user()->can('update', $user),
+ ],
+ 'delete' => [
+ 'url' => null,
+ 'title' => $user->status->isActive()
+ ? __('ui.button.suspend')
+ : __('ui.button.delete'),
+ 'message' => $user->status->isActive()
+ ? __('ui.confirmation.suspend', ['resource' => __('resources.user')])
+ : __('ui.confirmation.delete', ['resource' => __('resources.user')]),
+ 'success_message' => $user->status->isActive()
+ ? __('ui.crud.success.suspended', ['resource' => __('resources.user')])
+ : __('ui.crud.success.deleted', ['resource' => __('resources.user')]),
+ 'permission' => auth()->user()->can('delete', $user),
+ ],
+ 'table_name' => 'user-table',
+ 'id' => $user->ulid,
+ ])
+ )
+ ->rawColumns(['action', 'status'])
+ ->addIndexColumn();
+ }
+
+ /**
+ * Get the query source of dataTable.
+ *
+ * @return QueryBuilder
+ */
+ public function query(User $model): QueryBuilder
+ {
+ $query = $model->with(['roles'])
+ ->newQuery();
+
+ if (request()->has('role') && request('role') != '') {
+ $query->whereHas('roles', fn ($query) => $query
+ ->where('name', request('role')));
+ }
+
+ if (request()->has('status') && request('status') != '') {
+ $query->where('status', request('status'));
+ }
+
+ return $query;
+ }
+
+ /**
+ * Optional method if you want to use the html builder.
+ */
+ public function html(): HtmlBuilder
+ {
+ return $this->builder()
+ ->setTableId('user-table')
+ ->columns($this->getColumns())
+ ->ajax([
+ 'data' => 'function(d) {
+ d.role = $("#role-filter").val()
+ d.status = $("#status-filter").val()
+ }',
+ ])
+ ->orderBy(-1)
+ ->layout([
+ 'topStart' => [
+ 'className' => 'col-md-auto me-auto d-flex flex-sm-row flex-column justify-content-center justify-content-md-start align-items-center align-items-md-start gap-1',
+ 'features' => ['buttons', 'pageLength'],
+ ],
+ 'topEnd' => [
+ 'className' => 'col-md-auto ms-auto d-flex flex-sm-row flex-column justify-content-center justify-content-md-end align-items-center align-items-md-start gap-1',
+ 'features' => ['status-filter', 'role-filter', 'search'],
+ ],
+
+ 'bottomStart' => 'info',
+ 'bottomEnd' => 'paging',
+ ])
+ ->parameters([
+ 'language' => [
+ 'search' => '',
+ 'searchPlaceholder' => __('ui.button.lookup'),
+ ],
+ 'fixedColumns' => [
+ 'start' => 2,
+ ],
+ 'scrollX' => true,
+ 'scrollCollapse' => true,
+ 'responsive' => true,
+ ])
+ ->buttons([
+ Button::make('add')
+ ->action('$("#user-form-modal").modal("show");')
+ ->text(svg('tabler-plus', ['width' => 16, 'height' => 16])->toHtml())
+ ->addClass('btn-sm'),
+ Button::make('excel')
+ ->text(svg('tabler-file-excel', ['width' => 16, 'height' => 16])->toHtml())
+ ->addClass('btn-sm')
+ ->action("Livewire.dispatch('export-excel')"),
+ Button::make('excel')
+ ->text(svg('tabler-table-import', ['width' => 16, 'height' => 16])->toHtml())
+ ->titleAttr(__('ui.title.import', ['resource' => 'Excel']))
+ ->addClass('btn-sm')
+ ->action("$('#excel-import-modal').modal('show')"),
+ Button::make('reload')
+ ->text(svg('tabler-reload', ['width' => 16, 'height' => 16])->toHtml())
+ ->addClass('btn-sm'),
+ ]);
+ }
+
+ /**
+ * Get the dataTable columns definition.
+ */
+ public function getColumns(): array
+ {
+ return [
+ Column::computed('DT_RowIndex')
+ ->title('#'),
+ Column::make('name')
+ ->title(__('domains/identity/field.user.name'))
+ ->searchable(true),
+ Column::make('email')
+ ->title(__('domains/identity/field.user.email'))
+ ->searchable(true),
+ Column::computed('roles[0].name')
+ ->title(__('resources.role')),
+ Column::computed('status')
+ ->title(__('domains/identity/field.user.status')),
+ Column::computed('action')
+ ->title(__('ui.label.actions'))
+ ->exportable(false)
+ ->printable(false)
+ ->width(60)
+ ->addClass('text-center'),
+ ];
+ }
+
+ /**
+ * Get the filename for export.
+ */
+ protected function filename(): string
+ {
+ return 'User_'.date('YmdHis');
+ }
+}
diff --git a/app/Http/Ingestion/Excel/Identity/UserImport.php b/app/Http/Ingestion/Excel/Identity/UserImport.php
new file mode 100644
index 0000000..caa6f08
--- /dev/null
+++ b/app/Http/Ingestion/Excel/Identity/UserImport.php
@@ -0,0 +1,33 @@
+execute(
+ rows: $rows,
+ mapper: app(UserDataMapper::class),
+ modelClass: User::class,
+ );
+ }
+
+ public function chunkSize(): int
+ {
+ return 200; // Optimal balance for shared hosting memory limits
+ }
+}
diff --git a/app/Http/Middleware/HandleLayoutDataAttributes.php b/app/Http/Middleware/HandleLayoutDataAttributes.php
new file mode 100644
index 0000000..5aa388f
--- /dev/null
+++ b/app/Http/Middleware/HandleLayoutDataAttributes.php
@@ -0,0 +1,86 @@
+route();
+
+ if ($route) {
+ $layoutInstance = $this->getLayoutAttributeDirectly($route);
+
+ if ($layoutInstance) {
+ // 💡 CRITICAL: We pass route parameters (the bound Eloquent models)
+ // so the named-route parameter builder can extract IDs (e.g., {user.id})
+ $this->applyLayout->execute($layoutInstance, $route->parameters());
+ }
+ }
+
+ return $next($request);
+ }
+
+ /**
+ * Extracts the LayoutData attribute directly from the route structure safely.
+ *
+ * @throws Exception
+ */
+ private function getLayoutAttributeDirectly(mixed $route): ?LayoutData
+ {
+ if (isset($route->defaults['layout_data']) && $route->defaults['layout_data'] instanceof LayoutData) {
+ return $route->defaults['layout_data'];
+ }
+
+ try {
+ $controller = $route->getController();
+
+ if ($controller instanceof ViewController) {
+ return isset($route->defaults['layout_data']) && $route->defaults['layout_data'] instanceof LayoutData
+ ? $route->defaults['layout_data']
+ : null;
+ }
+
+ $method = $route->getActionMethod();
+
+ if ($method === get_class($controller) || (method_exists($controller, '__invoke') && ! method_exists($controller, $method))) {
+ $method = '__invoke';
+ }
+
+ if (method_exists($controller, $method)) {
+ $reflection = new ReflectionMethod($controller, $method);
+ $attributes = $reflection->getAttributes(LayoutData::class);
+
+ if (! empty($attributes)) {
+ $layoutInstance = $attributes[0]->newInstance();
+ if (! $layoutInstance instanceof LayoutData) {
+ throw new Exception('Expected type of LayoutData object');
+ }
+
+ return $layoutInstance;
+ }
+ }
+ } catch (ReflectionException $e) {
+ return null;
+ }
+
+ return null;
+ }
+}
diff --git a/app/Http/Middleware/HandlePreferredLanguage.php b/app/Http/Middleware/HandlePreferredLanguage.php
new file mode 100644
index 0000000..b645795
--- /dev/null
+++ b/app/Http/Middleware/HandlePreferredLanguage.php
@@ -0,0 +1,42 @@
+has('locale')) {
+ $lang = session()->get('locale');
+ } else {
+ $lang = $this->getSystemSettings->get(SystemSettingKey::DEFAULT_LANGUAGE);
+ $userSetting = $request->user()?->settings;
+
+ if ($userSetting !== null && isset($userSetting[UserSettingKey::LANGUAGE->value])) {
+ $lang = $userSetting[UserSettingKey::LANGUAGE->value];
+ }
+ }
+
+ setlocale(LC_ALL, $lang);
+ app()->setLocale($lang);
+
+ return $next($request);
+ }
+}
diff --git a/app/Http/Middleware/HandlePreferredTimezone.php b/app/Http/Middleware/HandlePreferredTimezone.php
new file mode 100644
index 0000000..e6adc8b
--- /dev/null
+++ b/app/Http/Middleware/HandlePreferredTimezone.php
@@ -0,0 +1,34 @@
+getSystemSettings->get(SystemSettingKey::TIMEZONE);
+ $userSetting = $request->user()?->settings;
+
+ if ($userSetting !== null) {
+ $timezone = $userSetting[UserSettingKey::TIMEZONE->value];
+ }
+
+ config(['app.display_timezone' => $timezone]);
+
+ return $next($request);
+ }
+}
diff --git a/app/Http/Middleware/HandleSeoAttributes.php b/app/Http/Middleware/HandleSeoAttributes.php
new file mode 100644
index 0000000..de04a55
--- /dev/null
+++ b/app/Http/Middleware/HandleSeoAttributes.php
@@ -0,0 +1,81 @@
+route();
+
+ if ($route) {
+ $seoInstance = $this->getSeoAttributeDirectly($route);
+
+ if ($seoInstance) {
+ $this->setSeo->applySeo($seoInstance, $request->all(), $request);
+ }
+ }
+
+ return $next($request);
+ }
+
+ /**
+ * Extracts the attribute directly from the route structure safely.
+ *
+ * @throws Exception
+ */
+ private function getSeoAttributeDirectly(mixed $route): ?Seo
+ {
+ if (isset($route->defaults['seo']) && $route->defaults['seo'] instanceof Seo) {
+ return $route->defaults['seo'];
+ }
+
+ try {
+ $controller = $route->getController();
+
+ if ($controller instanceof ViewController) {
+ return isset($route->defaults['seo']) && $route->defaults['seo'] instanceof Seo
+ ? $route->defaults['seo']
+ : null;
+ }
+
+ $method = $route->getActionMethod();
+
+ if ($method === get_class($controller) || (method_exists($controller, '__invoke') && ! method_exists($controller, $method))) {
+ $method = '__invoke';
+ }
+
+ if (method_exists($controller, $method)) {
+ $reflection = new ReflectionMethod($controller, $method);
+ $attributes = $reflection->getAttributes(Seo::class);
+
+ if (! empty($attributes)) {
+ $seoInstance = $attributes[0]->newInstance();
+ if (! $seoInstance instanceof Seo) {
+ throw new Exception('Expected type of Seo object');
+ }
+
+ return $seoInstance;
+ }
+ }
+ } catch (ReflectionException $e) {
+ return null;
+ }
+
+ return null;
+ }
+}
diff --git a/app/Http/Middleware/HandleSeoSetting.php b/app/Http/Middleware/HandleSeoSetting.php
new file mode 100644
index 0000000..edf47ac
--- /dev/null
+++ b/app/Http/Middleware/HandleSeoSetting.php
@@ -0,0 +1,35 @@
+getSystemSettings->get(SystemSettingKey::WEB_NAME);
+ $description = $this->getSystemSettings->get(SystemSettingKey::WEB_DESCRIPTION);
+
+ config(['seotools.meta.defaults.title' => $title]);
+ config(['seotools.opengraph.defaults.title' => $title]);
+ config(['seotools.json-ld.defaults.title' => $title]);
+
+ config(['seotools.meta.defaults.description' => $description]);
+ config(['seotools.opengraph.defaults.description' => $description]);
+ config(['seotools.json-ld.defaults.description' => $description]);
+
+ return $next($request);
+ }
+}
diff --git a/app/Http/Requests/Api/ApiRequest.php b/app/Http/Requests/Api/ApiRequest.php
new file mode 100644
index 0000000..a94eb24
--- /dev/null
+++ b/app/Http/Requests/Api/ApiRequest.php
@@ -0,0 +1,25 @@
+json([
+ 'message' => trans('ui.crud.error.validation_failed'),
+ 'errors' => $validator->errors(),
+ ], 422));
+ }
+}
diff --git a/app/Http/Requests/Api/Identity/UserRequest.php b/app/Http/Requests/Api/Identity/UserRequest.php
new file mode 100644
index 0000000..ebf57c1
--- /dev/null
+++ b/app/Http/Requests/Api/Identity/UserRequest.php
@@ -0,0 +1,34 @@
+ ['email', 'required'],
+ 'name' => ['string', 'required'],
+ 'role' => ['string', 'required'],
+ 'password' => Password::default(),
+ ];
+ }
+
+ public function attributes(): array
+ {
+ return [
+ 'email' => __('domains/identity/field.user.email'),
+ 'name' => __('domains/identity/field.user.name'),
+ 'role' => __('resources.role'),
+ 'password' => __('domains/identity/field.user.password'),
+ ];
+ }
+}
diff --git a/app/Http/Requests/Web/Account/ProfileUpdateRequest.php b/app/Http/Requests/Web/Account/ProfileUpdateRequest.php
new file mode 100644
index 0000000..be530a9
--- /dev/null
+++ b/app/Http/Requests/Web/Account/ProfileUpdateRequest.php
@@ -0,0 +1,42 @@
+|string>
+ */
+ public function rules(): array
+ {
+ return [
+ 'name' => ['required', 'string', 'max:255'],
+ 'email' => [
+ 'required',
+ 'string',
+ 'lowercase',
+ 'email',
+ 'max:255',
+ Rule::unique(User::class)->ignore($this->user()->id),
+ ],
+ 'gender' => ['required', 'in:male,female'],
+ 'date_of_birth' => ['required', 'date'],
+ 'phone_number' => ['required', 'numeric', 'max_digits:12', 'min_digits:9'],
+ ];
+ }
+
+ public function attributes(): array
+ {
+ return [
+ 'name' => __('domains/identity/field.user.name'),
+ 'email' => __('domains/identity/field.user.email'),
+ ];
+ }
+}
diff --git a/app/Http/Requests/Web/Auth/LoginRequest.php b/app/Http/Requests/Web/Auth/LoginRequest.php
new file mode 100644
index 0000000..668ec23
--- /dev/null
+++ b/app/Http/Requests/Web/Auth/LoginRequest.php
@@ -0,0 +1,94 @@
+|string>
+ */
+ public function rules(): array
+ {
+ return [
+ 'email' => ['required', 'string', 'email'],
+ 'password' => ['required', 'string'],
+ ];
+ }
+
+ public function attributes(): array
+ {
+ return [
+ 'email' => __('domains/identity/field.user.email'),
+ 'password' => __('domains/identity/field.user.password'),
+ ];
+ }
+
+ /**
+ * Attempt to authenticate the request's credentials.
+ *
+ * @throws ValidationException
+ */
+ public function authenticate(): void
+ {
+ $this->ensureIsNotRateLimited();
+
+ if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
+ RateLimiter::hit($this->throttleKey());
+
+ throw ValidationException::withMessages([
+ 'email' => trans('auth.failed'),
+ ]);
+ }
+
+ RateLimiter::clear($this->throttleKey());
+ }
+
+ /**
+ * Ensure the login request is not rate limited.
+ *
+ * @throws ValidationException
+ */
+ public function ensureIsNotRateLimited(): void
+ {
+ if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
+ return;
+ }
+
+ event(new Lockout($this));
+
+ $seconds = RateLimiter::availableIn($this->throttleKey());
+
+ throw ValidationException::withMessages([
+ 'email' => trans('auth.throttle', [
+ 'seconds' => $seconds,
+ 'minutes' => ceil($seconds / 60),
+ ]),
+ ]);
+ }
+
+ /**
+ * Get the rate limiting throttle key for the request.
+ */
+ public function throttleKey(): string
+ {
+ return Str::transliterate(Str::lower($this->string('email')).'|'.$this->ip());
+ }
+}
diff --git a/app/Http/Requests/Web/Identity/RoleRequest.php b/app/Http/Requests/Web/Identity/RoleRequest.php
new file mode 100644
index 0000000..63cca87
--- /dev/null
+++ b/app/Http/Requests/Web/Identity/RoleRequest.php
@@ -0,0 +1,42 @@
+|string>
+ */
+ public function rules(): array
+ {
+ return [
+ 'name' => ['required', 'string'],
+ 'guard_name' => ['required', 'string'],
+ 'permissions' => ['array', 'min:1'],
+ 'permissions.*' => ['required'],
+ ];
+ }
+
+ public function attributes(): array
+ {
+ return [
+ 'name' => __('domains/identity/field.role.name'),
+ 'guard_name' => __('domains/identity/field.role.guard_name'),
+ 'permissions' => __('domains/identity/field.role.permissions'),
+ ];
+ }
+}
diff --git a/app/Http/Requests/Web/Identity/UserRequest.php b/app/Http/Requests/Web/Identity/UserRequest.php
new file mode 100644
index 0000000..edbecd8
--- /dev/null
+++ b/app/Http/Requests/Web/Identity/UserRequest.php
@@ -0,0 +1,44 @@
+|string>
+ */
+ public function rules(): array
+ {
+ return [
+ 'email' => ['email', 'required'],
+ 'name' => ['string', 'required'],
+ 'role' => ['string', 'required'],
+ 'password' => Password::default(),
+ ];
+ }
+
+ public function attributes(): array
+ {
+ return [
+ 'email' => __('domains/identity/field.user.email'),
+ 'name' => __('domains/identity/field.user.name'),
+ 'role' => __('resources.role'),
+ 'password' => __('domains/identity/field.user.password'),
+ ];
+ }
+}
diff --git a/app/Http/Resources/LookupResource.php b/app/Http/Resources/LookupResource.php
new file mode 100644
index 0000000..bbf973b
--- /dev/null
+++ b/app/Http/Resources/LookupResource.php
@@ -0,0 +1,22 @@
+
+ */
+ public function toArray(Request $request): array
+ {
+ return [
+ 'id' => $this->id,
+ 'text' => $this->text,
+ ];
+ }
+}
diff --git a/app/Http/Resources/SuccessResource.php b/app/Http/Resources/SuccessResource.php
new file mode 100644
index 0000000..d79ba00
--- /dev/null
+++ b/app/Http/Resources/SuccessResource.php
@@ -0,0 +1,25 @@
+
+ */
+ public function toArray(Request $request): array
+ {
+ return [
+ 'message' => $this->message,
+ ];
+ }
+}
diff --git a/app/Livewire/Concerns/HasLayoutDataAttributes.php b/app/Livewire/Concerns/HasLayoutDataAttributes.php
new file mode 100644
index 0000000..9015583
--- /dev/null
+++ b/app/Livewire/Concerns/HasLayoutDataAttributes.php
@@ -0,0 +1,28 @@
+getAttributes(LayoutData::class)[0] ?? null;
+
+ if ($attribute) {
+ /** @var LayoutData $instance */
+ $instance = $attribute->newInstance();
+ // Execute the processor action, passing $this (the component) as the data context
+ $applyLayout->execute($instance, $this);
+ }
+ }
+}
diff --git a/app/Livewire/Concerns/HasSeoAttributes.php b/app/Livewire/Concerns/HasSeoAttributes.php
new file mode 100644
index 0000000..d55753e
--- /dev/null
+++ b/app/Livewire/Concerns/HasSeoAttributes.php
@@ -0,0 +1,42 @@
+getAttributes(Seo::class)[0]
+ ?? $reflection->getMethod('render')->getAttributes(Seo::class)[0]
+ ?? null;
+
+ if ($attribute) {
+ /** @var Seo $seo */
+ $seo = $attribute->newInstance();
+
+ $setSeo->applySeo($seo, $this, request());
+ }
+ }
+
+ /**
+ * Livewire Lifecycle hook: triggers on every render.
+ */
+ public function renderingHasSeoAttributes(SetSeoMetadata $setSeo): void
+ {
+ $this->applySeoMetadata($setSeo);
+ }
+}
diff --git a/app/Livewire/Concerns/WithModal.php b/app/Livewire/Concerns/WithModal.php
new file mode 100644
index 0000000..7fa598e
--- /dev/null
+++ b/app/Livewire/Concerns/WithModal.php
@@ -0,0 +1,42 @@
+resourceName);
+
+ return __('ui.title.'.$this->mode, ['resource' => $resource]);
+ }
+
+ #[Computed]
+ public function message(): string
+ {
+ $resource = __('resources.'.$this->resourceName);
+
+ return match ($this->mode) {
+ 'create' => __('ui.crud.success.created', ['resource' => $resource]),
+ 'update' => __('ui.crud.success.updated', ['resource' => $resource]),
+ default => __('ui.crud.success.deleted', ['resource' => $resource]),
+ };
+ }
+}
diff --git a/app/Livewire/Concerns/WithToast.php b/app/Livewire/Concerns/WithToast.php
new file mode 100644
index 0000000..dbce954
--- /dev/null
+++ b/app/Livewire/Concerns/WithToast.php
@@ -0,0 +1,26 @@
+js("window.toast(\"$message\", \"success\")");
+ }
+
+ public function warning(string $message): void
+ {
+ $this->js("window.toast(\"$message\", \"warning\")");
+ }
+
+ public function error(string $message): void
+ {
+ $this->js("window.toast(\"$message\", \"warning\")");
+ }
+}
diff --git a/app/Livewire/Forms/Account/UpdatePasswordForm.php b/app/Livewire/Forms/Account/UpdatePasswordForm.php
new file mode 100644
index 0000000..f4a6906
--- /dev/null
+++ b/app/Livewire/Forms/Account/UpdatePasswordForm.php
@@ -0,0 +1,26 @@
+ ['required', 'current_password'],
+ 'new_password' => ['required', 'min:8', 'confirmed', 'different:current_password'],
+ ];
+ }
+}
diff --git a/app/Livewire/Forms/Account/UpdateProfileForm.php b/app/Livewire/Forms/Account/UpdateProfileForm.php
new file mode 100644
index 0000000..19b4431
--- /dev/null
+++ b/app/Livewire/Forms/Account/UpdateProfileForm.php
@@ -0,0 +1,39 @@
+ ['required', 'string', 'max:255', Rule::unique(User::class, 'name')->ignore($userId)],
+ 'email' => ['required', 'string', 'email', Rule::unique(User::class, 'email')->ignore($userId)],
+ 'gender' => ['required', new Enum(GenderOption::class)],
+ 'date_of_birth' => ['required'],
+ 'phone_number' => ['required', 'numeric', 'min_digits:10'],
+ ];
+ }
+}
diff --git a/app/Livewire/Forms/Auth/ConfirmPasswordForm.php b/app/Livewire/Forms/Auth/ConfirmPasswordForm.php
new file mode 100644
index 0000000..aa5fef6
--- /dev/null
+++ b/app/Livewire/Forms/Auth/ConfirmPasswordForm.php
@@ -0,0 +1,12 @@
+ ['required', 'string', 'max:255', Rule::unique(User::class, 'name')],
+ 'email' => ['required', 'string', 'email', Rule::unique(User::class, 'email')],
+ 'password' => [Password::default(), 'required', 'confirmed'],
+ ];
+ }
+}
diff --git a/app/Livewire/Forms/Auth/ResetPasswordForm.php b/app/Livewire/Forms/Auth/ResetPasswordForm.php
new file mode 100644
index 0000000..12e2ca3
--- /dev/null
+++ b/app/Livewire/Forms/Auth/ResetPasswordForm.php
@@ -0,0 +1,32 @@
+ ['required'],
+ 'email' => ['required', 'email'],
+ 'password' => ['required', 'confirmed', PasswordRule::defaults()],
+ ];
+ }
+}
diff --git a/app/Livewire/Forms/Identity/RoleForm.php b/app/Livewire/Forms/Identity/RoleForm.php
new file mode 100644
index 0000000..cdd1d11
--- /dev/null
+++ b/app/Livewire/Forms/Identity/RoleForm.php
@@ -0,0 +1,18 @@
+ ['required', 'string', 'max:255', Rule::unique(User::class, 'name')->ignore($userId, 'ulid')],
+ 'email' => ['required', 'string', 'email', Rule::unique(User::class, 'email')->ignore($userId, 'ulid')],
+ 'role_name' => ['required'],
+ 'password' => [Password::default(), 'required', 'confirmed'],
+ ];
+
+ if ($isUpdate) {
+ unset($rules['password']);
+ }
+
+ return $rules;
+ }
+}
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
new file mode 100644
index 0000000..452e6b6
--- /dev/null
+++ b/app/Providers/AppServiceProvider.php
@@ -0,0 +1,24 @@
+buildDomainEvent();
+ }
+
+ public function buildDomainEvent(): void
+ {
+ $domainProviders = glob(app_path('Domains/*/Providers/*ServiceProvider.php'));
+
+ foreach ($domainProviders as $file) {
+ // Convert file path directly to its full PHP Namespace
+ // Example: /var/www/app/Domains/Identity/Providers/IdentityServiceProvider.php
+ // Becomes: App\Domains\Identity\Providers\IdentityServiceProvider
+ $relativePath = str_replace(app_path(), 'App', $file);
+ $className = str_replace(['/', '.php'], ['\\', ''], $relativePath);
+
+ // If the class exists and contains our static $listen array, register it
+ if (class_exists($className) && property_exists($className, 'listen')) {
+ foreach ($className::$listen as $event => $listeners) {
+ foreach ((array) $listeners as $listener) {
+ Event::listen($event, $listener);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/Providers/UiServiceProvider.php b/app/Providers/UiServiceProvider.php
new file mode 100644
index 0000000..7b75164
--- /dev/null
+++ b/app/Providers/UiServiceProvider.php
@@ -0,0 +1,46 @@
+app->singleton(ResolveDynamicText::class, function ($app) {
+ return new ResolveDynamicText;
+ });
+ }
+
+ public function boot(): void
+ {
+ // Google Tag Manager Head
+ Blade::directive('gtmHead', function () {
+ return "get(\App\Domains\System\Enums\SystemSettingKey::GOOGLE_TAG_MANAGER_ID); if (!empty(\$gtmId)): ?>
+
+ ";
+ });
+
+ // Google Tag Manager Body
+ Blade::directive('gtmBody', function () {
+ return "get(\App\Domains\System\Enums\SystemSettingKey::GOOGLE_TAG_MANAGER_ID); if (!empty(\$gtmId)): ?>
+
+ ";
+ });
+
+ // Google Tag Manager Meta
+ Blade::directive('webmasterMeta', function () {
+ return "get(\App\Domains\System\Enums\SystemSettingKey::GOOGLE_WEBMASTER_ID); if (!empty(\$webmasterId)): ?>
+ \" />
+ ";
+ });
+ }
+}
diff --git a/app/UI/Actions/ApplyLayoutMetadata.php b/app/UI/Actions/ApplyLayoutMetadata.php
new file mode 100644
index 0000000..1206221
--- /dev/null
+++ b/app/UI/Actions/ApplyLayoutMetadata.php
@@ -0,0 +1,80 @@
+context;
+ $breadcrumbs = [];
+
+ foreach ($attributeInstance->breadcrumbs as $labelKey => $routeConfig) {
+ // 1. Resolve the text label (Supports translation keys or direct placeholders)
+ $label = $this->textResolver->execute($labelKey, $dataContext, $contextTarget);
+ $url = null;
+
+ if (! empty($routeConfig)) {
+ $routeName = '';
+ $routeParams = [];
+
+ // SCENARIO A: Route configuration has parameters array: ["route.name", ["id" => "{user.id}"]]
+ if (is_array($routeConfig)) {
+ $routeName = $routeConfig[0] ?? '';
+ $rawParams = $routeConfig[1] ?? [];
+
+ foreach ($rawParams as $paramKey => $paramValue) {
+ // Resolve internal variable bindings within parameters (e.g., "{user.id}" -> 4)
+ $resolvedValue = $this->textResolver->execute($paramValue, $dataContext, $contextTarget);
+ $routeParams[$paramKey] = $resolvedValue;
+ }
+ }
+ // SCENARIO B: Route configuration is a simple flat string: "identity.dashboard"
+ elseif (is_string($routeConfig)) {
+ $routeName = $routeConfig;
+ }
+
+ // 2. Compile URL safely via route name if it exists in Laravel's routing registry
+ if (! empty($routeName) && Route::has($routeName)) {
+ $url = route($routeName, $routeParams);
+ } else {
+ // Fallback to treat as a raw URI link path if it's not a registered route name
+ $url = ! empty($routeName) ? url($routeName) : null;
+ }
+ }
+
+ $breadcrumbs[] = [
+ 'label' => $label,
+ 'url' => $url,
+ ];
+ }
+
+ // 3. Resolve layout headline text patterns
+ $header = null;
+ if ($attributeInstance->header) {
+ $header = $this->textResolver->execute($attributeInstance->header, $dataContext, $contextTarget);
+ }
+
+ if (empty($header) && ! empty($breadcrumbs)) {
+ $lastCrumb = end($breadcrumbs);
+ $header = $lastCrumb['label'] ?? '';
+ }
+
+ // 4. Inject properties into the layout view instance memory
+ View::composer([
+ 'components.layouts.app',
+ 'components.layouts.guest',
+ ], fn ($view) => $view->with([
+ 'breadcrumbs' => $breadcrumbs,
+ 'header' => $header ?? config('app.name', 'Antigravity App'),
+ ]));
+ }
+}
diff --git a/app/UI/Actions/ResolveDynamicText.php b/app/UI/Actions/ResolveDynamicText.php
new file mode 100644
index 0000000..cf745ad
--- /dev/null
+++ b/app/UI/Actions/ResolveDynamicText.php
@@ -0,0 +1,146 @@
+ [context_key => resolved_object_or_scalar]]
+ */
+ private array $resolvedContextCache = [];
+
+ public function execute(?string $value, array|object $context, ?string $contextTarget = null): ?string
+ {
+ if (! $value) {
+ return null;
+ }
+
+ if (str_contains($value, '.') && Lang::has($value)) {
+ return $this->localizeAndBind($value, $context, $contextTarget);
+ }
+
+ return $this->resolveExplicitPlaceholders($value, $context, $contextTarget);
+ }
+
+ private function localizeAndBind(string $key, array|object $data, ?string $contextTarget): string
+ {
+ $templateString = __($key);
+ preg_match_all('/:([a-zA-Z0-9_]+)/', $templateString, $matches);
+
+ $bindings = [];
+ if (! empty($matches[1])) {
+ foreach ($matches[1] as $variableName) {
+ $targetProperty = $variableName;
+ $objectKey = $contextTarget;
+
+ $bindings[$variableName] = $this->resolveNestedValue(
+ $objectKey ?? $variableName,
+ $objectKey ? $targetProperty : null,
+ $data
+ );
+ }
+ }
+
+ return __($key, $bindings);
+ }
+
+ private function resolveExplicitPlaceholders(string $text, array|object $data, ?string $contextTarget): string
+ {
+ preg_match_all('/\{([^}]+)\}/', $text, $matches);
+ if (empty($matches[1])) {
+ return $text;
+ }
+
+ foreach ($matches[1] as $placeholder) {
+ if (str_contains($placeholder, '.')) {
+ [$objectName, $property] = explode('.', $placeholder, 2);
+ } else {
+ $objectName = $contextTarget ?? $placeholder;
+ $property = $contextTarget ? $placeholder : null;
+ }
+
+ $replacementValue = $this->resolveNestedValue($objectName, $property, $data);
+ $text = str_replace("{{$placeholder}}", $replacementValue, $text);
+ }
+
+ return $text;
+ }
+
+ private function resolveNestedValue(string $objectName, ?string $property, array|object $data): string
+ {
+ // 1. Generate a unique cache signature for the current payload state
+ $contextKey = is_object($data) ? spl_object_hash($data) : md5(serialize($data));
+
+ // 2. Warm up the context cache exactly once per component/request
+ if (! isset($this->resolvedContextCache[$contextKey])) {
+ $this->resolvedContextCache[$contextKey] = is_object($data)
+ ? $this->extractInspectableObjects($data)
+ : $data;
+ }
+
+ $cachedContext = $this->resolvedContextCache[$contextKey];
+ $target = null;
+
+ // 3. Look up the cached target context safely without firing new database calls
+ if (isset($cachedContext[$objectName])) {
+ $target = $cachedContext[$objectName];
+ } elseif ($property === null && isset($cachedContext[$objectName]) && is_scalar($cachedContext[$objectName])) {
+ return (string) $cachedContext[$objectName];
+ }
+
+ if ($property === null) {
+ return $target && is_scalar($target) ? (string) $target : '';
+ }
+
+ if ($target && is_object($target) && isset($target->{$property})) {
+ return (string) $target->{$property};
+ }
+ if ($target && is_array($target) && isset($target[$property])) {
+ return (string) $target[$property];
+ }
+
+ return '';
+ }
+
+ /**
+ * Reflects and extracts object contexts ONCE, caching the output in memory.
+ */
+ private function extractInspectableObjects(object $component): array
+ {
+ $objects = [];
+ $reflection = new ReflectionClass($component);
+
+ // Scan Public Properties
+ foreach ($reflection->getProperties(ReflectionProperty::IS_PUBLIC) as $prop) {
+ $value = $prop->getValue($component);
+ $objects[$prop->getName()] = $value;
+ }
+
+ // Scan Public Methods (e.g. Livewire 4 #[Computed] properties)
+ foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
+ if ($method->getNumberOfParameters() === 0 && ! $method->isStatic()) {
+ try {
+ $name = $method->getName();
+ // Ignore internal framework overrides and lifecycle names
+ if (! str_starts_with($name, '__') && ! str_starts_with($name, 'get') && ! str_contains($name, 'rendering')) {
+
+ // 💡 THIS WAS THE CULPRIT: Invoking this on every string match triggered query loops.
+ // Now it runs exactly once per request render pass, and the model is stored in memory.
+ $objects[$name] = $component->{$name};
+ }
+ } catch (Throwable $e) {
+ continue;
+ }
+ }
+ }
+
+ return $objects;
+ }
+}
diff --git a/app/UI/Actions/SetSeoMetadata.php b/app/UI/Actions/SetSeoMetadata.php
new file mode 100644
index 0000000..b323139
--- /dev/null
+++ b/app/UI/Actions/SetSeoMetadata.php
@@ -0,0 +1,48 @@
+ $seo->title,
+ 'description' => $seo->description,
+ 'keywords' => is_array($seo->keywords) ? implode(', ', $seo->keywords) : $seo->keywords,
+ 'image' => $seo->image,
+ 'context_target' => $seo->context, // 👈 Save context target
+ ];
+
+ $this->updateSeoTools(rawData: $rawData, viewData: $viewData, request: $request);
+ }
+
+ public function updateSeoTools(array $rawData, array|object $viewData, Request $request): void
+ {
+ $contextTarget = $rawData['context_target'] ?? null;
+
+ // Pass the context target to the text resolver
+ $title = $this->textResolver->execute(value: $rawData['title'], context: $viewData, contextTarget: $contextTarget);
+ $description = $this->textResolver->execute(value: $rawData['description'], context: $viewData, contextTarget: $contextTarget);
+ $keywords = $this->textResolver->execute(value: $rawData['keywords'], context: $viewData, contextTarget: $contextTarget);
+ $image = $rawData['image'];
+
+ SEOTools::setTitle($title ?? __('seo.default_title'));
+ SEOTools::setDescription($description ?? __('seo.default_description'));
+ SEOTools::metatags()->setCanonical($request->url());
+
+ if ($keywords) {
+ SEOTools::metatags()->setKeywords($keywords);
+ }
+ if ($image) {
+ SEOTools::addImages($image);
+ SEOTools::opengraph()->addProperty('image', asset($image));
+ }
+ }
+}
diff --git a/app/UI/Enums/FileType.php b/app/UI/Enums/FileType.php
new file mode 100644
index 0000000..9eddd94
--- /dev/null
+++ b/app/UI/Enums/FileType.php
@@ -0,0 +1,21 @@
+ ['application/pdf', 'application/vnd.ms-word', 'application/vnd.oasis.opendocument.text'],
+ self::IMAGE => ['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'],
+ self::AUDIO => ['audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/webm', 'audio/x-wav', 'audio/aac'],
+
+ default => ['text/plain'],
+ };
+ }
+}
diff --git a/app/UI/Enums/InputType.php b/app/UI/Enums/InputType.php
new file mode 100644
index 0000000..2fb83de
--- /dev/null
+++ b/app/UI/Enums/InputType.php
@@ -0,0 +1,24 @@
+ 'form.textarea',
+ self::SELECT => 'form.select',
+ self::FILE => 'filepond::upload',
+ self::CHECKBOX => 'form.checkbox',
+ default => 'form.input',
+ };
+ }
+}
diff --git a/app/UI/Support/Excel/StyledExport.php b/app/UI/Support/Excel/StyledExport.php
new file mode 100644
index 0000000..5e47146
--- /dev/null
+++ b/app/UI/Support/Excel/StyledExport.php
@@ -0,0 +1,64 @@
+domainExport->query();
+ }
+
+ public function headings(): array
+ {
+ return $this->domainExport->headings();
+ }
+
+ public function map(mixed $row): array
+ {
+ return $this->domainExport->map($row);
+ }
+
+ /**
+ * Apply Excel formatting to specific columns (Dates, Currency, Percentages).
+ */
+ public function columnFormats(): array
+ {
+ return $this->domainExport->columnFormats();
+ }
+
+ /**
+ * Apply visual styling to specific rows or columns.
+ */
+ public function styles(Worksheet $sheet): void
+ {
+ $sheet->freezePane('A2');
+ $sheet->getPageSetup()->setOrientation('landscape');
+ $columnStyle = $sheet->getStyle('A1:'.$sheet->getHighestColumn().$sheet->getHighestRow());
+ $columnStyle->getAlignment()->setWrapText(true);
+ $columnStyle->getBorders()->getAllBorders()->setBorderStyle(Border::BORDER_THIN);
+ $columnStyle->getAlignment()->setHorizontal(Alignment::HORIZONTAL_CENTER);
+ }
+}
diff --git a/app/UI/Support/LayoutState.php b/app/UI/Support/LayoutState.php
new file mode 100644
index 0000000..4b07595
--- /dev/null
+++ b/app/UI/Support/LayoutState.php
@@ -0,0 +1,18 @@
+breadcrumbs = $breadcrumbs;
+ }
+
+ public function getBreadcrumbs(): array
+ {
+ return $this->breadcrumbs;
+ }
+}
diff --git a/artisan b/artisan
new file mode 100755
index 0000000..c35e31d
--- /dev/null
+++ b/artisan
@@ -0,0 +1,18 @@
+#!/usr/bin/env php
+handleCommand(new ArgvInput);
+
+exit($status);
diff --git a/boost.json b/boost.json
new file mode 100644
index 0000000..85ba671
--- /dev/null
+++ b/boost.json
@@ -0,0 +1,21 @@
+{
+ "agents": [
+ "gemini"
+ ],
+ "cloud": false,
+ "guidelines": true,
+ "mcp": true,
+ "nightwatch": false,
+ "packages": [
+ "spatie/laravel-permission",
+ "spatie/laravel-backup"
+ ],
+ "sail": false,
+ "skills": [
+ "ai-sdk-development",
+ "laravel-best-practices",
+ "pest-testing",
+ "laravel-backup",
+ "laravel-permission-development"
+ ]
+}
diff --git a/bootstrap/app.php b/bootstrap/app.php
new file mode 100644
index 0000000..45f397d
--- /dev/null
+++ b/bootstrap/app.php
@@ -0,0 +1,40 @@
+withCommands([
+ __DIR__.'/../app/Console/Commands',
+ ])
+ ->withRouting(
+ web: __DIR__.'/../routes/web.php',
+ api: __DIR__.'/../routes/api.php',
+ commands: __DIR__.'/../routes/console.php',
+ health: '/up',
+ )
+ ->withMiddleware(function (Middleware $middleware): void {
+ $middleware->alias([
+ 'seo' => HandleSeoAttributes::class,
+ 'layouts' => HandleLayoutDataAttributes::class,
+ ]);
+ $middleware->web(append: [
+ HandleSeoSetting::class,
+ HandlePreferredTimezone::class,
+ HandlePreferredLanguage::class,
+ ]);
+ $middleware->statefulApi();
+ $middleware->api(prepend: [
+ EnsureFrontendRequestsAreStateful::class
+ ]);
+ })
+ ->withExceptions(function (Exceptions $exceptions): void {
+ //
+ })->create();
diff --git a/bootstrap/cache/.gitignore b/bootstrap/cache/.gitignore
new file mode 100644
index 0000000..d6b7ef3
--- /dev/null
+++ b/bootstrap/cache/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/bootstrap/providers.php b/bootstrap/providers.php
new file mode 100644
index 0000000..cd6427b
--- /dev/null
+++ b/bootstrap/providers.php
@@ -0,0 +1,10 @@
+=5.5"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35||^5.6.3||^9.5",
+ "yoast/phpunit-polyfills": "^1.0"
+ },
+ "suggest": {
+ "ext-awscrt": "Make sure you install awscrt native extension to use any of the functionality."
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "AWS SDK Common Runtime Team",
+ "email": "aws-sdk-common-runtime@amazon.com"
+ }
+ ],
+ "description": "AWS Common Runtime for PHP",
+ "homepage": "https://github.com/awslabs/aws-crt-php",
+ "keywords": [
+ "amazon",
+ "aws",
+ "crt",
+ "sdk"
+ ],
+ "support": {
+ "issues": "https://github.com/awslabs/aws-crt-php/issues",
+ "source": "https://github.com/awslabs/aws-crt-php/tree/v1.2.7"
+ },
+ "time": "2024-10-18T22:15:13+00:00"
+ },
+ {
+ "name": "aws/aws-sdk-php",
+ "version": "3.384.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/aws/aws-sdk-php.git",
+ "reference": "8e232a5703896541a7a34691a41ece5bd6170269"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/8e232a5703896541a7a34691a41ece5bd6170269",
+ "reference": "8e232a5703896541a7a34691a41ece5bd6170269",
+ "shasum": ""
+ },
+ "require": {
+ "aws/aws-crt-php": "^1.2.3",
+ "ext-json": "*",
+ "ext-pcre": "*",
+ "ext-simplexml": "*",
+ "guzzlehttp/guzzle": "^7.4.5",
+ "guzzlehttp/promises": "^2.0",
+ "guzzlehttp/psr7": "^2.4.5",
+ "mtdowling/jmespath.php": "^2.8.0",
+ "php": ">=8.1",
+ "psr/http-message": "^1.0 || ^2.0",
+ "symfony/filesystem": "^v5.4.45 || ^v6.4.3 || ^v7.1.0 || ^v8.0.0"
+ },
+ "require-dev": {
+ "andrewsville/php-token-reflection": "^1.4",
+ "aws/aws-php-sns-message-validator": "~1.0",
+ "behat/behat": "~3.0",
+ "composer/composer": "^2.7.8",
+ "dms/phpunit-arraysubset-asserts": "^v0.5.0",
+ "doctrine/cache": "~1.4",
+ "ext-dom": "*",
+ "ext-openssl": "*",
+ "ext-sockets": "*",
+ "phpunit/phpunit": "^10.0",
+ "psr/cache": "^2.0 || ^3.0",
+ "psr/simple-cache": "^2.0 || ^3.0",
+ "sebastian/comparator": "^1.2.3 || ^4.0 || ^5.0",
+ "yoast/phpunit-polyfills": "^2.0"
+ },
+ "suggest": {
+ "aws/aws-php-sns-message-validator": "To validate incoming SNS notifications",
+ "doctrine/cache": "To use the DoctrineCacheAdapter",
+ "ext-curl": "To send requests using cURL",
+ "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages",
+ "ext-pcntl": "To use client-side monitoring",
+ "ext-sockets": "To use client-side monitoring"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/functions.php"
+ ],
+ "psr-4": {
+ "Aws\\": "src/"
+ },
+ "exclude-from-classmap": [
+ "src/data/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "Amazon Web Services",
+ "homepage": "https://aws.amazon.com"
+ }
+ ],
+ "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project",
+ "homepage": "https://aws.amazon.com/sdk-for-php",
+ "keywords": [
+ "amazon",
+ "aws",
+ "cloud",
+ "dynamodb",
+ "ec2",
+ "glacier",
+ "s3",
+ "sdk"
+ ],
+ "support": {
+ "forum": "https://github.com/aws/aws-sdk-php/discussions",
+ "issues": "https://github.com/aws/aws-sdk-php/issues",
+ "source": "https://github.com/aws/aws-sdk-php/tree/3.384.4"
+ },
+ "time": "2026-06-05T18:05:57+00:00"
+ },
+ {
+ "name": "barryvdh/laravel-dompdf",
+ "version": "v3.1.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/barryvdh/laravel-dompdf.git",
+ "reference": "ee3b72b19ccdf57d0243116ecb2b90261344dedc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/barryvdh/laravel-dompdf/zipball/ee3b72b19ccdf57d0243116ecb2b90261344dedc",
+ "reference": "ee3b72b19ccdf57d0243116ecb2b90261344dedc",
+ "shasum": ""
+ },
+ "require": {
+ "dompdf/dompdf": "^3.0",
+ "illuminate/support": "^9|^10|^11|^12|^13.0",
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "larastan/larastan": "^2.7|^3.0",
+ "orchestra/testbench": "^7|^8|^9.16|^10|^11.0",
+ "phpro/grumphp": "^2.5",
+ "squizlabs/php_codesniffer": "^3.5"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "aliases": {
+ "PDF": "Barryvdh\\DomPDF\\Facade\\Pdf",
+ "Pdf": "Barryvdh\\DomPDF\\Facade\\Pdf"
+ },
+ "providers": [
+ "Barryvdh\\DomPDF\\ServiceProvider"
+ ]
+ },
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Barryvdh\\DomPDF\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Barry vd. Heuvel",
+ "email": "barryvdh@gmail.com"
+ }
+ ],
+ "description": "A DOMPDF Wrapper for Laravel",
+ "keywords": [
+ "dompdf",
+ "laravel",
+ "pdf"
+ ],
+ "support": {
+ "issues": "https://github.com/barryvdh/laravel-dompdf/issues",
+ "source": "https://github.com/barryvdh/laravel-dompdf/tree/v3.1.2"
+ },
+ "funding": [
+ {
+ "url": "https://fruitcake.nl",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/barryvdh",
+ "type": "github"
+ }
+ ],
+ "time": "2026-02-21T08:51:10+00:00"
+ },
+ {
+ "name": "blade-ui-kit/blade-icons",
+ "version": "1.10.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/driesvints/blade-icons.git",
+ "reference": "74189a80bbaa4966aebaee54fec3a3c2ef0a5f3a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/driesvints/blade-icons/zipball/74189a80bbaa4966aebaee54fec3a3c2ef0a5f3a",
+ "reference": "74189a80bbaa4966aebaee54fec3a3c2ef0a5f3a",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/contracts": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0",
+ "illuminate/filesystem": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0",
+ "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0",
+ "illuminate/view": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0",
+ "php": "^7.4|^8.0",
+ "symfony/console": "^5.3|^6.0|^7.0|^8.0",
+ "symfony/finder": "^5.3|^6.0|^7.0|^8.0"
+ },
+ "require-dev": {
+ "mockery/mockery": "^1.5.1",
+ "orchestra/testbench": "^6.0|^7.0|^8.0|^9.0|^10.0|^11.0",
+ "phpunit/phpunit": "^9.0|^10.5|^11.0"
+ },
+ "bin": [
+ "bin/blade-icons-generate"
+ ],
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "BladeUI\\Icons\\BladeIconsServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/helpers.php"
+ ],
+ "psr-4": {
+ "BladeUI\\Icons\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Dries Vints",
+ "homepage": "https://driesvints.com"
+ }
+ ],
+ "description": "A package to easily make use of icons in your Laravel Blade views.",
+ "homepage": "https://github.com/driesvints/blade-icons",
+ "keywords": [
+ "blade",
+ "icons",
+ "laravel",
+ "svg"
+ ],
+ "support": {
+ "issues": "https://github.com/driesvints/blade-icons/issues",
+ "source": "https://github.com/driesvints/blade-icons"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sponsors/driesvints",
+ "type": "github"
+ },
+ {
+ "url": "https://www.paypal.com/paypalme/driesvints",
+ "type": "paypal"
+ }
+ ],
+ "time": "2026-04-23T19:03:45+00:00"
+ },
+ {
+ "name": "brick/math",
+ "version": "0.14.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/brick/math.git",
+ "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/brick/math/zipball/63422359a44b7f06cae63c3b429b59e8efcc0629",
+ "reference": "63422359a44b7f06cae63c3b429b59e8efcc0629",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.2"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.2",
+ "phpstan/phpstan": "2.1.22",
+ "phpunit/phpunit": "^11.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Brick\\Math\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Arbitrary-precision arithmetic library",
+ "keywords": [
+ "Arbitrary-precision",
+ "BigInteger",
+ "BigRational",
+ "arithmetic",
+ "bigdecimal",
+ "bignum",
+ "bignumber",
+ "brick",
+ "decimal",
+ "integer",
+ "math",
+ "mathematics",
+ "rational"
+ ],
+ "support": {
+ "issues": "https://github.com/brick/math/issues",
+ "source": "https://github.com/brick/math/tree/0.14.8"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/BenMorel",
+ "type": "github"
+ }
+ ],
+ "time": "2026-02-10T14:33:43+00:00"
+ },
+ {
+ "name": "carbonphp/carbon-doctrine-types",
+ "version": "3.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/CarbonPHP/carbon-doctrine-types.git",
+ "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/CarbonPHP/carbon-doctrine-types/zipball/18ba5ddfec8976260ead6e866180bd5d2f71aa1d",
+ "reference": "18ba5ddfec8976260ead6e866180bd5d2f71aa1d",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1"
+ },
+ "conflict": {
+ "doctrine/dbal": "<4.0.0 || >=5.0.0"
+ },
+ "require-dev": {
+ "doctrine/dbal": "^4.0.0",
+ "nesbot/carbon": "^2.71.0 || ^3.0.0",
+ "phpunit/phpunit": "^10.3"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Carbon\\Doctrine\\": "src/Carbon/Doctrine/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "KyleKatarn",
+ "email": "kylekatarnls@gmail.com"
+ }
+ ],
+ "description": "Types to use Carbon in Doctrine",
+ "keywords": [
+ "carbon",
+ "date",
+ "datetime",
+ "doctrine",
+ "time"
+ ],
+ "support": {
+ "issues": "https://github.com/CarbonPHP/carbon-doctrine-types/issues",
+ "source": "https://github.com/CarbonPHP/carbon-doctrine-types/tree/3.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/kylekatarnls",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/Carbon",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/nesbot/carbon",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-02-09T16:56:22+00:00"
+ },
+ {
+ "name": "composer/pcre",
+ "version": "3.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/pcre.git",
+ "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/pcre/zipball/b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
+ "reference": "b2bed4734f0cc156ee1fe9c0da2550420d99a21e",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "conflict": {
+ "phpstan/phpstan": "<1.11.10"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.12 || ^2",
+ "phpstan/phpstan-strict-rules": "^1 || ^2",
+ "phpunit/phpunit": "^8 || ^9"
+ },
+ "type": "library",
+ "extra": {
+ "phpstan": {
+ "includes": [
+ "extension.neon"
+ ]
+ },
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Pcre\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ }
+ ],
+ "description": "PCRE wrapping library that offers type-safe preg_* replacements.",
+ "keywords": [
+ "PCRE",
+ "preg",
+ "regex",
+ "regular expression"
+ ],
+ "support": {
+ "issues": "https://github.com/composer/pcre/issues",
+ "source": "https://github.com/composer/pcre/tree/3.3.2"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-11-12T16:29:46+00:00"
+ },
+ {
+ "name": "composer/semver",
+ "version": "3.4.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/semver.git",
+ "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95",
+ "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.2 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.11",
+ "symfony/phpunit-bridge": "^3 || ^7"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Semver\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nils Adermann",
+ "email": "naderman@naderman.de",
+ "homepage": "http://www.naderman.de"
+ },
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ },
+ {
+ "name": "Rob Bast",
+ "email": "rob.bast@gmail.com",
+ "homepage": "http://robbast.nl"
+ }
+ ],
+ "description": "Semver library that offers utilities, version constraint parsing and validation.",
+ "keywords": [
+ "semantic",
+ "semver",
+ "validation",
+ "versioning"
+ ],
+ "support": {
+ "irc": "ircs://irc.libera.chat:6697/composer",
+ "issues": "https://github.com/composer/semver/issues",
+ "source": "https://github.com/composer/semver/tree/3.4.4"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ }
+ ],
+ "time": "2025-08-20T19:15:30+00:00"
+ },
+ {
+ "name": "dflydev/dot-access-data",
+ "version": "v3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/dflydev/dflydev-dot-access-data.git",
+ "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f",
+ "reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^0.12.42",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.3",
+ "scrutinizer/ocular": "1.6.0",
+ "squizlabs/php_codesniffer": "^3.5",
+ "vimeo/psalm": "^4.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Dflydev\\DotAccessData\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Dragonfly Development Inc.",
+ "email": "info@dflydev.com",
+ "homepage": "http://dflydev.com"
+ },
+ {
+ "name": "Beau Simensen",
+ "email": "beau@dflydev.com",
+ "homepage": "http://beausimensen.com"
+ },
+ {
+ "name": "Carlos Frutos",
+ "email": "carlos@kiwing.it",
+ "homepage": "https://github.com/cfrutos"
+ },
+ {
+ "name": "Colin O'Dell",
+ "email": "colinodell@gmail.com",
+ "homepage": "https://www.colinodell.com"
+ }
+ ],
+ "description": "Given a deep data structure, access data by dot notation.",
+ "homepage": "https://github.com/dflydev/dflydev-dot-access-data",
+ "keywords": [
+ "access",
+ "data",
+ "dot",
+ "notation"
+ ],
+ "support": {
+ "issues": "https://github.com/dflydev/dflydev-dot-access-data/issues",
+ "source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3"
+ },
+ "time": "2024-07-08T12:26:09+00:00"
+ },
+ {
+ "name": "doctrine/inflector",
+ "version": "2.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/inflector.git",
+ "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/inflector/zipball/6d6c96277ea252fc1304627204c3d5e6e15faa3b",
+ "reference": "6d6c96277ea252fc1304627204c3d5e6e15faa3b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^12.0 || ^13.0",
+ "phpstan/phpstan": "^1.12 || ^2.0",
+ "phpstan/phpstan-phpunit": "^1.4 || ^2.0",
+ "phpstan/phpstan-strict-rules": "^1.6 || ^2.0",
+ "phpunit/phpunit": "^8.5 || ^12.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Inflector\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
+ },
+ {
+ "name": "Johannes Schmitt",
+ "email": "schmittjoh@gmail.com"
+ }
+ ],
+ "description": "PHP Doctrine Inflector is a small library that can perform string manipulations with regard to upper/lowercase and singular/plural forms of words.",
+ "homepage": "https://www.doctrine-project.org/projects/inflector.html",
+ "keywords": [
+ "inflection",
+ "inflector",
+ "lowercase",
+ "manipulation",
+ "php",
+ "plural",
+ "singular",
+ "strings",
+ "uppercase",
+ "words"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/inflector/issues",
+ "source": "https://github.com/doctrine/inflector/tree/2.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finflector",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-10T19:31:58+00:00"
+ },
+ {
+ "name": "doctrine/lexer",
+ "version": "3.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/lexer.git",
+ "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/lexer/zipball/31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd",
+ "reference": "31ad66abc0fc9e1a1f2d9bc6a42668d2fbbcd6dd",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^12",
+ "phpstan/phpstan": "^1.10",
+ "phpunit/phpunit": "^10.5",
+ "psalm/plugin-phpunit": "^0.18.3",
+ "vimeo/psalm": "^5.21"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Common\\Lexer\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Johannes Schmitt",
+ "email": "schmittjoh@gmail.com"
+ }
+ ],
+ "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.",
+ "homepage": "https://www.doctrine-project.org/projects/lexer.html",
+ "keywords": [
+ "annotations",
+ "docblock",
+ "lexer",
+ "parser",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/lexer/issues",
+ "source": "https://github.com/doctrine/lexer/tree/3.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-02-05T11:56:58+00:00"
+ },
+ {
+ "name": "dompdf/dompdf",
+ "version": "v3.1.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/dompdf/dompdf.git",
+ "reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/dompdf/dompdf/zipball/f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496",
+ "reference": "f11ead23a8a76d0ff9bbc6c7c8fd7e05ca328496",
+ "shasum": ""
+ },
+ "require": {
+ "dompdf/php-font-lib": "^1.0.0",
+ "dompdf/php-svg-lib": "^1.0.0",
+ "ext-dom": "*",
+ "ext-mbstring": "*",
+ "masterminds/html5": "^2.0",
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "ext-gd": "*",
+ "ext-json": "*",
+ "ext-zip": "*",
+ "mockery/mockery": "^1.3",
+ "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11",
+ "squizlabs/php_codesniffer": "^3.5",
+ "symfony/process": "^4.4 || ^5.4 || ^6.2 || ^7.0"
+ },
+ "suggest": {
+ "ext-gd": "Needed to process images",
+ "ext-gmagick": "Improves image processing performance",
+ "ext-imagick": "Improves image processing performance",
+ "ext-zlib": "Needed for pdf stream compression"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Dompdf\\": "src/"
+ },
+ "classmap": [
+ "lib/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-2.1"
+ ],
+ "authors": [
+ {
+ "name": "The Dompdf Community",
+ "homepage": "https://github.com/dompdf/dompdf/blob/master/AUTHORS.md"
+ }
+ ],
+ "description": "DOMPDF is a CSS 2.1 compliant HTML to PDF converter",
+ "homepage": "https://github.com/dompdf/dompdf",
+ "support": {
+ "issues": "https://github.com/dompdf/dompdf/issues",
+ "source": "https://github.com/dompdf/dompdf/tree/v3.1.5"
+ },
+ "time": "2026-03-03T13:54:37+00:00"
+ },
+ {
+ "name": "dompdf/php-font-lib",
+ "version": "1.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/dompdf/php-font-lib.git",
+ "reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/dompdf/php-font-lib/zipball/a6e9a688a2a80016ac080b97be73d3e10c444c9a",
+ "reference": "a6e9a688a2a80016ac080b97be73d3e10c444c9a",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11 || ^12"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "FontLib\\": "src/FontLib"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-2.1-or-later"
+ ],
+ "authors": [
+ {
+ "name": "The FontLib Community",
+ "homepage": "https://github.com/dompdf/php-font-lib/blob/master/AUTHORS.md"
+ }
+ ],
+ "description": "A library to read, parse, export and make subsets of different types of font files.",
+ "homepage": "https://github.com/dompdf/php-font-lib",
+ "support": {
+ "issues": "https://github.com/dompdf/php-font-lib/issues",
+ "source": "https://github.com/dompdf/php-font-lib/tree/1.0.2"
+ },
+ "time": "2026-01-20T14:10:26+00:00"
+ },
+ {
+ "name": "dompdf/php-svg-lib",
+ "version": "1.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/dompdf/php-svg-lib.git",
+ "reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/dompdf/php-svg-lib/zipball/8259ffb930817e72b1ff1caef5d226501f3dfeb1",
+ "reference": "8259ffb930817e72b1ff1caef5d226501f3dfeb1",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": "^7.1 || ^8.0",
+ "sabberworm/php-css-parser": "^8.4 || ^9.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^7.5 || ^8 || ^9 || ^10 || ^11"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Svg\\": "src/Svg"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "LGPL-3.0-or-later"
+ ],
+ "authors": [
+ {
+ "name": "The SvgLib Community",
+ "homepage": "https://github.com/dompdf/php-svg-lib/blob/master/AUTHORS.md"
+ }
+ ],
+ "description": "A library to read, parse and export to PDF SVG files.",
+ "homepage": "https://github.com/dompdf/php-svg-lib",
+ "support": {
+ "issues": "https://github.com/dompdf/php-svg-lib/issues",
+ "source": "https://github.com/dompdf/php-svg-lib/tree/1.0.2"
+ },
+ "time": "2026-01-02T16:01:13+00:00"
+ },
+ {
+ "name": "dragonmantank/cron-expression",
+ "version": "v3.6.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/dragonmantank/cron-expression.git",
+ "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/d61a8a9604ec1f8c3d150d09db6ce98b32675013",
+ "reference": "d61a8a9604ec1f8c3d150d09db6ce98b32675013",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.2|^8.3|^8.4|^8.5"
+ },
+ "replace": {
+ "mtdowling/cron-expression": "^1.0"
+ },
+ "require-dev": {
+ "phpstan/extension-installer": "^1.4.3",
+ "phpstan/phpstan": "^1.12.32|^2.1.31",
+ "phpunit/phpunit": "^8.5.48|^9.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Cron\\": "src/Cron/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Chris Tankersley",
+ "email": "chris@ctankersley.com",
+ "homepage": "https://github.com/dragonmantank"
+ }
+ ],
+ "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due",
+ "keywords": [
+ "cron",
+ "schedule"
+ ],
+ "support": {
+ "issues": "https://github.com/dragonmantank/cron-expression/issues",
+ "source": "https://github.com/dragonmantank/cron-expression/tree/v3.6.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/dragonmantank",
+ "type": "github"
+ }
+ ],
+ "time": "2025-10-31T18:51:33+00:00"
+ },
+ {
+ "name": "egulias/email-validator",
+ "version": "4.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/egulias/EmailValidator.git",
+ "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/egulias/EmailValidator/zipball/d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa",
+ "reference": "d42c8731f0624ad6bdc8d3e5e9a4524f68801cfa",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/lexer": "^2.0 || ^3.0",
+ "php": ">=8.1",
+ "symfony/polyfill-intl-idn": "^1.26"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^10.2",
+ "vimeo/psalm": "^5.12"
+ },
+ "suggest": {
+ "ext-intl": "PHP Internationalization Libraries are required to use the SpoofChecking validation"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Egulias\\EmailValidator\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Eduardo Gulias Davis"
+ }
+ ],
+ "description": "A library for validating emails against several RFCs",
+ "homepage": "https://github.com/egulias/EmailValidator",
+ "keywords": [
+ "email",
+ "emailvalidation",
+ "emailvalidator",
+ "validation",
+ "validator"
+ ],
+ "support": {
+ "issues": "https://github.com/egulias/EmailValidator/issues",
+ "source": "https://github.com/egulias/EmailValidator/tree/4.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/egulias",
+ "type": "github"
+ }
+ ],
+ "time": "2025-03-06T22:45:56+00:00"
+ },
+ {
+ "name": "fruitcake/php-cors",
+ "version": "v1.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/fruitcake/php-cors.git",
+ "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/fruitcake/php-cors/zipball/38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379",
+ "reference": "38aaa6c3fd4c157ffe2a4d10aa8b9b16ba8de379",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1",
+ "symfony/http-foundation": "^5.4|^6.4|^7.3|^8"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^2",
+ "phpunit/phpunit": "^9",
+ "squizlabs/php_codesniffer": "^4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Fruitcake\\Cors\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fruitcake",
+ "homepage": "https://fruitcake.nl"
+ },
+ {
+ "name": "Barryvdh",
+ "email": "barryvdh@gmail.com"
+ }
+ ],
+ "description": "Cross-origin resource sharing library for the Symfony HttpFoundation",
+ "homepage": "https://github.com/fruitcake/php-cors",
+ "keywords": [
+ "cors",
+ "laravel",
+ "symfony"
+ ],
+ "support": {
+ "issues": "https://github.com/fruitcake/php-cors/issues",
+ "source": "https://github.com/fruitcake/php-cors/tree/v1.4.0"
+ },
+ "funding": [
+ {
+ "url": "https://fruitcake.nl",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/barryvdh",
+ "type": "github"
+ }
+ ],
+ "time": "2025-12-03T09:33:47+00:00"
+ },
+ {
+ "name": "graham-campbell/result-type",
+ "version": "v1.1.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/GrahamCampbell/Result-Type.git",
+ "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/e01f4a821471308ba86aa202fed6698b6b695e3b",
+ "reference": "e01f4a821471308ba86aa202fed6698b6b695e3b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0",
+ "phpoption/phpoption": "^1.9.5"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^8.5.41 || ^9.6.22 || ^10.5.45 || ^11.5.7"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "GrahamCampbell\\ResultType\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ }
+ ],
+ "description": "An Implementation Of The Result Type",
+ "keywords": [
+ "Graham Campbell",
+ "GrahamCampbell",
+ "Result Type",
+ "Result-Type",
+ "result"
+ ],
+ "support": {
+ "issues": "https://github.com/GrahamCampbell/Result-Type/issues",
+ "source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.1.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-12-27T19:43:20+00:00"
+ },
+ {
+ "name": "guzzlehttp/guzzle",
+ "version": "7.11.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/guzzle.git",
+ "reference": "5af96f374e0ab4ebd747b8310888c99d3adb0a8c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/5af96f374e0ab4ebd747b8310888c99d3adb0a8c",
+ "reference": "5af96f374e0ab4ebd747b8310888c99d3adb0a8c",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "guzzlehttp/promises": "^2.5",
+ "guzzlehttp/psr7": "^2.11",
+ "php": "^7.2.5 || ^8.0",
+ "psr/http-client": "^1.0",
+ "symfony/deprecation-contracts": "^2.5 || ^3.0",
+ "symfony/polyfill-php80": "^1.24"
+ },
+ "provide": {
+ "psr/http-client-implementation": "1.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "ext-curl": "*",
+ "guzzle/client-integration-tests": "3.0.2",
+ "guzzlehttp/test-server": "^0.5",
+ "php-http/message-factory": "^1.1",
+ "phpunit/phpunit": "^8.5.52 || ^9.6.34",
+ "psr/log": "^1.1 || ^2.0 || ^3.0"
+ },
+ "suggest": {
+ "ext-curl": "Required for CURL handler support",
+ "ext-intl": "Required for Internationalized Domain Name (IDN) support",
+ "psr/log": "Required for using the Log middleware"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/functions_include.php"
+ ],
+ "psr-4": {
+ "GuzzleHttp\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Jeremy Lindblom",
+ "email": "jeremeamia@gmail.com",
+ "homepage": "https://github.com/jeremeamia"
+ },
+ {
+ "name": "George Mponos",
+ "email": "gmponos@gmail.com",
+ "homepage": "https://github.com/gmponos"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://github.com/sagikazarmark"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ }
+ ],
+ "description": "Guzzle is a PHP HTTP client library",
+ "keywords": [
+ "client",
+ "curl",
+ "framework",
+ "http",
+ "http client",
+ "psr-18",
+ "psr-7",
+ "rest",
+ "web service"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/guzzle/issues",
+ "source": "https://github.com/guzzle/guzzle/tree/7.11.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/guzzle",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-06-07T22:54:06+00:00"
+ },
+ {
+ "name": "guzzlehttp/promises",
+ "version": "2.5.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/promises.git",
+ "reference": "4360e982f87f5f258bf872d094647791db2f4c8e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/promises/zipball/4360e982f87f5f258bf872d094647791db2f4c8e",
+ "reference": "4360e982f87f5f258bf872d094647791db2f4c8e",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0",
+ "symfony/deprecation-contracts": "^2.5 || ^3.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "phpunit/phpunit": "^8.5.52 || ^9.6.34"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Promise\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ }
+ ],
+ "description": "Guzzle promises library",
+ "keywords": [
+ "promise"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/promises/issues",
+ "source": "https://github.com/guzzle/promises/tree/2.5.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/promises",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-06-02T12:23:43+00:00"
+ },
+ {
+ "name": "guzzlehttp/psr7",
+ "version": "2.11.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/psr7.git",
+ "reference": "bbb5e61349fa5cb822b3e87842b951088b76b81f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/bbb5e61349fa5cb822b3e87842b951088b76b81f",
+ "reference": "bbb5e61349fa5cb822b3e87842b951088b76b81f",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0",
+ "psr/http-factory": "^1.0",
+ "psr/http-message": "^1.1 || ^2.0",
+ "ralouphie/getallheaders": "^3.0",
+ "symfony/deprecation-contracts": "^2.5 || ^3.0",
+ "symfony/polyfill-php80": "^1.24"
+ },
+ "provide": {
+ "psr/http-factory-implementation": "1.0",
+ "psr/http-message-implementation": "1.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "http-interop/http-factory-tests": "1.1.0",
+ "jshttp/mime-db": "1.54.0.1",
+ "phpunit/phpunit": "^8.5.52 || ^9.6.34"
+ },
+ "suggest": {
+ "laminas/laminas-httphandlerrunner": "Emit PSR-7 responses"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\Psr7\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "George Mponos",
+ "email": "gmponos@gmail.com",
+ "homepage": "https://github.com/gmponos"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://github.com/sagikazarmark"
+ },
+ {
+ "name": "Tobias Schultze",
+ "email": "webmaster@tubo-world.de",
+ "homepage": "https://github.com/Tobion"
+ },
+ {
+ "name": "Márk Sági-Kazár",
+ "email": "mark.sagikazar@gmail.com",
+ "homepage": "https://sagikazarmark.hu"
+ }
+ ],
+ "description": "PSR-7 message implementation that also provides common utility methods",
+ "keywords": [
+ "http",
+ "message",
+ "psr-7",
+ "request",
+ "response",
+ "stream",
+ "uri",
+ "url"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/psr7/issues",
+ "source": "https://github.com/guzzle/psr7/tree/2.11.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/psr7",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-06-02T12:30:48+00:00"
+ },
+ {
+ "name": "guzzlehttp/uri-template",
+ "version": "v1.0.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/guzzle/uri-template.git",
+ "reference": "eef7f87bab6f204eba3c39224d8075c70c637946"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/guzzle/uri-template/zipball/eef7f87bab6f204eba3c39224d8075c70c637946",
+ "reference": "eef7f87bab6f204eba3c39224d8075c70c637946",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0",
+ "symfony/polyfill-php80": "^1.24"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "phpunit/phpunit": "^8.5.52 || ^9.6.34",
+ "uri-template/tests": "1.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "GuzzleHttp\\UriTemplate\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ },
+ {
+ "name": "George Mponos",
+ "email": "gmponos@gmail.com",
+ "homepage": "https://github.com/gmponos"
+ },
+ {
+ "name": "Tobias Nyholm",
+ "email": "tobias.nyholm@gmail.com",
+ "homepage": "https://github.com/Nyholm"
+ }
+ ],
+ "description": "A polyfill class for uri_template of PHP",
+ "keywords": [
+ "guzzlehttp",
+ "uri-template"
+ ],
+ "support": {
+ "issues": "https://github.com/guzzle/uri-template/issues",
+ "source": "https://github.com/guzzle/uri-template/tree/v1.0.6"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/Nyholm",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/guzzlehttp/uri-template",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-23T22:00:21+00:00"
+ },
+ {
+ "name": "laravel/ai",
+ "version": "v0.6.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/ai.git",
+ "reference": "b457ea4c0ec0fb1bca5504666332b653f7648d7e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/ai/zipball/b457ea4c0ec0fb1bca5504666332b653f7648d7e",
+ "reference": "b457ea4c0ec0fb1bca5504666332b653f7648d7e",
+ "shasum": ""
+ },
+ "require": {
+ "aws/aws-sdk-php": "^3.339",
+ "illuminate/console": "^12.0|^13.0",
+ "illuminate/container": "^12.0|^13.0",
+ "illuminate/contracts": "^12.0|^13.0",
+ "illuminate/filesystem": "^12.0|^13.0",
+ "illuminate/json-schema": "^12.0|^13.0",
+ "illuminate/support": "^12.0|^13.0",
+ "laravel/prompts": "^0.3.6",
+ "laravel/serializable-closure": "^2.0",
+ "php": "^8.3"
+ },
+ "require-dev": {
+ "laravel/pint": "^1.26",
+ "mockery/mockery": "^1.6.12",
+ "orchestra/testbench": "^10.6|^11.0",
+ "pestphp/pest": "^3.0|^4.0",
+ "pestphp/pest-plugin-laravel": "^3.0|^4.0"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Laravel\\Ai\\AiServiceProvider"
+ ]
+ },
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "functions.php"
+ ],
+ "psr-4": {
+ "Laravel\\Ai\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "The official AI SDK for Laravel.",
+ "homepage": "https://github.com/laravel/ai",
+ "keywords": [
+ "ai",
+ "laravel"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/ai/issues",
+ "source": "https://github.com/laravel/ai"
+ },
+ "time": "2026-05-11T10:46:54+00:00"
+ },
+ {
+ "name": "laravel/breeze",
+ "version": "v2.4.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/breeze.git",
+ "reference": "4f20e7b2cc8d25daa85d8647241a89c8e0930305"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/breeze/zipball/4f20e7b2cc8d25daa85d8647241a89c8e0930305",
+ "reference": "4f20e7b2cc8d25daa85d8647241a89c8e0930305",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/console": "^11.0|^12.0|^13.0",
+ "illuminate/filesystem": "^11.0|^12.0|^13.0",
+ "illuminate/support": "^11.0|^12.0|^13.0",
+ "illuminate/validation": "^11.0|^12.0|^13.0",
+ "php": "^8.2.0",
+ "symfony/console": "^7.0|^8.0"
+ },
+ "require-dev": {
+ "laravel/framework": "^11.0|^12.0|^13.0",
+ "orchestra/testbench-core": "^9.0|^10.0|^11.0",
+ "phpstan/phpstan": "^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Laravel\\Breeze\\BreezeServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laravel\\Breeze\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ }
+ ],
+ "description": "Minimal Laravel authentication scaffolding with Blade and Tailwind.",
+ "keywords": [
+ "auth",
+ "laravel"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/breeze/issues",
+ "source": "https://github.com/laravel/breeze"
+ },
+ "time": "2026-05-14T16:54:25+00:00"
+ },
+ {
+ "name": "laravel/framework",
+ "version": "v13.14.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/framework.git",
+ "reference": "e60b1c817a9ef7da319e4007de6cfda5301a58c0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/framework/zipball/e60b1c817a9ef7da319e4007de6cfda5301a58c0",
+ "reference": "e60b1c817a9ef7da319e4007de6cfda5301a58c0",
+ "shasum": ""
+ },
+ "require": {
+ "brick/math": "^0.14.2 || ^0.15 || ^0.16 || ^0.17",
+ "composer-runtime-api": "^2.2",
+ "doctrine/inflector": "^2.0.5",
+ "dragonmantank/cron-expression": "^3.4",
+ "egulias/email-validator": "^4.0",
+ "ext-ctype": "*",
+ "ext-filter": "*",
+ "ext-hash": "*",
+ "ext-mbstring": "*",
+ "ext-openssl": "*",
+ "ext-session": "*",
+ "ext-tokenizer": "*",
+ "fruitcake/php-cors": "^1.3",
+ "guzzlehttp/guzzle": "^7.8.2",
+ "guzzlehttp/promises": "^2.0.3",
+ "guzzlehttp/uri-template": "^1.0",
+ "laravel/prompts": "^0.3.0",
+ "laravel/serializable-closure": "^2.0.10",
+ "league/commonmark": "^2.8.1",
+ "league/flysystem": "^3.25.1",
+ "league/flysystem-local": "^3.25.1",
+ "league/uri": "^7.5.1",
+ "monolog/monolog": "^3.0",
+ "nesbot/carbon": "^3.8.4",
+ "nunomaduro/termwind": "^2.0",
+ "php": "^8.3",
+ "psr/container": "^1.1.1 || ^2.0.1",
+ "psr/log": "^1.0 || ^2.0 || ^3.0",
+ "psr/simple-cache": "^1.0 || ^2.0 || ^3.0",
+ "ramsey/uuid": "^4.7",
+ "symfony/console": "^7.4.0 || ^8.0.0",
+ "symfony/error-handler": "^7.4.0 || ^8.0.0",
+ "symfony/finder": "^7.4.0 || ^8.0.0",
+ "symfony/http-foundation": "^7.4.0 || ^8.0.0",
+ "symfony/http-kernel": "^7.4.0 || ^8.0.0",
+ "symfony/mailer": "^7.4.0 || ^8.0.0",
+ "symfony/mime": "^7.4.0 || ^8.0.0",
+ "symfony/polyfill-php84": "^1.36",
+ "symfony/polyfill-php85": "^1.36",
+ "symfony/polyfill-php86": "^1.36",
+ "symfony/process": "^7.4.5 || ^8.0.5",
+ "symfony/routing": "^7.4.0 || ^8.0.0",
+ "symfony/uid": "^7.4.0 || ^8.0.0",
+ "symfony/var-dumper": "^7.4.0 || ^8.0.0",
+ "tijsverkoyen/css-to-inline-styles": "^2.2.5",
+ "vlucas/phpdotenv": "^5.6.1",
+ "voku/portable-ascii": "^2.0.2"
+ },
+ "conflict": {
+ "tightenco/collect": "<5.5.33"
+ },
+ "provide": {
+ "psr/container-implementation": "1.1 || 2.0",
+ "psr/log-implementation": "1.0 || 2.0 || 3.0",
+ "psr/simple-cache-implementation": "1.0 || 2.0 || 3.0"
+ },
+ "replace": {
+ "illuminate/auth": "self.version",
+ "illuminate/broadcasting": "self.version",
+ "illuminate/bus": "self.version",
+ "illuminate/cache": "self.version",
+ "illuminate/collections": "self.version",
+ "illuminate/concurrency": "self.version",
+ "illuminate/conditionable": "self.version",
+ "illuminate/config": "self.version",
+ "illuminate/console": "self.version",
+ "illuminate/container": "self.version",
+ "illuminate/contracts": "self.version",
+ "illuminate/cookie": "self.version",
+ "illuminate/database": "self.version",
+ "illuminate/encryption": "self.version",
+ "illuminate/events": "self.version",
+ "illuminate/filesystem": "self.version",
+ "illuminate/hashing": "self.version",
+ "illuminate/http": "self.version",
+ "illuminate/json-schema": "self.version",
+ "illuminate/log": "self.version",
+ "illuminate/macroable": "self.version",
+ "illuminate/mail": "self.version",
+ "illuminate/notifications": "self.version",
+ "illuminate/pagination": "self.version",
+ "illuminate/pipeline": "self.version",
+ "illuminate/process": "self.version",
+ "illuminate/queue": "self.version",
+ "illuminate/redis": "self.version",
+ "illuminate/reflection": "self.version",
+ "illuminate/routing": "self.version",
+ "illuminate/session": "self.version",
+ "illuminate/support": "self.version",
+ "illuminate/testing": "self.version",
+ "illuminate/translation": "self.version",
+ "illuminate/validation": "self.version",
+ "illuminate/view": "self.version",
+ "spatie/once": "*"
+ },
+ "require-dev": {
+ "ably/ably-php": "^1.0",
+ "aws/aws-sdk-php": "^3.322.9",
+ "ext-gmp": "*",
+ "fakerphp/faker": "^1.24",
+ "guzzlehttp/psr7": "^2.9",
+ "laravel/pint": "^1.18",
+ "league/flysystem-aws-s3-v3": "^3.25.1",
+ "league/flysystem-ftp": "^3.25.1",
+ "league/flysystem-path-prefixing": "^3.25.1",
+ "league/flysystem-read-only": "^3.25.1",
+ "league/flysystem-sftp-v3": "^3.25.1",
+ "mockery/mockery": "^1.6.10",
+ "opis/json-schema": "^2.4.1",
+ "orchestra/testbench-core": "^11.0.0",
+ "pda/pheanstalk": "^7.0.0 || ^8.0.0",
+ "php-http/discovery": "^1.15",
+ "phpstan/phpstan": "^2.0",
+ "phpunit/phpunit": "^11.5.50 || ^12.5.8 || ^13.0.3",
+ "predis/predis": "^2.3 || ^3.0",
+ "rector/rector": "^2.3",
+ "resend/resend-php": "^1.0",
+ "symfony/cache": "^7.4.0 || ^8.0.0",
+ "symfony/http-client": "^7.4.0 || ^8.0.0",
+ "symfony/psr-http-message-bridge": "^7.4.0 || ^8.0.0",
+ "symfony/translation": "^7.4.0 || ^8.0.0"
+ },
+ "suggest": {
+ "ably/ably-php": "Required to use the Ably broadcast driver (^1.0).",
+ "aws/aws-sdk-php": "Required to use the SQS queue driver, DynamoDb failed job storage, and SES mail driver (^3.322.9).",
+ "brianium/paratest": "Required to run tests in parallel (^7.0 || ^8.0).",
+ "ext-apcu": "Required to use the APC cache driver.",
+ "ext-fileinfo": "Required to use the Filesystem class.",
+ "ext-ftp": "Required to use the Flysystem FTP driver.",
+ "ext-gd": "Required to use Illuminate\\Http\\Testing\\FileFactory::image().",
+ "ext-memcached": "Required to use the memcache cache driver.",
+ "ext-pcntl": "Required to use all features of the queue worker and console signal trapping.",
+ "ext-pdo": "Required to use all database features.",
+ "ext-posix": "Required to use all features of the queue worker.",
+ "ext-redis": "Required to use the Redis cache and queue drivers (^4.0 || ^5.0 || ^6.0).",
+ "fakerphp/faker": "Required to generate fake data using the fake() helper (^1.23).",
+ "filp/whoops": "Required for friendly error pages in development (^2.14.3).",
+ "laravel/tinker": "Required to use the tinker console command (^2.0).",
+ "league/flysystem-aws-s3-v3": "Required to use the Flysystem S3 driver (^3.25.1).",
+ "league/flysystem-ftp": "Required to use the Flysystem FTP driver (^3.25.1).",
+ "league/flysystem-path-prefixing": "Required to use the scoped driver (^3.25.1).",
+ "league/flysystem-read-only": "Required to use read-only disks (^3.25.1)",
+ "league/flysystem-sftp-v3": "Required to use the Flysystem SFTP driver (^3.25.1).",
+ "mockery/mockery": "Required to use mocking (^1.6).",
+ "pda/pheanstalk": "Required to use the beanstalk queue driver (^7.0 || ^8.0).",
+ "php-http/discovery": "Required to use PSR-7 bridging features (^1.15).",
+ "phpunit/phpunit": "Required to use assertions and run tests (^11.5.50 || ^12.5.8 || ^13.0.3).",
+ "predis/predis": "Required to use the predis connector (^2.3 || ^3.0).",
+ "psr/http-message": "Required to allow Storage::put to accept a StreamInterface (^1.0).",
+ "pusher/pusher-php-server": "Required to use the Pusher broadcast driver (^6.0 || ^7.0).",
+ "resend/resend-php": "Required to enable support for the Resend mail transport (^0.10.0 || ^1.0).",
+ "spatie/fork": "Required to use the 'fork' concurrency driver (^1.2).",
+ "symfony/cache": "Required to PSR-6 cache bridge (^7.4 || ^8.0).",
+ "symfony/filesystem": "Required to enable support for relative symbolic links (^7.4 || ^8.0).",
+ "symfony/http-client": "Required to enable support for the Symfony API mail transports (^7.4 || ^8.0).",
+ "symfony/mailgun-mailer": "Required to enable support for the Mailgun mail transport (^7.4 || ^8.0).",
+ "symfony/postmark-mailer": "Required to enable support for the Postmark mail transport (^7.4 || ^8.0).",
+ "symfony/psr-http-message-bridge": "Required to use PSR-7 bridging features (^7.4 || ^8.0)."
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "13.0.x-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/Illuminate/Collections/functions.php",
+ "src/Illuminate/Collections/helpers.php",
+ "src/Illuminate/Events/functions.php",
+ "src/Illuminate/Filesystem/functions.php",
+ "src/Illuminate/Foundation/helpers.php",
+ "src/Illuminate/Log/functions.php",
+ "src/Illuminate/Reflection/helpers.php",
+ "src/Illuminate/Support/functions.php",
+ "src/Illuminate/Support/helpers.php"
+ ],
+ "psr-4": {
+ "Illuminate\\": "src/Illuminate/",
+ "Illuminate\\Support\\": [
+ "src/Illuminate/Macroable/",
+ "src/Illuminate/Collections/",
+ "src/Illuminate/Conditionable/",
+ "src/Illuminate/Reflection/"
+ ]
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ }
+ ],
+ "description": "The Laravel Framework.",
+ "homepage": "https://laravel.com",
+ "keywords": [
+ "framework",
+ "laravel"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/framework/issues",
+ "source": "https://github.com/laravel/framework"
+ },
+ "time": "2026-06-04T18:46:35+00:00"
+ },
+ {
+ "name": "laravel/prompts",
+ "version": "v0.3.18",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/prompts.git",
+ "reference": "a19af51bb144bf87f08397921fa619f85c7d4e72"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/prompts/zipball/a19af51bb144bf87f08397921fa619f85c7d4e72",
+ "reference": "a19af51bb144bf87f08397921fa619f85c7d4e72",
+ "shasum": ""
+ },
+ "require": {
+ "composer-runtime-api": "^2.2",
+ "ext-mbstring": "*",
+ "php": "^8.1",
+ "symfony/console": "^6.2|^7.0|^8.0"
+ },
+ "conflict": {
+ "illuminate/console": ">=10.17.0 <10.25.0",
+ "laravel/framework": ">=10.17.0 <10.25.0"
+ },
+ "require-dev": {
+ "illuminate/collections": "^10.0|^11.0|^12.0|^13.0",
+ "mockery/mockery": "^1.5",
+ "pestphp/pest": "^2.3|^3.4|^4.0",
+ "phpstan/phpstan": "^1.12.28",
+ "phpstan/phpstan-mockery": "^1.1.3"
+ },
+ "suggest": {
+ "ext-pcntl": "Required for the spinner to be animated."
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "0.3.x-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/helpers.php"
+ ],
+ "psr-4": {
+ "Laravel\\Prompts\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Add beautiful and user-friendly forms to your command-line applications.",
+ "support": {
+ "issues": "https://github.com/laravel/prompts/issues",
+ "source": "https://github.com/laravel/prompts/tree/v0.3.18"
+ },
+ "time": "2026-05-19T00:47:18+00:00"
+ },
+ {
+ "name": "laravel/sanctum",
+ "version": "v4.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/sanctum.git",
+ "reference": "2a9bccc18e9907808e0018dd15fa643937886b1e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/sanctum/zipball/2a9bccc18e9907808e0018dd15fa643937886b1e",
+ "reference": "2a9bccc18e9907808e0018dd15fa643937886b1e",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "illuminate/console": "^11.0|^12.0|^13.0",
+ "illuminate/contracts": "^11.0|^12.0|^13.0",
+ "illuminate/database": "^11.0|^12.0|^13.0",
+ "illuminate/support": "^11.0|^12.0|^13.0",
+ "php": "^8.2",
+ "symfony/console": "^7.0|^8.0"
+ },
+ "require-dev": {
+ "mockery/mockery": "^1.6",
+ "orchestra/testbench": "^9.15|^10.8|^11.0",
+ "phpstan/phpstan": "^1.10"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Laravel\\Sanctum\\SanctumServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laravel\\Sanctum\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ }
+ ],
+ "description": "Laravel Sanctum provides a featherweight authentication system for SPAs and simple APIs.",
+ "keywords": [
+ "auth",
+ "laravel",
+ "sanctum"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/sanctum/issues",
+ "source": "https://github.com/laravel/sanctum"
+ },
+ "time": "2026-04-30T11:46:25+00:00"
+ },
+ {
+ "name": "laravel/serializable-closure",
+ "version": "v2.0.13",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/serializable-closure.git",
+ "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b566ee0dd251f3c4078bed003a7ce015f5ea6dce",
+ "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "illuminate/support": "^10.0|^11.0|^12.0|^13.0",
+ "nesbot/carbon": "^2.67|^3.0",
+ "pestphp/pest": "^2.36|^3.0|^4.0",
+ "phpstan/phpstan": "^2.0",
+ "symfony/var-dumper": "^6.2.0|^7.0.0|^8.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laravel\\SerializableClosure\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ },
+ {
+ "name": "Nuno Maduro",
+ "email": "nuno@laravel.com"
+ }
+ ],
+ "description": "Laravel Serializable Closure provides an easy and secure way to serialize closures in PHP.",
+ "keywords": [
+ "closure",
+ "laravel",
+ "serializable"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/serializable-closure/issues",
+ "source": "https://github.com/laravel/serializable-closure"
+ },
+ "time": "2026-04-16T14:03:50+00:00"
+ },
+ {
+ "name": "laravel/tinker",
+ "version": "v3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/tinker.git",
+ "reference": "4faba77764bd33411735936acdf30446d058c78b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/tinker/zipball/4faba77764bd33411735936acdf30446d058c78b",
+ "reference": "4faba77764bd33411735936acdf30446d058c78b",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/console": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0",
+ "illuminate/contracts": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0",
+ "illuminate/support": "^8.0|^9.0|^10.0|^11.0|^12.0|^13.0",
+ "php": "^8.1",
+ "psy/psysh": "^0.12.0",
+ "symfony/var-dumper": "^5.4|^6.0|^7.0|^8.0"
+ },
+ "require-dev": {
+ "mockery/mockery": "~1.3.3|^1.4.2",
+ "phpstan/phpstan": "^1.10",
+ "phpunit/phpunit": "^10.5|^11.5"
+ },
+ "suggest": {
+ "illuminate/database": "The Illuminate Database package (^8.0|^9.0|^10.0|^11.0|^12.0|^13.0)."
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Laravel\\Tinker\\TinkerServiceProvider"
+ ]
+ },
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laravel\\Tinker\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ }
+ ],
+ "description": "Powerful REPL for the Laravel framework.",
+ "keywords": [
+ "REPL",
+ "Tinker",
+ "laravel",
+ "psysh"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/tinker/issues",
+ "source": "https://github.com/laravel/tinker/tree/v3.0.2"
+ },
+ "time": "2026-03-17T14:54:13+00:00"
+ },
+ {
+ "name": "league/commonmark",
+ "version": "2.8.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/commonmark.git",
+ "reference": "59fb075d2101740c337c7216e3f32b36c204218b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/59fb075d2101740c337c7216e3f32b36c204218b",
+ "reference": "59fb075d2101740c337c7216e3f32b36c204218b",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "league/config": "^1.1.1",
+ "php": "^7.4 || ^8.0",
+ "psr/event-dispatcher": "^1.0",
+ "symfony/deprecation-contracts": "^2.1 || ^3.0",
+ "symfony/polyfill-php80": "^1.16"
+ },
+ "require-dev": {
+ "cebe/markdown": "^1.0",
+ "commonmark/cmark": "0.31.1",
+ "commonmark/commonmark.js": "0.31.1",
+ "composer/package-versions-deprecated": "^1.8",
+ "embed/embed": "^4.4",
+ "erusev/parsedown": "^1.0",
+ "ext-json": "*",
+ "github/gfm": "0.29.0",
+ "michelf/php-markdown": "^1.4 || ^2.0",
+ "nyholm/psr7": "^1.5",
+ "phpstan/phpstan": "^1.8.2",
+ "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0",
+ "scrutinizer/ocular": "^1.8.1",
+ "symfony/finder": "^5.3 | ^6.0 | ^7.0 || ^8.0",
+ "symfony/process": "^5.4 | ^6.0 | ^7.0 || ^8.0",
+ "symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0 || ^8.0",
+ "unleashedtech/php-coding-standard": "^3.1.1",
+ "vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0"
+ },
+ "suggest": {
+ "symfony/yaml": "v2.3+ required if using the Front Matter extension"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "2.9-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "League\\CommonMark\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Colin O'Dell",
+ "email": "colinodell@gmail.com",
+ "homepage": "https://www.colinodell.com",
+ "role": "Lead Developer"
+ }
+ ],
+ "description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)",
+ "homepage": "https://commonmark.thephpleague.com",
+ "keywords": [
+ "commonmark",
+ "flavored",
+ "gfm",
+ "github",
+ "github-flavored",
+ "markdown",
+ "md",
+ "parser"
+ ],
+ "support": {
+ "docs": "https://commonmark.thephpleague.com/",
+ "forum": "https://github.com/thephpleague/commonmark/discussions",
+ "issues": "https://github.com/thephpleague/commonmark/issues",
+ "rss": "https://github.com/thephpleague/commonmark/releases.atom",
+ "source": "https://github.com/thephpleague/commonmark"
+ },
+ "funding": [
+ {
+ "url": "https://www.colinodell.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.paypal.me/colinpodell/10.00",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/colinodell",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/league/commonmark",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-03-19T13:16:38+00:00"
+ },
+ {
+ "name": "league/config",
+ "version": "v1.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/config.git",
+ "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3",
+ "reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3",
+ "shasum": ""
+ },
+ "require": {
+ "dflydev/dot-access-data": "^3.0.1",
+ "nette/schema": "^1.2",
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.8.2",
+ "phpunit/phpunit": "^9.5.5",
+ "scrutinizer/ocular": "^1.8.1",
+ "unleashedtech/php-coding-standard": "^3.1",
+ "vimeo/psalm": "^4.7.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.2-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "League\\Config\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Colin O'Dell",
+ "email": "colinodell@gmail.com",
+ "homepage": "https://www.colinodell.com",
+ "role": "Lead Developer"
+ }
+ ],
+ "description": "Define configuration arrays with strict schemas and access values with dot notation",
+ "homepage": "https://config.thephpleague.com",
+ "keywords": [
+ "array",
+ "config",
+ "configuration",
+ "dot",
+ "dot-access",
+ "nested",
+ "schema"
+ ],
+ "support": {
+ "docs": "https://config.thephpleague.com/",
+ "issues": "https://github.com/thephpleague/config/issues",
+ "rss": "https://github.com/thephpleague/config/releases.atom",
+ "source": "https://github.com/thephpleague/config"
+ },
+ "funding": [
+ {
+ "url": "https://www.colinodell.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.paypal.me/colinpodell/10.00",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/colinodell",
+ "type": "github"
+ }
+ ],
+ "time": "2022-12-11T20:36:23+00:00"
+ },
+ {
+ "name": "league/flysystem",
+ "version": "3.34.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/flysystem.git",
+ "reference": "2daaac3b0d4c83ea7ed5d8586e786f5d00f3540e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/2daaac3b0d4c83ea7ed5d8586e786f5d00f3540e",
+ "reference": "2daaac3b0d4c83ea7ed5d8586e786f5d00f3540e",
+ "shasum": ""
+ },
+ "require": {
+ "league/flysystem-local": "^3.0.0",
+ "league/mime-type-detection": "^1.0.0",
+ "php": "^8.0.2"
+ },
+ "conflict": {
+ "async-aws/core": "<1.19.0",
+ "async-aws/s3": "<1.14.0",
+ "aws/aws-sdk-php": "3.209.31 || 3.210.0",
+ "guzzlehttp/guzzle": "<7.0",
+ "guzzlehttp/ringphp": "<1.1.1",
+ "phpseclib/phpseclib": "3.0.15",
+ "symfony/http-client": "<5.2"
+ },
+ "require-dev": {
+ "async-aws/s3": "^1.5 || ^2.0",
+ "async-aws/simple-s3": "^1.1 || ^2.0",
+ "aws/aws-sdk-php": "^3.295.10",
+ "composer/semver": "^3.0",
+ "ext-fileinfo": "*",
+ "ext-ftp": "*",
+ "ext-mongodb": "^1.3|^2",
+ "ext-zip": "*",
+ "friendsofphp/php-cs-fixer": "^3.5",
+ "google/cloud-storage": "^1.23",
+ "guzzlehttp/psr7": "^2.6",
+ "microsoft/azure-storage-blob": "^1.1",
+ "mongodb/mongodb": "^1.2|^2",
+ "phpseclib/phpseclib": "^3.0.36",
+ "phpstan/phpstan": "^1.10",
+ "phpunit/phpunit": "^9.5.11|^10.0",
+ "sabre/dav": "^4.6.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "League\\Flysystem\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Frank de Jonge",
+ "email": "info@frankdejonge.nl"
+ }
+ ],
+ "description": "File storage abstraction for PHP",
+ "keywords": [
+ "WebDAV",
+ "aws",
+ "cloud",
+ "file",
+ "files",
+ "filesystem",
+ "filesystems",
+ "ftp",
+ "s3",
+ "sftp",
+ "storage"
+ ],
+ "support": {
+ "issues": "https://github.com/thephpleague/flysystem/issues",
+ "source": "https://github.com/thephpleague/flysystem/tree/3.34.0"
+ },
+ "time": "2026-05-14T10:28:08+00:00"
+ },
+ {
+ "name": "league/flysystem-local",
+ "version": "3.31.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/flysystem-local.git",
+ "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/flysystem-local/zipball/2f669db18a4c20c755c2bb7d3a7b0b2340488079",
+ "reference": "2f669db18a4c20c755c2bb7d3a7b0b2340488079",
+ "shasum": ""
+ },
+ "require": {
+ "ext-fileinfo": "*",
+ "league/flysystem": "^3.0.0",
+ "league/mime-type-detection": "^1.0.0",
+ "php": "^8.0.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "League\\Flysystem\\Local\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Frank de Jonge",
+ "email": "info@frankdejonge.nl"
+ }
+ ],
+ "description": "Local filesystem adapter for Flysystem.",
+ "keywords": [
+ "Flysystem",
+ "file",
+ "files",
+ "filesystem",
+ "local"
+ ],
+ "support": {
+ "source": "https://github.com/thephpleague/flysystem-local/tree/3.31.0"
+ },
+ "time": "2026-01-23T15:30:45+00:00"
+ },
+ {
+ "name": "league/fractal",
+ "version": "0.20.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/fractal.git",
+ "reference": "573ca2e0e348a7fe573a3e8fbc29a6588ece8c4e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/fractal/zipball/573ca2e0e348a7fe573a3e8fbc29a6588ece8c4e",
+ "reference": "573ca2e0e348a7fe573a3e8fbc29a6588ece8c4e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.4"
+ },
+ "require-dev": {
+ "doctrine/orm": "^2.5",
+ "illuminate/contracts": "~5.0",
+ "laminas/laminas-paginator": "~2.12",
+ "mockery/mockery": "^1.3",
+ "pagerfanta/pagerfanta": "~1.0.0|~4.0.0",
+ "phpstan/phpstan": "^1.4",
+ "phpunit/phpunit": "^9.5",
+ "squizlabs/php_codesniffer": "~3.4",
+ "vimeo/psalm": "^4.30"
+ },
+ "suggest": {
+ "illuminate/pagination": "The Illuminate Pagination component.",
+ "laminas/laminas-paginator": "Laminas Framework Paginator",
+ "pagerfanta/pagerfanta": "Pagerfanta Paginator"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "0.20.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "League\\Fractal\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Phil Sturgeon",
+ "email": "me@philsturgeon.uk",
+ "homepage": "http://philsturgeon.uk/",
+ "role": "Developer"
+ }
+ ],
+ "description": "Handle the output of complex data structures ready for API output.",
+ "homepage": "http://fractal.thephpleague.com/",
+ "keywords": [
+ "api",
+ "json",
+ "league",
+ "rest"
+ ],
+ "support": {
+ "issues": "https://github.com/thephpleague/fractal/issues",
+ "source": "https://github.com/thephpleague/fractal/tree/0.20.2"
+ },
+ "time": "2025-02-14T21:33:14+00:00"
+ },
+ {
+ "name": "league/mime-type-detection",
+ "version": "1.16.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/mime-type-detection.git",
+ "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9",
+ "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9",
+ "shasum": ""
+ },
+ "require": {
+ "ext-fileinfo": "*",
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.2",
+ "phpstan/phpstan": "^0.12.68",
+ "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "League\\MimeTypeDetection\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Frank de Jonge",
+ "email": "info@frankdejonge.nl"
+ }
+ ],
+ "description": "Mime-type detection for Flysystem",
+ "support": {
+ "issues": "https://github.com/thephpleague/mime-type-detection/issues",
+ "source": "https://github.com/thephpleague/mime-type-detection/tree/1.16.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/frankdejonge",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/league/flysystem",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-09-21T08:32:55+00:00"
+ },
+ {
+ "name": "league/uri",
+ "version": "7.8.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/uri.git",
+ "reference": "08cf38e3924d4f56238125547b5720496fac8fd4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/uri/zipball/08cf38e3924d4f56238125547b5720496fac8fd4",
+ "reference": "08cf38e3924d4f56238125547b5720496fac8fd4",
+ "shasum": ""
+ },
+ "require": {
+ "league/uri-interfaces": "^7.8.1",
+ "php": "^8.1",
+ "psr/http-factory": "^1"
+ },
+ "conflict": {
+ "league/uri-schemes": "^1.0"
+ },
+ "suggest": {
+ "ext-bcmath": "to improve IPV4 host parsing",
+ "ext-dom": "to convert the URI into an HTML anchor tag",
+ "ext-fileinfo": "to create Data URI from file contennts",
+ "ext-gmp": "to improve IPV4 host parsing",
+ "ext-intl": "to handle IDN host with the best performance",
+ "ext-uri": "to use the PHP native URI class",
+ "jeremykendall/php-domain-parser": "to further parse the URI host and resolve its Public Suffix and Top Level Domain",
+ "league/uri-components": "to provide additional tools to manipulate URI objects components",
+ "league/uri-polyfill": "to backport the PHP URI extension for older versions of PHP",
+ "php-64bit": "to improve IPV4 host parsing",
+ "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification",
+ "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "7.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "League\\Uri\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ignace Nyamagana Butera",
+ "email": "nyamsprod@gmail.com",
+ "homepage": "https://nyamsprod.com"
+ }
+ ],
+ "description": "URI manipulation library",
+ "homepage": "https://uri.thephpleague.com",
+ "keywords": [
+ "URN",
+ "data-uri",
+ "file-uri",
+ "ftp",
+ "hostname",
+ "http",
+ "https",
+ "middleware",
+ "parse_str",
+ "parse_url",
+ "psr-7",
+ "query-string",
+ "querystring",
+ "rfc2141",
+ "rfc3986",
+ "rfc3987",
+ "rfc6570",
+ "rfc8141",
+ "uri",
+ "uri-template",
+ "url",
+ "ws"
+ ],
+ "support": {
+ "docs": "https://uri.thephpleague.com",
+ "forum": "https://thephpleague.slack.com",
+ "issues": "https://github.com/thephpleague/uri-src/issues",
+ "source": "https://github.com/thephpleague/uri/tree/7.8.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sponsors/nyamsprod",
+ "type": "github"
+ }
+ ],
+ "time": "2026-03-15T20:22:25+00:00"
+ },
+ {
+ "name": "league/uri-interfaces",
+ "version": "7.8.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thephpleague/uri-interfaces.git",
+ "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thephpleague/uri-interfaces/zipball/85d5c77c5d6d3af6c54db4a78246364908f3c928",
+ "reference": "85d5c77c5d6d3af6c54db4a78246364908f3c928",
+ "shasum": ""
+ },
+ "require": {
+ "ext-filter": "*",
+ "php": "^8.1",
+ "psr/http-message": "^1.1 || ^2.0"
+ },
+ "suggest": {
+ "ext-bcmath": "to improve IPV4 host parsing",
+ "ext-gmp": "to improve IPV4 host parsing",
+ "ext-intl": "to handle IDN host with the best performance",
+ "php-64bit": "to improve IPV4 host parsing",
+ "rowbot/url": "to handle URLs using the WHATWG URL Living Standard specification",
+ "symfony/polyfill-intl-idn": "to handle IDN host via the Symfony polyfill if ext-intl is not present"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "7.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "League\\Uri\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ignace Nyamagana Butera",
+ "email": "nyamsprod@gmail.com",
+ "homepage": "https://nyamsprod.com"
+ }
+ ],
+ "description": "Common tools for parsing and resolving RFC3987/RFC3986 URI",
+ "homepage": "https://uri.thephpleague.com",
+ "keywords": [
+ "data-uri",
+ "file-uri",
+ "ftp",
+ "hostname",
+ "http",
+ "https",
+ "parse_str",
+ "parse_url",
+ "psr-7",
+ "query-string",
+ "querystring",
+ "rfc3986",
+ "rfc3987",
+ "rfc6570",
+ "uri",
+ "url",
+ "ws"
+ ],
+ "support": {
+ "docs": "https://uri.thephpleague.com",
+ "forum": "https://thephpleague.slack.com",
+ "issues": "https://github.com/thephpleague/uri-src/issues",
+ "source": "https://github.com/thephpleague/uri-interfaces/tree/7.8.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sponsors/nyamsprod",
+ "type": "github"
+ }
+ ],
+ "time": "2026-03-08T20:05:35+00:00"
+ },
+ {
+ "name": "livewire/livewire",
+ "version": "v4.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/livewire/livewire.git",
+ "reference": "6a9dd03f45a4b200abfd0ff644745b23fa7baaaa"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/livewire/livewire/zipball/6a9dd03f45a4b200abfd0ff644745b23fa7baaaa",
+ "reference": "6a9dd03f45a4b200abfd0ff644745b23fa7baaaa",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/database": "^10.0|^11.0|^12.0|^13.0",
+ "illuminate/routing": "^10.0|^11.0|^12.0|^13.0",
+ "illuminate/support": "^10.0|^11.0|^12.0|^13.0",
+ "illuminate/validation": "^10.0|^11.0|^12.0|^13.0",
+ "laravel/prompts": "^0.1.24|^0.2|^0.3",
+ "league/mime-type-detection": "^1.9",
+ "php": "^8.1",
+ "symfony/console": "^6.0|^7.0|^8.0",
+ "symfony/http-kernel": "^6.2|^7.0|^8.0"
+ },
+ "require-dev": {
+ "calebporzio/sushi": "^2.1",
+ "laravel/framework": "^10.15.0|^11.0|^12.0|^13.0",
+ "mockery/mockery": "^1.3.1",
+ "orchestra/testbench": "^8.21.0|^9.0|^10.0|^11.0",
+ "orchestra/testbench-dusk": "^8.24|^9.1|^10.0|^11.0",
+ "phpunit/phpunit": "^10.4|^11.5|^12.5",
+ "psy/psysh": "^0.11.22|^0.12"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "aliases": {
+ "Livewire": "Livewire\\Livewire"
+ },
+ "providers": [
+ "Livewire\\LivewireServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/helpers.php"
+ ],
+ "psr-4": {
+ "Livewire\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Caleb Porzio",
+ "email": "calebporzio@gmail.com"
+ }
+ ],
+ "description": "A front-end framework for Laravel.",
+ "support": {
+ "issues": "https://github.com/livewire/livewire/issues",
+ "source": "https://github.com/livewire/livewire/tree/v4.3.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/livewire",
+ "type": "github"
+ }
+ ],
+ "time": "2026-06-02T08:58:52+00:00"
+ },
+ {
+ "name": "maatwebsite/excel",
+ "version": "4.x-dev",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/SpartnerNL/Laravel-Excel.git",
+ "reference": "a31cc13f5ef07fda91893ad3820d1e54d09cb99d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/SpartnerNL/Laravel-Excel/zipball/a31cc13f5ef07fda91893ad3820d1e54d09cb99d",
+ "reference": "a31cc13f5ef07fda91893ad3820d1e54d09cb99d",
+ "shasum": ""
+ },
+ "require": {
+ "composer/semver": "^3.3",
+ "illuminate/support": "^12.0 || ^13.0",
+ "php": "^8.3",
+ "phpoffice/phpspreadsheet": "^5.3",
+ "psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
+ },
+ "require-dev": {
+ "brianium/paratest": "^7.19 || ^8.0",
+ "driftingly/rector-laravel": "^2.3",
+ "ext-sqlite3": "*",
+ "larastan/larastan": "^3.9",
+ "laravel/pint": "^1.0",
+ "laravel/scout": "^10.0 || ^11.0",
+ "orchestra/testbench": "^10.0 || ^11.0",
+ "phpstan/extension-installer": "^1.4",
+ "phpstan/phpstan": "^2.1.55",
+ "phpstan/phpstan-mockery": "^2.0",
+ "phpunit/phpunit": "^12.5.3 || ^13.0.0",
+ "predis/predis": "^1.1",
+ "rector/rector": "^2.4.2"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "aliases": {
+ "Excel": "Maatwebsite\\Excel\\Facades\\Excel"
+ },
+ "providers": [
+ "Maatwebsite\\Excel\\ExcelServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Maatwebsite\\Excel\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Patrick Brouwers",
+ "email": "patrick@spartner.nl"
+ }
+ ],
+ "description": "Supercharged Excel exports and imports in Laravel",
+ "keywords": [
+ "PHPExcel",
+ "batch",
+ "csv",
+ "excel",
+ "export",
+ "import",
+ "laravel",
+ "php",
+ "phpspreadsheet"
+ ],
+ "support": {
+ "issues": "https://github.com/SpartnerNL/Laravel-Excel/issues",
+ "source": "https://github.com/SpartnerNL/Laravel-Excel/tree/4.x"
+ },
+ "funding": [
+ {
+ "url": "https://laravel-excel.com/commercial-support",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/patrickbrouwers",
+ "type": "github"
+ }
+ ],
+ "time": "2026-06-01T17:23:34+00:00"
+ },
+ {
+ "name": "maennchen/zipstream-php",
+ "version": "3.2.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/maennchen/ZipStream-PHP.git",
+ "reference": "77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e",
+ "reference": "77bebeb4c6c340bb3c11c843b2cffd8bbfde4d5e",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "ext-zlib": "*",
+ "php-64bit": "^8.3"
+ },
+ "require-dev": {
+ "brianium/paratest": "^7.7",
+ "ext-zip": "*",
+ "friendsofphp/php-cs-fixer": "^3.86",
+ "guzzlehttp/guzzle": "^7.5",
+ "mikey179/vfsstream": "^1.6",
+ "php-coveralls/php-coveralls": "^2.5",
+ "phpunit/phpunit": "^12.0",
+ "vimeo/psalm": "^6.0"
+ },
+ "suggest": {
+ "guzzlehttp/psr7": "^2.4",
+ "psr/http-message": "^2.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "ZipStream\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Paul Duncan",
+ "email": "pabs@pablotron.org"
+ },
+ {
+ "name": "Jonatan Männchen",
+ "email": "jonatan@maennchen.ch"
+ },
+ {
+ "name": "Jesse Donat",
+ "email": "donatj@gmail.com"
+ },
+ {
+ "name": "András Kolesár",
+ "email": "kolesar@kolesar.hu"
+ }
+ ],
+ "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.",
+ "keywords": [
+ "stream",
+ "zip"
+ ],
+ "support": {
+ "issues": "https://github.com/maennchen/ZipStream-PHP/issues",
+ "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/maennchen",
+ "type": "github"
+ }
+ ],
+ "time": "2026-04-11T18:38:28+00:00"
+ },
+ {
+ "name": "markbaker/complex",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/MarkBaker/PHPComplex.git",
+ "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/MarkBaker/PHPComplex/zipball/95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
+ "reference": "95c56caa1cf5c766ad6d65b6344b807c1e8405b9",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "dev-master",
+ "phpcompatibility/php-compatibility": "^9.3",
+ "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
+ "squizlabs/php_codesniffer": "^3.7"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Complex\\": "classes/src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mark Baker",
+ "email": "mark@lange.demon.co.uk"
+ }
+ ],
+ "description": "PHP Class for working with complex numbers",
+ "homepage": "https://github.com/MarkBaker/PHPComplex",
+ "keywords": [
+ "complex",
+ "mathematics"
+ ],
+ "support": {
+ "issues": "https://github.com/MarkBaker/PHPComplex/issues",
+ "source": "https://github.com/MarkBaker/PHPComplex/tree/3.0.2"
+ },
+ "time": "2022-12-06T16:21:08+00:00"
+ },
+ {
+ "name": "markbaker/matrix",
+ "version": "3.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/MarkBaker/PHPMatrix.git",
+ "reference": "728434227fe21be27ff6d86621a1b13107a2562c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/MarkBaker/PHPMatrix/zipball/728434227fe21be27ff6d86621a1b13107a2562c",
+ "reference": "728434227fe21be27ff6d86621a1b13107a2562c",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "dev-master",
+ "phpcompatibility/php-compatibility": "^9.3",
+ "phpdocumentor/phpdocumentor": "2.*",
+ "phploc/phploc": "^4.0",
+ "phpmd/phpmd": "2.*",
+ "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0",
+ "sebastian/phpcpd": "^4.0",
+ "squizlabs/php_codesniffer": "^3.7"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Matrix\\": "classes/src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mark Baker",
+ "email": "mark@demon-angel.eu"
+ }
+ ],
+ "description": "PHP Class for working with matrices",
+ "homepage": "https://github.com/MarkBaker/PHPMatrix",
+ "keywords": [
+ "mathematics",
+ "matrix",
+ "vector"
+ ],
+ "support": {
+ "issues": "https://github.com/MarkBaker/PHPMatrix/issues",
+ "source": "https://github.com/MarkBaker/PHPMatrix/tree/3.0.1"
+ },
+ "time": "2022-12-02T22:17:43+00:00"
+ },
+ {
+ "name": "masterminds/html5",
+ "version": "2.10.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Masterminds/html5-php.git",
+ "reference": "fcf91eb64359852f00d921887b219479b4f21251"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Masterminds/html5-php/zipball/fcf91eb64359852f00d921887b219479b4f21251",
+ "reference": "fcf91eb64359852f00d921887b219479b4f21251",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "php": ">=5.3.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^4.8.35 || ^5.7.21 || ^6 || ^7 || ^8 || ^9"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.7-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Masterminds\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Matt Butcher",
+ "email": "technosophos@gmail.com"
+ },
+ {
+ "name": "Matt Farina",
+ "email": "matt@mattfarina.com"
+ },
+ {
+ "name": "Asmir Mustafic",
+ "email": "goetas@gmail.com"
+ }
+ ],
+ "description": "An HTML5 parser and serializer.",
+ "homepage": "http://masterminds.github.io/html5-php",
+ "keywords": [
+ "HTML5",
+ "dom",
+ "html",
+ "parser",
+ "querypath",
+ "serializer",
+ "xml"
+ ],
+ "support": {
+ "issues": "https://github.com/Masterminds/html5-php/issues",
+ "source": "https://github.com/Masterminds/html5-php/tree/2.10.0"
+ },
+ "time": "2025-07-25T09:04:22+00:00"
+ },
+ {
+ "name": "monolog/monolog",
+ "version": "3.10.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Seldaek/monolog.git",
+ "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0",
+ "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/log": "^2.0 || ^3.0"
+ },
+ "provide": {
+ "psr/log-implementation": "3.0.0"
+ },
+ "require-dev": {
+ "aws/aws-sdk-php": "^3.0",
+ "doctrine/couchdb": "~1.0@dev",
+ "elasticsearch/elasticsearch": "^7 || ^8",
+ "ext-json": "*",
+ "graylog2/gelf-php": "^1.4.2 || ^2.0",
+ "guzzlehttp/guzzle": "^7.4.5",
+ "guzzlehttp/psr7": "^2.2",
+ "mongodb/mongodb": "^1.8 || ^2.0",
+ "php-amqplib/php-amqplib": "~2.4 || ^3",
+ "php-console/php-console": "^3.1.8",
+ "phpstan/phpstan": "^2",
+ "phpstan/phpstan-deprecation-rules": "^2",
+ "phpstan/phpstan-strict-rules": "^2",
+ "phpunit/phpunit": "^10.5.17 || ^11.0.7",
+ "predis/predis": "^1.1 || ^2",
+ "rollbar/rollbar": "^4.0",
+ "ruflin/elastica": "^7 || ^8",
+ "symfony/mailer": "^5.4 || ^6",
+ "symfony/mime": "^5.4 || ^6"
+ },
+ "suggest": {
+ "aws/aws-sdk-php": "Allow sending log messages to AWS services like DynamoDB",
+ "doctrine/couchdb": "Allow sending log messages to a CouchDB server",
+ "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client",
+ "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)",
+ "ext-curl": "Required to send log messages using the IFTTTHandler, the LogglyHandler, the SendGridHandler, the SlackWebhookHandler or the TelegramBotHandler",
+ "ext-mbstring": "Allow to work properly with unicode symbols",
+ "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)",
+ "ext-openssl": "Required to send log messages using SSL",
+ "ext-sockets": "Allow sending log messages to a Syslog server (via UDP driver)",
+ "graylog2/gelf-php": "Allow sending log messages to a GrayLog2 server",
+ "mongodb/mongodb": "Allow sending log messages to a MongoDB server (via library)",
+ "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib",
+ "rollbar/rollbar": "Allow sending log messages to Rollbar",
+ "ruflin/elastica": "Allow sending log messages to an Elastic Search server"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Monolog\\": "src/Monolog"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "https://seld.be"
+ }
+ ],
+ "description": "Sends your logs to files, sockets, inboxes, databases and various web services",
+ "homepage": "https://github.com/Seldaek/monolog",
+ "keywords": [
+ "log",
+ "logging",
+ "psr-3"
+ ],
+ "support": {
+ "issues": "https://github.com/Seldaek/monolog/issues",
+ "source": "https://github.com/Seldaek/monolog/tree/3.10.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Seldaek",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/monolog/monolog",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-02T08:56:05+00:00"
+ },
+ {
+ "name": "mtdowling/jmespath.php",
+ "version": "2.8.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/jmespath/jmespath.php.git",
+ "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/a2a865e05d5f420b50cc2f85bb78d565db12a6bc",
+ "reference": "a2a865e05d5f420b50cc2f85bb78d565db12a6bc",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0",
+ "symfony/polyfill-mbstring": "^1.17"
+ },
+ "require-dev": {
+ "composer/xdebug-handler": "^3.0.3",
+ "phpunit/phpunit": "^8.5.33"
+ },
+ "bin": [
+ "bin/jp.php"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.8-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/JmesPath.php"
+ ],
+ "psr-4": {
+ "JmesPath\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Michael Dowling",
+ "email": "mtdowling@gmail.com",
+ "homepage": "https://github.com/mtdowling"
+ }
+ ],
+ "description": "Declaratively specify how to extract elements from a JSON document",
+ "keywords": [
+ "json",
+ "jsonpath"
+ ],
+ "support": {
+ "issues": "https://github.com/jmespath/jmespath.php/issues",
+ "source": "https://github.com/jmespath/jmespath.php/tree/2.8.0"
+ },
+ "time": "2024-09-04T18:46:31+00:00"
+ },
+ {
+ "name": "nesbot/carbon",
+ "version": "3.11.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/CarbonPHP/carbon.git",
+ "reference": "e890471a3494740f7d9326d72ce6a8c559ffee60"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/e890471a3494740f7d9326d72ce6a8c559ffee60",
+ "reference": "e890471a3494740f7d9326d72ce6a8c559ffee60",
+ "shasum": ""
+ },
+ "require": {
+ "carbonphp/carbon-doctrine-types": "<100.0",
+ "ext-json": "*",
+ "php": "^8.1",
+ "psr/clock": "^1.0",
+ "symfony/clock": "^6.3.12 || ^7.0 || ^8.0",
+ "symfony/polyfill-mbstring": "^1.0",
+ "symfony/translation": "^4.4.18 || ^5.2.1 || ^6.0 || ^7.0 || ^8.0"
+ },
+ "provide": {
+ "psr/clock-implementation": "1.0"
+ },
+ "require-dev": {
+ "doctrine/dbal": "^3.6.3 || ^4.0",
+ "doctrine/orm": "^2.15.2 || ^3.0",
+ "friendsofphp/php-cs-fixer": "^v3.87.1",
+ "kylekatarnls/multi-tester": "^2.5.3",
+ "phpmd/phpmd": "^2.15.0",
+ "phpstan/extension-installer": "^1.4.3",
+ "phpstan/phpstan": "^2.1.22",
+ "phpunit/phpunit": "^10.5.53",
+ "squizlabs/php_codesniffer": "^3.13.4 || ^4.0.0"
+ },
+ "bin": [
+ "bin/carbon"
+ ],
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Carbon\\Laravel\\ServiceProvider"
+ ]
+ },
+ "phpstan": {
+ "includes": [
+ "extension.neon"
+ ]
+ },
+ "branch-alias": {
+ "dev-2.x": "2.x-dev",
+ "dev-master": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Carbon\\": "src/Carbon/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Brian Nesbitt",
+ "email": "brian@nesbot.com",
+ "homepage": "https://markido.com"
+ },
+ {
+ "name": "kylekatarnls",
+ "homepage": "https://github.com/kylekatarnls"
+ }
+ ],
+ "description": "An API extension for DateTime that supports 281 different languages.",
+ "homepage": "https://carbonphp.github.io/carbon/",
+ "keywords": [
+ "date",
+ "datetime",
+ "time"
+ ],
+ "support": {
+ "docs": "https://carbonphp.github.io/carbon/guide/getting-started/introduction.html",
+ "issues": "https://github.com/CarbonPHP/carbon/issues",
+ "source": "https://github.com/CarbonPHP/carbon"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sponsors/kylekatarnls",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/Carbon#sponsor",
+ "type": "opencollective"
+ },
+ {
+ "url": "https://tidelift.com/subscription/pkg/packagist-nesbot-carbon?utm_source=packagist-nesbot-carbon&utm_medium=referral&utm_campaign=readme",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-04-07T09:57:54+00:00"
+ },
+ {
+ "name": "nette/schema",
+ "version": "v1.3.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nette/schema.git",
+ "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nette/schema/zipball/f0ab1a3cda782dbc5da270d28545236aa80c4002",
+ "reference": "f0ab1a3cda782dbc5da270d28545236aa80c4002",
+ "shasum": ""
+ },
+ "require": {
+ "nette/utils": "^4.0",
+ "php": "8.1 - 8.5"
+ },
+ "require-dev": {
+ "nette/phpstan-rules": "^1.0",
+ "nette/tester": "^2.6",
+ "phpstan/extension-installer": "^1.4@stable",
+ "phpstan/phpstan": "^2.1.39@stable",
+ "tracy/tracy": "^2.8"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.3-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Nette\\": "src"
+ },
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause",
+ "GPL-2.0-only",
+ "GPL-3.0-only"
+ ],
+ "authors": [
+ {
+ "name": "David Grudl",
+ "homepage": "https://davidgrudl.com"
+ },
+ {
+ "name": "Nette Community",
+ "homepage": "https://nette.org/contributors"
+ }
+ ],
+ "description": "📐 Nette Schema: validating data structures against a given Schema.",
+ "homepage": "https://nette.org",
+ "keywords": [
+ "config",
+ "nette"
+ ],
+ "support": {
+ "issues": "https://github.com/nette/schema/issues",
+ "source": "https://github.com/nette/schema/tree/v1.3.5"
+ },
+ "time": "2026-02-23T03:47:12+00:00"
+ },
+ {
+ "name": "nette/utils",
+ "version": "v4.1.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nette/utils.git",
+ "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nette/utils/zipball/7da6c396d7ebe142bc857c20479d5e70a5e1aac7",
+ "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7",
+ "shasum": ""
+ },
+ "require": {
+ "php": "8.2 - 8.5"
+ },
+ "conflict": {
+ "nette/finder": "<3",
+ "nette/schema": "<1.2.2"
+ },
+ "require-dev": {
+ "jetbrains/phpstorm-attributes": "^1.2",
+ "nette/phpstan-rules": "^1.0",
+ "nette/tester": "^2.5",
+ "phpstan/extension-installer": "^1.4@stable",
+ "phpstan/phpstan": "^2.1@stable",
+ "tracy/tracy": "^2.9"
+ },
+ "suggest": {
+ "ext-gd": "to use Image",
+ "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()",
+ "ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()",
+ "ext-json": "to use Nette\\Utils\\Json",
+ "ext-mbstring": "to use Strings::lower() etc...",
+ "ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "4.1-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Nette\\": "src"
+ },
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause",
+ "GPL-2.0-only",
+ "GPL-3.0-only"
+ ],
+ "authors": [
+ {
+ "name": "David Grudl",
+ "homepage": "https://davidgrudl.com"
+ },
+ {
+ "name": "Nette Community",
+ "homepage": "https://nette.org/contributors"
+ }
+ ],
+ "description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.",
+ "homepage": "https://nette.org",
+ "keywords": [
+ "array",
+ "core",
+ "datetime",
+ "images",
+ "json",
+ "nette",
+ "paginator",
+ "password",
+ "slugify",
+ "string",
+ "unicode",
+ "utf-8",
+ "utility",
+ "validation"
+ ],
+ "support": {
+ "issues": "https://github.com/nette/utils/issues",
+ "source": "https://github.com/nette/utils/tree/v4.1.4"
+ },
+ "time": "2026-05-11T20:49:54+00:00"
+ },
+ {
+ "name": "nikic/php-parser",
+ "version": "v5.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nikic/PHP-Parser.git",
+ "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82",
+ "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82",
+ "shasum": ""
+ },
+ "require": {
+ "ext-ctype": "*",
+ "ext-json": "*",
+ "ext-tokenizer": "*",
+ "php": ">=7.4"
+ },
+ "require-dev": {
+ "ircmaxell/php-yacc": "^0.0.7",
+ "phpunit/phpunit": "^9.0"
+ },
+ "bin": [
+ "bin/php-parse"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PhpParser\\": "lib/PhpParser"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Nikita Popov"
+ }
+ ],
+ "description": "A PHP parser written in PHP",
+ "keywords": [
+ "parser",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/nikic/PHP-Parser/issues",
+ "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0"
+ },
+ "time": "2025-12-06T11:56:16+00:00"
+ },
+ {
+ "name": "nunomaduro/termwind",
+ "version": "v2.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nunomaduro/termwind.git",
+ "reference": "712a31b768f5daea284c2169a7d227031001b9a8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/712a31b768f5daea284c2169a7d227031001b9a8",
+ "reference": "712a31b768f5daea284c2169a7d227031001b9a8",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": "^8.2",
+ "symfony/console": "^7.4.4 || ^8.0.4"
+ },
+ "require-dev": {
+ "illuminate/console": "^11.47.0",
+ "laravel/pint": "^1.27.1",
+ "mockery/mockery": "^1.6.12",
+ "pestphp/pest": "^2.36.0 || ^3.8.4 || ^4.3.2",
+ "phpstan/phpstan": "^1.12.32",
+ "phpstan/phpstan-strict-rules": "^1.6.2",
+ "symfony/var-dumper": "^7.3.5 || ^8.0.4",
+ "thecodingmachine/phpstan-strict-rules": "^1.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Termwind\\Laravel\\TermwindServiceProvider"
+ ]
+ },
+ "branch-alias": {
+ "dev-2.x": "2.x-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/Functions.php"
+ ],
+ "psr-4": {
+ "Termwind\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nuno Maduro",
+ "email": "enunomaduro@gmail.com"
+ }
+ ],
+ "description": "It's like Tailwind CSS, but for the console.",
+ "keywords": [
+ "cli",
+ "console",
+ "css",
+ "package",
+ "php",
+ "style"
+ ],
+ "support": {
+ "issues": "https://github.com/nunomaduro/termwind/issues",
+ "source": "https://github.com/nunomaduro/termwind/tree/v2.4.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.paypal.com/paypalme/enunomaduro",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/nunomaduro",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/xiCO2k",
+ "type": "github"
+ }
+ ],
+ "time": "2026-02-16T23:10:27+00:00"
+ },
+ {
+ "name": "openspout/openspout",
+ "version": "v5.7.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/openspout/openspout.git",
+ "reference": "f383ae8ab4c735b6a6a0cef396e9799900584f3e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/openspout/openspout/zipball/f383ae8ab4c735b6a6a0cef396e9799900584f3e",
+ "reference": "f383ae8ab4c735b6a6a0cef396e9799900584f3e",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-filter": "*",
+ "ext-libxml": "*",
+ "ext-xmlreader": "*",
+ "ext-zip": "*",
+ "php": "~8.4.0 || ~8.5.0"
+ },
+ "require-dev": {
+ "ext-fileinfo": "*",
+ "ext-zlib": "*",
+ "friendsofphp/php-cs-fixer": "^3.95.2",
+ "infection/infection": "^0.33.2",
+ "phpbench/phpbench": "^1.6.1",
+ "phpstan/phpstan": "^2.2.1",
+ "phpstan/phpstan-phpunit": "^2.0.16",
+ "phpstan/phpstan-strict-rules": "^2.0.11",
+ "phpunit/phpunit": "^13.1.13"
+ },
+ "suggest": {
+ "ext-iconv": "To handle non UTF-8 CSV files (if \"php-mbstring\" is not already installed or is too limited)",
+ "ext-mbstring": "To handle non UTF-8 CSV files (if \"iconv\" is not already installed)"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "OpenSpout\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Adrien Loison",
+ "email": "adrien@box.com"
+ }
+ ],
+ "description": "PHP Library to read and write spreadsheet files (CSV, XLSX and ODS), in a fast and scalable way",
+ "homepage": "https://github.com/openspout/openspout",
+ "keywords": [
+ "OOXML",
+ "csv",
+ "excel",
+ "memory",
+ "odf",
+ "ods",
+ "office",
+ "open",
+ "php",
+ "read",
+ "scale",
+ "spreadsheet",
+ "stream",
+ "write",
+ "xlsx"
+ ],
+ "support": {
+ "issues": "https://github.com/openspout/openspout/issues",
+ "source": "https://github.com/openspout/openspout/tree/v5.7.2"
+ },
+ "funding": [
+ {
+ "url": "https://paypal.me/filippotessarotto",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/Slamdunk",
+ "type": "github"
+ }
+ ],
+ "time": "2026-05-29T11:43:33+00:00"
+ },
+ {
+ "name": "owen-it/laravel-auditing",
+ "version": "v14.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/owen-it/laravel-auditing.git",
+ "reference": "34e8a21890082a7a353894a4acdeb2d301dbe0d4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/owen-it/laravel-auditing/zipball/34e8a21890082a7a353894a4acdeb2d301dbe0d4",
+ "reference": "34e8a21890082a7a353894a4acdeb2d301dbe0d4",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "illuminate/console": "^11.0|^12.0|^13.0",
+ "illuminate/database": "^11.0|^12.0|^13.0",
+ "illuminate/filesystem": "^11.0|^12.0|^13.0",
+ "php": "^8.2"
+ },
+ "require-dev": {
+ "mockery/mockery": "^1.5.1",
+ "orchestra/testbench": "^9.0|^10.0|^11.0",
+ "phpunit/phpunit": "^11.0|^12.5.12"
+ },
+ "type": "package",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "OwenIt\\Auditing\\AuditingServiceProvider"
+ ]
+ },
+ "branch-alias": {
+ "dev-master": "v14-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "OwenIt\\Auditing\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Antério Vieira",
+ "email": "anteriovieira@gmail.com"
+ },
+ {
+ "name": "Raphael França",
+ "email": "raphaelfrancabsb@gmail.com"
+ },
+ {
+ "name": "Morten D. Hansen",
+ "email": "morten@visia.dk"
+ }
+ ],
+ "description": "Audit changes of your Eloquent models in Laravel",
+ "homepage": "https://laravel-auditing.com",
+ "keywords": [
+ "Accountability",
+ "Audit",
+ "auditing",
+ "changes",
+ "eloquent",
+ "history",
+ "laravel",
+ "log",
+ "logging",
+ "lumen",
+ "observer",
+ "record",
+ "revision",
+ "tracking"
+ ],
+ "support": {
+ "issues": "https://github.com/owen-it/laravel-auditing/issues",
+ "source": "https://github.com/owen-it/laravel-auditing"
+ },
+ "time": "2026-03-27T13:27:17+00:00"
+ },
+ {
+ "name": "phpoffice/phpspreadsheet",
+ "version": "5.8.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
+ "reference": "01964d92536edf1a3a874b9580a52824bebf6fbb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/01964d92536edf1a3a874b9580a52824bebf6fbb",
+ "reference": "01964d92536edf1a3a874b9580a52824bebf6fbb",
+ "shasum": ""
+ },
+ "require": {
+ "composer/pcre": "^1||^2||^3",
+ "ext-ctype": "*",
+ "ext-dom": "*",
+ "ext-fileinfo": "*",
+ "ext-filter": "*",
+ "ext-gd": "*",
+ "ext-iconv": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-simplexml": "*",
+ "ext-xml": "*",
+ "ext-xmlreader": "*",
+ "ext-xmlwriter": "*",
+ "ext-zip": "*",
+ "ext-zlib": "*",
+ "maennchen/zipstream-php": "^2.1 || ^3.0",
+ "markbaker/complex": "^3.0",
+ "markbaker/matrix": "^3.0",
+ "php": "^8.1",
+ "psr/simple-cache": "^1.0 || ^2.0 || ^3.0"
+ },
+ "require-dev": {
+ "dealerdirect/phpcodesniffer-composer-installer": "dev-main",
+ "dompdf/dompdf": "^2.0 || ^3.0",
+ "ext-intl": "*",
+ "friendsofphp/php-cs-fixer": "^3.2",
+ "mitoteam/jpgraph": "^10.5",
+ "mpdf/mpdf": "^8.1.1",
+ "phpcompatibility/php-compatibility": "^9.3",
+ "phpstan/phpstan": "^1.1 || ^2.0",
+ "phpstan/phpstan-deprecation-rules": "^1.0 || ^2.0",
+ "phpstan/phpstan-phpunit": "^1.0 || ^2.0",
+ "phpunit/phpunit": "^10.5",
+ "squizlabs/php_codesniffer": "^3.7",
+ "tecnickcom/tcpdf": "^6.5"
+ },
+ "suggest": {
+ "dompdf/dompdf": "Option for rendering PDF with PDF Writer",
+ "ext-intl": "PHP Internationalization Functions, required for NumberFormat Wizard and StringHelper::setLocale()",
+ "mitoteam/jpgraph": "Option for rendering charts, or including charts with PDF or HTML Writers",
+ "mpdf/mpdf": "Option for rendering PDF with PDF Writer",
+ "tecnickcom/tcpdf": "Option for rendering PDF with PDF Writer"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "PhpOffice\\PhpSpreadsheet\\": "src/PhpSpreadsheet"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Maarten Balliauw",
+ "homepage": "https://blog.maartenballiauw.be"
+ },
+ {
+ "name": "Mark Baker",
+ "homepage": "https://markbakeruk.net"
+ },
+ {
+ "name": "Franck Lefevre",
+ "homepage": "https://rootslabs.net"
+ },
+ {
+ "name": "Erik Tilt"
+ },
+ {
+ "name": "Adrien Crivelli"
+ },
+ {
+ "name": "Owen Leibman"
+ }
+ ],
+ "description": "PHPSpreadsheet - Read, Create and Write Spreadsheet documents in PHP - Spreadsheet engine",
+ "homepage": "https://github.com/PHPOffice/PhpSpreadsheet",
+ "keywords": [
+ "OpenXML",
+ "excel",
+ "gnumeric",
+ "ods",
+ "php",
+ "spreadsheet",
+ "xls",
+ "xlsx"
+ ],
+ "support": {
+ "issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
+ "source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/5.8.0"
+ },
+ "time": "2026-06-07T03:51:10+00:00"
+ },
+ {
+ "name": "phpoption/phpoption",
+ "version": "1.9.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/schmittjoh/php-option.git",
+ "reference": "75365b91986c2405cf5e1e012c5595cd487a98be"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/schmittjoh/php-option/zipball/75365b91986c2405cf5e1e012c5595cd487a98be",
+ "reference": "75365b91986c2405cf5e1e012c5595cd487a98be",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2.5 || ^8.0"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "phpunit/phpunit": "^8.5.44 || ^9.6.25 || ^10.5.53 || ^11.5.34"
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ },
+ "branch-alias": {
+ "dev-master": "1.9-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "PhpOption\\": "src/PhpOption/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "Apache-2.0"
+ ],
+ "authors": [
+ {
+ "name": "Johannes M. Schmitt",
+ "email": "schmittjoh@gmail.com",
+ "homepage": "https://github.com/schmittjoh"
+ },
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ }
+ ],
+ "description": "Option Type for PHP",
+ "keywords": [
+ "language",
+ "option",
+ "php",
+ "type"
+ ],
+ "support": {
+ "issues": "https://github.com/schmittjoh/php-option/issues",
+ "source": "https://github.com/schmittjoh/php-option/tree/1.9.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-12-27T19:41:33+00:00"
+ },
+ {
+ "name": "psr/clock",
+ "version": "1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/clock.git",
+ "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/clock/zipball/e41a24703d4560fd0acb709162f73b8adfc3aa0d",
+ "reference": "e41a24703d4560fd0acb709162f73b8adfc3aa0d",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Psr\\Clock\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for reading the clock.",
+ "homepage": "https://github.com/php-fig/clock",
+ "keywords": [
+ "clock",
+ "now",
+ "psr",
+ "psr-20",
+ "time"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/clock/issues",
+ "source": "https://github.com/php-fig/clock/tree/1.0.0"
+ },
+ "time": "2022-11-25T14:36:26+00:00"
+ },
+ {
+ "name": "psr/container",
+ "version": "2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/container.git",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.4.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Container\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common Container Interface (PHP FIG PSR-11)",
+ "homepage": "https://github.com/php-fig/container",
+ "keywords": [
+ "PSR-11",
+ "container",
+ "container-interface",
+ "container-interop",
+ "psr"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/container/issues",
+ "source": "https://github.com/php-fig/container/tree/2.0.2"
+ },
+ "time": "2021-11-05T16:47:00+00:00"
+ },
+ {
+ "name": "psr/event-dispatcher",
+ "version": "1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/event-dispatcher.git",
+ "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0",
+ "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\EventDispatcher\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Standard interfaces for event handling.",
+ "keywords": [
+ "events",
+ "psr",
+ "psr-14"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/event-dispatcher/issues",
+ "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0"
+ },
+ "time": "2019-01-08T18:20:26+00:00"
+ },
+ {
+ "name": "psr/http-client",
+ "version": "1.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-client.git",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-client/zipball/bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "reference": "bb5906edc1c324c9a05aa0873d40117941e5fa90",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.0 || ^8.0",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Client\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP clients",
+ "homepage": "https://github.com/php-fig/http-client",
+ "keywords": [
+ "http",
+ "http-client",
+ "psr",
+ "psr-18"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-client"
+ },
+ "time": "2023-09-23T14:17:50+00:00"
+ },
+ {
+ "name": "psr/http-factory",
+ "version": "1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-factory.git",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-factory/zipball/2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "reference": "2b4765fddfe3b508ac62f829e852b1501d3f6e8a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1",
+ "psr/http-message": "^1.0 || ^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "PSR-17: Common interfaces for PSR-7 HTTP message factories",
+ "keywords": [
+ "factory",
+ "http",
+ "message",
+ "psr",
+ "psr-17",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-factory"
+ },
+ "time": "2024-04-15T12:06:14+00:00"
+ },
+ {
+ "name": "psr/http-message",
+ "version": "2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/http-message.git",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/http-message/zipball/402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "reference": "402d35bcb92c70c026d1a6a9883f06b2ead23d71",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Http\\Message\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for HTTP messages",
+ "homepage": "https://github.com/php-fig/http-message",
+ "keywords": [
+ "http",
+ "http-message",
+ "psr",
+ "psr-7",
+ "request",
+ "response"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/http-message/tree/2.0"
+ },
+ "time": "2023-04-04T09:54:51+00:00"
+ },
+ {
+ "name": "psr/log",
+ "version": "3.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/log.git",
+ "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
+ "reference": "f16e1d5863e37f8d8c2a01719f5b34baa2b714d3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Log\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for logging libraries",
+ "homepage": "https://github.com/php-fig/log",
+ "keywords": [
+ "log",
+ "psr",
+ "psr-3"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/log/tree/3.0.2"
+ },
+ "time": "2024-09-11T13:17:53+00:00"
+ },
+ {
+ "name": "psr/simple-cache",
+ "version": "3.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/simple-cache.git",
+ "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865",
+ "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\SimpleCache\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interfaces for simple caching",
+ "keywords": [
+ "cache",
+ "caching",
+ "psr",
+ "psr-16",
+ "simple-cache"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/simple-cache/tree/3.0.0"
+ },
+ "time": "2021-10-29T13:26:27+00:00"
+ },
+ {
+ "name": "psy/psysh",
+ "version": "v0.12.23",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/bobthecow/psysh.git",
+ "reference": "4dcc0f08047d52bbde475eda481146fd8e27e1a4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/bobthecow/psysh/zipball/4dcc0f08047d52bbde475eda481146fd8e27e1a4",
+ "reference": "4dcc0f08047d52bbde475eda481146fd8e27e1a4",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "ext-tokenizer": "*",
+ "nikic/php-parser": "^5.0 || ^4.0",
+ "php": "^8.0 || ^7.4",
+ "symfony/console": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4",
+ "symfony/var-dumper": "^8.0 || ^7.0 || ^6.0 || ^5.0 || ^4.0 || ^3.4"
+ },
+ "conflict": {
+ "symfony/console": "4.4.37 || 5.3.14 || 5.3.15 || 5.4.3 || 5.4.4 || 6.0.3 || 6.0.4"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.2",
+ "composer/class-map-generator": "^1.6"
+ },
+ "suggest": {
+ "composer/class-map-generator": "Improved tab completion performance with better class discovery.",
+ "ext-pcntl": "Enabling the PCNTL extension makes PsySH a lot happier :)",
+ "ext-posix": "If you have PCNTL, you'll want the POSIX extension as well."
+ },
+ "bin": [
+ "bin/psysh"
+ ],
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": false,
+ "forward-command": false
+ },
+ "branch-alias": {
+ "dev-main": "0.12.x-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/functions.php"
+ ],
+ "psr-4": {
+ "Psy\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Justin Hileman",
+ "email": "justin@justinhileman.info"
+ }
+ ],
+ "description": "An interactive shell for modern PHP.",
+ "homepage": "https://psysh.org",
+ "keywords": [
+ "REPL",
+ "console",
+ "interactive",
+ "shell"
+ ],
+ "support": {
+ "issues": "https://github.com/bobthecow/psysh/issues",
+ "source": "https://github.com/bobthecow/psysh/tree/v0.12.23"
+ },
+ "time": "2026-05-23T13:41:31+00:00"
+ },
+ {
+ "name": "ralouphie/getallheaders",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ralouphie/getallheaders.git",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ralouphie/getallheaders/zipball/120b605dfeb996808c31b6477290a714d356e822",
+ "reference": "120b605dfeb996808c31b6477290a714d356e822",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=5.6"
+ },
+ "require-dev": {
+ "php-coveralls/php-coveralls": "^2.1",
+ "phpunit/phpunit": "^5 || ^6.5"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/getallheaders.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ralph Khattar",
+ "email": "ralph.khattar@gmail.com"
+ }
+ ],
+ "description": "A polyfill for getallheaders.",
+ "support": {
+ "issues": "https://github.com/ralouphie/getallheaders/issues",
+ "source": "https://github.com/ralouphie/getallheaders/tree/develop"
+ },
+ "time": "2019-03-08T08:55:37+00:00"
+ },
+ {
+ "name": "ramsey/collection",
+ "version": "2.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ramsey/collection.git",
+ "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ramsey/collection/zipball/344572933ad0181accbf4ba763e85a0306a8c5e2",
+ "reference": "344572933ad0181accbf4ba763e85a0306a8c5e2",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "captainhook/plugin-composer": "^5.3",
+ "ergebnis/composer-normalize": "^2.45",
+ "fakerphp/faker": "^1.24",
+ "hamcrest/hamcrest-php": "^2.0",
+ "jangregor/phpstan-prophecy": "^2.1",
+ "mockery/mockery": "^1.6",
+ "php-parallel-lint/php-console-highlighter": "^1.0",
+ "php-parallel-lint/php-parallel-lint": "^1.4",
+ "phpspec/prophecy-phpunit": "^2.3",
+ "phpstan/extension-installer": "^1.4",
+ "phpstan/phpstan": "^2.1",
+ "phpstan/phpstan-mockery": "^2.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpunit/phpunit": "^10.5",
+ "ramsey/coding-standard": "^2.3",
+ "ramsey/conventional-commits": "^1.6",
+ "roave/security-advisories": "dev-latest"
+ },
+ "type": "library",
+ "extra": {
+ "captainhook": {
+ "force-install": true
+ },
+ "ramsey/conventional-commits": {
+ "configFile": "conventional-commits.json"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Ramsey\\Collection\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ben Ramsey",
+ "email": "ben@benramsey.com",
+ "homepage": "https://benramsey.com"
+ }
+ ],
+ "description": "A PHP library for representing and manipulating collections.",
+ "keywords": [
+ "array",
+ "collection",
+ "hash",
+ "map",
+ "queue",
+ "set"
+ ],
+ "support": {
+ "issues": "https://github.com/ramsey/collection/issues",
+ "source": "https://github.com/ramsey/collection/tree/2.1.1"
+ },
+ "time": "2025-03-22T05:38:12+00:00"
+ },
+ {
+ "name": "ramsey/uuid",
+ "version": "4.9.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ramsey/uuid.git",
+ "reference": "8429c78ca35a09f27565311b98101e2826affde0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ramsey/uuid/zipball/8429c78ca35a09f27565311b98101e2826affde0",
+ "reference": "8429c78ca35a09f27565311b98101e2826affde0",
+ "shasum": ""
+ },
+ "require": {
+ "brick/math": "^0.8.16 || ^0.9 || ^0.10 || ^0.11 || ^0.12 || ^0.13 || ^0.14",
+ "php": "^8.0",
+ "ramsey/collection": "^1.2 || ^2.0"
+ },
+ "replace": {
+ "rhumsaa/uuid": "self.version"
+ },
+ "require-dev": {
+ "captainhook/captainhook": "^5.25",
+ "captainhook/plugin-composer": "^5.3",
+ "dealerdirect/phpcodesniffer-composer-installer": "^1.0",
+ "ergebnis/composer-normalize": "^2.47",
+ "mockery/mockery": "^1.6",
+ "paragonie/random-lib": "^2",
+ "php-mock/php-mock": "^2.6",
+ "php-mock/php-mock-mockery": "^1.5",
+ "php-parallel-lint/php-parallel-lint": "^1.4.0",
+ "phpbench/phpbench": "^1.2.14",
+ "phpstan/extension-installer": "^1.4",
+ "phpstan/phpstan": "^2.1",
+ "phpstan/phpstan-mockery": "^2.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpunit/phpunit": "^9.6",
+ "slevomat/coding-standard": "^8.18",
+ "squizlabs/php_codesniffer": "^3.13"
+ },
+ "suggest": {
+ "ext-bcmath": "Enables faster math with arbitrary-precision integers using BCMath.",
+ "ext-gmp": "Enables faster math with arbitrary-precision integers using GMP.",
+ "ext-uuid": "Enables the use of PeclUuidTimeGenerator and PeclUuidRandomGenerator.",
+ "paragonie/random-lib": "Provides RandomLib for use with the RandomLibAdapter",
+ "ramsey/uuid-doctrine": "Allows the use of Ramsey\\Uuid\\Uuid as Doctrine field type."
+ },
+ "type": "library",
+ "extra": {
+ "captainhook": {
+ "force-install": true
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/functions.php"
+ ],
+ "psr-4": {
+ "Ramsey\\Uuid\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "A PHP library for generating and working with universally unique identifiers (UUIDs).",
+ "keywords": [
+ "guid",
+ "identifier",
+ "uuid"
+ ],
+ "support": {
+ "issues": "https://github.com/ramsey/uuid/issues",
+ "source": "https://github.com/ramsey/uuid/tree/4.9.2"
+ },
+ "time": "2025-12-14T04:43:48+00:00"
+ },
+ {
+ "name": "sabberworm/php-css-parser",
+ "version": "v9.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/MyIntervals/PHP-CSS-Parser.git",
+ "reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/MyIntervals/PHP-CSS-Parser/zipball/88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949",
+ "reference": "88dbd0f7f91abbfe4402d0a3071e9ff4d81ed949",
+ "shasum": ""
+ },
+ "require": {
+ "ext-iconv": "*",
+ "php": "^7.2.0 || ~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0 || ~8.5.0",
+ "thecodingmachine/safe": "^1.3 || ^2.5 || ^3.4"
+ },
+ "require-dev": {
+ "php-parallel-lint/php-parallel-lint": "1.4.0",
+ "phpstan/extension-installer": "1.4.3",
+ "phpstan/phpstan": "1.12.32 || 2.1.32",
+ "phpstan/phpstan-phpunit": "1.4.2 || 2.0.8",
+ "phpstan/phpstan-strict-rules": "1.6.2 || 2.0.7",
+ "phpunit/phpunit": "8.5.52",
+ "rawr/phpunit-data-provider": "3.3.1",
+ "rector/rector": "1.2.10 || 2.2.8",
+ "rector/type-perfect": "1.0.0 || 2.1.0",
+ "squizlabs/php_codesniffer": "4.0.1",
+ "thecodingmachine/phpstan-safe-rule": "1.2.0 || 1.4.1"
+ },
+ "suggest": {
+ "ext-mbstring": "for parsing UTF-8 CSS"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "9.4.x-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/Rule/Rule.php",
+ "src/RuleSet/RuleContainer.php"
+ ],
+ "psr-4": {
+ "Sabberworm\\CSS\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Raphael Schweikert"
+ },
+ {
+ "name": "Oliver Klee",
+ "email": "github@oliverklee.de"
+ },
+ {
+ "name": "Jake Hotson",
+ "email": "jake.github@qzdesign.co.uk"
+ }
+ ],
+ "description": "Parser for CSS Files written in PHP",
+ "homepage": "https://www.sabberworm.com/blog/2010/6/10/php-css-parser",
+ "keywords": [
+ "css",
+ "parser",
+ "stylesheet"
+ ],
+ "support": {
+ "issues": "https://github.com/MyIntervals/PHP-CSS-Parser/issues",
+ "source": "https://github.com/MyIntervals/PHP-CSS-Parser/tree/v9.3.0"
+ },
+ "time": "2026-03-03T17:31:43+00:00"
+ },
+ {
+ "name": "secondnetwork/blade-tabler-icons",
+ "version": "v3.44.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/secondnetwork/blade-tabler-icons.git",
+ "reference": "856ad75e2c0704096bf4a708b6464620e41d1a34"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/secondnetwork/blade-tabler-icons/zipball/856ad75e2c0704096bf4a708b6464620e41d1a34",
+ "reference": "856ad75e2c0704096bf4a708b6464620e41d1a34",
+ "shasum": ""
+ },
+ "require": {
+ "blade-ui-kit/blade-icons": "^1.8",
+ "illuminate/support": "^9.0|^10.0|^11.0|^12.0|^13.0",
+ "php": "^8.0"
+ },
+ "require-dev": {
+ "orchestra/testbench": "^6.0|^7.0|^9.0|^10.0",
+ "phpunit/phpunit": "^9.0|^10.0|^11.0|^12.0"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "secondnetwork\\TablerIcons\\BladeTablerIconsServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "secondnetwork\\TablerIcons\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Andreas Farah"
+ }
+ ],
+ "description": "A package to easily make use of tabler-icons in your Laravel Blade views.",
+ "homepage": "https://github.com/secondnetwork/blade-tabler-icons",
+ "keywords": [
+ "blade",
+ "laravel",
+ "tabler-icons"
+ ],
+ "support": {
+ "issues": "https://github.com/secondnetwork/blade-tabler-icons/issues",
+ "source": "https://github.com/secondnetwork/blade-tabler-icons/tree/v3.44.0"
+ },
+ "time": "2026-05-11T14:36:11+00:00"
+ },
+ {
+ "name": "spatie/db-dumper",
+ "version": "4.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/spatie/db-dumper.git",
+ "reference": "f8f2785574de84aa4642a4df8b59a6dd578dd5d3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/spatie/db-dumper/zipball/f8f2785574de84aa4642a4df8b59a6dd578dd5d3",
+ "reference": "f8f2785574de84aa4642a4df8b59a6dd578dd5d3",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.3",
+ "symfony/process": "^7.0|^8.0"
+ },
+ "require-dev": {
+ "pestphp/pest": "^4.0",
+ "phpstan/phpstan": "^2.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Spatie\\DbDumper\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Freek Van der Herten",
+ "email": "freek@spatie.be",
+ "homepage": "https://spatie.be",
+ "role": "Developer"
+ }
+ ],
+ "description": "Dump databases",
+ "homepage": "https://github.com/spatie/db-dumper",
+ "keywords": [
+ "database",
+ "db-dumper",
+ "dump",
+ "mysqldump",
+ "spatie"
+ ],
+ "support": {
+ "source": "https://github.com/spatie/db-dumper/tree/4.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://spatie.be/open-source/support-us",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/spatie",
+ "type": "github"
+ }
+ ],
+ "time": "2026-05-04T11:43:33+00:00"
+ },
+ {
+ "name": "spatie/laravel-backup",
+ "version": "10.2.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/spatie/laravel-backup.git",
+ "reference": "fd8ae12e6a8401dd4de6d3beb5f37d9a627064f3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/spatie/laravel-backup/zipball/fd8ae12e6a8401dd4de6d3beb5f37d9a627064f3",
+ "reference": "fd8ae12e6a8401dd4de6d3beb5f37d9a627064f3",
+ "shasum": ""
+ },
+ "require": {
+ "ext-zip": "^1.14.0",
+ "illuminate/console": "^12.40|^13.0",
+ "illuminate/contracts": "^12.40|^13.0",
+ "illuminate/events": "^12.40|^13.0",
+ "illuminate/filesystem": "^12.40|^13.0",
+ "illuminate/notifications": "^12.40|^13.0",
+ "illuminate/support": "^12.40|^13.0",
+ "league/flysystem": "^3.30.2",
+ "php": "^8.3",
+ "spatie/db-dumper": "^4.0",
+ "spatie/laravel-package-tools": "^1.92.7",
+ "spatie/laravel-signal-aware-command": "^2.1",
+ "spatie/temporary-directory": "^2.3",
+ "symfony/console": "^7.3.6|^8.0",
+ "symfony/finder": "^7.3.5|^8.0"
+ },
+ "require-dev": {
+ "composer-runtime-api": "^2.0",
+ "ext-pcntl": "*",
+ "larastan/larastan": "^3.8",
+ "laravel/slack-notification-channel": "^3.7",
+ "league/flysystem-aws-s3-v3": "^3.30.1",
+ "mockery/mockery": "^1.6.12",
+ "orchestra/testbench": "^10.8|^11.0",
+ "pestphp/pest": "^4.1.5",
+ "phpstan/extension-installer": "^1.4.3",
+ "phpstan/phpstan-deprecation-rules": "^2.0.3",
+ "phpstan/phpstan-phpunit": "^2.0.8",
+ "rector/rector": "^2.2.8"
+ },
+ "suggest": {
+ "laravel/slack-notification-channel": "Required for sending notifications via Slack"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Spatie\\Backup\\BackupServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/Helpers/functions.php"
+ ],
+ "psr-4": {
+ "Spatie\\Backup\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Freek Van der Herten",
+ "email": "freek@spatie.be",
+ "homepage": "https://spatie.be",
+ "role": "Developer"
+ }
+ ],
+ "description": "A Laravel package to backup your application",
+ "homepage": "https://github.com/spatie/laravel-backup",
+ "keywords": [
+ "backup",
+ "database",
+ "laravel-backup",
+ "spatie"
+ ],
+ "support": {
+ "issues": "https://github.com/spatie/laravel-backup/issues",
+ "source": "https://github.com/spatie/laravel-backup/tree/10.2.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sponsors/spatie",
+ "type": "github"
+ },
+ {
+ "url": "https://spatie.be/open-source/support-us",
+ "type": "other"
+ }
+ ],
+ "time": "2026-06-01T22:44:58+00:00"
+ },
+ {
+ "name": "spatie/laravel-package-tools",
+ "version": "1.93.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/spatie/laravel-package-tools.git",
+ "reference": "d5552849801f2642aea710557463234b59ef65eb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/d5552849801f2642aea710557463234b59ef65eb",
+ "reference": "d5552849801f2642aea710557463234b59ef65eb",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/contracts": "^10.0|^11.0|^12.0|^13.0",
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "mockery/mockery": "^1.5",
+ "orchestra/testbench": "^8.0|^9.2|^10.0|^11.0",
+ "pestphp/pest": "^2.1|^3.1|^4.0",
+ "phpunit/php-code-coverage": "^10.0|^11.0|^12.0",
+ "phpunit/phpunit": "^10.5|^11.5|^12.5",
+ "spatie/pest-plugin-test-time": "^2.2|^3.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Spatie\\LaravelPackageTools\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Freek Van der Herten",
+ "email": "freek@spatie.be",
+ "role": "Developer"
+ }
+ ],
+ "description": "Tools for creating Laravel packages",
+ "homepage": "https://github.com/spatie/laravel-package-tools",
+ "keywords": [
+ "laravel-package-tools",
+ "spatie"
+ ],
+ "support": {
+ "issues": "https://github.com/spatie/laravel-package-tools/issues",
+ "source": "https://github.com/spatie/laravel-package-tools/tree/1.93.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/spatie",
+ "type": "github"
+ }
+ ],
+ "time": "2026-05-19T14:06:37+00:00"
+ },
+ {
+ "name": "spatie/laravel-permission",
+ "version": "7.4.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/spatie/laravel-permission.git",
+ "reference": "15a9daf02ba02d3ae77aaa6da582708231ef999b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/spatie/laravel-permission/zipball/15a9daf02ba02d3ae77aaa6da582708231ef999b",
+ "reference": "15a9daf02ba02d3ae77aaa6da582708231ef999b",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/auth": "^12.0|^13.0",
+ "illuminate/container": "^12.0|^13.0",
+ "illuminate/contracts": "^12.0|^13.0",
+ "illuminate/database": "^12.0|^13.0",
+ "php": "^8.3",
+ "spatie/laravel-package-tools": "^1.0"
+ },
+ "require-dev": {
+ "larastan/larastan": "^3.9",
+ "laravel/passport": "^13.0",
+ "laravel/pint": "^1.0",
+ "orchestra/testbench": "^10.0|^11.0",
+ "pestphp/pest": "^3.0|^4.0",
+ "pestphp/pest-plugin-laravel": "^3.0|^4.1",
+ "phpstan/phpstan": "^2.1"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Spatie\\Permission\\PermissionServiceProvider"
+ ]
+ },
+ "branch-alias": {
+ "dev-main": "7.x-dev",
+ "dev-master": "7.x-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/helpers.php"
+ ],
+ "psr-4": {
+ "Spatie\\Permission\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Freek Van der Herten",
+ "email": "freek@spatie.be",
+ "homepage": "https://spatie.be",
+ "role": "Developer"
+ }
+ ],
+ "description": "Permission handling for Laravel 12 and up",
+ "homepage": "https://github.com/spatie/laravel-permission",
+ "keywords": [
+ "acl",
+ "laravel",
+ "permission",
+ "permissions",
+ "rbac",
+ "roles",
+ "security",
+ "spatie"
+ ],
+ "support": {
+ "issues": "https://github.com/spatie/laravel-permission/issues",
+ "source": "https://github.com/spatie/laravel-permission/tree/7.4.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/spatie",
+ "type": "github"
+ }
+ ],
+ "time": "2026-05-30T19:21:26+00:00"
+ },
+ {
+ "name": "spatie/laravel-signal-aware-command",
+ "version": "2.1.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/spatie/laravel-signal-aware-command.git",
+ "reference": "54dcc1efd152bfb3eb0faf56a5fc28569b864b5d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/spatie/laravel-signal-aware-command/zipball/54dcc1efd152bfb3eb0faf56a5fc28569b864b5d",
+ "reference": "54dcc1efd152bfb3eb0faf56a5fc28569b864b5d",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/contracts": "^11.0|^12.0|^13.0",
+ "php": "^8.2",
+ "spatie/laravel-package-tools": "^1.4.3",
+ "symfony/console": "^7.0|^8.0"
+ },
+ "require-dev": {
+ "brianium/paratest": "^6.2|^7.0",
+ "ext-pcntl": "*",
+ "nunomaduro/collision": "^5.3|^6.0|^7.0|^8.0",
+ "orchestra/testbench": "^9.0|^10.0",
+ "pestphp/pest-plugin-laravel": "^1.3|^2.0|^3.0",
+ "phpunit/phpunit": "^9.5|^10|^11",
+ "spatie/laravel-ray": "^1.17"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "aliases": {
+ "Signal": "Spatie\\SignalAwareCommand\\Facades\\Signal"
+ },
+ "providers": [
+ "Spatie\\SignalAwareCommand\\SignalAwareCommandServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Spatie\\SignalAwareCommand\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Freek Van der Herten",
+ "email": "freek@spatie.be",
+ "role": "Developer"
+ }
+ ],
+ "description": "Handle signals in artisan commands",
+ "homepage": "https://github.com/spatie/laravel-signal-aware-command",
+ "keywords": [
+ "laravel",
+ "laravel-signal-aware-command",
+ "spatie"
+ ],
+ "support": {
+ "issues": "https://github.com/spatie/laravel-signal-aware-command/issues",
+ "source": "https://github.com/spatie/laravel-signal-aware-command/tree/2.1.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/spatie",
+ "type": "github"
+ }
+ ],
+ "time": "2026-02-22T08:16:31+00:00"
+ },
+ {
+ "name": "spatie/livewire-filepond",
+ "version": "1.7.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/spatie/livewire-filepond.git",
+ "reference": "8ca00dcb9e7b37b9d17922d7e7d51a8cd3cfdef4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/spatie/livewire-filepond/zipball/8ca00dcb9e7b37b9d17922d7e7d51a8cd3cfdef4",
+ "reference": "8ca00dcb9e7b37b9d17922d7e7d51a8cd3cfdef4",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/contracts": "^11.0|^12.0|^13.0",
+ "livewire/livewire": "^4.1.3",
+ "php": "^8.2",
+ "spatie/laravel-package-tools": "^1.92.7"
+ },
+ "require-dev": {
+ "larastan/larastan": "^3.9.2",
+ "laravel/pint": "^1.27",
+ "nunomaduro/collision": "^8.8.3",
+ "orchestra/testbench": "^9.0.0|^10.9|^11.0",
+ "pestphp/pest": "^3.8.5|^4.0",
+ "pestphp/pest-plugin-arch": "^3.1.1|^4.0",
+ "pestphp/pest-plugin-laravel": "^3.2|^4.0",
+ "phpstan/extension-installer": "^1.4.3",
+ "phpstan/phpstan-deprecation-rules": "^2.0.3",
+ "phpstan/phpstan-phpunit": "^2.0.12"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Spatie\\LivewireFilepond\\LivewireFilepondServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Spatie\\LivewireFilepond\\": "src/",
+ "Spatie\\LivewireFilepond\\Database\\Factories\\": "database/factories/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Rias Van der Veken",
+ "email": "rias@spatie.be",
+ "role": "Developer"
+ }
+ ],
+ "description": "Upload files using Filepond in Livewire components",
+ "homepage": "https://github.com/spatie/livewire-filepond",
+ "keywords": [
+ "laravel",
+ "livewire",
+ "livewire-filepond",
+ "spatie",
+ "uploads"
+ ],
+ "support": {
+ "issues": "https://github.com/spatie/livewire-filepond/issues",
+ "source": "https://github.com/spatie/livewire-filepond/tree/1.7.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/Spatie",
+ "type": "github"
+ }
+ ],
+ "time": "2026-02-27T15:21:23+00:00"
+ },
+ {
+ "name": "spatie/temporary-directory",
+ "version": "2.3.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/spatie/temporary-directory.git",
+ "reference": "662e481d6ec07ef29fd05010433428851a42cd07"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/spatie/temporary-directory/zipball/662e481d6ec07ef29fd05010433428851a42cd07",
+ "reference": "662e481d6ec07ef29fd05010433428851a42cd07",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^9.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Spatie\\TemporaryDirectory\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Alex Vanderbist",
+ "email": "alex@spatie.be",
+ "homepage": "https://spatie.be",
+ "role": "Developer"
+ }
+ ],
+ "description": "Easily create, use and destroy temporary directories",
+ "homepage": "https://github.com/spatie/temporary-directory",
+ "keywords": [
+ "php",
+ "spatie",
+ "temporary-directory"
+ ],
+ "support": {
+ "issues": "https://github.com/spatie/temporary-directory/issues",
+ "source": "https://github.com/spatie/temporary-directory/tree/2.3.1"
+ },
+ "funding": [
+ {
+ "url": "https://spatie.be/open-source/support-us",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/spatie",
+ "type": "github"
+ }
+ ],
+ "time": "2026-01-12T07:42:22+00:00"
+ },
+ {
+ "name": "symfony/clock",
+ "version": "v8.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/clock.git",
+ "reference": "701ef4de9705d6c32292ebee5e8044094a09fbf6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/clock/zipball/701ef4de9705d6c32292ebee5e8044094a09fbf6",
+ "reference": "701ef4de9705d6c32292ebee5e8044094a09fbf6",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4.1",
+ "psr/clock": "^1.0"
+ },
+ "provide": {
+ "psr/clock-implementation": "1.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/now.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\Clock\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Decouples applications from the system clock",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "clock",
+ "psr20",
+ "time"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/clock/tree/v8.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-29T05:06:50+00:00"
+ },
+ {
+ "name": "symfony/console",
+ "version": "v8.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/console.git",
+ "reference": "f5a856c6ecb56b3c21ed94a5b7bf940d857d110a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/console/zipball/f5a856c6ecb56b3c21ed94a5b7bf940d857d110a",
+ "reference": "f5a856c6ecb56b3c21ed94a5b7bf940d857d110a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-mbstring": "^1.0",
+ "symfony/polyfill-php85": "^1.32",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/string": "^7.4.6|^8.0.6"
+ },
+ "conflict": {
+ "symfony/dependency-injection": "<8.1",
+ "symfony/event-dispatcher": "<8.1"
+ },
+ "provide": {
+ "psr/log-implementation": "1.0|2.0|3.0"
+ },
+ "require-dev": {
+ "psr/log": "^1|^2|^3",
+ "symfony/config": "^7.4|^8.0",
+ "symfony/dependency-injection": "^8.1",
+ "symfony/event-dispatcher": "^8.1",
+ "symfony/filesystem": "^7.4|^8.0",
+ "symfony/http-foundation": "^7.4|^8.0",
+ "symfony/http-kernel": "^7.4|^8.0",
+ "symfony/lock": "^7.4|^8.0",
+ "symfony/messenger": "^7.4|^8.0",
+ "symfony/mime": "^7.4|^8.0",
+ "symfony/process": "^7.4|^8.0",
+ "symfony/stopwatch": "^7.4|^8.0",
+ "symfony/uid": "^7.4|^8.0",
+ "symfony/validator": "^7.4|^8.0",
+ "symfony/var-dumper": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Console\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Eases the creation of beautiful and testable command line interfaces",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "cli",
+ "command-line",
+ "console",
+ "terminal"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/console/tree/v8.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-29T05:06:50+00:00"
+ },
+ {
+ "name": "symfony/css-selector",
+ "version": "v8.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/css-selector.git",
+ "reference": "dc0e2be45c9b5588c82414f02ac574b4b986abcd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/css-selector/zipball/dc0e2be45c9b5588c82414f02ac574b4b986abcd",
+ "reference": "dc0e2be45c9b5588c82414f02ac574b4b986abcd",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\CssSelector\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Jean-François Simon",
+ "email": "jeanfrancois.simon@sensiolabs.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Converts CSS selectors to XPath expressions",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/css-selector/tree/v8.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-29T05:06:50+00:00"
+ },
+ {
+ "name": "symfony/deprecation-contracts",
+ "version": "v3.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/deprecation-contracts.git",
+ "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/50f59d1f3ca46d41ac911f97a78626b6756af35b",
+ "reference": "50f59d1f3ca46d41ac911f97a78626b6756af35b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.7-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "function.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "A generic function and convention to trigger deprecation notices",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v3.7.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-04-13T15:52:40+00:00"
+ },
+ {
+ "name": "symfony/error-handler",
+ "version": "v8.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/error-handler.git",
+ "reference": "d8aeb1abd3fef84795567850d3a567bdb5945ee5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/error-handler/zipball/d8aeb1abd3fef84795567850d3a567bdb5945ee5",
+ "reference": "d8aeb1abd3fef84795567850d3a567bdb5945ee5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4.1",
+ "psr/log": "^1|^2|^3",
+ "symfony/polyfill-php85": "^1.32",
+ "symfony/var-dumper": "^7.4|^8.0"
+ },
+ "conflict": {
+ "symfony/deprecation-contracts": "<2.5"
+ },
+ "require-dev": {
+ "symfony/console": "^7.4|^8.0",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/http-kernel": "^7.4|^8.0",
+ "symfony/serializer": "^7.4|^8.0",
+ "symfony/webpack-encore-bundle": "^1.0|^2.0"
+ },
+ "bin": [
+ "Resources/bin/patch-type-declarations"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\ErrorHandler\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides tools to manage errors and ease debugging PHP code",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/error-handler/tree/v8.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-29T05:06:50+00:00"
+ },
+ {
+ "name": "symfony/event-dispatcher",
+ "version": "v8.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/event-dispatcher.git",
+ "reference": "f249ae3f680958b6f1f9dd76e5747cf0695b4102"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f249ae3f680958b6f1f9dd76e5747cf0695b4102",
+ "reference": "f249ae3f680958b6f1f9dd76e5747cf0695b4102",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/event-dispatcher-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "symfony/security-http": "<7.4",
+ "symfony/service-contracts": "<2.5"
+ },
+ "provide": {
+ "psr/event-dispatcher-implementation": "1.0",
+ "symfony/event-dispatcher-implementation": "2.0|3.0"
+ },
+ "require-dev": {
+ "psr/log": "^1|^2|^3",
+ "symfony/config": "^7.4|^8.0",
+ "symfony/dependency-injection": "^7.4|^8.0",
+ "symfony/error-handler": "^7.4|^8.0",
+ "symfony/expression-language": "^7.4|^8.0",
+ "symfony/framework-bundle": "^7.4|^8.0",
+ "symfony/http-foundation": "^7.4|^8.0",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/stopwatch": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\EventDispatcher\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/event-dispatcher/tree/v8.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-29T05:06:50+00:00"
+ },
+ {
+ "name": "symfony/event-dispatcher-contracts",
+ "version": "v3.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/event-dispatcher-contracts.git",
+ "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/ccba7060602b7fed0b03c85bf025257f76d9ef32",
+ "reference": "ccba7060602b7fed0b03c85bf025257f76d9ef32",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/event-dispatcher": "^1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.7-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\EventDispatcher\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to dispatching event",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.7.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-05T13:30:16+00:00"
+ },
+ {
+ "name": "symfony/filesystem",
+ "version": "v8.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/filesystem.git",
+ "reference": "99aec13b82b4967ec5088222c4a3ecca955949c2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/99aec13b82b4967ec5088222c4a3ecca955949c2",
+ "reference": "99aec13b82b4967ec5088222c4a3ecca955949c2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-mbstring": "~1.8"
+ },
+ "require-dev": {
+ "symfony/process": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Filesystem\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides basic utilities for the filesystem",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/filesystem/tree/v8.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-29T05:06:50+00:00"
+ },
+ {
+ "name": "symfony/finder",
+ "version": "v8.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/finder.git",
+ "reference": "58d2e767a66052c1487356f953445634a8194c64"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/58d2e767a66052c1487356f953445634a8194c64",
+ "reference": "58d2e767a66052c1487356f953445634a8194c64",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4.1"
+ },
+ "require-dev": {
+ "symfony/filesystem": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Finder\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Finds files and directories via an intuitive fluent interface",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/finder/tree/v8.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-29T05:06:50+00:00"
+ },
+ {
+ "name": "symfony/http-foundation",
+ "version": "v8.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-foundation.git",
+ "reference": "af11474600f06718086c2cda4fa6fa8d0a672e7e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-foundation/zipball/af11474600f06718086c2cda4fa6fa8d0a672e7e",
+ "reference": "af11474600f06718086c2cda4fa6fa8d0a672e7e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4.1",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/polyfill-mbstring": "^1.1"
+ },
+ "conflict": {
+ "doctrine/dbal": "<4.3"
+ },
+ "require-dev": {
+ "doctrine/dbal": "^4.3",
+ "predis/predis": "^1.1|^2.0",
+ "symfony/cache": "^7.4|^8.0",
+ "symfony/clock": "^7.4|^8.0",
+ "symfony/dependency-injection": "^7.4|^8.0",
+ "symfony/expression-language": "^7.4|^8.0",
+ "symfony/http-kernel": "^7.4|^8.0",
+ "symfony/mime": "^7.4|^8.0",
+ "symfony/rate-limiter": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\HttpFoundation\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Defines an object-oriented layer for the HTTP specification",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/http-foundation/tree/v8.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-29T05:06:50+00:00"
+ },
+ {
+ "name": "symfony/http-kernel",
+ "version": "v8.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/http-kernel.git",
+ "reference": "cefeb37c82eed3e0c42fa25ba64cd3a908d90f39"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/http-kernel/zipball/cefeb37c82eed3e0c42fa25ba64cd3a908d90f39",
+ "reference": "cefeb37c82eed3e0c42fa25ba64cd3a908d90f39",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4.1",
+ "psr/log": "^1|^2|^3",
+ "symfony/deprecation-contracts": "^2.5|^3",
+ "symfony/error-handler": "^7.4|^8.0",
+ "symfony/event-dispatcher": "^7.4|^8.0",
+ "symfony/http-foundation": "^7.4|^8.0",
+ "symfony/polyfill-ctype": "^1.8"
+ },
+ "conflict": {
+ "symfony/dependency-injection": "<8.1",
+ "symfony/flex": "<2.10",
+ "symfony/http-client-contracts": "<2.5",
+ "symfony/translation-contracts": "<2.5",
+ "symfony/var-dumper": "<8.1",
+ "symfony/web-profiler-bundle": "<8.1",
+ "twig/twig": "<3.21"
+ },
+ "provide": {
+ "psr/log-implementation": "1.0|2.0|3.0"
+ },
+ "require-dev": {
+ "psr/cache": "^1.0|^2.0|^3.0",
+ "symfony/browser-kit": "^7.4|^8.0",
+ "symfony/clock": "^7.4|^8.0",
+ "symfony/config": "^7.4|^8.0",
+ "symfony/console": "^7.4|^8.0",
+ "symfony/css-selector": "^7.4|^8.0",
+ "symfony/dependency-injection": "^8.1",
+ "symfony/dom-crawler": "^7.4|^8.0",
+ "symfony/expression-language": "^7.4|^8.0",
+ "symfony/finder": "^7.4|^8.0",
+ "symfony/http-client-contracts": "^2.5|^3",
+ "symfony/process": "^7.4|^8.0",
+ "symfony/property-access": "^7.4|^8.0",
+ "symfony/rate-limiter": "^7.4|^8.0",
+ "symfony/routing": "^7.4|^8.0",
+ "symfony/serializer": "^7.4|^8.0",
+ "symfony/stopwatch": "^7.4|^8.0",
+ "symfony/translation": "^7.4|^8.0",
+ "symfony/translation-contracts": "^2.5|^3",
+ "symfony/uid": "^7.4|^8.0",
+ "symfony/validator": "^7.4|^8.0",
+ "symfony/var-dumper": "^8.1",
+ "symfony/var-exporter": "^7.4|^8.0",
+ "twig/twig": "^3.21"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\HttpKernel\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides a structured process for converting a Request into a Response",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/http-kernel/tree/v8.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-29T08:46:08+00:00"
+ },
+ {
+ "name": "symfony/mailer",
+ "version": "v8.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/mailer.git",
+ "reference": "9418d772df3a03a142e3bc06f602adb2b8724877"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/mailer/zipball/9418d772df3a03a142e3bc06f602adb2b8724877",
+ "reference": "9418d772df3a03a142e3bc06f602adb2b8724877",
+ "shasum": ""
+ },
+ "require": {
+ "egulias/email-validator": "^2.1.10|^3|^4",
+ "php": ">=8.4.1",
+ "psr/event-dispatcher": "^1",
+ "psr/log": "^1|^2|^3",
+ "symfony/event-dispatcher": "^7.4|^8.0",
+ "symfony/mime": "^7.4|^8.0",
+ "symfony/service-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "symfony/http-client-contracts": "<2.5"
+ },
+ "require-dev": {
+ "symfony/console": "^7.4|^8.0",
+ "symfony/http-client": "^7.4|^8.0",
+ "symfony/messenger": "^7.4|^8.0",
+ "symfony/twig-bridge": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Mailer\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Helps sending emails",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/mailer/tree/v8.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-29T05:06:50+00:00"
+ },
+ {
+ "name": "symfony/mime",
+ "version": "v8.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/mime.git",
+ "reference": "b164ae7e3f7915aacfe9ee155f2f358502440664"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/mime/zipball/b164ae7e3f7915aacfe9ee155f2f358502440664",
+ "reference": "b164ae7e3f7915aacfe9ee155f2f358502440664",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4.1",
+ "symfony/polyfill-intl-idn": "^1.10",
+ "symfony/polyfill-mbstring": "^1.0"
+ },
+ "conflict": {
+ "egulias/email-validator": "~3.0.0",
+ "phpdocumentor/reflection-docblock": "<5.2|>=7",
+ "phpdocumentor/type-resolver": "<1.5.1"
+ },
+ "require-dev": {
+ "egulias/email-validator": "^2.1.10|^3.1|^4",
+ "league/html-to-markdown": "^5.0",
+ "phpdocumentor/reflection-docblock": "^5.2|^6.0",
+ "symfony/dependency-injection": "^7.4|^8.0",
+ "symfony/process": "^7.4|^8.0",
+ "symfony/property-access": "^7.4|^8.0",
+ "symfony/property-info": "^7.4|^8.0",
+ "symfony/serializer": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Mime\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Allows manipulating MIME messages",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "mime",
+ "mime-type"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/mime/tree/v8.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-29T05:06:50+00:00"
+ },
+ {
+ "name": "symfony/polyfill-ctype",
+ "version": "v1.37.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-ctype.git",
+ "reference": "141046a8f9477948ff284fa65be2095baafb94f2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/141046a8f9477948ff284fa65be2095baafb94f2",
+ "reference": "141046a8f9477948ff284fa65be2095baafb94f2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-ctype": "*"
+ },
+ "suggest": {
+ "ext-ctype": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Ctype\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Gert de Pagter",
+ "email": "BackEndTea@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for ctype functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "ctype",
+ "polyfill",
+ "portable"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-04-10T16:19:22+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-grapheme",
+ "version": "v1.38.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-grapheme.git",
+ "reference": "e9247d281d694a5120554d9afaf54e070e88a603"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603",
+ "reference": "e9247d281d694a5120554d9afaf54e070e88a603",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Grapheme\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for intl's grapheme_* functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "grapheme",
+ "intl",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-26T05:58:03+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-idn",
+ "version": "v1.38.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-idn.git",
+ "reference": "dc21118016c039a66235cf93d96b435ffb282412"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/dc21118016c039a66235cf93d96b435ffb282412",
+ "reference": "dc21118016c039a66235cf93d96b435ffb282412",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2",
+ "symfony/polyfill-intl-normalizer": "^1.10"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Idn\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Laurent Bassin",
+ "email": "laurent@bassin.info"
+ },
+ {
+ "name": "Trevor Rowbotham",
+ "email": "trevor.rowbotham@pm.me"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for intl's idn_to_ascii and idn_to_utf8 functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "idn",
+ "intl",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-intl-idn/tree/v1.38.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-25T15:22:23+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-normalizer",
+ "version": "v1.38.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
+ "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/2d446c214bdbe5b71bde5011b060a05fece3ae6b",
+ "reference": "2d446c214bdbe5b71bde5011b060a05fece3ae6b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Normalizer\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for intl's Normalizer class and related functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "intl",
+ "normalizer",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.38.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-25T13:48:31+00:00"
+ },
+ {
+ "name": "symfony/polyfill-mbstring",
+ "version": "v1.38.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-mbstring.git",
+ "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6",
+ "reference": "d3d318bad5e7a1bfbd026009c8bfb8d8f99ae6b6",
+ "shasum": ""
+ },
+ "require": {
+ "ext-iconv": "*",
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-mbstring": "*"
+ },
+ "suggest": {
+ "ext-mbstring": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Mbstring\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for the Mbstring extension",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "mbstring",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.38.2"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-27T06:59:30+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php80",
+ "version": "v1.37.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php80.git",
+ "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
+ "reference": "dfb55726c3a76ea3b6459fcfda1ec2d80a682411",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php80\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ion Bazan",
+ "email": "ion.bazan@gmail.com"
+ },
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.37.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-04-10T16:19:22+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php84",
+ "version": "v1.38.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php84.git",
+ "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php84/zipball/f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa",
+ "reference": "f4e1dfaee5b74aba5964fe1fd4dfc7ba5e3085fa",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php84\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.4+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php84/tree/v1.38.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-26T12:51:13+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php85",
+ "version": "v1.38.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php85.git",
+ "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1",
+ "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php85\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.5+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php85/tree/v1.38.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-26T02:25:22+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php86",
+ "version": "v1.38.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php86.git",
+ "reference": "fcec68d64f46dc84e1f6ffcf2c6dda40ff3143ad"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php86/zipball/fcec68d64f46dc84e1f6ffcf2c6dda40ff3143ad",
+ "reference": "fcec68d64f46dc84e1f6ffcf2c6dda40ff3143ad",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php86\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.6+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php86/tree/v1.38.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-25T11:52:35+00:00"
+ },
+ {
+ "name": "symfony/polyfill-uuid",
+ "version": "v1.37.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-uuid.git",
+ "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-uuid/zipball/26dfec253c4cf3e51b541b52ddf7e42cb0908e94",
+ "reference": "26dfec253c4cf3e51b541b52ddf7e42cb0908e94",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2"
+ },
+ "provide": {
+ "ext-uuid": "*"
+ },
+ "suggest": {
+ "ext-uuid": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/polyfill",
+ "name": "symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Uuid\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Grégoire Pineau",
+ "email": "lyrixx@lyrixx.info"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for uuid functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "uuid"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-uuid/tree/v1.37.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-04-10T16:19:22+00:00"
+ },
+ {
+ "name": "symfony/process",
+ "version": "v8.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/process.git",
+ "reference": "c4a9e58f235a6bf7f97ffbfedae2687353ac79e5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/process/zipball/c4a9e58f235a6bf7f97ffbfedae2687353ac79e5",
+ "reference": "c4a9e58f235a6bf7f97ffbfedae2687353ac79e5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Process\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Executes commands in sub-processes",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/process/tree/v8.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-29T05:06:50+00:00"
+ },
+ {
+ "name": "symfony/routing",
+ "version": "v8.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/routing.git",
+ "reference": "fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/routing/zipball/fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3",
+ "reference": "fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4.1",
+ "symfony/deprecation-contracts": "^2.5|^3"
+ },
+ "require-dev": {
+ "psr/log": "^1|^2|^3",
+ "symfony/config": "^7.4|^8.0",
+ "symfony/dependency-injection": "^7.4|^8.0",
+ "symfony/expression-language": "^7.4|^8.0",
+ "symfony/http-foundation": "^7.4|^8.0",
+ "symfony/yaml": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Routing\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Maps an HTTP request to a set of configuration variables",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "router",
+ "routing",
+ "uri",
+ "url"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/routing/tree/v8.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-29T05:06:50+00:00"
+ },
+ {
+ "name": "symfony/service-contracts",
+ "version": "v3.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/service-contracts.git",
+ "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d25d82433a80eba6aa0e6c24b61d7370d99e444a",
+ "reference": "d25d82433a80eba6aa0e6c24b61d7370d99e444a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/container": "^1.1|^2.0",
+ "symfony/deprecation-contracts": "^2.5|^3"
+ },
+ "conflict": {
+ "ext-psr": "<1.1|>=2"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.7-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Service\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Test/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to writing services",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/service-contracts/tree/v3.7.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-03-28T09:44:51+00:00"
+ },
+ {
+ "name": "symfony/string",
+ "version": "v8.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/string.git",
+ "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/string/zipball/afd5944f4005862d961efb85c8bbd5c523c4e3c9",
+ "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4.1",
+ "symfony/polyfill-ctype": "^1.8",
+ "symfony/polyfill-intl-grapheme": "^1.33",
+ "symfony/polyfill-intl-normalizer": "^1.0",
+ "symfony/polyfill-mbstring": "^1.0"
+ },
+ "conflict": {
+ "symfony/translation-contracts": "<2.5"
+ },
+ "require-dev": {
+ "symfony/emoji": "^7.4|^8.0",
+ "symfony/http-client": "^7.4|^8.0",
+ "symfony/intl": "^7.4|^8.0",
+ "symfony/translation-contracts": "^2.5|^3.0",
+ "symfony/var-exporter": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/functions.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\String\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "grapheme",
+ "i18n",
+ "string",
+ "unicode",
+ "utf-8",
+ "utf8"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/string/tree/v8.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-29T05:06:50+00:00"
+ },
+ {
+ "name": "symfony/translation",
+ "version": "v8.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/translation.git",
+ "reference": "b2bd012ca28c4acae830ee1206a5b6e35dd99693"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/translation/zipball/b2bd012ca28c4acae830ee1206a5b6e35dd99693",
+ "reference": "b2bd012ca28c4acae830ee1206a5b6e35dd99693",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4.1",
+ "symfony/polyfill-mbstring": "^1.0",
+ "symfony/translation-contracts": "^3.6.1"
+ },
+ "conflict": {
+ "nikic/php-parser": "<5.0",
+ "symfony/http-client-contracts": "<2.5",
+ "symfony/service-contracts": "<2.5"
+ },
+ "provide": {
+ "symfony/translation-implementation": "2.3|3.0"
+ },
+ "require-dev": {
+ "nikic/php-parser": "^5.0",
+ "psr/log": "^1|^2|^3",
+ "symfony/config": "^7.4|^8.0",
+ "symfony/console": "^7.4|^8.0",
+ "symfony/dependency-injection": "^7.4|^8.0",
+ "symfony/finder": "^7.4|^8.0",
+ "symfony/http-client-contracts": "^2.5|^3.0",
+ "symfony/http-kernel": "^7.4|^8.0",
+ "symfony/intl": "^7.4|^8.0",
+ "symfony/polyfill-intl-icu": "^1.21",
+ "symfony/routing": "^7.4|^8.0",
+ "symfony/service-contracts": "^2.5|^3",
+ "symfony/yaml": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/functions.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\Translation\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides tools to internationalize your application",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/translation/tree/v8.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-29T05:06:50+00:00"
+ },
+ {
+ "name": "symfony/translation-contracts",
+ "version": "v3.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/translation-contracts.git",
+ "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/translation-contracts/zipball/0ab302977a952b42fd51475c4ebac81f8da0a95d",
+ "reference": "0ab302977a952b42fd51475c4ebac81f8da0a95d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "thanks": {
+ "url": "https://github.com/symfony/contracts",
+ "name": "symfony/contracts"
+ },
+ "branch-alias": {
+ "dev-main": "3.7-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Translation\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Test/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to translation",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/translation-contracts/tree/v3.7.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-01-05T13:30:16+00:00"
+ },
+ {
+ "name": "symfony/uid",
+ "version": "v8.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/uid.git",
+ "reference": "7393f157a55f7e70a4de0334435c55a5a8fe749a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/uid/zipball/7393f157a55f7e70a4de0334435c55a5a8fe749a",
+ "reference": "7393f157a55f7e70a4de0334435c55a5a8fe749a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4.1",
+ "symfony/polyfill-uuid": "^1.15"
+ },
+ "require-dev": {
+ "symfony/console": "^7.4|^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Uid\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Grégoire Pineau",
+ "email": "lyrixx@lyrixx.info"
+ },
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides an object-oriented API to generate and represent UIDs",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "UID",
+ "ulid",
+ "uuid"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/uid/tree/v8.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-29T05:06:50+00:00"
+ },
+ {
+ "name": "symfony/var-dumper",
+ "version": "v8.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/var-dumper.git",
+ "reference": "c2c4df1d21477cc21c9f6dc1b14d07c3abc4963e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/var-dumper/zipball/c2c4df1d21477cc21c9f6dc1b14d07c3abc4963e",
+ "reference": "c2c4df1d21477cc21c9f6dc1b14d07c3abc4963e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4.1",
+ "symfony/polyfill-mbstring": "^1.0"
+ },
+ "conflict": {
+ "symfony/console": "<7.4",
+ "symfony/error-handler": "<7.4"
+ },
+ "require-dev": {
+ "symfony/console": "^7.4|^8.0",
+ "symfony/http-kernel": "^7.4|^8.0",
+ "symfony/process": "^7.4|^8.0",
+ "symfony/uid": "^7.4|^8.0",
+ "twig/twig": "^3.12"
+ },
+ "bin": [
+ "Resources/bin/var-dump-server"
+ ],
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/functions/dump.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\VarDumper\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides mechanisms for walking through any arbitrary PHP variable",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "debug",
+ "dump"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/var-dumper/tree/v8.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-29T05:06:50+00:00"
+ },
+ {
+ "name": "thecodingmachine/safe",
+ "version": "v3.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/thecodingmachine/safe.git",
+ "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/thecodingmachine/safe/zipball/705683a25bacf0d4860c7dea4d7947bfd09eea19",
+ "reference": "705683a25bacf0d4860c7dea4d7947bfd09eea19",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.1"
+ },
+ "require-dev": {
+ "php-parallel-lint/php-parallel-lint": "^1.4",
+ "phpstan/phpstan": "^2",
+ "phpunit/phpunit": "^10",
+ "squizlabs/php_codesniffer": "^3.2"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "lib/special_cases.php",
+ "generated/apache.php",
+ "generated/apcu.php",
+ "generated/array.php",
+ "generated/bzip2.php",
+ "generated/calendar.php",
+ "generated/classobj.php",
+ "generated/com.php",
+ "generated/cubrid.php",
+ "generated/curl.php",
+ "generated/datetime.php",
+ "generated/dir.php",
+ "generated/eio.php",
+ "generated/errorfunc.php",
+ "generated/exec.php",
+ "generated/fileinfo.php",
+ "generated/filesystem.php",
+ "generated/filter.php",
+ "generated/fpm.php",
+ "generated/ftp.php",
+ "generated/funchand.php",
+ "generated/gettext.php",
+ "generated/gmp.php",
+ "generated/gnupg.php",
+ "generated/hash.php",
+ "generated/ibase.php",
+ "generated/ibmDb2.php",
+ "generated/iconv.php",
+ "generated/image.php",
+ "generated/imap.php",
+ "generated/info.php",
+ "generated/inotify.php",
+ "generated/json.php",
+ "generated/ldap.php",
+ "generated/libxml.php",
+ "generated/lzf.php",
+ "generated/mailparse.php",
+ "generated/mbstring.php",
+ "generated/misc.php",
+ "generated/mysql.php",
+ "generated/mysqli.php",
+ "generated/network.php",
+ "generated/oci8.php",
+ "generated/opcache.php",
+ "generated/openssl.php",
+ "generated/outcontrol.php",
+ "generated/pcntl.php",
+ "generated/pcre.php",
+ "generated/pgsql.php",
+ "generated/posix.php",
+ "generated/ps.php",
+ "generated/pspell.php",
+ "generated/readline.php",
+ "generated/rnp.php",
+ "generated/rpminfo.php",
+ "generated/rrd.php",
+ "generated/sem.php",
+ "generated/session.php",
+ "generated/shmop.php",
+ "generated/sockets.php",
+ "generated/sodium.php",
+ "generated/solr.php",
+ "generated/spl.php",
+ "generated/sqlsrv.php",
+ "generated/ssdeep.php",
+ "generated/ssh2.php",
+ "generated/stream.php",
+ "generated/strings.php",
+ "generated/swoole.php",
+ "generated/uodbc.php",
+ "generated/uopz.php",
+ "generated/url.php",
+ "generated/var.php",
+ "generated/xdiff.php",
+ "generated/xml.php",
+ "generated/xmlrpc.php",
+ "generated/yaml.php",
+ "generated/yaz.php",
+ "generated/zip.php",
+ "generated/zlib.php"
+ ],
+ "classmap": [
+ "lib/DateTime.php",
+ "lib/DateTimeImmutable.php",
+ "lib/Exceptions/",
+ "generated/Exceptions/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHP core functions that throw exceptions instead of returning FALSE on error",
+ "support": {
+ "issues": "https://github.com/thecodingmachine/safe/issues",
+ "source": "https://github.com/thecodingmachine/safe/tree/v3.4.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/OskarStark",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/shish",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/silasjoisten",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/staabm",
+ "type": "github"
+ }
+ ],
+ "time": "2026-02-04T18:08:13+00:00"
+ },
+ {
+ "name": "tightenco/ziggy",
+ "version": "v2.6.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/tighten/ziggy.git",
+ "reference": "8a0b645921623f77dceaf543d61ecd51a391d96e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/tighten/ziggy/zipball/8a0b645921623f77dceaf543d61ecd51a391d96e",
+ "reference": "8a0b645921623f77dceaf543d61ecd51a391d96e",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "laravel/framework": ">=9.0",
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "laravel/folio": "^1.1",
+ "orchestra/testbench": "^8.0 || ^9.0 || ^10.0",
+ "pestphp/pest": "^2.0 || ^3.0 || ^4.0",
+ "pestphp/pest-plugin-laravel": "^2.0 || ^3.0 || ^4.0"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Tighten\\Ziggy\\ZiggyServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Tighten\\Ziggy\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Daniel Coulbourne",
+ "email": "daniel@tighten.co"
+ },
+ {
+ "name": "Jake Bathman",
+ "email": "jake@tighten.co"
+ },
+ {
+ "name": "Jacob Baker-Kretzmar",
+ "email": "jacob@tighten.co"
+ }
+ ],
+ "description": "Use your Laravel named routes in JavaScript.",
+ "homepage": "https://github.com/tighten/ziggy",
+ "keywords": [
+ "Ziggy",
+ "javascript",
+ "laravel",
+ "routes"
+ ],
+ "support": {
+ "issues": "https://github.com/tighten/ziggy/issues",
+ "source": "https://github.com/tighten/ziggy/tree/v2.6.2"
+ },
+ "time": "2026-03-05T14:41:19+00:00"
+ },
+ {
+ "name": "tijsverkoyen/css-to-inline-styles",
+ "version": "v2.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/tijsverkoyen/CssToInlineStyles.git",
+ "reference": "f0292ccf0ec75843d65027214426b6b163b48b41"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/tijsverkoyen/CssToInlineStyles/zipball/f0292ccf0ec75843d65027214426b6b163b48b41",
+ "reference": "f0292ccf0ec75843d65027214426b6b163b48b41",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "php": "^7.4 || ^8.0",
+ "symfony/css-selector": "^5.4 || ^6.0 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^2.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpunit/phpunit": "^8.5.21 || ^9.5.10"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "TijsVerkoyen\\CssToInlineStyles\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Tijs Verkoyen",
+ "email": "css_to_inline_styles@verkoyen.eu",
+ "role": "Developer"
+ }
+ ],
+ "description": "CssToInlineStyles is a class that enables you to convert HTML-pages/files into HTML-pages/files with inline styles. This is very useful when you're sending emails.",
+ "homepage": "https://github.com/tijsverkoyen/CssToInlineStyles",
+ "support": {
+ "issues": "https://github.com/tijsverkoyen/CssToInlineStyles/issues",
+ "source": "https://github.com/tijsverkoyen/CssToInlineStyles/tree/v2.4.0"
+ },
+ "time": "2025-12-02T11:56:42+00:00"
+ },
+ {
+ "name": "vlucas/phpdotenv",
+ "version": "v5.6.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/vlucas/phpdotenv.git",
+ "reference": "955e7815d677a3eaa7075231212f2110983adecc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc",
+ "reference": "955e7815d677a3eaa7075231212f2110983adecc",
+ "shasum": ""
+ },
+ "require": {
+ "ext-pcre": "*",
+ "graham-campbell/result-type": "^1.1.4",
+ "php": "^7.2.5 || ^8.0",
+ "phpoption/phpoption": "^1.9.5",
+ "symfony/polyfill-ctype": "^1.26",
+ "symfony/polyfill-mbstring": "^1.26",
+ "symfony/polyfill-php80": "^1.26"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.8.2",
+ "ext-filter": "*",
+ "phpunit/phpunit": "^8.5.34 || ^9.6.13 || ^10.4.2"
+ },
+ "suggest": {
+ "ext-filter": "Required to use the boolean validator."
+ },
+ "type": "library",
+ "extra": {
+ "bamarni-bin": {
+ "bin-links": true,
+ "forward-command": false
+ },
+ "branch-alias": {
+ "dev-master": "5.6-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Dotenv\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Graham Campbell",
+ "email": "hello@gjcampbell.co.uk",
+ "homepage": "https://github.com/GrahamCampbell"
+ },
+ {
+ "name": "Vance Lucas",
+ "email": "vance@vancelucas.com",
+ "homepage": "https://github.com/vlucas"
+ }
+ ],
+ "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.",
+ "keywords": [
+ "dotenv",
+ "env",
+ "environment"
+ ],
+ "support": {
+ "issues": "https://github.com/vlucas/phpdotenv/issues",
+ "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/GrahamCampbell",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-12-27T19:49:13+00:00"
+ },
+ {
+ "name": "voku/portable-ascii",
+ "version": "2.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/voku/portable-ascii.git",
+ "reference": "8e1051fe39379367aecf014f41744ce7539a856f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/voku/portable-ascii/zipball/8e1051fe39379367aecf014f41744ce7539a856f",
+ "reference": "8e1051fe39379367aecf014f41744ce7539a856f",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "~8.5 || ~9.6 || ~10.5 || ~11.5"
+ },
+ "suggest": {
+ "ext-intl": "Use Intl for transliterator_transliterate() support"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "voku\\": "src/voku/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Lars Moelleken",
+ "homepage": "https://www.moelleken.org/"
+ }
+ ],
+ "description": "Portable ASCII library - performance optimized (ascii) string functions for php.",
+ "homepage": "https://github.com/voku/portable-ascii",
+ "keywords": [
+ "ascii",
+ "clean",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/voku/portable-ascii/issues",
+ "source": "https://github.com/voku/portable-ascii/tree/2.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://www.paypal.me/moelleken",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/voku",
+ "type": "github"
+ },
+ {
+ "url": "https://opencollective.com/portable-ascii",
+ "type": "open_collective"
+ },
+ {
+ "url": "https://www.patreon.com/voku",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/voku/portable-ascii",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-04-26T05:33:54+00:00"
+ },
+ {
+ "name": "yajra/laravel-datatables",
+ "version": "v13.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/yajra/datatables.git",
+ "reference": "7d471daf6caf7ae70aa882e6670c6b994d58fa7d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/yajra/datatables/zipball/7d471daf6caf7ae70aa882e6670c6b994d58fa7d",
+ "reference": "7d471daf6caf7ae70aa882e6670c6b994d58fa7d",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.3",
+ "yajra/laravel-datatables-buttons": "^13",
+ "yajra/laravel-datatables-editor": "^13",
+ "yajra/laravel-datatables-export": "^13",
+ "yajra/laravel-datatables-fractal": "^13",
+ "yajra/laravel-datatables-html": "^13",
+ "yajra/laravel-datatables-oracle": "^13"
+ },
+ "require-dev": {
+ "larastan/larastan": "^3.1",
+ "laravel/pint": "^1.21",
+ "orchestra/testbench": "^11",
+ "pestphp/pest": "^4",
+ "pestphp/pest-plugin-laravel": "^4",
+ "rector/rector": "^2.0.9"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "13.x-dev"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Arjay Angeles",
+ "email": "aqangeles@gmail.com"
+ }
+ ],
+ "description": "Laravel DataTables Complete Package.",
+ "keywords": [
+ "datatables",
+ "jquery",
+ "laravel"
+ ],
+ "support": {
+ "issues": "https://github.com/yajra/datatables/issues",
+ "source": "https://github.com/yajra/datatables/tree/v13.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sponsors/yajra",
+ "type": "github"
+ }
+ ],
+ "time": "2026-03-25T07:43:24+00:00"
+ },
+ {
+ "name": "yajra/laravel-datatables-buttons",
+ "version": "v13.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/yajra/laravel-datatables-buttons.git",
+ "reference": "426346860a88f69b93a0f51a5af8290e0e4bb55b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/yajra/laravel-datatables-buttons/zipball/426346860a88f69b93a0f51a5af8290e0e4bb55b",
+ "reference": "426346860a88f69b93a0f51a5af8290e0e4bb55b",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/console": "^13",
+ "php": "^8.3",
+ "yajra/laravel-datatables-html": "^13",
+ "yajra/laravel-datatables-oracle": "^13"
+ },
+ "require-dev": {
+ "larastan/larastan": "^3.1",
+ "laravel/pint": "^1.21",
+ "maatwebsite/excel": "^3.1.64",
+ "openspout/openspout": "^4.24 || ^5.3",
+ "orchestra/testbench": "^11",
+ "rector/rector": "^2.0"
+ },
+ "suggest": {
+ "barryvdh/laravel-snappy": "Allows exporting of dataTable to PDF using the print view.",
+ "dompdf/dompdf": "Allows exporting of dataTable to PDF using the DomPDF.",
+ "maatwebsite/excel": "Exporting of dataTables (excel, csv and PDF) using maatwebsite package.",
+ "openspout/openspout": "Faster streaming export of dataTables (Excel/CSV) when using the fast export option (OpenSpout ^4.24 or ^5.3).",
+ "yajra/laravel-datatables-export": "Exporting of dataTables (excel, csv and PDF) via livewire and queue worker."
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Yajra\\DataTables\\ButtonsServiceProvider"
+ ]
+ },
+ "branch-alias": {
+ "dev-master": "13.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Yajra\\DataTables\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Arjay Angeles",
+ "email": "aqangeles@gmail.com"
+ }
+ ],
+ "description": "Laravel DataTables Buttons Plugin.",
+ "keywords": [
+ "buttons",
+ "datatables",
+ "jquery",
+ "laravel"
+ ],
+ "support": {
+ "issues": "https://github.com/yajra/laravel-datatables-buttons/issues",
+ "source": "https://github.com/yajra/laravel-datatables-buttons/tree/v13.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sponsors/yajra",
+ "type": "github"
+ }
+ ],
+ "time": "2026-03-28T09:30:37+00:00"
+ },
+ {
+ "name": "yajra/laravel-datatables-editor",
+ "version": "v13.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/yajra/laravel-datatables-editor.git",
+ "reference": "552b20cc03541d977fffadf55340b3612bff3052"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/yajra/laravel-datatables-editor/zipball/552b20cc03541d977fffadf55340b3612bff3052",
+ "reference": "552b20cc03541d977fffadf55340b3612bff3052",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/console": "^13",
+ "illuminate/database": "^13",
+ "illuminate/http": "^13",
+ "illuminate/validation": "^13",
+ "php": "^8.3"
+ },
+ "require-dev": {
+ "larastan/larastan": "^3.1",
+ "laravel/pint": "^1.21.2",
+ "orchestra/testbench": "^11",
+ "rector/rector": "^2.0.10"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Yajra\\DataTables\\EditorServiceProvider"
+ ]
+ },
+ "branch-alias": {
+ "dev-master": "13.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Yajra\\DataTables\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Arjay Angeles",
+ "email": "aqangeles@gmail.com"
+ }
+ ],
+ "description": "Laravel DataTables Editor plugin for Laravel 5.5+.",
+ "keywords": [
+ "JS",
+ "datatables",
+ "editor",
+ "html",
+ "jquery",
+ "laravel"
+ ],
+ "support": {
+ "issues": "https://github.com/yajra/laravel-datatables-editor/issues",
+ "source": "https://github.com/yajra/laravel-datatables-editor/tree/v13.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.paypal.me/yajra",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/yajra",
+ "type": "github"
+ },
+ {
+ "url": "https://www.patreon.com/yajra",
+ "type": "patreon"
+ }
+ ],
+ "time": "2026-03-18T03:15:37+00:00"
+ },
+ {
+ "name": "yajra/laravel-datatables-export",
+ "version": "v13.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/yajra/laravel-datatables-export.git",
+ "reference": "bb2f1ae2d3ee6793472c063f352593404a547745"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/yajra/laravel-datatables-export/zipball/bb2f1ae2d3ee6793472c063f352593404a547745",
+ "reference": "bb2f1ae2d3ee6793472c063f352593404a547745",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "livewire/livewire": "^4.2.2",
+ "openspout/openspout": "^4.24.5 || ^5.0",
+ "php": "^8.3",
+ "phpoffice/phpspreadsheet": "^5.5",
+ "yajra/laravel-datatables-buttons": "^13.0.2"
+ },
+ "require-dev": {
+ "larastan/larastan": "^3.9.3",
+ "laravel/pint": "^1.29",
+ "orchestra/testbench": "^11.0",
+ "pestphp/pest": "^4.4.3",
+ "pestphp/pest-plugin-laravel": "^4.1",
+ "rector/rector": "^2.3.9"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Yajra\\DataTables\\ExportServiceProvider"
+ ]
+ },
+ "branch-alias": {
+ "dev-master": "13.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Yajra\\DataTables\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Arjay Angeles",
+ "email": "aqangeles@gmail.com"
+ }
+ ],
+ "description": "Laravel DataTables Queued Export Plugin.",
+ "keywords": [
+ "datatables",
+ "excel",
+ "export",
+ "laravel",
+ "livewire",
+ "queue"
+ ],
+ "support": {
+ "issues": "https://github.com/yajra/laravel-datatables-export/issues",
+ "source": "https://github.com/yajra/laravel-datatables-export/tree/v13.2.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sponsors/yajra",
+ "type": "github"
+ }
+ ],
+ "time": "2026-05-28T02:36:01+00:00"
+ },
+ {
+ "name": "yajra/laravel-datatables-fractal",
+ "version": "v13.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/yajra/laravel-datatables-fractal.git",
+ "reference": "fa31d80c160d9b656344eaf9e14036c5dae07684"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/yajra/laravel-datatables-fractal/zipball/fa31d80c160d9b656344eaf9e14036c5dae07684",
+ "reference": "fa31d80c160d9b656344eaf9e14036c5dae07684",
+ "shasum": ""
+ },
+ "require": {
+ "league/fractal": "^0.20.1",
+ "php": "^8.3",
+ "yajra/laravel-datatables-oracle": "^13"
+ },
+ "require-dev": {
+ "larastan/larastan": "^3.1",
+ "laravel/pint": "^1.21",
+ "orchestra/testbench": "^11.0",
+ "rector/rector": "^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Yajra\\DataTables\\FractalServiceProvider"
+ ]
+ },
+ "branch-alias": {
+ "dev-master": "13.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Yajra\\DataTables\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Arjay Angeles",
+ "email": "aqangeles@gmail.com"
+ }
+ ],
+ "description": "Laravel DataTables Fractal Plugin.",
+ "keywords": [
+ "api",
+ "datatables",
+ "fractal",
+ "laravel"
+ ],
+ "support": {
+ "issues": "https://github.com/yajra/laravel-datatables-fractal/issues",
+ "source": "https://github.com/yajra/laravel-datatables-fractal/tree/v13.0.0"
+ },
+ "time": "2026-03-18T02:55:54+00:00"
+ },
+ {
+ "name": "yajra/laravel-datatables-html",
+ "version": "v13.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/yajra/laravel-datatables-html.git",
+ "reference": "cdb32f509ef2e70e447beaba540d32cfd8ee648d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/yajra/laravel-datatables-html/zipball/cdb32f509ef2e70e447beaba540d32cfd8ee648d",
+ "reference": "cdb32f509ef2e70e447beaba540d32cfd8ee648d",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "yajra/laravel-datatables-oracle": "^13.0"
+ },
+ "require-dev": {
+ "larastan/larastan": "^3.1",
+ "laravel/pint": "^1.21",
+ "livewire/livewire": "^3.4|^4.0",
+ "orchestra/testbench": "^11",
+ "rector/rector": "^2.0"
+ },
+ "suggest": {
+ "laravel/livewire": "Required for Livewire layout support."
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Yajra\\DataTables\\HtmlServiceProvider"
+ ]
+ },
+ "branch-alias": {
+ "dev-master": "13.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Yajra\\DataTables\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Arjay Angeles",
+ "email": "aqangeles@gmail.com"
+ }
+ ],
+ "description": "Laravel DataTables HTML builder plugin",
+ "keywords": [
+ "JS",
+ "datatables",
+ "html",
+ "jquery",
+ "laravel",
+ "yajra"
+ ],
+ "support": {
+ "issues": "https://github.com/yajra/laravel-datatables-html/issues",
+ "source": "https://github.com/yajra/laravel-datatables-html/tree/v13.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.paypal.me/yajra",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/yajra",
+ "type": "github"
+ },
+ {
+ "url": "https://www.patreon.com/yajra",
+ "type": "patreon"
+ }
+ ],
+ "time": "2026-03-18T03:24:28+00:00"
+ },
+ {
+ "name": "yajra/laravel-datatables-oracle",
+ "version": "v13.1.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/yajra/laravel-datatables.git",
+ "reference": "ded9345b7c00c85ce0fe0cea40e24e61c8489e10"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/yajra/laravel-datatables/zipball/ded9345b7c00c85ce0fe0cea40e24e61c8489e10",
+ "reference": "ded9345b7c00c85ce0fe0cea40e24e61c8489e10",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/database": "^13",
+ "illuminate/filesystem": "^13",
+ "illuminate/http": "^13",
+ "illuminate/support": "^13",
+ "illuminate/view": "^13",
+ "php": "^8.3"
+ },
+ "require-dev": {
+ "algolia/algoliasearch-client-php": "^3.4.1",
+ "larastan/larastan": "^3.1.0",
+ "laravel/pint": "^1.14",
+ "laravel/scout": "^10.8.3",
+ "meilisearch/meilisearch-php": "^1.6.1",
+ "orchestra/testbench": "^11",
+ "rector/rector": "^2.0"
+ },
+ "suggest": {
+ "yajra/laravel-datatables-buttons": "Plugin for server-side exporting of dataTables.",
+ "yajra/laravel-datatables-editor": "Plugin to use DataTables Editor (requires a license).",
+ "yajra/laravel-datatables-export": "Plugin for server-side exporting using livewire and queue worker.",
+ "yajra/laravel-datatables-fractal": "Plugin for server-side response using Fractal.",
+ "yajra/laravel-datatables-html": "Plugin for server-side HTML builder of dataTables."
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "aliases": {
+ "DataTables": "Yajra\\DataTables\\Facades\\DataTables"
+ },
+ "providers": [
+ "Yajra\\DataTables\\DataTablesServiceProvider"
+ ]
+ },
+ "branch-alias": {
+ "dev-master": "13.x-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/helper.php"
+ ],
+ "psr-4": {
+ "Yajra\\DataTables\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Arjay Angeles",
+ "email": "aqangeles@gmail.com"
+ }
+ ],
+ "description": "jQuery DataTables API for Laravel",
+ "keywords": [
+ "datatables",
+ "jquery",
+ "laravel",
+ "yajra"
+ ],
+ "support": {
+ "issues": "https://github.com/yajra/laravel-datatables/issues",
+ "source": "https://github.com/yajra/laravel-datatables/tree/v13.1.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sponsors/yajra",
+ "type": "github"
+ }
+ ],
+ "time": "2026-05-19T03:34:29+00:00"
+ }
+ ],
+ "packages-dev": [
+ {
+ "name": "barryvdh/laravel-debugbar",
+ "version": "v4.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/fruitcake/laravel-debugbar.git",
+ "reference": "3d76ea8d78b82225b92789de65fc630c1cd8e80c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/fruitcake/laravel-debugbar/zipball/3d76ea8d78b82225b92789de65fc630c1cd8e80c",
+ "reference": "3d76ea8d78b82225b92789de65fc630c1cd8e80c",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/routing": "^11|^12|^13.0",
+ "illuminate/session": "^11|^12|^13.0",
+ "illuminate/support": "^11|^12|^13.0",
+ "php": "^8.2",
+ "php-debugbar/php-debugbar": "^3.7.2",
+ "php-debugbar/symfony-bridge": "^1.1"
+ },
+ "require-dev": {
+ "larastan/larastan": "^3",
+ "laravel/octane": "^2",
+ "laravel/pennant": "^1",
+ "laravel/pint": "^1",
+ "laravel/telescope": "^5.16",
+ "livewire/livewire": "^3.7|^4",
+ "mockery/mockery": "^1.3.3",
+ "orchestra/testbench-dusk": "^9|^10|^11",
+ "php-debugbar/twig-bridge": "^2.0",
+ "phpstan/phpstan-phpunit": "^2",
+ "phpstan/phpstan-strict-rules": "^2.0",
+ "phpunit/phpunit": "^11",
+ "shipmonk/phpstan-rules": "^4.3"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "aliases": {
+ "Debugbar": "Fruitcake\\LaravelDebugbar\\Facades\\Debugbar"
+ },
+ "providers": [
+ "Fruitcake\\LaravelDebugbar\\ServiceProvider"
+ ]
+ },
+ "branch-alias": {
+ "dev-master": "4.2-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/helpers.php"
+ ],
+ "psr-4": {
+ "Fruitcake\\LaravelDebugbar\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fruitcake",
+ "homepage": "https://fruitcake.nl"
+ },
+ {
+ "name": "Barry vd. Heuvel",
+ "email": "barryvdh@gmail.com"
+ }
+ ],
+ "description": "PHP Debugbar integration for Laravel",
+ "keywords": [
+ "barryvdh",
+ "debug",
+ "debugbar",
+ "dev",
+ "laravel",
+ "profiler",
+ "webprofiler"
+ ],
+ "support": {
+ "issues": "https://github.com/fruitcake/laravel-debugbar/issues",
+ "source": "https://github.com/fruitcake/laravel-debugbar/tree/v4.3.0"
+ },
+ "funding": [
+ {
+ "url": "https://fruitcake.nl",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/barryvdh",
+ "type": "github"
+ }
+ ],
+ "time": "2026-06-04T07:54:01+00:00"
+ },
+ {
+ "name": "barryvdh/laravel-ide-helper",
+ "version": "v3.7.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/barryvdh/laravel-ide-helper.git",
+ "reference": "ad7e37676f1ff985d55ef1b6b96a0c0a40f2609a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/barryvdh/laravel-ide-helper/zipball/ad7e37676f1ff985d55ef1b6b96a0c0a40f2609a",
+ "reference": "ad7e37676f1ff985d55ef1b6b96a0c0a40f2609a",
+ "shasum": ""
+ },
+ "require": {
+ "barryvdh/reflection-docblock": "^2.4",
+ "composer/class-map-generator": "^1.0",
+ "ext-json": "*",
+ "illuminate/console": "^11.15 || ^12 || ^13.0",
+ "illuminate/database": "^11.15 || ^12 || ^13.0",
+ "illuminate/filesystem": "^11.15 || ^12 || ^13.0",
+ "illuminate/support": "^11.15 || ^12 || ^13.0",
+ "php": "^8.2"
+ },
+ "require-dev": {
+ "ext-pdo_sqlite": "*",
+ "friendsofphp/php-cs-fixer": "^3",
+ "illuminate/config": "^11.15 || ^12 || ^13.0",
+ "illuminate/view": "^11.15 || ^12 || ^13.0",
+ "larastan/larastan": "^3.1",
+ "mockery/mockery": "^1.4",
+ "orchestra/testbench": "^9.2 || ^10 || ^11.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpunit/phpunit": "^10.5 || ^11.5.3 || ^12.5.12",
+ "spatie/phpunit-snapshot-assertions": "^4 || ^5",
+ "vlucas/phpdotenv": "^5"
+ },
+ "suggest": {
+ "illuminate/events": "Required for automatic helper generation (^6|^7|^8|^9|^10|^11)."
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Barryvdh\\LaravelIdeHelper\\IdeHelperServiceProvider"
+ ]
+ },
+ "branch-alias": {
+ "dev-master": "3.6-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Barryvdh\\LaravelIdeHelper\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Barry vd. Heuvel",
+ "email": "barryvdh@gmail.com"
+ }
+ ],
+ "description": "Laravel IDE Helper, generates correct PHPDocs for all Facade classes, to improve auto-completion.",
+ "keywords": [
+ "autocomplete",
+ "codeintel",
+ "dev",
+ "helper",
+ "ide",
+ "laravel",
+ "netbeans",
+ "phpdoc",
+ "phpstorm",
+ "sublime"
+ ],
+ "support": {
+ "issues": "https://github.com/barryvdh/laravel-ide-helper/issues",
+ "source": "https://github.com/barryvdh/laravel-ide-helper/tree/v3.7.0"
+ },
+ "funding": [
+ {
+ "url": "https://fruitcake.nl",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/barryvdh",
+ "type": "github"
+ }
+ ],
+ "time": "2026-03-17T14:12:51+00:00"
+ },
+ {
+ "name": "barryvdh/reflection-docblock",
+ "version": "v2.4.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/barryvdh/ReflectionDocBlock.git",
+ "reference": "4f5ba70c30c81f2ce03a16a9965832cfcc31ed3b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/barryvdh/ReflectionDocBlock/zipball/4f5ba70c30c81f2ce03a16a9965832cfcc31ed3b",
+ "reference": "4f5ba70c30c81f2ce03a16a9965832cfcc31ed3b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^8.5.14|^9"
+ },
+ "suggest": {
+ "dflydev/markdown": "~1.0",
+ "erusev/parsedown": "~1.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-0": {
+ "Barryvdh": [
+ "src/"
+ ]
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mike van Riel",
+ "email": "mike.vanriel@naenius.com"
+ }
+ ],
+ "support": {
+ "source": "https://github.com/barryvdh/ReflectionDocBlock/tree/v2.4.1"
+ },
+ "time": "2026-03-05T20:09:01+00:00"
+ },
+ {
+ "name": "brianium/paratest",
+ "version": "v7.20.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/paratestphp/paratest.git",
+ "reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/paratestphp/paratest/zipball/81c80677c9ec0ed4ef16b246167f11dec81a6e3d",
+ "reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-pcre": "*",
+ "ext-reflection": "*",
+ "ext-simplexml": "*",
+ "fidry/cpu-core-counter": "^1.3.0",
+ "jean85/pretty-package-versions": "^2.1.1",
+ "php": "~8.3.0 || ~8.4.0 || ~8.5.0",
+ "phpunit/php-code-coverage": "^12.5.3 || ^13.0.1",
+ "phpunit/php-file-iterator": "^6.0.1 || ^7",
+ "phpunit/php-timer": "^8 || ^9",
+ "phpunit/phpunit": "^12.5.14 || ^13.0.5",
+ "sebastian/environment": "^8.0.3 || ^9",
+ "symfony/console": "^7.4.7 || ^8.0.7",
+ "symfony/process": "^7.4.5 || ^8.0.5"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^14.0.0",
+ "ext-pcntl": "*",
+ "ext-pcov": "*",
+ "ext-posix": "*",
+ "phpstan/phpstan": "^2.1.44",
+ "phpstan/phpstan-deprecation-rules": "^2.0.4",
+ "phpstan/phpstan-phpunit": "^2.0.16",
+ "phpstan/phpstan-strict-rules": "^2.0.10",
+ "symfony/filesystem": "^7.4.6 || ^8.0.6"
+ },
+ "bin": [
+ "bin/paratest",
+ "bin/paratest_for_phpstorm"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "ParaTest\\": [
+ "src/"
+ ]
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Brian Scaturro",
+ "email": "scaturrob@gmail.com",
+ "role": "Developer"
+ },
+ {
+ "name": "Filippo Tessarotto",
+ "email": "zoeslam@gmail.com",
+ "role": "Developer"
+ }
+ ],
+ "description": "Parallel testing for PHP",
+ "homepage": "https://github.com/paratestphp/paratest",
+ "keywords": [
+ "concurrent",
+ "parallel",
+ "phpunit",
+ "testing"
+ ],
+ "support": {
+ "issues": "https://github.com/paratestphp/paratest/issues",
+ "source": "https://github.com/paratestphp/paratest/tree/v7.20.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sponsors/Slamdunk",
+ "type": "github"
+ },
+ {
+ "url": "https://paypal.me/filippotessarotto",
+ "type": "paypal"
+ }
+ ],
+ "time": "2026-03-29T15:46:14+00:00"
+ },
+ {
+ "name": "composer/class-map-generator",
+ "version": "1.7.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/class-map-generator.git",
+ "reference": "86d8208fc3c649a3a999daf1a63c25201be2990f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/class-map-generator/zipball/86d8208fc3c649a3a999daf1a63c25201be2990f",
+ "reference": "86d8208fc3c649a3a999daf1a63c25201be2990f",
+ "shasum": ""
+ },
+ "require": {
+ "composer/pcre": "^2.1 || ^3.1",
+ "php": "^7.2 || ^8.0",
+ "symfony/finder": "^4.4 || ^5.3 || ^6 || ^7 || ^8"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.12 || ^2",
+ "phpstan/phpstan-deprecation-rules": "^1 || ^2",
+ "phpstan/phpstan-phpunit": "^1 || ^2",
+ "phpstan/phpstan-strict-rules": "^1.1 || ^2",
+ "phpunit/phpunit": "^8",
+ "symfony/filesystem": "^5.4 || ^6 || ^7 || ^8"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\ClassMapGenerator\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "https://seld.be"
+ }
+ ],
+ "description": "Utilities to scan PHP code and generate class maps.",
+ "keywords": [
+ "classmap"
+ ],
+ "support": {
+ "issues": "https://github.com/composer/class-map-generator/issues",
+ "source": "https://github.com/composer/class-map-generator/tree/1.7.3"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ }
+ ],
+ "time": "2026-05-05T09:17:07+00:00"
+ },
+ {
+ "name": "composer/xdebug-handler",
+ "version": "3.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/xdebug-handler.git",
+ "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/6c1925561632e83d60a44492e0b344cf48ab85ef",
+ "reference": "6c1925561632e83d60a44492e0b344cf48ab85ef",
+ "shasum": ""
+ },
+ "require": {
+ "composer/pcre": "^1 || ^2 || ^3",
+ "php": "^7.2.5 || ^8.0",
+ "psr/log": "^1 || ^2 || ^3"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.0",
+ "phpstan/phpstan-strict-rules": "^1.1",
+ "phpunit/phpunit": "^8.5 || ^9.6 || ^10.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Composer\\XdebugHandler\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "John Stevenson",
+ "email": "john-stevenson@blueyonder.co.uk"
+ }
+ ],
+ "description": "Restarts a process without Xdebug.",
+ "keywords": [
+ "Xdebug",
+ "performance"
+ ],
+ "support": {
+ "irc": "ircs://irc.libera.chat:6697/composer",
+ "issues": "https://github.com/composer/xdebug-handler/issues",
+ "source": "https://github.com/composer/xdebug-handler/tree/3.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2024-05-06T16:37:16+00:00"
+ },
+ {
+ "name": "doctrine/deprecations",
+ "version": "1.1.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/deprecations.git",
+ "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/deprecations/zipball/d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
+ "reference": "d4fe3e6fd9bb9e72557a19674f44d8ac7db4c6ca",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "conflict": {
+ "phpunit/phpunit": "<=7.5 || >=14"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^9 || ^12 || ^14",
+ "phpstan/phpstan": "1.4.10 || 2.1.30",
+ "phpstan/phpstan-phpunit": "^1.0 || ^2",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12.4 || ^13.0",
+ "psr/log": "^1 || ^2 || ^3"
+ },
+ "suggest": {
+ "psr/log": "Allows logging deprecations via PSR-3 logger implementation"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Deprecations\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.",
+ "homepage": "https://www.doctrine-project.org/",
+ "support": {
+ "issues": "https://github.com/doctrine/deprecations/issues",
+ "source": "https://github.com/doctrine/deprecations/tree/1.1.6"
+ },
+ "time": "2026-02-07T07:09:04+00:00"
+ },
+ {
+ "name": "fakerphp/faker",
+ "version": "v1.24.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/FakerPHP/Faker.git",
+ "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5",
+ "reference": "e0ee18eb1e6dc3cda3ce9fd97e5a0689a88a64b5",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0",
+ "psr/container": "^1.0 || ^2.0",
+ "symfony/deprecation-contracts": "^2.2 || ^3.0"
+ },
+ "conflict": {
+ "fzaninotto/faker": "*"
+ },
+ "require-dev": {
+ "bamarni/composer-bin-plugin": "^1.4.1",
+ "doctrine/persistence": "^1.3 || ^2.0",
+ "ext-intl": "*",
+ "phpunit/phpunit": "^9.5.26",
+ "symfony/phpunit-bridge": "^5.4.16"
+ },
+ "suggest": {
+ "doctrine/orm": "Required to use Faker\\ORM\\Doctrine",
+ "ext-curl": "Required by Faker\\Provider\\Image to download images.",
+ "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.",
+ "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.",
+ "ext-mbstring": "Required for multibyte Unicode string functionality."
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Faker\\": "src/Faker/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "François Zaninotto"
+ }
+ ],
+ "description": "Faker is a PHP library that generates fake data for you.",
+ "keywords": [
+ "data",
+ "faker",
+ "fixtures"
+ ],
+ "support": {
+ "issues": "https://github.com/FakerPHP/Faker/issues",
+ "source": "https://github.com/FakerPHP/Faker/tree/v1.24.1"
+ },
+ "time": "2024-11-21T13:46:39+00:00"
+ },
+ {
+ "name": "fidry/cpu-core-counter",
+ "version": "1.3.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/theofidry/cpu-core-counter.git",
+ "reference": "db9508f7b1474469d9d3c53b86f817e344732678"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/db9508f7b1474469d9d3c53b86f817e344732678",
+ "reference": "db9508f7b1474469d9d3c53b86f817e344732678",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "require-dev": {
+ "fidry/makefile": "^0.2.0",
+ "fidry/php-cs-fixer-config": "^1.1.2",
+ "phpstan/extension-installer": "^1.2.0",
+ "phpstan/phpstan": "^2.0",
+ "phpstan/phpstan-deprecation-rules": "^2.0.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpstan/phpstan-strict-rules": "^2.0",
+ "phpunit/phpunit": "^8.5.31 || ^9.5.26",
+ "webmozarts/strict-phpunit": "^7.5"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Fidry\\CpuCoreCounter\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Théo FIDRY",
+ "email": "theo.fidry@gmail.com"
+ }
+ ],
+ "description": "Tiny utility to get the number of CPU cores.",
+ "keywords": [
+ "CPU",
+ "core"
+ ],
+ "support": {
+ "issues": "https://github.com/theofidry/cpu-core-counter/issues",
+ "source": "https://github.com/theofidry/cpu-core-counter/tree/1.3.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theofidry",
+ "type": "github"
+ }
+ ],
+ "time": "2025-08-14T07:29:31+00:00"
+ },
+ {
+ "name": "filp/whoops",
+ "version": "2.18.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/filp/whoops.git",
+ "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/filp/whoops/zipball/d2102955e48b9fd9ab24280a7ad12ed552752c4d",
+ "reference": "d2102955e48b9fd9ab24280a7ad12ed552752c4d",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0",
+ "psr/log": "^1.0.1 || ^2.0 || ^3.0"
+ },
+ "require-dev": {
+ "mockery/mockery": "^1.0",
+ "phpunit/phpunit": "^7.5.20 || ^8.5.8 || ^9.3.3",
+ "symfony/var-dumper": "^4.0 || ^5.0"
+ },
+ "suggest": {
+ "symfony/var-dumper": "Pretty print complex values better with var-dumper available",
+ "whoops/soap": "Formats errors as SOAP responses"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.7-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Whoops\\": "src/Whoops/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Filipe Dobreira",
+ "homepage": "https://github.com/filp",
+ "role": "Developer"
+ }
+ ],
+ "description": "php error handling for cool kids",
+ "homepage": "https://filp.github.io/whoops/",
+ "keywords": [
+ "error",
+ "exception",
+ "handling",
+ "library",
+ "throwable",
+ "whoops"
+ ],
+ "support": {
+ "issues": "https://github.com/filp/whoops/issues",
+ "source": "https://github.com/filp/whoops/tree/2.18.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/denis-sokolov",
+ "type": "github"
+ }
+ ],
+ "time": "2025-08-08T12:00:00+00:00"
+ },
+ {
+ "name": "hamcrest/hamcrest-php",
+ "version": "v2.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/hamcrest/hamcrest-php.git",
+ "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/hamcrest/hamcrest-php/zipball/f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487",
+ "reference": "f8b1c0173b22fa6ec77a81fe63e5b01eba7e6487",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4|^8.0"
+ },
+ "replace": {
+ "cordoval/hamcrest-php": "*",
+ "davedevelopment/hamcrest-php": "*",
+ "kodova/hamcrest-php": "*"
+ },
+ "require-dev": {
+ "phpunit/php-file-iterator": "^1.4 || ^2.0 || ^3.0",
+ "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0 || ^8.0 || ^9.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "hamcrest"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "description": "This is the PHP port of Hamcrest Matchers",
+ "keywords": [
+ "test"
+ ],
+ "support": {
+ "issues": "https://github.com/hamcrest/hamcrest-php/issues",
+ "source": "https://github.com/hamcrest/hamcrest-php/tree/v2.1.1"
+ },
+ "time": "2025-04-30T06:54:44+00:00"
+ },
+ {
+ "name": "jean85/pretty-package-versions",
+ "version": "2.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/Jean85/pretty-package-versions.git",
+ "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a",
+ "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a",
+ "shasum": ""
+ },
+ "require": {
+ "composer-runtime-api": "^2.1.0",
+ "php": "^7.4|^8.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.2",
+ "jean85/composer-provided-replaced-stub-package": "^1.0",
+ "phpstan/phpstan": "^2.0",
+ "phpunit/phpunit": "^7.5|^8.5|^9.6",
+ "rector/rector": "^2.0",
+ "vimeo/psalm": "^4.3 || ^5.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Jean85\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Alessandro Lai",
+ "email": "alessandro.lai85@gmail.com"
+ }
+ ],
+ "description": "A library to get pretty versions strings of installed dependencies",
+ "keywords": [
+ "composer",
+ "package",
+ "release",
+ "versions"
+ ],
+ "support": {
+ "issues": "https://github.com/Jean85/pretty-package-versions/issues",
+ "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1"
+ },
+ "time": "2025-03-19T14:43:43+00:00"
+ },
+ {
+ "name": "laravel/boost",
+ "version": "v2.4.9",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/boost.git",
+ "reference": "f0359e55f6c3782023a35baf1d3df817053d69e8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/boost/zipball/f0359e55f6c3782023a35baf1d3df817053d69e8",
+ "reference": "f0359e55f6c3782023a35baf1d3df817053d69e8",
+ "shasum": ""
+ },
+ "require": {
+ "guzzlehttp/guzzle": "^7.9",
+ "illuminate/console": "^11.45.3|^12.41.1|^13.0",
+ "illuminate/contracts": "^11.45.3|^12.41.1|^13.0",
+ "illuminate/routing": "^11.45.3|^12.41.1|^13.0",
+ "illuminate/support": "^11.45.3|^12.41.1|^13.0",
+ "laravel/mcp": "^0.7.1",
+ "laravel/prompts": "^0.3.10",
+ "laravel/roster": "^0.5.0",
+ "php": "^8.2"
+ },
+ "require-dev": {
+ "laravel/pint": "^1.27.0",
+ "mockery/mockery": "^1.6.12",
+ "orchestra/testbench": "^9.15.0|^10.6|^11.0",
+ "pestphp/pest": "^2.36.0|^3.8.4|^4.1.5",
+ "phpstan/phpstan": "^2.1.27",
+ "rector/rector": "^2.1"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Laravel\\Boost\\BoostServiceProvider"
+ ]
+ },
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laravel\\Boost\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Laravel Boost accelerates AI-assisted development by providing the essential context and structure that AI needs to generate high-quality, Laravel-specific code.",
+ "homepage": "https://github.com/laravel/boost",
+ "keywords": [
+ "ai",
+ "dev",
+ "laravel"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/boost/issues",
+ "source": "https://github.com/laravel/boost"
+ },
+ "time": "2026-06-04T10:33:57+00:00"
+ },
+ {
+ "name": "laravel/mcp",
+ "version": "v0.7.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/mcp.git",
+ "reference": "08962a276357f89164f78b38407c08187ab26cfe"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/mcp/zipball/08962a276357f89164f78b38407c08187ab26cfe",
+ "reference": "08962a276357f89164f78b38407c08187ab26cfe",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "ext-mbstring": "*",
+ "illuminate/console": "^11.45.3|^12.41.1|^13.0",
+ "illuminate/container": "^11.45.3|^12.41.1|^13.0",
+ "illuminate/contracts": "^11.45.3|^12.41.1|^13.0",
+ "illuminate/http": "^11.45.3|^12.41.1|^13.0",
+ "illuminate/json-schema": "^12.41.1|^13.0",
+ "illuminate/routing": "^11.45.3|^12.41.1|^13.0",
+ "illuminate/support": "^11.45.3|^12.41.1|^13.0",
+ "illuminate/validation": "^11.45.3|^12.41.1|^13.0",
+ "php": "^8.2",
+ "symfony/process": "^7.4.5|^8.0.5"
+ },
+ "require-dev": {
+ "laravel/pint": "^1.20",
+ "orchestra/testbench": "^9.15|^10.8|^11.0",
+ "pestphp/pest": "^3.8.5|^4.3.2",
+ "phpstan/phpstan": "^2.1.27",
+ "rector/rector": "^2.2.4"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "aliases": {
+ "Mcp": "Laravel\\Mcp\\Server\\Facades\\Mcp"
+ },
+ "providers": [
+ "Laravel\\Mcp\\Server\\McpServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laravel\\Mcp\\": "src/",
+ "Laravel\\Mcp\\Server\\": "src/Server/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ }
+ ],
+ "description": "Rapidly build MCP servers for your Laravel applications.",
+ "homepage": "https://github.com/laravel/mcp",
+ "keywords": [
+ "laravel",
+ "mcp"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/mcp/issues",
+ "source": "https://github.com/laravel/mcp"
+ },
+ "time": "2026-05-22T11:45:29+00:00"
+ },
+ {
+ "name": "laravel/pail",
+ "version": "v1.2.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/pail.git",
+ "reference": "2f7d27dada8effc48b8c424445a69cca7007daaa"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/pail/zipball/2f7d27dada8effc48b8c424445a69cca7007daaa",
+ "reference": "2f7d27dada8effc48b8c424445a69cca7007daaa",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "illuminate/console": "^10.24|^11.0|^12.0|^13.0",
+ "illuminate/contracts": "^10.24|^11.0|^12.0|^13.0",
+ "illuminate/log": "^10.24|^11.0|^12.0|^13.0",
+ "illuminate/process": "^10.24|^11.0|^12.0|^13.0",
+ "illuminate/support": "^10.24|^11.0|^12.0|^13.0",
+ "nunomaduro/termwind": "^1.15|^2.0",
+ "php": "^8.2",
+ "symfony/console": "^6.0|^7.0|^8.0"
+ },
+ "require-dev": {
+ "laravel/framework": "^10.24|^11.0|^12.0|^13.0",
+ "laravel/pint": "^1.13",
+ "orchestra/testbench-core": "^8.13|^9.17|^10.8|^11.0",
+ "pestphp/pest": "^2.20|^3.0|^4.0",
+ "pestphp/pest-plugin-type-coverage": "^2.3|^3.0|^4.0",
+ "phpstan/phpstan": "^1.12.27",
+ "symfony/var-dumper": "^6.3|^7.0|^8.0",
+ "symfony/yaml": "^6.3|^7.0|^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Laravel\\Pail\\PailServiceProvider"
+ ]
+ },
+ "branch-alias": {
+ "dev-main": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laravel\\Pail\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Taylor Otwell",
+ "email": "taylor@laravel.com"
+ },
+ {
+ "name": "Nuno Maduro",
+ "email": "enunomaduro@gmail.com"
+ }
+ ],
+ "description": "Easily delve into your Laravel application's log files directly from the command line.",
+ "homepage": "https://github.com/laravel/pail",
+ "keywords": [
+ "dev",
+ "laravel",
+ "logs",
+ "php",
+ "tail"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/pail/issues",
+ "source": "https://github.com/laravel/pail"
+ },
+ "time": "2026-05-20T22:24:57+00:00"
+ },
+ {
+ "name": "laravel/pint",
+ "version": "v1.29.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/pint.git",
+ "reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/pint/zipball/0770e9b7fafd50d4586881d456d6eb41c9247a80",
+ "reference": "0770e9b7fafd50d4586881d456d6eb41c9247a80",
+ "shasum": ""
+ },
+ "require": {
+ "ext-json": "*",
+ "ext-mbstring": "*",
+ "ext-tokenizer": "*",
+ "ext-xml": "*",
+ "php": "^8.2.0"
+ },
+ "require-dev": {
+ "friendsofphp/php-cs-fixer": "^3.95.1",
+ "illuminate/view": "^12.56.0",
+ "larastan/larastan": "^3.9.6",
+ "laravel-zero/framework": "^12.1.0",
+ "mockery/mockery": "^1.6.12",
+ "nunomaduro/termwind": "^2.4.0",
+ "pestphp/pest": "^3.8.6",
+ "shipfastlabs/agent-detector": "^1.1.3"
+ },
+ "bin": [
+ "builds/pint"
+ ],
+ "type": "project",
+ "autoload": {
+ "psr-4": {
+ "App\\": "app/",
+ "Database\\Seeders\\": "database/seeders/",
+ "Database\\Factories\\": "database/factories/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nuno Maduro",
+ "email": "enunomaduro@gmail.com"
+ }
+ ],
+ "description": "An opinionated code formatter for PHP.",
+ "homepage": "https://laravel.com",
+ "keywords": [
+ "dev",
+ "format",
+ "formatter",
+ "lint",
+ "linter",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/pint/issues",
+ "source": "https://github.com/laravel/pint"
+ },
+ "time": "2026-04-20T15:26:14+00:00"
+ },
+ {
+ "name": "laravel/roster",
+ "version": "v0.5.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/laravel/roster.git",
+ "reference": "5089de7615f72f78e831590ff9d0435fed0102bb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/laravel/roster/zipball/5089de7615f72f78e831590ff9d0435fed0102bb",
+ "reference": "5089de7615f72f78e831590ff9d0435fed0102bb",
+ "shasum": ""
+ },
+ "require": {
+ "illuminate/console": "^11.0|^12.0|^13.0",
+ "illuminate/contracts": "^11.0|^12.0|^13.0",
+ "illuminate/routing": "^11.0|^12.0|^13.0",
+ "illuminate/support": "^11.0|^12.0|^13.0",
+ "php": "^8.2",
+ "symfony/yaml": "^7.2|^8.0"
+ },
+ "require-dev": {
+ "laravel/pint": "^1.14",
+ "mockery/mockery": "^1.6",
+ "orchestra/testbench": "^9.0|^10.0|^11.0",
+ "pestphp/pest": "^3.0|^4.1",
+ "phpstan/phpstan": "^2.0"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "Laravel\\Roster\\RosterServiceProvider"
+ ]
+ },
+ "branch-alias": {
+ "dev-master": "1.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Laravel\\Roster\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Detect packages & approaches in use within a Laravel project",
+ "homepage": "https://github.com/laravel/roster",
+ "keywords": [
+ "dev",
+ "laravel"
+ ],
+ "support": {
+ "issues": "https://github.com/laravel/roster/issues",
+ "source": "https://github.com/laravel/roster"
+ },
+ "time": "2026-03-05T07:58:43+00:00"
+ },
+ {
+ "name": "mockery/mockery",
+ "version": "1.6.12",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/mockery/mockery.git",
+ "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/mockery/mockery/zipball/1f4efdd7d3beafe9807b08156dfcb176d18f1699",
+ "reference": "1f4efdd7d3beafe9807b08156dfcb176d18f1699",
+ "shasum": ""
+ },
+ "require": {
+ "hamcrest/hamcrest-php": "^2.0.1",
+ "lib-pcre": ">=7.0",
+ "php": ">=7.3"
+ },
+ "conflict": {
+ "phpunit/phpunit": "<8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^8.5 || ^9.6.17",
+ "symplify/easy-coding-standard": "^12.1.14"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "library/helpers.php",
+ "library/Mockery.php"
+ ],
+ "psr-4": {
+ "Mockery\\": "library/Mockery"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Pádraic Brady",
+ "email": "padraic.brady@gmail.com",
+ "homepage": "https://github.com/padraic",
+ "role": "Author"
+ },
+ {
+ "name": "Dave Marshall",
+ "email": "dave.marshall@atstsolutions.co.uk",
+ "homepage": "https://davedevelopment.co.uk",
+ "role": "Developer"
+ },
+ {
+ "name": "Nathanael Esayeas",
+ "email": "nathanael.esayeas@protonmail.com",
+ "homepage": "https://github.com/ghostwriter",
+ "role": "Lead Developer"
+ }
+ ],
+ "description": "Mockery is a simple yet flexible PHP mock object framework",
+ "homepage": "https://github.com/mockery/mockery",
+ "keywords": [
+ "BDD",
+ "TDD",
+ "library",
+ "mock",
+ "mock objects",
+ "mockery",
+ "stub",
+ "test",
+ "test double",
+ "testing"
+ ],
+ "support": {
+ "docs": "https://docs.mockery.io/",
+ "issues": "https://github.com/mockery/mockery/issues",
+ "rss": "https://github.com/mockery/mockery/releases.atom",
+ "security": "https://github.com/mockery/mockery/security/advisories",
+ "source": "https://github.com/mockery/mockery"
+ },
+ "time": "2024-05-16T03:13:13+00:00"
+ },
+ {
+ "name": "myclabs/deep-copy",
+ "version": "1.13.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/myclabs/DeepCopy.git",
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a",
+ "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "conflict": {
+ "doctrine/collections": "<1.6.8",
+ "doctrine/common": "<2.13.3 || >=3 <3.2.2"
+ },
+ "require-dev": {
+ "doctrine/collections": "^1.6.8",
+ "doctrine/common": "^2.13.3 || ^3.2.2",
+ "phpspec/prophecy": "^1.10",
+ "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "src/DeepCopy/deep_copy.php"
+ ],
+ "psr-4": {
+ "DeepCopy\\": "src/DeepCopy/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "Create deep copies (clones) of your objects",
+ "keywords": [
+ "clone",
+ "copy",
+ "duplicate",
+ "object",
+ "object graph"
+ ],
+ "support": {
+ "issues": "https://github.com/myclabs/DeepCopy/issues",
+ "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4"
+ },
+ "funding": [
+ {
+ "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-01T08:46:24+00:00"
+ },
+ {
+ "name": "nunomaduro/collision",
+ "version": "v8.9.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/nunomaduro/collision.git",
+ "reference": "716af8f95a470e9094cfca09ed897b023be191a5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/nunomaduro/collision/zipball/716af8f95a470e9094cfca09ed897b023be191a5",
+ "reference": "716af8f95a470e9094cfca09ed897b023be191a5",
+ "shasum": ""
+ },
+ "require": {
+ "filp/whoops": "^2.18.4",
+ "nunomaduro/termwind": "^2.4.0",
+ "php": "^8.2.0",
+ "symfony/console": "^7.4.8 || ^8.0.8"
+ },
+ "conflict": {
+ "laravel/framework": "<11.48.0 || >=14.0.0",
+ "phpunit/phpunit": "<11.5.50 || >=14.0.0"
+ },
+ "require-dev": {
+ "brianium/paratest": "^7.8.5",
+ "larastan/larastan": "^3.9.6",
+ "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.5.0",
+ "laravel/pint": "^1.29.1",
+ "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.2.1",
+ "pestphp/pest": "^3.8.5 || ^4.4.3 || ^5.0.0",
+ "sebastian/environment": "^7.2.1 || ^8.0.4 || ^9.3.0"
+ },
+ "type": "library",
+ "extra": {
+ "laravel": {
+ "providers": [
+ "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider"
+ ]
+ },
+ "branch-alias": {
+ "dev-8.x": "8.x-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "./src/Adapters/Phpunit/Autoload.php"
+ ],
+ "psr-4": {
+ "NunoMaduro\\Collision\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nuno Maduro",
+ "email": "enunomaduro@gmail.com"
+ }
+ ],
+ "description": "Cli error handling for console/command-line PHP applications.",
+ "keywords": [
+ "artisan",
+ "cli",
+ "command-line",
+ "console",
+ "dev",
+ "error",
+ "handling",
+ "laravel",
+ "laravel-zero",
+ "php",
+ "symfony"
+ ],
+ "support": {
+ "issues": "https://github.com/nunomaduro/collision/issues",
+ "source": "https://github.com/nunomaduro/collision"
+ },
+ "funding": [
+ {
+ "url": "https://www.paypal.com/paypalme/enunomaduro",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/nunomaduro",
+ "type": "github"
+ },
+ {
+ "url": "https://www.patreon.com/nunomaduro",
+ "type": "patreon"
+ }
+ ],
+ "time": "2026-04-21T14:04:20+00:00"
+ },
+ {
+ "name": "pestphp/pest",
+ "version": "v4.7.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/pestphp/pest.git",
+ "reference": "40b88b62ef8a7c6fcae5fc28f1fa747f601c131b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/pestphp/pest/zipball/40b88b62ef8a7c6fcae5fc28f1fa747f601c131b",
+ "reference": "40b88b62ef8a7c6fcae5fc28f1fa747f601c131b",
+ "shasum": ""
+ },
+ "require": {
+ "brianium/paratest": "^7.20.0",
+ "composer/xdebug-handler": "^3.0.5",
+ "nunomaduro/collision": "^8.9.4",
+ "nunomaduro/termwind": "^2.4.0",
+ "pestphp/pest-plugin": "^4.0.0",
+ "pestphp/pest-plugin-arch": "^4.0.2",
+ "pestphp/pest-plugin-mutate": "^4.0.1",
+ "pestphp/pest-plugin-profanity": "^4.2.1",
+ "php": "^8.3.0",
+ "phpunit/phpunit": "^12.5.28",
+ "symfony/process": "^7.4.13|^8.1.0"
+ },
+ "conflict": {
+ "filp/whoops": "<2.18.3",
+ "phpunit/phpunit": ">12.5.28",
+ "sebastian/exporter": "<7.0.0",
+ "webmozart/assert": "<1.11.0"
+ },
+ "require-dev": {
+ "mrpunyapal/peststan": "^0.2.10",
+ "pestphp/pest-dev-tools": "^4.1.0",
+ "pestphp/pest-plugin-browser": "^4.3.1",
+ "pestphp/pest-plugin-type-coverage": "^4.0.4",
+ "psy/psysh": "^0.12.23"
+ },
+ "bin": [
+ "bin/pest"
+ ],
+ "type": "library",
+ "extra": {
+ "pest": {
+ "plugins": [
+ "Pest\\Mutate\\Plugins\\Mutate",
+ "Pest\\Plugins\\Configuration",
+ "Pest\\Plugins\\Bail",
+ "Pest\\Plugins\\Cache",
+ "Pest\\Plugins\\Coverage",
+ "Pest\\Plugins\\Init",
+ "Pest\\Plugins\\Environment",
+ "Pest\\Plugins\\Help",
+ "Pest\\Plugins\\Memory",
+ "Pest\\Plugins\\Only",
+ "Pest\\Plugins\\Printer",
+ "Pest\\Plugins\\ProcessIsolation",
+ "Pest\\Plugins\\Profile",
+ "Pest\\Plugins\\Retry",
+ "Pest\\Plugins\\Snapshot",
+ "Pest\\Plugins\\Verbose",
+ "Pest\\Plugins\\Version",
+ "Pest\\Plugins\\Shard",
+ "Pest\\Plugins\\Tia",
+ "Pest\\Plugins\\Parallel"
+ ]
+ },
+ "phpstan": {
+ "includes": [
+ "extension.neon"
+ ]
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/Functions.php",
+ "src/Pest.php"
+ ],
+ "psr-4": {
+ "Pest\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nuno Maduro",
+ "email": "enunomaduro@gmail.com"
+ }
+ ],
+ "description": "The elegant PHP Testing Framework.",
+ "keywords": [
+ "framework",
+ "pest",
+ "php",
+ "test",
+ "testing",
+ "unit"
+ ],
+ "support": {
+ "issues": "https://github.com/pestphp/pest/issues",
+ "source": "https://github.com/pestphp/pest/tree/v4.7.2"
+ },
+ "funding": [
+ {
+ "url": "https://www.paypal.com/paypalme/enunomaduro",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/nunomaduro",
+ "type": "github"
+ }
+ ],
+ "time": "2026-06-01T06:08:59+00:00"
+ },
+ {
+ "name": "pestphp/pest-plugin",
+ "version": "v4.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/pestphp/pest-plugin.git",
+ "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/9d4b93d7f73d3f9c3189bb22c220fef271cdf568",
+ "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568",
+ "shasum": ""
+ },
+ "require": {
+ "composer-plugin-api": "^2.0.0",
+ "composer-runtime-api": "^2.2.2",
+ "php": "^8.3"
+ },
+ "conflict": {
+ "pestphp/pest": "<4.0.0"
+ },
+ "require-dev": {
+ "composer/composer": "^2.8.10",
+ "pestphp/pest": "^4.0.0",
+ "pestphp/pest-dev-tools": "^4.0.0"
+ },
+ "type": "composer-plugin",
+ "extra": {
+ "class": "Pest\\Plugin\\Manager"
+ },
+ "autoload": {
+ "psr-4": {
+ "Pest\\Plugin\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "The Pest plugin manager",
+ "keywords": [
+ "framework",
+ "manager",
+ "pest",
+ "php",
+ "plugin",
+ "test",
+ "testing",
+ "unit"
+ ],
+ "support": {
+ "source": "https://github.com/pestphp/pest-plugin/tree/v4.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/nunomaduro",
+ "type": "github"
+ },
+ {
+ "url": "https://www.patreon.com/nunomaduro",
+ "type": "patreon"
+ }
+ ],
+ "time": "2025-08-20T12:35:58+00:00"
+ },
+ {
+ "name": "pestphp/pest-plugin-arch",
+ "version": "v4.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/pestphp/pest-plugin-arch.git",
+ "reference": "3fb0d02a91b9da504b139dc7ab2a31efb7c3215c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/3fb0d02a91b9da504b139dc7ab2a31efb7c3215c",
+ "reference": "3fb0d02a91b9da504b139dc7ab2a31efb7c3215c",
+ "shasum": ""
+ },
+ "require": {
+ "pestphp/pest-plugin": "^4.0.0",
+ "php": "^8.3",
+ "ta-tikoma/phpunit-architecture-test": "^0.8.7"
+ },
+ "require-dev": {
+ "pestphp/pest": "^4.4.6",
+ "pestphp/pest-dev-tools": "^4.1.0"
+ },
+ "type": "library",
+ "extra": {
+ "pest": {
+ "plugins": [
+ "Pest\\Arch\\Plugin"
+ ]
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/Autoload.php"
+ ],
+ "psr-4": {
+ "Pest\\Arch\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "The Arch plugin for Pest PHP.",
+ "keywords": [
+ "arch",
+ "architecture",
+ "framework",
+ "pest",
+ "php",
+ "plugin",
+ "test",
+ "testing",
+ "unit"
+ ],
+ "support": {
+ "source": "https://github.com/pestphp/pest-plugin-arch/tree/v4.0.2"
+ },
+ "funding": [
+ {
+ "url": "https://www.paypal.com/paypalme/enunomaduro",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/nunomaduro",
+ "type": "github"
+ }
+ ],
+ "time": "2026-04-10T17:20:19+00:00"
+ },
+ {
+ "name": "pestphp/pest-plugin-laravel",
+ "version": "v4.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/pestphp/pest-plugin-laravel.git",
+ "reference": "3057a36669ff11416cc0dc2b521b3aec58c488d0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/pestphp/pest-plugin-laravel/zipball/3057a36669ff11416cc0dc2b521b3aec58c488d0",
+ "reference": "3057a36669ff11416cc0dc2b521b3aec58c488d0",
+ "shasum": ""
+ },
+ "require": {
+ "laravel/framework": "^11.45.2|^12.52.0|^13.0",
+ "pestphp/pest": "^4.4.1",
+ "php": "^8.3.0"
+ },
+ "require-dev": {
+ "laravel/dusk": "^8.3.6",
+ "orchestra/testbench": "^9.13.0|^10.9.0|^11.0",
+ "pestphp/pest-dev-tools": "^4.1.0"
+ },
+ "type": "library",
+ "extra": {
+ "pest": {
+ "plugins": [
+ "Pest\\Laravel\\Plugin"
+ ]
+ },
+ "laravel": {
+ "providers": [
+ "Pest\\Laravel\\PestServiceProvider"
+ ]
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/Autoload.php"
+ ],
+ "psr-4": {
+ "Pest\\Laravel\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "The Pest Laravel Plugin",
+ "keywords": [
+ "framework",
+ "laravel",
+ "pest",
+ "php",
+ "test",
+ "testing",
+ "unit"
+ ],
+ "support": {
+ "source": "https://github.com/pestphp/pest-plugin-laravel/tree/v4.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://www.paypal.com/paypalme/enunomaduro",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/nunomaduro",
+ "type": "github"
+ }
+ ],
+ "time": "2026-02-21T00:29:45+00:00"
+ },
+ {
+ "name": "pestphp/pest-plugin-mutate",
+ "version": "v4.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/pestphp/pest-plugin-mutate.git",
+ "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/pestphp/pest-plugin-mutate/zipball/d9b32b60b2385e1688a68cc227594738ec26d96c",
+ "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^5.6.1",
+ "pestphp/pest-plugin": "^4.0.0",
+ "php": "^8.3",
+ "psr/simple-cache": "^3.0.0"
+ },
+ "require-dev": {
+ "pestphp/pest": "^4.0.0",
+ "pestphp/pest-dev-tools": "^4.0.0",
+ "pestphp/pest-plugin-type-coverage": "^4.0.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Pest\\Mutate\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nuno Maduro",
+ "email": "enunomaduro@gmail.com"
+ },
+ {
+ "name": "Sandro Gehri",
+ "email": "sandrogehri@gmail.com"
+ }
+ ],
+ "description": "Mutates your code to find untested cases",
+ "keywords": [
+ "framework",
+ "mutate",
+ "mutation",
+ "pest",
+ "php",
+ "plugin",
+ "test",
+ "testing",
+ "unit"
+ ],
+ "support": {
+ "source": "https://github.com/pestphp/pest-plugin-mutate/tree/v4.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://www.paypal.com/paypalme/enunomaduro",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/gehrisandro",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nunomaduro",
+ "type": "github"
+ }
+ ],
+ "time": "2025-08-21T20:19:25+00:00"
+ },
+ {
+ "name": "pestphp/pest-plugin-profanity",
+ "version": "v4.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/pestphp/pest-plugin-profanity.git",
+ "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/pestphp/pest-plugin-profanity/zipball/343cfa6f3564b7e35df0ebb77b7fa97039f72b27",
+ "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27",
+ "shasum": ""
+ },
+ "require": {
+ "pestphp/pest-plugin": "^4.0.0",
+ "php": "^8.3"
+ },
+ "require-dev": {
+ "faissaloux/pest-plugin-inside": "^1.9",
+ "pestphp/pest": "^4.0.0",
+ "pestphp/pest-dev-tools": "^4.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "pest": {
+ "plugins": [
+ "Pest\\Profanity\\Plugin"
+ ]
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Pest\\Profanity\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "The Pest Profanity Plugin",
+ "keywords": [
+ "framework",
+ "pest",
+ "php",
+ "plugin",
+ "profanity",
+ "test",
+ "testing",
+ "unit"
+ ],
+ "support": {
+ "source": "https://github.com/pestphp/pest-plugin-profanity/tree/v4.2.1"
+ },
+ "time": "2025-12-08T00:13:17+00:00"
+ },
+ {
+ "name": "phar-io/manifest",
+ "version": "2.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/manifest.git",
+ "reference": "54750ef60c58e43759730615a392c31c80e23176"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176",
+ "reference": "54750ef60c58e43759730615a392c31c80e23176",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-phar": "*",
+ "ext-xmlwriter": "*",
+ "phar-io/version": "^3.0.1",
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)",
+ "support": {
+ "issues": "https://github.com/phar-io/manifest/issues",
+ "source": "https://github.com/phar-io/manifest/tree/2.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2024-03-03T12:33:53+00:00"
+ },
+ {
+ "name": "phar-io/version",
+ "version": "3.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phar-io/version.git",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Heuer",
+ "email": "sebastian@phpeople.de",
+ "role": "Developer"
+ },
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "Library for handling version information and constraints",
+ "support": {
+ "issues": "https://github.com/phar-io/version/issues",
+ "source": "https://github.com/phar-io/version/tree/3.2.1"
+ },
+ "time": "2022-02-21T01:04:05+00:00"
+ },
+ {
+ "name": "php-debugbar/php-debugbar",
+ "version": "v3.7.6",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-debugbar/php-debugbar.git",
+ "reference": "1690ee1728827f9deb4b60457fa387cf44672c56"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-debugbar/php-debugbar/zipball/1690ee1728827f9deb4b60457fa387cf44672c56",
+ "reference": "1690ee1728827f9deb4b60457fa387cf44672c56",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.2",
+ "psr/log": "^1|^2|^3",
+ "symfony/var-dumper": "^5.4|^6|^7|^8"
+ },
+ "replace": {
+ "maximebf/debugbar": "self.version"
+ },
+ "require-dev": {
+ "dbrekelmans/bdi": "^1.4",
+ "friendsofphp/php-cs-fixer": "^3.92",
+ "monolog/monolog": "^3.9",
+ "php-debugbar/doctrine-bridge": "^3@dev",
+ "php-debugbar/monolog-bridge": "^1@dev",
+ "php-debugbar/symfony-bridge": "^1@dev",
+ "php-debugbar/twig-bridge": "^2@dev",
+ "phpstan/phpstan": "^2.1",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpstan/phpstan-strict-rules": "^2.0",
+ "phpunit/phpunit": "^10",
+ "predis/predis": "^3.3",
+ "shipmonk/phpstan-rules": "^4.3",
+ "symfony/browser-kit": "^6.4|7.0",
+ "symfony/dom-crawler": "^6.4|^7",
+ "symfony/event-dispatcher": "^5.4|^6.4|^7.3|^8.0",
+ "symfony/http-foundation": "^5.4|^6.4|^7.3|^8.0",
+ "symfony/mailer": "^5.4|^6.4|^7.3|^8.0",
+ "symfony/panther": "^1|^2.1",
+ "twig/twig": "^3.11.2"
+ },
+ "suggest": {
+ "php-debugbar/doctrine-bridge": "To integrate Doctrine with php-debugbar.",
+ "php-debugbar/monolog-bridge": "To integrate Monolog with php-debugbar.",
+ "php-debugbar/symfony-bridge": "To integrate Symfony with php-debugbar.",
+ "php-debugbar/twig-bridge": "To integrate Twig with php-debugbar."
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "DebugBar\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Maxime Bouroumeau-Fuseau",
+ "email": "maxime.bouroumeau@gmail.com",
+ "homepage": "http://maximebf.com"
+ },
+ {
+ "name": "Barry vd. Heuvel",
+ "email": "barryvdh@gmail.com"
+ }
+ ],
+ "description": "Debug bar in the browser for php application",
+ "homepage": "https://github.com/php-debugbar/php-debugbar",
+ "keywords": [
+ "debug",
+ "debug bar",
+ "debugbar",
+ "dev",
+ "profiler",
+ "toolbar"
+ ],
+ "support": {
+ "issues": "https://github.com/php-debugbar/php-debugbar/issues",
+ "source": "https://github.com/php-debugbar/php-debugbar/tree/v3.7.6"
+ },
+ "funding": [
+ {
+ "url": "https://fruitcake.nl",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/barryvdh",
+ "type": "github"
+ }
+ ],
+ "time": "2026-04-30T07:31:44+00:00"
+ },
+ {
+ "name": "php-debugbar/symfony-bridge",
+ "version": "v1.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-debugbar/symfony-bridge.git",
+ "reference": "e37d2debe5d316408b00d0ab2688d9c2cf59b5ad"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-debugbar/symfony-bridge/zipball/e37d2debe5d316408b00d0ab2688d9c2cf59b5ad",
+ "reference": "e37d2debe5d316408b00d0ab2688d9c2cf59b5ad",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^8.2",
+ "php-debugbar/php-debugbar": "^3.1",
+ "symfony/http-foundation": "^5.4|^6.4|^7.3|^8.0"
+ },
+ "require-dev": {
+ "dbrekelmans/bdi": "^1.4",
+ "phpunit/phpunit": "^10",
+ "symfony/browser-kit": "^6|^7",
+ "symfony/dom-crawler": "^6|^7",
+ "symfony/mailer": "^5.4|^6.4|^7.3|^8.0",
+ "symfony/panther": "^1|^2.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "DebugBar\\Bridge\\Symfony\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Maxime Bouroumeau-Fuseau",
+ "email": "maxime.bouroumeau@gmail.com",
+ "homepage": "http://maximebf.com"
+ },
+ {
+ "name": "Barry vd. Heuvel",
+ "email": "barryvdh@gmail.com"
+ }
+ ],
+ "description": "Symfony bridge for PHP Debugbar",
+ "homepage": "https://github.com/php-debugbar/php-debugbar",
+ "keywords": [
+ "debugbar",
+ "dev",
+ "symfony"
+ ],
+ "support": {
+ "issues": "https://github.com/php-debugbar/symfony-bridge/issues",
+ "source": "https://github.com/php-debugbar/symfony-bridge/tree/v1.1.0"
+ },
+ "time": "2026-01-15T14:47:34+00:00"
+ },
+ {
+ "name": "phpdocumentor/reflection-common",
+ "version": "2.2.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/ReflectionCommon.git",
+ "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b",
+ "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.2 || ^8.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-2.x": "2.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jaap van Otterdijk",
+ "email": "opensource@ijaap.nl"
+ }
+ ],
+ "description": "Common reflection classes used by phpdocumentor to reflect the code structure",
+ "homepage": "http://www.phpdoc.org",
+ "keywords": [
+ "FQSEN",
+ "phpDocumentor",
+ "phpdoc",
+ "reflection",
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues",
+ "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x"
+ },
+ "time": "2020-06-27T09:03:43+00:00"
+ },
+ {
+ "name": "phpdocumentor/reflection-docblock",
+ "version": "6.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git",
+ "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/7bae67520aa9f5ecc506d646810bd40d9da54582",
+ "reference": "7bae67520aa9f5ecc506d646810bd40d9da54582",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/deprecations": "^1.1",
+ "ext-filter": "*",
+ "php": "^7.4 || ^8.0",
+ "phpdocumentor/reflection-common": "^2.2",
+ "phpdocumentor/type-resolver": "^2.0",
+ "phpstan/phpdoc-parser": "^2.0",
+ "webmozart/assert": "^1.9.1 || ^2"
+ },
+ "require-dev": {
+ "mockery/mockery": "~1.3.5 || ~1.6.0",
+ "phpstan/extension-installer": "^1.1",
+ "phpstan/phpstan": "^1.8",
+ "phpstan/phpstan-mockery": "^1.1",
+ "phpstan/phpstan-webmozart-assert": "^1.2",
+ "phpunit/phpunit": "^9.5",
+ "psalm/phar": "^5.26",
+ "shipmonk/dead-code-detector": "^0.5.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "5.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mike van Riel",
+ "email": "me@mikevanriel.com"
+ },
+ {
+ "name": "Jaap van Otterdijk",
+ "email": "opensource@ijaap.nl"
+ }
+ ],
+ "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.",
+ "support": {
+ "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues",
+ "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/6.0.3"
+ },
+ "time": "2026-03-18T20:49:53+00:00"
+ },
+ {
+ "name": "phpdocumentor/type-resolver",
+ "version": "2.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpDocumentor/TypeResolver.git",
+ "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/327a05bbee54120d4786a0dc67aad30226ad4cf9",
+ "reference": "327a05bbee54120d4786a0dc67aad30226ad4cf9",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/deprecations": "^1.0",
+ "php": "^7.4 || ^8.0",
+ "phpdocumentor/reflection-common": "^2.0",
+ "phpstan/phpdoc-parser": "^2.0"
+ },
+ "require-dev": {
+ "ext-tokenizer": "*",
+ "phpbench/phpbench": "^1.2",
+ "phpstan/extension-installer": "^1.4",
+ "phpstan/phpstan": "^2.1",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpunit/phpunit": "^9.5",
+ "psalm/phar": "^4"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-1.x": "1.x-dev",
+ "dev-2.x": "2.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "phpDocumentor\\Reflection\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Mike van Riel",
+ "email": "me@mikevanriel.com"
+ }
+ ],
+ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names",
+ "support": {
+ "issues": "https://github.com/phpDocumentor/TypeResolver/issues",
+ "source": "https://github.com/phpDocumentor/TypeResolver/tree/2.0.0"
+ },
+ "time": "2026-01-06T21:53:42+00:00"
+ },
+ {
+ "name": "phpstan/phpdoc-parser",
+ "version": "2.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/phpstan/phpdoc-parser.git",
+ "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a",
+ "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/annotations": "^2.0",
+ "nikic/php-parser": "^5.3.0",
+ "php-parallel-lint/php-parallel-lint": "^1.2",
+ "phpstan/extension-installer": "^1.0",
+ "phpstan/phpstan": "^2.0",
+ "phpstan/phpstan-phpunit": "^2.0",
+ "phpstan/phpstan-strict-rules": "^2.0",
+ "phpunit/phpunit": "^9.6",
+ "symfony/process": "^5.2"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "PHPStan\\PhpDocParser\\": [
+ "src/"
+ ]
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "PHPDoc parser with support for nullable, intersection and generic types",
+ "support": {
+ "issues": "https://github.com/phpstan/phpdoc-parser/issues",
+ "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.2"
+ },
+ "time": "2026-01-25T14:56:51+00:00"
+ },
+ {
+ "name": "phpunit/php-code-coverage",
+ "version": "12.5.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-code-coverage.git",
+ "reference": "186dab580576598076de6818596d12b61801880e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/186dab580576598076de6818596d12b61801880e",
+ "reference": "186dab580576598076de6818596d12b61801880e",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-libxml": "*",
+ "ext-xmlwriter": "*",
+ "nikic/php-parser": "^5.7.0",
+ "php": ">=8.3",
+ "phpunit/php-text-template": "^5.0",
+ "sebastian/complexity": "^5.0",
+ "sebastian/environment": "^8.1.2",
+ "sebastian/lines-of-code": "^4.0.1",
+ "sebastian/version": "^6.0",
+ "theseer/tokenizer": "^2.0.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.5.28"
+ },
+ "suggest": {
+ "ext-pcov": "PHP extension that provides line coverage",
+ "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "12.5.x-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.",
+ "homepage": "https://github.com/sebastianbergmann/php-code-coverage",
+ "keywords": [
+ "coverage",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
+ "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.7"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-06-01T13:24:19+00:00"
+ },
+ {
+ "name": "phpunit/php-file-iterator",
+ "version": "6.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-file-iterator.git",
+ "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5",
+ "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "FilterIterator implementation that filters files based on a list of suffixes.",
+ "homepage": "https://github.com/sebastianbergmann/php-file-iterator/",
+ "keywords": [
+ "filesystem",
+ "iterator"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues",
+ "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-02-02T14:04:18+00:00"
+ },
+ {
+ "name": "phpunit/php-invoker",
+ "version": "6.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-invoker.git",
+ "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406",
+ "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.3"
+ },
+ "require-dev": {
+ "ext-pcntl": "*",
+ "phpunit/phpunit": "^12.0"
+ },
+ "suggest": {
+ "ext-pcntl": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Invoke callables with a timeout",
+ "homepage": "https://github.com/sebastianbergmann/php-invoker/",
+ "keywords": [
+ "process"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-invoker/issues",
+ "security": "https://github.com/sebastianbergmann/php-invoker/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2025-02-07T04:58:58+00:00"
+ },
+ {
+ "name": "phpunit/php-text-template",
+ "version": "5.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-text-template.git",
+ "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53",
+ "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Simple template engine.",
+ "homepage": "https://github.com/sebastianbergmann/php-text-template/",
+ "keywords": [
+ "template"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-text-template/issues",
+ "security": "https://github.com/sebastianbergmann/php-text-template/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2025-02-07T04:59:16+00:00"
+ },
+ {
+ "name": "phpunit/php-timer",
+ "version": "8.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/php-timer.git",
+ "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc",
+ "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "8.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Utility class for timing",
+ "homepage": "https://github.com/sebastianbergmann/php-timer/",
+ "keywords": [
+ "timer"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/php-timer/issues",
+ "security": "https://github.com/sebastianbergmann/php-timer/security/policy",
+ "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2025-02-07T04:59:38+00:00"
+ },
+ {
+ "name": "phpunit/phpunit",
+ "version": "12.5.28",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/phpunit.git",
+ "reference": "5895d05f5bf421ed230fbd76e1277e4b8955def4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/5895d05f5bf421ed230fbd76e1277e4b8955def4",
+ "reference": "5895d05f5bf421ed230fbd76e1277e4b8955def4",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-json": "*",
+ "ext-libxml": "*",
+ "ext-mbstring": "*",
+ "ext-xml": "*",
+ "ext-xmlwriter": "*",
+ "myclabs/deep-copy": "^1.13.4",
+ "phar-io/manifest": "^2.0.4",
+ "phar-io/version": "^3.2.1",
+ "php": ">=8.3",
+ "phpunit/php-code-coverage": "^12.5.6",
+ "phpunit/php-file-iterator": "^6.0.1",
+ "phpunit/php-invoker": "^6.0.0",
+ "phpunit/php-text-template": "^5.0.0",
+ "phpunit/php-timer": "^8.0.0",
+ "sebastian/cli-parser": "^4.2.1",
+ "sebastian/comparator": "^7.1.8",
+ "sebastian/diff": "^7.0.0",
+ "sebastian/environment": "^8.1.2",
+ "sebastian/exporter": "^7.0.3",
+ "sebastian/global-state": "^8.0.2",
+ "sebastian/object-enumerator": "^7.0.0",
+ "sebastian/recursion-context": "^7.0.1",
+ "sebastian/type": "^6.0.4",
+ "sebastian/version": "^6.0.0",
+ "staabm/side-effects-detector": "^1.0.5"
+ },
+ "bin": [
+ "phpunit"
+ ],
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "12.5-dev"
+ }
+ },
+ "autoload": {
+ "files": [
+ "src/Framework/Assert/Functions.php"
+ ],
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "The PHP Unit Testing framework.",
+ "homepage": "https://phpunit.de/",
+ "keywords": [
+ "phpunit",
+ "testing",
+ "xunit"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/phpunit/issues",
+ "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
+ "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.28"
+ },
+ "funding": [
+ {
+ "url": "https://phpunit.de/sponsoring.html",
+ "type": "other"
+ }
+ ],
+ "time": "2026-05-27T14:01:10+00:00"
+ },
+ {
+ "name": "sebastian/cli-parser",
+ "version": "4.2.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/cli-parser.git",
+ "reference": "7d05781b13f7dec9043a629a21d086ed74582a15"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/7d05781b13f7dec9043a629a21d086ed74582a15",
+ "reference": "7d05781b13f7dec9043a629a21d086ed74582a15",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.5.25"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "4.2-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for parsing CLI options",
+ "homepage": "https://github.com/sebastianbergmann/cli-parser",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/cli-parser/issues",
+ "security": "https://github.com/sebastianbergmann/cli-parser/security/policy",
+ "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-17T05:29:34+00:00"
+ },
+ {
+ "name": "sebastian/comparator",
+ "version": "7.1.8",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/comparator.git",
+ "reference": "7c65c1e79836812819705b473a90c12399542485"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/7c65c1e79836812819705b473a90c12399542485",
+ "reference": "7c65c1e79836812819705b473a90c12399542485",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-mbstring": "*",
+ "php": ">=8.3",
+ "sebastian/diff": "^7.0",
+ "sebastian/exporter": "^7.0.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.5.25"
+ },
+ "suggest": {
+ "ext-bcmath": "For comparing BcMath\\Number objects"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "7.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@2bepublished.at"
+ }
+ ],
+ "description": "Provides the functionality to compare PHP values for equality",
+ "homepage": "https://github.com/sebastianbergmann/comparator",
+ "keywords": [
+ "comparator",
+ "compare",
+ "equality"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/comparator/issues",
+ "security": "https://github.com/sebastianbergmann/comparator/security/policy",
+ "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.8"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-21T04:45:25+00:00"
+ },
+ {
+ "name": "sebastian/complexity",
+ "version": "5.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/complexity.git",
+ "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb",
+ "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^5.0",
+ "php": ">=8.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for calculating the complexity of PHP code units",
+ "homepage": "https://github.com/sebastianbergmann/complexity",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/complexity/issues",
+ "security": "https://github.com/sebastianbergmann/complexity/security/policy",
+ "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2025-02-07T04:55:25+00:00"
+ },
+ {
+ "name": "sebastian/diff",
+ "version": "7.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/diff.git",
+ "reference": "7ab1ea946c012266ca32390913653d844ecd085f"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f",
+ "reference": "7ab1ea946c012266ca32390913653d844ecd085f",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.0",
+ "symfony/process": "^7.2"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "7.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Kore Nordmann",
+ "email": "mail@kore-nordmann.de"
+ }
+ ],
+ "description": "Diff implementation",
+ "homepage": "https://github.com/sebastianbergmann/diff",
+ "keywords": [
+ "diff",
+ "udiff",
+ "unidiff",
+ "unified diff"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/diff/issues",
+ "security": "https://github.com/sebastianbergmann/diff/security/policy",
+ "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2025-02-07T04:55:46+00:00"
+ },
+ {
+ "name": "sebastian/environment",
+ "version": "8.1.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/environment.git",
+ "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/9d32c685773823b1983e256ae4ecd48a10d6e439",
+ "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.5.26"
+ },
+ "suggest": {
+ "ext-posix": "*"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "8.1-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Provides functionality to handle HHVM/PHP environments",
+ "homepage": "https://github.com/sebastianbergmann/environment",
+ "keywords": [
+ "Xdebug",
+ "environment",
+ "hhvm"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/environment/issues",
+ "security": "https://github.com/sebastianbergmann/environment/security/policy",
+ "source": "https://github.com/sebastianbergmann/environment/tree/8.1.2"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/environment",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-25T13:40:20+00:00"
+ },
+ {
+ "name": "sebastian/exporter",
+ "version": "7.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/exporter.git",
+ "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23",
+ "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23",
+ "shasum": ""
+ },
+ "require": {
+ "ext-mbstring": "*",
+ "php": ">=8.3",
+ "sebastian/recursion-context": "^7.0.1"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.5.25"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "7.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Volker Dusch",
+ "email": "github@wallbash.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ },
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ }
+ ],
+ "description": "Provides the functionality to export PHP variables for visualization",
+ "homepage": "https://www.github.com/sebastianbergmann/exporter",
+ "keywords": [
+ "export",
+ "exporter"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/exporter/issues",
+ "security": "https://github.com/sebastianbergmann/exporter/security/policy",
+ "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-20T04:37:17+00:00"
+ },
+ {
+ "name": "sebastian/global-state",
+ "version": "8.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/global-state.git",
+ "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b164d3274d6537ab462591c5755f76a8f5b1aae9",
+ "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.3",
+ "sebastian/object-reflector": "^5.0",
+ "sebastian/recursion-context": "^7.0.1"
+ },
+ "require-dev": {
+ "ext-dom": "*",
+ "phpunit/phpunit": "^12.5.28"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "8.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Snapshotting of global state",
+ "homepage": "https://www.github.com/sebastianbergmann/global-state",
+ "keywords": [
+ "global state"
+ ],
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/global-state/issues",
+ "security": "https://github.com/sebastianbergmann/global-state/security/policy",
+ "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-06-01T15:10:33+00:00"
+ },
+ {
+ "name": "sebastian/lines-of-code",
+ "version": "4.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/lines-of-code.git",
+ "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d543b8ef219dcd8da262cbb958639a96bedba10e",
+ "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^5.7.0",
+ "php": ">=8.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.5.25"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "4.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for counting the lines of code in PHP source code",
+ "homepage": "https://github.com/sebastianbergmann/lines-of-code",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/lines-of-code/issues",
+ "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy",
+ "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/lines-of-code",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-19T16:22:07+00:00"
+ },
+ {
+ "name": "sebastian/object-enumerator",
+ "version": "7.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-enumerator.git",
+ "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894",
+ "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.3",
+ "sebastian/object-reflector": "^5.0",
+ "sebastian/recursion-context": "^7.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "7.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Traverses array structures and object graphs to enumerate all referenced objects",
+ "homepage": "https://github.com/sebastianbergmann/object-enumerator/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-enumerator/issues",
+ "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy",
+ "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2025-02-07T04:57:48+00:00"
+ },
+ {
+ "name": "sebastian/object-reflector",
+ "version": "5.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/object-reflector.git",
+ "reference": "4bfa827c969c98be1e527abd576533293c634f6a"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a",
+ "reference": "4bfa827c969c98be1e527abd576533293c634f6a",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "5.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ }
+ ],
+ "description": "Allows reflection of object attributes, including inherited and non-public ones",
+ "homepage": "https://github.com/sebastianbergmann/object-reflector/",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/object-reflector/issues",
+ "security": "https://github.com/sebastianbergmann/object-reflector/security/policy",
+ "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2025-02-07T04:58:17+00:00"
+ },
+ {
+ "name": "sebastian/recursion-context",
+ "version": "7.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/recursion-context.git",
+ "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c",
+ "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "7.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Jeff Welch",
+ "email": "whatthejeff@gmail.com"
+ },
+ {
+ "name": "Adam Harvey",
+ "email": "aharvey@php.net"
+ }
+ ],
+ "description": "Provides functionality to recursively process PHP variables",
+ "homepage": "https://github.com/sebastianbergmann/recursion-context",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/recursion-context/issues",
+ "security": "https://github.com/sebastianbergmann/recursion-context/security/policy",
+ "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2025-08-13T04:44:59+00:00"
+ },
+ {
+ "name": "sebastian/type",
+ "version": "6.0.4",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/type.git",
+ "reference": "82ff822c2edc46724be9f7411d3163021f602773"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/82ff822c2edc46724be9f7411d3163021f602773",
+ "reference": "82ff822c2edc46724be9f7411d3163021f602773",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.3"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^12.5.25"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Collection of value objects that represent the types of the PHP type system",
+ "homepage": "https://github.com/sebastianbergmann/type",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/type/issues",
+ "security": "https://github.com/sebastianbergmann/type/security/policy",
+ "source": "https://github.com/sebastianbergmann/type/tree/6.0.4"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/type",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-20T06:45:45+00:00"
+ },
+ {
+ "name": "sebastian/version",
+ "version": "6.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/version.git",
+ "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c",
+ "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.3"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "6.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library that helps with managing the version number of Git-hosted PHP projects",
+ "homepage": "https://github.com/sebastianbergmann/version",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/version/issues",
+ "security": "https://github.com/sebastianbergmann/version/security/policy",
+ "source": "https://github.com/sebastianbergmann/version/tree/6.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ }
+ ],
+ "time": "2025-02-07T05:00:38+00:00"
+ },
+ {
+ "name": "staabm/side-effects-detector",
+ "version": "1.0.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/staabm/side-effects-detector.git",
+ "reference": "d8334211a140ce329c13726d4a715adbddd0a163"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163",
+ "reference": "d8334211a140ce329c13726d4a715adbddd0a163",
+ "shasum": ""
+ },
+ "require": {
+ "ext-tokenizer": "*",
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "phpstan/extension-installer": "^1.4.3",
+ "phpstan/phpstan": "^1.12.6",
+ "phpunit/phpunit": "^9.6.21",
+ "symfony/var-dumper": "^5.4.43",
+ "tomasvotruba/type-coverage": "1.0.0",
+ "tomasvotruba/unused-public": "1.0.0"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "lib/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "description": "A static analysis tool to detect side effects in PHP code",
+ "keywords": [
+ "static analysis"
+ ],
+ "support": {
+ "issues": "https://github.com/staabm/side-effects-detector/issues",
+ "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/staabm",
+ "type": "github"
+ }
+ ],
+ "time": "2024-10-20T05:08:20+00:00"
+ },
+ {
+ "name": "symfony/yaml",
+ "version": "v8.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/yaml.git",
+ "reference": "efb42bd2c6f4f3ccfd4683583449938b5fc146b0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/yaml/zipball/efb42bd2c6f4f3ccfd4683583449938b5fc146b0",
+ "reference": "efb42bd2c6f4f3ccfd4683583449938b5fc146b0",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.4.1",
+ "symfony/polyfill-ctype": "^1.8"
+ },
+ "conflict": {
+ "symfony/console": "<7.4"
+ },
+ "require-dev": {
+ "symfony/console": "^7.4|^8.0",
+ "yaml/yaml-test-suite": "*"
+ },
+ "bin": [
+ "Resources/bin/yaml-lint"
+ ],
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Yaml\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Loads and dumps YAML files",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/yaml/tree/v8.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://github.com/nicolas-grekas",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-05-29T05:06:50+00:00"
+ },
+ {
+ "name": "ta-tikoma/phpunit-architecture-test",
+ "version": "0.8.7",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/ta-tikoma/phpunit-architecture-test.git",
+ "reference": "1248f3f506ca9641d4f68cebcd538fa489754db8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/1248f3f506ca9641d4f68cebcd538fa489754db8",
+ "reference": "1248f3f506ca9641d4f68cebcd538fa489754db8",
+ "shasum": ""
+ },
+ "require": {
+ "nikic/php-parser": "^4.18.0 || ^5.0.0",
+ "php": "^8.1.0",
+ "phpdocumentor/reflection-docblock": "^5.3.0 || ^6.0.0",
+ "phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0 || ^13.0.0",
+ "symfony/finder": "^6.4.0 || ^7.0.0 || ^8.0.0"
+ },
+ "require-dev": {
+ "laravel/pint": "^1.13.7",
+ "phpstan/phpstan": "^1.10.52"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "PHPUnit\\Architecture\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ni Shi",
+ "email": "futik0ma011@gmail.com"
+ },
+ {
+ "name": "Nuno Maduro",
+ "email": "enunomaduro@gmail.com"
+ }
+ ],
+ "description": "Methods for testing application architecture",
+ "keywords": [
+ "architecture",
+ "phpunit",
+ "stucture",
+ "test",
+ "testing"
+ ],
+ "support": {
+ "issues": "https://github.com/ta-tikoma/phpunit-architecture-test/issues",
+ "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.7"
+ },
+ "time": "2026-02-17T17:25:14+00:00"
+ },
+ {
+ "name": "theseer/tokenizer",
+ "version": "2.0.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/theseer/tokenizer.git",
+ "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4",
+ "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4",
+ "shasum": ""
+ },
+ "require": {
+ "ext-dom": "*",
+ "ext-tokenizer": "*",
+ "ext-xmlwriter": "*",
+ "php": "^8.1"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Arne Blankerts",
+ "email": "arne@blankerts.de",
+ "role": "Developer"
+ }
+ ],
+ "description": "A small library for converting tokenized PHP source code into XML and potentially other formats",
+ "support": {
+ "issues": "https://github.com/theseer/tokenizer/issues",
+ "source": "https://github.com/theseer/tokenizer/tree/2.0.1"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/theseer",
+ "type": "github"
+ }
+ ],
+ "time": "2025-12-08T11:19:18+00:00"
+ },
+ {
+ "name": "webmozart/assert",
+ "version": "2.4.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/webmozarts/assert.git",
+ "reference": "9007ea6f45ecf352a9422b36644e4bfc039b9155"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/webmozarts/assert/zipball/9007ea6f45ecf352a9422b36644e4bfc039b9155",
+ "reference": "9007ea6f45ecf352a9422b36644e4bfc039b9155",
+ "shasum": ""
+ },
+ "require": {
+ "ext-ctype": "*",
+ "ext-date": "*",
+ "ext-filter": "*",
+ "php": "^8.2"
+ },
+ "suggest": {
+ "ext-intl": "",
+ "ext-simplexml": "",
+ "ext-spl": ""
+ },
+ "type": "library",
+ "extra": {
+ "psalm": {
+ "pluginClass": "Webmozart\\Assert\\PsalmPlugin"
+ },
+ "branch-alias": {
+ "dev-master": "2.0-dev",
+ "dev-feature/2-0": "2.0-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Webmozart\\Assert\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Bernhard Schussek",
+ "email": "bschussek@gmail.com"
+ },
+ {
+ "name": "Woody Gilk",
+ "email": "woody.gilk@gmail.com"
+ }
+ ],
+ "description": "Assertions to validate method input/output with nice error messages.",
+ "keywords": [
+ "assert",
+ "check",
+ "validate"
+ ],
+ "support": {
+ "issues": "https://github.com/webmozarts/assert/issues",
+ "source": "https://github.com/webmozarts/assert/tree/2.4.0"
+ },
+ "time": "2026-05-20T13:07:01+00:00"
+ }
+ ],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": {
+ "maatwebsite/excel": 20
+ },
+ "prefer-stable": true,
+ "prefer-lowest": false,
+ "platform": {
+ "php": "^8.4",
+ "ext-zip": "*"
+ },
+ "platform-dev": {},
+ "plugin-api-version": "2.9.0"
+}
diff --git a/config/app.php b/config/app.php
new file mode 100644
index 0000000..423eed5
--- /dev/null
+++ b/config/app.php
@@ -0,0 +1,126 @@
+ env('APP_NAME', 'Laravel'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Application Environment
+ |--------------------------------------------------------------------------
+ |
+ | This value determines the "environment" your application is currently
+ | running in. This may determine how you prefer to configure various
+ | services the application utilizes. Set this in your ".env" file.
+ |
+ */
+
+ 'env' => env('APP_ENV', 'production'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Application Debug Mode
+ |--------------------------------------------------------------------------
+ |
+ | When your application is in debug mode, detailed error messages with
+ | stack traces will be shown on every error that occurs within your
+ | application. If disabled, a simple generic error page is shown.
+ |
+ */
+
+ 'debug' => (bool) env('APP_DEBUG', false),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Application URL
+ |--------------------------------------------------------------------------
+ |
+ | This URL is used by the console to properly generate URLs when using
+ | the Artisan command line tool. You should set this to the root of
+ | the application so that it's available within Artisan commands.
+ |
+ */
+
+ 'url' => env('APP_URL', 'http://localhost'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Application Timezone
+ |--------------------------------------------------------------------------
+ |
+ | Here you may specify the default timezone for your application, which
+ | will be used by the PHP date and date-time functions. The timezone
+ | is set to "UTC" by default as it is suitable for most use cases.
+ |
+ */
+
+ 'timezone' => 'UTC',
+
+ /*
+ |--------------------------------------------------------------------------
+ | Application Locale Configuration
+ |--------------------------------------------------------------------------
+ |
+ | The application locale determines the default locale that will be used
+ | by Laravel's translation / localization methods. This option can be
+ | set to any locale for which you plan to have translation strings.
+ |
+ */
+
+ 'locale' => env('APP_LOCALE', 'en'),
+
+ 'fallback_locale' => env('APP_FALLBACK_LOCALE', 'en'),
+
+ 'faker_locale' => env('APP_FAKER_LOCALE', 'en_US'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Encryption Key
+ |--------------------------------------------------------------------------
+ |
+ | This key is utilized by Laravel's encryption services and should be set
+ | to a random, 32 character string to ensure that all encrypted values
+ | are secure. You should do this prior to deploying the application.
+ |
+ */
+
+ 'cipher' => 'AES-256-CBC',
+
+ 'key' => env('APP_KEY'),
+
+ 'previous_keys' => [
+ ...array_filter(
+ explode(',', (string) env('APP_PREVIOUS_KEYS', ''))
+ ),
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Maintenance Mode Driver
+ |--------------------------------------------------------------------------
+ |
+ | These configuration options determine the driver used to determine and
+ | manage Laravel's "maintenance mode" status. The "cache" driver will
+ | allow maintenance mode to be controlled across multiple machines.
+ |
+ | Supported drivers: "file", "cache"
+ |
+ */
+
+ 'maintenance' => [
+ 'driver' => env('APP_MAINTENANCE_DRIVER', 'file'),
+ 'store' => env('APP_MAINTENANCE_STORE', 'database'),
+ ],
+
+];
diff --git a/config/audit.php b/config/audit.php
new file mode 100644
index 0000000..46f6dd1
--- /dev/null
+++ b/config/audit.php
@@ -0,0 +1,204 @@
+ env('AUDITING_ENABLED', true),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Audit Implementation
+ |--------------------------------------------------------------------------
+ |
+ | Define which Audit model implementation should be used.
+ |
+ */
+
+ 'implementation' => Audit::class,
+
+ /*
+ |--------------------------------------------------------------------------
+ | User Morph prefix & Guards
+ |--------------------------------------------------------------------------
+ |
+ | Define the morph prefix and authentication guards for the User resolver.
+ |
+ */
+
+ 'user' => [
+ 'morph_prefix' => 'user',
+ 'guards' => [
+ 'web',
+ 'api',
+ ],
+ 'resolver' => UserResolver::class,
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Audit Resolvers
+ |--------------------------------------------------------------------------
+ |
+ | Define the IP Address, User Agent and URL resolver implementations.
+ |
+ */
+ 'resolvers' => [
+ 'ip_address' => IpAddressResolver::class,
+ 'user_agent' => UserAgentResolver::class,
+ 'url' => UrlResolver::class,
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Audit Events
+ |--------------------------------------------------------------------------
+ |
+ | The Eloquent events that trigger an Audit.
+ |
+ */
+
+ 'events' => [
+ 'created',
+ 'updated',
+ 'deleted',
+ 'restored',
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Strict Mode
+ |--------------------------------------------------------------------------
+ |
+ | Enable the strict mode when auditing?
+ |
+ */
+
+ 'strict' => false,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Global exclude
+ |--------------------------------------------------------------------------
+ |
+ | Have something you always want to exclude by default? - add it here.
+ | Note that this is overwritten (not merged) with local exclude
+ |
+ */
+
+ 'exclude' => [],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Empty Values
+ |--------------------------------------------------------------------------
+ |
+ | Should Audit records be stored when the recorded old_values & new_values
+ | are both empty?
+ |
+ | Some events may be empty on purpose. Use allowed_empty_values to exclude
+ | those from the empty values check. For example when auditing
+ | model retrieved events which will never have new and old values.
+ |
+ |
+ */
+
+ 'empty_values' => true,
+ 'allowed_empty_values' => [
+ 'retrieved',
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Allowed Array Values
+ |--------------------------------------------------------------------------
+ |
+ | Should the array values be audited?
+ |
+ | By default, array values are not allowed. This is to prevent performance
+ | issues when storing large amounts of data. You can override this by
+ | setting allow_array_values to true.
+ */
+ 'allowed_array_values' => false,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Audit Timestamps
+ |--------------------------------------------------------------------------
+ |
+ | Should the created_at, updated_at and deleted_at timestamps be audited?
+ |
+ */
+
+ 'timestamps' => false,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Audit Threshold
+ |--------------------------------------------------------------------------
+ |
+ | Specify a threshold for the amount of Audit records a model can have.
+ | Zero means no limit.
+ |
+ */
+
+ 'threshold' => 0,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Audit Driver
+ |--------------------------------------------------------------------------
+ |
+ | The default audit driver used to keep track of changes.
+ |
+ */
+
+ 'driver' => 'database',
+
+ /*
+ |--------------------------------------------------------------------------
+ | Audit Driver Configurations
+ |--------------------------------------------------------------------------
+ |
+ | Available audit drivers and respective configurations.
+ |
+ */
+
+ 'drivers' => [
+ 'database' => [
+ 'table' => 'audits',
+ 'connection' => null,
+ ],
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Audit Queue Configurations
+ |--------------------------------------------------------------------------
+ |
+ | Available audit queue configurations.
+ |
+ */
+
+ 'queue' => [
+ 'enable' => false,
+ 'connection' => 'sync',
+ 'queue' => 'default',
+ 'delay' => 0,
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Audit Console
+ |--------------------------------------------------------------------------
+ |
+ | Whether console events should be audited (eg. php artisan db:seed).
+ |
+ */
+
+ 'console' => false,
+];
diff --git a/config/auth.php b/config/auth.php
new file mode 100644
index 0000000..9f79feb
--- /dev/null
+++ b/config/auth.php
@@ -0,0 +1,122 @@
+ [
+ 'guard' => env('AUTH_GUARD', 'web'),
+ 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Authentication Guards
+ |--------------------------------------------------------------------------
+ |
+ | Next, you may define every authentication guard for your application.
+ | Of course, a great default configuration has been defined for you
+ | which utilizes session storage plus the Eloquent user provider.
+ |
+ | All authentication guards have a user provider, which defines how the
+ | users are actually retrieved out of your database or other storage
+ | system used by the application. Typically, Eloquent is utilized.
+ |
+ | Supported: "session"
+ |
+ */
+
+ 'guards' => [
+ 'web' => [
+ 'driver' => 'session',
+ 'provider' => 'users',
+ ],
+
+ 'api' => [
+ 'driver' => 'sanctum',
+ 'provider' => 'users',
+ ],
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | User Providers
+ |--------------------------------------------------------------------------
+ |
+ | All authentication guards have a user provider, which defines how the
+ | users are actually retrieved out of your database or other storage
+ | system used by the application. Typically, Eloquent is utilized.
+ |
+ | If you have multiple user tables or models you may configure multiple
+ | providers to represent the model / table. These providers may then
+ | be assigned to any extra authentication guards you have defined.
+ |
+ | Supported: "database", "eloquent"
+ |
+ */
+
+ 'providers' => [
+ 'users' => [
+ 'driver' => 'eloquent',
+ 'model' => env('AUTH_MODEL', User::class),
+ ],
+
+ // 'users' => [
+ // 'driver' => 'database',
+ // 'table' => 'users',
+ // ],
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Resetting Passwords
+ |--------------------------------------------------------------------------
+ |
+ | These configuration options specify the behavior of Laravel's password
+ | reset functionality, including the table utilized for token storage
+ | and the user provider that is invoked to actually retrieve users.
+ |
+ | The expiry time is the number of minutes that each reset token will be
+ | considered valid. This security feature keeps tokens short-lived so
+ | they have less time to be guessed. You may change this as needed.
+ |
+ | The throttle setting is the number of seconds a user must wait before
+ | generating more password reset tokens. This prevents the user from
+ | quickly generating a very large amount of password reset tokens.
+ |
+ */
+
+ 'passwords' => [
+ 'users' => [
+ 'provider' => 'users',
+ 'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
+ 'expire' => 60,
+ 'throttle' => 60,
+ ],
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Password Confirmation Timeout
+ |--------------------------------------------------------------------------
+ |
+ | Here you may define the number of seconds before a password confirmation
+ | window expires and users are asked to re-enter their password via the
+ | confirmation screen. By default, the timeout lasts for three hours.
+ |
+ */
+
+ 'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800),
+
+];
diff --git a/config/backup.php b/config/backup.php
new file mode 100644
index 0000000..034a675
--- /dev/null
+++ b/config/backup.php
@@ -0,0 +1,386 @@
+ [
+ /*
+ * The name of this application. You can use this name to monitor
+ * the backups.
+ */
+ 'name' => env('APP_NAME', 'laravel-backup'),
+
+ 'source' => [
+ 'files' => [
+ /*
+ * The list of directories and files that will be included in the backup.
+ */
+ 'include' => [
+ storage_path('/app/private'),
+ storage_path('/app/public'),
+ // storage_path(), // Include if you use zero downtime deployments and don't follow symlinks
+ ],
+
+ /*
+ * These directories and files will be excluded from the backup.
+ *
+ * Directories used by the backup process will automatically be excluded.
+ */
+ 'exclude' => [
+ base_path('vendor'),
+ base_path('node_modules'),
+ storage_path('framework'),
+ storage_path('app/private/livewire-tmp'),
+ storage_path('app/public/livewire-tmp'),
+ ],
+
+ /*
+ * Determines if symlinks should be followed.
+ */
+ 'follow_links' => false,
+
+ /*
+ * Determines if it should avoid unreadable folders.
+ */
+ 'ignore_unreadable_directories' => false,
+
+ /*
+ * This path is used to make directories in resulting zip-file relative
+ * Set to `null` to include complete absolute path
+ * Example: base_path()
+ */
+ 'relative_path' => base_path(),
+ ],
+
+ /*
+ * The names of the connections to the databases that should be backed up
+ * MySQL, PostgreSQL, SQLite and Mongo databases are supported.
+ *
+ * The content of the database dump may be customized for each connection
+ * by adding a 'dump' key to the connection settings in config/database.php.
+ * E.g.
+ * 'mysql' => [
+ * ...
+ * 'dump' => [
+ * 'exclude_tables' => [
+ * 'table_to_exclude_from_backup',
+ * 'another_table_to_exclude'
+ * ]
+ * ],
+ * ],
+ *
+ * If you are using only InnoDB tables on a MySQL server, you can
+ * also supply the useSingleTransaction option to avoid table locking.
+ *
+ * E.g.
+ * 'mysql' => [
+ * ...
+ * 'dump' => [
+ * 'useSingleTransaction' => true,
+ * ],
+ * ],
+ *
+ * For a complete list of available customization options, see https://github.com/spatie/db-dumper
+ */
+ 'databases' => [
+ env('DB_CONNECTION', 'mysql'),
+ ],
+ ],
+
+ /*
+ * The database dump can be compressed to decrease disk space usage.
+ *
+ * Out of the box Laravel-backup supplies
+ * Spatie\DbDumper\Compressors\GzipCompressor::class.
+ *
+ * You can also create custom compressor. More info on that here:
+ * https://github.com/spatie/db-dumper#using-compression
+ *
+ * If you do not want any compressor at all, set it to null.
+ */
+ 'database_dump_compressor' => null,
+
+ /*
+ * If specified, the database dumped file name will contain a timestamp (e.g.: 'Y-m-d-H-i-s').
+ */
+ 'database_dump_file_timestamp_format' => null,
+
+ /*
+ * The base of the dump filename, either 'database' or 'connection'
+ *
+ * If 'database' (default), the dumped filename will contain the database name.
+ * If 'connection', the dumped filename will contain the connection name.
+ */
+ 'database_dump_filename_base' => 'database',
+
+ /*
+ * The file extension used for the database dump files.
+ *
+ * If not specified, the file extension will be .archive for MongoDB and .sql for all other databases
+ * The file extension should be specified without a leading .
+ */
+ 'database_dump_file_extension' => '',
+
+ 'destination' => [
+ /*
+ * The compression algorithm to be used for creating the zip archive.
+ *
+ * If backing up only database, you may choose gzip compression for db dump and no compression at zip.
+ *
+ * Some common algorithms are listed below:
+ * ZipArchive::CM_STORE (no compression at all; set 0 as compression level)
+ * ZipArchive::CM_DEFAULT
+ * ZipArchive::CM_DEFLATE
+ * ZipArchive::CM_BZIP2
+ * ZipArchive::CM_XZ
+ *
+ * For more check https://www.php.net/manual/zip.constants.php and confirm it's supported by your system.
+ */
+ 'compression_method' => ZipArchive::CM_DEFAULT,
+
+ /*
+ * The compression level corresponding to the used algorithm; an integer between 0 and 9.
+ *
+ * Check supported levels for the chosen algorithm, usually 1 means the fastest and weakest compression,
+ * while 9 the slowest and strongest one.
+ *
+ * Setting of 0 for some algorithms may switch to the strongest compression.
+ */
+ 'compression_level' => 9,
+
+ /*
+ * The filename prefix used for the backup zip file.
+ */
+ 'filename_prefix' => '',
+
+ /*
+ * The disk names on which the backups will be stored.
+ */
+ 'disks' => [
+ 'backup',
+ ],
+
+ /*
+ * Determines whether to allow backups to continue when some targets fail instead of failing completely.
+ */
+ 'continue_on_failure' => false,
+ ],
+
+ /*
+ * The directory where the temporary files will be stored.
+ */
+ 'temporary_directory' => storage_path('app/backup-temp'),
+
+ /*
+ * The password to be used for archive encryption.
+ * Set to `null` to disable encryption.
+ */
+ 'password' => env('BACKUP_ARCHIVE_PASSWORD'),
+
+ /*
+ * The encryption algorithm to be used for archive encryption.
+ * Set to 'none' to disable encryption.
+ *
+ * Supported: 'none', 'default', 'aes128', 'aes192', 'aes256'
+ *
+ * When set to 'default', we'll use AES-256 if available on your system.
+ */
+ 'encryption' => 'default',
+
+ /*
+ * After creating the zip, verify it can be opened and contains files.
+ * Recommended for critical backups but adds a small overhead.
+ */
+ 'verify_backup' => false,
+
+ /*
+ * The number of attempts, in case the backup command encounters an exception
+ */
+ 'tries' => 1,
+
+ /*
+ * The number of seconds to wait before attempting a new backup if the previous try failed
+ * Set to `0` for none
+ */
+ 'retry_delay' => 0,
+ ],
+
+ /*
+ * You can get notified when specific events occur. Out of the box you can use 'mail' and 'slack'.
+ * For Slack you need to install laravel/slack-notification-channel.
+ *
+ * You can also use your own notification classes, just make sure the class is named after one of
+ * the `Spatie\Backup\Notifications\Notifications` classes.
+ */
+ 'notifications' => [
+ 'notifications' => [
+ BackupHasFailedNotification::class => ['mail'],
+ UnhealthyBackupWasFoundNotification::class => ['mail'],
+ CleanupHasFailedNotification::class => ['mail'],
+ BackupWasSuccessfulNotification::class => ['mail'],
+ HealthyBackupWasFoundNotification::class => ['mail'],
+ CleanupWasSuccessfulNotification::class => ['mail'],
+ ],
+
+ /*
+ * Here you can specify the notifiable to which the notifications should be sent. The default
+ * notifiable will use the variables specified in this config file.
+ */
+ 'notifiable' => Notifiable::class,
+
+ 'mail' => [
+ 'to' => 'your@example.com',
+
+ 'from' => [
+ 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
+ 'name' => env('MAIL_FROM_NAME', 'Example'),
+ ],
+ ],
+
+ 'slack' => [
+ 'webhook_url' => '',
+
+ /*
+ * If this is set to null the default channel of the webhook will be used.
+ */
+ 'channel' => null,
+
+ 'username' => null,
+
+ 'icon' => null,
+ ],
+
+ 'discord' => [
+ 'webhook_url' => '',
+
+ /*
+ * If this is an empty string, the name field on the webhook will be used.
+ */
+ 'username' => '',
+
+ /*
+ * If this is an empty string, the avatar on the webhook will be used.
+ */
+ 'avatar_url' => '',
+ ],
+
+ /*
+ * A generic webhook channel that POSTs JSON to a URL.
+ * Useful for Mattermost, Microsoft Teams, or custom integrations.
+ */
+ 'webhook' => [
+ 'url' => '',
+ ],
+ ],
+
+ /*
+ * The log channel used for backup activity messages.
+ *
+ * Set to a channel name defined in config/logging.php to use that channel.
+ * Set to false to disable backup logging entirely.
+ * Set to null to use the default log channel.
+ */
+ 'log_channel' => null,
+
+ /*
+ * Here you can specify which backups should be monitored.
+ * If a backup does not meet the specified requirements the
+ * UnHealthyBackupWasFound event will be fired.
+ */
+ 'monitor_backups' => [
+ [
+ 'name' => env('APP_NAME', 'laravel-backup'),
+ 'disks' => ['local'],
+ 'health_checks' => [
+ MaximumAgeInDays::class => 1,
+ MaximumStorageInMegabytes::class => 5000,
+ ],
+ ],
+
+ /*
+ [
+ 'name' => 'name of the second app',
+ 'disks' => ['local', 's3'],
+ 'health_checks' => [
+ \Spatie\Backup\Tasks\Monitor\HealthChecks\MaximumAgeInDays::class => 1,
+ \Spatie\Backup\Tasks\Monitor\HealthChecks\MaximumStorageInMegabytes::class => 5000,
+ ],
+ ],
+ */
+ ],
+
+ 'cleanup' => [
+ /*
+ * The strategy that will be used to cleanup old backups. The default strategy
+ * will keep all backups for a certain amount of days. After that period only
+ * a daily backup will be kept. After that period only weekly backups will
+ * be kept and so on.
+ *
+ * No matter how you configure it the default strategy will never
+ * delete the newest backup.
+ */
+ 'strategy' => DefaultStrategy::class,
+
+ 'default_strategy' => [
+ /*
+ * The number of days for which backups must be kept.
+ */
+ 'keep_all_backups_for_days' => 7,
+
+ /*
+ * After the "keep_all_backups_for_days" period is over, the most recent backup
+ * of that day will be kept. Older backups within the same day will be removed.
+ * If you create backups only once a day, no backups will be removed yet.
+ */
+ 'keep_daily_backups_for_days' => 16,
+
+ /*
+ * After the "keep_daily_backups_for_days" period is over, the most recent backup
+ * of that week will be kept. Older backups within the same week will be removed.
+ * If you create backups only once a week, no backups will be removed yet.
+ */
+ 'keep_weekly_backups_for_weeks' => 8,
+
+ /*
+ * After the "keep_weekly_backups_for_weeks" period is over, the most recent backup
+ * of that month will be kept. Older backups within the same month will be removed.
+ */
+ 'keep_monthly_backups_for_months' => 4,
+
+ /*
+ * After the "keep_monthly_backups_for_months" period is over, the most recent backup
+ * of that year will be kept. Older backups within the same year will be removed.
+ */
+ 'keep_yearly_backups_for_years' => 2,
+
+ /*
+ * After cleaning up the backups remove the oldest backup until
+ * this amount of megabytes has been reached.
+ * Set null for unlimited size.
+ */
+ 'delete_oldest_backups_when_using_more_megabytes_than' => 5000,
+ ],
+
+ /*
+ * The number of attempts, in case the cleanup command encounters an exception
+ */
+ 'tries' => 1,
+
+ /*
+ * The number of seconds to wait before attempting a new cleanup if the previous try failed
+ * Set to `0` for none
+ */
+ 'retry_delay' => 0,
+ ],
+
+];
diff --git a/config/cache.php b/config/cache.php
new file mode 100644
index 0000000..c68acdf
--- /dev/null
+++ b/config/cache.php
@@ -0,0 +1,130 @@
+ env('CACHE_STORE', 'database'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Cache Stores
+ |--------------------------------------------------------------------------
+ |
+ | Here you may define all of the cache "stores" for your application as
+ | well as their drivers. You may even define multiple stores for the
+ | same cache driver to group types of items stored in your caches.
+ |
+ | Supported drivers: "array", "database", "file", "memcached",
+ | "redis", "dynamodb", "octane",
+ | "failover", "null"
+ |
+ */
+
+ 'stores' => [
+
+ 'array' => [
+ 'driver' => 'array',
+ 'serialize' => false,
+ ],
+
+ 'database' => [
+ 'driver' => 'database',
+ 'connection' => env('DB_CACHE_CONNECTION'),
+ 'table' => env('DB_CACHE_TABLE', 'cache'),
+ 'lock_connection' => env('DB_CACHE_LOCK_CONNECTION'),
+ 'lock_table' => env('DB_CACHE_LOCK_TABLE'),
+ ],
+
+ 'file' => [
+ 'driver' => 'file',
+ 'path' => storage_path('framework/cache/data'),
+ 'lock_path' => storage_path('framework/cache/data'),
+ ],
+
+ 'memcached' => [
+ 'driver' => 'memcached',
+ 'persistent_id' => env('MEMCACHED_PERSISTENT_ID'),
+ 'sasl' => [
+ env('MEMCACHED_USERNAME'),
+ env('MEMCACHED_PASSWORD'),
+ ],
+ 'options' => [
+ // Memcached::OPT_CONNECT_TIMEOUT => 2000,
+ ],
+ 'servers' => [
+ [
+ 'host' => env('MEMCACHED_HOST', '127.0.0.1'),
+ 'port' => env('MEMCACHED_PORT', 11211),
+ 'weight' => 100,
+ ],
+ ],
+ ],
+
+ 'redis' => [
+ 'driver' => 'redis',
+ 'connection' => env('REDIS_CACHE_CONNECTION', 'cache'),
+ 'lock_connection' => env('REDIS_CACHE_LOCK_CONNECTION', 'default'),
+ ],
+
+ 'dynamodb' => [
+ 'driver' => 'dynamodb',
+ 'key' => env('AWS_ACCESS_KEY_ID'),
+ 'secret' => env('AWS_SECRET_ACCESS_KEY'),
+ 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
+ 'table' => env('DYNAMODB_CACHE_TABLE', 'cache'),
+ 'endpoint' => env('DYNAMODB_ENDPOINT'),
+ ],
+
+ 'octane' => [
+ 'driver' => 'octane',
+ ],
+
+ 'failover' => [
+ 'driver' => 'failover',
+ 'stores' => [
+ 'database',
+ 'array',
+ ],
+ ],
+
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Cache Key Prefix
+ |--------------------------------------------------------------------------
+ |
+ | When utilizing the APC, database, memcached, Redis, and DynamoDB cache
+ | stores, there might be other applications using the same cache. For
+ | that reason, you may prefix every cache key to avoid collisions.
+ |
+ */
+
+ 'prefix' => env('CACHE_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-cache-'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Serializable Classes
+ |--------------------------------------------------------------------------
+ |
+ | This value determines the classes that can be unserialized from cache
+ | storage. By default, no PHP classes will be unserialized from your
+ | cache to prevent gadget chain attacks if your APP_KEY is leaked.
+ |
+ */
+
+ 'serializable_classes' => false,
+
+];
diff --git a/config/database.php b/config/database.php
new file mode 100644
index 0000000..64709ce
--- /dev/null
+++ b/config/database.php
@@ -0,0 +1,184 @@
+ env('DB_CONNECTION', 'sqlite'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Database Connections
+ |--------------------------------------------------------------------------
+ |
+ | Below are all of the database connections defined for your application.
+ | An example configuration is provided for each database system which
+ | is supported by Laravel. You're free to add / remove connections.
+ |
+ */
+
+ 'connections' => [
+
+ 'sqlite' => [
+ 'driver' => 'sqlite',
+ 'url' => env('DB_URL'),
+ 'database' => env('DB_DATABASE', database_path('database.sqlite')),
+ 'prefix' => '',
+ 'foreign_key_constraints' => env('DB_FOREIGN_KEYS', true),
+ 'busy_timeout' => null,
+ 'journal_mode' => null,
+ 'synchronous' => null,
+ 'transaction_mode' => 'DEFERRED',
+ ],
+
+ 'mysql' => [
+ 'driver' => 'mysql',
+ 'url' => env('DB_URL'),
+ 'host' => env('DB_HOST', '127.0.0.1'),
+ 'port' => env('DB_PORT', '3306'),
+ 'database' => env('DB_DATABASE', 'laravel'),
+ 'username' => env('DB_USERNAME', 'root'),
+ 'password' => env('DB_PASSWORD', ''),
+ 'unix_socket' => env('DB_SOCKET', ''),
+ 'charset' => env('DB_CHARSET', 'utf8mb4'),
+ 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
+ 'prefix' => '',
+ 'prefix_indexes' => true,
+ 'strict' => true,
+ 'engine' => null,
+ 'options' => extension_loaded('pdo_mysql') ? array_filter([
+ (PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
+ ]) : [],
+ ],
+
+ 'mariadb' => [
+ 'driver' => 'mariadb',
+ 'url' => env('DB_URL'),
+ 'host' => env('DB_HOST', '127.0.0.1'),
+ 'port' => env('DB_PORT', '3306'),
+ 'database' => env('DB_DATABASE', 'laravel'),
+ 'username' => env('DB_USERNAME', 'root'),
+ 'password' => env('DB_PASSWORD', ''),
+ 'unix_socket' => env('DB_SOCKET', ''),
+ 'charset' => env('DB_CHARSET', 'utf8mb4'),
+ 'collation' => env('DB_COLLATION', 'utf8mb4_unicode_ci'),
+ 'prefix' => '',
+ 'prefix_indexes' => true,
+ 'strict' => true,
+ 'engine' => null,
+ 'options' => extension_loaded('pdo_mysql') ? array_filter([
+ (PHP_VERSION_ID >= 80500 ? Mysql::ATTR_SSL_CA : PDO::MYSQL_ATTR_SSL_CA) => env('MYSQL_ATTR_SSL_CA'),
+ ]) : [],
+ ],
+
+ 'pgsql' => [
+ 'driver' => 'pgsql',
+ 'url' => env('DB_URL'),
+ 'host' => env('DB_HOST', '127.0.0.1'),
+ 'port' => env('DB_PORT', '5432'),
+ 'database' => env('DB_DATABASE', 'laravel'),
+ 'username' => env('DB_USERNAME', 'root'),
+ 'password' => env('DB_PASSWORD', ''),
+ 'charset' => env('DB_CHARSET', 'utf8'),
+ 'prefix' => '',
+ 'prefix_indexes' => true,
+ 'search_path' => 'public',
+ 'sslmode' => env('DB_SSLMODE', 'prefer'),
+ ],
+
+ 'sqlsrv' => [
+ 'driver' => 'sqlsrv',
+ 'url' => env('DB_URL'),
+ 'host' => env('DB_HOST', 'localhost'),
+ 'port' => env('DB_PORT', '1433'),
+ 'database' => env('DB_DATABASE', 'laravel'),
+ 'username' => env('DB_USERNAME', 'root'),
+ 'password' => env('DB_PASSWORD', ''),
+ 'charset' => env('DB_CHARSET', 'utf8'),
+ 'prefix' => '',
+ 'prefix_indexes' => true,
+ // 'encrypt' => env('DB_ENCRYPT', 'yes'),
+ // 'trust_server_certificate' => env('DB_TRUST_SERVER_CERTIFICATE', 'false'),
+ ],
+
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Migration Repository Table
+ |--------------------------------------------------------------------------
+ |
+ | This table keeps track of all the migrations that have already run for
+ | your application. Using this information, we can determine which of
+ | the migrations on disk haven't actually been run on the database.
+ |
+ */
+
+ 'migrations' => [
+ 'table' => 'migrations',
+ 'update_date_on_publish' => true,
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Redis Databases
+ |--------------------------------------------------------------------------
+ |
+ | Redis is an open source, fast, and advanced key-value store that also
+ | provides a richer body of commands than a typical key-value system
+ | such as Memcached. You may define your connection settings here.
+ |
+ */
+
+ 'redis' => [
+
+ 'client' => env('REDIS_CLIENT', 'phpredis'),
+
+ 'options' => [
+ 'cluster' => env('REDIS_CLUSTER', 'redis'),
+ 'prefix' => env('REDIS_PREFIX', Str::slug((string) env('APP_NAME', 'laravel')).'-database-'),
+ 'persistent' => env('REDIS_PERSISTENT', false),
+ ],
+
+ 'default' => [
+ 'url' => env('REDIS_URL'),
+ 'host' => env('REDIS_HOST', '127.0.0.1'),
+ 'username' => env('REDIS_USERNAME'),
+ 'password' => env('REDIS_PASSWORD'),
+ 'port' => env('REDIS_PORT', '6379'),
+ 'database' => env('REDIS_DB', '0'),
+ 'max_retries' => env('REDIS_MAX_RETRIES', 3),
+ 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
+ 'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
+ 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
+ ],
+
+ 'cache' => [
+ 'url' => env('REDIS_URL'),
+ 'host' => env('REDIS_HOST', '127.0.0.1'),
+ 'username' => env('REDIS_USERNAME'),
+ 'password' => env('REDIS_PASSWORD'),
+ 'port' => env('REDIS_PORT', '6379'),
+ 'database' => env('REDIS_CACHE_DB', '1'),
+ 'max_retries' => env('REDIS_MAX_RETRIES', 3),
+ 'backoff_algorithm' => env('REDIS_BACKOFF_ALGORITHM', 'decorrelated_jitter'),
+ 'backoff_base' => env('REDIS_BACKOFF_BASE', 100),
+ 'backoff_cap' => env('REDIS_BACKOFF_CAP', 1000),
+ ],
+
+ ],
+
+];
diff --git a/config/datatables-buttons.php b/config/datatables-buttons.php
new file mode 100644
index 0000000..f5406bd
--- /dev/null
+++ b/config/datatables-buttons.php
@@ -0,0 +1,89 @@
+ [
+ /*
+ * Base namespace/directory to create the new file.
+ * This is appended on default Laravel namespace.
+ * Usage: php artisan datatables:make User
+ * Output: App\DataTables\UserDataTable
+ * With Model: App\User (default model)
+ * Export filename: users_timestamp
+ */
+ 'base' => 'Http\DataTables',
+
+ /*
+ * Base namespace/directory where your model's are located.
+ * This is appended on default Laravel namespace.
+ * Usage: php artisan datatables:make Post --model
+ * Output: App\DataTables\PostDataTable
+ * With Model: App\Post
+ * Export filename: posts_timestamp
+ */
+ 'model' => 'App\\Domains\\*\\Models\\',
+ ],
+
+ /*
+ * Set Custom stub folder
+ */
+ // 'stub' => '/resources/custom_stub',
+
+ /*
+ * PDF generator to be used when converting the table to pdf.
+ * Available generators: excel, snappy
+ * Snappy package: barryvdh/laravel-snappy
+ * Excel package: maatwebsite/excel
+ */
+ 'pdf_generator' => 'excel',
+
+ /*
+ * Snappy PDF options.
+ */
+ 'snappy' => [
+ 'options' => [
+ 'no-outline' => true,
+ 'margin-left' => '0',
+ 'margin-right' => '0',
+ 'margin-top' => '10mm',
+ 'margin-bottom' => '10mm',
+ ],
+ 'orientation' => 'landscape',
+ ],
+
+ /*
+ * Default html builder parameters.
+ */
+ 'parameters' => [
+ 'dom' => '<"dt-header d-flex justify-content-between align-items-center p-3"Bf><"table-responsive"t><"d-flex justify-content-between align-items-center p-3"ip>',
+ 'order' => [[-1, 'desc']],
+ 'buttons' => [
+ 'excel',
+ 'pdf',
+ 'reload',
+ ],
+ ],
+
+ /*
+ * Generator command default options value.
+ */
+ 'generator' => [
+ /*
+ * Default columns to generate when not set.
+ */
+ 'columns' => 'DT_RowIndex,add your columns,created_at,updated_at',
+
+ /*
+ * Default buttons to generate when not set.
+ */
+ 'buttons' => 'excel,pdf,reload',
+
+ /*
+ * Default DOM to generate when not set.
+ */
+ 'dom' => '<"dt-header"Bf><"table-responsive"t><"d-flex justify-content-between align-items-center p-3"ip>',
+
+ ],
+];
diff --git a/config/datatables-export.php b/config/datatables-export.php
new file mode 100644
index 0000000..87e77c7
--- /dev/null
+++ b/config/datatables-export.php
@@ -0,0 +1,109 @@
+ 'lazy',
+
+ /*
+ |--------------------------------------------------------------------------
+ | Chunk Size
+ |--------------------------------------------------------------------------
+ |
+ | Chunk size to be used when using lazy method.
+ |
+ */
+ 'chunk' => 1000,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Export filesystem disk
+ |--------------------------------------------------------------------------
+ |
+ | Export filesystem disk where generated files will be stored.
+ |
+ */
+ 'disk' => 'local',
+
+ /*
+ |--------------------------------------------------------------------------
+ | Use S3 for final file destination
+ |--------------------------------------------------------------------------
+ |
+ | After generating the file locally, it can be uploaded to s3.
+ |
+ */
+ 's3_disk' => '',
+
+ /*
+ |--------------------------------------------------------------------------
+ | Mail from address
+ |--------------------------------------------------------------------------
+ |
+ | Will be used to email report from this address.
+ |
+ */
+ 'mail_from' => env('MAIL_FROM_ADDRESS', ''),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Default Date Format
+ |--------------------------------------------------------------------------
+ |
+ | Default export format for date.
+ |
+ */
+ 'default_date_format' => 'yyyy-mm-dd',
+
+ /*
+ |--------------------------------------------------------------------------
+ | Valid Date Formats
+ |--------------------------------------------------------------------------
+ |
+ | List of valid date formats to be used for auto-detection.
+ |
+ */
+ 'date_formats' => [
+ 'mm/dd/yyyy',
+ ...NumberFormat::DATE_TIME_OR_DATETIME_ARRAY,
+ ...NumberFormat::TIME_OR_DATETIME_ARRAY,
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Valid Text Formats
+ |--------------------------------------------------------------------------
+ |
+ | List of valid text formats to be used.
+ |
+ */
+ 'text_formats' => [
+ NumberFormat::FORMAT_TEXT,
+ NumberFormat::FORMAT_GENERAL,
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Purge Options
+ |--------------------------------------------------------------------------
+ |
+ | Purge all exported by purge.days old files.
+ |
+ */
+ 'purge' => [
+ 'days' => 1,
+ ],
+];
diff --git a/config/datatables-html.php b/config/datatables-html.php
new file mode 100644
index 0000000..40a996a
--- /dev/null
+++ b/config/datatables-html.php
@@ -0,0 +1,27 @@
+ 'LaravelDataTables',
+
+ /*
+ * Default table attributes when generating the table.
+ */
+ 'table' => [
+ 'class' => 'table',
+ 'id' => 'dataTableBuilder',
+ ],
+
+ /*
+ * Html builder script template.
+ */
+ 'script' => 'datatables::script',
+
+ /*
+ * Html builder script template for DataTables Editor integration.
+ */
+ 'editor' => 'datatables::editor',
+];
diff --git a/config/datatables.php b/config/datatables.php
new file mode 100644
index 0000000..eafb1ef
--- /dev/null
+++ b/config/datatables.php
@@ -0,0 +1,135 @@
+ [
+ /*
+ * Smart search will enclose search keyword with wildcard string "%keyword%".
+ * SQL: column LIKE "%keyword%"
+ */
+ 'smart' => true,
+
+ /*
+ * Multi-term search will explode search keyword using spaces resulting into multiple term search.
+ */
+ 'multi_term' => true,
+
+ /*
+ * Case insensitive will search the keyword in lower case format.
+ * SQL: LOWER(column) LIKE LOWER(keyword)
+ */
+ 'case_insensitive' => true,
+
+ /*
+ * Wild card will add "%" in between every characters of the keyword.
+ * SQL: column LIKE "%k%e%y%w%o%r%d%"
+ */
+ 'use_wildcards' => false,
+
+ /*
+ * Perform a search which starts with the given keyword.
+ * SQL: column LIKE "keyword%"
+ */
+ 'starts_with' => false,
+ ],
+
+ /*
+ * DataTables internal index id response column name.
+ */
+ 'index_column' => 'DT_RowIndex',
+
+ /*
+ * List of available builders for DataTables.
+ * This is where you can register your custom DataTables builder.
+ */
+ 'engines' => [
+ 'eloquent' => EloquentDataTable::class,
+ 'query' => QueryDataTable::class,
+ 'collection' => CollectionDataTable::class,
+ 'paginator' => PaginatorDataTable::class,
+ 'resource' => ApiResourceDataTable::class,
+ ],
+
+ /*
+ * DataTables accepted builder to engine mapping.
+ * This is where you can override which engine a builder should use
+ * Note, only change this if you know what you are doing!
+ */
+ 'builders' => [
+ // Illuminate\Database\Eloquent\Relations\Relation::class => 'eloquent',
+ // Illuminate\Database\Eloquent\Builder::class => 'eloquent',
+ // Illuminate\Database\Query\Builder::class => 'query',
+ // Illuminate\Support\Collection::class => 'collection',
+ // Illuminate\Pagination\LengthAwarePaginator::class => 'paginator',
+ ],
+
+ /*
+ * Nulls last sql pattern for PostgreSQL & Oracle.
+ * For MySQL, use 'CASE WHEN :column IS NULL THEN 1 ELSE 0 END, :column :direction'
+ */
+ 'nulls_last_sql' => ':column :direction NULLS LAST',
+
+ /*
+ * User friendly message to be displayed on user if error occurs.
+ * Possible values:
+ * null - The exception message will be used on error response.
+ * 'throw' - Throws a \Yajra\DataTables\Exceptions\Exception. Use your custom error handler if needed.
+ * 'custom message' - Any friendly message to be displayed to the user. You can also use translation key.
+ */
+ 'error' => env('DATATABLES_ERROR', null),
+
+ /*
+ * Default columns definition of DataTable utility functions.
+ */
+ 'columns' => [
+ /*
+ * List of columns hidden/removed on json response.
+ */
+ 'excess' => ['rn', 'row_num'],
+
+ /*
+ * List of columns to be escaped. If set to *, all columns are escape.
+ * Note: You can set the value to empty array to disable XSS protection.
+ */
+ 'escape' => '*',
+
+ /*
+ * List of columns that are allowed to display html content.
+ * Note: Adding columns to list will make us available to XSS attacks.
+ */
+ 'raw' => ['action'],
+
+ /*
+ * List of columns are forbidden from being searched/sorted.
+ */
+ 'blacklist' => ['password', 'remember_token'],
+
+ /*
+ * List of columns that are only allowed for search/sort.
+ * If set to *, all columns are allowed.
+ */
+ 'whitelist' => '*',
+ ],
+
+ /*
+ * JsonResponse header and options config.
+ */
+ 'json' => [
+ 'header' => [],
+ 'options' => 0,
+ ],
+
+ /*
+ * Default condition to determine if a parameter is a callback or not.
+ * Callbacks needs to start by those terms, or they will be cast to string.
+ */
+ 'callback' => ['$', '$.', 'function'],
+];
diff --git a/config/excel.php b/config/excel.php
new file mode 100644
index 0000000..55e3199
--- /dev/null
+++ b/config/excel.php
@@ -0,0 +1,381 @@
+ [
+
+ /*
+ |--------------------------------------------------------------------------
+ | Chunk size
+ |--------------------------------------------------------------------------
+ |
+ | When using FromQuery, the query is automatically chunked.
+ | Here you can specify how big the chunk should be.
+ |
+ */
+ 'chunk_size' => 100,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Pre-calculate formulas during export
+ |--------------------------------------------------------------------------
+ */
+ 'pre_calculate_formulas' => false,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Enable strict null comparison
+ |--------------------------------------------------------------------------
+ |
+ | When enabling strict null comparison empty cells ('') will
+ | be added to the sheet.
+ */
+ 'strict_null_comparison' => false,
+
+ /*
+ |--------------------------------------------------------------------------
+ | CSV Settings
+ |--------------------------------------------------------------------------
+ |
+ | Configure e.g. delimiter, enclosure and line ending for CSV exports.
+ |
+ */
+ 'csv' => [
+ 'delimiter' => ',',
+ 'enclosure' => '"',
+ 'line_ending' => PHP_EOL,
+ 'use_bom' => false,
+ 'include_separator_line' => false,
+ 'excel_compatibility' => false,
+ 'output_encoding' => '',
+ 'test_auto_detect' => true,
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Worksheet properties
+ |--------------------------------------------------------------------------
+ |
+ | Configure e.g. default title, creator, subject,...
+ |
+ */
+ 'properties' => [
+ 'creator' => '',
+ 'lastModifiedBy' => '',
+ 'title' => '',
+ 'description' => '',
+ 'subject' => '',
+ 'keywords' => '',
+ 'category' => '',
+ 'manager' => '',
+ 'company' => '',
+ ],
+ ],
+
+ 'imports' => [
+
+ /*
+ |--------------------------------------------------------------------------
+ | Read Only
+ |--------------------------------------------------------------------------
+ |
+ | When dealing with imports, you might only be interested in the
+ | data that the sheet exists. By default we ignore all styles,
+ | however if you want to do some logic based on style data
+ | you can enable it by setting read_only to false.
+ |
+ */
+ 'read_only' => true,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Ignore Empty
+ |--------------------------------------------------------------------------
+ |
+ | When dealing with imports, you might be interested in ignoring
+ | rows that have null values or empty strings. By default rows
+ | containing empty strings or empty values are not ignored but can be
+ | ignored by enabling the setting ignore_empty to true.
+ |
+ */
+ 'ignore_empty' => false,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Heading Row Formatter
+ |--------------------------------------------------------------------------
+ |
+ | Configure the heading row formatter.
+ | Available options: none|slug|custom
+ |
+ */
+ 'heading_row' => [
+ 'formatter' => 'slug',
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | CSV Settings
+ |--------------------------------------------------------------------------
+ |
+ | Configure e.g. delimiter, enclosure and line ending for CSV imports.
+ |
+ */
+ 'csv' => [
+ 'delimiter' => null,
+ 'enclosure' => '"',
+ 'escape_character' => '\\',
+ 'contiguous' => false,
+ 'input_encoding' => Csv::GUESS_ENCODING,
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Worksheet properties
+ |--------------------------------------------------------------------------
+ |
+ | Configure e.g. default title, creator, subject,...
+ |
+ */
+ 'properties' => [
+ 'creator' => '',
+ 'lastModifiedBy' => '',
+ 'title' => '',
+ 'description' => '',
+ 'subject' => '',
+ 'keywords' => '',
+ 'category' => '',
+ 'manager' => '',
+ 'company' => '',
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Cell Middleware
+ |--------------------------------------------------------------------------
+ |
+ | Configure middleware that is executed on getting a cell value
+ |
+ */
+ 'cells' => [
+ 'middleware' => [
+ // \Maatwebsite\Excel\Middleware\TrimCellValue::class,
+ // \Maatwebsite\Excel\Middleware\ConvertEmptyCellValuesToNull::class,
+ ],
+ ],
+
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Extension detector
+ |--------------------------------------------------------------------------
+ |
+ | Configure here which writer/reader type should be used when the package
+ | needs to guess the correct type based on the extension alone.
+ |
+ */
+ 'extension_detector' => [
+ 'xlsx' => Excel::XLSX,
+ 'xlsm' => Excel::XLSX,
+ 'xltx' => Excel::XLSX,
+ 'xltm' => Excel::XLSX,
+ 'xls' => Excel::XLS,
+ 'xlt' => Excel::XLS,
+ 'ods' => Excel::ODS,
+ 'ots' => Excel::ODS,
+ 'slk' => Excel::SLK,
+ 'xml' => Excel::XML,
+ 'gnumeric' => Excel::GNUMERIC,
+ 'htm' => Excel::HTML,
+ 'html' => Excel::HTML,
+ 'csv' => Excel::CSV,
+ 'tsv' => Excel::TSV,
+
+ /*
+ |--------------------------------------------------------------------------
+ | PDF Extension
+ |--------------------------------------------------------------------------
+ |
+ | Configure here which Pdf driver should be used by default.
+ | Available options: Excel::MPDF | Excel::TCPDF | Excel::DOMPDF
+ |
+ */
+ 'pdf' => Excel::DOMPDF,
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Value Binder
+ |--------------------------------------------------------------------------
+ |
+ | PhpSpreadsheet offers a way to hook into the process of a value being
+ | written to a cell. In there some assumptions are made on how the
+ | value should be formatted. If you want to change those defaults,
+ | you can implement your own default value binder.
+ |
+ | Possible value binders:
+ |
+ | [x] Maatwebsite\Excel\DefaultValueBinder::class
+ | [x] PhpOffice\PhpSpreadsheet\Cell\StringValueBinder::class
+ | [x] PhpOffice\PhpSpreadsheet\Cell\AdvancedValueBinder::class
+ |
+ */
+ 'value_binder' => [
+ 'default' => DefaultValueBinder::class,
+ ],
+
+ 'cache' => [
+ /*
+ |--------------------------------------------------------------------------
+ | Default cell caching driver
+ |--------------------------------------------------------------------------
+ |
+ | By default PhpSpreadsheet keeps all cell values in memory, however when
+ | dealing with large files, this might result into memory issues. If you
+ | want to mitigate that, you can configure a cell caching driver here.
+ | When using the illuminate driver, it will store each value in the
+ | cache store. This can slow down the process, because it needs to
+ | store each value. You can use the "batch" store if you want to
+ | only persist to the store when the memory limit is reached.
+ |
+ | Drivers: memory|illuminate|batch
+ |
+ */
+ 'driver' => 'memory',
+
+ /*
+ |--------------------------------------------------------------------------
+ | Batch memory caching
+ |--------------------------------------------------------------------------
+ |
+ | When dealing with the "batch" caching driver, it will only
+ | persist to the store when the memory limit is reached.
+ | Here you can tweak the memory limit to your liking.
+ |
+ */
+ 'batch' => [
+ 'memory_limit' => 10000,
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Illuminate cache
+ |--------------------------------------------------------------------------
+ |
+ | When using the "illuminate" caching driver, it will automatically use
+ | your default cache store. However if you prefer to have the cell
+ | cache on a separate store, you can configure the store name here.
+ | You can use any store defined in your cache config. When leaving
+ | at "null" it will use the default store.
+ |
+ */
+ 'illuminate' => [
+ 'store' => null,
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Cache Time-to-live (TTL)
+ |--------------------------------------------------------------------------
+ |
+ | The TTL of items written to cache. If you want to keep the items cached
+ | indefinitely, set this to null. Otherwise, set a number of seconds,
+ | a \DateInterval, or a callable.
+ |
+ | Allowable types: callable|\DateInterval|int|null
+ |
+ */
+ 'default_ttl' => 10800,
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Transaction Handler
+ |--------------------------------------------------------------------------
+ |
+ | By default the import is wrapped in a transaction. This is useful
+ | for when an import may fail and you want to retry it. With the
+ | transactions, the previous import gets rolled-back.
+ |
+ | You can disable the transaction handler by setting this to null.
+ | Or you can choose a custom made transaction handler here.
+ |
+ | Supported handlers: null|db
+ |
+ */
+ 'transactions' => [
+ 'handler' => 'db',
+ 'db' => [
+ 'connection' => null,
+ ],
+ ],
+
+ 'temporary_files' => [
+
+ /*
+ |--------------------------------------------------------------------------
+ | Local Temporary Path
+ |--------------------------------------------------------------------------
+ |
+ | When exporting and importing files, we use a temporary file, before
+ | storing reading or downloading. Here you can customize that path.
+ | permissions is an array with the permission flags for the directory (dir)
+ | and the create file (file).
+ |
+ */
+ 'local_path' => storage_path('framework/cache/laravel-excel'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Local Temporary Path Permissions
+ |--------------------------------------------------------------------------
+ |
+ | Permissions is an array with the permission flags for the directory (dir)
+ | and the create file (file).
+ | If omitted the default permissions of the filesystem will be used.
+ |
+ */
+ 'local_permissions' => [
+ // 'dir' => 0755,
+ // 'file' => 0644,
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Remote Temporary Disk
+ |--------------------------------------------------------------------------
+ |
+ | When dealing with a multi server setup with queues in which you
+ | cannot rely on having a shared local temporary path, you might
+ | want to store the temporary file on a shared disk. During the
+ | queue executing, we'll retrieve the temporary file from that
+ | location instead. When left to null, it will always use
+ | the local path. This setting only has effect when using
+ | in conjunction with queued imports and exports.
+ |
+ */
+ 'remote_disk' => null,
+ 'remote_prefix' => null,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Force Resync
+ |--------------------------------------------------------------------------
+ |
+ | When dealing with a multi server setup as above, it's possible
+ | for the clean up that occurs after entire queue has been run to only
+ | cleanup the server that the last AfterImportJob runs on. The rest of the server
+ | would still have the local temporary file stored on it. In this case your
+ | local storage limits can be exceeded and future imports won't be processed.
+ | To mitigate this you can set this config value to be true, so that after every
+ | queued chunk is processed the local temporary file is deleted on the server that
+ | processed it.
+ |
+ */
+ 'force_resync_remote' => null,
+ ],
+];
diff --git a/config/filesystems.php b/config/filesystems.php
new file mode 100644
index 0000000..a112158
--- /dev/null
+++ b/config/filesystems.php
@@ -0,0 +1,87 @@
+ env('FILESYSTEM_DISK', 'local'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Filesystem Disks
+ |--------------------------------------------------------------------------
+ |
+ | Below you may configure as many filesystem disks as necessary, and you
+ | may even configure multiple disks for the same driver. Examples for
+ | most supported storage drivers are configured here for reference.
+ |
+ | Supported drivers: "local", "ftp", "sftp", "s3"
+ |
+ */
+
+ 'disks' => [
+
+ 'local' => [
+ 'driver' => 'local',
+ 'root' => storage_path('app/private'),
+ 'serve' => true,
+ 'throw' => false,
+ 'report' => false,
+ ],
+
+ 'backup' => [
+ 'driver' => 'local',
+ 'root' => storage_path('app/backups'),
+ 'serve' => true,
+ 'url' => '/storage/backups',
+ ],
+
+ 'public' => [
+ 'driver' => 'local',
+ 'root' => storage_path('app/public'),
+ 'url' => rtrim(env('APP_URL', 'http://localhost'), '/').'/storage',
+ 'visibility' => 'public',
+ 'throw' => false,
+ 'report' => false,
+ ],
+
+ 's3' => [
+ 'driver' => 's3',
+ 'key' => env('AWS_ACCESS_KEY_ID'),
+ 'secret' => env('AWS_SECRET_ACCESS_KEY'),
+ 'region' => env('AWS_DEFAULT_REGION'),
+ 'bucket' => env('AWS_BUCKET'),
+ 'url' => env('AWS_URL'),
+ 'endpoint' => env('AWS_ENDPOINT'),
+ 'use_path_style_endpoint' => env('AWS_USE_PATH_STYLE_ENDPOINT', false),
+ 'throw' => false,
+ 'report' => false,
+ ],
+
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Symbolic Links
+ |--------------------------------------------------------------------------
+ |
+ | Here you may configure the symbolic links that will be created when the
+ | `storage:link` Artisan command is executed. The array keys should be
+ | the locations of the links and the values should be their targets.
+ |
+ */
+
+ 'links' => [
+ public_path('storage') => storage_path('app/public'),
+ ],
+
+];
diff --git a/config/livewire.php b/config/livewire.php
new file mode 100644
index 0000000..f8492a0
--- /dev/null
+++ b/config/livewire.php
@@ -0,0 +1,296 @@
+ [
+ resource_path('views/components'),
+ resource_path('views/livewire'),
+ ],
+
+ /*
+ |---------------------------------------------------------------------------
+ | Component Namespaces
+ |---------------------------------------------------------------------------
+ |
+ | This value sets default namespaces that will be used to resolve view-based
+ | components like single-file and multi-file components. These folders'll
+ | also be referenced when creating new components via the make command.
+ |
+ */
+
+ 'component_namespaces' => [
+ 'layouts' => resource_path('views/components/layouts'),
+ 'pages' => resource_path('views/pages'),
+ 'widgets' => resource_path('views/widgets'),
+ ],
+
+ /*
+ |---------------------------------------------------------------------------
+ | Page Layout
+ |---------------------------------------------------------------------------
+ | The view that will be used as the layout when rendering a single component as
+ | an entire page via `Route::livewire('/post/create', 'pages::create-post')`.
+ | In this case, the content of pages::create-post will render into $slot.
+ |
+ */
+
+ 'component_layout' => 'layouts::app',
+
+ /*
+ |---------------------------------------------------------------------------
+ | Lazy Loading Placeholder
+ |---------------------------------------------------------------------------
+ | Livewire allows you to lazy load components that would otherwise slow down
+ | the initial page load. Every component can have a custom placeholder or
+ | you can define the default placeholder view for all components below.
+ |
+ */
+
+ 'component_placeholder' => null, // Example: 'placeholders::skeleton'
+
+ /*
+ |---------------------------------------------------------------------------
+ | Make Command
+ |---------------------------------------------------------------------------
+ | This value determines the default configuration for the artisan make command
+ | You can configure the component type (sfc, mfc, class) and whether to use
+ | the high-voltage (⚡) emoji as a prefix in the sfc|mfc component names.
+ |
+ */
+
+ 'make_command' => [
+ 'type' => 'mfc', // Options: 'sfc', 'mfc', 'class'
+ 'emoji' => true, // Options: true, false
+ 'with' => [
+ 'js' => false,
+ 'css' => false,
+ 'test' => false,
+ ],
+ ],
+
+ /*
+ |---------------------------------------------------------------------------
+ | Class Namespace
+ |---------------------------------------------------------------------------
+ |
+ | This value sets the root class namespace for Livewire component classes in
+ | your application. This value will change where component auto-discovery
+ | finds components. It's also referenced by the file creation commands.
+ |
+ */
+
+ 'class_namespace' => 'App\\Livewire',
+
+ /*
+ |---------------------------------------------------------------------------
+ | Class Path
+ |---------------------------------------------------------------------------
+ |
+ | This value is used to specify the path where Livewire component class files
+ | are created when running creation commands like `artisan make:livewire`.
+ | This path is customizable to match your projects directory structure.
+ |
+ */
+
+ 'class_path' => app_path('Livewire'),
+
+ /*
+ |---------------------------------------------------------------------------
+ | View Path
+ |---------------------------------------------------------------------------
+ |
+ | This value is used to specify where Livewire component Blade templates are
+ | stored when running file creation commands like `artisan make:livewire`.
+ | It is also used if you choose to omit a component's render() method.
+ |
+ */
+
+ 'view_path' => resource_path('views/livewire'),
+
+ /*
+ |---------------------------------------------------------------------------
+ | Temporary File Uploads
+ |---------------------------------------------------------------------------
+ |
+ | Livewire handles file uploads by storing uploads in a temporary directory
+ | before the file is stored permanently. All file uploads are directed to
+ | a global endpoint for temporary storage. You may configure this below:
+ |
+ */
+
+ 'temporary_file_upload' => [
+ 'disk' => env('LIVEWIRE_TEMPORARY_FILE_UPLOAD_DISK'), // Example: 'local', 's3' | Default: 'default'
+ 'rules' => null, // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB)
+ 'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp'
+ 'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1'
+ 'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs...
+ 'png',
+ 'gif',
+ 'bmp',
+ 'svg',
+ 'wav',
+ 'mp4',
+ 'mov',
+ 'avi',
+ 'wmv',
+ 'mp3',
+ 'm4a',
+ 'jpg',
+ 'jpeg',
+ 'mpga',
+ 'webp',
+ 'wma',
+ ],
+ 'max_upload_time' => 5, // Max duration (in minutes) before an upload is invalidated...
+ 'cleanup' => true, // Should cleanup temporary uploads older than 24 hrs...
+ ],
+
+ /*
+ |---------------------------------------------------------------------------
+ | Render On Redirect
+ |---------------------------------------------------------------------------
+ |
+ | This value determines if Livewire will run a component's `render()` method
+ | after a redirect has been triggered using something like `redirect(...)`
+ | Setting this to true will render the view once more before redirecting
+ |
+ */
+
+ 'render_on_redirect' => false,
+
+ /*
+ |---------------------------------------------------------------------------
+ | Eloquent Model Binding
+ |---------------------------------------------------------------------------
+ |
+ | Previous versions of Livewire supported binding directly to eloquent model
+ | properties using wire:model by default. However, this behavior has been
+ | deemed too "magical" and has therefore been put under a feature flag.
+ |
+ */
+
+ 'legacy_model_binding' => false,
+
+ /*
+ |---------------------------------------------------------------------------
+ | Auto-inject Frontend Assets
+ |---------------------------------------------------------------------------
+ |
+ | By default, Livewire automatically injects its JavaScript and CSS into the
+ | and of pages containing Livewire components. By disabling
+ | this behavior, you need to use @livewireStyles and @livewireScripts.
+ |
+ */
+
+ 'inject_assets' => false,
+
+ /*
+ |---------------------------------------------------------------------------
+ | Navigate (SPA mode)
+ |---------------------------------------------------------------------------
+ |
+ | By adding `wire:navigate` to links in your Livewire application, Livewire
+ | will prevent the default link handling and instead request those pages
+ | via AJAX, creating an SPA-like effect. Configure this behavior here.
+ |
+ */
+
+ 'navigate' => [
+ 'show_progress_bar' => true,
+ 'progress_bar_color' => '#2299dd',
+ ],
+
+ /*
+ |---------------------------------------------------------------------------
+ | HTML Morph Markers
+ |---------------------------------------------------------------------------
+ |
+ | Livewire intelligently "morphs" existing HTML into the newly rendered HTML
+ | after each update. To make this process more reliable, Livewire injects
+ | "markers" into the rendered Blade surrounding @if, @class & @foreach.
+ |
+ */
+
+ 'inject_morph_markers' => true,
+
+ /*
+ |---------------------------------------------------------------------------
+ | Smart Wire Keys
+ |---------------------------------------------------------------------------
+ |
+ | Livewire uses loops and keys used within loops to generate smart keys that
+ | are applied to nested components that don't have them. This makes using
+ | nested components more reliable by ensuring that they all have keys.
+ |
+ */
+
+ 'smart_wire_keys' => true,
+
+ /*
+ |---------------------------------------------------------------------------
+ | Pagination Theme
+ |---------------------------------------------------------------------------
+ |
+ | When enabling Livewire's pagination feature by using the `WithPagination`
+ | trait, Livewire will use Tailwind templates to render pagination views
+ | on the page. If you want Bootstrap CSS, you can specify: "bootstrap"
+ |
+ */
+
+ 'pagination_theme' => 'bootstrap',
+
+ /*
+ |---------------------------------------------------------------------------
+ | Release Token
+ |---------------------------------------------------------------------------
+ |
+ | This token is stored client-side and sent along with each request to check
+ | a users session to see if a new release has invalidated it. If there is
+ | a mismatch it will throw an error and prompt for a browser refresh.
+ |
+ */
+
+ 'release_token' => 'a',
+
+ /*
+ |---------------------------------------------------------------------------
+ | CSP Safe
+ |---------------------------------------------------------------------------
+ |
+ | This config is used to determine if Livewire will use the CSP-safe version
+ | of Alpine in its bundle. This is useful for applications that are using
+ | strict Content Security Policy (CSP) to protect against XSS attacks.
+ |
+ */
+
+ 'csp_safe' => false,
+
+ /*
+ |---------------------------------------------------------------------------
+ | Payload Guards
+ |---------------------------------------------------------------------------
+ |
+ | These settings protect against malicious or oversized payloads that could
+ | cause denial of service. The default values should feel reasonable for
+ | most web applications. Each can be set to null to disable the limit.
+ |
+ */
+
+ 'payload' => [
+ 'max_size' => 1024 * 1024, // 1MB - maximum request payload size in bytes
+ 'max_nesting_depth' => 10, // Maximum depth of dot-notation property paths
+ 'max_calls' => 50, // Maximum method calls per request
+ 'max_components' => 200, // Maximum components per batch request
+ ],
+];
diff --git a/config/logging.php b/config/logging.php
new file mode 100644
index 0000000..b09cb25
--- /dev/null
+++ b/config/logging.php
@@ -0,0 +1,132 @@
+ env('LOG_CHANNEL', 'stack'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Deprecations Log Channel
+ |--------------------------------------------------------------------------
+ |
+ | This option controls the log channel that should be used to log warnings
+ | regarding deprecated PHP and library features. This allows you to get
+ | your application ready for upcoming major versions of dependencies.
+ |
+ */
+
+ 'deprecations' => [
+ 'channel' => env('LOG_DEPRECATIONS_CHANNEL', 'null'),
+ 'trace' => env('LOG_DEPRECATIONS_TRACE', false),
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Log Channels
+ |--------------------------------------------------------------------------
+ |
+ | Here you may configure the log channels for your application. Laravel
+ | utilizes the Monolog PHP logging library, which includes a variety
+ | of powerful log handlers and formatters that you're free to use.
+ |
+ | Available drivers: "single", "daily", "slack", "syslog",
+ | "errorlog", "monolog", "custom", "stack"
+ |
+ */
+
+ 'channels' => [
+
+ 'stack' => [
+ 'driver' => 'stack',
+ 'channels' => explode(',', (string) env('LOG_STACK', 'single')),
+ 'ignore_exceptions' => false,
+ ],
+
+ 'single' => [
+ 'driver' => 'single',
+ 'path' => storage_path('logs/laravel.log'),
+ 'level' => env('LOG_LEVEL', 'debug'),
+ 'replace_placeholders' => true,
+ ],
+
+ 'daily' => [
+ 'driver' => 'daily',
+ 'path' => storage_path('logs/laravel.log'),
+ 'level' => env('LOG_LEVEL', 'debug'),
+ 'days' => env('LOG_DAILY_DAYS', 14),
+ 'replace_placeholders' => true,
+ ],
+
+ 'slack' => [
+ 'driver' => 'slack',
+ 'url' => env('LOG_SLACK_WEBHOOK_URL'),
+ 'username' => env('LOG_SLACK_USERNAME', env('APP_NAME', 'Laravel')),
+ 'emoji' => env('LOG_SLACK_EMOJI', ':boom:'),
+ 'level' => env('LOG_LEVEL', 'critical'),
+ 'replace_placeholders' => true,
+ ],
+
+ 'papertrail' => [
+ 'driver' => 'monolog',
+ 'level' => env('LOG_LEVEL', 'debug'),
+ 'handler' => env('LOG_PAPERTRAIL_HANDLER', SyslogUdpHandler::class),
+ 'handler_with' => [
+ 'host' => env('PAPERTRAIL_URL'),
+ 'port' => env('PAPERTRAIL_PORT'),
+ 'connectionString' => 'tls://'.env('PAPERTRAIL_URL').':'.env('PAPERTRAIL_PORT'),
+ ],
+ 'processors' => [PsrLogMessageProcessor::class],
+ ],
+
+ 'stderr' => [
+ 'driver' => 'monolog',
+ 'level' => env('LOG_LEVEL', 'debug'),
+ 'handler' => StreamHandler::class,
+ 'handler_with' => [
+ 'stream' => 'php://stderr',
+ ],
+ 'formatter' => env('LOG_STDERR_FORMATTER'),
+ 'processors' => [PsrLogMessageProcessor::class],
+ ],
+
+ 'syslog' => [
+ 'driver' => 'syslog',
+ 'level' => env('LOG_LEVEL', 'debug'),
+ 'facility' => env('LOG_SYSLOG_FACILITY', LOG_USER),
+ 'replace_placeholders' => true,
+ ],
+
+ 'errorlog' => [
+ 'driver' => 'errorlog',
+ 'level' => env('LOG_LEVEL', 'debug'),
+ 'replace_placeholders' => true,
+ ],
+
+ 'null' => [
+ 'driver' => 'monolog',
+ 'handler' => NullHandler::class,
+ ],
+
+ 'emergency' => [
+ 'path' => storage_path('logs/laravel.log'),
+ ],
+
+ ],
+
+];
diff --git a/config/mail.php b/config/mail.php
new file mode 100644
index 0000000..e32e88d
--- /dev/null
+++ b/config/mail.php
@@ -0,0 +1,118 @@
+ env('MAIL_MAILER', 'log'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Mailer Configurations
+ |--------------------------------------------------------------------------
+ |
+ | Here you may configure all of the mailers used by your application plus
+ | their respective settings. Several examples have been configured for
+ | you and you are free to add your own as your application requires.
+ |
+ | Laravel supports a variety of mail "transport" drivers that can be used
+ | when delivering an email. You may specify which one you're using for
+ | your mailers below. You may also add additional mailers if needed.
+ |
+ | Supported: "smtp", "sendmail", "mailgun", "ses", "ses-v2",
+ | "postmark", "resend", "log", "array",
+ | "failover", "roundrobin"
+ |
+ */
+
+ 'mailers' => [
+
+ 'smtp' => [
+ 'transport' => 'smtp',
+ 'scheme' => env('MAIL_SCHEME'),
+ 'url' => env('MAIL_URL'),
+ 'host' => env('MAIL_HOST', '127.0.0.1'),
+ 'port' => env('MAIL_PORT', 2525),
+ 'username' => env('MAIL_USERNAME'),
+ 'password' => env('MAIL_PASSWORD'),
+ 'timeout' => null,
+ 'local_domain' => env('MAIL_EHLO_DOMAIN', parse_url((string) env('APP_URL', 'http://localhost'), PHP_URL_HOST)),
+ ],
+
+ 'ses' => [
+ 'transport' => 'ses',
+ ],
+
+ 'postmark' => [
+ 'transport' => 'postmark',
+ // 'message_stream_id' => env('POSTMARK_MESSAGE_STREAM_ID'),
+ // 'client' => [
+ // 'timeout' => 5,
+ // ],
+ ],
+
+ 'resend' => [
+ 'transport' => 'resend',
+ ],
+
+ 'sendmail' => [
+ 'transport' => 'sendmail',
+ 'path' => env('MAIL_SENDMAIL_PATH', '/usr/sbin/sendmail -bs -i'),
+ ],
+
+ 'log' => [
+ 'transport' => 'log',
+ 'channel' => env('MAIL_LOG_CHANNEL'),
+ ],
+
+ 'array' => [
+ 'transport' => 'array',
+ ],
+
+ 'failover' => [
+ 'transport' => 'failover',
+ 'mailers' => [
+ 'smtp',
+ 'log',
+ ],
+ 'retry_after' => 60,
+ ],
+
+ 'roundrobin' => [
+ 'transport' => 'roundrobin',
+ 'mailers' => [
+ 'ses',
+ 'postmark',
+ ],
+ 'retry_after' => 60,
+ ],
+
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Global "From" Address
+ |--------------------------------------------------------------------------
+ |
+ | You may wish for all emails sent by your application to be sent from
+ | the same address. Here you may specify a name and address that is
+ | used globally for all emails that are sent by your application.
+ |
+ */
+
+ 'from' => [
+ 'address' => env('MAIL_FROM_ADDRESS', 'hello@example.com'),
+ 'name' => env('MAIL_FROM_NAME', env('APP_NAME', 'Laravel')),
+ ],
+
+];
diff --git a/config/permission.php b/config/permission.php
new file mode 100644
index 0000000..633ef76
--- /dev/null
+++ b/config/permission.php
@@ -0,0 +1,206 @@
+ [
+
+ /*
+ * When using the "HasPermissions" trait from this package, we need to know which
+ * Eloquent model should be used to retrieve your permissions. Of course, it
+ * is often just the "Permission" model but you may use whatever you like.
+ *
+ * The model you want to use as a Permission model needs to implement the
+ * `Spatie\Permission\Contracts\Permission` contract.
+ */
+
+ 'permission' => Permission::class,
+
+ /*
+ * When using the "HasRoles" trait from this package, we need to know which
+ * Eloquent model should be used to retrieve your roles. Of course, it
+ * is often just the "Role" model but you may use whatever you like.
+ *
+ * The model you want to use as a Role model needs to implement the
+ * `Spatie\Permission\Contracts\Role` contract.
+ */
+
+ 'role' => Role::class,
+
+ ],
+
+ 'table_names' => [
+
+ /*
+ * When using the "HasRoles" trait from this package, we need to know which
+ * table should be used to retrieve your roles. We have chosen a basic
+ * default value but you may easily change it to any table you like.
+ */
+
+ 'roles' => 'roles',
+
+ /*
+ * When using the "HasPermissions" trait from this package, we need to know which
+ * table should be used to retrieve your permissions. We have chosen a basic
+ * default value but you may easily change it to any table you like.
+ */
+
+ 'permissions' => 'permissions',
+
+ /*
+ * When using the "HasPermissions" trait from this package, we need to know which
+ * table should be used to retrieve your models permissions. We have chosen a
+ * basic default value but you may easily change it to any table you like.
+ */
+
+ 'model_has_permissions' => 'model_has_permissions',
+
+ /*
+ * When using the "HasRoles" trait from this package, we need to know which
+ * table should be used to retrieve your models roles. We have chosen a
+ * basic default value but you may easily change it to any table you like.
+ */
+
+ 'model_has_roles' => 'model_has_roles',
+
+ /*
+ * When using the "HasRoles" trait from this package, we need to know which
+ * table should be used to retrieve your roles permissions. We have chosen a
+ * basic default value but you may easily change it to any table you like.
+ */
+
+ 'role_has_permissions' => 'role_has_permissions',
+ ],
+
+ 'column_names' => [
+ /*
+ * Change this if you want to name the related pivots other than defaults
+ */
+ 'role_pivot_key' => null, // default 'role_id',
+ 'permission_pivot_key' => null, // default 'permission_id',
+
+ /*
+ * Change this if you want to name the related model primary key other than
+ * `model_id`.
+ *
+ * For example, this would be nice if your primary keys are all UUIDs. In
+ * that case, name this `model_uuid`.
+ */
+
+ 'model_morph_key' => 'model_id',
+
+ /*
+ * Change this if you want to use the teams feature and your related model's
+ * foreign key is other than `team_id`.
+ */
+
+ 'team_foreign_key' => 'team_id',
+ ],
+
+ /*
+ * When set to true, the method for checking permissions will be registered on the gate.
+ * Set this to false if you want to implement custom logic for checking permissions.
+ */
+
+ 'register_permission_check_method' => true,
+
+ /*
+ * When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered
+ * this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated
+ * NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it.
+ */
+ 'register_octane_reset_listener' => false,
+
+ /*
+ * Events will fire when a role or permission is assigned/unassigned:
+ * \Spatie\Permission\Events\RoleAttachedEvent
+ * \Spatie\Permission\Events\RoleDetachedEvent
+ * \Spatie\Permission\Events\PermissionAttachedEvent
+ * \Spatie\Permission\Events\PermissionDetachedEvent
+ *
+ * To enable, set to true, and then create listeners to watch these events.
+ */
+ 'events_enabled' => false,
+
+ /*
+ * Teams Feature.
+ * When set to true the package implements teams using the 'team_foreign_key'.
+ * If you want the migrations to register the 'team_foreign_key', you must
+ * set this to true before doing the migration.
+ * If you already did the migration then you must make a new migration to also
+ * add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions'
+ * (view the latest version of this package's migration file)
+ */
+
+ 'teams' => false,
+
+ /*
+ * The class to use to resolve the permissions team id
+ */
+ 'team_resolver' => DefaultTeamResolver::class,
+
+ /*
+ * Passport Client Credentials Grant
+ * When set to true the package will use Passports Client to check permissions
+ */
+
+ 'use_passport_client_credentials' => false,
+
+ /*
+ * When set to true, the required permission names are added to exception messages.
+ * This could be considered an information leak in some contexts, so the default
+ * setting is false here for optimum safety.
+ */
+
+ 'display_permission_in_exception' => false,
+
+ /*
+ * When set to true, the required role names are added to exception messages.
+ * This could be considered an information leak in some contexts, so the default
+ * setting is false here for optimum safety.
+ */
+
+ 'display_role_in_exception' => false,
+
+ /*
+ * By default wildcard permission lookups are disabled.
+ * See documentation to understand supported syntax.
+ */
+
+ 'enable_wildcard_permission' => false,
+
+ /*
+ * The class to use for interpreting wildcard permissions.
+ * If you need to modify delimiters, override the class and specify its name here.
+ */
+ // 'wildcard_permission' => Spatie\Permission\WildcardPermission::class,
+
+ /* Cache-specific settings */
+
+ 'cache' => [
+
+ /*
+ * By default all permissions are cached for 24 hours to speed up performance.
+ * When permissions or roles are updated the cache is flushed automatically.
+ */
+
+ 'expiration_time' => DateInterval::createFromDateString('24 hours'),
+
+ /*
+ * The cache key used to store all permissions.
+ */
+
+ 'key' => 'spatie.permission.cache',
+
+ /*
+ * You may optionally indicate a specific cache driver to use for permission and
+ * role caching using any of the `store` drivers listed in the cache.php config
+ * file. Using 'default' here means to use the `default` set in cache.php.
+ */
+
+ 'store' => 'default',
+ ],
+];
diff --git a/config/queue.php b/config/queue.php
new file mode 100644
index 0000000..79c2c0a
--- /dev/null
+++ b/config/queue.php
@@ -0,0 +1,129 @@
+ env('QUEUE_CONNECTION', 'database'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Queue Connections
+ |--------------------------------------------------------------------------
+ |
+ | Here you may configure the connection options for every queue backend
+ | used by your application. An example configuration is provided for
+ | each backend supported by Laravel. You're also free to add more.
+ |
+ | Drivers: "sync", "database", "beanstalkd", "sqs", "redis",
+ | "deferred", "background", "failover", "null"
+ |
+ */
+
+ 'connections' => [
+
+ 'sync' => [
+ 'driver' => 'sync',
+ ],
+
+ 'database' => [
+ 'driver' => 'database',
+ 'connection' => env('DB_QUEUE_CONNECTION'),
+ 'table' => env('DB_QUEUE_TABLE', 'jobs'),
+ 'queue' => env('DB_QUEUE', 'default'),
+ 'retry_after' => (int) env('DB_QUEUE_RETRY_AFTER', 90),
+ 'after_commit' => false,
+ ],
+
+ 'beanstalkd' => [
+ 'driver' => 'beanstalkd',
+ 'host' => env('BEANSTALKD_QUEUE_HOST', 'localhost'),
+ 'queue' => env('BEANSTALKD_QUEUE', 'default'),
+ 'retry_after' => (int) env('BEANSTALKD_QUEUE_RETRY_AFTER', 90),
+ 'block_for' => 0,
+ 'after_commit' => false,
+ ],
+
+ 'sqs' => [
+ 'driver' => 'sqs',
+ 'key' => env('AWS_ACCESS_KEY_ID'),
+ 'secret' => env('AWS_SECRET_ACCESS_KEY'),
+ 'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
+ 'queue' => env('SQS_QUEUE', 'default'),
+ 'suffix' => env('SQS_SUFFIX'),
+ 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
+ 'after_commit' => false,
+ ],
+
+ 'redis' => [
+ 'driver' => 'redis',
+ 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
+ 'queue' => env('REDIS_QUEUE', 'default'),
+ 'retry_after' => (int) env('REDIS_QUEUE_RETRY_AFTER', 90),
+ 'block_for' => null,
+ 'after_commit' => false,
+ ],
+
+ 'deferred' => [
+ 'driver' => 'deferred',
+ ],
+
+ 'background' => [
+ 'driver' => 'background',
+ ],
+
+ 'failover' => [
+ 'driver' => 'failover',
+ 'connections' => [
+ 'database',
+ 'deferred',
+ ],
+ ],
+
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Job Batching
+ |--------------------------------------------------------------------------
+ |
+ | The following options configure the database and table that store job
+ | batching information. These options can be updated to any database
+ | connection and table which has been defined by your application.
+ |
+ */
+
+ 'batching' => [
+ 'database' => env('DB_CONNECTION', 'sqlite'),
+ 'table' => 'job_batches',
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Failed Queue Jobs
+ |--------------------------------------------------------------------------
+ |
+ | These options configure the behavior of failed queue job logging so you
+ | can control how and where failed jobs are stored. Laravel ships with
+ | support for storing failed jobs in a simple file or in a database.
+ |
+ | Supported drivers: "database-uuids", "dynamodb", "file", "null"
+ |
+ */
+
+ 'failed' => [
+ 'driver' => env('QUEUE_FAILED_DRIVER', 'database-uuids'),
+ 'database' => env('DB_CONNECTION', 'sqlite'),
+ 'table' => 'failed_jobs',
+ ],
+
+];
diff --git a/config/sanctum.php b/config/sanctum.php
new file mode 100644
index 0000000..cde73cf
--- /dev/null
+++ b/config/sanctum.php
@@ -0,0 +1,87 @@
+ explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
+ '%s%s',
+ 'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
+ Sanctum::currentApplicationUrlWithPort(),
+ // Sanctum::currentRequestHost(),
+ ))),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Sanctum Guards
+ |--------------------------------------------------------------------------
+ |
+ | This array contains the authentication guards that will be checked when
+ | Sanctum is trying to authenticate a request. If none of these guards
+ | are able to authenticate the request, Sanctum will use the bearer
+ | token that's present on an incoming request for authentication.
+ |
+ */
+
+ 'guard' => ['web'],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Expiration Minutes
+ |--------------------------------------------------------------------------
+ |
+ | This value controls the number of minutes until an issued token will be
+ | considered expired. This will override any values set in the token's
+ | "expires_at" attribute, but first-party sessions are not affected.
+ |
+ */
+
+ 'expiration' => null,
+
+ /*
+ |--------------------------------------------------------------------------
+ | Token Prefix
+ |--------------------------------------------------------------------------
+ |
+ | Sanctum can prefix new tokens in order to take advantage of numerous
+ | security scanning initiatives maintained by open source platforms
+ | that notify developers if they commit tokens into repositories.
+ |
+ | See: https://docs.github.com/en/code-security/secret-scanning/about-secret-scanning
+ |
+ */
+
+ 'token_prefix' => env('SANCTUM_TOKEN_PREFIX', ''),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Sanctum Middleware
+ |--------------------------------------------------------------------------
+ |
+ | When authenticating your first-party SPA with Sanctum you may need to
+ | customize some of the middleware Sanctum uses while processing the
+ | request. You may change the middleware listed below as required.
+ |
+ */
+
+ 'middleware' => [
+ 'authenticate_session' => AuthenticateSession::class,
+ 'encrypt_cookies' => EncryptCookies::class,
+ 'validate_csrf_token' => ValidateCsrfToken::class,
+ ],
+
+];
diff --git a/config/seotools.php b/config/seotools.php
new file mode 100644
index 0000000..dee555a
--- /dev/null
+++ b/config/seotools.php
@@ -0,0 +1,70 @@
+ env('SEO_TOOLS_INERTIA', false),
+ 'meta' => [
+ /*
+ * The default configurations to be used by the meta generator.
+ */
+ 'defaults' => [
+ 'title' => config('app.name'), // set false to total remove
+ 'titleBefore' => false, // Put defaults.title before page title, like 'It's Over 9000! - Dashboard'
+ 'description' => null, // set false to total remove
+ 'separator' => ' - ',
+ 'keywords' => [],
+ 'canonical' => null, // Set to null or 'full' to use Url::full(), set to 'current' to use Url::current(), set false to total remove
+ 'robots' => 'all', // Set to 'all', 'none' or any combination of index/noindex and follow/nofollow
+ ],
+ /*
+ * Webmaster tags are always added.
+ */
+ 'webmaster_tags' => [
+ 'google' => null,
+ 'bing' => null,
+ 'alexa' => null,
+ 'pinterest' => null,
+ 'yandex' => null,
+ 'norton' => null,
+ ],
+
+ 'add_notranslate_class' => false,
+ ],
+ 'opengraph' => [
+ /*
+ * The default configurations to be used by the opengraph generator.
+ */
+ 'defaults' => [
+ 'title' => config('app.name'), // set false to total remove
+ 'description' => false, // set false to total remove
+ 'url' => null, // Set null for using Url::current(), set false to total remove
+ 'type' => false,
+ 'site_name' => false,
+ 'images' => [],
+ ],
+ ],
+ 'twitter' => [
+ /*
+ * The default values to be used by the twitter cards generator.
+ */
+ 'defaults' => [
+ // 'card' => 'summary',
+ // 'site' => '@LuizVinicius73',
+ ],
+ ],
+ 'json-ld' => [
+ /*
+ * The default configurations to be used by the json-ld generator.
+ */
+ 'defaults' => [
+ 'title' => config('app.name'), // set false to total remove
+ 'description' => false, // set false to total remove
+ 'url' => null, // Set to null or 'full' to use Url::full(), set to 'current' to use Url::current(), set false to total remove
+ 'type' => 'WebPage',
+ 'images' => [],
+ ],
+ ],
+];
diff --git a/config/services.php b/config/services.php
new file mode 100644
index 0000000..6a90eb8
--- /dev/null
+++ b/config/services.php
@@ -0,0 +1,38 @@
+ [
+ 'key' => env('POSTMARK_API_KEY'),
+ ],
+
+ 'resend' => [
+ 'key' => env('RESEND_API_KEY'),
+ ],
+
+ 'ses' => [
+ 'key' => env('AWS_ACCESS_KEY_ID'),
+ 'secret' => env('AWS_SECRET_ACCESS_KEY'),
+ 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
+ ],
+
+ 'slack' => [
+ 'notifications' => [
+ 'bot_user_oauth_token' => env('SLACK_BOT_USER_OAUTH_TOKEN'),
+ 'channel' => env('SLACK_BOT_USER_DEFAULT_CHANNEL'),
+ ],
+ ],
+
+];
diff --git a/config/session.php b/config/session.php
new file mode 100644
index 0000000..f574482
--- /dev/null
+++ b/config/session.php
@@ -0,0 +1,233 @@
+ env('SESSION_DRIVER', 'database'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Session Lifetime
+ |--------------------------------------------------------------------------
+ |
+ | Here you may specify the number of minutes that you wish the session
+ | to be allowed to remain idle before it expires. If you want them
+ | to expire immediately when the browser is closed then you may
+ | indicate that via the expire_on_close configuration option.
+ |
+ */
+
+ 'lifetime' => (int) env('SESSION_LIFETIME', 120),
+
+ 'expire_on_close' => env('SESSION_EXPIRE_ON_CLOSE', false),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Session Encryption
+ |--------------------------------------------------------------------------
+ |
+ | This option allows you to easily specify that all of your session data
+ | should be encrypted before it's stored. All encryption is performed
+ | automatically by Laravel and you may use the session like normal.
+ |
+ */
+
+ 'encrypt' => env('SESSION_ENCRYPT', false),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Session File Location
+ |--------------------------------------------------------------------------
+ |
+ | When utilizing the "file" session driver, the session files are placed
+ | on disk. The default storage location is defined here; however, you
+ | are free to provide another location where they should be stored.
+ |
+ */
+
+ 'files' => storage_path('framework/sessions'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Session Database Connection
+ |--------------------------------------------------------------------------
+ |
+ | When using the "database" or "redis" session drivers, you may specify a
+ | connection that should be used to manage these sessions. This should
+ | correspond to a connection in your database configuration options.
+ |
+ */
+
+ 'connection' => env('SESSION_CONNECTION'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Session Database Table
+ |--------------------------------------------------------------------------
+ |
+ | When using the "database" session driver, you may specify the table to
+ | be used to store sessions. Of course, a sensible default is defined
+ | for you; however, you're welcome to change this to another table.
+ |
+ */
+
+ 'table' => env('SESSION_TABLE', 'sessions'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Session Cache Store
+ |--------------------------------------------------------------------------
+ |
+ | When using one of the framework's cache driven session backends, you may
+ | define the cache store which should be used to store the session data
+ | between requests. This must match one of your defined cache stores.
+ |
+ | Affects: "dynamodb", "memcached", "redis"
+ |
+ */
+
+ 'store' => env('SESSION_STORE'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Session Sweeping Lottery
+ |--------------------------------------------------------------------------
+ |
+ | Some session drivers must manually sweep their storage location to get
+ | rid of old sessions from storage. Here are the chances that it will
+ | happen on a given request. By default, the odds are 2 out of 100.
+ |
+ */
+
+ 'lottery' => [2, 100],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Session Cookie Name
+ |--------------------------------------------------------------------------
+ |
+ | Here you may change the name of the session cookie that is created by
+ | the framework. Typically, you should not need to change this value
+ | since doing so does not grant a meaningful security improvement.
+ |
+ */
+
+ 'cookie' => env(
+ 'SESSION_COOKIE',
+ Str::slug((string) env('APP_NAME', 'laravel')).'-session'
+ ),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Session Cookie Path
+ |--------------------------------------------------------------------------
+ |
+ | The session cookie path determines the path for which the cookie will
+ | be regarded as available. Typically, this will be the root path of
+ | your application, but you're free to change this when necessary.
+ |
+ */
+
+ 'path' => env('SESSION_PATH', '/'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Session Cookie Domain
+ |--------------------------------------------------------------------------
+ |
+ | This value determines the domain and subdomains the session cookie is
+ | available to. By default, the cookie will be available to the root
+ | domain without subdomains. Typically, this shouldn't be changed.
+ |
+ */
+
+ 'domain' => env('SESSION_DOMAIN'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | HTTPS Only Cookies
+ |--------------------------------------------------------------------------
+ |
+ | By setting this option to true, session cookies will only be sent back
+ | to the server if the browser has a HTTPS connection. This will keep
+ | the cookie from being sent to you when it can't be done securely.
+ |
+ */
+
+ 'secure' => env('SESSION_SECURE_COOKIE'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | HTTP Access Only
+ |--------------------------------------------------------------------------
+ |
+ | Setting this value to true will prevent JavaScript from accessing the
+ | value of the cookie and the cookie will only be accessible through
+ | the HTTP protocol. It's unlikely you should disable this option.
+ |
+ */
+
+ 'http_only' => env('SESSION_HTTP_ONLY', true),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Same-Site Cookies
+ |--------------------------------------------------------------------------
+ |
+ | This option determines how your cookies behave when cross-site requests
+ | take place, and can be used to mitigate CSRF attacks. By default, we
+ | will set this value to "lax" to permit secure cross-site requests.
+ |
+ | See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value
+ |
+ | Supported: "lax", "strict", "none", null
+ |
+ */
+
+ 'same_site' => env('SESSION_SAME_SITE', 'lax'),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Partitioned Cookies
+ |--------------------------------------------------------------------------
+ |
+ | Setting this value to true will tie the cookie to the top-level site for
+ | a cross-site context. Partitioned cookies are accepted by the browser
+ | when flagged "secure" and the Same-Site attribute is set to "none".
+ |
+ */
+
+ 'partitioned' => env('SESSION_PARTITIONED_COOKIE', false),
+
+ /*
+ |--------------------------------------------------------------------------
+ | Session Serialization
+ |--------------------------------------------------------------------------
+ |
+ | This value controls the serialization strategy for session data, which
+ | is JSON by default. Setting this to "php" allows the storage of PHP
+ | objects in the session but can make an application vulnerable to
+ | "gadget chain" serialization attacks if the APP_KEY is leaked.
+ |
+ | Supported: "json", "php"
+ |
+ */
+
+ 'serialization' => 'json',
+
+];
diff --git a/database/.gitignore b/database/.gitignore
new file mode 100644
index 0000000..9b19b93
--- /dev/null
+++ b/database/.gitignore
@@ -0,0 +1 @@
+*.sqlite*
diff --git a/database/factories/Account/ProfileFactory.php b/database/factories/Account/ProfileFactory.php
new file mode 100644
index 0000000..1b7476c
--- /dev/null
+++ b/database/factories/Account/ProfileFactory.php
@@ -0,0 +1,18 @@
+ [GenderOption::MALE->value, GenderOption::FEMALE->value][rand(0, 1)],
+ 'date_of_birth' => $this->faker->date(max: '2004-01-01'),
+ 'phone_number' => $this->faker->randomNumber(9),
+ ];
+ }
+}
diff --git a/database/factories/Identity/UserFactory.php b/database/factories/Identity/UserFactory.php
new file mode 100644
index 0000000..bbcc27a
--- /dev/null
+++ b/database/factories/Identity/UserFactory.php
@@ -0,0 +1,51 @@
+
+ */
+class UserFactory extends Factory
+{
+ /**
+ * The name of the factory's corresponding model.
+ */
+ protected $model = User::class;
+
+ /**
+ * The current password being used by the factory.
+ */
+ protected static ?string $password;
+
+ /**
+ * Define the model's default state.
+ *
+ * @return array
+ */
+ public function definition(): array
+ {
+ return [
+ 'name' => fake()->name(),
+ 'email' => fake()->unique()->safeEmail(),
+ 'email_verified_at' => now(),
+ 'status' => UserStatus::ACTIVE,
+ 'password' => static::$password ??= 'password',
+ 'remember_token' => Str::random(10),
+ ];
+ }
+
+ /**
+ * Indicate that the model's email address should be unverified.
+ */
+ public function unverified(): static
+ {
+ return $this->state(fn (array $attributes) => [
+ 'email_verified_at' => null,
+ ]);
+ }
+}
diff --git a/database/factories/System/FileFactory.php b/database/factories/System/FileFactory.php
new file mode 100644
index 0000000..4ef537b
--- /dev/null
+++ b/database/factories/System/FileFactory.php
@@ -0,0 +1,19 @@
+
+ */
+class FileFactory extends Factory
+{
+ protected $model = File::class;
+
+ public function definition(): array
+ {
+ return [];
+ }
+}
diff --git a/database/migrations/0001_01_01_000000_create_users_table.php b/database/migrations/0001_01_01_000000_create_users_table.php
new file mode 100644
index 0000000..54dbbd8
--- /dev/null
+++ b/database/migrations/0001_01_01_000000_create_users_table.php
@@ -0,0 +1,53 @@
+id();
+ $table->ulid()->unique();
+ $table->string('name');
+ $table->string('email')->unique();
+ $table->timestamp('email_verified_at')->nullable();
+ $table->string('password');
+ $table->string('status')->default(UserStatus::ACTIVE);
+ $table->json('settings')->nullable();
+ $table->rememberToken();
+ $table->timestamps();
+ });
+
+ Schema::create('password_reset_tokens', function (Blueprint $table) {
+ $table->string('email')->primary();
+ $table->string('token');
+ $table->timestamp('created_at')->nullable();
+ });
+
+ Schema::create('sessions', function (Blueprint $table) {
+ $table->string('id')->primary();
+ $table->foreignId('user_id')->nullable()->index();
+ $table->string('ip_address', 45)->nullable();
+ $table->text('user_agent')->nullable();
+ $table->longText('payload');
+ $table->integer('last_activity')->index();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('users');
+ Schema::dropIfExists('password_reset_tokens');
+ Schema::dropIfExists('sessions');
+ }
+};
diff --git a/database/migrations/0001_01_01_000001_create_cache_table.php b/database/migrations/0001_01_01_000001_create_cache_table.php
new file mode 100644
index 0000000..06dc7a5
--- /dev/null
+++ b/database/migrations/0001_01_01_000001_create_cache_table.php
@@ -0,0 +1,35 @@
+string('key')->primary();
+ $table->mediumText('value');
+ $table->bigInteger('expiration')->index();
+ });
+
+ Schema::create('cache_locks', function (Blueprint $table) {
+ $table->string('key')->primary();
+ $table->string('owner');
+ $table->bigInteger('expiration')->index();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('cache');
+ Schema::dropIfExists('cache_locks');
+ }
+};
diff --git a/database/migrations/0001_01_01_000002_create_jobs_table.php b/database/migrations/0001_01_01_000002_create_jobs_table.php
new file mode 100644
index 0000000..425e705
--- /dev/null
+++ b/database/migrations/0001_01_01_000002_create_jobs_table.php
@@ -0,0 +1,57 @@
+id();
+ $table->string('queue')->index();
+ $table->longText('payload');
+ $table->unsignedTinyInteger('attempts');
+ $table->unsignedInteger('reserved_at')->nullable();
+ $table->unsignedInteger('available_at');
+ $table->unsignedInteger('created_at');
+ });
+
+ Schema::create('job_batches', function (Blueprint $table) {
+ $table->string('id')->primary();
+ $table->string('name');
+ $table->integer('total_jobs');
+ $table->integer('pending_jobs');
+ $table->integer('failed_jobs');
+ $table->longText('failed_job_ids');
+ $table->mediumText('options')->nullable();
+ $table->integer('cancelled_at')->nullable();
+ $table->integer('created_at');
+ $table->integer('finished_at')->nullable();
+ });
+
+ Schema::create('failed_jobs', function (Blueprint $table) {
+ $table->id();
+ $table->string('uuid')->unique();
+ $table->text('connection');
+ $table->text('queue');
+ $table->longText('payload');
+ $table->longText('exception');
+ $table->timestamp('failed_at')->useCurrent();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('jobs');
+ Schema::dropIfExists('job_batches');
+ Schema::dropIfExists('failed_jobs');
+ }
+};
diff --git a/database/migrations/2026_04_23_045248_create_permission_tables.php b/database/migrations/2026_04_23_045248_create_permission_tables.php
new file mode 100644
index 0000000..8a65ddc
--- /dev/null
+++ b/database/migrations/2026_04_23_045248_create_permission_tables.php
@@ -0,0 +1,138 @@
+id(); // permission id
+ $table->string('name');
+ $table->string('guard_name');
+ $table->timestamps();
+
+ $table->unique(['name', 'guard_name']);
+ });
+
+ /**
+ * See `docs/prerequisites.md` for suggested lengths on 'name' and 'guard_name' if "1071 Specified key was too long" errors are encountered.
+ */
+ Schema::create($tableNames['roles'], static function (Blueprint $table) use ($teams, $columnNames) {
+ $table->id(); // role id
+ $table->ulid()->unique();
+ if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
+ $table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
+ $table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
+ }
+ $table->string('name');
+ $table->string('guard_name');
+ $table->timestamps();
+ if ($teams || config('permission.testing')) {
+ $table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
+ } else {
+ $table->unique(['name', 'guard_name']);
+ }
+ });
+
+ Schema::create($tableNames['model_has_permissions'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
+ $table->unsignedBigInteger($pivotPermission);
+
+ $table->string('model_type');
+ $table->unsignedBigInteger($columnNames['model_morph_key']);
+ $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
+
+ $table->foreign($pivotPermission)
+ ->references('id') // permission id
+ ->on($tableNames['permissions'])
+ ->cascadeOnDelete();
+ if ($teams) {
+ $table->unsignedBigInteger($columnNames['team_foreign_key']);
+ $table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
+
+ $table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
+ 'model_has_permissions_permission_model_type_primary');
+ } else {
+ $table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
+ 'model_has_permissions_permission_model_type_primary');
+ }
+ });
+
+ Schema::create($tableNames['model_has_roles'], static function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
+ $table->unsignedBigInteger($pivotRole);
+
+ $table->string('model_type');
+ $table->unsignedBigInteger($columnNames['model_morph_key']);
+ $table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
+
+ $table->foreign($pivotRole)
+ ->references('id') // role id
+ ->on($tableNames['roles'])
+ ->cascadeOnDelete();
+ if ($teams) {
+ $table->unsignedBigInteger($columnNames['team_foreign_key']);
+ $table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
+
+ $table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
+ 'model_has_roles_role_model_type_primary');
+ } else {
+ $table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
+ 'model_has_roles_role_model_type_primary');
+ }
+ });
+
+ Schema::create($tableNames['role_has_permissions'], static function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
+ $table->unsignedBigInteger($pivotPermission);
+ $table->unsignedBigInteger($pivotRole);
+
+ $table->foreign($pivotPermission)
+ ->references('id') // permission id
+ ->on($tableNames['permissions'])
+ ->cascadeOnDelete();
+
+ $table->foreign($pivotRole)
+ ->references('id') // role id
+ ->on($tableNames['roles'])
+ ->cascadeOnDelete();
+
+ $table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
+ });
+
+ app('cache')
+ ->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
+ ->forget(config('permission.cache.key'));
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ $tableNames = config('permission.table_names');
+
+ throw_if(empty($tableNames), 'Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
+
+ Schema::dropIfExists($tableNames['role_has_permissions']);
+ Schema::dropIfExists($tableNames['model_has_roles']);
+ Schema::dropIfExists($tableNames['model_has_permissions']);
+ Schema::dropIfExists($tableNames['roles']);
+ Schema::dropIfExists($tableNames['permissions']);
+ }
+};
diff --git a/database/migrations/2026_04_23_045257_add_description_and_group_to_permissions_table.php b/database/migrations/2026_04_23_045257_add_description_and_group_to_permissions_table.php
new file mode 100644
index 0000000..9af6964
--- /dev/null
+++ b/database/migrations/2026_04_23_045257_add_description_and_group_to_permissions_table.php
@@ -0,0 +1,19 @@
+string('description')->after('name');
+ $table->string('group')->after('description');
+ });
+ }
+};
diff --git a/database/migrations/2026_05_01_172941_create_system_settings_table.php b/database/migrations/2026_05_01_172941_create_system_settings_table.php
new file mode 100644
index 0000000..872d870
--- /dev/null
+++ b/database/migrations/2026_05_01_172941_create_system_settings_table.php
@@ -0,0 +1,28 @@
+id();
+ $table->string('key')->unique();
+ $table->text('value')->nullable();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('system_settings');
+ }
+};
diff --git a/database/migrations/2026_05_01_172951_create_profiles_table.php b/database/migrations/2026_05_01_172951_create_profiles_table.php
new file mode 100644
index 0000000..c15d01e
--- /dev/null
+++ b/database/migrations/2026_05_01_172951_create_profiles_table.php
@@ -0,0 +1,31 @@
+id();
+ $table->foreignId('user_id')->constrained()->cascadeOnDelete();
+ $table->string('gender')->nullable();
+ $table->date('date_of_birth')->nullable();
+ $table->string('phone_number')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('profiles');
+ }
+};
diff --git a/database/migrations/2026_05_18_140347_create_notifications_table.php b/database/migrations/2026_05_18_140347_create_notifications_table.php
new file mode 100644
index 0000000..d738032
--- /dev/null
+++ b/database/migrations/2026_05_18_140347_create_notifications_table.php
@@ -0,0 +1,31 @@
+uuid('id')->primary();
+ $table->string('type');
+ $table->morphs('notifiable');
+ $table->text('data');
+ $table->timestamp('read_at')->nullable();
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('notifications');
+ }
+};
diff --git a/database/migrations/2026_05_27_122504_create_backups_table.php b/database/migrations/2026_05_27_122504_create_backups_table.php
new file mode 100644
index 0000000..f351e11
--- /dev/null
+++ b/database/migrations/2026_05_27_122504_create_backups_table.php
@@ -0,0 +1,31 @@
+id();
+ $table->string('file_name');
+ $table->string('disk');
+ $table->string('path');
+ $table->string('size');
+ $table->string('type');
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('backups');
+ }
+};
diff --git a/database/migrations/2026_06_05_131433_create_files_table.php b/database/migrations/2026_06_05_131433_create_files_table.php
new file mode 100644
index 0000000..171d741
--- /dev/null
+++ b/database/migrations/2026_06_05_131433_create_files_table.php
@@ -0,0 +1,36 @@
+id();
+ $table->string('name');
+ $table->string('path');
+ $table->bigInteger('size');
+ $table->string('mime_type');
+ $table->string('relation_name');
+ $table->string('disk');
+ $table->json('options')->nullable();
+ $table->unsignedBigInteger('uploader_id');
+ $table->morphs('fileable');
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('files');
+ }
+};
diff --git a/database/migrations/2026_06_08_021449_create_audits_table.php b/database/migrations/2026_06_08_021449_create_audits_table.php
new file mode 100644
index 0000000..1307f7d
--- /dev/null
+++ b/database/migrations/2026_06_08_021449_create_audits_table.php
@@ -0,0 +1,48 @@
+create($table, function (Blueprint $table) {
+
+ $morphPrefix = config('audit.user.morph_prefix', 'user');
+
+ $table->bigIncrements('id');
+ $table->string($morphPrefix.'_type')->nullable();
+ $table->unsignedBigInteger($morphPrefix.'_id')->nullable();
+ $table->string('event');
+ $table->morphs('auditable');
+ $table->text('old_values')->nullable();
+ $table->text('new_values')->nullable();
+ $table->text('url')->nullable();
+ $table->ipAddress('ip_address')->nullable();
+ $table->string('user_agent', 1023)->nullable();
+ $table->string('tags')->nullable();
+ $table->timestamps();
+
+ $table->index([$morphPrefix.'_id', $morphPrefix.'_type']);
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ $connection = config('audit.drivers.database.connection', config('database.default'));
+ $table = config('audit.drivers.database.table', 'audits');
+
+ Schema::connection($connection)->drop($table);
+ }
+};
diff --git a/database/migrations/2026_06_30_094838_create_personal_access_tokens_table.php b/database/migrations/2026_06_30_094838_create_personal_access_tokens_table.php
new file mode 100644
index 0000000..40ff706
--- /dev/null
+++ b/database/migrations/2026_06_30_094838_create_personal_access_tokens_table.php
@@ -0,0 +1,33 @@
+id();
+ $table->morphs('tokenable');
+ $table->text('name');
+ $table->string('token', 64)->unique();
+ $table->text('abilities')->nullable();
+ $table->timestamp('last_used_at')->nullable();
+ $table->timestamp('expires_at')->nullable()->index();
+ $table->timestamps();
+ });
+ }
+
+ /**
+ * Reverse the migrations.
+ */
+ public function down(): void
+ {
+ Schema::dropIfExists('personal_access_tokens');
+ }
+};
diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php
new file mode 100644
index 0000000..0435e41
--- /dev/null
+++ b/database/seeders/DatabaseSeeder.php
@@ -0,0 +1,21 @@
+call(RoleSeeder::class);
+ $this->call(UserSeeder::class);
+ $this->call(SystemSettingSeeder::class);
+ }
+}
diff --git a/database/seeders/RoleSeeder.php b/database/seeders/RoleSeeder.php
new file mode 100644
index 0000000..253fe55
--- /dev/null
+++ b/database/seeders/RoleSeeder.php
@@ -0,0 +1,43 @@
+ $role->value,
+ 'guard_name' => 'web',
+ ]);
+ }
+
+ $roleCollection = collect($roleCollection);
+
+ $permissions = RoleType::permissions();
+
+ foreach ($permissions as $permission) {
+ $roles = $roleCollection->filter(fn ($value) => in_array($value->name, array_column($permission['roles'], 'value')));
+ Permission::create([
+ 'name' => $permission['name'],
+ 'description' => $permission['description'],
+ 'group' => $permission['group'],
+ 'guard_name' => $permission['guard_name'],
+ ])->roles()->sync($roles);
+ }
+
+ app(PermissionRegistrar::class)->forgetCachedPermissions();
+ }
+}
diff --git a/database/seeders/SystemSettingSeeder.php b/database/seeders/SystemSettingSeeder.php
new file mode 100644
index 0000000..4a999c9
--- /dev/null
+++ b/database/seeders/SystemSettingSeeder.php
@@ -0,0 +1,24 @@
+ $setting->value,
+ 'value' => $setting->default(),
+ ]);
+ }
+ }
+}
diff --git a/database/seeders/UserSeeder.php b/database/seeders/UserSeeder.php
new file mode 100644
index 0000000..7a7c335
--- /dev/null
+++ b/database/seeders/UserSeeder.php
@@ -0,0 +1,44 @@
+ 'System Administrator',
+ 'email' => 'system@web.io',
+ 'password' => 'password',
+ 'email_verified_at' => now(),
+ ]);
+
+ $systemRoles = Role::withoutGlobalScopes([HideSystemAdminRole::class])
+ ->where('name', RoleType::SYSTEM_ADMIN)
+ ->first();
+
+ $admin->profile()->create($profile->definition());
+ $admin->assignRole($systemRoles);
+
+ foreach (range(1, 1) as $i) {
+ User::factory()
+ ->count(100)
+ ->create()
+ ->each(function ($user) use ($profile) {
+ $user->profile()->create($profile->definition());
+ $user->assignRole(RoleType::USER);
+ });
+ }
+ }
+}
diff --git a/lang/en/auth.php b/lang/en/auth.php
new file mode 100644
index 0000000..aed4b01
--- /dev/null
+++ b/lang/en/auth.php
@@ -0,0 +1,21 @@
+ 'These credentials do not match our records.',
+ 'password' => 'The provided password is incorrect.',
+ 'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
+ 'inactive' => 'Your account is inactive or suspended. Please contact support.',
+
+];
diff --git a/lang/en/domains/account/enum.php b/lang/en/domains/account/enum.php
new file mode 100644
index 0000000..25812ba
--- /dev/null
+++ b/lang/en/domains/account/enum.php
@@ -0,0 +1,25 @@
+ [
+ GenderOption::MALE->value => 'Male',
+ GenderOption::FEMALE->value => 'Female',
+ ],
+ 'user_settings' => [
+ 'notification' => 'Notification',
+ 'language' => 'Language',
+ 'timezone' => 'Timezone',
+ 'options' => [
+ 'language' => [
+ 'en' => 'English',
+ 'id' => 'Indonesian',
+ ],
+ 'notification' => [
+ 'on' => 'On',
+ 'off' => 'Off',
+ ],
+ ],
+ ],
+];
diff --git a/lang/en/domains/account/field.php b/lang/en/domains/account/field.php
new file mode 100644
index 0000000..c6cce67
--- /dev/null
+++ b/lang/en/domains/account/field.php
@@ -0,0 +1,10 @@
+ [
+ 'avatar' => 'Avatar',
+ 'gender' => 'Gender',
+ 'date_of_birth' => 'Date of Birth',
+ 'phone_number' => 'Phone Number',
+ ],
+];
diff --git a/lang/en/domains/account/pages.php b/lang/en/domains/account/pages.php
new file mode 100644
index 0000000..d6b896c
--- /dev/null
+++ b/lang/en/domains/account/pages.php
@@ -0,0 +1,20 @@
+ [
+ 'title' => 'Profile Information',
+ 'description' => "Update your account's profile information and email address.",
+ ],
+ 'password' => [
+ 'title' => 'Update Password',
+ 'description' => 'Ensure your account is using a long, random password to stay secure.',
+ ],
+ 'delete_account' => [
+ 'title' => 'Delete Account',
+ 'description' => 'Once your account is deleted, all of its resources and data will be permanently deleted.',
+ ],
+ 'user_settings' => [
+ 'title' => 'User Settings',
+ 'description' => 'Manage your application preferences like theme and language.',
+ ],
+];
diff --git a/lang/en/domains/account/seo.php b/lang/en/domains/account/seo.php
new file mode 100644
index 0000000..2a026ac
--- /dev/null
+++ b/lang/en/domains/account/seo.php
@@ -0,0 +1,9 @@
+ [
+ 'title' => 'Profile Information',
+ 'description' => "Update your account's profile information and email address.",
+ 'keywords' => 'profile, account settings, email update',
+ ],
+];
diff --git a/lang/en/domains/auth/field.php b/lang/en/domains/auth/field.php
new file mode 100644
index 0000000..8920896
--- /dev/null
+++ b/lang/en/domains/auth/field.php
@@ -0,0 +1,35 @@
+ [
+ 'email' => 'Email address',
+ 'password' => 'Password',
+ 'remember' => 'Remember me',
+ 'forgot_password' => 'Forgot Password?',
+ 'submit' => 'Sign in',
+ ],
+ 'register' => [
+ 'name' => 'Full Name',
+ 'email' => 'Email address',
+ 'password' => 'Password',
+ 'confirm_password' => 'Confirm Password',
+ 'submit' => 'Sign up',
+ ],
+ 'forgot_password' => [
+ 'email' => 'Email address',
+ 'submit' => 'Email Password Reset Link',
+ ],
+ 'reset_password' => [
+ 'email' => 'Email address',
+ 'password' => 'New Password',
+ 'confirm_password' => 'Confirm Password',
+ 'submit' => 'Reset Password',
+ ],
+ 'verify_email' => [
+ 'submit' => 'Resend Verification Email',
+ ],
+ 'confirm_password' => [
+ 'password' => 'Password',
+ 'submit' => 'Confirm',
+ ],
+];
diff --git a/lang/en/domains/auth/messages.php b/lang/en/domains/auth/messages.php
new file mode 100644
index 0000000..db6338a
--- /dev/null
+++ b/lang/en/domains/auth/messages.php
@@ -0,0 +1,8 @@
+ 'Your password has been successfully reset.',
+ 'invalid_token' => 'This password reset token is invalid.',
+ 'reset_link_sent' => 'Password reset link has been sent to your email.',
+ 'reset_link_failed' => 'Please check your email to reset the password.',
+];
diff --git a/lang/en/domains/auth/notifications.php b/lang/en/domains/auth/notifications.php
new file mode 100644
index 0000000..55420b8
--- /dev/null
+++ b/lang/en/domains/auth/notifications.php
@@ -0,0 +1,28 @@
+ [
+ 'subject' => 'New sign-in to your account',
+ 'greeting' => 'Hello :name!',
+ 'intro' => 'We noticed a new sign-in to your :app account.',
+ 'time' => 'Time: :time',
+ 'ip' => 'IP Address: :ip',
+ 'browser' => 'Browser: :browser',
+ 'outro' => 'If this was you, you can safely ignore this email. If you did not make this request, please secure your account immediately.',
+ 'action' => 'Go to Dashboard',
+ 'title' => 'Sign In Activity',
+ 'message' => 'You have successfully signed in to your account from :ip.',
+ ],
+ 'verify_email' => [
+ 'subject' => 'Verify your email address',
+ 'intro' => 'Please click the button below to verify your email address.',
+ 'action' => 'Verify Email Address',
+ 'outro' => 'If you did not create an account, no further action is required.',
+ ],
+ 'reset_password' => [
+ 'subject' => 'Reset Password Notification',
+ 'intro' => 'You are receiving this email because we received a password reset request for your account.',
+ 'action' => 'Reset Password',
+ 'outro' => 'If you did not request a password reset, no further action is required.',
+ ],
+];
diff --git a/lang/en/domains/auth/pages.php b/lang/en/domains/auth/pages.php
new file mode 100644
index 0000000..48b8681
--- /dev/null
+++ b/lang/en/domains/auth/pages.php
@@ -0,0 +1,31 @@
+ [
+ 'header' => 'Sign in to your account',
+ 'no_account' => "Don't have an account?",
+ 'register_link' => 'Sign up',
+ ],
+ 'register' => [
+ 'header' => 'Create a new account',
+ 'has_account' => 'Already have an account?',
+ 'login_link' => 'Sign in',
+ ],
+ 'forgot_password' => [
+ 'header' => 'Forgot your password?',
+ 'subheader' => 'No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.',
+ 'back_to_login' => 'Back to sign in',
+ ],
+ 'reset_password' => [
+ 'header' => 'Reset Password',
+ ],
+ 'verify_email' => [
+ 'header' => 'Verify Email',
+ 'subheader' => "Thanks for signing up! Before getting started, could you verify your email address by clicking on the link we just emailed to you? If you didn't receive the email, we will gladly send you another.",
+ 'resend_link' => 'A new verification link has been sent to the email address you provided during registration.',
+ ],
+ 'confirm_password' => [
+ 'header' => 'Confirm Password',
+ 'subheader' => 'This is a secure area of the application. Please confirm your password before continuing.',
+ ],
+];
diff --git a/lang/en/domains/auth/seo.php b/lang/en/domains/auth/seo.php
new file mode 100644
index 0000000..fb7612a
--- /dev/null
+++ b/lang/en/domains/auth/seo.php
@@ -0,0 +1,34 @@
+ [
+ 'title' => 'Sign In',
+ 'description' => 'Sign in to your account to access the dashboard.',
+ 'keywords' => 'login, sign in, auth',
+ ],
+ 'register' => [
+ 'title' => 'Sign Up',
+ 'description' => 'Create a new account to get started.',
+ 'keywords' => 'register, sign up, auth',
+ ],
+ 'forgot_password' => [
+ 'title' => 'Forgot Password',
+ 'description' => 'Recover your account password.',
+ 'keywords' => 'forgot password, recover, auth',
+ ],
+ 'reset_password' => [
+ 'title' => 'Reset Password',
+ 'description' => 'Set a new password for your account.',
+ 'keywords' => 'reset password, auth',
+ ],
+ 'verify_email' => [
+ 'title' => 'Verify Email',
+ 'description' => 'Verify your email address to secure your account.',
+ 'keywords' => 'verify email, auth',
+ ],
+ 'confirm_password' => [
+ 'title' => 'Confirm Password',
+ 'description' => 'Please confirm your password before continuing.',
+ 'keywords' => 'confirm password, auth',
+ ],
+];
diff --git a/lang/en/domains/identity/dashboard.php b/lang/en/domains/identity/dashboard.php
new file mode 100644
index 0000000..e77f294
--- /dev/null
+++ b/lang/en/domains/identity/dashboard.php
@@ -0,0 +1,25 @@
+ [
+ 'title' => 'Total Users',
+ 'growth' => ':rate since last month',
+ ],
+ 'new_user_count' => [
+ 'title' => 'New Users',
+ 'growth' => ':rate since last month',
+ ],
+ 'user_verification_rate' => [
+ 'title' => 'Verification Rate',
+ 'detail' => ':verified verified / :unverified Unverified',
+ ],
+ 'user_growth' => [
+ 'title' => 'User Growth',
+ 'subtitle' => 'Monthly trend of new user registrations in :year',
+ 'series_name' => 'Registration',
+ ],
+ 'role_distribution' => [
+ 'title' => 'Roles',
+ 'subtitle' => 'Breakdown of users assigned to each role',
+ ],
+];
diff --git a/lang/en/domains/identity/enum.php b/lang/en/domains/identity/enum.php
new file mode 100644
index 0000000..f87b774
--- /dev/null
+++ b/lang/en/domains/identity/enum.php
@@ -0,0 +1,10 @@
+ [
+ UserStatus::ACTIVE->value => 'Active',
+ UserStatus::INACTIVE->value => 'Inactive',
+ ],
+];
diff --git a/lang/en/domains/identity/field.php b/lang/en/domains/identity/field.php
new file mode 100644
index 0000000..cfc8e18
--- /dev/null
+++ b/lang/en/domains/identity/field.php
@@ -0,0 +1,21 @@
+ [
+ 'name' => 'Full Name',
+ 'email' => 'Email Address',
+ 'status' => 'Status',
+ 'password' => 'Secure Password',
+ 'current_password' => 'Current Password',
+ 'new_password' => 'New Password',
+ 'password_confirmation' => 'Confirm Password',
+ 'verified' => 'Verified',
+ 'unverified' => 'Unverified',
+ ],
+ 'role' => [
+ 'name' => 'Role Name',
+ 'permission_count' => 'Permission Count',
+ 'guard_name' => 'Guard Name',
+ 'permissions' => 'Permissions',
+ ],
+];
diff --git a/lang/en/domains/identity/messages.php b/lang/en/domains/identity/messages.php
new file mode 100644
index 0000000..d983cd3
--- /dev/null
+++ b/lang/en/domains/identity/messages.php
@@ -0,0 +1,22 @@
+ [
+ 'password_reset_link_sent' => 'Password reset link has been sent.',
+ 'user_role_updated' => 'User role has been updated.',
+ 'user_status_updated' => 'User status has been updated.'
+ ],
+ 'exceptions' => [
+ 'failed_to_send_password_reset_link' => 'Failed to send password reset link.',
+ 'failed_to_update_user_role' => 'Failed to update user role.',
+ 'failed_to_update_user_status' => 'Failed to update user status.',
+ 'user_already_active' => 'This user is already active.',
+ 'user_already_suspended' => 'This user is already suspended.',
+ 'user_already_in_status' => 'User is already :status.',
+ 'user_cannot_be_edited' => 'This user can\'t be edited.',
+ 'user_cannot_be_purged' => 'You can\'t purge an admin user.',
+ 'user_cannot_be_suspended' => 'You can\'t suspend an admin user.',
+ 'cannot_remove_system_role' => 'Can\'t remove system role.',
+ 'role_has_users' => 'This role has a users attached to it.',
+ ],
+];
diff --git a/lang/en/domains/identity/notifications.php b/lang/en/domains/identity/notifications.php
new file mode 100644
index 0000000..628ea38
--- /dev/null
+++ b/lang/en/domains/identity/notifications.php
@@ -0,0 +1,29 @@
+ [
+ 'subject' => 'Welcome!',
+ 'body' => 'Your account is ready.',
+ 'action' => 'Login',
+ ],
+ 'governance' => [
+ 'user_suspended' => [
+ 'subject' => 'Account Suspended',
+ 'intro' => 'We are writing to inform you that your account has been suspended.',
+ 'body' => 'This action was taken due to a violation of our terms of service or a security concern regarding your account.',
+ 'outro' => 'If you believe this is an error, please contact our support team.',
+ ],
+ 'user_purged' => [
+ 'subject' => 'Account Permanently Removed',
+ 'intro' => 'We are writing to inform you that your account and all associated data have been permanently removed.',
+ 'body' => 'This action was taken either per your request or due to a severe violation of our policies.',
+ 'outro' => 'If you have any questions, please contact our support team.',
+ ],
+ 'user_activated' => [
+ 'subject' => 'Account Activated',
+ 'intro' => 'We are pleased to inform you that your account has been successfully activated.',
+ 'body' => 'You can now log in and access all the features of your account.',
+ 'outro' => 'If you have any questions, please contact our support team.',
+ ],
+ ],
+];
diff --git a/lang/en/domains/identity/pages.php b/lang/en/domains/identity/pages.php
new file mode 100644
index 0000000..18659d6
--- /dev/null
+++ b/lang/en/domains/identity/pages.php
@@ -0,0 +1,20 @@
+ [
+ 'account_info' => 'Account Info',
+ 'user_info' => 'User Info',
+ 'menu' => [
+ 'role_update' => 'Update Role',
+ 'toggle_status' => 'Toggle Status',
+ 'password_reset' => 'Send Password Reset',
+ ],
+ 'confirmation' => [
+ 'toggle_status' => 'Are you sure you want to change the status of this user?',
+ 'status_changed' => 'User status has been successfully updated.',
+ 'status_unchanged' => 'User status was not changed.',
+ 'send_password_reset' => 'Are you sure you want to send a password reset link to this user?',
+ 'password_reset_success' => 'Password reset link has been successfully sent.',
+ ],
+ ],
+];
diff --git a/lang/en/domains/identity/seo.php b/lang/en/domains/identity/seo.php
new file mode 100644
index 0000000..b6ab2a3
--- /dev/null
+++ b/lang/en/domains/identity/seo.php
@@ -0,0 +1,19 @@
+ [
+ 'title' => 'User Management',
+ 'description' => 'Manage user accounts, roles, and permissions.',
+ 'keywords' => 'user management, users, identity',
+ ],
+ 'user_detail' => [
+ 'title' => 'Detail of :name',
+ 'description' => 'Manage user accounts, roles, and permissions.',
+ 'keywords' => 'user management, users, identity',
+ ],
+ 'role' => [
+ 'title' => 'Role Management',
+ 'description' => 'Manage user roles and permissions.',
+ 'keywords' => 'role management, roles, permissions, identity',
+ ],
+];
diff --git a/lang/en/domains/system/field.php b/lang/en/domains/system/field.php
new file mode 100644
index 0000000..5d5802c
--- /dev/null
+++ b/lang/en/domains/system/field.php
@@ -0,0 +1,31 @@
+ [
+ 'file' => 'Backup File',
+ ],
+ 'settings' => [
+ 'web' => [
+ 'name' => 'Web Name',
+ 'description' => 'Web Description',
+ 'address' => 'Web Address',
+ 'phone' => 'Web Phone',
+ 'email' => 'Web Email',
+ 'logo' => 'Web Logo',
+ 'favicon' => 'Web Favicon',
+ ],
+ 'default_language' => 'Default Language',
+ 'timezone' => 'Timezone',
+ 'google' => [
+ 'tag_manager_id' => 'Google Tag Manager ID',
+ 'webmaster_id' => 'Google Webmaster ID',
+ ],
+ ],
+ 'audit' => [
+ 'ip_address' => 'IP Address',
+ 'browser' => 'Browser',
+ 'field' => 'Field',
+ 'old' => 'Old',
+ 'new' => 'New',
+ ],
+];
diff --git a/lang/en/domains/system/messages.php b/lang/en/domains/system/messages.php
new file mode 100644
index 0000000..4e61879
--- /dev/null
+++ b/lang/en/domains/system/messages.php
@@ -0,0 +1,15 @@
+ [
+ 'delete_success' => 'Backup data deleted successfully.',
+ 'backup_success' => 'System backup completed successfully.',
+ 'backup_error' => 'Failed to backup the system.',
+ 'verification_error' => 'Backup zip file could not be verified on target storage.',
+ 'restored_success' => 'System restore completed successfully.',
+ 'restored_error' => 'Failed to restore the system.',
+ 'download_error' => 'Failed to download :path.',
+ 'file_not_found' => 'Backup file not found.',
+ 'path_required' => 'Path is required.',
+ ],
+];
diff --git a/lang/en/domains/system/notifications.php b/lang/en/domains/system/notifications.php
new file mode 100644
index 0000000..14b20c4
--- /dev/null
+++ b/lang/en/domains/system/notifications.php
@@ -0,0 +1,18 @@
+ [
+ 'import_email' => [
+ 'subject' => 'Your Excel Import Has Been Processed',
+ 'intro' => 'Your Excel import request has been completed.',
+ 'body' => 'The data from your uploaded file has been successfully imported into the system.',
+ 'outro' => 'If you did not initiate this import, please contact the administrator immediately.',
+ ],
+ 'export_email' => [
+ 'subject' => 'Your Excel Export Is Ready',
+ 'intro' => 'Your Excel export request has been completed.',
+ 'body' => 'Please find the exported file attached to this email.',
+ 'outro' => 'If you did not request this export, please contact the administrator immediately.',
+ ],
+ ],
+];
diff --git a/lang/en/domains/system/pages.php b/lang/en/domains/system/pages.php
new file mode 100644
index 0000000..30dbcf5
--- /dev/null
+++ b/lang/en/domains/system/pages.php
@@ -0,0 +1,22 @@
+ [
+ 'title' => 'System Backup Catalogs',
+ 'backup_button' => 'Back Up',
+ 'upload_button' => 'Upload Backup File',
+ 'upload_modal_title' => 'Upload Backup File',
+ 'empty_state' => 'No Backup File Stored',
+ 'confirmation' => [
+ 'delete' => 'Are you sure you want to delete this backup?',
+ ],
+ ],
+ 'settings' => [
+ 'update_title' => 'Update Settings',
+ 'sections' => [
+ 'web' => 'Web',
+ 'general' => 'General',
+ 'webmaster' => 'Webmaster',
+ ],
+ ],
+];
diff --git a/lang/en/domains/system/seo.php b/lang/en/domains/system/seo.php
new file mode 100644
index 0000000..f12c5a2
--- /dev/null
+++ b/lang/en/domains/system/seo.php
@@ -0,0 +1,14 @@
+ [
+ 'title' => 'System Settings',
+ 'description' => 'System settings and configurations.',
+ 'keywords' => 'system, settings, configurations',
+ ],
+ 'backup' => [
+ 'title' => 'System Backups',
+ 'description' => 'Backup and restore the database and assets.',
+ 'keywords' => 'backup, restore, database, asset',
+ ],
+];
diff --git a/lang/en/event.php b/lang/en/event.php
new file mode 100644
index 0000000..63f2bf5
--- /dev/null
+++ b/lang/en/event.php
@@ -0,0 +1,8 @@
+ 'Created',
+ 'updated' => 'Updated',
+ 'deleted' => 'Deleted',
+ 'permissions_synced' => 'Permissions synced',
+];
diff --git a/lang/en/pagination.php b/lang/en/pagination.php
new file mode 100644
index 0000000..d481411
--- /dev/null
+++ b/lang/en/pagination.php
@@ -0,0 +1,19 @@
+ '« Previous',
+ 'next' => 'Next »',
+
+];
diff --git a/lang/en/passwords.php b/lang/en/passwords.php
new file mode 100644
index 0000000..fad3a7d
--- /dev/null
+++ b/lang/en/passwords.php
@@ -0,0 +1,22 @@
+ 'Your password has been reset.',
+ 'sent' => 'We have emailed your password reset link.',
+ 'throttled' => 'Please wait before retrying.',
+ 'token' => 'This password reset token is invalid.',
+ 'user' => "We can't find a user with that email address.",
+
+];
diff --git a/lang/en/permissions.php b/lang/en/permissions.php
new file mode 100644
index 0000000..e52b83d
--- /dev/null
+++ b/lang/en/permissions.php
@@ -0,0 +1,34 @@
+ [
+ 'group-name' => 'Dashboard',
+ 'index' => 'Grants authority to access and view the data summaries on the main application dashboard.',
+ 'admin' => 'Provides specialized access to the administrator control panel for comprehensive system monitoring.',
+ 'user' => 'Provides access to a personalized dashboard containing information relevant to general users.',
+ ],
+ 'user' => [
+ 'group-name' => 'User Management',
+ 'viewAny' => 'Allows users to view and search the list of all user accounts registered in the system.',
+ 'view' => 'Allows users to view the profile and details of a specific user account.',
+ 'create' => 'Grants access to register and add new user accounts to the system.',
+ 'update' => 'Allows modification of profile data, status, and other detailed information for existing user accounts.',
+ 'delete' => 'Provides authority to permanently remove or deactivate user accounts from the system.',
+ ],
+ 'role' => [
+ 'group-name' => 'Role & Permission',
+ 'viewAny' => 'Allows users to view and search the list of all role levels available within the application.',
+ 'view' => 'Allows users to view the details and assigned permissions of a specific role.',
+ 'create' => 'Provides the ability to design and create new role levels with specific access permissions.',
+ 'update' => 'Allows for role name modification and readjustment of the permission list for existing roles.',
+ 'delete' => 'Provides authority to remove role levels that are no longer required by the system.',
+ ],
+ 'system-setting' => [
+ 'group-name' => 'System Setting',
+ 'manage' => 'Allows to view and manage system setting',
+ ],
+ 'system-backup' => [
+ 'group-name' => 'System Backup',
+ 'manage' => 'Allows to view and manage system backup',
+ ],
+];
diff --git a/lang/en/resources.php b/lang/en/resources.php
new file mode 100644
index 0000000..9957695
--- /dev/null
+++ b/lang/en/resources.php
@@ -0,0 +1,14 @@
+ 'User',
+ 'role' => 'Role',
+ 'profile' => 'Profile',
+ 'settings' => 'Settings',
+ 'password' => 'Password',
+ 'backup' => 'Backup',
+ 'backup_file' => 'Backup File',
+ 'avatar' => 'Avatar',
+ 'system_settings' => 'System Settings',
+ 'audit' => 'Audit',
+];
diff --git a/lang/en/seo.php b/lang/en/seo.php
new file mode 100644
index 0000000..b03541d
--- /dev/null
+++ b/lang/en/seo.php
@@ -0,0 +1,11 @@
+ 'Laravel Base',
+ 'default_description' => 'Laravel Base using Livewire v4, Bootstrap, and DataTables.net by Syarif Ubaidillah.',
+ 'dashboard' => [
+ 'title' => 'Dashboard',
+ 'description' => 'Dashboard analytic for this application.',
+ 'keywords' => 'dashboard, analytics, review',
+ ],
+];
diff --git a/lang/en/ui.php b/lang/en/ui.php
new file mode 100644
index 0000000..50eee7a
--- /dev/null
+++ b/lang/en/ui.php
@@ -0,0 +1,92 @@
+ [
+ 'dashboard' => 'Dashboard',
+ 'identity' => 'User Management',
+ 'users' => 'Users',
+ 'roles' => 'Roles & Permissions',
+ 'settings' => 'System Settings',
+ 'system_backup' => 'System Backup',
+ ],
+ 'title' => [
+ 'index' => ':resource Data',
+ 'create' => 'Create New :resource',
+ 'update' => 'Update :resource',
+ 'delete' => 'Delete :resource',
+ 'view' => 'View :resource',
+ 'upload' => 'Upload :resource',
+ 'restore' => 'Restore :resource',
+ 'import' => 'Import :resource',
+ ],
+ 'greetings' => [
+ 'morning' => 'Good morning, :name',
+ 'welcome' => 'Welcome back to the dashboard',
+ ],
+ 'label' => [
+ 'id' => 'ID',
+ 'actions' => 'Actions',
+ 'created_at' => 'Created At',
+ 'updated_at' => 'Updated At',
+ 'status' => 'Status',
+ 'no_data' => 'No Data',
+ ],
+ 'button' => [
+ 'save' => 'Save Changes',
+ 'cancel' => 'Cancel',
+ 'back' => 'Back',
+ 'close' => 'Close',
+ 'create' => 'Create',
+ 'update' => 'Update',
+ 'upload' => 'Upload',
+ 'edit' => 'Edit',
+ 'delete' => 'Delete',
+ 'suspend' => 'Suspend',
+ 'view' => 'View',
+ 'log' => 'Log',
+ 'yes' => 'Yes',
+ 'no' => 'No',
+ 'lookup' => 'Search...',
+ 'logout' => 'Logout',
+ ],
+ 'confirmation' => [
+ 'logout' => 'Are you sure you want to logout?',
+ 'delete' => 'Are you sure you want to delete this :resource? This action cannot be undone.',
+ 'suspend' => 'Are you sure you want to suspend this :resource?',
+ ],
+ 'crud' => [
+ 'success' => [
+ 'created' => ':resource has been created successfully.',
+ 'updated' => ':resource has been updated successfully.',
+ 'deleted' => ':resource has been removed.',
+ 'suspended' => ':resource has been suspended.',
+ 'uploaded' => ':resource has been uploaded successfully.',
+ ],
+ 'error' => [
+ 'forbidden' => 'You do not have permission to perform this action.',
+ 'validation_failed' => 'The given data was invalid.',
+ 'generic' => 'Something went wrong. Please try again.',
+ ],
+ ],
+ 'loading' => 'Loading...',
+ 'errors' => [
+ 'oops' => 'Oops… You just found an error page',
+ '404' => 'We are sorry but the page you are looking for was not found.',
+ '500' => 'We are sorry but our server encountered an internal error.',
+ 'take_me_home' => 'Take me home',
+ ],
+ 'notification' => [
+ 'empty' => 'No notifications',
+ 'read_all' => 'Read all notifications',
+ 'unread' => 'unread messages',
+ ],
+ 'excel' => [
+ 'import' => [
+ 'file_label' => 'Excel File',
+ 'success' => 'Import queued. You will receive an email when it is complete.',
+ ],
+ 'export' => [
+ 'success' => 'Export queued. You will receive an email with the file when it is ready.',
+ ],
+ ],
+];
diff --git a/lang/en/validation.php b/lang/en/validation.php
new file mode 100644
index 0000000..63ec29a
--- /dev/null
+++ b/lang/en/validation.php
@@ -0,0 +1,200 @@
+ 'The :attribute field must be accepted.',
+ 'accepted_if' => 'The :attribute field must be accepted when :other is :value.',
+ 'active_url' => 'The :attribute field must be a valid URL.',
+ 'after' => 'The :attribute field must be a date after :date.',
+ 'after_or_equal' => 'The :attribute field must be a date after or equal to :date.',
+ 'alpha' => 'The :attribute field must only contain letters.',
+ 'alpha_dash' => 'The :attribute field must only contain letters, numbers, dashes, and underscores.',
+ 'alpha_num' => 'The :attribute field must only contain letters and numbers.',
+ 'any_of' => 'The :attribute field is invalid.',
+ 'array' => 'The :attribute field must be an array.',
+ 'ascii' => 'The :attribute field must only contain single-byte alphanumeric characters and symbols.',
+ 'before' => 'The :attribute field must be a date before :date.',
+ 'before_or_equal' => 'The :attribute field must be a date before or equal to :date.',
+ 'between' => [
+ 'array' => 'The :attribute field must have between :min and :max items.',
+ 'file' => 'The :attribute field must be between :min and :max kilobytes.',
+ 'numeric' => 'The :attribute field must be between :min and :max.',
+ 'string' => 'The :attribute field must be between :min and :max characters.',
+ ],
+ 'boolean' => 'The :attribute field must be true or false.',
+ 'can' => 'The :attribute field contains an unauthorized value.',
+ 'confirmed' => 'The :attribute field confirmation does not match.',
+ 'contains' => 'The :attribute field is missing a required value.',
+ 'current_password' => 'The password is incorrect.',
+ 'date' => 'The :attribute field must be a valid date.',
+ 'date_equals' => 'The :attribute field must be a date equal to :date.',
+ 'date_format' => 'The :attribute field must match the format :format.',
+ 'decimal' => 'The :attribute field must have :decimal decimal places.',
+ 'declined' => 'The :attribute field must be declined.',
+ 'declined_if' => 'The :attribute field must be declined when :other is :value.',
+ 'different' => 'The :attribute field and :other must be different.',
+ 'digits' => 'The :attribute field must be :digits digits.',
+ 'digits_between' => 'The :attribute field must be between :min and :max digits.',
+ 'dimensions' => 'The :attribute field has invalid image dimensions.',
+ 'distinct' => 'The :attribute field has a duplicate value.',
+ 'doesnt_contain' => 'The :attribute field must not contain any of the following: :values.',
+ 'doesnt_end_with' => 'The :attribute field must not end with one of the following: :values.',
+ 'doesnt_start_with' => 'The :attribute field must not start with one of the following: :values.',
+ 'email' => 'The :attribute field must be a valid email address.',
+ 'encoding' => 'The :attribute field must be encoded in :encoding.',
+ 'ends_with' => 'The :attribute field must end with one of the following: :values.',
+ 'enum' => 'The selected :attribute is invalid.',
+ 'exists' => 'The selected :attribute is invalid.',
+ 'extensions' => 'The :attribute field must have one of the following extensions: :values.',
+ 'file' => 'The :attribute field must be a file.',
+ 'filled' => 'The :attribute field must have a value.',
+ 'gt' => [
+ 'array' => 'The :attribute field must have more than :value items.',
+ 'file' => 'The :attribute field must be greater than :value kilobytes.',
+ 'numeric' => 'The :attribute field must be greater than :value.',
+ 'string' => 'The :attribute field must be greater than :value characters.',
+ ],
+ 'gte' => [
+ 'array' => 'The :attribute field must have :value items or more.',
+ 'file' => 'The :attribute field must be greater than or equal to :value kilobytes.',
+ 'numeric' => 'The :attribute field must be greater than or equal to :value.',
+ 'string' => 'The :attribute field must be greater than or equal to :value characters.',
+ ],
+ 'hex_color' => 'The :attribute field must be a valid hexadecimal color.',
+ 'image' => 'The :attribute field must be an image.',
+ 'in' => 'The selected :attribute is invalid.',
+ 'in_array' => 'The :attribute field must exist in :other.',
+ 'in_array_keys' => 'The :attribute field must contain at least one of the following keys: :values.',
+ 'integer' => 'The :attribute field must be an integer.',
+ 'ip' => 'The :attribute field must be a valid IP address.',
+ 'ipv4' => 'The :attribute field must be a valid IPv4 address.',
+ 'ipv6' => 'The :attribute field must be a valid IPv6 address.',
+ 'json' => 'The :attribute field must be a valid JSON string.',
+ 'list' => 'The :attribute field must be a list.',
+ 'lowercase' => 'The :attribute field must be lowercase.',
+ 'lt' => [
+ 'array' => 'The :attribute field must have less than :value items.',
+ 'file' => 'The :attribute field must be less than :value kilobytes.',
+ 'numeric' => 'The :attribute field must be less than :value.',
+ 'string' => 'The :attribute field must be less than :value characters.',
+ ],
+ 'lte' => [
+ 'array' => 'The :attribute field must not have more than :value items.',
+ 'file' => 'The :attribute field must be less than or equal to :value kilobytes.',
+ 'numeric' => 'The :attribute field must be less than or equal to :value.',
+ 'string' => 'The :attribute field must be less than or equal to :value characters.',
+ ],
+ 'mac_address' => 'The :attribute field must be a valid MAC address.',
+ 'max' => [
+ 'array' => 'The :attribute field must not have more than :max items.',
+ 'file' => 'The :attribute field must not be greater than :max kilobytes.',
+ 'numeric' => 'The :attribute field must not be greater than :max.',
+ 'string' => 'The :attribute field must not be greater than :max characters.',
+ ],
+ 'max_digits' => 'The :attribute field must not have more than :max digits.',
+ 'mimes' => 'The :attribute field must be a file of type: :values.',
+ 'mimetypes' => 'The :attribute field must be a file of type: :values.',
+ 'min' => [
+ 'array' => 'The :attribute field must have at least :min items.',
+ 'file' => 'The :attribute field must be at least :min kilobytes.',
+ 'numeric' => 'The :attribute field must be at least :min.',
+ 'string' => 'The :attribute field must be at least :min characters.',
+ ],
+ 'min_digits' => 'The :attribute field must have at least :min digits.',
+ 'missing' => 'The :attribute field must be missing.',
+ 'missing_if' => 'The :attribute field must be missing when :other is :value.',
+ 'missing_unless' => 'The :attribute field must be missing unless :other is :value.',
+ 'missing_with' => 'The :attribute field must be missing when :values is present.',
+ 'missing_with_all' => 'The :attribute field must be missing when :values are present.',
+ 'multiple_of' => 'The :attribute field must be a multiple of :value.',
+ 'not_in' => 'The selected :attribute is invalid.',
+ 'not_regex' => 'The :attribute field format is invalid.',
+ 'numeric' => 'The :attribute field must be a number.',
+ 'password' => [
+ 'letters' => 'The :attribute field must contain at least one letter.',
+ 'mixed' => 'The :attribute field must contain at least one uppercase and one lowercase letter.',
+ 'numbers' => 'The :attribute field must contain at least one number.',
+ 'symbols' => 'The :attribute field must contain at least one symbol.',
+ 'uncompromised' => 'The given :attribute has appeared in a data leak. Please choose a different :attribute.',
+ ],
+ 'present' => 'The :attribute field must be present.',
+ 'present_if' => 'The :attribute field must be present when :other is :value.',
+ 'present_unless' => 'The :attribute field must be present unless :other is :value.',
+ 'present_with' => 'The :attribute field must be present when :values is present.',
+ 'present_with_all' => 'The :attribute field must be present when :values are present.',
+ 'prohibited' => 'The :attribute field is prohibited.',
+ 'prohibited_if' => 'The :attribute field is prohibited when :other is :value.',
+ 'prohibited_if_accepted' => 'The :attribute field is prohibited when :other is accepted.',
+ 'prohibited_if_declined' => 'The :attribute field is prohibited when :other is declined.',
+ 'prohibited_unless' => 'The :attribute field is prohibited unless :other is in :values.',
+ 'prohibits' => 'The :attribute field prohibits :other from being present.',
+ 'regex' => 'The :attribute field format is invalid.',
+ 'required' => 'The :attribute field is required.',
+ 'required_array_keys' => 'The :attribute field must contain entries for: :values.',
+ 'required_if' => 'The :attribute field is required when :other is :value.',
+ 'required_if_accepted' => 'The :attribute field is required when :other is accepted.',
+ 'required_if_declined' => 'The :attribute field is required when :other is declined.',
+ 'required_unless' => 'The :attribute field is required unless :other is in :values.',
+ 'required_with' => 'The :attribute field is required when :values is present.',
+ 'required_with_all' => 'The :attribute field is required when :values are present.',
+ 'required_without' => 'The :attribute field is required when :values is not present.',
+ 'required_without_all' => 'The :attribute field is required when none of :values are present.',
+ 'same' => 'The :attribute field must match :other.',
+ 'size' => [
+ 'array' => 'The :attribute field must contain :size items.',
+ 'file' => 'The :attribute field must be :size kilobytes.',
+ 'numeric' => 'The :attribute field must be :size.',
+ 'string' => 'The :attribute field must be :size characters.',
+ ],
+ 'starts_with' => 'The :attribute field must start with one of the following: :values.',
+ 'string' => 'The :attribute field must be a string.',
+ 'timezone' => 'The :attribute field must be a valid timezone.',
+ 'unique' => 'The :attribute has already been taken.',
+ 'uploaded' => 'The :attribute failed to upload.',
+ 'uppercase' => 'The :attribute field must be uppercase.',
+ 'url' => 'The :attribute field must be a valid URL.',
+ 'ulid' => 'The :attribute field must be a valid ULID.',
+ 'uuid' => 'The :attribute field must be a valid UUID.',
+
+ /*
+ |--------------------------------------------------------------------------
+ | Custom Validation Language Lines
+ |--------------------------------------------------------------------------
+ |
+ | Here you may specify custom validation messages for attributes using the
+ | convention "attribute.rule" to name the lines. This makes it quick to
+ | specify a specific custom language line for a given attribute rule.
+ |
+ */
+
+ 'custom' => [
+ 'attribute-name' => [
+ 'rule-name' => 'custom-message',
+ ],
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Custom Validation Attributes
+ |--------------------------------------------------------------------------
+ |
+ | The following language lines are used to swap our attribute placeholder
+ | with something more reader friendly such as "E-Mail Address" instead
+ | of "email". This simply helps us make our message more expressive.
+ |
+ */
+
+ 'attributes' => [],
+
+];
diff --git a/lang/id/auth.php b/lang/id/auth.php
new file mode 100644
index 0000000..86c3c16
--- /dev/null
+++ b/lang/id/auth.php
@@ -0,0 +1,21 @@
+ 'Kredensial ini tidak cocok dengan data kami.',
+ 'password' => 'Kata sandi yang diberikan salah.',
+ 'throttle' => 'Terlalu banyak percobaan masuk. Silakan coba lagi dalam :seconds detik.',
+ 'inactive' => 'Akun Anda tidak aktif atau ditangguhkan. Silakan hubungi dukungan.',
+
+];
diff --git a/lang/id/domains/account/enum.php b/lang/id/domains/account/enum.php
new file mode 100644
index 0000000..e8095c4
--- /dev/null
+++ b/lang/id/domains/account/enum.php
@@ -0,0 +1,25 @@
+ [
+ GenderOption::MALE->value => 'Laki-laki',
+ GenderOption::FEMALE->value => 'Perempuan',
+ ],
+ 'user_settings' => [
+ 'notification' => 'Notifikasi',
+ 'language' => 'Bahasa',
+ 'timezone' => 'Zona Waktu',
+ 'options' => [
+ 'language' => [
+ 'en' => 'Inggris',
+ 'id' => 'Indonesia',
+ ],
+ 'notification' => [
+ 'on' => 'Aktif',
+ 'off' => 'Nonaktif',
+ ],
+ ],
+ ],
+];
diff --git a/lang/id/domains/account/field.php b/lang/id/domains/account/field.php
new file mode 100644
index 0000000..0c8976e
--- /dev/null
+++ b/lang/id/domains/account/field.php
@@ -0,0 +1,10 @@
+ [
+ 'avatar' => 'Avatar',
+ 'gender' => 'Jenis Kelamin',
+ 'date_of_birth' => 'Tanggal Lahir',
+ 'phone_number' => 'Nomor Telepon',
+ ],
+];
diff --git a/lang/id/domains/account/pages.php b/lang/id/domains/account/pages.php
new file mode 100644
index 0000000..fb0bf69
--- /dev/null
+++ b/lang/id/domains/account/pages.php
@@ -0,0 +1,20 @@
+ [
+ 'title' => 'Informasi Profil',
+ 'description' => 'Perbarui informasi profil akun dan alamat email Anda.',
+ ],
+ 'password' => [
+ 'title' => 'Perbarui Kata Sandi',
+ 'description' => 'Pastikan akun Anda menggunakan kata sandi yang panjang dan acak untuk tetap aman.',
+ ],
+ 'delete_account' => [
+ 'title' => 'Hapus Akun',
+ 'description' => 'Setelah akun Anda dihapus, semua sumber daya dan datanya akan dihapus secara permanen.',
+ ],
+ 'user_settings' => [
+ 'title' => 'Pengaturan Pengguna',
+ 'description' => 'Kelola preferensi aplikasi Anda seperti tema dan bahasa.',
+ ],
+];
diff --git a/lang/id/domains/account/seo.php b/lang/id/domains/account/seo.php
new file mode 100644
index 0000000..bde1416
--- /dev/null
+++ b/lang/id/domains/account/seo.php
@@ -0,0 +1,9 @@
+ [
+ 'title' => 'Informasi Profil',
+ 'description' => 'Perbarui informasi profil akun dan alamat email Anda.',
+ 'keywords' => 'profil, pengaturan akun, perbarui email',
+ ],
+];
diff --git a/lang/id/domains/auth/field.php b/lang/id/domains/auth/field.php
new file mode 100644
index 0000000..dbd86fb
--- /dev/null
+++ b/lang/id/domains/auth/field.php
@@ -0,0 +1,35 @@
+ [
+ 'email' => 'Alamat Email',
+ 'password' => 'Kata Sandi',
+ 'remember' => 'Ingat saya',
+ 'forgot_password' => 'Lupa Kata Sandi?',
+ 'submit' => 'Masuk',
+ ],
+ 'register' => [
+ 'name' => 'Nama Lengkap',
+ 'email' => 'Alamat Email',
+ 'password' => 'Kata Sandi',
+ 'confirm_password' => 'Konfirmasi Kata Sandi',
+ 'submit' => 'Daftar',
+ ],
+ 'forgot_password' => [
+ 'email' => 'Alamat Email',
+ 'submit' => 'Kirim Tautan Atur Ulang Kata Sandi',
+ ],
+ 'reset_password' => [
+ 'email' => 'Alamat Email',
+ 'password' => 'Kata Sandi Baru',
+ 'confirm_password' => 'Konfirmasi Kata Sandi',
+ 'submit' => 'Atur Ulang Kata Sandi',
+ ],
+ 'verify_email' => [
+ 'submit' => 'Kirim Ulang Email Verifikasi',
+ ],
+ 'confirm_password' => [
+ 'password' => 'Kata Sandi',
+ 'submit' => 'Konfirmasi',
+ ],
+];
diff --git a/lang/id/domains/auth/messages.php b/lang/id/domains/auth/messages.php
new file mode 100644
index 0000000..d4495cc
--- /dev/null
+++ b/lang/id/domains/auth/messages.php
@@ -0,0 +1,8 @@
+ 'Kata sandi Anda telah berhasil diatur ulang.',
+ 'invalid_token' => 'Token atur ulang kata sandi ini tidak valid.',
+ 'reset_link_sent' => 'Tautan atur ulang kata sandi telah dikirim ke email Anda.',
+ 'reset_link_failed' => 'Silakan periksa email Anda untuk mengatur ulang kata sandi.',
+];
diff --git a/lang/id/domains/auth/notifications.php b/lang/id/domains/auth/notifications.php
new file mode 100644
index 0000000..e9165a3
--- /dev/null
+++ b/lang/id/domains/auth/notifications.php
@@ -0,0 +1,28 @@
+ [
+ 'subject' => 'Masuk baru ke akun Anda',
+ 'greeting' => 'Halo :name!',
+ 'intro' => 'Kami menyadari ada aktivitas masuk baru ke akun :app Anda.',
+ 'time' => 'Waktu: :time',
+ 'ip' => 'Alamat IP: :ip',
+ 'browser' => 'Peramban: :browser',
+ 'outro' => 'Jika ini Anda, Anda dapat mengabaikan email ini. Jika Anda tidak masuk, harap segera amankan akun Anda.',
+ 'action' => 'Ke Dasbor',
+ 'title' => 'Aktivitas Masuk',
+ 'message' => 'Anda telah berhasil masuk ke akun Anda dari :ip.',
+ ],
+ 'verify_email' => [
+ 'subject' => 'Verifikasi alamat email Anda',
+ 'intro' => 'Silakan klik tombol di bawah ini untuk memverifikasi alamat email Anda.',
+ 'action' => 'Verifikasi Alamat Email',
+ 'outro' => 'Jika Anda tidak membuat akun, tidak ada tindakan lebih lanjut yang diperlukan.',
+ ],
+ 'reset_password' => [
+ 'subject' => 'Pemberitahuan Atur Ulang Kata Sandi',
+ 'intro' => 'Anda menerima email ini karena kami menerima permintaan atur ulang kata sandi untuk akun Anda.',
+ 'action' => 'Atur Ulang Kata Sandi',
+ 'outro' => 'Jika Anda tidak meminta atur ulang kata sandi, tidak ada tindakan lebih lanjut yang diperlukan.',
+ ],
+];
diff --git a/lang/id/domains/auth/pages.php b/lang/id/domains/auth/pages.php
new file mode 100644
index 0000000..5052980
--- /dev/null
+++ b/lang/id/domains/auth/pages.php
@@ -0,0 +1,31 @@
+ [
+ 'header' => 'Masuk ke akun Anda',
+ 'no_account' => 'Belum punya akun?',
+ 'register_link' => 'Daftar',
+ ],
+ 'register' => [
+ 'header' => 'Buat akun baru',
+ 'has_account' => 'Sudah punya akun?',
+ 'login_link' => 'Masuk',
+ ],
+ 'forgot_password' => [
+ 'header' => 'Lupa kata sandi Anda?',
+ 'subheader' => 'Tidak masalah. Beritahu kami alamat email Anda dan kami akan mengirimkan tautan atur ulang kata sandi yang memungkinkan Anda memilih yang baru.',
+ 'back_to_login' => 'Kembali ke halaman masuk',
+ ],
+ 'reset_password' => [
+ 'header' => 'Atur Ulang Kata Sandi',
+ ],
+ 'verify_email' => [
+ 'header' => 'Verifikasi Email',
+ 'subheader' => 'Terima kasih telah mendaftar! Sebelum memulai, bisakah Anda memverifikasi alamat email Anda dengan mengklik tautan yang baru saja kami kirimkan melalui email? Jika Anda tidak menerima email tersebut, kami dengan senang hati akan mengirimkan yang baru.',
+ 'resend_link' => 'Tautan verifikasi baru telah dikirim ke alamat email yang Anda berikan saat pendaftaran.',
+ ],
+ 'confirm_password' => [
+ 'header' => 'Konfirmasi Kata Sandi',
+ 'subheader' => 'Ini adalah area aman aplikasi. Silakan konfirmasi kata sandi Anda sebelum melanjutkan.',
+ ],
+];
diff --git a/lang/id/domains/auth/seo.php b/lang/id/domains/auth/seo.php
new file mode 100644
index 0000000..745620d
--- /dev/null
+++ b/lang/id/domains/auth/seo.php
@@ -0,0 +1,34 @@
+ [
+ 'title' => 'Masuk',
+ 'description' => 'Masuk ke akun Anda untuk mengakses dasbor.',
+ 'keywords' => 'masuk, login, auth',
+ ],
+ 'register' => [
+ 'title' => 'Daftar',
+ 'description' => 'Buat akun baru untuk memulai.',
+ 'keywords' => 'daftar, register, auth',
+ ],
+ 'forgot_password' => [
+ 'title' => 'Lupa Kata Sandi',
+ 'description' => 'Pulihkan kata sandi akun Anda.',
+ 'keywords' => 'lupa kata sandi, pulihkan, auth',
+ ],
+ 'reset_password' => [
+ 'title' => 'Atur Ulang Kata Sandi',
+ 'description' => 'Tetapkan kata sandi baru untuk akun Anda.',
+ 'keywords' => 'atur ulang kata sandi, auth',
+ ],
+ 'verify_email' => [
+ 'title' => 'Verifikasi Email',
+ 'description' => 'Verifikasi alamat email Anda untuk mengamankan akun Anda.',
+ 'keywords' => 'verifikasi email, auth',
+ ],
+ 'confirm_password' => [
+ 'title' => 'Konfirmasi Kata Sandi',
+ 'description' => 'Harap konfirmasi kata sandi Anda sebelum melanjutkan.',
+ 'keywords' => 'konfirmasi kata sandi, auth',
+ ],
+];
diff --git a/lang/id/domains/identity/dashboard.php b/lang/id/domains/identity/dashboard.php
new file mode 100644
index 0000000..e48c81b
--- /dev/null
+++ b/lang/id/domains/identity/dashboard.php
@@ -0,0 +1,25 @@
+ [
+ 'title' => 'Total Pengguna',
+ 'growth' => ':rate sejak bulan lalu',
+ ],
+ 'new_user_count' => [
+ 'title' => 'Pengguna Baru',
+ 'growth' => ':rate sejak bulan lalu',
+ ],
+ 'user_verification_rate' => [
+ 'title' => 'Tingkat Verifikasi',
+ 'detail' => ':verified terverifikasi / :unverified Belum Terverifikasi',
+ ],
+ 'user_growth' => [
+ 'title' => 'Pertumbuhan',
+ 'subtitle' => 'Tren bulanan pendaftaran pengguna baru di tahun :year',
+ 'series_name' => 'Registrasi',
+ ],
+ 'role_distribution' => [
+ 'title' => 'Peran',
+ 'subtitle' => 'Rincian pengguna yang ditugaskan untuk setiap peran',
+ ],
+];
diff --git a/lang/id/domains/identity/enum.php b/lang/id/domains/identity/enum.php
new file mode 100644
index 0000000..5fc7ee8
--- /dev/null
+++ b/lang/id/domains/identity/enum.php
@@ -0,0 +1,10 @@
+ [
+ UserStatus::ACTIVE->value => 'Aktif',
+ UserStatus::INACTIVE->value => 'Non Aktif',
+ ],
+];
diff --git a/lang/id/domains/identity/field.php b/lang/id/domains/identity/field.php
new file mode 100644
index 0000000..dee1133
--- /dev/null
+++ b/lang/id/domains/identity/field.php
@@ -0,0 +1,21 @@
+ [
+ 'name' => 'Nama Lengkap',
+ 'email' => 'Alamat Email',
+ 'status' => 'Status',
+ 'password' => 'Kata Sandi Aman',
+ 'current_password' => 'Kata Sandi Saat Ini',
+ 'new_password' => 'Kata Sandi Baru',
+ 'password_confirmation' => 'Konfirmasi Kata Sandi',
+ 'verified' => 'Terverifikasi',
+ 'unverified' => 'Belum Terverifikasi',
+ ],
+ 'role' => [
+ 'name' => 'Nama Peran',
+ 'permission_count' => 'Jumlah Izin',
+ 'guard_name' => 'Nama Guard',
+ 'permissions' => 'Izin',
+ ],
+];
diff --git a/lang/id/domains/identity/messages.php b/lang/id/domains/identity/messages.php
new file mode 100644
index 0000000..daed0ce
--- /dev/null
+++ b/lang/id/domains/identity/messages.php
@@ -0,0 +1,22 @@
+ [
+ 'password_reset_link_sent' => 'Tautan atur ulang kata sandi telah dikirim.',
+ 'user_role_updated' => 'Peran pengguna telah diperbarui.',
+ 'user_status_updated' => 'Status pengguna telah diperbarui.'
+ ],
+ 'exceptions' => [
+ 'failed_to_send_password_reset_link' => 'Gagal mengirim tautan atur ulang kata sandi.',
+ 'failed_to_update_user_role' => 'Gagal memperbarui peran pengguna.',
+ 'failed_to_update_user_status' => 'Gagal memperbarui status pengguna.',
+ 'user_already_active' => 'Pengguna ini sudah aktif.',
+ 'user_already_suspended' => 'Pengguna ini sudah dinonaktifkan.',
+ 'user_already_in_status' => 'Pengguna sudah dalam status :status.',
+ 'user_cannot_be_edited' => 'Pengguna ini tidak dapat diubah.',
+ 'user_cannot_be_purged' => 'Anda tidak dapat menghapus admin.',
+ 'user_cannot_be_suspended' => 'Anda tidak dapat menonaktifkan admin.',
+ 'cannot_remove_system_role' => 'Tidak dapat menghapus peran sistem.',
+ 'role_has_users' => 'Peran ini memiliki pengguna yang terlampir.',
+ ],
+];
diff --git a/lang/id/domains/identity/notifications.php b/lang/id/domains/identity/notifications.php
new file mode 100644
index 0000000..ef539a9
--- /dev/null
+++ b/lang/id/domains/identity/notifications.php
@@ -0,0 +1,29 @@
+ [
+ 'subject' => 'Selamat Datang!',
+ 'body' => 'Akun Anda sudah siap.',
+ 'action' => 'Masuk',
+ ],
+ 'governance' => [
+ 'user_suspended' => [
+ 'subject' => 'Akun Ditangguhkan',
+ 'intro' => 'Kami menulis email ini untuk memberitahu bahwa akun Anda telah ditangguhkan.',
+ 'body' => 'Tindakan ini diambil karena pelanggaran terhadap persyaratan layanan kami atau masalah keamanan terkait akun Anda.',
+ 'outro' => 'Jika Anda merasa ini adalah kesalahan, silakan hubungi tim dukungan kami.',
+ ],
+ 'user_purged' => [
+ 'subject' => 'Akun Dihapus Secara Permanen',
+ 'intro' => 'Kami menulis email ini untuk memberitahu bahwa akun Anda dan semua data terkait telah dihapus secara permanen.',
+ 'body' => 'Tindakan ini diambil baik berdasarkan permintaan Anda atau karena pelanggaran berat terhadap kebijakan kami.',
+ 'outro' => 'Jika Anda memiliki pertanyaan, silakan hubungi tim dukungan kami.',
+ ],
+ 'user_activated' => [
+ 'subject' => 'Akun Diaktifkan',
+ 'intro' => 'Kami senang memberitahu Anda bahwa akun Anda telah berhasil diaktifkan.',
+ 'body' => 'Anda sekarang dapat masuk dan mengakses semua fitur akun Anda.',
+ 'outro' => 'Jika Anda memiliki pertanyaan, silakan hubungi tim dukungan kami.',
+ ],
+ ],
+];
diff --git a/lang/id/domains/identity/pages.php b/lang/id/domains/identity/pages.php
new file mode 100644
index 0000000..8da875c
--- /dev/null
+++ b/lang/id/domains/identity/pages.php
@@ -0,0 +1,20 @@
+ [
+ 'account_info' => 'Info Akun',
+ 'user_info' => 'Info Pengguna',
+ 'menu' => [
+ 'role_update' => 'Ubah Peran',
+ 'toggle_status' => 'Ubah Status',
+ 'password_reset' => 'Kirim Reset Kata Sandi',
+ ],
+ 'confirmation' => [
+ 'toggle_status' => 'Apakah Anda yakin ingin mengubah status pengguna ini?',
+ 'status_changed' => 'Status pengguna berhasil diperbarui.',
+ 'status_unchanged' => 'Status pengguna tidak diubah.',
+ 'send_password_reset' => 'Apakah Anda yakin ingin mengirimkan tautan setel ulang kata sandi kepada pengguna ini?',
+ 'password_reset_success' => 'Tautan setel ulang kata sandi berhasil dikirim.',
+ ],
+ ],
+];
diff --git a/lang/id/domains/identity/seo.php b/lang/id/domains/identity/seo.php
new file mode 100644
index 0000000..6ed7c77
--- /dev/null
+++ b/lang/id/domains/identity/seo.php
@@ -0,0 +1,19 @@
+ [
+ 'title' => 'Manajemen Pengguna',
+ 'description' => 'Kelola akun pengguna, peran, dan izin.',
+ 'keywords' => 'manajemen pengguna, pengguna, identitas',
+ ],
+ 'user_detail' => [
+ 'title' => 'Detail dari :name',
+ 'description' => 'Kelola akun pengguna, peran, dan izin.',
+ 'keywords' => 'manajemen pengguna, pengguna, identitas',
+ ],
+ 'role' => [
+ 'title' => 'Manajemen Peran',
+ 'description' => 'Kelola peran pengguna dan izin.',
+ 'keywords' => 'manajemen peran, peran, izin, identitas',
+ ],
+];
diff --git a/lang/id/domains/system/field.php b/lang/id/domains/system/field.php
new file mode 100644
index 0000000..3b439b2
--- /dev/null
+++ b/lang/id/domains/system/field.php
@@ -0,0 +1,31 @@
+ [
+ 'file' => 'Berkas Cadangan',
+ ],
+ 'settings' => [
+ 'web' => [
+ 'name' => 'Nama Web',
+ 'description' => 'Deskripsi Web',
+ 'address' => 'Alamat Web',
+ 'phone' => 'Telepon Web',
+ 'email' => 'Email Web',
+ 'logo' => 'Logo Web',
+ 'favicon' => 'Favicon Web',
+ ],
+ 'default_language' => 'Bahasa Default',
+ 'timezone' => 'Zona Waktu',
+ 'google' => [
+ 'tag_manager_id' => 'ID Google Tag Manager',
+ 'webmaster_id' => 'ID Google Webmaster',
+ ],
+ ],
+ 'audit' => [
+ 'ip_address' => 'Alamat IP',
+ 'browser' => 'Peramban',
+ 'field' => 'Kolom',
+ 'old' => 'Lama',
+ 'new' => 'Baru',
+ ],
+];
diff --git a/lang/id/domains/system/messages.php b/lang/id/domains/system/messages.php
new file mode 100644
index 0000000..4b9591a
--- /dev/null
+++ b/lang/id/domains/system/messages.php
@@ -0,0 +1,15 @@
+ [
+ 'delete_success' => 'Data cadangan berhasil dihapus.',
+ 'backup_success' => 'Sistem berhasil dicadangkan.',
+ 'backup_error' => 'Gagal mencadangkan sistem.',
+ 'verification_error' => 'Berkas zip cadangan tidak dapat diverifikasi di penyimpanan target.',
+ 'restored_success' => 'Sistem berhasil dipulihkan.',
+ 'restored_error' => 'Gagal memulihkan sistem.',
+ 'download_error' => 'Gagal mengunduh :path.',
+ 'file_not_found' => 'Berkas cadangan tidak ditemukan.',
+ 'path_required' => 'Jalur diperlukan.',
+ ],
+];
diff --git a/lang/id/domains/system/notifications.php b/lang/id/domains/system/notifications.php
new file mode 100644
index 0000000..95beb44
--- /dev/null
+++ b/lang/id/domains/system/notifications.php
@@ -0,0 +1,18 @@
+ [
+ 'import_email' => [
+ 'subject' => 'Impor Excel Anda Telah Diproses',
+ 'intro' => 'Permintaan impor Excel Anda telah selesai.',
+ 'body' => 'Data dari berkas yang Anda unggah telah berhasil diimpor ke dalam sistem.',
+ 'outro' => 'Jika Anda tidak memulai impor ini, segera hubungi administrator.',
+ ],
+ 'export_email' => [
+ 'subject' => 'Ekspor Excel Anda Siap',
+ 'intro' => 'Permintaan ekspor Excel Anda telah selesai.',
+ 'body' => 'Silakan temukan berkas hasil ekspor yang terlampir pada email ini.',
+ 'outro' => 'Jika Anda tidak meminta ekspor ini, segera hubungi administrator.',
+ ],
+ ],
+];
diff --git a/lang/id/domains/system/pages.php b/lang/id/domains/system/pages.php
new file mode 100644
index 0000000..f64440e
--- /dev/null
+++ b/lang/id/domains/system/pages.php
@@ -0,0 +1,22 @@
+ [
+ 'title' => 'Katalog Cadangan Sistem',
+ 'backup_button' => 'Cadangkan',
+ 'upload_button' => 'Unggah Berkas Cadangan',
+ 'upload_modal_title' => 'Unggah Berkas Cadangan',
+ 'empty_state' => 'Tidak Ada Berkas Cadangan yang Disimpan',
+ 'confirmation' => [
+ 'delete' => 'Apakah Anda yakin ingin menghapus cadangan ini?',
+ ],
+ ],
+ 'settings' => [
+ 'update_title' => 'Perbarui Pengaturan',
+ 'sections' => [
+ 'web' => 'Web',
+ 'general' => 'Umum',
+ 'webmaster' => 'Webmaster',
+ ],
+ ],
+];
diff --git a/lang/id/domains/system/seo.php b/lang/id/domains/system/seo.php
new file mode 100644
index 0000000..4ddfb8c
--- /dev/null
+++ b/lang/id/domains/system/seo.php
@@ -0,0 +1,14 @@
+ [
+ 'title' => 'Pengaturan Sistem',
+ 'description' => 'Pengaturan dan konfigurasi sistem.',
+ 'keywords' => 'sistem, pengaturan, konfigurasi',
+ ],
+ 'backup' => [
+ 'title' => 'Cadangan Sistem',
+ 'description' => 'Cadangkan dan pulihkan basis data dan aset.',
+ 'keywords' => 'cadangkan, pulihkan, basis data, aset',
+ ],
+];
diff --git a/lang/id/event.php b/lang/id/event.php
new file mode 100644
index 0000000..a45d0bb
--- /dev/null
+++ b/lang/id/event.php
@@ -0,0 +1,8 @@
+ 'Dibuat',
+ 'updated' => 'Diperbarui',
+ 'deleted' => 'Dihapus',
+ 'permissions_synced' => 'Perizinan disinkronisasi',
+];
diff --git a/lang/id/pagination.php b/lang/id/pagination.php
new file mode 100644
index 0000000..8409f26
--- /dev/null
+++ b/lang/id/pagination.php
@@ -0,0 +1,19 @@
+ '« Sebelumnya',
+ 'next' => 'Berikutnya »',
+
+];
diff --git a/lang/id/passwords.php b/lang/id/passwords.php
new file mode 100644
index 0000000..6608d7f
--- /dev/null
+++ b/lang/id/passwords.php
@@ -0,0 +1,22 @@
+ 'Kata sandi Anda telah diatur ulang.',
+ 'sent' => 'Kami telah mengirimkan tautan atur ulang kata sandi melalui email.',
+ 'throttled' => 'Harap tunggu sebelum mencoba kembali.',
+ 'token' => 'Token atur ulang kata sandi ini tidak valid.',
+ 'user' => 'Kami tidak dapat menemukan pengguna dengan alamat email tersebut.',
+
+];
diff --git a/lang/id/permissions.php b/lang/id/permissions.php
new file mode 100644
index 0000000..f62b500
--- /dev/null
+++ b/lang/id/permissions.php
@@ -0,0 +1,34 @@
+ [
+ 'group-name' => 'Dasbor',
+ 'index' => 'Memberikan wewenang untuk mengakses dan melihat ringkasan data pada dasbor utama aplikasi.',
+ 'admin' => 'Memberikan akses khusus ke panel kontrol administrator untuk pemantauan sistem yang komprehensif.',
+ 'user' => 'Memberikan akses ke dasbor pribadi yang berisi informasi yang relevan bagi pengguna umum.',
+ ],
+ 'user' => [
+ 'group-name' => 'Manajemen Pengguna',
+ 'viewAny' => 'Memungkinkan pengguna untuk melihat dan mencari daftar semua akun pengguna yang terdaftar di sistem.',
+ 'view' => 'Memungkinkan pengguna untuk melihat informasi rinci dari akun pengguna.',
+ 'create' => 'Memberikan akses untuk mendaftarkan dan menambah akun pengguna baru ke sistem.',
+ 'update' => 'Memungkinkan modifikasi data profil, status, dan informasi rinci lainnya untuk akun pengguna yang ada.',
+ 'delete' => 'Memberikan wewenang untuk menghapus atau menonaktifkan akun pengguna secara permanen dari sistem.',
+ ],
+ 'role' => [
+ 'group-name' => 'Peran & Izin',
+ 'viewAny' => 'Memungkinkan pengguna untuk melihat dan mencari daftar semua peran yang tersedia di dalam aplikasi.',
+ 'view' => 'Memungkinkan pengguna untuk melihat informasi rinci dari peran.',
+ 'create' => 'Memberikan kemampuan untuk merancang dan membuat tingkat peran baru dengan izin akses tertentu.',
+ 'update' => 'Memungkinkan modifikasi nama peran dan penyesuaian kembali daftar izin untuk peran yang ada.',
+ 'delete' => 'Memberikan wewenang untuk menghapus tingkat peran yang tidak lagi diperlukan oleh sistem.',
+ ],
+ 'system-setting' => [
+ 'group-name' => 'Pengaturan Sistem',
+ 'manage' => 'Memberikan akses pengaturan sistem.',
+ ],
+ 'system-backup' => [
+ 'group-name' => 'Cadangkan Sistem',
+ 'manage' => 'Memberikan akses untuk membuat cadangan dan memulihkan sistem.',
+ ],
+];
diff --git a/lang/id/resources.php b/lang/id/resources.php
new file mode 100644
index 0000000..2d86d5d
--- /dev/null
+++ b/lang/id/resources.php
@@ -0,0 +1,14 @@
+ 'Pengguna',
+ 'role' => 'Peran',
+ 'profile' => 'Profil',
+ 'settings' => 'Pengaturan',
+ 'password' => 'Kata Sandi',
+ 'backup' => 'Cadangan',
+ 'backup_file' => 'Berkas Cadangan',
+ 'avatar' => 'Avatar',
+ 'system_settings' => 'Pengaturan Sistem',
+ 'audit' => 'Audit',
+];
diff --git a/lang/id/seo.php b/lang/id/seo.php
new file mode 100644
index 0000000..24ca9ae
--- /dev/null
+++ b/lang/id/seo.php
@@ -0,0 +1,11 @@
+ 'Laravel Base',
+ 'default_description' => 'Dasar Laravel menggunakan Livewire v4, Bootstrap, dan DataTables.net oleh Syarif Ubaidillah.',
+ 'dashboard' => [
+ 'title' => 'Dasbor',
+ 'description' => 'Dasbor analitik untuk aplikasi ini.',
+ 'keywords' => 'dasbor, analitik, ulasan',
+ ],
+];
diff --git a/lang/id/ui.php b/lang/id/ui.php
new file mode 100644
index 0000000..66f0dd6
--- /dev/null
+++ b/lang/id/ui.php
@@ -0,0 +1,92 @@
+ [
+ 'dashboard' => 'Dasbor',
+ 'identity' => 'Manajemen Pengguna',
+ 'users' => 'Pengguna',
+ 'roles' => 'Peran & Izin',
+ 'settings' => 'Pengaturan Sistem',
+ 'system_backup' => 'Cadangkan Sistem',
+ ],
+ 'title' => [
+ 'index' => 'Data :resource',
+ 'create' => 'Buat :resource Baru',
+ 'update' => 'Perbarui :resource',
+ 'delete' => 'Hapus :resource',
+ 'view' => 'Lihat :resource',
+ 'upload' => 'Unggah :resource',
+ 'restore' => 'Pulihkan :resource',
+ 'import' => 'Impor :resource',
+ ],
+ 'greetings' => [
+ 'morning' => 'Selamat pagi, :name',
+ 'welcome' => 'Selamat datang kembali di dasbor',
+ ],
+ 'label' => [
+ 'id' => 'ID',
+ 'actions' => 'Aksi',
+ 'created_at' => 'Dibuat Pada',
+ 'updated_at' => 'Diperbarui Pada',
+ 'status' => 'Status',
+ 'no_data' => 'Tidak Ada Data',
+ ],
+ 'button' => [
+ 'save' => 'Simpan Perubahan',
+ 'cancel' => 'Batal',
+ 'back' => 'Kembali',
+ 'close' => 'Tutup',
+ 'create' => 'Buat',
+ 'update' => 'Perbarui',
+ 'upload' => 'Unggah',
+ 'edit' => 'Ubah',
+ 'delete' => 'Hapus',
+ 'suspend' => 'Tangguhkan',
+ 'view' => 'Lihat',
+ 'log' => 'Log',
+ 'yes' => 'Ya',
+ 'no' => 'Tidak',
+ 'lookup' => 'Cari...',
+ 'logout' => 'Keluar',
+ ],
+ 'confirmation' => [
+ 'logout' => 'Apakah Anda yakin ingin keluar?',
+ 'delete' => 'Apakah Anda yakin ingin menghapus :resource ini? Tindakan ini tidak dapat dibatalkan.',
+ 'suspend' => 'Apakah Anda yakin ingin menangguhkan :resource ini?',
+ ],
+ 'crud' => [
+ 'success' => [
+ 'created' => ':resource telah berhasil dibuat.',
+ 'updated' => ':resource telah berhasil diperbarui.',
+ 'deleted' => ':resource telah dihapus.',
+ 'suspended' => ':resource telah ditangguhkan.',
+ 'uploaded' => ':resource telah berhasil diunggah.',
+ ],
+ 'error' => [
+ 'forbidden' => 'Anda tidak memiliki izin untuk melakukan tindakan ini.',
+ 'validation_failed' => 'Data yang diberikan tidak valid.',
+ 'generic' => 'Terjadi kesalahan. Silakan coba lagi.',
+ ],
+ ],
+ 'loading' => 'Memuat...',
+ 'errors' => [
+ 'oops' => 'Ups... Terjadi kesalahan.',
+ '404' => 'Maaf, halaman yang Anda cari tidak ditemukan.',
+ '500' => 'Maaf, server kami sedang mengalami gangguan.',
+ 'take_me_home' => 'Kembali ke Beranda',
+ ],
+ 'notification' => [
+ 'empty' => 'Tidak ada pemberitahuan',
+ 'read_all' => 'Baca semua pemberitahuan',
+ 'unread' => 'pesan belum dibaca',
+ ],
+ 'excel' => [
+ 'import' => [
+ 'file_label' => 'Berkas Excel',
+ 'success' => 'Impor dijadwalkan. Anda akan menerima email ketika selesai.',
+ ],
+ 'export' => [
+ 'success' => 'Ekspor dijadwalkan. Anda akan menerima email beserta berkasnya ketika siap.',
+ ],
+ ],
+];
diff --git a/lang/id/validation.php b/lang/id/validation.php
new file mode 100644
index 0000000..61b1b45
--- /dev/null
+++ b/lang/id/validation.php
@@ -0,0 +1,193 @@
+ ':attribute harus diterima.',
+ 'accepted_if' => ':attribute harus diterima ketika :other adalah :value.',
+ 'active_url' => ':attribute bukan URL yang valid.',
+ 'after' => ':attribute harus berupa tanggal setelah :date.',
+ 'after_or_equal' => ':attribute harus berupa tanggal setelah atau sama dengan :date.',
+ 'alpha' => ':attribute hanya boleh berisi huruf.',
+ 'alpha_dash' => ':attribute hanya boleh berisi huruf, angka, strip, dan garis bawah.',
+ 'alpha_num' => ':attribute hanya boleh berisi huruf dan angka.',
+ 'array' => ':attribute harus berupa array.',
+ 'ascii' => ':attribute hanya boleh berisi karakter alfanumerik dan simbol single-byte.',
+ 'before' => ':attribute harus berupa tanggal sebelum :date.',
+ 'before_or_equal' => ':attribute harus berupa tanggal sebelum atau sama dengan :date.',
+ 'between' => [
+ 'array' => ':attribute harus memiliki antara :min dan :max item.',
+ 'file' => ':attribute harus berukuran antara :min dan :max kilobita.',
+ 'numeric' => ':attribute harus bernilai antara :min dan :max.',
+ 'string' => ':attribute harus berukuran antara :min dan :max karakter.',
+ ],
+ 'boolean' => ':attribute harus bernilai true atau false.',
+ 'can' => ':attribute berisi nilai yang tidak diizinkan.',
+ 'confirmed' => 'Konfirmasi :attribute tidak cocok.',
+ 'contains' => ':attribute kekurangan nilai yang diperlukan.',
+ 'current_password' => 'Kata sandi salah.',
+ 'date' => ':attribute bukan tanggal yang valid.',
+ 'date_equals' => ':attribute harus berupa tanggal yang sama dengan :date.',
+ 'date_format' => ':attribute tidak cocok dengan format :format.',
+ 'decimal' => ':attribute harus memiliki :decimal tempat desimal.',
+ 'declined' => ':attribute harus ditolak.',
+ 'declined_if' => ':attribute harus ditolak ketika :other adalah :value.',
+ 'different' => ':attribute dan :other harus berbeda.',
+ 'digits' => ':attribute harus terdiri dari :digits angka.',
+ 'digits_between' => ':attribute harus berukuran antara :min dan :max angka.',
+ 'dimensions' => ':attribute memiliki dimensi gambar yang tidak valid.',
+ 'distinct' => ':attribute memiliki nilai duplikat.',
+ 'doesnt_contain' => ':attribute tidak boleh berisi salah satu dari berikut ini: :values.',
+ 'doesnt_end_with' => ':attribute tidak boleh diakhiri dengan salah satu dari berikut ini: :values.',
+ 'doesnt_start_with' => ':attribute tidak boleh diawali dengan salah satu dari berikut ini: :values.',
+ 'email' => ':attribute harus berupa alamat email yang valid.',
+ 'ends_with' => ':attribute harus diakhiri dengan salah satu dari berikut ini: :values.',
+ 'enum' => ':attribute yang dipilih tidak valid.',
+ 'exists' => ':attribute yang dipilih tidak valid.',
+ 'extensions' => ':attribute harus memiliki salah satu ekstensi berikut: :values.',
+ 'file' => ':attribute harus berupa berkas.',
+ 'filled' => ':attribute harus memiliki nilai.',
+ 'gt' => [
+ 'array' => ':attribute harus memiliki lebih dari :value item.',
+ 'file' => ':attribute harus berukuran lebih besar dari :value kilobita.',
+ 'numeric' => ':attribute harus bernilai lebih besar dari :value.',
+ 'string' => ':attribute harus berukuran lebih besar dari :value karakter.',
+ ],
+ 'gte' => [
+ 'array' => ':attribute harus memiliki :value item atau lebih.',
+ 'file' => ':attribute harus berukuran lebih besar dari atau sama dengan :value kilobita.',
+ 'numeric' => ':attribute harus bernilai lebih besar dari atau sama dengan :value.',
+ 'string' => ':attribute harus berukuran lebih besar dari atau sama dengan :value karakter.',
+ ],
+ 'hex_color' => ':attribute harus berupa warna heksadesimal yang valid.',
+ 'image' => ':attribute harus berupa gambar.',
+ 'in' => ':attribute yang dipilih tidak valid.',
+ 'in_array' => ':attribute tidak ada di :other.',
+ 'integer' => ':attribute harus berupa bilangan bulat.',
+ 'ip' => ':attribute harus berupa alamat IP yang valid.',
+ 'ipv4' => ':attribute harus berupa alamat IPv4 yang valid.',
+ 'ipv6' => ':attribute harus berupa alamat IPv6 yang valid.',
+ 'json' => ':attribute harus berupa string JSON yang valid.',
+ 'lowercase' => ':attribute harus berupa huruf kecil.',
+ 'lt' => [
+ 'array' => ':attribute harus memiliki kurang dari :value item.',
+ 'file' => ':attribute harus berukuran kurang dari :value kilobita.',
+ 'numeric' => ':attribute harus bernilai kurang dari :value.',
+ 'string' => ':attribute harus berukuran kurang dari :value karakter.',
+ ],
+ 'lte' => [
+ 'array' => ':attribute tidak boleh memiliki lebih dari :value item.',
+ 'file' => ':attribute harus berukuran kurang dari atau sama dengan :value kilobita.',
+ 'numeric' => ':attribute harus bernilai kurang dari atau sama dengan :value.',
+ 'string' => ':attribute harus berukuran kurang dari atau sama dengan :value karakter.',
+ ],
+ 'mac_address' => ':attribute harus berupa alamat MAC yang valid.',
+ 'max' => [
+ 'array' => ':attribute tidak boleh memiliki lebih dari :max item.',
+ 'file' => ':attribute tidak boleh berukuran lebih besar dari :max kilobita.',
+ 'numeric' => ':attribute tidak boleh bernilai lebih besar dari :max.',
+ 'string' => ':attribute tidak boleh berukuran lebih besar dari :max karakter.',
+ ],
+ 'max_digits' => ':attribute tidak boleh memiliki lebih dari :max angka.',
+ 'mimes' => ':attribute harus berupa berkas bertipe: :values.',
+ 'mimetypes' => ':attribute harus berupa berkas bertipe: :values.',
+ 'min' => [
+ 'array' => ':attribute harus memiliki setidaknya :min item.',
+ 'file' => ':attribute harus berukuran setidaknya :min kilobita.',
+ 'numeric' => ':attribute harus bernilai setidaknya :min.',
+ 'string' => ':attribute harus berukuran setidaknya :min karakter.',
+ ],
+ 'min_digits' => ':attribute harus memiliki setidaknya :min angka.',
+ 'missing' => ':attribute harus tidak ada.',
+ 'missing_if' => ':attribute harus tidak ada ketika :other adalah :value.',
+ 'missing_unless' => ':attribute harus tidak ada kecuali :other adalah :value.',
+ 'missing_with' => ':attribute harus tidak ada ketika :values ada.',
+ 'missing_with_all' => ':attribute harus tidak ada ketika :values ada.',
+ 'multiple_of' => ':attribute harus merupakan kelipatan dari :value.',
+ 'not_in' => ':attribute yang dipilih tidak valid.',
+ 'not_regex' => 'Format :attribute tidak valid.',
+ 'numeric' => ':attribute harus berupa angka.',
+ 'password' => [
+ 'letters' => ':attribute harus berisi setidaknya satu huruf.',
+ 'mixed' => ':attribute harus berisi setidaknya satu huruf besar dan satu huruf kecil.',
+ 'numbers' => ':attribute harus berisi setidaknya satu angka.',
+ 'symbols' => ':attribute harus berisi setidaknya satu simbol.',
+ 'uncompromised' => ':attribute yang diberikan telah muncul dalam kebocoran data. Silakan pilih :attribute yang berbeda.',
+ ],
+ 'present' => ':attribute harus ada.',
+ 'present_if' => ':attribute harus ada ketika :other adalah :value.',
+ 'present_unless' => ':attribute harus ada kecuali :other adalah :value.',
+ 'present_with' => ':attribute harus ada ketika :values ada.',
+ 'present_with_all' => ':attribute harus ada ketika :values ada.',
+ 'prohibited' => ':attribute dilarang.',
+ 'prohibited_if' => ':attribute dilarang ketika :other adalah :value.',
+ 'prohibited_unless' => ':attribute dilarang kecuali :other ada dalam :values.',
+ 'prohibits' => ':attribute melarang :other untuk ada.',
+ 'regex' => 'Format :attribute tidak valid.',
+ 'required' => ':attribute wajib diisi.',
+ 'required_array_keys' => ':attribute harus berisi entri untuk: :values.',
+ 'required_if' => ':attribute wajib diisi ketika :other adalah :value.',
+ 'required_if_accepted' => ':attribute wajib diisi ketika :other diterima.',
+ 'required_if_declined' => ':attribute wajib diisi ketika :other ditolak.',
+ 'required_unless' => ':attribute wajib diisi kecuali :other ada dalam :values.',
+ 'required_with' => ':attribute wajib diisi ketika :values ada.',
+ 'required_with_all' => ':attribute wajib diisi ketika :values ada.',
+ 'required_without' => ':attribute wajib diisi ketika :values tidak ada.',
+ 'required_without_all' => ':attribute wajib diisi ketika tidak ada :values yang ada.',
+ 'same' => ':attribute dan :other harus sama.',
+ 'size' => [
+ 'array' => ':attribute harus mengandung :size item.',
+ 'file' => ':attribute harus berukuran :size kilobita.',
+ 'numeric' => ':attribute harus berukuran :size.',
+ 'string' => ':attribute harus berukuran :size karakter.',
+ ],
+ 'starts_with' => ':attribute harus diawali dengan salah satu dari berikut ini: :values.',
+ 'string' => ':attribute harus berupa string.',
+ 'timezone' => ':attribute harus berupa zona waktu yang valid.',
+ 'unique' => ':attribute sudah ada sebelumnya.',
+ 'uploaded' => ':attribute gagal diunggah.',
+ 'uppercase' => ':attribute harus berupa huruf besar.',
+ 'url' => ':attribute harus berupa URL yang valid.',
+ 'uuid' => ':attribute harus berupa UUID yang valid.',
+
+ /*
+ |--------------------------------------------------------------------------
+ | Baris Bahasa Validasi Kustom
+ |--------------------------------------------------------------------------
+ |
+ | Di sini Anda dapat menentukan pesan validasi kustom untuk atribut menggunakan
+ | konvensi "attribute.rule" untuk menamai baris. Ini memudahkan dalam
+ | menentukan baris bahasa kustom tertentu untuk aturan atribut tertentu.
+ |
+ */
+
+ 'custom' => [
+ 'attribute-name' => [
+ 'rule-name' => 'custom-message',
+ ],
+ ],
+
+ /*
+ |--------------------------------------------------------------------------
+ | Atribut Validasi Kustom
+ |--------------------------------------------------------------------------
+ |
+ | Baris bahasa berikut digunakan untuk menukar placeholder atribut kami
+ | dengan sesuatu yang lebih ramah pembaca seperti "Alamat E-Mail" daripada
+ | "email". Ini membantu kami membuat pesan kami lebih ekspresif.
+ |
+ */
+
+ 'attributes' => [],
+
+];
diff --git a/lang/vendor/backup/en/notifications.php b/lang/vendor/backup/en/notifications.php
new file mode 100644
index 0000000..73811bd
--- /dev/null
+++ b/lang/vendor/backup/en/notifications.php
@@ -0,0 +1,45 @@
+ 'Exception message: :message',
+ 'exception_trace' => 'Exception trace: :trace',
+ 'exception_message_title' => 'Exception message',
+ 'exception_trace_title' => 'Exception trace',
+
+ 'backup_failed_subject' => 'Failed backup of :application_name',
+ 'backup_failed_body' => 'Important: An error occurred while backing up :application_name',
+
+ 'backup_successful_subject' => 'Successful new backup of :application_name',
+ 'backup_successful_subject_title' => 'Successful new backup!',
+ 'backup_successful_body' => 'Great news, a new backup of :application_name was successfully created on the disk named :disk_name.',
+
+ 'cleanup_failed_subject' => 'Cleaning up the backups of :application_name failed.',
+ 'cleanup_failed_body' => 'An error occurred while cleaning up the backups of :application_name',
+
+ 'cleanup_successful_subject' => 'Clean up of :application_name backups successful',
+ 'cleanup_successful_subject_title' => 'Clean up of backups successful!',
+ 'cleanup_successful_body' => 'The clean up of the :application_name backups on the disk named :disk_name was successful.',
+
+ 'healthy_backup_found_subject' => 'The backups for :application_name on disk :disk_name are healthy',
+ 'healthy_backup_found_subject_title' => 'The backups for :application_name are healthy',
+ 'healthy_backup_found_body' => 'The backups for :application_name are considered healthy. Good job!',
+
+ 'unhealthy_backup_found_subject' => 'Important: The backups for :application_name are unhealthy',
+ 'unhealthy_backup_found_subject_title' => 'Important: The backups for :application_name are unhealthy. :problem',
+ 'unhealthy_backup_found_body' => 'The backups for :application_name on disk :disk_name are unhealthy.',
+ 'unhealthy_backup_found_not_reachable' => 'The backup destination cannot be reached. :error',
+ 'unhealthy_backup_found_empty' => 'There are no backups of this application at all.',
+ 'unhealthy_backup_found_old' => 'The latest backup made on :date is considered too old.',
+ 'unhealthy_backup_found_unknown' => 'Sorry, an exact reason cannot be determined.',
+ 'unhealthy_backup_found_full' => 'The backups are using too much storage. Current usage is :disk_usage which is higher than the allowed limit of :disk_limit.',
+
+ 'no_backups_info' => 'No backups were made yet',
+ 'application_name' => 'Application name',
+ 'backup_name' => 'Backup name',
+ 'disk' => 'Disk',
+ 'newest_backup_size' => 'Newest backup size',
+ 'number_of_backups' => 'Number of backups',
+ 'total_storage_used' => 'Total storage used',
+ 'newest_backup_date' => 'Newest backup date',
+ 'oldest_backup_date' => 'Oldest backup date',
+];
diff --git a/lang/vendor/backup/id/notifications.php b/lang/vendor/backup/id/notifications.php
new file mode 100644
index 0000000..12364b5
--- /dev/null
+++ b/lang/vendor/backup/id/notifications.php
@@ -0,0 +1,45 @@
+ 'Pesan pengecualian: :message',
+ 'exception_trace' => 'Jejak pengecualian: :trace',
+ 'exception_message_title' => 'Pesan pengecualian',
+ 'exception_trace_title' => 'Jejak pengecualian',
+
+ 'backup_failed_subject' => 'Gagal backup :application_name',
+ 'backup_failed_body' => 'Penting: Sebuah error terjadi ketika membackup :application_name',
+
+ 'backup_successful_subject' => 'Backup baru sukses dari :application_name',
+ 'backup_successful_subject_title' => 'Backup baru sukses!',
+ 'backup_successful_body' => 'Kabar baik, sebuah backup baru dari :application_name sukses dibuat pada disk bernama :disk_name.',
+
+ 'cleanup_failed_subject' => 'Membersihkan backup dari :application_name yang gagal.',
+ 'cleanup_failed_body' => 'Sebuah error teradi ketika membersihkan backup dari :application_name',
+
+ 'cleanup_successful_subject' => 'Sukses membersihkan backup :application_name',
+ 'cleanup_successful_subject_title' => 'Sukses membersihkan backup!',
+ 'cleanup_successful_body' => 'Pembersihan backup :application_name pada disk bernama :disk_name telah sukses.',
+
+ 'healthy_backup_found_subject' => 'Backup untuk :application_name pada disk :disk_name sehat',
+ 'healthy_backup_found_subject_title' => 'Backup untuk :application_name sehat',
+ 'healthy_backup_found_body' => 'Backup untuk :application_name dipertimbangkan sehat. Kerja bagus!',
+
+ 'unhealthy_backup_found_subject' => 'Penting: Backup untuk :application_name tidak sehat',
+ 'unhealthy_backup_found_subject_title' => 'Penting: Backup untuk :application_name tidak sehat. :problem',
+ 'unhealthy_backup_found_body' => 'Backup untuk :application_name pada disk :disk_name tidak sehat.',
+ 'unhealthy_backup_found_not_reachable' => 'Tujuan backup tidak dapat terjangkau. :error',
+ 'unhealthy_backup_found_empty' => 'Tidak ada backup pada aplikasi ini sama sekali.',
+ 'unhealthy_backup_found_old' => 'Backup terakhir dibuat pada :date dimana dipertimbahkan sudah sangat lama.',
+ 'unhealthy_backup_found_unknown' => 'Maaf, sebuah alasan persisnya tidak dapat ditentukan.',
+ 'unhealthy_backup_found_full' => 'Backup menggunakan terlalu banyak kapasitas penyimpanan. Penggunaan terkini adalah :disk_usage dimana lebih besar dari batas yang diperbolehkan yaitu :disk_limit.',
+
+ 'no_backups_info' => 'Belum ada backup yang dibuat',
+ 'application_name' => 'Nama aplikasi',
+ 'backup_name' => 'Nama cadangan',
+ 'disk' => 'Disk',
+ 'newest_backup_size' => 'Ukuran cadangan terbaru',
+ 'number_of_backups' => 'Jumlah cadangan',
+ 'total_storage_used' => 'Total penyimpanan yang digunakan',
+ 'newest_backup_date' => 'Ukuran cadangan terbaru',
+ 'oldest_backup_date' => 'Ukuran cadangan tertua',
+];
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..84952e1
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,4011 @@
+{
+ "name": "laravel-base",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "dependencies": {
+ "datatables.net-bs5": "^2.3.7",
+ "datatables.net-dt": "^2.3.7",
+ "datatables.net-fixedcolumns": "^5.0.5",
+ "filepond": "^4.32.12",
+ "jquery": "^4.0.0",
+ "laravel-datatables-vite": "^0.6.2",
+ "quill": "^2.0.3",
+ "select2": "^4.1.0-rc.0",
+ "select2-bootstrap-5-theme": "^1.3.0",
+ "vite-plugin-node-polyfills": "^0.26.0",
+ "ziggy-js": "^2.6.2"
+ },
+ "devDependencies": {
+ "@popperjs/core": "^2.11.8",
+ "alpinejs": "^3.4.2",
+ "alpinejs-axios": "^1.0.12",
+ "apexcharts": "^5.10.5",
+ "autoprefixer": "^10.4.27",
+ "axios": "1.15.2",
+ "concurrently": "^9.0.1",
+ "laravel-vite-plugin": "^3.0.0",
+ "postcss": "8.5.10",
+ "sass": "^1.99.0",
+ "sweetalert2": "^11.26.24",
+ "vite": "^8.0.7"
+ }
+ },
+ "node_modules/@emnapi/core": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
+ "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/wasi-threads": "1.2.0",
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz",
+ "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@emnapi/wasi-threads": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz",
+ "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "license": "MIT"
+ },
+ "node_modules/@napi-rs/wasm-runtime": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz",
+ "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@tybys/wasm-util": "^0.10.1"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Brooooooklyn"
+ },
+ "peerDependencies": {
+ "@emnapi/core": "^1.7.1",
+ "@emnapi/runtime": "^1.7.1"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.123.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.123.0.tgz",
+ "integrity": "sha512-YtECP/y8Mj1lSHiUWGSRzy/C6teUKlS87dEfuVKT09LgQbUsBW1rNg+MiJ4buGu3yuADV60gbIvo9/HplA56Ew==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@parcel/watcher": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz",
+ "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "detect-libc": "^2.0.3",
+ "is-glob": "^4.0.3",
+ "node-addon-api": "^7.0.0",
+ "picomatch": "^4.0.3"
+ },
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "@parcel/watcher-android-arm64": "2.5.6",
+ "@parcel/watcher-darwin-arm64": "2.5.6",
+ "@parcel/watcher-darwin-x64": "2.5.6",
+ "@parcel/watcher-freebsd-x64": "2.5.6",
+ "@parcel/watcher-linux-arm-glibc": "2.5.6",
+ "@parcel/watcher-linux-arm-musl": "2.5.6",
+ "@parcel/watcher-linux-arm64-glibc": "2.5.6",
+ "@parcel/watcher-linux-arm64-musl": "2.5.6",
+ "@parcel/watcher-linux-x64-glibc": "2.5.6",
+ "@parcel/watcher-linux-x64-musl": "2.5.6",
+ "@parcel/watcher-win32-arm64": "2.5.6",
+ "@parcel/watcher-win32-ia32": "2.5.6",
+ "@parcel/watcher-win32-x64": "2.5.6"
+ }
+ },
+ "node_modules/@parcel/watcher-android-arm64": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz",
+ "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-arm64": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz",
+ "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-darwin-x64": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz",
+ "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-freebsd-x64": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz",
+ "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-glibc": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz",
+ "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==",
+ "cpu": [
+ "arm"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm-musl": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz",
+ "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==",
+ "cpu": [
+ "arm"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-glibc": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz",
+ "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-arm64-musl": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz",
+ "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-x64-glibc": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz",
+ "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-linux-x64-musl": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz",
+ "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-arm64": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz",
+ "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-ia32": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz",
+ "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==",
+ "cpu": [
+ "ia32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@parcel/watcher-win32-x64": {
+ "version": "2.5.6",
+ "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz",
+ "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 10.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/@popperjs/core": {
+ "version": "2.11.8",
+ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
+ "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.13.tgz",
+ "integrity": "sha512-5ZiiecKH2DXAVJTNN13gNMUcCDg4Jy8ZjbXEsPnqa248wgOVeYRX0iqXXD5Jz4bI9BFHgKsI2qmyJynstbmr+g==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.13.tgz",
+ "integrity": "sha512-tz/v/8G77seu8zAB3A5sK3UFoOl06zcshEzhUO62sAEtrEuW/H1CcyoupOrD+NbQJytYgA4CppXPzlrmp4JZKA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.13.tgz",
+ "integrity": "sha512-8DakphqOz8JrMYWTJmWA+vDJxut6LijZ8Xcdc4flOlAhU7PNVwo2MaWBF9iXjJAPo5rC/IxEFZDhJ3GC7NHvug==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.13.tgz",
+ "integrity": "sha512-4wBQFfjDuXYN/SVI8inBF3Aa+isq40rc6VMFbk5jcpolUBTe5cYnMsHZ51nFWsx3PVyyNN3vgoESki0Hmr/4BA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.13.tgz",
+ "integrity": "sha512-JW/e4yPIXLms+jmnbwwy5LA/LxVwZUWLN8xug+V200wzaVi5TEGIWQlh8o91gWYFxW609euI98OCCemmWGuPrw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.13.tgz",
+ "integrity": "sha512-ZfKWpXiUymDnavepCaM6KG/uGydJ4l2nBmMxg60Ci4CbeefpqjPWpfaZM7PThOhk2dssqBAcwLc6rAyr0uTdXg==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.13.tgz",
+ "integrity": "sha512-bmRg3O6Z0gq9yodKKWCIpnlH051sEfdVwt+6m5UDffAQMUUqU0xjnQqqAUm+Gu7ofAAly9DqiQDtKu2nPDEABA==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.13.tgz",
+ "integrity": "sha512-8Wtnbw4k7pMYN9B/mOEAsQ8HOiq7AZ31Ig4M9BKn2So4xRaFEhtCSa4ZJaOutOWq50zpgR4N5+L/opnlaCx8wQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.13.tgz",
+ "integrity": "sha512-D/0Nlo8mQuxSMohNJUF2lDXWRsFDsHldfRRgD9bRgktj+EndGPj4DOV37LqDKPYS+osdyhZEH7fTakTAEcW7qg==",
+ "cpu": [
+ "s390x"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.13.tgz",
+ "integrity": "sha512-eRrPvat2YaVQcwwKi/JzOP6MKf1WRnOCr+VaI3cTWz3ZoLcP/654z90lVCJ4dAuMEpPdke0n+qyAqXDZdIC4rA==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.13.tgz",
+ "integrity": "sha512-PsdONiFRp8hR8KgVjTWjZ9s7uA3uueWL0t74/cKHfM4dR5zXYv4AjB8BvA+QDToqxAFg4ZkcVEqeu5F7inoz5w==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.13.tgz",
+ "integrity": "sha512-hCNXgC5dI3TVOLrPT++PKFNZ+1EtS0mLQwfXXXSUD/+rGlB65gZDwN/IDuxLpQP4x8RYYHqGomlUXzpO8aVI2w==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-wasm32-wasi": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.13.tgz",
+ "integrity": "sha512-viLS5C5et8NFtLWw9Sw3M/w4vvnVkbWkO7wSNh3C+7G1+uCkGpr6PcjNDSFcNtmXY/4trjPBqUfcOL+P3sWy/g==",
+ "cpu": [
+ "wasm32"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/core": "1.9.1",
+ "@emnapi/runtime": "1.9.1",
+ "@napi-rs/wasm-runtime": "^1.1.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.13.tgz",
+ "integrity": "sha512-Fqa3Tlt1xL4wzmAYxGNFV36Hb+VfPc9PYU+E25DAnswXv3ODDu/yyWjQDbXMo5AGWkQVjLgQExuVu8I/UaZhPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.13.tgz",
+ "integrity": "sha512-/pLI5kPkGEi44TDlnbio3St/5gUFeN51YWNAk/Gnv6mEQBOahRBh52qVFVBpmrnU01n2yysvBML9Ynu7K4kGAQ==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.13.tgz",
+ "integrity": "sha512-3ngTAv6F/Py35BsYbeeLeecvhMKdsKm4AoOETVhAA+Qc8nrA2I0kF7oa93mE9qnIurngOSpMnQ0x2nQY2FPviA==",
+ "license": "MIT"
+ },
+ "node_modules/@rollup/plugin-inject": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/@rollup/plugin-inject/-/plugin-inject-5.0.5.tgz",
+ "integrity": "sha512-2+DEJbNBoPROPkgTDNe8/1YXWcqxbN5DTjASVIOx8HS+pITXushyNiBV56RB08zuptzz8gT3YfkqriTBVycepg==",
+ "license": "MIT",
+ "dependencies": {
+ "@rollup/pluginutils": "^5.0.1",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.3"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rollup/pluginutils": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz",
+ "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "^1.0.0",
+ "estree-walker": "^2.0.2",
+ "picomatch": "^4.0.2"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "peerDependencies": {
+ "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0"
+ },
+ "peerDependenciesMeta": {
+ "rollup": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@tybys/wasm-util": {
+ "version": "0.10.1",
+ "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
+ "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
+ "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.1.5.tgz",
+ "integrity": "sha512-1tdfLmNjWG6t/CsPldh+foumYFo3cpyCHgBYQ34ylaMsJ+SNHQ1kApMIa8jN+i593zQuaw3AdWH0nJTARzCFhg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.1.5"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.1.5.tgz",
+ "integrity": "sha512-oJ4F3TnvpXaQwZJNF3ZK+kLPHKarDmJjJ6jyzVNDKH9md1dptjC7lWR//jrGuLdek/U6iltWxqAnYOu8gCiOvA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/alpinejs": {
+ "version": "3.15.11",
+ "resolved": "https://registry.npmjs.org/alpinejs/-/alpinejs-3.15.11.tgz",
+ "integrity": "sha512-m26gkTg/MId8O+F4jHKK3vB3SjbFxxk/JHP+qzmw1H6aQrZuPAg4CUoAefnASzzp/eNroBjrRQe7950bNeaBJw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "~3.1.1"
+ }
+ },
+ "node_modules/alpinejs-axios": {
+ "version": "1.0.12",
+ "resolved": "https://registry.npmjs.org/alpinejs-axios/-/alpinejs-axios-1.0.12.tgz",
+ "integrity": "sha512-55qWJvkc/lIYzcoHTwgsdeghc+u4uQ01/SRwmr83XO6dYFGipCkEV+/EmesN19cMvD0iDkgIm9mfR/xLVRYtDg==",
+ "dev": true
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/apexcharts": {
+ "version": "5.10.5",
+ "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-5.10.5.tgz",
+ "integrity": "sha512-RirosfLQLqYpWBdn4Pdv9B1M0M2FepzVxPRpcuXQPTilvuZvKt02vgVlEexhCVu2p4fApDIV/3yC9voAIK+qjw==",
+ "dev": true,
+ "license": "SEE LICENSE IN LICENSE"
+ },
+ "node_modules/asn1.js": {
+ "version": "4.10.1",
+ "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
+ "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
+ "license": "MIT",
+ "dependencies": {
+ "bn.js": "^4.0.0",
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
+ "node_modules/asn1.js/node_modules/bn.js": {
+ "version": "4.12.3",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
+ "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
+ "license": "MIT"
+ },
+ "node_modules/assert": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/assert/-/assert-2.1.0.tgz",
+ "integrity": "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "is-nan": "^1.3.2",
+ "object-is": "^1.1.5",
+ "object.assign": "^4.1.4",
+ "util": "^0.12.5"
+ }
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/autoprefixer": {
+ "version": "10.4.27",
+ "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz",
+ "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/autoprefixer"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "browserslist": "^4.28.1",
+ "caniuse-lite": "^1.0.30001774",
+ "fraction.js": "^5.3.4",
+ "picocolors": "^1.1.1",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "bin": {
+ "autoprefixer": "bin/autoprefixer"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ },
+ "peerDependencies": {
+ "postcss": "^8.1.0"
+ }
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/axios": {
+ "version": "1.15.2",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz",
+ "integrity": "sha512-wLrXxPtcrPTsNlJmKjkPnNPK2Ihe0hn0wGSaTEiHRPxwjvJwT3hKmXF4dpqxmPO9SoNb2FsYXj/xEo0gHN+D5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.11",
+ "form-data": "^4.0.5",
+ "proxy-from-env": "^2.1.0"
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/baseline-browser-mapping": {
+ "version": "2.10.16",
+ "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz",
+ "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "baseline-browser-mapping": "dist/cli.cjs"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/bn.js": {
+ "version": "5.2.3",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.3.tgz",
+ "integrity": "sha512-EAcmnPkxpntVL+DS7bO1zhcZNvCkxqtkd0ZY53h06GNQ3DEkkGZ/gKgmDv6DdZQGj9BgfSPKtJJ7Dp1GPP8f7w==",
+ "license": "MIT"
+ },
+ "node_modules/bootstrap": {
+ "version": "5.3.8",
+ "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz",
+ "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/twbs"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/bootstrap"
+ }
+ ],
+ "license": "MIT",
+ "peerDependencies": {
+ "@popperjs/core": "^2.11.8"
+ }
+ },
+ "node_modules/bootstrap-icons": {
+ "version": "1.13.1",
+ "resolved": "https://registry.npmjs.org/bootstrap-icons/-/bootstrap-icons-1.13.1.tgz",
+ "integrity": "sha512-ijombt4v6bv5CLeXvRWKy7CuM3TRTuPEuGaGKvTV5cz65rQSY8RQ2JcHt6b90cBBAC7s8fsf2EkQDldzCoXUjw==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/twbs"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/bootstrap"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/brorand": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
+ "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==",
+ "license": "MIT"
+ },
+ "node_modules/browser-resolve": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-2.0.0.tgz",
+ "integrity": "sha512-7sWsQlYL2rGLy2IWm8WL8DCTJvYLc/qlOnsakDac87SOoCd16WLsaAMdCiAqsTNHIe+SXfaqyxyo6THoWqs8WQ==",
+ "license": "MIT",
+ "dependencies": {
+ "resolve": "^1.17.0"
+ }
+ },
+ "node_modules/browserify-aes": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
+ "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
+ "license": "MIT",
+ "dependencies": {
+ "buffer-xor": "^1.0.3",
+ "cipher-base": "^1.0.0",
+ "create-hash": "^1.1.0",
+ "evp_bytestokey": "^1.0.3",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.0.1"
+ }
+ },
+ "node_modules/browserify-cipher": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz",
+ "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==",
+ "license": "MIT",
+ "dependencies": {
+ "browserify-aes": "^1.0.4",
+ "browserify-des": "^1.0.0",
+ "evp_bytestokey": "^1.0.0"
+ }
+ },
+ "node_modules/browserify-des": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz",
+ "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==",
+ "license": "MIT",
+ "dependencies": {
+ "cipher-base": "^1.0.1",
+ "des.js": "^1.0.0",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "node_modules/browserify-rsa": {
+ "version": "4.1.1",
+ "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz",
+ "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==",
+ "license": "MIT",
+ "dependencies": {
+ "bn.js": "^5.2.1",
+ "randombytes": "^2.1.0",
+ "safe-buffer": "^5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/browserify-sign": {
+ "version": "4.2.5",
+ "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.5.tgz",
+ "integrity": "sha512-C2AUdAJg6rlM2W5QMp2Q4KGQMVBwR1lIimTsUnutJ8bMpW5B52pGpR2gEnNBNwijumDo5FojQ0L9JrXA8m4YEw==",
+ "license": "ISC",
+ "dependencies": {
+ "bn.js": "^5.2.2",
+ "browserify-rsa": "^4.1.1",
+ "create-hash": "^1.2.0",
+ "create-hmac": "^1.1.7",
+ "elliptic": "^6.6.1",
+ "inherits": "^2.0.4",
+ "parse-asn1": "^5.1.9",
+ "readable-stream": "^2.3.8",
+ "safe-buffer": "^5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/browserify-sign/node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "license": "MIT"
+ },
+ "node_modules/browserify-sign/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/browserify-sign/node_modules/readable-stream/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/browserify-sign/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/browserify-sign/node_modules/string_decoder/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/browserify-zlib": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz",
+ "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==",
+ "license": "MIT",
+ "dependencies": {
+ "pako": "~1.0.5"
+ }
+ },
+ "node_modules/browserslist": {
+ "version": "4.28.2",
+ "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz",
+ "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "baseline-browser-mapping": "^2.10.12",
+ "caniuse-lite": "^1.0.30001782",
+ "electron-to-chromium": "^1.5.328",
+ "node-releases": "^2.0.36",
+ "update-browserslist-db": "^1.2.3"
+ },
+ "bin": {
+ "browserslist": "cli.js"
+ },
+ "engines": {
+ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "5.7.1",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz",
+ "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.1.13"
+ }
+ },
+ "node_modules/buffer-xor": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz",
+ "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==",
+ "license": "MIT"
+ },
+ "node_modules/builtin-status-codes": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz",
+ "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==",
+ "license": "MIT"
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz",
+ "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.0",
+ "es-define-property": "^1.0.0",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/call-bind-apply-helpers": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+ "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/call-bound": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "get-intrinsic": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/caniuse-lite": {
+ "version": "1.0.30001787",
+ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001787.tgz",
+ "integrity": "sha512-mNcrMN9KeI68u7muanUpEejSLghOKlVhRqS/Za2IeyGllJ9I9otGpR9g3nsw7n4W378TE/LyIteA0+/FOZm4Kg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/caniuse-lite"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "CC-BY-4.0"
+ },
+ "node_modules/chalk": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
+ "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.1.0",
+ "supports-color": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/chalk/node_modules/supports-color": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
+ "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/chokidar": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
+ "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "readdirp": "^4.0.1"
+ },
+ "engines": {
+ "node": ">= 14.16.0"
+ },
+ "funding": {
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/cipher-base": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.7.tgz",
+ "integrity": "sha512-Mz9QMT5fJe7bKI7MH31UilT5cEK5EHHRCccw/YRFsRY47AuNgaV6HY3rscp0/I4Q+tTW/5zoqpSeRRI54TkDWA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.4",
+ "safe-buffer": "^5.2.1",
+ "to-buffer": "^1.2.2"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/concurrently": {
+ "version": "9.2.1",
+ "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-9.2.1.tgz",
+ "integrity": "sha512-fsfrO0MxV64Znoy8/l1vVIjjHa29SZyyqPgQBwhiDcaW8wJc2W3XWVOGx4M3oJBnv/zdUZIIp1gDeS98GzP8Ng==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "chalk": "4.1.2",
+ "rxjs": "7.8.2",
+ "shell-quote": "1.8.3",
+ "supports-color": "8.1.1",
+ "tree-kill": "1.2.2",
+ "yargs": "17.7.2"
+ },
+ "bin": {
+ "conc": "dist/bin/concurrently.js",
+ "concurrently": "dist/bin/concurrently.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/open-cli-tools/concurrently?sponsor=1"
+ }
+ },
+ "node_modules/console-browserify": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz",
+ "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA=="
+ },
+ "node_modules/constants-browserify": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz",
+ "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==",
+ "license": "MIT"
+ },
+ "node_modules/core-util-is": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
+ "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
+ "license": "MIT"
+ },
+ "node_modules/create-ecdh": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz",
+ "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==",
+ "license": "MIT",
+ "dependencies": {
+ "bn.js": "^4.1.0",
+ "elliptic": "^6.5.3"
+ }
+ },
+ "node_modules/create-ecdh/node_modules/bn.js": {
+ "version": "4.12.3",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
+ "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
+ "license": "MIT"
+ },
+ "node_modules/create-hash": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
+ "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
+ "license": "MIT",
+ "dependencies": {
+ "cipher-base": "^1.0.1",
+ "inherits": "^2.0.1",
+ "md5.js": "^1.3.4",
+ "ripemd160": "^2.0.1",
+ "sha.js": "^2.4.0"
+ }
+ },
+ "node_modules/create-hmac": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
+ "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
+ "license": "MIT",
+ "dependencies": {
+ "cipher-base": "^1.0.3",
+ "create-hash": "^1.1.0",
+ "inherits": "^2.0.1",
+ "ripemd160": "^2.0.0",
+ "safe-buffer": "^5.0.1",
+ "sha.js": "^2.4.8"
+ }
+ },
+ "node_modules/create-require": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz",
+ "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==",
+ "license": "MIT"
+ },
+ "node_modules/crypto-browserify": {
+ "version": "3.12.1",
+ "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz",
+ "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==",
+ "license": "MIT",
+ "dependencies": {
+ "browserify-cipher": "^1.0.1",
+ "browserify-sign": "^4.2.3",
+ "create-ecdh": "^4.0.4",
+ "create-hash": "^1.2.0",
+ "create-hmac": "^1.1.7",
+ "diffie-hellman": "^5.0.3",
+ "hash-base": "~3.0.4",
+ "inherits": "^2.0.4",
+ "pbkdf2": "^3.1.2",
+ "public-encrypt": "^4.0.3",
+ "randombytes": "^2.1.0",
+ "randomfill": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/datatables.net": {
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-2.3.7.tgz",
+ "integrity": "sha512-AvsjG/Nkp6OxeyBKYZauemuzQCPogE1kOtKwG4sYjvdqGCSLiGaJagQwXv4YxG+ts5vaJr6qKGG9ec3g6vTo3w==",
+ "license": "MIT",
+ "dependencies": {
+ "jquery": ">=1.7"
+ }
+ },
+ "node_modules/datatables.net-bs5": {
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/datatables.net-bs5/-/datatables.net-bs5-2.3.7.tgz",
+ "integrity": "sha512-RiCEMpMXDBeMDwjSrMpmcXDU6mibRMuOn7Wk7k3SlOfLEY3FQHO7S2m+K7teXYeaNlCLyjJMU+6BUUwlBCpLFw==",
+ "license": "MIT",
+ "dependencies": {
+ "datatables.net": "2.3.7",
+ "jquery": ">=1.7"
+ }
+ },
+ "node_modules/datatables.net-buttons": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/datatables.net-buttons/-/datatables.net-buttons-3.2.6.tgz",
+ "integrity": "sha512-rLqkB3xLIAYwVLt+lUSxybo/1WqveTAxhQm6wj6yvXlJiWq+oJ8MKW6H1q90QrXbNp0fGngnfD0cmpMZnNnnNw==",
+ "license": "MIT",
+ "dependencies": {
+ "datatables.net": "^2",
+ "jquery": ">=1.7"
+ }
+ },
+ "node_modules/datatables.net-buttons-bs5": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/datatables.net-buttons-bs5/-/datatables.net-buttons-bs5-3.2.6.tgz",
+ "integrity": "sha512-RJfbaxnAys0OtcZcJL58/3aMVVKs2yQDBI8PNA0h/4mdKaJ/dVezZTFy5CYLrO1HjAGosfL0iv4sIs/BafaW7w==",
+ "license": "MIT",
+ "dependencies": {
+ "datatables.net-bs5": "^2",
+ "datatables.net-buttons": "3.2.6",
+ "jquery": ">=1.7"
+ }
+ },
+ "node_modules/datatables.net-dt": {
+ "version": "2.3.7",
+ "resolved": "https://registry.npmjs.org/datatables.net-dt/-/datatables.net-dt-2.3.7.tgz",
+ "integrity": "sha512-OXXIliY5MXnI+284Gt73F+fEdnW2u5y9jiptlvjDDb3YlyqXU4E/YZUB262a068sM/+qakb6RixN1SWn18uF2g==",
+ "license": "MIT",
+ "dependencies": {
+ "datatables.net": "2.3.7",
+ "jquery": ">=1.7"
+ }
+ },
+ "node_modules/datatables.net-fixedcolumns": {
+ "version": "5.0.5",
+ "resolved": "https://registry.npmjs.org/datatables.net-fixedcolumns/-/datatables.net-fixedcolumns-5.0.5.tgz",
+ "integrity": "sha512-/oCQzRCT6+sM+5zlvAEj1WamVHHtSOLOi8gruTpppj5ewXDRbst1VbJZaS1sKGS2XXZOK+uCWHkh8H3kdZBzWw==",
+ "license": "MIT",
+ "dependencies": {
+ "datatables.net": "^2",
+ "jquery": ">=1.7"
+ }
+ },
+ "node_modules/datatables.net-select": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/datatables.net-select/-/datatables.net-select-2.1.0.tgz",
+ "integrity": "sha512-6B1sI5pfvBen+8kySl1T0hkGoUmrw+5YGqaW4NCg+1srMw+48YV5SdWPiyCQDoev2ajBdKzHuG/wzD2INMbmJw==",
+ "license": "MIT",
+ "dependencies": {
+ "datatables.net": "^2",
+ "jquery": ">=1.7"
+ }
+ },
+ "node_modules/datatables.net-select-bs5": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/datatables.net-select-bs5/-/datatables.net-select-bs5-2.1.0.tgz",
+ "integrity": "sha512-N6VFo3DHsMCzM1Ef2pbUh1+pUd/MSHk48Z+X0QvbeknShWKdx0PS7FhLkaq8OalhHiEH0lyCy5L+1iR4bgAu9A==",
+ "license": "MIT",
+ "dependencies": {
+ "datatables.net-bs5": "^2",
+ "datatables.net-select": "2.1.0",
+ "jquery": ">=1.7"
+ }
+ },
+ "node_modules/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/des.js": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz",
+ "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.1",
+ "minimalistic-assert": "^1.0.0"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/diffie-hellman": {
+ "version": "5.0.3",
+ "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
+ "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
+ "license": "MIT",
+ "dependencies": {
+ "bn.js": "^4.1.0",
+ "miller-rabin": "^4.0.0",
+ "randombytes": "^2.0.0"
+ }
+ },
+ "node_modules/diffie-hellman/node_modules/bn.js": {
+ "version": "4.12.3",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
+ "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
+ "license": "MIT"
+ },
+ "node_modules/domain-browser": {
+ "version": "4.22.0",
+ "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-4.22.0.tgz",
+ "integrity": "sha512-IGBwjF7tNk3cwypFNH/7bfzBcgSCbaMOD3GsaY1AU/JRrnHnYgEM0+9kQt52iZxjNsjBtJYtao146V+f8jFZNw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://bevry.me/fund"
+ }
+ },
+ "node_modules/dunder-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+ "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.2.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/electron-to-chromium": {
+ "version": "1.5.333",
+ "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.333.tgz",
+ "integrity": "sha512-skNh4FsE+IpCJV7xAQGbQ4eyOGvcEctVBAk7a5KPzxC3alES9rLrT+2IsPRPgeQr8LVxdJr8BHQ9481+TOr0xg==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/elliptic": {
+ "version": "6.6.1",
+ "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz",
+ "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==",
+ "license": "MIT",
+ "dependencies": {
+ "bn.js": "^4.11.9",
+ "brorand": "^1.1.0",
+ "hash.js": "^1.0.0",
+ "hmac-drbg": "^1.0.1",
+ "inherits": "^2.0.4",
+ "minimalistic-assert": "^1.0.1",
+ "minimalistic-crypto-utils": "^1.0.1"
+ }
+ },
+ "node_modules/elliptic/node_modules/bn.js": {
+ "version": "4.12.3",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
+ "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
+ "license": "MIT"
+ },
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+ "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-errors": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+ "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-object-atoms": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+ "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+ "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.6",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
+ },
+ "node_modules/eventemitter3": {
+ "version": "5.0.4",
+ "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz",
+ "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==",
+ "license": "MIT"
+ },
+ "node_modules/events": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
+ "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.8.x"
+ }
+ },
+ "node_modules/evp_bytestokey": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz",
+ "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==",
+ "license": "MIT",
+ "dependencies": {
+ "md5.js": "^1.3.4",
+ "safe-buffer": "^5.1.1"
+ }
+ },
+ "node_modules/fast-diff": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz",
+ "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/filepond": {
+ "version": "4.32.12",
+ "resolved": "https://registry.npmjs.org/filepond/-/filepond-4.32.12.tgz",
+ "integrity": "sha512-wro0/deLwua9CkL7HJygOlDaPo1c7Ov8kfzbZjH7m4hdi56gdHGDeRWnYa+qXnRm3Lty69gUGVFb3JZy/9vT8g==",
+ "license": "MIT"
+ },
+ "node_modules/find-up": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
+ "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
+ "license": "MIT",
+ "dependencies": {
+ "locate-path": "^6.0.0",
+ "path-exists": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.11",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
+ "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/for-each": {
+ "version": "0.3.5",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz",
+ "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==",
+ "license": "MIT",
+ "dependencies": {
+ "is-callable": "^1.2.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+ "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "es-set-tostringtag": "^2.1.0",
+ "hasown": "^2.0.2",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/function-bind": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+ "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/generator-function": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz",
+ "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+ "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind-apply-helpers": "^1.0.2",
+ "es-define-property": "^1.0.1",
+ "es-errors": "^1.3.0",
+ "es-object-atoms": "^1.1.1",
+ "function-bind": "^1.1.2",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-symbols": "^1.1.0",
+ "hasown": "^2.0.2",
+ "math-intrinsics": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-proto": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+ "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+ "license": "MIT",
+ "dependencies": {
+ "dunder-proto": "^1.0.1",
+ "es-object-atoms": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/gopd": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+ "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-flag": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
+ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "license": "MIT",
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+ "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-tostringtag": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+ "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+ "license": "MIT",
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/hash-base": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz",
+ "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.4",
+ "safe-buffer": "^5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/hash.js": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
+ "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "minimalistic-assert": "^1.0.1"
+ }
+ },
+ "node_modules/hasown": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+ "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+ "license": "MIT",
+ "dependencies": {
+ "function-bind": "^1.1.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/hmac-drbg": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
+ "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==",
+ "license": "MIT",
+ "dependencies": {
+ "hash.js": "^1.0.3",
+ "minimalistic-assert": "^1.0.0",
+ "minimalistic-crypto-utils": "^1.0.1"
+ }
+ },
+ "node_modules/https-browserify": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz",
+ "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==",
+ "license": "MIT"
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/immutable": {
+ "version": "5.1.5",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz",
+ "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==",
+ "devOptional": true,
+ "license": "MIT"
+ },
+ "node_modules/inherits": {
+ "version": "2.0.4",
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
+ "license": "ISC"
+ },
+ "node_modules/is-arguments": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz",
+ "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.16.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
+ "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==",
+ "license": "MIT",
+ "dependencies": {
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-generator-function": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz",
+ "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.4",
+ "generator-function": "^2.0.0",
+ "get-proto": "^1.0.1",
+ "has-tostringtag": "^1.0.2",
+ "safe-regex-test": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-glob": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
+ "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-nan": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/is-nan/-/is-nan-1.3.2.tgz",
+ "integrity": "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.0",
+ "define-properties": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-regex": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz",
+ "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-typed-array": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz",
+ "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==",
+ "license": "MIT",
+ "dependencies": {
+ "which-typed-array": "^1.1.16"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "license": "MIT"
+ },
+ "node_modules/isomorphic-timers-promises": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/isomorphic-timers-promises/-/isomorphic-timers-promises-1.0.1.tgz",
+ "integrity": "sha512-u4sej9B1LPSxTGKB/HiuzvEQnXH0ECYkSVQU39koSwmFAxhlEAFl9RdTvLv4TOTQUgBS5O3O5fwUxk6byBZ+IQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/jquery": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/jquery/-/jquery-4.0.0.tgz",
+ "integrity": "sha512-TXCHVR3Lb6TZdtw1l3RTLf8RBWVGexdxL6AC8/e0xZKEpBflBsjh9/8LXw+dkNFuOyW9B7iB3O1sP7hS0Kiacg==",
+ "license": "MIT"
+ },
+ "node_modules/laravel-datatables-vite": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/laravel-datatables-vite/-/laravel-datatables-vite-0.6.2.tgz",
+ "integrity": "sha512-bJS0vPSa+3kQJhYZONOOxBzT3EWpDdKCKIy6vJd4cednLTisspTSCL72/tbvIseLgowUOy7dJODuLOI4Q3MaGA==",
+ "license": "MIT",
+ "dependencies": {
+ "bootstrap": "^5.2.2",
+ "bootstrap-icons": "^1.9.1",
+ "datatables.net": "^2.1.8",
+ "datatables.net-bs5": "^2.1.8",
+ "datatables.net-buttons-bs5": "^3.1.2",
+ "datatables.net-select-bs5": "^2.1.0",
+ "jquery": "^3.6.1"
+ }
+ },
+ "node_modules/laravel-datatables-vite/node_modules/jquery": {
+ "version": "3.7.1",
+ "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz",
+ "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
+ "license": "MIT"
+ },
+ "node_modules/laravel-vite-plugin": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-3.0.1.tgz",
+ "integrity": "sha512-Bx8sVcLIaZT1d0eisABcmjQ1GZdJpaXcV66A8RhXGp9JgR3iL8jDnvakVDXuH87Tn5S9KNx3VOhmJZW1CSexOg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picocolors": "^1.0.0",
+ "tinyglobby": "^0.2.12",
+ "vite-plugin-full-reload": "^1.1.0"
+ },
+ "bin": {
+ "clean-orphaned-assets": "bin/clean.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "vite": "^8.0.0"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
+ "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.32.0",
+ "lightningcss-darwin-arm64": "1.32.0",
+ "lightningcss-darwin-x64": "1.32.0",
+ "lightningcss-freebsd-x64": "1.32.0",
+ "lightningcss-linux-arm-gnueabihf": "1.32.0",
+ "lightningcss-linux-arm64-gnu": "1.32.0",
+ "lightningcss-linux-arm64-musl": "1.32.0",
+ "lightningcss-linux-x64-gnu": "1.32.0",
+ "lightningcss-linux-x64-musl": "1.32.0",
+ "lightningcss-win32-arm64-msvc": "1.32.0",
+ "lightningcss-win32-x64-msvc": "1.32.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
+ "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
+ "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
+ "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
+ "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
+ "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
+ "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
+ "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
+ "cpu": [
+ "arm64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
+ "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
+ "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
+ "cpu": [
+ "x64"
+ ],
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
+ "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.32.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
+ "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/locate-path": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
+ "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
+ "license": "MIT",
+ "dependencies": {
+ "p-locate": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/lodash-es": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
+ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.clonedeep": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz",
+ "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==",
+ "license": "MIT"
+ },
+ "node_modules/lodash.isequal": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz",
+ "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==",
+ "deprecated": "This package is deprecated. Use require('node:util').isDeepStrictEqual instead.",
+ "license": "MIT"
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.21",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz",
+ "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.5"
+ }
+ },
+ "node_modules/math-intrinsics": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+ "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/md5.js": {
+ "version": "1.3.5",
+ "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
+ "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==",
+ "license": "MIT",
+ "dependencies": {
+ "hash-base": "^3.0.0",
+ "inherits": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "node_modules/miller-rabin": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz",
+ "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==",
+ "license": "MIT",
+ "dependencies": {
+ "bn.js": "^4.0.0",
+ "brorand": "^1.0.1"
+ },
+ "bin": {
+ "miller-rabin": "bin/miller-rabin"
+ }
+ },
+ "node_modules/miller-rabin/node_modules/bn.js": {
+ "version": "4.12.3",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
+ "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
+ "license": "MIT"
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/minimalistic-assert": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
+ "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
+ "license": "ISC"
+ },
+ "node_modules/minimalistic-crypto-utils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
+ "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.11",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
+ "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/node-addon-api": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
+ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/node-releases": {
+ "version": "2.0.37",
+ "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz",
+ "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/node-stdlib-browser": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/node-stdlib-browser/-/node-stdlib-browser-1.3.1.tgz",
+ "integrity": "sha512-X75ZN8DCLftGM5iKwoYLA3rjnrAEs97MkzvSd4q2746Tgpg8b8XWiBGiBG4ZpgcAqBgtgPHTiAc8ZMCvZuikDw==",
+ "license": "MIT",
+ "dependencies": {
+ "assert": "^2.0.0",
+ "browser-resolve": "^2.0.0",
+ "browserify-zlib": "^0.2.0",
+ "buffer": "^5.7.1",
+ "console-browserify": "^1.1.0",
+ "constants-browserify": "^1.0.0",
+ "create-require": "^1.1.1",
+ "crypto-browserify": "^3.12.1",
+ "domain-browser": "4.22.0",
+ "events": "^3.0.0",
+ "https-browserify": "^1.0.0",
+ "isomorphic-timers-promises": "^1.0.1",
+ "os-browserify": "^0.3.0",
+ "path-browserify": "^1.0.1",
+ "pkg-dir": "^5.0.0",
+ "process": "^0.11.10",
+ "punycode": "^1.4.1",
+ "querystring-es3": "^0.2.1",
+ "readable-stream": "^3.6.0",
+ "stream-browserify": "^3.0.0",
+ "stream-http": "^3.2.0",
+ "string_decoder": "^1.0.0",
+ "timers-browserify": "^2.0.4",
+ "tty-browserify": "0.0.1",
+ "url": "^0.11.4",
+ "util": "^0.12.4",
+ "vm-browserify": "^1.0.1"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/object-inspect": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-is": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz",
+ "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.7",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz",
+ "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.3",
+ "define-properties": "^1.2.1",
+ "es-object-atoms": "^1.0.0",
+ "has-symbols": "^1.1.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/os-browserify": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz",
+ "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==",
+ "license": "MIT"
+ },
+ "node_modules/p-limit": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
+ "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
+ "license": "MIT",
+ "dependencies": {
+ "yocto-queue": "^0.1.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/p-locate": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
+ "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
+ "license": "MIT",
+ "dependencies": {
+ "p-limit": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/pako": {
+ "version": "1.0.11",
+ "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
+ "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
+ "license": "(MIT AND Zlib)"
+ },
+ "node_modules/parchment": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz",
+ "integrity": "sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/parse-asn1": {
+ "version": "5.1.9",
+ "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.9.tgz",
+ "integrity": "sha512-fIYNuZ/HastSb80baGOuPRo1O9cf4baWw5WsAp7dBuUzeTD/BoaG8sVTdlPFksBE2lF21dN+A1AnrpIjSWqHHg==",
+ "license": "ISC",
+ "dependencies": {
+ "asn1.js": "^4.10.1",
+ "browserify-aes": "^1.2.0",
+ "evp_bytestokey": "^1.0.3",
+ "pbkdf2": "^3.1.5",
+ "safe-buffer": "^5.2.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/path-browserify": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
+ "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
+ "license": "MIT"
+ },
+ "node_modules/path-exists": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
+ "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/path-parse": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
+ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
+ "license": "MIT"
+ },
+ "node_modules/pbkdf2": {
+ "version": "3.1.5",
+ "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.5.tgz",
+ "integrity": "sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "create-hash": "^1.2.0",
+ "create-hmac": "^1.1.7",
+ "ripemd160": "^2.0.3",
+ "safe-buffer": "^5.2.1",
+ "sha.js": "^2.4.12",
+ "to-buffer": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
+ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/pkg-dir": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz",
+ "integrity": "sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA==",
+ "license": "MIT",
+ "dependencies": {
+ "find-up": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/possible-typed-array-names": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz",
+ "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.10",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
+ "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.11",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/postcss-value-parser": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz",
+ "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/process": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+ "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/process-nextick-args": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
+ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
+ "license": "MIT"
+ },
+ "node_modules/proxy-from-env": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/public-encrypt": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz",
+ "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==",
+ "license": "MIT",
+ "dependencies": {
+ "bn.js": "^4.1.0",
+ "browserify-rsa": "^4.0.0",
+ "create-hash": "^1.1.0",
+ "parse-asn1": "^5.0.0",
+ "randombytes": "^2.0.1",
+ "safe-buffer": "^5.1.2"
+ }
+ },
+ "node_modules/public-encrypt/node_modules/bn.js": {
+ "version": "4.12.3",
+ "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.3.tgz",
+ "integrity": "sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==",
+ "license": "MIT"
+ },
+ "node_modules/punycode": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
+ "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
+ "license": "MIT"
+ },
+ "node_modules/qs": {
+ "version": "6.15.1",
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.1.tgz",
+ "integrity": "sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "side-channel": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/qs-esm": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/qs-esm/-/qs-esm-7.0.3.tgz",
+ "integrity": "sha512-8jbjCR0PPbqoQcv83C2K/zvVeytRPwPpt3WPDbq51qyLAxcWGtXVRjSe6GHtLCoVbg9+NEFkv7GyUxqjcDIJzw==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/querystring-es3": {
+ "version": "0.2.1",
+ "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz",
+ "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==",
+ "engines": {
+ "node": ">=0.4.x"
+ }
+ },
+ "node_modules/quill": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/quill/-/quill-2.0.3.tgz",
+ "integrity": "sha512-xEYQBqfYx/sfb33VJiKnSJp8ehloavImQ2A6564GAbqG55PGw1dAWUn1MUbQB62t0azawUS2CZZhWCjO8gRvTw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "eventemitter3": "^5.0.1",
+ "lodash-es": "^4.17.21",
+ "parchment": "^3.0.0",
+ "quill-delta": "^5.1.0"
+ },
+ "engines": {
+ "npm": ">=8.2.3"
+ }
+ },
+ "node_modules/quill-delta": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz",
+ "integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==",
+ "license": "MIT",
+ "dependencies": {
+ "fast-diff": "^1.3.0",
+ "lodash.clonedeep": "^4.5.0",
+ "lodash.isequal": "^4.5.0"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ }
+ },
+ "node_modules/randombytes": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz",
+ "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "node_modules/randomfill": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz",
+ "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==",
+ "license": "MIT",
+ "dependencies": {
+ "randombytes": "^2.0.5",
+ "safe-buffer": "^5.1.0"
+ }
+ },
+ "node_modules/readable-stream": {
+ "version": "3.6.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "string_decoder": "^1.1.1",
+ "util-deprecate": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/readdirp": {
+ "version": "4.1.2",
+ "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
+ "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
+ "devOptional": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 14.18.0"
+ },
+ "funding": {
+ "type": "individual",
+ "url": "https://paulmillr.com/funding/"
+ }
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/resolve": {
+ "version": "1.22.11",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz",
+ "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-core-module": "^2.16.1",
+ "path-parse": "^1.0.7",
+ "supports-preserve-symlinks-flag": "^1.0.0"
+ },
+ "bin": {
+ "resolve": "bin/resolve"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/ripemd160": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.3.tgz",
+ "integrity": "sha512-5Di9UC0+8h1L6ZD2d7awM7E/T4uA1fJRlx6zk/NvdCCVEoAnFqvHmCuNeIKoCeIixBX/q8uM+6ycDvF8woqosA==",
+ "license": "MIT",
+ "dependencies": {
+ "hash-base": "^3.1.2",
+ "inherits": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/ripemd160/node_modules/hash-base": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.2.tgz",
+ "integrity": "sha512-Bb33KbowVTIj5s7Ked1OsqHUeCpz//tPwR+E2zJgJKo9Z5XolZ9b6bdUgjmYlwnWhoOQKoTd1TYToZGn5mAYOg==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.4",
+ "readable-stream": "^2.3.8",
+ "safe-buffer": "^5.2.1",
+ "to-buffer": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/ripemd160/node_modules/isarray": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
+ "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
+ "license": "MIT"
+ },
+ "node_modules/ripemd160/node_modules/readable-stream": {
+ "version": "2.3.8",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
+ "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
+ "license": "MIT",
+ "dependencies": {
+ "core-util-is": "~1.0.0",
+ "inherits": "~2.0.3",
+ "isarray": "~1.0.0",
+ "process-nextick-args": "~2.0.0",
+ "safe-buffer": "~5.1.1",
+ "string_decoder": "~1.1.1",
+ "util-deprecate": "~1.0.1"
+ }
+ },
+ "node_modules/ripemd160/node_modules/readable-stream/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/ripemd160/node_modules/string_decoder": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
+ "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.1.0"
+ }
+ },
+ "node_modules/ripemd160/node_modules/string_decoder/node_modules/safe-buffer": {
+ "version": "5.1.2",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
+ "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
+ "license": "MIT"
+ },
+ "node_modules/rolldown": {
+ "version": "1.0.0-rc.13",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.13.tgz",
+ "integrity": "sha512-bvVj8YJmf0rq4pSFmH7laLa6pYrhghv3PRzrCdRAr23g66zOKVJ4wkvFtgohtPLWmthgg8/rkaqRHrpUEh0Zbw==",
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.123.0",
+ "@rolldown/pluginutils": "1.0.0-rc.13"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.0.0-rc.13",
+ "@rolldown/binding-darwin-arm64": "1.0.0-rc.13",
+ "@rolldown/binding-darwin-x64": "1.0.0-rc.13",
+ "@rolldown/binding-freebsd-x64": "1.0.0-rc.13",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.13",
+ "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.13",
+ "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.13",
+ "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.13",
+ "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.13",
+ "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.13",
+ "@rolldown/binding-linux-x64-musl": "1.0.0-rc.13",
+ "@rolldown/binding-openharmony-arm64": "1.0.0-rc.13",
+ "@rolldown/binding-wasm32-wasi": "1.0.0-rc.13",
+ "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.13",
+ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.13"
+ }
+ },
+ "node_modules/rxjs": {
+ "version": "7.8.2",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz",
+ "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/safe-buffer": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
+ "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/safe-regex-test": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz",
+ "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "is-regex": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/sass": {
+ "version": "1.99.0",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.99.0.tgz",
+ "integrity": "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==",
+ "devOptional": true,
+ "license": "MIT",
+ "dependencies": {
+ "chokidar": "^4.0.0",
+ "immutable": "^5.1.5",
+ "source-map-js": ">=0.6.2 <2.0.0"
+ },
+ "bin": {
+ "sass": "sass.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ },
+ "optionalDependencies": {
+ "@parcel/watcher": "^2.4.1"
+ }
+ },
+ "node_modules/select2": {
+ "version": "4.1.0-rc.0",
+ "resolved": "https://registry.npmjs.org/select2/-/select2-4.1.0-rc.0.tgz",
+ "integrity": "sha512-Hr9TdhyHCZUtwznEH2CBf7967mEM0idtJ5nMtjvk3Up5tPukOLXbHUNmh10oRfeNIhj+3GD3niu+g6sVK+gK0A==",
+ "license": "MIT"
+ },
+ "node_modules/select2-bootstrap-5-theme": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/select2-bootstrap-5-theme/-/select2-bootstrap-5-theme-1.3.0.tgz",
+ "integrity": "sha512-uEJDruP4tmwyKcs3V0oP7CIsyC45PGF5ddo8unwTp8OucJ1PSuTOBr+xbWaHTQPGnvp7N96psVQ5UBMQvFCcHA==",
+ "license": "MIT",
+ "dependencies": {
+ "bootstrap": "^5.1.3"
+ }
+ },
+ "node_modules/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "license": "MIT",
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/setimmediate": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
+ "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
+ "license": "MIT"
+ },
+ "node_modules/sha.js": {
+ "version": "2.4.12",
+ "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.12.tgz",
+ "integrity": "sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==",
+ "license": "(MIT AND BSD-3-Clause)",
+ "dependencies": {
+ "inherits": "^2.0.4",
+ "safe-buffer": "^5.2.1",
+ "to-buffer": "^1.2.0"
+ },
+ "bin": {
+ "sha.js": "bin.js"
+ },
+ "engines": {
+ "node": ">= 0.10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/shell-quote": {
+ "version": "1.8.3",
+ "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz",
+ "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.3",
+ "side-channel-list": "^1.0.0",
+ "side-channel-map": "^1.0.1",
+ "side-channel-weakmap": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-list": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz",
+ "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==",
+ "license": "MIT",
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "object-inspect": "^1.13.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-map": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/side-channel-weakmap": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.2",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.5",
+ "object-inspect": "^1.13.3",
+ "side-channel-map": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stream-browserify": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-3.0.0.tgz",
+ "integrity": "sha512-H73RAHsVBapbim0tU2JwwOiXUj+fikfiaoYAKHF3VJfA0pe2BCzkhAHBlLG6REzE+2WNZcxOXjK7lkso+9euLA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "~2.0.4",
+ "readable-stream": "^3.5.0"
+ }
+ },
+ "node_modules/stream-http": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-3.2.0.tgz",
+ "integrity": "sha512-Oq1bLqisTyK3TSCXpPbT4sdeYNdmyZJv1LxpEm2vu1ZhK89kSE5YXwZc3cWk0MagGaKriBh9mCFbVGtO+vY29A==",
+ "license": "MIT",
+ "dependencies": {
+ "builtin-status-codes": "^3.0.0",
+ "inherits": "^2.0.4",
+ "readable-stream": "^3.6.0",
+ "xtend": "^4.0.2"
+ }
+ },
+ "node_modules/string_decoder": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
+ "license": "MIT",
+ "dependencies": {
+ "safe-buffer": "~5.2.0"
+ }
+ },
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-color": {
+ "version": "8.1.1",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
+ "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/supports-color?sponsor=1"
+ }
+ },
+ "node_modules/supports-preserve-symlinks-flag": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
+ "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/sweetalert2": {
+ "version": "11.26.24",
+ "resolved": "https://registry.npmjs.org/sweetalert2/-/sweetalert2-11.26.24.tgz",
+ "integrity": "sha512-SLgukW4wicewpW5VOukSXY5Z6DL/z7HCOK2ODSjmQPiSphCN8gJAmh9npoceXOtBRNoDN0xIz+zHYthtfiHmjg==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "individual",
+ "url": "https://github.com/sponsors/limonte"
+ }
+ },
+ "node_modules/timers-browserify": {
+ "version": "2.0.12",
+ "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz",
+ "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "setimmediate": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=0.6.0"
+ }
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.16",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz",
+ "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==",
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/to-buffer": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.2.2.tgz",
+ "integrity": "sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==",
+ "license": "MIT",
+ "dependencies": {
+ "isarray": "^2.0.5",
+ "safe-buffer": "^5.2.1",
+ "typed-array-buffer": "^1.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/tree-kill": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz",
+ "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "tree-kill": "cli.js"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "devOptional": true,
+ "license": "0BSD"
+ },
+ "node_modules/tty-browserify": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.1.tgz",
+ "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==",
+ "license": "MIT"
+ },
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz",
+ "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==",
+ "license": "MIT",
+ "dependencies": {
+ "call-bound": "^1.0.3",
+ "es-errors": "^1.3.0",
+ "is-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/update-browserslist-db": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
+ "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/browserslist"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/browserslist"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "escalade": "^3.2.0",
+ "picocolors": "^1.1.1"
+ },
+ "bin": {
+ "update-browserslist-db": "cli.js"
+ },
+ "peerDependencies": {
+ "browserslist": ">= 4.21.0"
+ }
+ },
+ "node_modules/url": {
+ "version": "0.11.4",
+ "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz",
+ "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==",
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^1.4.1",
+ "qs": "^6.12.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/util": {
+ "version": "0.12.5",
+ "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
+ "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==",
+ "license": "MIT",
+ "dependencies": {
+ "inherits": "^2.0.3",
+ "is-arguments": "^1.0.4",
+ "is-generator-function": "^1.0.7",
+ "is-typed-array": "^1.1.3",
+ "which-typed-array": "^1.1.2"
+ }
+ },
+ "node_modules/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "license": "MIT"
+ },
+ "node_modules/vite": {
+ "version": "8.0.7",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.7.tgz",
+ "integrity": "sha512-P1PbweD+2/udplnThz3btF4cf6AgPky7kk23RtHUkJIU5BIxwPprhRGmOAHs6FTI7UiGbTNrgNP6jSYD6JaRnw==",
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.32.0",
+ "picomatch": "^4.0.4",
+ "postcss": "^8.5.8",
+ "rolldown": "1.0.0-rc.13",
+ "tinyglobby": "^0.2.15"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.1.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite-plugin-full-reload": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/vite-plugin-full-reload/-/vite-plugin-full-reload-1.2.0.tgz",
+ "integrity": "sha512-kz18NW79x0IHbxRSHm0jttP4zoO9P9gXh+n6UTwlNKnviTTEpOlum6oS9SmecrTtSr+muHEn5TUuC75UovQzcA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "picocolors": "^1.0.0",
+ "picomatch": "^2.3.1"
+ }
+ },
+ "node_modules/vite-plugin-full-reload/node_modules/picomatch": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
+ "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/vite-plugin-node-polyfills": {
+ "version": "0.26.0",
+ "resolved": "https://registry.npmjs.org/vite-plugin-node-polyfills/-/vite-plugin-node-polyfills-0.26.0.tgz",
+ "integrity": "sha512-BAe5YzJf368XGev02hDvioidx4uVH8dqEJlG73bjQSxM26/AQnGcKFomq9n3vGq5yqpSHKN4h1XQNxx9l98mBg==",
+ "license": "MIT",
+ "dependencies": {
+ "@rollup/plugin-inject": "^5.0.5",
+ "node-stdlib-browser": "^1.3.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/davidmyersdev"
+ },
+ "peerDependencies": {
+ "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/vm-browserify": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
+ "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==",
+ "license": "MIT"
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.20",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz",
+ "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==",
+ "license": "MIT",
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.8",
+ "call-bound": "^1.0.4",
+ "for-each": "^0.3.5",
+ "get-proto": "^1.0.1",
+ "gopd": "^1.2.0",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yocto-queue": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+ "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ziggy-js": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/ziggy-js/-/ziggy-js-2.6.2.tgz",
+ "integrity": "sha512-41xc9wRvv5Olh8pZjKSaL5vDAjw4BfTDMFoeLwRpDGc2B+uqcdwIKd81EHs6uwpqahdRrU7uMab0xWj0hBSDjg==",
+ "license": "MIT",
+ "dependencies": {
+ "qs-esm": "^7.0.3"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..71d29bb
--- /dev/null
+++ b/package.json
@@ -0,0 +1,36 @@
+{
+ "$schema": "https://www.schemastore.org/package.json",
+ "private": true,
+ "type": "module",
+ "scripts": {
+ "build": "vite build",
+ "dev": "vite"
+ },
+ "devDependencies": {
+ "@popperjs/core": "^2.11.8",
+ "alpinejs": "^3.4.2",
+ "alpinejs-axios": "^1.0.12",
+ "apexcharts": "^5.10.5",
+ "autoprefixer": "^10.4.27",
+ "axios": "1.15.2",
+ "concurrently": "^9.0.1",
+ "laravel-vite-plugin": "^3.0.0",
+ "postcss": "8.5.10",
+ "sass": "^1.99.0",
+ "sweetalert2": "^11.26.24",
+ "vite": "^8.0.7"
+ },
+ "dependencies": {
+ "datatables.net-bs5": "^2.3.7",
+ "datatables.net-dt": "^2.3.7",
+ "datatables.net-fixedcolumns": "^5.0.5",
+ "filepond": "^4.32.12",
+ "jquery": "^4.0.0",
+ "laravel-datatables-vite": "^0.6.2",
+ "quill": "^2.0.3",
+ "select2": "^4.1.0-rc.0",
+ "select2-bootstrap-5-theme": "^1.3.0",
+ "vite-plugin-node-polyfills": "^0.26.0",
+ "ziggy-js": "^2.6.2"
+ }
+}
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..abcde01
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,40 @@
+
+
+
+
+ tests/Architecture
+
+
+ tests/Unit
+
+
+ tests/Feature
+
+
+
+
+ app
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..6658cd9
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,5 @@
+export default {
+ plugins: {
+ autoprefixer: {},
+ },
+};
diff --git a/public/.htaccess b/public/.htaccess
new file mode 100644
index 0000000..b574a59
--- /dev/null
+++ b/public/.htaccess
@@ -0,0 +1,25 @@
+
+
+ Options -MultiViews -Indexes
+
+
+ RewriteEngine On
+
+ # Handle Authorization Header
+ RewriteCond %{HTTP:Authorization} .
+ RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
+
+ # Handle X-XSRF-Token Header
+ RewriteCond %{HTTP:x-xsrf-token} .
+ RewriteRule .* - [E=HTTP_X_XSRF_TOKEN:%{HTTP:X-XSRF-Token}]
+
+ # Redirect Trailing Slashes If Not A Folder...
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteCond %{REQUEST_URI} (.+)/$
+ RewriteRule ^ %1 [L,R=301]
+
+ # Send Requests To Front Controller...
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteRule ^ index.php [L]
+
diff --git a/public/favicon.ico b/public/favicon.ico
new file mode 100644
index 0000000..e69de29
diff --git a/public/index.php b/public/index.php
new file mode 100644
index 0000000..ee8f07e
--- /dev/null
+++ b/public/index.php
@@ -0,0 +1,20 @@
+handleRequest(Request::capture());
diff --git a/public/robots.txt b/public/robots.txt
new file mode 100644
index 0000000..eb05362
--- /dev/null
+++ b/public/robots.txt
@@ -0,0 +1,2 @@
+User-agent: *
+Disallow:
diff --git a/resources/js/alpine/ask.js b/resources/js/alpine/ask.js
new file mode 100644
index 0000000..200646c
--- /dev/null
+++ b/resources/js/alpine/ask.js
@@ -0,0 +1,114 @@
+export default function alpineAsk(Alpine) {
+ Alpine.magic('ask', () => {
+ return {
+ livewire: (eventName, { id, title, textMessage, confirmText, cancelText, successMessage, onSuccess = () => { } }) => {
+ return window.Swal.fire({
+ title,
+ text: textMessage,
+ icon: 'warning',
+ showConfirmButton: true,
+ showCancelButton: true,
+ confirmButtonText: confirmText,
+ cancelButtonText: cancelText,
+ showLoaderOnConfirm: true,
+ preConfirm: async () => {
+ return new Promise((resolve) => {
+ const completeEvent = eventName + '-completed';
+ const failureEvent = eventName + '-failed';
+
+ let unsubscribeSuccess;
+ let unsubscribeFailure;
+
+ // Centralized event unbinding utility
+ function cleanup() {
+ if (typeof unsubscribeSuccess === 'function') unsubscribeSuccess();
+ if (typeof unsubscribeFailure === 'function') unsubscribeFailure();
+ }
+
+ // Emergency Request Timeout Safety Boundary
+ const timeoutId = setTimeout(() => {
+ cleanup();
+ window.Swal.showValidationMessage('Server timeout while waiting for completion event.');
+ resolve(false); // Stops loader, keeps modal open
+ }, 10000);
+
+ // 1. Success Event Handler Loop
+ unsubscribeSuccess = window.Livewire.on(completeEvent, () => {
+ clearTimeout(timeoutId);
+ cleanup();
+ try {
+ onSuccess();
+ resolve(true); // Closes SweetAlert
+ } catch (e) {
+ window.Swal.showValidationMessage(`Success callback error: ${e.message}`);
+ resolve(false);
+ }
+ });
+
+ // 2. Failure Event Handler Loop
+ unsubscribeFailure = window.Livewire.on(failureEvent, (eventPayload) => {
+ clearTimeout(timeoutId);
+ cleanup();
+
+ // Livewire v3 passes event data wrapped in an object or array
+ const payloadData = eventPayload?.detail || eventPayload;
+ const message = payloadData?.message || (Array.isArray(payloadData) ? payloadData[0]?.message : null) || 'Backend execution failure';
+
+ window.Swal.showValidationMessage(message);
+ resolve(false); // Stops loader, lets user retry
+ });
+
+ // 3. Fire off the request payload execution hook directly to Livewire
+ try {
+ window.Livewire.dispatch(eventName, { id: id });
+ } catch (dispatchError) {
+ clearTimeout(timeoutId);
+ cleanup();
+ window.Swal.showValidationMessage(`Dispatch failed: ${dispatchError.message}`);
+ resolve(false);
+ }
+ });
+ }
+ }).then(result => {
+ if (result && result.isConfirmed && successMessage) {
+ // Safe check for global toast function
+ if (typeof toast === 'function') {
+ toast(successMessage, 'success');
+ } else if (window.toast === 'function') {
+ window.toast(successMessage, 'success');
+ }
+ }
+ });
+ },
+ ajax: ({ title, textMessage, confirmText, cancelText, successMessage, onSuccess = () => { } }) => {
+ return window.Swal.fire({
+ title,
+ text: textMessage,
+ icon: 'warning',
+ showConfirmButton: true,
+ showCancelButton: true,
+ confirmButtonText: confirmText,
+ cancelButtonText: cancelText,
+ showLoaderOnConfirm: true,
+ preConfirm: async () => {
+ try {
+ await onSuccess();
+ return true;
+ } catch (error) {
+ console.error(error);
+ const errorMsg = error?.message || 'Execution processing failed.';
+ window.Swal.showValidationMessage(errorMsg);
+ return false;
+ }
+ }
+ }).then(result => {
+ if (result && result.isConfirmed && successMessage) {
+ if (typeof toast === 'function') {
+ toast(successMessage, 'success');
+ }
+ }
+ });
+ }
+ }
+ })
+}
diff --git a/resources/js/alpine/axios.js b/resources/js/alpine/axios.js
new file mode 100644
index 0000000..456a0e7
--- /dev/null
+++ b/resources/js/alpine/axios.js
@@ -0,0 +1,41 @@
+import axios from 'axios';
+import { route } from 'ziggy-js';
+import { Ziggy } from '../ziggy';
+
+window.route = (name, params = {}, absolute = false) => {
+ return route(name, params, absolute, Ziggy);
+}
+
+axios.defaults.withCredentials = true;
+axios.defaults.xsrfCookieName = 'XSRF-TOKEN';
+axios.defaults.xsrfHeaderName = 'X-XSRF-TOKEN';
+window.axios = axios;
+
+export default function alpineAxios(Alpine) {
+ Alpine.magic('alpine', () => {
+ return {
+ route: (name, params = {}) => {
+ return route(name, params, undefined, Ziggy);
+ },
+ get: (url, config = {}) => {
+ return axios.get(url, config);
+ },
+ post: (url, config = {}) => {
+ return axios.post(url, config);
+ },
+ put: (url, config = {}) => {
+ return axios.put(url, config);
+ },
+ patch: (url, config = {}) => {
+ return axios.patch(url, config);
+ },
+ delete: (url, config = {}) => {
+ return axios.delete(url, config);
+ },
+ formErrors: (error) => {
+ return Object.fromEntries(Object.entries(error?.response?.data
+ ?.errors).map(([key, value]) => [key, value[0]]));
+ },
+ }
+ })
+}
\ No newline at end of file
diff --git a/resources/js/alpine/bs.js b/resources/js/alpine/bs.js
new file mode 100644
index 0000000..83eaa91
--- /dev/null
+++ b/resources/js/alpine/bs.js
@@ -0,0 +1,43 @@
+export default function bs(Alpine) {
+
+ Alpine.directive('bs', (el, { value, modifiers, expression }, { evaluateLater, cleanup }) => {
+
+ let evaluate = expression ? evaluateLater(expression) : () => { }
+ let handler = e => {
+ evaluate(() => { }, { scope: { '$event': e }, params: [e] })
+ }
+
+ let wrapHandler = (callback, wrapper) => (e) => wrapper(callback, e)
+ const eventName = `${modifiers}.bs.${value}`
+
+ handler = wrapHandler(handler, (next, e) => {
+ next(e);
+ });
+
+ el.addEventListener(eventName, handler);
+ cleanup(() => {
+ el.removeEventListener(eventName, handler)
+ })
+ })
+
+ Alpine.magic('bs', (el) => {
+ return new Proxy({}, {
+ get(target, type) {
+ return {
+ on(event, callback) {
+ el.addEventListener(`${event}.bs.${type}`, (e) => {
+ callback(e);
+ });
+ },
+ instance(element = undefined) {
+ const className = type.charAt(0).toUpperCase() + type.slice(1);
+ return bootstrap[className].getInstance(element ?? el);
+ },
+ updateHTML(attr, html) {
+ el.querySelector(attr).innerHTML = html
+ }
+ };
+ }
+ });
+ });
+}
\ No newline at end of file
diff --git a/resources/js/alpine/chart.js b/resources/js/alpine/chart.js
new file mode 100644
index 0000000..ac1f110
--- /dev/null
+++ b/resources/js/alpine/chart.js
@@ -0,0 +1,25 @@
+import '../plugin/apexchart.js'
+
+export default function AlpineChart(Alpine) {
+ Alpine.directive('chart', function (el, { expression }, { evaluate, cleanup }) {
+ let config = evaluate(expression);
+
+ const chart = new window.ApexCharts(el, config);
+ chart.render();
+
+ const event = function(event) {
+ const newData = event.detail
+ chart.updateSeries(newData.series, true, true);
+ chart.updateOptions(newData.options, true, true);
+ }
+
+ name = el.getAttribute('id');
+
+ window.addEventListener(`chart-${name}-update`, event)
+
+ cleanup(() => {
+ chart.destroy();
+ window.removeEventListener(`chart-${name}-update`, event)
+ });
+ })
+}
diff --git a/resources/js/alpine/quill.js b/resources/js/alpine/quill.js
new file mode 100644
index 0000000..a95c0a3
--- /dev/null
+++ b/resources/js/alpine/quill.js
@@ -0,0 +1,46 @@
+export default function alpineQuill(Alpine) {
+ Alpine.directive('quill', function (el, { expression }, { evaluate, cleanup }) {
+ const options = expression ? evaluate(expression) : {};
+ const defaultOptions = { theme: 'snow', ...options };
+
+ if (typeof window.Quill === 'undefined') {
+ throw new Error('Quill must be loaded');
+ }
+
+ const container = document.createElement('div');
+ el.appendChild(container);
+ const quill = new window.Quill(container, defaultOptions);
+
+ let isUpdating = false;
+
+ // 1. Text changes handler
+ quill.on('text-change', () => {
+ if (isUpdating) return;
+ isUpdating = true;
+ const htmlContent = quill.root.innerHTML;
+ el.value = htmlContent === '
' ? '' : htmlContent;
+ el.dispatchEvent(new Event('input', { bubbles: true }));
+ isUpdating = false;
+ });
+
+ // 2. Element Value Getter/Setter
+ Object.defineProperty(el, 'value', {
+ get() { return quill.root.innerHTML; },
+ set(newValue) {
+ if (isUpdating || newValue === quill.root.innerHTML) return;
+ isUpdating = true;
+ quill.root.innerHTML = newValue || '';
+ isUpdating = false;
+ },
+ configurable: true
+ });
+
+ quill.on('selection-change', (range) => {
+ if (range == null) {
+ el.dispatchEvent(new Event('blur', { bubbles: true }));
+ }
+ });
+
+ cleanup(() => el.innerHTML = '')
+ })
+}
diff --git a/resources/js/alpine/select2.js b/resources/js/alpine/select2.js
new file mode 100644
index 0000000..4e8e9ef
--- /dev/null
+++ b/resources/js/alpine/select2.js
@@ -0,0 +1,77 @@
+
+export default function alpineSelect2(Alpine) {
+ Alpine.directive('select2', function (el, { expression }, { evaluate, cleanup }) {
+ let config = evaluate(expression);
+
+ if((window.jQuery||window.jquery||window.$) && (typeof window.$.fn.select2 !== 'undefined')) {
+ let select2Default = {
+ placeholder: config.placeholder || 'Select an option',
+ ajax: config.url ? {
+ url: config.url,
+ dataType: 'json',
+ delay: 250,
+ xhrFields: {
+ withCredentials: true
+ },
+ data: function (params) {
+ return {
+ search: params.terms
+ }
+ },
+ processResults: function (data) {
+ return {
+ results: data.data || data
+ }
+ }
+ } : undefined
+ }
+
+ config = Object.assign(config, select2Default)
+ let $el = $(el).select2(config)
+
+ const modelName = el.getAttribute('wire:model') ||
+ el.getAttribute('wire:model.live') ||
+ el.getAttribute('wire:model.blur') || 'select2-clear';
+
+ let isUpdating = false
+
+ $el.on('change', (e) => {
+ if(isUpdating) return
+ isUpdating = true
+ el.dispatchEvent(new Event('change', {bubbles: true}))
+ isUpdating = false
+ })
+
+ // Fix for allowClear: prevent dropdown from opening when clicking the 'x'
+ $el.on('select2:unselecting', function (e) {
+ $(this).data('unselecting', true);
+ }).on('select2:opening', function (e) {
+ if ($(this).data('unselecting')) {
+ $(this).removeData('unselecting');
+ e.preventDefault();
+ }
+ });
+
+ const event = (e) => {
+ $el.val(null).trigger('change', {bubbles: true})
+ }
+
+ window.addEventListener(`${modelName}-clear`, event)
+
+ const observer = new MutationObserver(() => {
+ if (isUpdating) return;
+
+ isUpdating = true;
+ $el.trigger('change.select2'); // Sync Select2 UI with new value safely
+ isUpdating = false;
+ });
+ observer.observe(el, { attributes: true, attributeFilter: ['value'] });
+
+ cleanup(() => {
+ $el.select2('destroy')
+ window.removeEventListener(`${modelName}-clear`, event)
+ observer.disconnect()
+ })
+ }
+ })
+}
diff --git a/resources/js/alpinejs.js b/resources/js/alpinejs.js
new file mode 100644
index 0000000..9a39084
--- /dev/null
+++ b/resources/js/alpinejs.js
@@ -0,0 +1,23 @@
+// resources/js/app.js
+import { Alpine, Livewire } from '../../vendor/livewire/livewire/dist/livewire.esm';
+import bs from './alpine/bs';
+import alpineAxios from './alpine/axios';
+import alpineSelect2 from "./alpine/select2.js";
+import alpineQuill from "./alpine/quill.js";
+import alpineAsk from "./alpine/ask.js";
+import AlpineChart from "./alpine/chart.js";
+
+// 1. Expose both instances globally so Blade directives can see them
+window.Alpine = Alpine;
+window.Livewire = Livewire;
+
+// 2. Wrap plugin registration inside the initialization event listener
+Alpine.plugin(bs);
+Alpine.plugin(alpineAxios);
+Alpine.plugin(alpineAsk);
+Alpine.plugin(alpineSelect2);
+Alpine.plugin(alpineQuill);
+Alpine.plugin(AlpineChart);
+
+// 3. Boot Livewire
+Livewire.start();
diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js
new file mode 100644
index 0000000..e251574
--- /dev/null
+++ b/resources/js/bootstrap.js
@@ -0,0 +1,4 @@
+import * as bootstrap from 'bootstrap';
+import './sidebar.js'
+
+window.bootstrap = bootstrap
diff --git a/resources/js/plugin/apexchart.js b/resources/js/plugin/apexchart.js
new file mode 100644
index 0000000..b5e4f7c
--- /dev/null
+++ b/resources/js/plugin/apexchart.js
@@ -0,0 +1,4 @@
+import ApexCharts from 'apexcharts';
+import '../../scss/plugin/apexchart.scss';
+
+window.ApexCharts = ApexCharts;
\ No newline at end of file
diff --git a/resources/js/plugin/datatables.js b/resources/js/plugin/datatables.js
new file mode 100644
index 0000000..ab95177
--- /dev/null
+++ b/resources/js/plugin/datatables.js
@@ -0,0 +1,78 @@
+import 'datatables.net-fixedcolumns';
+import '../../scss/plugin/datatables.scss';
+
+window.ModulesFullyLoaded = false;
+window.DataTableLockedQueue = window.DataTableLockedQueue || [];
+
+import('laravel-datatables-vite').then(() => {
+ document.dispatchEvent(new CustomEvent('DOMContentLoaded'))
+ window.ModulesFullyLoaded = true;
+ window.checkAndFlushDataTables();
+});
+
+window.addEventListener('yajra:boot', (event) => {
+ const { id, config } = event.detail;
+ const selector = `#${id}`;
+ requestAnimationFrame(() => {
+ const $el = jQuery(selector);
+
+ if ($el.length && !DataTable.isDataTable(selector)) {
+ config.destroy = true;
+
+ try {
+ window.LaravelDataTables = window.LaravelDataTables || {};
+ window.LaravelDataTables[id] = $el.DataTable(config);
+ } catch (e) {
+ console.error(`Failed to boot DataTable [${id}]:`, e);
+ }
+ }
+ });
+});
+
+window.checkAndFlushDataTables = function () {
+ if (window.ModulesFullyLoaded) {
+ if (window.DataTableLockedQueue && window.DataTableLockedQueue.length) {
+ window.DataTableLockedQueue.forEach(bootFunction => bootFunction());
+ window.DataTableLockedQueue = [];
+ }
+ }
+};
+
+document.addEventListener('livewire:navigate', (event) => {
+ document.dispatchEvent(new CustomEvent('DOMContentLoaded'))
+})
+
+document.addEventListener('livewire:navigating', () => {
+ Object.keys(window.LaravelDataTables).forEach(id => {
+ const selector = `#${id}`;
+ const $table = jQuery(selector);
+
+ if (DataTable.isDataTable(selector)) {
+ // 1. Grab the actual DataTables API instance
+ const dtInstance = $table.DataTable();
+
+ // OPTIMIZATION 1: Kill Ghost AJAX Requests
+ // If the user navigates away while the table is still fetching data,
+ // the callback will execute on a dead DOM, causing massive memory leaks.
+ const settings = dtInstance.settings()[0];
+ if (settings && settings.jqXHR) {
+ settings.jqXHR.abort();
+ }
+
+ // OPTIMIZATION 2: Flush Internal Data Cache
+ // Calling .clear() purges DataTables' internal array of row data BEFORE
+ // we destroy it, saving the browser's Garbage Collector from doing the heavy lifting.
+ dtInstance.clear().destroy();
+
+ // OPTIMIZATION 3: Deep jQuery Cleanup
+ // Remove all data and unbind events attached to the table wrapper
+ $table.off().removeData();
+ }
+
+ // OPTIMIZATION 4: Explicitly sever the reference
+ // Using 'delete' tells the V8 JavaScript engine to immediately flag
+ // this specific object memory for sweeping, rather than waiting for reassignment.
+ delete window.LaravelDataTables[id];
+ });
+ window.LaravelDataTables = [];
+});
diff --git a/resources/js/plugin/filepond.js b/resources/js/plugin/filepond.js
new file mode 100644
index 0000000..5329810
--- /dev/null
+++ b/resources/js/plugin/filepond.js
@@ -0,0 +1,2 @@
+import '../../../vendor/spatie/livewire-filepond/resources/dist/filepond'
+import '../../scss/plugin/filepond.scss'
diff --git a/resources/js/plugin/jquery.js b/resources/js/plugin/jquery.js
new file mode 100644
index 0000000..fce0c36
--- /dev/null
+++ b/resources/js/plugin/jquery.js
@@ -0,0 +1,3 @@
+import jQuery from "jquery";
+
+window.jQuery = window.$ = jQuery
diff --git a/resources/js/plugin/quill.js b/resources/js/plugin/quill.js
new file mode 100644
index 0000000..cedeed5
--- /dev/null
+++ b/resources/js/plugin/quill.js
@@ -0,0 +1,4 @@
+import Quill from 'quill';
+import '../../scss/plugin/quill.scss';
+
+window.Quill = Quill;
diff --git a/resources/js/plugin/select2.js b/resources/js/plugin/select2.js
new file mode 100644
index 0000000..150e407
--- /dev/null
+++ b/resources/js/plugin/select2.js
@@ -0,0 +1,3 @@
+import select2 from 'select2';
+import '../../scss/plugin/select2.scss';
+select2(null,window.$)
diff --git a/resources/js/plugin/sweetalert2.js b/resources/js/plugin/sweetalert2.js
new file mode 100644
index 0000000..3550da4
--- /dev/null
+++ b/resources/js/plugin/sweetalert2.js
@@ -0,0 +1,27 @@
+import Swal from "sweetalert2";
+import "../../scss/plugin/sweetalert.scss";
+
+window.Swal = Swal.mixin({
+ target: '#swal-container',
+})
+
+window.toast = function (text, type = 'success') {
+ window.Swal.fire({
+ icon: type,
+ text: text,
+ toast: true,
+ position: 'top-end',
+ showConfirmButton: false,
+ timer: 3000,
+ timerProgressBar: true,
+ })
+}
+
+
+document.addEventListener('livewire:navigating', () => {
+ // If a user clicks a link while a SweetAlert is processing/open,
+ // cleanly destroy it so the overlay doesn't carry over to the new page.
+ if (window.Swal && window.Swal.isVisible()) {
+ window.Swal.close();
+ }
+});
diff --git a/resources/js/sidebar.js b/resources/js/sidebar.js
new file mode 100644
index 0000000..d26193a
--- /dev/null
+++ b/resources/js/sidebar.js
@@ -0,0 +1,31 @@
+const sidebar = document.getElementById('sidebar');
+const content = document.getElementById('content');
+const topbar = document.getElementById('topbar');
+const toggleBtn = document.getElementById('toggleBtn');
+const mobileBtn = document.getElementById('mobileBtn');
+const overlay = document.getElementById('overlay');
+
+// Desktop collapse
+if (toggleBtn) {
+ toggleBtn.addEventListener('click', () => {
+ if (sidebar) sidebar.classList.toggle('collapsed');
+ if (content) content.classList.toggle('full');
+ if (topbar) topbar.classList.toggle('full');
+ });
+}
+
+// Mobile sidebar open
+if (mobileBtn) {
+ mobileBtn.addEventListener('click', () => {
+ if (sidebar) sidebar.classList.add('mobile-show');
+ if (overlay) overlay.classList.add('show');
+ });
+}
+
+// 🔥 Click outside to close
+if (overlay) {
+ overlay.addEventListener('click', () => {
+ if (sidebar) sidebar.classList.remove('mobile-show');
+ if (overlay) overlay.classList.remove('show');
+ });
+}
\ No newline at end of file
diff --git a/resources/js/ziggy.js b/resources/js/ziggy.js
new file mode 100644
index 0000000..70d2d6f
--- /dev/null
+++ b/resources/js/ziggy.js
@@ -0,0 +1,5 @@
+const Ziggy = {"url":"https:\/\/laravel.local","port":null,"defaults":{},"routes":{"boost.browser-logs":{"uri":"_boost\/browser-logs","methods":["POST"]},"passport.token":{"uri":"oauth\/token","methods":["POST"]},"passport.authorizations.authorize":{"uri":"oauth\/authorize","methods":["GET","HEAD"]},"passport.device":{"uri":"oauth\/device","methods":["GET","HEAD"]},"passport.device.code":{"uri":"oauth\/device\/code","methods":["POST"]},"passport.token.refresh":{"uri":"oauth\/token\/refresh","methods":["POST"]},"passport.authorizations.approve":{"uri":"oauth\/authorize","methods":["POST"]},"passport.authorizations.deny":{"uri":"oauth\/authorize","methods":["DELETE"]},"passport.device.authorizations.authorize":{"uri":"oauth\/device\/authorize","methods":["GET","HEAD"]},"passport.device.authorizations.approve":{"uri":"oauth\/device\/authorize","methods":["POST"]},"passport.device.authorizations.deny":{"uri":"oauth\/device\/authorize","methods":["DELETE"]},"default-livewire.update":{"uri":"livewire-0ecae939\/update","methods":["POST"]},"livewire.upload-file":{"uri":"livewire-0ecae939\/upload-file","methods":["POST"]},"livewire.preview-file":{"uri":"livewire-0ecae939\/preview-file\/{filename}","methods":["GET","HEAD"],"parameters":["filename"]},"api.v1.lookups.roles":{"uri":"api\/v1\/lookups\/roles","methods":["GET","HEAD"]},"api.v1.lookups.permissions":{"uri":"api\/v1\/lookups\/permissions","methods":["GET","HEAD"]},"dashboard":{"uri":"dashboard","methods":["GET","HEAD"]},"users.index":{"uri":"users","methods":["GET","HEAD"]},"users.store":{"uri":"users","methods":["POST"]},"users.show":{"uri":"users\/{user}","methods":["GET","HEAD"],"parameters":["user"],"bindings":{"user":"id"}},"users.update":{"uri":"users\/{user}","methods":["PUT","PATCH"],"parameters":["user"],"bindings":{"user":"id"}},"users.destroy":{"uri":"users\/{user}","methods":["DELETE"],"parameters":["user"],"bindings":{"user":"id"}},"roles.index":{"uri":"roles","methods":["GET","HEAD"]},"roles.store":{"uri":"roles","methods":["POST"]},"roles.show":{"uri":"roles\/{role}","methods":["GET","HEAD"],"parameters":["role"],"bindings":{"role":"id"}},"roles.update":{"uri":"roles\/{role}","methods":["PUT","PATCH"],"parameters":["role"],"bindings":{"role":"id"}},"roles.destroy":{"uri":"roles\/{role}","methods":["DELETE"],"parameters":["role"],"bindings":{"role":"id"}},"settings.index":{"uri":"settings","methods":["GET","HEAD"]},"settings.update":{"uri":"settings","methods":["PUT"]},"profile.index":{"uri":"profile","methods":["GET","HEAD"]},"profile.update":{"uri":"profile","methods":["PATCH"]},"profile.destroy":{"uri":"profile","methods":["DELETE"]},"profile.settings.index":{"uri":"profile\/settings","methods":["GET","HEAD"]},"profile.settings.update":{"uri":"profile\/settings","methods":["PATCH"]},"register":{"uri":"register","methods":["GET","HEAD"]},"login":{"uri":"login","methods":["GET","HEAD"]},"password.request":{"uri":"forgot-password","methods":["GET","HEAD"]},"password.email":{"uri":"forgot-password","methods":["POST"]},"password.reset":{"uri":"reset-password\/{token}","methods":["GET","HEAD"],"parameters":["token"]},"password.store":{"uri":"reset-password","methods":["POST"]},"verification.notice":{"uri":"verify-email","methods":["GET","HEAD"]},"verification.verify":{"uri":"verify-email\/{id}\/{hash}","methods":["GET","HEAD"],"parameters":["id","hash"]},"verification.send":{"uri":"email\/verification-notification","methods":["POST"]},"password.confirm":{"uri":"confirm-password","methods":["GET","HEAD"]},"password.update":{"uri":"password","methods":["PUT"]},"logout":{"uri":"logout","methods":["POST"]},"storage.local":{"uri":"storage\/{path}","methods":["GET","HEAD"],"wheres":{"path":".*"},"parameters":["path"]},"storage.local.upload":{"uri":"storage\/{path}","methods":["PUT"],"wheres":{"path":".*"},"parameters":["path"]}}};
+if (typeof window !== 'undefined' && typeof window.Ziggy !== 'undefined') {
+ Object.assign(Ziggy.routes, window.Ziggy.routes);
+}
+export { Ziggy };
diff --git a/resources/scss/_avatar.scss b/resources/scss/_avatar.scss
new file mode 100644
index 0000000..b421e64
--- /dev/null
+++ b/resources/scss/_avatar.scss
@@ -0,0 +1,153 @@
+// Avatar
+.avatar {
+ position: relative;
+ display: inline-block;
+ width: 3rem;
+ height: 3rem;
+}
+
+// Avatar Size
+.avatar-xs {
+ width: $avatar-size-xs;
+ height: $avatar-size-xs;
+}
+
+.avatar-sm {
+ width: $avatar-size-sm;
+ height: $avatar-size-sm;
+}
+
+.avatar-md {
+ width: $avatar-size-md;
+ height: $avatar-size-md;
+}
+
+.avatar-lg {
+ width: $avatar-size-lg;
+ height: $avatar-size-lg;
+}
+
+.avatar-xl {
+ width: $avatar-size-xl;
+ height: $avatar-size-xl;
+}
+
+.avatar-xxl {
+ width: $avatar-size-xxl;
+ height: $avatar-size-xxl;
+}
+
+// Avatar img
+.avatar img {
+ width: 100%;
+ height: 100%;
+ -o-object-fit: cover;
+ object-fit: cover;
+}
+
+.avatar-indicators {
+ position: relative;
+}
+
+.avatar-indicators:before {
+ content: '';
+ position: absolute;
+ bottom: 0px;
+ right: 5%;
+ width: 30%;
+ height: 30%;
+ border-radius: 50%;
+ border: 2px solid var(--#{$prefix}white);
+ display: table;
+}
+
+.avatar-xxl.avatar-indicators:before {
+ bottom: 5px;
+ right: 17%;
+ width: 16%;
+ height: 16%;
+}
+
+// Avatar indicators
+.avatar-offline:before {
+ background-color: var(--#{$prefix}gray-400);
+}
+
+.avatar-online:before {
+ background-color: $success;
+}
+
+.avatar-away:before {
+ background-color: $warning;
+}
+
+.avatar-busy:before {
+ background-color: $danger;
+}
+
+.avatar-info:before {
+ background-color: $info;
+}
+
+// Avatar intials
+.avatar-initials {
+ display: -ms-flexbox;
+ display: flex;
+ -ms-flex-pack: center;
+ justify-content: center;
+ -ms-flex-align: center;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ text-transform: uppercase;
+}
+
+// Color varient
+.avatar-primary .avatar-initials {
+ color: var(--#{$prefix}white);
+ background-color: $primary;
+}
+.avatar-secondary .avatar-initials {
+ color: var(--#{$prefix}white);
+ background-color: $secondary;
+}
+.avatar-success .avatar-initials {
+ color: var(--#{$prefix}white);
+ background-color: $success;
+}
+.avatar-warning .avatar-initials {
+ color: var(--#{$prefix}white);
+ background-color: $warning;
+}
+.avatar-info .avatar-initials {
+ color: var(--#{$prefix}white);
+ background-color: $info;
+}
+.avatar-danger .avatar-initials {
+ color: var(--#{$prefix}white);
+ background-color: $danger;
+}
+.avatar-light .avatar-initials {
+ color: var(--#{$prefix}white);
+ background-color: $light;
+}
+.avatar-dark .avatar-initials {
+ color: var(--#{$prefix}white);
+ background-color: $dark;
+}
+
+// Avatar Group
+.avatar-group .avatar + .avatar {
+ margin-left: -1.2rem;
+}
+
+.avatar-group .avatar:hover {
+ z-index: 2;
+}
+
+// Avatar border
+.avatar-group img,
+.avatar-group .avatar .avatar-initials {
+ border: 2px solid var(--#{$prefix}white);
+}
diff --git a/resources/scss/_border.scss b/resources/scss/_border.scss
new file mode 100644
index 0000000..b63f975
--- /dev/null
+++ b/resources/scss/_border.scss
@@ -0,0 +1,34 @@
+// Border dashed
+
+.border-dashed {
+ --ds-border-style: dashed;
+}
+
+.timeline-vertical {
+ position: relative;
+ .timeline-item {
+ .timeline-bar {
+ position: absolute;
+ height: 100px;
+ left: 5px;
+ top: 24px;
+ }
+ }
+}
+.timeline-vertical-height {
+ .timeline-item {
+ .timeline-bar {
+ height: calc(100% - 1rem) !important;
+ }
+ }
+}
+
+// vr border
+.vr {
+ display: inline-block;
+ align-self: stretch;
+ width: $vr-border-width;
+ min-height: 1em;
+ background-color: var(--#{$prefix}border-color) !important;
+ opacity: $hr-opacity;
+}
diff --git a/resources/scss/_button.scss b/resources/scss/_button.scss
new file mode 100644
index 0000000..c2b7670
--- /dev/null
+++ b/resources/scss/_button.scss
@@ -0,0 +1,39 @@
+.btn * {
+ // disable pointer event
+ pointer-events: none;
+}
+
+.btn-icon {
+ position: relative;
+ display: -ms-inline-flexbox;
+ display: inline-flex;
+ -ms-flex-negative: 0;
+ flex-shrink: 0;
+ -ms-flex-pack: center;
+ justify-content: center;
+ -ms-flex-align: center;
+ align-items: center;
+ font-size: 0.92969rem;
+ font-weight: 400;
+ width: 2.5rem;
+ height: 2.5rem;
+ padding: 0;
+}
+
+.btn-icon.btn-xs {
+ font-size: 0.75rem;
+ width: 1.75rem;
+ height: 1.75rem;
+}
+
+.btn-icon.btn-sm {
+ font-size: 0.875rem;
+ width: 2.1875rem;
+ height: 2.1875rem;
+}
+
+.btn-icon.btn-lg {
+ font-size: 1rem;
+ width: 3.36875rem;
+ height: 3.36875rem;
+}
\ No newline at end of file
diff --git a/resources/scss/_custom.scss b/resources/scss/_custom.scss
new file mode 100644
index 0000000..603e128
--- /dev/null
+++ b/resources/scss/_custom.scss
@@ -0,0 +1,138 @@
+.overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, .45);
+ backdrop-filter: blur(1px);
+ display: none;
+ z-index: 1030;
+}
+
+.overlay.show {
+ display: block;
+}
+
+.sidebar {
+ width: 240px;
+ background: $white;
+ transition: width .3s, left .3s;
+ border-right: 1px solid $gray-200;
+ height: 100vh;
+ position: fixed;
+ top: 0;
+ left: 0;
+ padding-top: 60px;
+ z-index: 1030;
+
+ .nav-link {
+ color: $gray-800;
+ padding: 8px 10px;
+ display: flex;
+ align-items: center;
+ font-size: 14px;
+ font-weight: 400;
+ gap: 12px;
+ white-space: nowrap;
+ margin: 1px 12px;
+ border-radius: 8px;
+
+ &:hover {
+ color: $primary;
+ background-color: rgba($primary, 0.095);
+ }
+
+ &.active {
+ color: $primary;
+ background-color: rgba($primary, 0.095);
+ }
+
+ .ti {
+ font-size: 18px;
+ }
+ }
+
+ .nav-text {
+ transition: opacity .2s;
+ }
+
+ .logo-area {
+ position: absolute;
+ top: 0;
+ left: 0;
+ height: 60px;
+ width: 100%;
+ background: $white;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ padding-left: 17px;
+ color: $gray-800;
+ border-bottom: 1px solid $gray-200;
+ }
+}
+
+.sidebar.collapsed {
+ width: 60px;
+
+ .nav-link {
+ margin: 0 0px;
+ background-color: transparent;
+ padding: 8px 18px;
+ }
+
+ .nav-text {
+ display: none;
+ }
+
+ .logo-text {
+ display: none;
+ }
+}
+
+.topbar {
+ height: 60px;
+ margin-left: 240px;
+ // transition: margin-left .3s;
+}
+
+.topbar.full {
+ margin-left: 60px;
+}
+
+.content {
+ margin-left: 240px;
+ // transition: margin-left .3s;
+ // padding: 20px;
+}
+
+.content.full {
+ margin-left: 60px;
+}
+
+@media (max-width: 992px) {
+ .sidebar {
+ left: -240px;
+ }
+
+ .sidebar.mobile-show {
+ left: 0;
+ }
+
+ .topbar {
+ margin-left: 0 !important;
+ width: 100% !important;
+ }
+
+ .content {
+ margin-left: 0 !important;
+ }
+}
+
+.min-vh-90 {
+ min-height: 90vh;
+}
+
+
+@import "./avatar";
+@import "./button";
+@import "./icon-shape";
+@import "./utilities";
\ No newline at end of file
diff --git a/resources/scss/_icon-shape.scss b/resources/scss/_icon-shape.scss
new file mode 100644
index 0000000..93ba461
--- /dev/null
+++ b/resources/scss/_icon-shape.scss
@@ -0,0 +1,50 @@
+// Icon shape
+
+.icon-xxs {
+ width: $icon-size-xxs;
+ height: $icon-size-xxs;
+ line-height: $icon-size-xxs;
+}
+.icon-xs {
+ width: $icon-size-xs;
+ height: $icon-size-xs;
+ line-height: $icon-size-xs;
+}
+.icon-sm {
+ width: $icon-size-sm;
+ height: $icon-size-sm;
+ line-height: $icon-size-sm;
+}
+.icon-md {
+ width: $icon-size-md;
+ height: $icon-size-md;
+ line-height: $icon-size-md;
+}
+.icon-lg {
+ width: $icon-size-lg;
+ height: $icon-size-lg;
+ line-height: $icon-size-lg;
+}
+.icon-xl {
+ width: $icon-size-xl;
+ height: $icon-size-xl;
+ line-height: $icon-size-xl;
+}
+.icon-xxl {
+ width: $icon-size-xxl;
+ height: $icon-size-xxl;
+ line-height: $icon-size-xxl;
+}
+
+.icon-xxxl {
+ width: $icon-size-xxxl;
+ height: $icon-size-xxxl;
+ line-height: $icon-size-xxxl;
+}
+.icon-shape {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ text-align: center;
+ vertical-align: middle;
+}
diff --git a/resources/scss/_utilities.scss b/resources/scss/_utilities.scss
new file mode 100644
index 0000000..a3db9e2
--- /dev/null
+++ b/resources/scss/_utilities.scss
@@ -0,0 +1,59 @@
+@import "bootstrap/scss/functions";
+@import "bootstrap/scss/variables";
+@import "bootstrap/scss/variables-dark";
+@import "bootstrap/scss/maps";
+@import "bootstrap/scss/mixins";
+@import "bootstrap/scss/utilities";
+
+$utilities: map-merge(
+ $utilities,
+ (
+ "font-size": (
+ rfs: true,
+ responsive: true,
+ property: font-size,
+ class: fs,
+ values: $font-sizes
+),
+"position": (
+ property: position,
+ responsive: true,
+ values: static relative absolute fixed sticky
+),
+"top": (
+ property: top,
+ responsive: true,
+ values: $position-values
+),
+"bottom": (
+ property: bottom,
+ responsive: true,
+ values: $position-values
+),
+"start": (
+ property: left,
+ class: start,
+ responsive: true,
+
+ values: $position-values
+),
+"end": (
+ property: right,
+ class: end,
+ responsive: true,
+ values: $position-values
+),
+"translate-middle": (
+ property: transform,
+ class: translate-middle,
+ responsive: true,
+ values: (
+ null: translate(-50%, -50%),
+ x: translateX(-50%),
+ y: translateY(-50%),
+ )
+),
+ )
+);
+
+@import "bootstrap/scss/utilities/api";
\ No newline at end of file
diff --git a/resources/scss/_variables.scss b/resources/scss/_variables.scss
new file mode 100644
index 0000000..98d7d0b
--- /dev/null
+++ b/resources/scss/_variables.scss
@@ -0,0 +1,141 @@
+// Example: override Bootstrap color variables
+
+$gray-50: #fafafa !default;
+$gray-100: #f5f5f5 !default;
+$gray-200: #e5e5e5 !default;
+$gray-300: #d4d4d4 !default;
+$gray-400: #a3a3a3 !default;
+$gray-500: #737373 !default;
+$gray-600: #525252 !default;
+$gray-700: #404040 !default;
+$gray-800: #262626 !default;
+$gray-900: #171717 !default;
+$gray-950: #0a0a0a !default;
+
+$white: #ffffff !default;
+$black: #000000 !default;
+
+$orange: #E66239 !default;
+$red: #FB2C36 !default;
+$green: #00C951 !default;
+$cyan: #00B8DB !default;
+$yellow: #F0B100 !default;
+
+
+$primary: $orange !default;
+$secondary: $gray-600 !default;
+$success: $green !default;
+$info: $cyan !default;
+$warning: $yellow !default;
+$danger: $red !default;
+
+$min-contrast-ratio: 2.5 !default;
+$link-hover-decoration: none !default;
+$link-color: $gray-700 !default;
+$link-hover-color: $primary !default;
+
+
+$light: $gray-100 !default;
+$border-color: $gray-200 !default;
+$border-color-translucent: $gray-200 !default;
+
+$enable-negative-margins: true !default;
+$enable-rounded: true !default;
+
+$headings-font-weight: 400 !default;
+$grid-gutter-width: 2.5rem !default;
+
+// scss-docs-start position-map
+$position-values: (
+ 0: 0,
+ 10: 10%,
+ 25: 25%,
+ 50: 50%,
+ 60: 60%,
+ 65:65%,
+ 70: 70%,
+ 75: 75%,
+ 100: 100%,
+ auto: auto
+) !default;
+// scss-docs-end position-map
+
+
+// scss-docs-start spacer-variables-maps
+$spacer: 1rem !default;
+$spacers: (
+ 0: 0,
+ 1: $spacer * 0.25,
+ // 4px
+ 2: $spacer * 0.5,
+ // 8px
+ 3: $spacer,
+ // 16px
+ 4: $spacer * 1.5,
+ // 24px
+ 5: $spacer * 2,
+ // 32px
+ 6: $spacer * 2.5,
+ // 40px
+ 7: $spacer * 3,
+ // 48px
+ 8: $spacer * 4,
+ // 64px
+ 9: $spacer * 5,
+ // 80px
+ 10: $spacer * 6,
+ // 96px
+ 11: $spacer * 8,
+ // 128px
+) !default;
+// scss-docs-end spacer-variables-maps
+
+$font-family-base: 'Poppins', sans-serif !default;
+$font-size-root: null !default;
+$font-size-base: .875rem !default; // Assumes the browser default, typically `16px`
+$font-size-sm: $font-size-base * .85 !default;
+$font-size-lg: $font-size-base * 1.25 !default;
+
+$font-xs: 0.75rem !default;
+
+$h1-font-size: $font-size-base * 2.5 !default;
+$h2-font-size: $font-size-base * 2 !default;
+$h3-font-size: $font-size-base * 1.75 !default;
+$h4-font-size: $font-size-base * 1.5 !default;
+$h5-font-size: $font-size-base * 1.25 !default;
+$h6-font-size: $font-size-base !default;
+
+// scss-docs-start border-radius-variables
+
+
+
+
+$input-btn-padding-y: 0.5rem !default;
+$input-btn-padding-x: .875rem !default;
+$table-th-font-weight: 400!default;
+
+
+
+$link-decoration: none !default;
+
+$link-hover-decoration: underline !default;
+
+
+// Icon
+$icon-color: $gray-500 !default;
+$icon-size-xxs: 1rem !default;
+$icon-size-xs: 1.5rem !default;
+$icon-size-sm: 2rem !default;
+$icon-size-md: 2.5rem !default;
+$icon-size-lg: 3rem !default;
+$icon-size-xl: 3.5rem !default;
+$icon-size-xxl: 4rem !default;
+$icon-size-xxxl: 7rem !default;
+
+// Avatar
+$avatar-size-xs: 1.5rem !default;
+$avatar-size-sm: 2rem !default;
+$avatar-size-md: 2.5rem !default;
+$avatar-size-lg: 3.5rem !default;
+$avatar-size-xl: 5rem !default;
+$avatar-size-xxl: 7.5rem !default;
diff --git a/resources/scss/app.scss b/resources/scss/app.scss
new file mode 100644
index 0000000..591fc2a
--- /dev/null
+++ b/resources/scss/app.scss
@@ -0,0 +1,13 @@
+@import url('https://fonts.googleapis.com/css2?family=Poppins:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
+
+// Import Bootstrap functions first
+@import "bootstrap/scss/functions";
+
+// Override Bootstrap variables here
+@import "./variables";
+
+// Import Bootstrap
+@import "bootstrap/scss/bootstrap";
+
+// Custom styles
+@import "./custom";
\ No newline at end of file
diff --git a/resources/scss/plugin/apexchart.scss b/resources/scss/plugin/apexchart.scss
new file mode 100644
index 0000000..96c4dfd
--- /dev/null
+++ b/resources/scss/plugin/apexchart.scss
@@ -0,0 +1,107 @@
+@import "../variables";
+@import "apexcharts/dist/apexcharts.css";
+
+// ApexCharts Theme Overrides
+.apexcharts-canvas {
+ font-family: $font-family-base !important;
+
+ // Tooltip
+ .apexcharts-tooltip {
+ font-family: $font-family-base;
+ font-size: $font-size-base;
+ border: 1px solid $border-color;
+ border-radius: 0.5rem;
+ box-shadow: 0 4px 16px rgba($black, 0.15);
+ background: $white;
+ color: $gray-800;
+
+ &.apexcharts-theme-light {
+ border: 1px solid $border-color;
+ background: $white;
+ }
+
+ .apexcharts-tooltip-title {
+ font-family: $font-family-base;
+ font-size: $font-size-sm;
+ font-weight: 600;
+ background: $gray-100;
+ border-bottom: 1px solid $border-color;
+ color: $gray-800;
+ padding: 0.375rem 0.75rem;
+ }
+
+ .apexcharts-tooltip-series-group {
+ padding: 0.375rem 0.75rem;
+ }
+
+ .apexcharts-tooltip-text {
+ font-family: $font-family-base;
+ font-size: $font-size-sm;
+ }
+
+ .apexcharts-tooltip-marker {
+ border-radius: 50%;
+ }
+ }
+
+ // Legend
+ .apexcharts-legend {
+ font-family: $font-family-base;
+ }
+
+ .apexcharts-legend-text {
+ font-family: $font-family-base !important;
+ font-size: $font-size-sm !important;
+ color: $gray-600 !important;
+ }
+
+ // X & Y axis labels
+ .apexcharts-xaxis-label,
+ .apexcharts-yaxis-label {
+ font-family: $font-family-base;
+ fill: $gray-500;
+ font-size: $font-size-sm;
+ }
+
+ // Grid lines
+ .apexcharts-gridline {
+ stroke: $gray-200;
+ }
+
+ // Y-axis line
+ .apexcharts-yaxis-border,
+ .apexcharts-xaxis-border {
+ stroke: $gray-200;
+ }
+
+ // Toolbar icons
+ .apexcharts-toolbar {
+ .apexcharts-menu {
+ font-family: $font-family-base;
+ font-size: $font-size-sm;
+ border: 1px solid $border-color;
+ border-radius: 0.5rem;
+ box-shadow: 0 4px 12px rgba($black, 0.15);
+ }
+
+ .apexcharts-menu-item:hover {
+ background: rgba($primary, 0.08);
+ color: $primary;
+ }
+ }
+
+ // Data labels
+ .apexcharts-data-labels text {
+ font-family: $font-family-base !important;
+ fill: $gray-700;
+ }
+}
+
+// Chart color palette aligned with brand colors
+.apexcharts-series {
+ &:nth-child(1) { --chart-color: #{$primary}; }
+ &:nth-child(2) { --chart-color: #{$info}; }
+ &:nth-child(3) { --chart-color: #{$success}; }
+ &:nth-child(4) { --chart-color: #{$warning}; }
+ &:nth-child(5) { --chart-color: #{$danger}; }
+}
diff --git a/resources/scss/plugin/datatables.scss b/resources/scss/plugin/datatables.scss
new file mode 100644
index 0000000..0ac8fbe
--- /dev/null
+++ b/resources/scss/plugin/datatables.scss
@@ -0,0 +1,222 @@
+@import "../variables";
+@import "datatables.net-bs5/css/dataTables.bootstrap5.min.css";
+
+// DataTables Theme Overrides
+table.dataTable {
+ font-family: $font-family-base;
+ font-size: $font-size-base;
+ color: $gray-800;
+ border-collapse: collapse !important;
+ border-color: $border-color;
+
+ // Header cells
+ thead {
+
+ th,
+ td {
+ font-weight: 500;
+ font-size: $font-size-sm;
+ color: $gray-600;
+ background: $gray-100;
+ border-bottom: 1px solid $border-color !important;
+ padding: 0.625rem 0.75rem;
+ white-space: nowrap;
+ }
+
+ // Sort icons
+ th.sorting,
+ th.sorting_asc,
+ th.sorting_desc {
+ cursor: pointer;
+
+ &::before,
+ &::after {
+ opacity: 0.4;
+ color: $gray-500;
+ }
+
+ &.sorting_asc::before,
+ &.sorting_desc::after {
+ opacity: 1;
+ color: $primary;
+ }
+ }
+ }
+
+ // Body cells
+ tbody {
+ td {
+ padding: 0.625rem 0.75rem;
+ vertical-align: middle;
+ border-top: 1px solid $border-color;
+ }
+
+ tr {
+ transition: background-color 0.15s;
+
+ &:hover>td {
+ background-color: rgba($primary, 0.04);
+
+ &.dtfc-fixed-left,
+ &.dtfc-fixed-right {
+ background-color: mix($primary, $white, 4%);
+ }
+ }
+
+ &.selected>td {
+ background-color: rgba($primary, 0.1);
+ color: $gray-900;
+
+ &.dtfc-fixed-left,
+ &.dtfc-fixed-right {
+ background-color: mix($primary, $white, 10%);
+ }
+ }
+ }
+ }
+
+ // Footer cells
+ tfoot {
+
+ th,
+ td {
+ border-top: 2px solid $border-color;
+ font-weight: 500;
+ color: $gray-800;
+ }
+ }
+
+ // Striped rows
+ &.table-striped>tbody>tr:nth-child(odd)>td {
+ background-color: rgba($gray-50, 0.6);
+
+ &.dtfc-fixed-left,
+ &.dtfc-fixed-right {
+ background-color: mix($gray-50, $white, 60%);
+ }
+ }
+}
+
+// Wrapper lengths control
+.dataTables_wrapper {
+ font-family: $font-family-base;
+ font-size: $font-size-base;
+ color: $gray-800;
+
+ // Length (items per page) select
+ .dataTables_length {
+ label {
+ font-size: $font-size-sm;
+ color: $gray-600;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ }
+
+ select {
+ font-family: $font-family-base;
+ font-size: $font-size-sm;
+ border: 1px solid $border-color;
+ border-radius: 0.5rem;
+ padding: 0.25rem 2.25rem 0.25rem 0.75rem; /* Increased padding-right for dropdown arrow */
+ color: $gray-800;
+ background-color: $white;
+ min-width: 80px; /* Force minimum width to prevent cut-off */
+
+ &:focus {
+ border-color: $primary;
+ outline: 0;
+ box-shadow: 0 0 0 3px rgba($primary, 0.12);
+ }
+ }
+ }
+
+ // Search input
+ .dataTables_filter {
+ label {
+ font-size: $font-size-sm;
+ color: $gray-600;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ }
+
+ input {
+ font-family: $font-family-base;
+ font-size: $font-size-sm;
+ border: 1px solid $border-color;
+ border-radius: 0.5rem;
+ padding: 0.375rem 0.75rem;
+ color: $gray-800;
+
+ &:focus {
+ border-color: $primary;
+ outline: 0;
+ box-shadow: 0 0 0 3px rgba($primary, 0.12);
+ }
+ }
+ }
+
+ // Info text
+ .dataTables_info {
+ font-size: $font-size-sm;
+ color: $gray-500;
+ padding-top: 0.5rem;
+ }
+
+ // Pagination
+ .dataTables_paginate {
+ .paginate_button {
+ font-family: $font-family-base;
+ font-size: $font-size-sm;
+ border-radius: 0.5rem !important;
+ border: 1px solid transparent !important;
+ color: $gray-800 !important;
+ padding: 0.3rem 0.65rem;
+ transition: all 0.15s;
+
+ &:hover {
+ background: rgba($primary, 0.08) !important;
+ color: $primary !important;
+ border-color: transparent !important;
+ box-shadow: none !important;
+ }
+
+ &.current,
+ &.current:hover {
+ background: $primary !important;
+ color: $white !important;
+ border-color: $primary !important;
+ box-shadow: none !important;
+ font-weight: 500;
+ }
+
+ &.disabled,
+ &.disabled:hover {
+ color: $gray-600 !important;
+ cursor: not-allowed;
+ }
+ }
+ }
+
+ // Processing indicator
+ .dataTables_processing {
+ font-family: $font-family-base;
+ font-size: $font-size-sm;
+ border: 1px solid $border-color;
+ border-radius: 0.5rem;
+ background: $white;
+ box-shadow: 0 4px 16px rgba($black, 0.15);
+ color: $gray-800;
+
+ >div:last-child>div {
+ background: $primary;
+ }
+ }
+}
+
+.dt-length {
+ select {
+ min-width: 70px !important;
+ }
+}
diff --git a/resources/scss/plugin/filepond.scss b/resources/scss/plugin/filepond.scss
new file mode 100644
index 0000000..01a65eb
--- /dev/null
+++ b/resources/scss/plugin/filepond.scss
@@ -0,0 +1,58 @@
+@import "../variables";
+@import "../../../vendor/spatie/livewire-filepond/resources/dist/filepond.css";
+
+.filepond--root {
+ max-width: 460px;
+ width: 100%;
+ border-radius: 1rem;
+ overflow: hidden;
+ border: 2px dashed $primary;
+ background: $light;
+ box-shadow: 0 8px 24px rgba(#000, 0.08);
+ transition: all 0.2s ease-in-out;
+
+ &:hover {
+ border-color: mix(#000, $primary, 20%);
+ box-shadow: 0 10px 28px rgba(#000, 0.12);
+ }
+
+ .filepond--panel-root {
+ background-color: rgba($primary, 0.05) !important;
+ border-radius: 1rem !important;
+ }
+
+ .filepond--drop-label {
+ font-size: 1.1rem;
+ color: $secondary;
+ font-weight: 700;
+ padding: 1.4rem;
+ }
+
+ .filepond--item {
+ border-radius: 0.75rem;
+ overflow: hidden;
+ box-shadow: 0 4px 12px rgba(#000, 0.06);
+ margin-bottom: 0.75rem;
+ transition: all 0.2s ease-in-out;
+
+ &:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 8px 20px rgba(#000, 0.1);
+ }
+ }
+
+ .filepond--file-action-button {
+ background-color: $primary !important;
+ color: #fff !important;
+ border-radius: 0.5rem !important;
+ transition: all 0.2s ease-in-out;
+
+ &:hover {
+ background-color: mix(#000, $primary, 20%) !important;
+ }
+ }
+
+ .filepond--credits {
+ display: none !important;
+ }
+}
diff --git a/resources/scss/plugin/quill.scss b/resources/scss/plugin/quill.scss
new file mode 100644
index 0000000..b9beb8b
--- /dev/null
+++ b/resources/scss/plugin/quill.scss
@@ -0,0 +1,2 @@
+@import "quill/dist/quill.core.css";
+@import "quill/dist/quill.snow.css";
diff --git a/resources/scss/plugin/select2.scss b/resources/scss/plugin/select2.scss
new file mode 100644
index 0000000..0be5016
--- /dev/null
+++ b/resources/scss/plugin/select2.scss
@@ -0,0 +1,277 @@
+@import "../variables";
+@import "select2/dist/css/select2.min.css";
+@import "select2-bootstrap-5-theme/dist/select2-bootstrap-5-theme.min.css";
+
+// Select2 Theme Overrides
+.select2-container {
+ font-family: $font-family-base;
+ font-size: $font-size-base;
+ width: 100% !important;
+
+ // Main selection box
+ .select2-selection--single,
+ .select2-selection--multiple {
+ font-family: $font-family-base;
+ font-size: $font-size-base;
+ border: 1px solid $border-color;
+ border-radius: 0.5rem !important;
+ background-color: $white;
+ transition: border-color 0.15s, box-shadow 0.15s;
+
+ &:hover {
+ border-color: $gray-600;
+ }
+ }
+
+ // Single select rendered value
+ .select2-selection--single {
+ height: calc(1.5em + 0.75rem + 2px);
+
+ .select2-selection__rendered {
+ color: $gray-800;
+ padding-left: 0.75rem;
+ padding-right: 2rem;
+ width: 90%;
+ line-height: calc(1.5em + 0.75rem);
+ overflow: hidden !important;
+ text-overflow: ellipsis !important;
+ white-space: nowrap !important;
+ display: block !important;
+
+ // Increase padding right when clear button is present so text truncates before it
+ &:has(.select2-selection__clear) {
+ padding-right: .3rem !important;
+ }
+ }
+
+ .select2-selection__placeholder {
+ color: $gray-600;
+ }
+
+ .select2-selection__clear {
+ position: absolute;
+ right: 1.8rem;
+ top: 50%;
+ transform: translateY(-50%);
+ z-index: 10;
+ cursor: pointer;
+ color: $gray-500;
+ background: transparent;
+ padding: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ height: 1rem;
+ width: 1rem;
+ line-height: 1;
+
+ &:hover {
+ color: $danger;
+ }
+ }
+
+ .select2-selection__arrow {
+ height: 100%;
+ right: 0.5rem;
+
+ b {
+ border-color: $gray-600 transparent transparent;
+ border-width: 5px 4px 0;
+ }
+ }
+ }
+
+ // Multiple select tags
+ .select2-selection--multiple {
+ padding: 0.25rem 0.5rem;
+ min-height: calc(1.5em + 0.75rem + 2px);
+
+ .select2-selection__choice {
+ background-color: rgba($primary, 0.1);
+ border: 1px solid rgba($primary, 0.2);
+ border-radius: 0.375rem;
+ color: $primary;
+ font-size: $font-size-sm;
+ font-weight: 500;
+ padding: 0.1rem 0.5rem;
+
+ .select2-selection__choice__remove {
+ color: $primary;
+ font-weight: 700;
+ margin-right: 4px;
+
+ &:hover {
+ color: $danger;
+ background: transparent;
+ }
+ }
+ }
+
+ .select2-search__field {
+ font-family: $font-family-base;
+ font-size: $font-size-sm;
+ color: $gray-800;
+
+ &::placeholder {
+ color: $gray-600;
+ }
+ }
+ }
+
+ // Focused state
+ &--open,
+ &--focus {
+
+ .select2-selection--single,
+ .select2-selection--multiple {
+ border-color: $primary !important;
+ box-shadow: 0 0 0 3px rgba($primary, 0.12) !important;
+ outline: 0;
+ }
+
+ .select2-selection--single .select2-selection__arrow b {
+ border-color: transparent transparent $primary;
+ border-width: 0 4px 5px;
+ }
+ }
+}
+
+// Support for form-control-sm
+select.form-control-sm + .select2-container,
+select.form-select-sm + .select2-container,
+.select2-container--sm {
+ .select2-selection--single,
+ .select2-selection--multiple {
+ border-radius: 0.375rem !important;
+ }
+
+ .select2-selection--single {
+ height: calc(1.5em + 0.5rem + 2px) !important;
+
+ .select2-selection__rendered {
+ padding-left: 0.5rem;
+ font-size: $font-size-sm;
+ line-height: calc(1.5em + 0.5rem) !important;
+ }
+
+ .select2-selection__clear {
+ right: 0.5rem;
+ }
+ }
+
+ .select2-selection--multiple {
+ min-height: calc(1.5em + 0.5rem + 2px) !important;
+ padding: 0.15rem 0.5rem;
+
+ .select2-search__field {
+ font-size: $font-size-sm;
+ }
+ }
+}
+
+// Dropdown panel
+.select2-dropdown {
+ font-family: $font-family-base;
+ font-size: $font-size-base;
+ border: 1px solid $border-color;
+ border-radius: 0.5rem !important;
+ box-shadow: 0 8px 24px rgba($black, 0.15);
+ overflow: hidden;
+
+ // Search field inside dropdown
+ .select2-search--dropdown {
+ padding: 0.5rem;
+ background: $gray-100;
+ border-bottom: 1px solid $border-color;
+
+ .select2-search__field {
+ font-family: $font-family-base;
+ font-size: $font-size-sm;
+ border: 1px solid $border-color;
+ border-radius: 0.375rem;
+ padding: 0.375rem 0.75rem;
+ color: $gray-800;
+ width: 100%;
+ background-color: $white;
+
+ &:focus {
+ border-color: $primary;
+ outline: 0;
+ box-shadow: 0 0 0 3px rgba($primary, 0.12);
+ }
+ }
+ }
+
+ // Option list
+ .select2-results__options {
+ padding: 0.25rem 0;
+ max-height: 220px;
+ overflow-y: auto;
+
+ // Scrollbar
+ &::-webkit-scrollbar {
+ width: 5px;
+ }
+
+ &::-webkit-scrollbar-track {
+ background: $gray-100;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: $gray-300;
+ border-radius: 4px;
+ }
+ }
+
+ .select2-results__option {
+ font-size: $font-size-sm;
+ color: $gray-800;
+ padding: 0.45rem 0.85rem;
+ transition: background 0.12s;
+
+ // Highlighted (hover or keyboard)
+ &--highlighted {
+ background-color: rgba($primary, 0.08) !important;
+ color: $primary !important;
+ }
+
+ // Selected
+ &[aria-selected='true'] {
+ background-color: rgba($primary, 0.12);
+ color: $primary;
+ font-weight: 500;
+ }
+
+ // Disabled
+ &[aria-disabled='true'] {
+ color: $gray-600;
+ cursor: not-allowed;
+ }
+
+ // Option group label
+ &--group {
+ color: $gray-500;
+ font-size: $font-xs;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.04em;
+ padding: 0.5rem 0.85rem 0.25rem;
+ cursor: default;
+ }
+ }
+}
+
+// Disabled state
+.select2-container--disabled {
+
+ .select2-selection--single,
+ .select2-selection--multiple {
+ background-color: $gray-100 !important;
+ border-color: $border-color !important;
+ cursor: not-allowed;
+
+ .select2-selection__rendered {
+ color: $gray-600;
+ }
+ }
+}
diff --git a/resources/scss/plugin/sweetalert.scss b/resources/scss/plugin/sweetalert.scss
new file mode 100644
index 0000000..cf33709
--- /dev/null
+++ b/resources/scss/plugin/sweetalert.scss
@@ -0,0 +1,142 @@
+@import "../variables";
+@import "sweetalert2/src/sweetalert2.scss";
+
+// Override SweetAlert2 variables
+// Brand Colors
+$swal2-confirm-button-background-color: $primary;
+$swal2-deny-button-background-color: $danger;
+$swal2-cancel-button-background-color: $secondary;
+
+// States
+$swal2-success: $success;
+$swal2-error: $danger;
+$swal2-warning: $warning;
+$swal2-info: $info;
+$swal2-question: $secondary;
+
+// Typography & Base Styling
+$swal2-font: $font-family-base;
+$swal2-color: $gray-800;
+
+// Buttons matching UI Form Controls
+$swal2-button-padding: $input-btn-padding-y (
+ $input-btn-padding-x * 1.5
+);
+
+$swal2-confirm-button-border-radius: 0.5rem;
+$swal2-deny-button-border-radius: 0.5rem;
+$swal2-cancel-button-border-radius: 0.5rem;
+$swal2-border-radius: 0.75rem;
+
+
+// Custom Class Styles for Premium Look
+.swal2-popup {
+ padding: 1.5rem;
+ box-shadow: 0 10px 40px rgba($black, 0.08);
+ border: 1px solid rgba($gray-900, 0.05);
+
+ .swal2-title {
+ font-weight: 600;
+ font-size: 1.25rem;
+ color: $gray-900;
+ margin-bottom: 0.75rem;
+ padding-top: 0.5rem;
+ }
+
+ .swal2-html-container {
+ font-size: 0.95rem;
+ color: $gray-600;
+ line-height: 1.6;
+ margin: 0.5rem 1rem 0;
+ }
+
+ .swal2-actions {
+ gap: 0.75rem;
+ margin-top: 1.75rem;
+
+ .swal2-styled {
+ font-weight: 500;
+ box-shadow: none !important;
+ transition: all 0.2s ease;
+ margin: 0 !important;
+
+ &:hover {
+ transform: translateY(-1px);
+ filter: brightness(1.05);
+ }
+
+ &:active {
+ transform: translateY(0);
+ }
+
+ &.swal2-confirm {
+ background-color: $primary !important;
+ }
+
+ &.swal2-cancel {
+ background-color: $gray-200 !important;
+ color: $gray-800 !important;
+ border: 1px solid $border-color;
+ }
+
+ &.swal2-deny {
+ background-color: $danger !important;
+ }
+ }
+ }
+
+ .swal2-icon {
+ margin-top: 0.5rem;
+ margin-bottom: 0.5rem;
+ border-width: 2px;
+
+ &.swal2-success {
+ border-color: $success;
+
+ [class^='swal2-success-line'] {
+ background-color: $success;
+ }
+
+ .swal2-success-ring {
+ border-color: rgba($success, 0.3);
+ }
+ }
+
+ &.swal2-error {
+ border-color: $danger;
+
+ .swal2-x-mark-line-left,
+ .swal2-x-mark-line-right {
+ background-color: $danger;
+ }
+ }
+ }
+}
+
+// Toast styling
+@keyframes swal2-toast-show {
+ 0% {
+ transform: scale(0.9) translateY(-10px);
+ opacity: 0;
+ }
+ 100% {
+ transform: scale(1) translateY(0);
+ opacity: 1;
+ }
+}
+
+.swal2-toast {
+ padding: 0.75rem 1rem !important;
+ box-shadow: 0 5px 15px rgba($black, 0.08) !important;
+ border-radius: 0.5rem !important;
+
+ &.swal2-show {
+ animation: swal2-toast-show 0.4s cubic-bezier(0.215, 0.61, 0.355, 1) forwards !important;
+ }
+
+ .swal2-title {
+ font-size: 0.875rem !important;
+ margin: 0 0.5rem !important;
+ padding-top: 0 !important;
+ }
+}
diff --git a/resources/views/components/badge.blade.php b/resources/views/components/badge.blade.php
new file mode 100644
index 0000000..21c9b28
--- /dev/null
+++ b/resources/views/components/badge.blade.php
@@ -0,0 +1,23 @@
+@props([
+ 'variant' => 'info',
+ 'label' => '',
+ 'rounded' => true,
+])
+
+@php
+ $variants = [
+ 'success' => 'bg-success-subtle text-success border-success-subtle',
+ 'danger' => 'bg-danger-subtle text-danger border-danger-subtle',
+ 'warning' => 'bg-warning-subtle text-warning border-warning-subtle',
+ 'info' => 'bg-info-subtle text-info border-info-subtle',
+ 'primary' => 'bg-primary-subtle text-primary border-primary-subtle',
+ 'secondary' => 'bg-light text-secondary border-light',
+ ];
+
+ $classes = $variants[$variant] ?? $variants['info'];
+ $roundedClass = $rounded ? 'rounded-pill' : 'rounded';
+@endphp
+
+merge(['class' => "badge border {$classes} {$roundedClass} px-3 py-2 fw-medium"]) }}>
+ {{ $label ?: $slot }}
+
diff --git a/resources/views/components/breadcrumb.blade.php b/resources/views/components/breadcrumb.blade.php
new file mode 100644
index 0000000..8e4afb6
--- /dev/null
+++ b/resources/views/components/breadcrumb.blade.php
@@ -0,0 +1,19 @@
+@props(['title' => '', 'item' => []])
+
+
+
+
+
{{ $title }}
+
+
+
+ @foreach ($item as $i)
+ $loop->last])>{{ $i }}
+ @endforeach
+
+
+
+
+
+
+
diff --git a/resources/views/components/button.blade.php b/resources/views/components/button.blade.php
new file mode 100644
index 0000000..9ad9231
--- /dev/null
+++ b/resources/views/components/button.blade.php
@@ -0,0 +1,61 @@
+@props([
+ 'label' => '',
+ 'theme' => 'secondary',
+ 'rounded' => false,
+ 'size' => '',
+ 'icon' => '',
+ 'iconProperty' => [],
+ 'iconOnly' => false,
+ 'loading' => null,
+ 'type' => 'button',
+])
+
+except('wire:loading')->merge([
+ 'type' => $type,
+ 'class' =>
+ 'btn btn-' .
+ $theme .
+ ($rounded ? ' btn-rounded' : '') .
+ ($size ? ' btn-' . $size : '') .
+ ($iconOnly ? ' btn-icon' : '') .
+ ($loading ? ' d-flex align-items-center justify-content-center' : ''),
+ 'wire:loading.attr' => $attributes->has('wire:loading') ? 'disabled' : false,
+ ]) }}
+ @if ($loading) x-bind:disabled="{{ $loading }}" @endif>
+ @if ($loading)
+
+
+ @if ($icon)
+ @svg($icon, $iconProperty)
+ @endif
+ {{ $label ?: $slot }}
+
+
+
+
+ {{ __('ui.loading') }}
+
+
+ @elseif ($attributes->has('wire:loading'))
+ has('wire:target')) wire:target="{{ $attributes->get('wire:target') }}" @endif>
+
+ @if ($icon)
+ @svg($icon, $iconProperty)
+ @endif
+ {{ $label ?: $slot }}
+
+
+ has('wire:target')) wire:target="{{ $attributes->get('wire:target') }}" @endif>
+
+ {{ __('ui.loading') }}
+
+
+ @else
+ @if ($icon)
+ @svg($icon, $iconProperty)
+ @endif
+ {{ $label ?: $slot }}
+ @endif
+
diff --git a/resources/views/components/card.blade.php b/resources/views/components/card.blade.php
new file mode 100644
index 0000000..5013284
--- /dev/null
+++ b/resources/views/components/card.blade.php
@@ -0,0 +1,27 @@
+@props([
+ 'title' => null,
+ 'subtitle' => null,
+])
+
+merge(['class' => 'card']) }}>
+
+
+
+ @if ($title)
+
{{ $title }}
+ @endif
+
+ @if ($subtitle)
+ {{ $subtitle }}
+ @endif
+
+ @isset($actions)
+
+ {{ $actions }}
+
+ @endisset
+
+
+ {{ $slot }}
+
+
diff --git a/resources/views/components/datatables/action-button.blade.php b/resources/views/components/datatables/action-button.blade.php
new file mode 100644
index 0000000..bcab7be
--- /dev/null
+++ b/resources/views/components/datatables/action-button.blade.php
@@ -0,0 +1,66 @@
+@props([
+ 'log' => false,
+ 'view' => [],
+ 'edit' => [],
+ 'delete' => [],
+ 'table_name' => '',
+ 'id' => 0,
+])
+
+
+ @if($log)
+
+ @svg('tabler-logs', ['width' => 16, 'height' => 16])
+
+ @endif
+ @if (!empty($view))
+ @if (isset($view['permission']) && $view['permission'])
+ @if (isset($view['modal']))
+
+ @svg('tabler-eye', ['width' => 16, 'height' => 16])
+
+ @else
+
+ @endif
+ @endif
+ @endif
+
+ @if (!empty($edit))
+ @if (isset($edit['permission']) && $edit['permission'])
+ @if (isset($edit['modal']))
+
+ @svg('tabler-pencil', ['width' => 16, 'height' => 16])
+
+ @else
+
+ @endif
+ @endif
+ @endif
+
+ @if (!empty($delete))
+ @if (isset($delete['permission']) && $delete['permission'])
+
+ @svg('tabler-trash', ['width' => 16, 'height' => 16])
+
+ @endif
+ @endif
+
diff --git a/resources/views/components/datatables/⚡delete-action.blade.php b/resources/views/components/datatables/⚡delete-action.blade.php
new file mode 100644
index 0000000..704d049
--- /dev/null
+++ b/resources/views/components/datatables/⚡delete-action.blade.php
@@ -0,0 +1,39 @@
+action === '') {
+ app($this->model)->where($this->keyName, $id)->delete();
+ } else {
+ $model = app($this->model)->where($this->keyName, $id)->first();
+ app($this->action)->execute($model);
+ }
+ $this->dispatch('delete-data-completed');
+ } catch (Exception $e) {
+ $this->dispatch('delete-data-failed', message: $e->getMessage());
+ }
+ }
+};
+?>
+
+
+
diff --git a/resources/views/components/datatables/⚡excel-manager.blade.php b/resources/views/components/datatables/⚡excel-manager.blade.php
new file mode 100644
index 0000000..2a99b35
--- /dev/null
+++ b/resources/views/components/datatables/⚡excel-manager.blade.php
@@ -0,0 +1,101 @@
+ ['required', 'file', 'mimetypes:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet']
+ ];
+ }
+
+ public function import(): void
+ {
+ if (!$this->importClass) return;
+
+ $filePath = $this->file->store('excel/import/' . $this->resourceName, ['disk' => 'local']);
+ $recipentEmail = auth('web')->user()->email;
+ $importId = Str::uuid()->toString();
+
+ $importInstance = new $this->importClass();
+ Excel::queueImport($importInstance, $filePath)->chain([
+ new NotifyImportComplete(recipientEmail: $recipentEmail)
+ ]);
+ $this->success(__('ui.excel.import.success'));
+ $this->dispatch('hide-excel-import-modal');
+ }
+
+ #[On('export-excel')]
+ public function export(): void
+ {
+ if (!$this->exportClass) return;
+ $recipentEmail = auth('web')->user()->email;
+ $storagePath = 'excel/export/' . $this->resourceName . '_' . time() . '.xlsx';
+ $exportInstance = new $this->exportClass();
+ $styledExport = new StyledExport($exportInstance);
+
+ \Maatwebsite\Excel\Facades\Excel::queue($styledExport, $storagePath)->chain([
+ new NotifyExportReady(
+ recipientEmail: $recipentEmail,
+ filePath: $storagePath,
+ downloadName: 'Report_' . $this->resourceName,
+ )
+ ]);
+
+ $this->success(__('ui.excel.export.success'));
+ }
+
+ public function hide(): void
+ {
+ $this->reset('file');
+ $this->dispatch('filepond-reset-file');
+ $this->resetValidation();
+ $this->resetErrorBag();
+ }
+};
+?>
+
+
+ @if($importClass)
+
+
+
+
+
+
+ @push('scripts')
+ @vite(['resources/js/plugin/filepond.js'])
+ @endpush
+
+ @endif
+
diff --git a/resources/views/components/form/checkbox.blade.php b/resources/views/components/form/checkbox.blade.php
new file mode 100644
index 0000000..6dbd00b
--- /dev/null
+++ b/resources/views/components/form/checkbox.blade.php
@@ -0,0 +1,21 @@
+@props(['label' => '', 'name' => '', 'feedback' => null, 'id'])
+
+@php
+ $name = $attributes->has('wire:model') ? $attributes->get('wire:model') : $name;
+ $id = $id ?? $name;
+@endphp
+
+ merge([
+ 'class' => 'form-check-input' . ($errors->has($name) ? ' is-invalid' : ''),
+ 'id' => $id,
+ ]) }}
+ type="checkbox">
+ {{ $label }}
+ @if ($feedback)
+
+ @elseif ($errors->has($name))
+ {{ $errors->first($name) }}
+ @endif
+
diff --git a/resources/views/components/form/input.blade.php b/resources/views/components/form/input.blade.php
new file mode 100644
index 0000000..4d17bd9
--- /dev/null
+++ b/resources/views/components/form/input.blade.php
@@ -0,0 +1,29 @@
+@props([
+ 'label' => '',
+ 'name' => '',
+ 'feedback' => null,
+ 'disabled' => false
+])
+
+@php
+ $name = $attributes->has('wire:model') ? $attributes->get('wire:model') : $name;
+@endphp
+
+has('x-show')) x-show="{{ $attributes->get('x-show') }}" x-cloak @endif>
+ {{ $label }}
+ merge([
+ 'class' => 'form-control' . ($errors->has($name) ? ' is-invalid' : ''),
+ 'name' => $name,
+ 'disabled' => $disabled,
+ 'id' => $attributes->has('id') ? $attributes->get('id') : $name,
+ 'placeholder' => $attributes->has('placeholder') ? $attributes->get('placeholder') : $label,
+ 'x-bind:class' => $feedback ? "{'is-invalid': feedback?.$name}" : false,
+ ]) }} />
+ @if ($feedback)
+
+ @elseif ($errors->has($name))
+ {{ $errors->first($name) }}
+ @endif
+
diff --git a/resources/views/components/form/select.blade.php b/resources/views/components/form/select.blade.php
new file mode 100644
index 0000000..c66b19c
--- /dev/null
+++ b/resources/views/components/form/select.blade.php
@@ -0,0 +1,39 @@
+@props([
+ 'label' => null,
+ 'name' => '',
+ 'feedback' => null,
+ 'options' => [],
+ 'noLabel' => false,
+])
+
+@php
+ $name = $attributes->has('wire:model') ? $attributes->get('wire:model') : $name;
+@endphp
+
+has('x-select2')) wire:ignore @endif class="form-group" @if ($attributes->has('x-show')) x-show="{{ $attributes->get('x-show') }}" x-cloak @endif>
+ @if(!$noLabel)
+ {{ $label }}
+ @endif
+ merge([
+ 'class' => 'form-select' . ($errors->has($name) ? ' is-invalid' : ''),
+ 'name' => $name,
+ 'id' => $attributes->has('id') ? $attributes->get('id') : $name,
+ 'x-bind:class' => $feedback ? "{'is-invalid': feedback?.$name}" : false,
+ ]) }}>
+ {{ $label }}
+ @if(!empty($options))
+ @foreach($options as $key => $value)
+ {{ $value }}
+ @endforeach
+ @else
+ {{ $slot ?? '' }}
+ @endif
+
+ @if ($feedback)
+
+ @elseif ($errors->has($name))
+ {{ $errors->first($name) }}
+ @endif
+
diff --git a/resources/views/components/form/textarea.blade.php b/resources/views/components/form/textarea.blade.php
new file mode 100644
index 0000000..23d2a88
--- /dev/null
+++ b/resources/views/components/form/textarea.blade.php
@@ -0,0 +1,29 @@
+@props([
+ 'label' => '',
+ 'name' => '',
+ 'feedback' => null,
+ 'disabled' => false
+])
+
+@php
+ $name = $attributes->has('wire:model') ? $attributes->get('wire:model') : $name;
+@endphp
+
+has('x-show')) x-show="{{ $attributes->get('x-show') }}" x-cloak @endif>
+ {{ $label }}
+
+ @if ($feedback)
+
+ @elseif ($errors->has($name))
+ {{ $errors->first($name) }}
+ @endif
+
diff --git a/resources/views/components/form/vertical-group.blade.php b/resources/views/components/form/vertical-group.blade.php
new file mode 100644
index 0000000..2108e5c
--- /dev/null
+++ b/resources/views/components/form/vertical-group.blade.php
@@ -0,0 +1,22 @@
+@props([
+ 'name' => '',
+ 'label' => '',
+ 'feedback' => null,
+])
+
+merge([
+ 'class' => 'mb-3',
+ 'x-cloak' => $attributes->has('x-show')
+]) }}>
+
+ {{ $label }}
+ @isset($action) {{ $action }} @endisset
+
+ {{ $slot }}
+ @if ($feedback)
+
+ @elseif ($errors->has($name))
+
{{ $errors->first($name) }}
+ @endif
+
diff --git a/resources/views/components/layouts/app.blade.php b/resources/views/components/layouts/app.blade.php
new file mode 100644
index 0000000..48d1b1d
--- /dev/null
+++ b/resources/views/components/layouts/app.blade.php
@@ -0,0 +1,65 @@
+
+
+
+
+
+ {!! SEO::generate() !!}
+
+
+
+
+ @webmasterMeta
+ @gtmHead
+
+ @livewireStyles
+ @stack('styles')
+ @vite(['resources/js/plugin/jquery.js', 'resources/js/bootstrap.js', 'resources/js/plugin/sweetalert2.js'])
+ @stack('scripts')
+ @vite(['resources/scss/app.scss', 'resources/js/alpinejs.js'])
+
+
+
+@gtmBody
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Copyright © 2026 InApp Inventory Dashboard. Developed by CodesCandy •
+ Distributed by ThemeWagon
+
+
+
+
+@stack('page-scripts')
+@livewireScriptConfig()
+
+
+
+
diff --git a/resources/views/components/layouts/guest.blade.php b/resources/views/components/layouts/guest.blade.php
new file mode 100644
index 0000000..5d3b746
--- /dev/null
+++ b/resources/views/components/layouts/guest.blade.php
@@ -0,0 +1,51 @@
+
+
+
+
+
+ {!! SEO::generate() !!}
+
+
+
+
+
+ @webmasterMeta
+ @gtmHead
+
+ @livewireStyles
+ @stack('styles')
+ @vite(['resources/js/plugin/jquery.js', 'resources/js/bootstrap.js', 'resources/js/plugin/sweetalert2.js'])
+ @stack('scripts')
+ @vite(['resources/scss/app.scss', 'resources/js/alpinejs.js'])
+
+
+
+@gtmBody
+
+
+
+
+
+
+
+
+
{{ __($title ?? 'Laravel') }}
+
+
+
+ @if (session()->has('status'))
+
+ {{ session('status') }}
+
+ @endif
+
+ {{ $slot }}
+
+
+
+
+
+@livewireScriptConfig()
+
+
+
diff --git a/resources/views/components/layouts/nav/sidebar.blade.php b/resources/views/components/layouts/nav/sidebar.blade.php
new file mode 100644
index 0000000..f0775e6
--- /dev/null
+++ b/resources/views/components/layouts/nav/sidebar.blade.php
@@ -0,0 +1,23 @@
+
diff --git a/resources/views/components/layouts/nav/sidebar/nav-link.blade.php b/resources/views/components/layouts/nav/sidebar/nav-link.blade.php
new file mode 100644
index 0000000..405486f
--- /dev/null
+++ b/resources/views/components/layouts/nav/sidebar/nav-link.blade.php
@@ -0,0 +1,12 @@
+@props([
+ 'route' => null,
+ 'icon' => '',
+ 'text' => '',
+ 'permission' => null
+])
+
+@if((is_string($permission) && auth()->user()->hasPermissionTo($permission)))
+
+ url()->current() == $route]) />
+
+@endif
diff --git a/resources/views/components/layouts/nav/sidebar/nav-text.blade.php b/resources/views/components/layouts/nav/sidebar/nav-text.blade.php
new file mode 100644
index 0000000..8f336b3
--- /dev/null
+++ b/resources/views/components/layouts/nav/sidebar/nav-text.blade.php
@@ -0,0 +1,3 @@
+@props(['text' => ''])
+
+{{ $text }}
diff --git a/resources/views/components/layouts/nav/topbar.blade.php b/resources/views/components/layouts/nav/topbar.blade.php
new file mode 100644
index 0000000..f264d52
--- /dev/null
+++ b/resources/views/components/layouts/nav/topbar.blade.php
@@ -0,0 +1,28 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/views/components/layouts/⚡language-switcher/language-switcher.blade.php b/resources/views/components/layouts/⚡language-switcher/language-switcher.blade.php
new file mode 100644
index 0000000..140bb3a
--- /dev/null
+++ b/resources/views/components/layouts/⚡language-switcher/language-switcher.blade.php
@@ -0,0 +1,34 @@
+
diff --git a/resources/views/components/layouts/⚡language-switcher/language-switcher.php b/resources/views/components/layouts/⚡language-switcher/language-switcher.php
new file mode 100644
index 0000000..58895f2
--- /dev/null
+++ b/resources/views/components/layouts/⚡language-switcher/language-switcher.php
@@ -0,0 +1,22 @@
+currentLocale = app()->getLocale();
+ }
+
+ public function changeLocale(string $locale): void
+ {
+ if (in_array($locale, ['en', 'id'])) {
+ session()->put('locale', $locale);
+ app()->setLocale($locale);
+ $this->redirect(request()->header('Referer') ?? route('dashboard'), navigate: true);
+ }
+ }
+};
diff --git a/resources/views/components/layouts/⚡notification/notification.blade.php b/resources/views/components/layouts/⚡notification/notification.blade.php
new file mode 100644
index 0000000..e3ec065
--- /dev/null
+++ b/resources/views/components/layouts/⚡notification/notification.blade.php
@@ -0,0 +1,48 @@
+
diff --git a/resources/views/components/layouts/⚡notification/notification.php b/resources/views/components/layouts/⚡notification/notification.php
new file mode 100644
index 0000000..17c17f0
--- /dev/null
+++ b/resources/views/components/layouts/⚡notification/notification.php
@@ -0,0 +1,26 @@
+user()->unreadNotifications()->latest()->get();
+ }
+
+ public function read(string $id): void
+ {
+ $this->notifications->where('id', $id)->first()?->markAsRead();
+ $this->__unset('notifications');
+ }
+
+ public function readAll(): void
+ {
+ $this->notifications->markAsRead();
+ $this->__unset('notifications');
+ }
+};
diff --git a/resources/views/components/layouts/⚡profile-dropdown/profile-dropdown.blade.php b/resources/views/components/layouts/⚡profile-dropdown/profile-dropdown.blade.php
new file mode 100644
index 0000000..6459649
--- /dev/null
+++ b/resources/views/components/layouts/⚡profile-dropdown/profile-dropdown.blade.php
@@ -0,0 +1,59 @@
+
diff --git a/resources/views/components/layouts/⚡profile-dropdown/profile-dropdown.php b/resources/views/components/layouts/⚡profile-dropdown/profile-dropdown.php
new file mode 100644
index 0000000..9343f34
--- /dev/null
+++ b/resources/views/components/layouts/⚡profile-dropdown/profile-dropdown.php
@@ -0,0 +1,31 @@
+refresh();
+ }
+
+ #[Computed]
+ public function user(): ?User
+ {
+ return app(GetAuthenticatedUserContext::class)->fetch();
+ }
+
+ public function logout(): void
+ {
+ Auth::logout();
+ session()->invalidate();
+ session()->regenerateToken();
+ $this->redirectRoute('login', navigate: true);
+ }
+};
diff --git a/resources/views/components/link.blade.php b/resources/views/components/link.blade.php
new file mode 100644
index 0000000..c82724c
--- /dev/null
+++ b/resources/views/components/link.blade.php
@@ -0,0 +1,25 @@
+@props([
+ 'href' => 'javascript:void(0)',
+ 'label' => '',
+ 'theme' => null,
+ 'icon' => null,
+ 'iconConfig' => [
+ 'width' => 24,
+ 'height' => 24,
+ ],
+])
+merge([
+ 'class' => $theme ? 'link-' . $theme : '',
+ 'href' => $href,
+ 'wire:navigate' => !in_array($href, ['#', 'javascript:void(0)', null]),
+ ]) }}>
+ @if ($icon && !str_contains($icon, 'svg'))
+ @svg($icon, $iconConfig)
+ @endif
+ @if (is_string($label))
+ {{ $label }}
+ @else
+ attributes }}>{{ $label }}
+ @endif
+
diff --git a/resources/views/components/menu/dropdown/container.blade.php b/resources/views/components/menu/dropdown/container.blade.php
new file mode 100644
index 0000000..2913561
--- /dev/null
+++ b/resources/views/components/menu/dropdown/container.blade.php
@@ -0,0 +1,18 @@
+@props([
+ 'title' => '',
+ 'icon' => null,
+ 'iconProperty' => [],
+ 'useToggle' => false
+])
+
+ merge(['class' => 'btn' . ($useToggle ? ' dropdown-toggle' : '')]) }}
+ type="button" data-bs-toggle="dropdown" aria-expanded="false">
+ @if($icon)
+ {{ svg($icon, $iconProperty) }}
+ @endif
+ {{ __($title) }}
+
+
+
diff --git a/resources/views/components/menu/dropdown/divider.blade.php b/resources/views/components/menu/dropdown/divider.blade.php
new file mode 100644
index 0000000..d4abf1f
--- /dev/null
+++ b/resources/views/components/menu/dropdown/divider.blade.php
@@ -0,0 +1 @@
+
diff --git a/resources/views/components/menu/dropdown/item.blade.php b/resources/views/components/menu/dropdown/item.blade.php
new file mode 100644
index 0000000..b66cbe8
--- /dev/null
+++ b/resources/views/components/menu/dropdown/item.blade.php
@@ -0,0 +1,11 @@
+@props([
+ 'label' => '',
+ 'icon' => '',
+ 'action' => 'javascript:void(0)',
+ 'iconProperty' => []
+])
+
+
+ merge(['class' => 'dropdown-item']) }} href="{{ $action }}" :label="$label"
+ :icon="$icon" :icon-config="$iconProperty" />
+
diff --git a/resources/views/components/modal.blade.php b/resources/views/components/modal.blade.php
new file mode 100644
index 0000000..a64b648
--- /dev/null
+++ b/resources/views/components/modal.blade.php
@@ -0,0 +1,89 @@
+@props([
+ 'title' => '',
+ 'id' => 'modal',
+ 'size' => '',
+ 'footerSlot' => null,
+ 'header' => null,
+ 'tabindex' => -1,
+ 'form' => false,
+ 'loadingState' => false,
+ 'livewire' => false,
+])
+
+except(['wire:target', 'wire:loading'])->merge([
+ 'class' => 'modal fade',
+ 'tabindex' => $tabindex,
+ 'id' => $id,
+ 'aria-labelledby' => str($title ?? $id)->snake() . 'Label',
+ 'aria-hidden' => true,
+ 'x-ref' => $attributes->has('x-data') ? $id : false,
+ 'wire:ignore.self' => true,
+]) }}
+ @if ($livewire) x-init="$bs.modal.on('show', (e) => {
+ let id = e?.relatedTarget?.dataset?.id;
+ if (id) $wire.show(id);
+ });
+
+ $bs.modal.on('hide', () => {
+ $wire.hide();
+ });"
+ x-on:hide-{{ $id }}.window="$bs.modal.instance($el).hide();" @endif>
+
+ @if ($form)
+
+ @endif
+
+
diff --git a/resources/views/dashboard.blade.php b/resources/views/dashboard.blade.php
new file mode 100644
index 0000000..9112a92
--- /dev/null
+++ b/resources/views/dashboard.blade.php
@@ -0,0 +1,25 @@
+
+
+ @can('dashboard.admin')
+
+
+ @endcan
+
+
diff --git a/resources/views/errors/404.blade.php b/resources/views/errors/404.blade.php
new file mode 100644
index 0000000..808820d
--- /dev/null
+++ b/resources/views/errors/404.blade.php
@@ -0,0 +1,15 @@
+
+
+
+
+
{{ __('ui.errors.oops') }}
+
+ {{ __('ui.errors.404') }}
+
+
+
+
+
+
+
diff --git a/resources/views/errors/500.blade.php b/resources/views/errors/500.blade.php
new file mode 100644
index 0000000..048d672
--- /dev/null
+++ b/resources/views/errors/500.blade.php
@@ -0,0 +1,15 @@
+
+
+
+
+
{{ __('ui.errors.oops') }}
+
+ {{ __('ui.errors.500') }}
+
+
+
+
+
+
+
diff --git a/resources/views/pages/account/profile/index.blade.php b/resources/views/pages/account/profile/index.blade.php
new file mode 100644
index 0000000..73b31d1
--- /dev/null
+++ b/resources/views/pages/account/profile/index.blade.php
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
diff --git a/resources/views/pages/account/profile/⚡update-avatar-modal/update-avatar-modal.blade.php b/resources/views/pages/account/profile/⚡update-avatar-modal/update-avatar-modal.blade.php
new file mode 100644
index 0000000..4462e8b
--- /dev/null
+++ b/resources/views/pages/account/profile/⚡update-avatar-modal/update-avatar-modal.blade.php
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+ @push('scripts')
+ @vite('resources/js/plugin/filepond.js')
+ @endpush
+
diff --git a/resources/views/pages/account/profile/⚡update-avatar-modal/update-avatar-modal.php b/resources/views/pages/account/profile/⚡update-avatar-modal/update-avatar-modal.php
new file mode 100644
index 0000000..d58d277
--- /dev/null
+++ b/resources/views/pages/account/profile/⚡update-avatar-modal/update-avatar-modal.php
@@ -0,0 +1,34 @@
+validate();
+ $updateUserAvatar->execute(auth('web')->user(), $this->file);
+ $this->dispatch('hide-update-avatar-modal');
+ $this->dispatch('profile-updated');
+ }
+
+ public function hide(): void
+ {
+ $this->resetValidation();
+ $this->dispatch('filepond-reset-file');
+ }
+};
diff --git a/resources/views/pages/account/profile/⚡update-password-modal/update-password-modal.blade.php b/resources/views/pages/account/profile/⚡update-password-modal/update-password-modal.blade.php
new file mode 100644
index 0000000..e9b97e6
--- /dev/null
+++ b/resources/views/pages/account/profile/⚡update-password-modal/update-password-modal.blade.php
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/views/pages/account/profile/⚡update-password-modal/update-password-modal.php b/resources/views/pages/account/profile/⚡update-password-modal/update-password-modal.php
new file mode 100644
index 0000000..638b201
--- /dev/null
+++ b/resources/views/pages/account/profile/⚡update-password-modal/update-password-modal.php
@@ -0,0 +1,40 @@
+form->validate();
+
+ $action->execute(auth('web')->user(), new UpdatePasswordDTO(
+ new_password: $this->form->new_password,
+ ));
+
+ $this->dispatch('hide-update-password-modal');
+ $this->success($this->message);
+ }
+
+ public function hide(): void
+ {
+ $this->form->reset();
+ $this->form->resetValidation();
+ }
+};
diff --git a/resources/views/pages/account/profile/⚡update-profile-modal/update-profile-modal.blade.php b/resources/views/pages/account/profile/⚡update-profile-modal/update-profile-modal.blade.php
new file mode 100644
index 0000000..4bbe8a6
--- /dev/null
+++ b/resources/views/pages/account/profile/⚡update-profile-modal/update-profile-modal.blade.php
@@ -0,0 +1,26 @@
+@php use App\Domains\Account\Enums\GenderOption; @endphp
+
+
+
+
+
+
+
+
+ @foreach (GenderOption::cases() as $case)
+ {{ $case->label() }}
+ @endforeach
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/views/pages/account/profile/⚡update-profile-modal/update-profile-modal.php b/resources/views/pages/account/profile/⚡update-profile-modal/update-profile-modal.php
new file mode 100644
index 0000000..45cd0e7
--- /dev/null
+++ b/resources/views/pages/account/profile/⚡update-profile-modal/update-profile-modal.php
@@ -0,0 +1,74 @@
+user()->load(['profile']);
+ }
+
+ public function show(int|string $id): void
+ {
+ $profile = $this->user->profile;
+ $this->form->fill($this->user->only(['name', 'email']));
+ if ($profile) {
+ $this->form->fill($profile->only(['gender', 'phone_number']));
+ $this->form->date_of_birth = $profile->date_of_birth?->format('Y-m-d');
+ }
+ $this->id = $this->user->id;
+ }
+
+ public function save(UpdateProfile $updateProfile, UpdateUserIdentity $updateUser): void
+ {
+ $this->form->validate($this->form->rules($this->id));
+
+ $updateUser->execute($this->user, new UpdateUserIdentityDTO(
+ name: $this->form->name,
+ email: $this->form->email,
+ ));
+
+ $updateProfile->execute(new UpdateProfileDTO(
+ userId: $this->id,
+ gender: $this->form->gender,
+ dateOfBirth: $this->form->date_of_birth,
+ phoneNumber: $this->form->phone_number,
+ ));
+
+ $this->success($this->message);
+ $this->dispatch('hide-update-profile-modal');
+ $this->dispatch('profile-updated');
+ }
+
+ public function hide(): void
+ {
+ $this->form->reset();
+ $this->reset('id');
+ }
+};
diff --git a/resources/views/pages/account/profile/⚡user-info/user-info.blade.php b/resources/views/pages/account/profile/⚡user-info/user-info.blade.php
new file mode 100644
index 0000000..78c775a
--- /dev/null
+++ b/resources/views/pages/account/profile/⚡user-info/user-info.blade.php
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+ @if($this->user->avatar)
+
+ @else
+
+ @endif
+
+
+
+
+
{{ __('domains/identity/field.user.name') }}
+
{{ $this->user->name }}
+
{{ __('domains/identity/field.user.email') }}
+
+ {{ $this->user->email }}
+ @if ($this->user->email_verified_at)
+ {{ __('domains/identity/field.user.verified') }}
+ @else
+ {{ __('domains/identity/field.user.unverified') }}
+ @endif
+
+
{{ __('domains/account/field.profile.gender') }}
+
{{ $this->user->profile?->gender?->label() }}
+
{{ __('domains/account/field.profile.date_of_birth') }}
+
+ {{ $this->user->profile?->date_of_birth->format('d/m/Y') }}
+
+
{{ __('domains/account/field.profile.phone_number') }}
+
{{ $this->user->profile?->phone_number }}
+
+
+
+
diff --git a/resources/views/pages/account/profile/⚡user-info/user-info.php b/resources/views/pages/account/profile/⚡user-info/user-info.php
new file mode 100644
index 0000000..f397e3c
--- /dev/null
+++ b/resources/views/pages/account/profile/⚡user-info/user-info.php
@@ -0,0 +1,39 @@
+refresh();
+ }
+
+ #[Computed]
+ public function user(): ?User
+ {
+ return app(GetAuthenticatedUserContext::class)->fetch();
+ }
+
+ public function resendVerificationEmail(ResendVerificationEmail $action): void
+ {
+ if ($this->user->email_verified_at) {
+ $this->warning(__('The email has already been verified.'));
+
+ return;
+ }
+
+ $action->execute($this->user);
+
+ $this->success(__('The verification email has been sent.'));
+ }
+};
diff --git a/resources/views/pages/account/profile/⚡user-settings/user-settings.blade.php b/resources/views/pages/account/profile/⚡user-settings/user-settings.blade.php
new file mode 100644
index 0000000..3336e66
--- /dev/null
+++ b/resources/views/pages/account/profile/⚡user-settings/user-settings.blade.php
@@ -0,0 +1,25 @@
+
+
+
+
+
+ @foreach ($settings as $setting)
+
+
+
{{ $setting['label'] }}
+
+ @foreach ($setting['options'] as $key => $value)
+
+ {{ $value }}
+ @endforeach
+
+
+
+ @endforeach
+
+
diff --git a/resources/views/pages/account/profile/⚡user-settings/user-settings.php b/resources/views/pages/account/profile/⚡user-settings/user-settings.php
new file mode 100644
index 0000000..013bd77
--- /dev/null
+++ b/resources/views/pages/account/profile/⚡user-settings/user-settings.php
@@ -0,0 +1,48 @@
+user()->settings ?? collect();
+
+ $this->settings = collect(UserSettingKey::cases())->map(function ($key) {
+ return [
+ 'key' => $key->value,
+ 'label' => $key->label(),
+ 'type' => $key->type(),
+ 'options' => $key->options(),
+ ];
+ })->toArray();
+
+ foreach (UserSettingKey::cases() as $key) {
+ $this->form[$key->value] = $userSettings->get($key->value, $key->default());
+ }
+ }
+
+ public function save(UpdateUserSettings $action): void
+ {
+ $rules = [];
+ foreach (UserSettingKey::cases() as $key) {
+ if ($key->validation()) {
+ $rules["form.{$key->value}"] = $key->validation();
+ }
+ }
+
+ $this->validate($rules);
+
+ $action->execute(auth('web')->user(), $this->form);
+ $this->success(__('ui.crud.success.updated', ['resource' => __('domains/account/pages.user_settings.title')]));
+ }
+};
diff --git a/resources/views/pages/auth/⚡confirm-password/confirm-password.blade.php b/resources/views/pages/auth/⚡confirm-password/confirm-password.blade.php
new file mode 100644
index 0000000..7050b7a
--- /dev/null
+++ b/resources/views/pages/auth/⚡confirm-password/confirm-password.blade.php
@@ -0,0 +1,13 @@
+
+
+ {{ __('domains/auth/pages.confirm_password.subheader') }}
+
+
+
+
diff --git a/resources/views/pages/auth/⚡confirm-password/confirm-password.php b/resources/views/pages/auth/⚡confirm-password/confirm-password.php
new file mode 100644
index 0000000..a76edb9
--- /dev/null
+++ b/resources/views/pages/auth/⚡confirm-password/confirm-password.php
@@ -0,0 +1,25 @@
+ 'domains/auth/pages.confirm_password.header'])]
+#[Seo(title: 'domains/auth/seo.confirm_password.title', description: 'domains/auth/seo.confirm_password.description', keywords: 'domains/auth/seo.confirm_password.keywords')]
+class extends Component
+{
+ use HasSeoAttributes;
+
+ public ConfirmPasswordForm $form;
+
+ public function confirmPassword(): void
+ {
+ $this->form->validate();
+
+ session()->put('auth.password_confirmed_at', time());
+
+ $this->redirectIntended(route('dashboard', absolute: false), navigate: true);
+ }
+};
diff --git a/resources/views/pages/auth/⚡forgot-password/forgot-password.blade.php b/resources/views/pages/auth/⚡forgot-password/forgot-password.blade.php
new file mode 100644
index 0000000..04cfb80
--- /dev/null
+++ b/resources/views/pages/auth/⚡forgot-password/forgot-password.blade.php
@@ -0,0 +1,20 @@
+
+
+ {{ __('domains/auth/pages.forgot_password.subheader') }}
+
+
+
+ {{--
--}}
+
+
+
+
+ {{ __('domains/auth/pages.forgot_password.back_to_login') }}
+
+
diff --git a/resources/views/pages/auth/⚡forgot-password/forgot-password.php b/resources/views/pages/auth/⚡forgot-password/forgot-password.php
new file mode 100644
index 0000000..2b0f115
--- /dev/null
+++ b/resources/views/pages/auth/⚡forgot-password/forgot-password.php
@@ -0,0 +1,35 @@
+ 'domains/auth/pages.forgot_password.header'])]
+#[Seo(title: 'domains/auth/seo.forgot_password.title', description: 'domains/auth/seo.forgot_password.description', keywords: 'domains/auth/seo.forgot_password.keywords')]
+class extends Component
+{
+ use HasSeoAttributes, WithToast;
+
+ public ForgotPasswordForm $form;
+
+ public function forgotPassword(SendPasswordResetLink $action): void
+ {
+ $this->form->validate();
+
+ $status = $action->execute(new ForgotPasswordDTO(
+ email: $this->form->email,
+ ));
+
+ if ($status === Password::RESET_LINK_SENT) {
+ $this->success(__('domains/auth/messages.reset_link_sent'));
+ } else {
+ $this->warning(__('domains/auth/messages.reset_link_failed'));
+ }
+ }
+};
diff --git a/resources/views/pages/auth/⚡login/login.blade.php b/resources/views/pages/auth/⚡login/login.blade.php
new file mode 100644
index 0000000..34cb4fd
--- /dev/null
+++ b/resources/views/pages/auth/⚡login/login.blade.php
@@ -0,0 +1,21 @@
+
+
+
+
+ {{ __('domains/auth/pages.login.no_account') }}
+
+
diff --git a/resources/views/pages/auth/⚡login/login.php b/resources/views/pages/auth/⚡login/login.php
new file mode 100644
index 0000000..268e262
--- /dev/null
+++ b/resources/views/pages/auth/⚡login/login.php
@@ -0,0 +1,91 @@
+ 'domains/auth/pages.login.header'])]
+#[Seo(title: 'domains/auth/seo.login.title', description: 'domains/auth/seo.login.description', keywords: 'domains/auth/seo.login.keywords')]
+class extends Component
+{
+ use HasSeoAttributes;
+
+ public LoginForm $form;
+
+ /**
+ * @throws ValidationException
+ */
+ public function login(): void
+ {
+ $this->form->validate();
+
+ $this->ensureIsNotRateLimited();
+
+ if (! Auth::attempt(
+ ['email' => $this->form->email, 'password' => $this->form->password],
+ $this->form->remember,
+ )) {
+ RateLimiter::hit($this->throttleKey());
+
+ throw ValidationException::withMessages([
+ 'form.email' => trans('auth.failed'),
+ ]);
+ }
+
+ $user = Auth::user();
+ if (! $user->status->isActive()) {
+ Auth::logout();
+ throw ValidationException::withMessages([
+ 'form.email' => trans('auth.inactive'),
+ ]);
+ }
+
+ RateLimiter::clear($this->throttleKey());
+
+ // Regenerate the session to prevent fixation attacks.
+ session()->regenerate();
+
+ // Extract HTTP primitives here — never pass request() into an Event.
+ $ipAddress = request()->ip() ?? 'Unknown';
+ $userAgent = request()->userAgent() ?? 'Unknown';
+
+ // Dispatch the event — listeners handle all side-effects asynchronously.
+ UserLoggedIn::dispatch(Auth::user(), $ipAddress, $userAgent);
+
+ $this->redirectIntended(route('dashboard', absolute: false), navigate: true);
+ }
+
+ /**
+ * @throws ValidationException
+ */
+ private function ensureIsNotRateLimited(): void
+ {
+ if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
+ return;
+ }
+
+ event(new Lockout(request()));
+
+ $seconds = RateLimiter::availableIn($this->throttleKey());
+
+ throw ValidationException::withMessages([
+ 'form.email' => trans('auth.throttle', [
+ 'seconds' => $seconds,
+ 'minutes' => ceil($seconds / 60),
+ ]),
+ ]);
+ }
+
+ private function throttleKey(): string
+ {
+ return Str::transliterate(Str::lower($this->form->email).'|'.request()->ip());
+ }
+};
diff --git a/resources/views/pages/auth/⚡register/register.blade.php b/resources/views/pages/auth/⚡register/register.blade.php
new file mode 100644
index 0000000..9ace86d
--- /dev/null
+++ b/resources/views/pages/auth/⚡register/register.blade.php
@@ -0,0 +1,23 @@
+
+
+
+ {{ __('domains/auth/pages.register.has_account') }}
+
+
diff --git a/resources/views/pages/auth/⚡register/register.php b/resources/views/pages/auth/⚡register/register.php
new file mode 100644
index 0000000..261af02
--- /dev/null
+++ b/resources/views/pages/auth/⚡register/register.php
@@ -0,0 +1,36 @@
+ 'domains/auth/pages.register.header'])]
+#[Seo(title: 'domains/auth/seo.register.title', description: 'domains/auth/seo.register.description', keywords: 'domains/auth/seo.register.keywords')]
+class extends Component
+{
+ use HasSeoAttributes;
+
+ public RegisterForm $form;
+
+ public function register(RegisterSelfServiceUser $action): void
+ {
+ $this->form->validate();
+
+ // The Identity Domain creates the user and fires the Registered event.
+ $user = $action->execute(new RegisterSelfServiceUserDTO(
+ name: $this->form->name,
+ email: $this->form->email,
+ password: $this->form->password,
+ ));
+
+ // The Gateway owns the session — Auth::login() lives here, not in the domain.
+ Auth::login($user);
+
+ $this->redirectRoute('dashboard', absolute: false, navigate: true);
+ }
+};
diff --git a/resources/views/pages/auth/⚡reset-password/reset-password.blade.php b/resources/views/pages/auth/⚡reset-password/reset-password.blade.php
new file mode 100644
index 0000000..81bc970
--- /dev/null
+++ b/resources/views/pages/auth/⚡reset-password/reset-password.blade.php
@@ -0,0 +1,14 @@
+
+
+
diff --git a/resources/views/pages/auth/⚡reset-password/reset-password.php b/resources/views/pages/auth/⚡reset-password/reset-password.php
new file mode 100644
index 0000000..7893c29
--- /dev/null
+++ b/resources/views/pages/auth/⚡reset-password/reset-password.php
@@ -0,0 +1,43 @@
+ 'domains/auth/pages.reset_password.header'])]
+#[Seo(title: 'domains/auth/seo.reset_password.title', description: 'domains/auth/seo.reset_password.description', keywords: 'domains/auth/seo.reset_password.keywords')]
+class extends Component
+{
+ use HasSeoAttributes;
+
+ public ResetPasswordForm $form;
+
+ public function mount(string $token): void
+ {
+ $this->form->token = $token;
+ }
+
+ public function resetPassword(ResetUserPassword $action): void
+ {
+ $this->form->validate();
+
+ $status = $action->execute(new ResetPasswordDTO(
+ token: $this->form->token,
+ email: $this->form->email,
+ password: $this->form->password,
+ password_confirmation: $this->form->password_confirmation,
+ ));
+
+ if ($status === Password::PASSWORD_RESET) {
+ session()->flash('status', __('domains/auth/messages.password_reset'));
+ $this->redirectRoute('login');
+ } else {
+ $this->addError('form.email', __('domains/auth/messages.invalid_token'));
+ }
+ }
+};
diff --git a/resources/views/pages/auth/⚡verify-email/verify-email.blade.php b/resources/views/pages/auth/⚡verify-email/verify-email.blade.php
new file mode 100644
index 0000000..eaab76b
--- /dev/null
+++ b/resources/views/pages/auth/⚡verify-email/verify-email.blade.php
@@ -0,0 +1,19 @@
+
+
+ {{ __('domains/auth/pages.verify_email.subheader') }}
+
+
+
+
+
+
+
+
diff --git a/resources/views/pages/auth/⚡verify-email/verify-email.php b/resources/views/pages/auth/⚡verify-email/verify-email.php
new file mode 100644
index 0000000..42f8c62
--- /dev/null
+++ b/resources/views/pages/auth/⚡verify-email/verify-email.php
@@ -0,0 +1,47 @@
+ 'domains/auth/pages.verify_email.header'])]
+#[Seo(title: 'domains/auth/seo.verify_email.title', description: 'domains/auth/seo.verify_email.description', keywords: 'domains/auth/seo.verify_email.keywords')]
+class extends Component
+{
+ use HasSeoAttributes, WithToast;
+
+ public function mount(): void
+ {
+ $this->checkVerified();
+ }
+
+ public function checkVerified(): void
+ {
+ if (request()->user()->hasVerifiedEmail()) {
+ $this->redirectRoute('dashboard');
+
+ return;
+ }
+ }
+
+ public function resendEmail(ResendVerificationEmail $action): void
+ {
+ $this->checkVerified();
+
+ // The Gateway resolves the User from the session and passes it
+ // explicitly — the domain action has no HTTP/request dependency.
+ $action->execute(Auth::user());
+
+ $this->success(__('domains/auth/pages.verify_email.resend_link'));
+ }
+
+ public function logout(): void
+ {
+ Auth::logout();
+ $this->redirectRoute('login', navigate: true);
+ }
+};
diff --git a/resources/views/pages/identity/roles/index.blade.php b/resources/views/pages/identity/roles/index.blade.php
new file mode 100644
index 0000000..7c5b5b9
--- /dev/null
+++ b/resources/views/pages/identity/roles/index.blade.php
@@ -0,0 +1,26 @@
+
+
+ {{ $dataTable->table() }}
+
+
+ @canany(['role.create', 'role.update'])
+
+ @endcanany
+
+ @can('role.view')
+
+ @endcan
+
+
+
+ @can('role.delete')
+
+ @endcan
+
+ @push('page-scripts')
+ @vite('resources/js/plugin/datatables.js')
+ {{ $dataTable->scripts(attributes: ['type' => 'module']) }}
+ @endpush
+
diff --git a/resources/views/pages/identity/roles/⚡form-modal/form-modal.blade.php b/resources/views/pages/identity/roles/⚡form-modal/form-modal.blade.php
new file mode 100644
index 0000000..570111a
--- /dev/null
+++ b/resources/views/pages/identity/roles/⚡form-modal/form-modal.blade.php
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+ Web
+ Api
+
+
+
+
+
+
+
+
{{ __('domains/identity/field.role.permissions') }}
+
+ @foreach ($this->permissions->groupBy('group') as $group)
+
+
+
+ {{ $group->first()->group }}
+
+
+ @foreach ($group as $permission)
+
+
+
+ @endforeach
+
+
+
+ @endforeach
+
+
+
+
+
+
+
+
diff --git a/resources/views/pages/identity/roles/⚡form-modal/form-modal.php b/resources/views/pages/identity/roles/⚡form-modal/form-modal.php
new file mode 100644
index 0000000..6d5f936
--- /dev/null
+++ b/resources/views/pages/identity/roles/⚡form-modal/form-modal.php
@@ -0,0 +1,77 @@
+form->validate();
+
+ if ($this->mode === 'create') {
+ $create->execute(new CreateRoleDTO(
+ name: $this->form->name,
+ guard_name: $this->form->guard_name,
+ permissions: $this->form->selected_permissions
+ ));
+ } elseif ($this->mode === 'update') {
+ $update->execute(Role::findById($this->id), new UpdateRoleDTO(
+ permissions: $this->form->selected_permissions,
+ ));
+ }
+
+ $this->success($this->message);
+ $this->dispatch('hide-role-form-modal');
+ $this->js("LaravelDataTables['role-table'].ajax.reload(null, false)");
+ $this->form->reset();
+ $this->form->resetValidation();
+ $this->reset('id', 'mode');
+ }
+
+ public function show(int|string $id): void
+ {
+ $this->id = $id;
+ $this->mode = 'update';
+ $role = Role::with(['permissions' => fn ($q) => $q->select('name')])->findOrFail($id);
+ $this->form->fill($role->only(['name', 'guard_name']));
+ $this->form->selected_permissions = $role->permissions->pluck('name')->toArray();
+ }
+
+ public function hide(): void
+ {
+ $this->form->reset();
+ $this->form->resetValidation();
+ $this->reset('id', 'mode');
+ }
+};
diff --git a/resources/views/pages/identity/roles/⚡view-modal/view-modal.blade.php b/resources/views/pages/identity/roles/⚡view-modal/view-modal.blade.php
new file mode 100644
index 0000000..f6d2c34
--- /dev/null
+++ b/resources/views/pages/identity/roles/⚡view-modal/view-modal.blade.php
@@ -0,0 +1,40 @@
+
+
+
+
+
+ {{ __('domains/identity/field.role.name') }}
+
{{ $this->role->name }}
+
+
+ {{ __('domains/identity/field.role.guard_name') }}
+
{{ $this->role->guard_name }}
+
+
+
+
+
+ @foreach ($this->role->permissions->groupBy('group') as $group)
+
+
+
+ {{ $group->first()->group }}
+
+
+ @foreach ($group as $permission)
+
+
+
+
+ {{ __($permission->description) }}
+
+ @endforeach
+
+
+
+ @endforeach
+
+
+ {{ __('ui.button.close') }}
+
+
diff --git a/resources/views/pages/identity/roles/⚡view-modal/view-modal.php b/resources/views/pages/identity/roles/⚡view-modal/view-modal.php
new file mode 100644
index 0000000..94c2fe7
--- /dev/null
+++ b/resources/views/pages/identity/roles/⚡view-modal/view-modal.php
@@ -0,0 +1,37 @@
+id ? Role::with('permissions')->findOrFail($this->id) : new Role;
+ }
+
+ public function show(int|string $id): void
+ {
+ $this->id = $id;
+ }
+
+ public function hide(): void
+ {
+ $this->reset();
+ }
+};
diff --git a/resources/views/pages/identity/users/index.blade.php b/resources/views/pages/identity/users/index.blade.php
new file mode 100644
index 0000000..4b2c26a
--- /dev/null
+++ b/resources/views/pages/identity/users/index.blade.php
@@ -0,0 +1,45 @@
+
+
+ {{ $dataTable->table() }}
+
+
+
+
+
+
+
+ @push('page-scripts')
+
+ @vite(['resources/js/plugin/datatables.js', 'resources/js/plugin/select2.js'])
+ {{ $dataTable->scripts(attributes: ['type' => 'module']) }}
+ @endpush
+
diff --git a/resources/views/pages/identity/users/⚡detail-view/detail-view.blade.php b/resources/views/pages/identity/users/⚡detail-view/detail-view.blade.php
new file mode 100644
index 0000000..7ce3e78
--- /dev/null
+++ b/resources/views/pages/identity/users/⚡detail-view/detail-view.blade.php
@@ -0,0 +1,84 @@
+@use(App\Domains\Identity\Enums\RoleType)
+
+
+
+
+
+
+ @if(!$this->user->hasRole([RoleType::SYSTEM_ADMIN, RoleType::ADMIN]))
+
+
+
+
+
+
+ @endif
+
+
+
+
+ {{ __('domains/identity/field.user.email') }}
+
+
+ {{ $this->user->email }}
+
+
+
+ {{ __('domains/identity/field.role.name') }}
+
+
+ {{ $this->user->role_name }}
+
+
+
+
+
+
+
+
+ {{ __('domains/identity/field.user.name') }}
+
+
+ {{ $this->user->name }}
+
+
+ {{ __('domains/account/field.profile.phone_number') }}
+
+
+ {{ $this->user->profile?->phone_number }}
+
+
+ {{ __('domains/account/field.profile.gender') }}
+
+
+ {{ $this->user->profile?->gender?->label() }}
+
+
+
+ {{ __('domains/account/field.profile.date_of_birth') }}
+
+
+ {{ $this->user->profile?->date_of_birth?->format('d/m/Y') }}
+
+
+
+
+
+
+
diff --git a/resources/views/pages/identity/users/⚡detail-view/detail-view.php b/resources/views/pages/identity/users/⚡detail-view/detail-view.php
new file mode 100644
index 0000000..d23fea8
--- /dev/null
+++ b/resources/views/pages/identity/users/⚡detail-view/detail-view.php
@@ -0,0 +1,76 @@
+ 'dashboard', 'domains/identity/seo.user.title' => 'users.index', '{name}' => ''], context: 'user')]
+#[Seo(title: 'domains/identity/seo.user_detail.title', context: 'user')]
+class extends Component
+{
+ use HasLayoutDataAttributes;
+ use HasSeoAttributes;
+ use WithToast;
+
+ #[Locked]
+ public ?string $id = null;
+
+ public function mount(string $user_id): void
+ {
+ $this->id = $user_id;
+ }
+
+ #[On('refresh-user-data')]
+ #[Computed]
+ public function user(): ?User
+ {
+ return $this->id ? User::with('profile')
+ ->where('ulid', $this->id)
+ ->first() : new User;
+ }
+
+ #[On('send-password-reset')]
+ public function sendResetPassword(SendPasswordResetLink $sendLink): void
+ {
+ try {
+ $sendLink->execute(new ForgotPasswordDTO(
+ email: $this->user->email,
+ ));
+
+ $this->dispatch('send-password-reset-completed');
+ $this->success(__('domains/identity/messages.success.password_reset_link_sent'));
+ } catch (Exception $e) {
+ $this->dispatch('send-password-reset-failed', message: $e->getMessage());
+ }
+ }
+
+ #[On('toggle-user-status')]
+ public function toggleStatus(SuspendUser $suspendUser, ActivateUserStatus $activateUserStatus): void
+ {
+ try {
+ if ($this->user->status->isActive()) {
+ $suspendUser->execute($this->user);
+ } else {
+ $activateUserStatus->execute($this->user);
+ }
+
+ $this->dispatch('toggle-user-status-completed');
+ $this->success(__('domains/identity/messages.success.user_status_updated'));
+ } catch (Exception $e) {
+ $this->dispatch('toggle-user-status-failed', message: $e->getMessage());
+ }
+ }
+};
diff --git a/resources/views/pages/identity/users/⚡form-modal/form-modal.blade.php b/resources/views/pages/identity/users/⚡form-modal/form-modal.blade.php
new file mode 100644
index 0000000..38d1485
--- /dev/null
+++ b/resources/views/pages/identity/users/⚡form-modal/form-modal.blade.php
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+ @unless ($id)
+
+
+
+ @endunless
+
+
+
+
+
+
+
diff --git a/resources/views/pages/identity/users/⚡form-modal/form-modal.php b/resources/views/pages/identity/users/⚡form-modal/form-modal.php
new file mode 100644
index 0000000..c51035a
--- /dev/null
+++ b/resources/views/pages/identity/users/⚡form-modal/form-modal.php
@@ -0,0 +1,73 @@
+form->validate($this->form->rules($this->id ?? 0, $this->mode === 'update'));
+
+ if ($this->mode === 'create') {
+ $create->execute(new ProvisionUserDTO(
+ name: $this->form->name,
+ email: $this->form->email,
+ password: $this->form->password,
+ role: $this->form->role_name,
+ ));
+ } elseif ($this->mode === 'update') {
+ $update->execute($this->user, new UpdateUserIdentityDTO(
+ name: $this->form->name,
+ email: $this->form->email,
+ ));
+ }
+
+ $this->success($this->message);
+ $this->dispatch('hide-user-form-modal');
+ $this->js("LaravelDataTables['user-table'].ajax.reload(null, false)");
+ }
+
+ #[Computed]
+ public function user(): ?User
+ {
+ return $this->id ? User::where('ulid', $this->id)->first() : null;
+ }
+
+ public function show(int|string $id): void
+ {
+ $this->id = $id;
+ $this->mode = 'update';
+ $this->form->fill($this->user->only(['name', 'email']));
+ $this->form->role_name = $this->user->role_name;
+ }
+
+ public function hide(): void
+ {
+ $this->form->reset();
+ $this->form->resetValidation();
+ $this->reset('id', 'mode');
+ }
+};
diff --git a/resources/views/pages/identity/users/⚡role-selection-modal/role-selection-modal.blade.php b/resources/views/pages/identity/users/⚡role-selection-modal/role-selection-modal.blade.php
new file mode 100644
index 0000000..932dba0
--- /dev/null
+++ b/resources/views/pages/identity/users/⚡role-selection-modal/role-selection-modal.blade.php
@@ -0,0 +1,19 @@
+
+
+
+ @foreach ($this->roles as $role)
+ {{ $role }}
+ @endforeach
+
+
+
+
+
+
+
+ @push('scripts')
+ @vite(['resources/js/plugin/select2.js'])
+ @endpush
+
diff --git a/resources/views/pages/identity/users/⚡role-selection-modal/role-selection-modal.php b/resources/views/pages/identity/users/⚡role-selection-modal/role-selection-modal.php
new file mode 100644
index 0000000..f998aa0
--- /dev/null
+++ b/resources/views/pages/identity/users/⚡role-selection-modal/role-selection-modal.php
@@ -0,0 +1,75 @@
+ ['required', 'string'],
+ ];
+ }
+
+ public function mount(): void
+ {
+ $this->role = $this->user?->role_name;
+ }
+
+ #[Computed]
+ public function user(): User
+ {
+ return $this->id ? User::where('ulid', $this->id)->first() : new User;
+ }
+
+ #[Computed]
+ public function roles(): array
+ {
+ return Role::select(['name'])
+ ->get()
+ ->pluck('name', 'name')
+ ->toArray();
+ }
+
+ public function save(UpdateUserRole $updateUserRole): void
+ {
+ try {
+ $this->validate();
+ $updateUserRole->execute($this->user, [$this->role]);
+ $this->dispatch('hide-role-selection-modal');
+ $this->dispatch('refresh-user-data');
+ $this->success(__('domains/identity/messages.success.user_role_updated'));
+ } catch (Exception $e) {
+ $this->warning($e->getMessage());
+ }
+ }
+
+ public function hide(): void
+ {
+ $this->resetValidation();
+ $this->resetErrorBag();
+ $this->dispatch('role-clear');
+ }
+};
diff --git a/resources/views/pages/system/audit/⚡audit-view-modal/audit-view-modal.blade.php b/resources/views/pages/system/audit/⚡audit-view-modal/audit-view-modal.blade.php
new file mode 100644
index 0000000..2c3fd37
--- /dev/null
+++ b/resources/views/pages/system/audit/⚡audit-view-modal/audit-view-modal.blade.php
@@ -0,0 +1,58 @@
+
+
+ @forelse($this->audit as $audit)
+
+
+
+
+
+
+
{{ __('resources.user') }}
+
{{ $audit->user->name }}
+
{{ __('domains/system/field.audit.ip_address') }}
+
{{ $audit->ip_address }}
+
{{ __('domains/system/field.audit.browser') }}
+
{{ $audit->user_agent }}
+
+
+
+
+ {{ __('domains/system/field.audit.field') }}
+ {{ __('domains/system/field.audit.old') }}
+ {{ __('domains/system/field.audit.new') }}
+
+
+
+ @foreach($audit->old_values as $key => $value)
+
+ {{ __($translation.$key) }}
+ {{ $value }}
+ {{ $audit->new_values[$key] ?? '-' }}
+
+ @endforeach
+
+
+
+
+
+
+
+
+ @empty
+
+ {{ __('ui.label.no_data') }}
+
+ @endforelse
+
+
+
+
+
diff --git a/resources/views/pages/system/audit/⚡audit-view-modal/audit-view-modal.php b/resources/views/pages/system/audit/⚡audit-view-modal/audit-view-modal.php
new file mode 100644
index 0000000..f63d899
--- /dev/null
+++ b/resources/views/pages/system/audit/⚡audit-view-modal/audit-view-modal.php
@@ -0,0 +1,51 @@
+model_id = $id;
+ }
+
+ #[Computed]
+ public function audit(): Collection
+ {
+ if (is_null($this->model_id)) {
+ return new Collection;
+ }
+ $model = app($this->model)->where($this->keyName, $this->model_id)->first();
+
+ return app(GetModelAuditLog::class)->get($model);
+ }
+
+ public function hide(): void
+ {
+ $this->model_id = null;
+ }
+};
diff --git a/resources/views/pages/system/backups/⚡backup-list/backup-list.blade.php b/resources/views/pages/system/backups/⚡backup-list/backup-list.blade.php
new file mode 100644
index 0000000..6a66cdb
--- /dev/null
+++ b/resources/views/pages/system/backups/⚡backup-list/backup-list.blade.php
@@ -0,0 +1,61 @@
+
+
+
+
+
+
+
+ @forelse($this->backups_data as $backup)
+
+
+
{{ $backup->file_name }}
+
+
+
+
+
+
+ {{ $backup->type }} ({{ $backup->size }} )
+
+ @empty
+
+
+
+
{{ __('domains/system/pages.backup.empty_state') }}
+
+
+ @endforelse
+
+
+
+
diff --git a/resources/views/pages/system/backups/⚡backup-list/backup-list.php b/resources/views/pages/system/backups/⚡backup-list/backup-list.php
new file mode 100644
index 0000000..60700b6
--- /dev/null
+++ b/resources/views/pages/system/backups/⚡backup-list/backup-list.php
@@ -0,0 +1,74 @@
+ 'dashboard', 'domains/system/seo.backup.title' => ''], context: 'user')]
+#[Seo(title: 'domains/system/seo.backup.title', description: 'domains/system/seo.backup.description', keywords: 'domains/system/seo.backup.keywords')]
+class extends Component
+{
+ use HasLayoutDataAttributes;
+ use HasSeoAttributes;
+ use WithToast;
+
+ #[On('reload-backup-data')]
+ #[Computed]
+ public function backupsData(): Collection|array
+ {
+ return Backup::all();
+ }
+
+ public function backup(SystemBackup $backup): void
+ {
+ try {
+ $backup->execute();
+ $this->success(__('domains/system/messages.backup.backup_success'));
+ } catch (Exception $exception) {
+ logger($exception->getMessage());
+ $this->error(__('domains/system/messages.backup.backup_error'));
+ }
+ }
+
+ public function download(Backup $backup): StreamedResponse
+ {
+ $file = Storage::disk($backup->disk)->exists($backup->path);
+ if (! $file) {
+ $this->error(__('domains/system/messages.backup.download_error', ['path' => $backup->path]));
+ }
+
+ return Storage::disk($backup->disk)->download($backup->path);
+ }
+
+ public function restore(Backup $backup, SystemRestore $restore): void
+ {
+ try {
+ $restore->execute($backup);
+ $this->success(__('domains/system/messages.backup.restored_success'));
+ } catch (Exception $exception) {
+ logger($exception->getMessage());
+ $this->error(__('domains/system/messages.backup.restored_error'));
+ }
+ }
+
+ #[On('delete-data')]
+ public function delete(Backup $id, DeleteBackup $action): void
+ {
+ $action->execute($id);
+ $this->dispatch('delete-data-completed');
+ }
+};
diff --git a/resources/views/pages/system/backups/⚡upload-backup-modal/upload-backup-modal.blade.php b/resources/views/pages/system/backups/⚡upload-backup-modal/upload-backup-modal.blade.php
new file mode 100644
index 0000000..76f2a95
--- /dev/null
+++ b/resources/views/pages/system/backups/⚡upload-backup-modal/upload-backup-modal.blade.php
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+ @push('scripts')
+ @filepondScripts
+ @endpush
+
diff --git a/resources/views/pages/system/backups/⚡upload-backup-modal/upload-backup-modal.php b/resources/views/pages/system/backups/⚡upload-backup-modal/upload-backup-modal.php
new file mode 100644
index 0000000..26dc9db
--- /dev/null
+++ b/resources/views/pages/system/backups/⚡upload-backup-modal/upload-backup-modal.php
@@ -0,0 +1,34 @@
+validate();
+ $uploadBackupFile->execute($this->file);
+ $this->success(__('ui.crud.success.uploaded', ['resource' => __('resources.backup_file')]));
+ $this->dispatch('hide-backup-file-upload-modal');
+ $this->dispatch('reload-backup-data');
+ }
+
+ public function hide(): void
+ {
+ $this->resetErrorBag();
+ $this->dispatch('filepond-reset-file');
+ }
+};
diff --git a/resources/views/pages/system/settings/⚡setting-list/setting-list.blade.php b/resources/views/pages/system/settings/⚡setting-list/setting-list.blade.php
new file mode 100644
index 0000000..2dc915e
--- /dev/null
+++ b/resources/views/pages/system/settings/⚡setting-list/setting-list.blade.php
@@ -0,0 +1,34 @@
+@use(App\UI\Enums\InputType)
+
+
+ @foreach ($this->settings as $group)
+
+
+ @foreach($group as $title => $section)
+
+
+ @foreach($section as $field)
+
+
+
+ @svg('tabler-edit', ['width' => 16, 'height' => 16])
+
+
+ @if($field->inputType() == InputType::FILE)
+
+ @else
+ {{ $this->settingsValue[$field->value] ?? '-' }}
+ @endif
+
+ @endforeach
+
+
+ @endforeach
+
+
+ @endforeach
+
+
+
diff --git a/resources/views/pages/system/settings/⚡setting-list/setting-list.php b/resources/views/pages/system/settings/⚡setting-list/setting-list.php
new file mode 100644
index 0000000..6e5dc71
--- /dev/null
+++ b/resources/views/pages/system/settings/⚡setting-list/setting-list.php
@@ -0,0 +1,44 @@
+ 'dashboard', 'domains/system/seo.settings.title' => ''], context: 'user')]
+#[Seo(title: 'domains/system/seo.settings.title', description: 'domains/system/seo.settings.description', keywords: 'domains/system/seo.settings.keywords')]
+class extends Component
+{
+ use HasLayoutDataAttributes, HasSeoAttributes, WithFilePond, WithToast;
+
+ public array $form = [];
+
+ public function mount(): void
+ {
+ if (! empty($this->settingsValue)) {
+ $this->form = $this->settingsValue;
+ }
+ }
+
+ #[Computed]
+ public function settings(): array
+ {
+ return SystemSettingKey::section();
+ }
+
+ #[On('setting-updated')]
+ #[Computed]
+ public function settingsValue(): array
+ {
+ return SystemSettings::all()->pluck('translated_value', 'key')->toArray();
+ }
+};
diff --git a/resources/views/pages/system/settings/⚡update-setting-modal/update-setting-modal.blade.php b/resources/views/pages/system/settings/⚡update-setting-modal/update-setting-modal.blade.php
new file mode 100644
index 0000000..5bd6cfa
--- /dev/null
+++ b/resources/views/pages/system/settings/⚡update-setting-modal/update-setting-modal.blade.php
@@ -0,0 +1,19 @@
+@use(App\UI\Enums\InputType)
+
+
+
+ @if($this->settingEnum)
+
+ @endif
+
+
+
+
+
+
+ @push('scripts')
+ @filepondScripts
+ @endpush
+
diff --git a/resources/views/pages/system/settings/⚡update-setting-modal/update-setting-modal.php b/resources/views/pages/system/settings/⚡update-setting-modal/update-setting-modal.php
new file mode 100644
index 0000000..5139515
--- /dev/null
+++ b/resources/views/pages/system/settings/⚡update-setting-modal/update-setting-modal.php
@@ -0,0 +1,77 @@
+ $this->settingEnum->validation(),
+ ];
+ }
+
+ public function show(int|string $id): void
+ {
+ $this->settingKey = $id;
+ $setting = SystemSettings::where('key', $id)
+ ->first();
+ if ($this->settingEnum->inputType() == InputType::SELECT) {
+ $this->settingValue = $setting->value;
+ } else {
+ $this->settingValue = $setting?->translated_value ?? '-';
+ }
+ }
+
+ #[Computed]
+ public function settingEnum(): ?SystemSettingKey
+ {
+ return SystemSettingKey::tryFrom($this->settingKey);
+ }
+
+ public function save(UpdateSettings $action): void
+ {
+ $this->validate();
+
+ $action->execute(new SystemSetingDTO(
+ key: $this->settingEnum,
+ value: $this->settingValue,
+ ));
+
+ $this->success(__('ui.crud.success.updated', ['resource' => $this->settingEnum->label()]));
+ $this->dispatch('hide-update-setting-modal');
+ $this->dispatch('setting-updated');
+ }
+
+ public function hide(): void
+ {
+ $this->resetValidation();
+ $this->reset();
+ }
+};
diff --git a/resources/views/vendor/datatables/editor.blade.php b/resources/views/vendor/datatables/editor.blade.php
new file mode 100644
index 0000000..3f274d4
--- /dev/null
+++ b/resources/views/vendor/datatables/editor.blade.php
@@ -0,0 +1,15 @@
+document.addEventListener("DOMContentLoaded", function(){
+ window.{{ config('datatables-html.namespace', 'LaravelDataTables') }} = window.{{ config('datatables-html.namespace', 'LaravelDataTables') }} || {};
+ $.ajaxSetup({headers: {'X-CSRF-TOKEN': '{{csrf_token()}}'}});
+ @foreach($editors as $editor)
+ var {{$editor->instance}} = window.{{ config('datatables-html.namespace', 'LaravelDataTables') }}["%1$s-{{$editor->instance}}"] = new $.fn.dataTable.Editor({!! $editor->toJson() !!});
+ {!! $editor->scripts !!}
+ @foreach ((array) $editor->events as $event)
+ {{$editor->instance}}.on('{!! $event['event'] !!}', {!! $event['script'] !!});
+ @endforeach
+ @endforeach
+ window.{{ config('datatables-html.namespace', 'LaravelDataTables') }}["%1$s"] = $("#%1$s").DataTable(%2$s);
+});
+@foreach ($scripts as $script)
+@include($script)
+@endforeach
diff --git a/resources/views/vendor/datatables/function.blade.php b/resources/views/vendor/datatables/function.blade.php
new file mode 100644
index 0000000..8ba7503
--- /dev/null
+++ b/resources/views/vendor/datatables/function.blade.php
@@ -0,0 +1,14 @@
+window.dtx = window.dtx || {};
+window.dtx["%1$s"] = function(opts) {
+ window.{{ config('datatables-html.namespace', 'LaravelDataTables') }} = window.{{ config('datatables-html.namespace', 'LaravelDataTables') }} || {};
+ @if(isset($editors))
+ @foreach($editors as $editor)
+ var {{$editor->instance}} = window.{{ config('datatables-html.namespace', 'LaravelDataTables') }}["%1$s-{{$editor->instance}}"] = new $.fn.dataTable.Editor({!! $editor->toJson() !!});
+ {!! $editor->scripts !!}
+ @foreach ((array) $editor->events as $event)
+ {{$editor->instance}}.on('{!! $event['event'] !!}', {!! $event['script'] !!});
+ @endforeach
+ @endforeach
+ @endif
+ return window.{{ config('datatables-html.namespace', 'LaravelDataTables') }}["%1$s"] = $("#%1$s").DataTable($.extend(%2$s, opts));
+}
diff --git a/resources/views/vendor/datatables/functions/batch_remove.blade.php b/resources/views/vendor/datatables/functions/batch_remove.blade.php
new file mode 100644
index 0000000..192a7e5
--- /dev/null
+++ b/resources/views/vendor/datatables/functions/batch_remove.blade.php
@@ -0,0 +1,14 @@
+$(function(){
+ @foreach($editors as $editor)
+ {{ config('datatables-html.namespace', 'LaravelDataTables') }}["%1$s-{{$editor->instance}}"].on('preSubmit', function(e, data, action) {
+ if (action !== 'remove') return;
+
+ for (let row_id of Object.keys(data.data))
+ {
+ data.data[row_id] = {
+ DT_RowId: data.data[row_id].DT_RowId
+ };
+ }
+ });
+ @endforeach
+});
diff --git a/resources/views/vendor/datatables/options.blade.php b/resources/views/vendor/datatables/options.blade.php
new file mode 100644
index 0000000..0da5763
--- /dev/null
+++ b/resources/views/vendor/datatables/options.blade.php
@@ -0,0 +1,6 @@
+window.{{ config('datatables-html.namespace', 'LaravelDataTables') }} = window.{{ config('datatables-html.namespace', 'LaravelDataTables') }} || {};
+window.{{ config('datatables-html.namespace', 'LaravelDataTables') }}.options = %2$s
+window.{{ config('datatables-html.namespace', 'LaravelDataTables') }}.editors = [];
+@foreach($editors as $editor)
+window.{{ config('datatables-html.namespace', 'LaravelDataTables') }}.editors["{{$editor->instance}}"] = {!! $editor->toJson() !!}
+@endforeach
diff --git a/resources/views/vendor/datatables/scout.blade.php b/resources/views/vendor/datatables/scout.blade.php
new file mode 100644
index 0000000..506ad40
--- /dev/null
+++ b/resources/views/vendor/datatables/scout.blade.php
@@ -0,0 +1,23 @@
+$(function(){
+ $('#%1$s').on('xhr.dt', function (e, settings, json, xhr) {
+ if (json == null || !('disableOrdering' in json)) return;
+
+ let table = {{ config('datatables-html.namespace', 'LaravelDataTables') }}[$(this).attr('id')];
+ if (json.disableOrdering) {
+ table.settings()[0].aoColumns.forEach(function(column) {
+ column.bSortable = false;
+ $(column.nTh).removeClass('sorting_asc sorting_desc sorting').addClass('sorting_disabled');
+ });
+ } else {
+ let changed = false;
+ table.settings()[0].aoColumns.forEach(function(column) {
+ if (column.bSortable) return;
+ column.bSortable = true;
+ changed = true;
+ });
+ if (changed) {
+ table.draw();
+ }
+ }
+ });
+});
diff --git a/resources/views/vendor/datatables/script.blade.php b/resources/views/vendor/datatables/script.blade.php
new file mode 100644
index 0000000..d20008e
--- /dev/null
+++ b/resources/views/vendor/datatables/script.blade.php
@@ -0,0 +1,12 @@
+// eslint-disable-next-line no-unused-vars
+
+window.DataTableLockedQueue = window.DataTableLockedQueue || [];
+window.DataTableLockedQueue.push(function() {
+window.dispatchEvent(new CustomEvent('yajra:boot', {
+detail: { id: "%1$s", config: %2$s }
+}));
+});
+
+if (typeof window.checkAndFlushDataTables === 'function') {
+window.checkAndFlushDataTables();
+}
diff --git a/resources/views/vendor/livewire-filepond/upload.blade.php b/resources/views/vendor/livewire-filepond/upload.blade.php
new file mode 100644
index 0000000..d7ea52d
--- /dev/null
+++ b/resources/views/vendor/livewire-filepond/upload.blade.php
@@ -0,0 +1,134 @@
+@php
+ $isCustomPlaceholder = isset($placeholder);
+@endphp
+
+@props([
+ 'label' => '',
+ 'multiple' => false,
+ 'required' => false,
+ 'disabled' => false,
+ 'maxFiles' => null,
+ 'placeholder' => __('Drag & Drop your files or Browse '),
+ 'maxfilesmsg' => __('You can upload a maximum of :max files.'),
+])
+
+@php
+ if (!($wireModelAttribute = $attributes->whereStartsWith('wire:model')->first())) {
+ throw new Exception('You must wire:model to the filepond input.');
+ }
+
+ $pondProperties = $attributes->except(['class', 'placeholder', 'required', 'disabled', 'multiple', 'wire:model']);
+
+ if ($maxFiles !== null) {
+ $pondProperties['max-files'] = $maxFiles;
+ }
+
+ // convert keys from kebab-case to camelCase
+ $pondProperties = collect($pondProperties)
+ ->mapWithKeys(fn($value, $key) => [Illuminate\Support\Str::camel($key) => $value])
+ ->toArray();
+ $pondLocalizations = __('livewire-filepond::filepond');
+@endphp
+
+
+
{{ $label }}
+
+
$errors->has($wireModelAttribute)])>
+
+
+
+
+
+ @if ($errors->has($wireModelAttribute))
+
{{ $errors->first($wireModelAttribute) }}
+ @endif
+
diff --git a/resources/views/vendor/livewire/bootstrap.blade.php b/resources/views/vendor/livewire/bootstrap.blade.php
new file mode 100644
index 0000000..1819fc6
--- /dev/null
+++ b/resources/views/vendor/livewire/bootstrap.blade.php
@@ -0,0 +1,102 @@
+@php
+if (! isset($scrollTo)) {
+ $scrollTo = 'body';
+}
+
+$scrollIntoViewJsSnippet = ($scrollTo !== false)
+ ? <<
+ @if ($paginator->hasPages())
+
+
+
+
+
+
+
+
+ {!! __('Showing') !!}
+ {{ $paginator->firstItem() }}
+ {!! __('to') !!}
+ {{ $paginator->lastItem() }}
+ {!! __('of') !!}
+ {{ $paginator->total() }}
+ {!! __('results') !!}
+
+
+
+
+
+
+
+
+ @endif
+
diff --git a/resources/views/vendor/livewire/simple-bootstrap.blade.php b/resources/views/vendor/livewire/simple-bootstrap.blade.php
new file mode 100644
index 0000000..a523342
--- /dev/null
+++ b/resources/views/vendor/livewire/simple-bootstrap.blade.php
@@ -0,0 +1,53 @@
+@php
+if (! isset($scrollTo)) {
+ $scrollTo = 'body';
+}
+
+$scrollIntoViewJsSnippet = ($scrollTo !== false)
+ ? <<
+ @if ($paginator->hasPages())
+
+
+
+ @endif
+
diff --git a/resources/views/vendor/livewire/simple-tailwind.blade.php b/resources/views/vendor/livewire/simple-tailwind.blade.php
new file mode 100644
index 0000000..d7f2560
--- /dev/null
+++ b/resources/views/vendor/livewire/simple-tailwind.blade.php
@@ -0,0 +1,56 @@
+@php
+if (! isset($scrollTo)) {
+ $scrollTo = 'body';
+}
+
+$scrollIntoViewJsSnippet = ($scrollTo !== false)
+ ? <<
+ @if ($paginator->hasPages())
+
+
+ {{-- Previous Page Link --}}
+ @if ($paginator->onFirstPage())
+
+ {!! __('pagination.previous') !!}
+
+ @else
+ @if(method_exists($paginator,'getCursorName'))
+
+ {!! __('pagination.previous') !!}
+
+ @else
+
+ {!! __('pagination.previous') !!}
+
+ @endif
+ @endif
+
+
+
+ {{-- Next Page Link --}}
+ @if ($paginator->hasMorePages())
+ @if(method_exists($paginator,'getCursorName'))
+
+ {!! __('pagination.next') !!}
+
+ @else
+
+ {!! __('pagination.next') !!}
+
+ @endif
+ @else
+
+ {!! __('pagination.next') !!}
+
+ @endif
+
+
+ @endif
+
diff --git a/resources/views/vendor/livewire/tailwind.blade.php b/resources/views/vendor/livewire/tailwind.blade.php
new file mode 100644
index 0000000..9ad7f28
--- /dev/null
+++ b/resources/views/vendor/livewire/tailwind.blade.php
@@ -0,0 +1,126 @@
+@php
+if (! isset($scrollTo)) {
+ $scrollTo = 'body';
+}
+
+$scrollIntoViewJsSnippet = ($scrollTo !== false)
+ ? <<
+ @if ($paginator->hasPages())
+
+
+
+ @if ($paginator->onFirstPage())
+
+ {!! __('pagination.previous') !!}
+
+ @else
+
+ {!! __('pagination.previous') !!}
+
+ @endif
+
+
+
+ @if ($paginator->hasMorePages())
+
+ {!! __('pagination.next') !!}
+
+ @else
+
+ {!! __('pagination.next') !!}
+
+ @endif
+
+
+
+
+
+
+ {!! __('Showing') !!}
+ {{ $paginator->firstItem() }}
+ {!! __('to') !!}
+ {{ $paginator->lastItem() }}
+ {!! __('of') !!}
+ {{ $paginator->total() }}
+ {!! __('results') !!}
+
+
+
+
+
+
+ {{-- Previous Page Link --}}
+ @if ($paginator->onFirstPage())
+
+
+
+
+
+
+
+ @else
+
+
+
+
+
+ @endif
+
+
+ {{-- Pagination Elements --}}
+ @foreach ($elements as $element)
+ {{-- "Three Dots" Separator --}}
+ @if (is_string($element))
+
+ {{ $element }}
+
+ @endif
+
+ {{-- Array Of Links --}}
+ @if (is_array($element))
+ @foreach ($element as $page => $url)
+
+ @if ($page == $paginator->currentPage())
+
+ {{ $page }}
+
+ @else
+
+ {{ $page }}
+
+ @endif
+
+ @endforeach
+ @endif
+ @endforeach
+
+
+ {{-- Next Page Link --}}
+ @if ($paginator->hasMorePages())
+
+
+
+
+
+ @else
+
+
+
+
+
+
+
+ @endif
+
+
+
+
+
+ @endif
+
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 0000000..050e969
--- /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 0000000..3ff41f8
--- /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 0000000..61cf0c5
--- /dev/null
+++ b/resources/views/vendor/mail/html/header.blade.php
@@ -0,0 +1,16 @@
+@props(['url'])
+@use(App\Domains\System\Queries\GetSystemSettings)
+@use(App\Domains\System\Enums\SystemSettingKey)
+
+
+
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 0000000..037efe3
--- /dev/null
+++ b/resources/views/vendor/mail/html/layout.blade.php
@@ -0,0 +1,58 @@
+
+
+
+{{ config('app.name') }}
+
+
+
+
+
+{!! $head ?? '' !!}
+
+
+
+
+
+
+
+{!! $header ?? '' !!}
+
+
+
+
+
+
+
+
+{!! Illuminate\Mail\Markdown::parse($slot) !!}
+
+{!! $subcopy ?? '' !!}
+
+
+
+
+
+
+{!! $footer ?? '' !!}
+
+
+
+
+
+
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 0000000..a16bace
--- /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') }} {{ config('app.name') }}. {{ __('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 0000000..2975a60
--- /dev/null
+++ b/resources/views/vendor/mail/html/panel.blade.php
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+{{ Illuminate\Mail\Markdown::parse($slot) }}
+
+
+
+
+
+
+
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 0000000..790ce6c
--- /dev/null
+++ b/resources/views/vendor/mail/html/subcopy.blade.php
@@ -0,0 +1,7 @@
+
+
+
+{{ Illuminate\Mail\Markdown::parse($slot) }}
+
+
+
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 0000000..a5f3348
--- /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 0000000..ac42905
--- /dev/null
+++ b/resources/views/vendor/mail/html/themes/default.css
@@ -0,0 +1,296 @@
+/* Base */
+
+body,
+body *:not(html):not(style):not(br):not(tr):not(code) {
+ box-sizing: border-box;
+ font-family: 'Poppins', sans-serif;
+ position: relative;
+}
+
+body {
+ -webkit-text-size-adjust: none;
+ background-color: #ffffff;
+ color: #525252;
+ 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: #171717;
+}
+
+a img {
+ border: none;
+}
+
+/* Typography */
+
+h1 {
+ color: #171717;
+ 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: #171717;
+ font-size: 19px;
+ font-weight: bold;
+ text-decoration: none;
+}
+
+/* Logo */
+
+.logo {
+ height: 75px;
+ margin-top: 15px;
+ margin-bottom: 10px;
+ max-height: 75px;
+ width: 75px;
+}
+
+/* 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: #e5e5e5;
+ 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 #e5e5e5;
+ 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: #a3a3a3;
+ font-size: 12px;
+ text-align: center;
+}
+
+.footer a {
+ color: #a3a3a3;
+ 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 #e5e5e5;
+ margin: 0;
+ padding-bottom: 8px;
+}
+
+.table td {
+ color: #525252;
+ 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: #E66239;
+ border-bottom: 8px solid #E66239;
+ border-left: 18px solid #E66239;
+ border-right: 18px solid #E66239;
+ border-top: 8px solid #E66239;
+}
+
+.button-green,
+.button-success {
+ background-color: #00C951;
+ border-bottom: 8px solid #00C951;
+ border-left: 18px solid #00C951;
+ border-right: 18px solid #00C951;
+ border-top: 8px solid #00C951;
+}
+
+.button-red,
+.button-error {
+ background-color: #FB2C36;
+ border-bottom: 8px solid #FB2C36;
+ border-left: 18px solid #FB2C36;
+ border-right: 18px solid #FB2C36;
+ border-top: 8px solid #FB2C36;
+}
+
+/* Panels */
+
+.panel {
+ border-left: #171717 solid 4px;
+ margin: 21px 0;
+}
+
+.panel-content {
+ background-color: #fafafa;
+ color: #525252;
+ padding: 16px;
+}
+
+.panel-content p {
+ color: #525252;
+}
+
+.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 0000000..97444eb
--- /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 0000000..3338f62
--- /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 0000000..97444eb
--- /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 0000000..ec58e83
--- /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 0000000..c357307
--- /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') }} {{ config('app.name') }}. {{ __('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 0000000..3338f62
--- /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 0000000..3338f62
--- /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 0000000..3338f62
--- /dev/null
+++ b/resources/views/vendor/mail/text/table.blade.php
@@ -0,0 +1 @@
+{{ $slot }}
diff --git a/resources/views/welcome.blade.php b/resources/views/welcome.blade.php
new file mode 100644
index 0000000..db648e0
--- /dev/null
+++ b/resources/views/welcome.blade.php
@@ -0,0 +1,2 @@
+
+
diff --git a/resources/views/widgets/charts/⚡role-distribution.blade.php b/resources/views/widgets/charts/⚡role-distribution.blade.php
new file mode 100644
index 0000000..92377da
--- /dev/null
+++ b/resources/views/widgets/charts/⚡role-distribution.blade.php
@@ -0,0 +1,75 @@
+dispatch('chart-role-distribution-update',
+ series: $this->roleDistributions['series'],
+ options: [
+ 'labels' => $this->roleDistributions['categories']
+ ]
+ );
+ }
+
+ #[Computed]
+ public function roleDistributions(): array
+ {
+ return app(GetRoleDistributions::class)->fetch();
+ }
+};
+?>
+
+
+
+
+ @svg('tabler-user-shield', [
+ 'class' => 'fs-4'
+ ])
+
+
+
{{ __('domains/identity/dashboard.role_distribution.title') }}
+
{{ __('domains/identity/dashboard.role_distribution.subtitle') }}
+
+
+
+
+
+
+@once
+ @push('page-scripts')
+ @vite('resources/js/plugin/apexchart.js')
+ @endpush
+@endonce
diff --git a/resources/views/widgets/charts/⚡user-growth.blade.php b/resources/views/widgets/charts/⚡user-growth.blade.php
new file mode 100644
index 0000000..6698e7b
--- /dev/null
+++ b/resources/views/widgets/charts/⚡user-growth.blade.php
@@ -0,0 +1,60 @@
+dispatch('chart-user-growth-update',
+ series: [[
+ 'name' => __('domains/identity/dashboard.user_growth.series_name'),
+ 'data' => $this->userGrowth['series']
+ ]],
+ options: [
+ 'xaxis' => [
+ 'categories' => $this->userGrowth['categories']
+ ]
+ ]
+ );
+ }
+
+ #[Computed]
+ public function userGrowth(): array
+ {
+ return app(GetUserGrowthTrends::class)->fetch();
+ }
+};
+?>
+
+
+
+
+ @svg('tabler-user-pin', [
+ 'class' => 'fs-4'
+ ])
+
+
+
{{ __('domains/identity/dashboard.user_growth.title') }}
+
{{ __('domains/identity/dashboard.user_growth.subtitle', ['year' => now()->year]) }}
+
+
+
+
+
+
+@once
+ @push('page-scripts')
+ @vite('resources/js/plugin/apexchart.js')
+ @endpush
+@endonce
diff --git a/resources/views/widgets/stats/⚡new-user-count.blade.php b/resources/views/widgets/stats/⚡new-user-count.blade.php
new file mode 100644
index 0000000..1882faf
--- /dev/null
+++ b/resources/views/widgets/stats/⚡new-user-count.blade.php
@@ -0,0 +1,29 @@
+fetch();
+ }
+};
+?>
+
+
+
+
+ @svg('tabler-user-star', [
+ 'class' => 'fs-4'
+ ])
+
+
+
{{ __('domains/identity/dashboard.new_user_count.title') }}
+
{{ $this->userCount['new_users'] }}
+
{{ __('domains/identity/dashboard.new_user_count.growth', ['rate' => $this->userCount['growth_rate']]) }}
+
+
+
diff --git a/resources/views/widgets/stats/⚡user-count.blade.php b/resources/views/widgets/stats/⚡user-count.blade.php
new file mode 100644
index 0000000..dd0a95b
--- /dev/null
+++ b/resources/views/widgets/stats/⚡user-count.blade.php
@@ -0,0 +1,29 @@
+fetch();
+ }
+};
+?>
+
+
+
+
+ @svg('tabler-users-group', [
+ 'class' => 'fs-4'
+ ])
+
+
+
{{ __('domains/identity/dashboard.user_count.title') }}
+
{{ $this->userCount['total_users'] }}
+
{{ __('domains/identity/dashboard.user_count.growth', ['rate' => $this->userCount['growth_rate']]) }}
+
+
+
diff --git a/resources/views/widgets/stats/⚡user-verification-rate.blade.php b/resources/views/widgets/stats/⚡user-verification-rate.blade.php
new file mode 100644
index 0000000..6b0883f
--- /dev/null
+++ b/resources/views/widgets/stats/⚡user-verification-rate.blade.php
@@ -0,0 +1,29 @@
+fetch();
+ }
+};
+?>
+
+
+
+
+ @svg('tabler-user-check', [
+ 'class' => 'fs-4'
+ ])
+
+
+
{{ __('domains/identity/dashboard.user_verification_rate.title') }}
+
{{ $this->userCount['verification_rate'] }}%
+
{{ __('domains/identity/dashboard.user_verification_rate.detail', ['verified' => $this->userCount['verified'], 'unverified' => $this->userCount['unverified']]) }}
+
+
+
diff --git a/routes/api.php b/routes/api.php
new file mode 100644
index 0000000..880c6d4
--- /dev/null
+++ b/routes/api.php
@@ -0,0 +1,11 @@
+as('api.v1.')->middleware('auth:sanctum')->group(function () {
+ Route::prefix('lookups')->as('lookups.')->group(function () {
+ Route::get('/roles', RoleLookupController::class)->name('roles');
+ });
+});
diff --git a/routes/auth.php b/routes/auth.php
new file mode 100644
index 0000000..7296a1b
--- /dev/null
+++ b/routes/auth.php
@@ -0,0 +1,22 @@
+group(function () {
+ Route::livewire('register', 'pages::auth.register')->name('register');
+ Route::livewire('login', 'pages::auth.login')->name('login');
+ Route::livewire('forgot-password', 'pages::auth.forgot-password')->name('password.request');
+ Route::livewire('reset-password/{token}', 'pages::auth.reset-password')->name('password.reset');
+});
+
+Route::middleware('auth')->group(function () {
+ Route::livewire('verify-email', 'pages::auth.verify-email')
+ ->name('verification.notice');
+
+ Route::get('verify-email/{id}/{hash}', VerifyEmailController::class)
+ ->middleware(['signed', 'throttle:6,1'])
+ ->name('verification.verify');
+
+ Route::livewire('confirm-password', 'pages::auth.confirm-password')->name('password.confirm');
+});
diff --git a/routes/console.php b/routes/console.php
new file mode 100644
index 0000000..3c9adf1
--- /dev/null
+++ b/routes/console.php
@@ -0,0 +1,8 @@
+comment(Inspiring::quote());
+})->purpose('Display an inspiring quote');
diff --git a/routes/web.php b/routes/web.php
new file mode 100644
index 0000000..03500ac
--- /dev/null
+++ b/routes/web.php
@@ -0,0 +1,37 @@
+ redirect()->route('dashboard'));
+
+Route::middleware(['web', 'auth', 'verified', 'seo', 'layouts'])->group(function () {
+ Route::view('/dashboard', 'dashboard')
+ ->defaults('seo', new Seo(
+ title: 'seo.dashboard.title',
+ description: 'seo.dashboard.description',
+ keywords: 'seo.dashboard.keywords'
+ ))->defaults('layout_data', new LayoutData(
+ header: 'seo.dashboard.title',
+ breadcrumbs: [
+ 'ui.menu.dashboard' => 'dashboard',
+ ],
+ ))->name('dashboard');
+
+ Route::livewire('/user/{user_id}', 'pages::identity.users.detail-view')->name('users.view');
+ Route::get('/users', UserController::class)->name('users.index');
+ Route::get('/roles', RoleController::class)->name('roles.index');
+
+ Route::middleware('password.confirm')->group(function () {
+ Route::livewire('/system/settings', 'pages::system.settings.setting-list')->name('system-setting.index');
+ Route::livewire('/system/backups', 'pages::system.backups.backup-list')->name('system-backup.index');
+
+ Route::get('/profile', ProfileController::class)->name('profile.index');
+ });
+});
+
+require __DIR__.'/auth.php';
diff --git a/storage/app/.gitignore b/storage/app/.gitignore
new file mode 100644
index 0000000..fedb287
--- /dev/null
+++ b/storage/app/.gitignore
@@ -0,0 +1,4 @@
+*
+!private/
+!public/
+!.gitignore
diff --git a/storage/app/private/.gitignore b/storage/app/private/.gitignore
new file mode 100644
index 0000000..d6b7ef3
--- /dev/null
+++ b/storage/app/private/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/storage/app/public/.gitignore b/storage/app/public/.gitignore
new file mode 100644
index 0000000..d6b7ef3
--- /dev/null
+++ b/storage/app/public/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/storage/debugbar/.gitignore b/storage/debugbar/.gitignore
new file mode 100644
index 0000000..d6b7ef3
--- /dev/null
+++ b/storage/debugbar/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/storage/framework/.gitignore b/storage/framework/.gitignore
new file mode 100644
index 0000000..05c4471
--- /dev/null
+++ b/storage/framework/.gitignore
@@ -0,0 +1,9 @@
+compiled.php
+config.php
+down
+events.scanned.php
+maintenance.php
+routes.php
+routes.scanned.php
+schedule-*
+services.json
diff --git a/storage/framework/cache/.gitignore b/storage/framework/cache/.gitignore
new file mode 100644
index 0000000..01e4a6c
--- /dev/null
+++ b/storage/framework/cache/.gitignore
@@ -0,0 +1,3 @@
+*
+!data/
+!.gitignore
diff --git a/storage/framework/cache/data/.gitignore b/storage/framework/cache/data/.gitignore
new file mode 100644
index 0000000..d6b7ef3
--- /dev/null
+++ b/storage/framework/cache/data/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/storage/framework/sessions/.gitignore b/storage/framework/sessions/.gitignore
new file mode 100644
index 0000000..d6b7ef3
--- /dev/null
+++ b/storage/framework/sessions/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/storage/framework/testing/.gitignore b/storage/framework/testing/.gitignore
new file mode 100644
index 0000000..d6b7ef3
--- /dev/null
+++ b/storage/framework/testing/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/storage/framework/views/.gitignore b/storage/framework/views/.gitignore
new file mode 100644
index 0000000..d6b7ef3
--- /dev/null
+++ b/storage/framework/views/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/storage/logs/.gitignore b/storage/logs/.gitignore
new file mode 100644
index 0000000..d6b7ef3
--- /dev/null
+++ b/storage/logs/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
diff --git a/tests/Architecture/BoundariesTest.php b/tests/Architecture/BoundariesTest.php
new file mode 100644
index 0000000..f5cde37
--- /dev/null
+++ b/tests/Architecture/BoundariesTest.php
@@ -0,0 +1,12 @@
+group('architecture');
+
+arch('system domain isolation')
+ ->expect('App\Domains\System')
+ ->not->toUse('App\Domains\Identity')
+ ->ignoring([
+ 'App\Domains\System\Listeners',
+ 'App\Domains\System\Policies',
+ 'App\Domains\System\Providers\SystemServiceProvider'
+ ]);
diff --git a/tests/Architecture/LayerTest.php b/tests/Architecture/LayerTest.php
new file mode 100644
index 0000000..78b5766
--- /dev/null
+++ b/tests/Architecture/LayerTest.php
@@ -0,0 +1,34 @@
+group('architecture');
+
+// Controllers should be ultra-lean entry points
+arch('controllers layer strictness')
+ ->expect('App\Http\Controllers')
+ ->not->toUse([
+ 'App\Domains\Account\Models',
+ 'App\Domains\Identity\Models',
+ 'App\Domains\System\Models',
+ ]); // Can't touch DB models directly
+
+arch('actions are protected from model layer leakage')
+ ->expect('App\Domains\*\Actions')
+ ->not->toBeUsedIn([
+ 'App\Domains\*\Models',
+ ]);
+
+// Actions should be purely executable logic
+arch('actions layer strictness')
+ ->expect('App\Domains\*\Actions')
+ ->toOnlyBeUsedIn([
+ 'App\Http\Controllers',
+ 'App\Http\Ingestion',
+ 'App\Console\Commands',
+ 'App\Domains\Account\Listeners',
+ 'App\Domains\Identity\Listeners',
+ 'App\Domains\System\Listeners',
+ 'App\Domains\Account\Actions',
+ 'App\Domains\Identity\Actions',
+ 'App\Domains\System\Actions',
+ 'App\Domains\Identity\Integration',
+ ]);
diff --git a/tests/Architecture/StrictnessTest.php b/tests/Architecture/StrictnessTest.php
new file mode 100644
index 0000000..7906955
--- /dev/null
+++ b/tests/Architecture/StrictnessTest.php
@@ -0,0 +1,7 @@
+group('architecture');
+
+arch('no debugging statement left')
+ ->expect(['dd', 'dump', 'var_dump'])
+ ->not->toBeUsed();
diff --git a/tests/Feature/Api/V1/Lookup/RoleLookupControllerTest.php b/tests/Feature/Api/V1/Lookup/RoleLookupControllerTest.php
new file mode 100644
index 0000000..3c50385
--- /dev/null
+++ b/tests/Feature/Api/V1/Lookup/RoleLookupControllerTest.php
@@ -0,0 +1,32 @@
+ 'admin', 'guard_name' => 'web']);
+ Role::create(['name' => 'editor', 'guard_name' => 'web']);
+
+ $user = User::factory()->create();
+
+ $response = $this->actingAs($user)
+ ->getJson(route('api.v1.lookups.roles', ['search' => 'admin']));
+
+ $response->assertStatus(200)
+ ->assertJsonCount(1, 'data')
+ ->assertJsonPath('data.0.id', 'admin')
+ ->assertJsonPath('data.0.text', 'admin');
+});
+
+test('it can fetch all roles when no search is provided', function () {
+ Role::create(['name' => 'admin', 'guard_name' => 'web']);
+ Role::create(['name' => 'editor', 'guard_name' => 'web']);
+
+ $user = User::factory()->create();
+
+ $response = $this->actingAs($user)
+ ->getJson(route('api.v1.lookups.roles'));
+
+ $response->assertStatus(200)
+ ->assertJsonCount(2, 'data');
+});
diff --git a/tests/Feature/Console/Commands/CleanOrphanedFilesTest.php b/tests/Feature/Console/Commands/CleanOrphanedFilesTest.php
new file mode 100644
index 0000000..da63fe4
--- /dev/null
+++ b/tests/Feature/Console/Commands/CleanOrphanedFilesTest.php
@@ -0,0 +1,28 @@
+shouldReceive('execute')
+ ->once()
+ ->with('public', '/')
+ ->andReturn([
+ 'db_orphans_removed' => 1,
+ 'disk_orphans_removed' => 2,
+ ]);
+
+ $this->instance(PruneOrphanedFiles::class, $actionMock);
+
+ $this->artisan('system:prune-files')
+ ->expectsOutput('Starting file reconciliation...')
+ ->expectsOutput('Reconciliation complete.')
+ ->expectsOutput('- DB Orphans Removed: 1')
+ ->expectsOutput('- Disk Stranded Files Removed: 2')
+ ->assertExitCode(0);
+});
diff --git a/tests/Feature/Domains/Account/Actions/Profile/UpdateProfileTest.php b/tests/Feature/Domains/Account/Actions/Profile/UpdateProfileTest.php
new file mode 100644
index 0000000..e11a981
--- /dev/null
+++ b/tests/Feature/Domains/Account/Actions/Profile/UpdateProfileTest.php
@@ -0,0 +1,51 @@
+create();
+ $dto = new UpdateProfileDTO(
+ userId: $user->id,
+ gender: GenderOption::MALE,
+ dateOfBirth: '1990-01-01',
+ phoneNumber: '1234567890'
+ );
+
+ $action = new UpdateProfile();
+ $action->execute($dto);
+
+ expect(Profile::where('user_id', $user->id)->exists())->toBeTrue()
+ ->and(Profile::where('user_id', $user->id)->first()->gender)->toBe(GenderOption::MALE);
+});
+
+test('it updates profile if it exists', function () {
+ $user = User::factory()->create();
+ Profile::create([
+ 'user_id' => $user->id,
+ 'gender' => GenderOption::MALE,
+ 'date_of_birth' => '1990-01-01',
+ 'phone_number' => '1234567890'
+ ]);
+
+ $dto = new UpdateProfileDTO(
+ userId: $user->id,
+ gender: GenderOption::FEMALE,
+ dateOfBirth: '1995-01-01',
+ phoneNumber: '0987654321'
+ );
+
+ $action = new UpdateProfile();
+ $action->execute($dto);
+
+ $profile = Profile::where('user_id', $user->id)->first();
+ expect($profile->gender)->toBe(GenderOption::FEMALE)
+ ->and($profile->date_of_birth->format('Y-m-d'))->toBe('1995-01-01')
+ ->and($profile->phone_number)->toBe('0987654321');
+});
diff --git a/tests/Feature/Domains/Identity/Actions/AccessControl/CreateSystemRoleTest.php b/tests/Feature/Domains/Identity/Actions/AccessControl/CreateSystemRoleTest.php
new file mode 100644
index 0000000..b8dbc9e
--- /dev/null
+++ b/tests/Feature/Domains/Identity/Actions/AccessControl/CreateSystemRoleTest.php
@@ -0,0 +1,37 @@
+ 'test.permission.1', 'guard_name' => 'web', 'description' => 'test', 'group' => 'test']);
+ $permission2 = Permission::create(['name' => 'test.permission.2', 'guard_name' => 'web', 'description' => 'test', 'group' => 'test']);
+
+ $dto = new CreateRoleDTO(
+ name: 'New Role',
+ guard_name: 'web',
+ permissions: ['test.permission.1', 'test.permission.2']
+ );
+
+ $action = new CreateSystemRole();
+
+ // Execute
+ $result = $action->execute($dto);
+
+ // Assert
+ expect($result)->toBeTrue();
+ $this->assertDatabaseHas('roles', [
+ 'name' => 'New Role',
+ 'guard_name' => 'web'
+ ]);
+
+ $role = Role::where('name', 'New Role')->first();
+ expect($role->hasPermissionTo('test.permission.1'))->toBeTrue()
+ ->and($role->hasPermissionTo('test.permission.2'))->toBeTrue();
+});
diff --git a/tests/Feature/Domains/Identity/Actions/AccessControl/RemoveSystemRoleTest.php b/tests/Feature/Domains/Identity/Actions/AccessControl/RemoveSystemRoleTest.php
new file mode 100644
index 0000000..f0daa75
--- /dev/null
+++ b/tests/Feature/Domains/Identity/Actions/AccessControl/RemoveSystemRoleTest.php
@@ -0,0 +1,41 @@
+ RoleType::SYSTEM_ADMIN->value, 'guard_name' => 'web']);
+ $action = new RemoveSystemRole();
+
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('Can\'t remove system role.');
+
+ $action->execute($role);
+});
+
+test('it throws exception when role has attached users', function () {
+ $role = Role::create(['name' => 'Custom Role', 'guard_name' => 'web']);
+ $user = User::factory()->create();
+ $user->assignRole($role->name);
+
+ $action = new RemoveSystemRole();
+
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage('This role has a users attached to it.');
+
+ $action->execute($role);
+});
+
+test('it does not throw exception for removable role', function () {
+ $role = Role::create(['name' => 'Custom Role', 'guard_name' => 'web']);
+ $action = new RemoveSystemRole();
+
+ $action->execute($role);
+
+ expect(true)->toBeTrue(); // If no exception, it passes
+});
diff --git a/tests/Feature/Domains/Identity/Actions/AccessControl/UpdateSystemRoleTest.php b/tests/Feature/Domains/Identity/Actions/AccessControl/UpdateSystemRoleTest.php
new file mode 100644
index 0000000..033bb1c
--- /dev/null
+++ b/tests/Feature/Domains/Identity/Actions/AccessControl/UpdateSystemRoleTest.php
@@ -0,0 +1,32 @@
+ 'Existing Role', 'guard_name' => 'web']);
+ $permission1 = Permission::create(['name' => 'test.permission.1', 'guard_name' => 'web', 'description' => 'test', 'group' => 'test']);
+ $permission2 = Permission::create(['name' => 'test.permission.2', 'guard_name' => 'web', 'description' => 'test', 'group' => 'test']);
+
+ $role->givePermissionTo('test.permission.1');
+
+ $dto = new UpdateRoleDTO(
+ permissions: ['test.permission.2']
+ );
+
+ $action = new UpdateSystemRole();
+
+ // Execute
+ $result = $action->execute($role, $dto);
+
+ // Assert
+ expect($result)->toBeTrue();
+ expect($role->hasPermissionTo('test.permission.1'))->toBeFalse()
+ ->and($role->hasPermissionTo('test.permission.2'))->toBeTrue();
+});
diff --git a/tests/Feature/Domains/Identity/Actions/AccessControl/UpdateUserRoleTest.php b/tests/Feature/Domains/Identity/Actions/AccessControl/UpdateUserRoleTest.php
new file mode 100644
index 0000000..640f962
--- /dev/null
+++ b/tests/Feature/Domains/Identity/Actions/AccessControl/UpdateUserRoleTest.php
@@ -0,0 +1,26 @@
+create();
+ $role1 = Role::create(['name' => 'Role 1', 'guard_name' => 'web']);
+ $role2 = Role::create(['name' => 'Role 2', 'guard_name' => 'web']);
+
+ $user->assignRole($role1);
+
+ $action = new UpdateUserRole();
+
+ // Execute
+ $action->execute($user, ['Role 2']);
+
+ // Assert
+ expect($user->hasRole('Role 1'))->toBeFalse()
+ ->and($user->hasRole('Role 2'))->toBeTrue();
+});
diff --git a/tests/Feature/Domains/Identity/Actions/Governance/ActivateUserStatusTest.php b/tests/Feature/Domains/Identity/Actions/Governance/ActivateUserStatusTest.php
new file mode 100644
index 0000000..f375e62
--- /dev/null
+++ b/tests/Feature/Domains/Identity/Actions/Governance/ActivateUserStatusTest.php
@@ -0,0 +1,33 @@
+create(['status' => UserStatus::INACTIVE]);
+ $action = new ActivateUserStatus(app(UpdateUserStatus::class));
+
+ $action->execute($user);
+
+ expect($user->fresh()->status)->toBe(UserStatus::ACTIVE);
+ Event::assertDispatched(UserWasActivated::class);
+});
+
+test('it throws exception if user already active', function () {
+ $user = User::factory()->create(['status' => UserStatus::ACTIVE]);
+ $action = new ActivateUserStatus(app(UpdateUserStatus::class));
+
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage(__('domains/identity/messages.exceptions.user_already_active'));
+
+ $action->execute($user);
+});
diff --git a/tests/Feature/Domains/Identity/Actions/Governance/PurgeUserTest.php b/tests/Feature/Domains/Identity/Actions/Governance/PurgeUserTest.php
new file mode 100644
index 0000000..17c586c
--- /dev/null
+++ b/tests/Feature/Domains/Identity/Actions/Governance/PurgeUserTest.php
@@ -0,0 +1,36 @@
+create();
+ $action = new PurgeUser();
+
+ $action->execute($user);
+
+ $this->assertDatabaseMissing('users', ['id' => $user->id]);
+ Event::assertDispatched(UserWasPurged::class);
+});
+
+test('it cannot purge an admin', function () {
+ Role::create(['name' => RoleType::ADMIN->value, 'guard_name' => 'web']);
+ $user = User::factory()->create();
+ $user->assignRole(RoleType::ADMIN->value);
+
+ $action = new PurgeUser();
+
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage(__('domains/identity/messages.exceptions.user_cannot_be_purged'));
+
+ $action->execute($user);
+});
diff --git a/tests/Feature/Domains/Identity/Actions/Governance/RemoveUserTest.php b/tests/Feature/Domains/Identity/Actions/Governance/RemoveUserTest.php
new file mode 100644
index 0000000..7d74400
--- /dev/null
+++ b/tests/Feature/Domains/Identity/Actions/Governance/RemoveUserTest.php
@@ -0,0 +1,40 @@
+create(['status' => UserStatus::ACTIVE]);
+
+ $suspendUser = Mockery::mock(SuspendUser::class);
+ $purgeUser = Mockery::mock(PurgeUser::class);
+
+ $suspendUser->shouldReceive('execute')->once()->with($user);
+ $purgeUser->shouldNotReceive('execute');
+
+ $action = new RemoveUser($purgeUser, $suspendUser);
+ $action->execute($user);
+
+ expect(true)->toBeTrue();
+});
+
+test('it purges inactive user', function () {
+ $user = User::factory()->create(['status' => UserStatus::INACTIVE]);
+
+ $suspendUser = Mockery::mock(SuspendUser::class);
+ $purgeUser = Mockery::mock(PurgeUser::class);
+
+ $suspendUser->shouldNotReceive('execute');
+ $purgeUser->shouldReceive('execute')->once()->with($user);
+
+ $action = new RemoveUser($purgeUser, $suspendUser);
+ $action->execute($user);
+
+ expect(true)->toBeTrue();
+});
diff --git a/tests/Feature/Domains/Identity/Actions/Governance/SuspendUserTest.php b/tests/Feature/Domains/Identity/Actions/Governance/SuspendUserTest.php
new file mode 100644
index 0000000..17a0144
--- /dev/null
+++ b/tests/Feature/Domains/Identity/Actions/Governance/SuspendUserTest.php
@@ -0,0 +1,61 @@
+create(['status' => UserStatus::ACTIVE]);
+
+ // Mock session
+ DB::table('sessions')->insert([
+ 'id' => 'session_id',
+ 'user_id' => $user->id,
+ 'ip_address' => '127.0.0.1',
+ 'user_agent' => 'test',
+ 'payload' => 'test',
+ 'last_activity' => time()
+ ]);
+
+ $action = new SuspendUser(app(UpdateUserStatus::class));
+
+ $action->execute($user);
+
+ expect($user->fresh()->status)->toBe(UserStatus::INACTIVE);
+ $this->assertDatabaseMissing('sessions', ['user_id' => $user->id]);
+ Event::assertDispatched(UserWasSuspended::class);
+});
+
+test('it cannot suspend an admin', function () {
+ Role::create(['name' => RoleType::ADMIN->value, 'guard_name' => 'web']);
+ $user = User::factory()->create(['status' => UserStatus::ACTIVE]);
+ $user->assignRole(RoleType::ADMIN->value);
+
+ $action = new SuspendUser(app(UpdateUserStatus::class));
+
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage(__('domains/identity/messages.exceptions.user_cannot_be_suspended'));
+
+ $action->execute($user);
+});
+
+test('it throws exception if user already suspended', function () {
+ $user = User::factory()->create(['status' => UserStatus::INACTIVE]);
+ $action = new SuspendUser(app(UpdateUserStatus::class));
+
+ $this->expectException(Exception::class);
+ $this->expectExceptionMessage(__('domains/identity/messages.exceptions.user_already_suspended'));
+
+ $action->execute($user);
+});
diff --git a/tests/Feature/Domains/Identity/Actions/Governance/UpdateUserStatusTest.php b/tests/Feature/Domains/Identity/Actions/Governance/UpdateUserStatusTest.php
new file mode 100644
index 0000000..be588e5
--- /dev/null
+++ b/tests/Feature/Domains/Identity/Actions/Governance/UpdateUserStatusTest.php
@@ -0,0 +1,35 @@
+create(['status' => UserStatus::ACTIVE]);
+ $action = new UpdateUserStatus();
+
+ $action->execute($user, UserStatus::INACTIVE);
+
+ $this->assertEquals(UserStatus::INACTIVE, $user->fresh()->status);
+ }
+
+ public function test_it_throws_exception_when_status_is_same(): void
+ {
+ $user = User::factory()->create(['status' => UserStatus::ACTIVE]);
+ $action = new UpdateUserStatus();
+
+ $this->expectException(Exception::class);
+ $action->execute($user, UserStatus::ACTIVE);
+ }
+}
diff --git a/tests/Feature/Domains/System/Actions/Backup/DeleteBackupTest.php b/tests/Feature/Domains/System/Actions/Backup/DeleteBackupTest.php
new file mode 100644
index 0000000..f89a8b0
--- /dev/null
+++ b/tests/Feature/Domains/System/Actions/Backup/DeleteBackupTest.php
@@ -0,0 +1,35 @@
+ 'backup.zip',
+ 'disk' => 'test-disk',
+ 'path' => 'test-backup/backup.zip',
+ 'size' => 1024,
+ 'type' => 'full',
+ ]);
+
+ Storage::disk('test-disk')->put('test-backup/backup.zip', 'content');
+
+ $action = new DeleteBackup();
+ $action->execute($backup);
+
+ Storage::disk('test-disk')->assertMissing('test-backup/backup.zip');
+ $this->assertDatabaseMissing('backups', ['id' => $backup->id]);
+ }
+}
diff --git a/tests/Feature/Domains/System/Actions/Backup/SyncBackupCatalogTest.php b/tests/Feature/Domains/System/Actions/Backup/SyncBackupCatalogTest.php
new file mode 100644
index 0000000..a5269f1
--- /dev/null
+++ b/tests/Feature/Domains/System/Actions/Backup/SyncBackupCatalogTest.php
@@ -0,0 +1,40 @@
+put('test-backup/backup1.zip', 'content');
+
+ // Create an orphaned DB record
+ Backup::create([
+ 'file_name' => 'test-backup backup2.zip',
+ 'disk' => 'test-disk',
+ 'path' => 'test-backup/backup2.zip',
+ 'size' => 1024,
+ 'type' => 'full',
+ ]);
+
+ $action = new SyncBackupCatalog();
+ $action->execute();
+
+ $this->assertDatabaseHas('backups', ['file_name' => 'test-backup backup1.zip']);
+ $this->assertDatabaseMissing('backups', ['file_name' => 'test-backup backup2.zip']);
+ }
+}
diff --git a/tests/Feature/Domains/System/Actions/Backup/SystemBackupTest.php b/tests/Feature/Domains/System/Actions/Backup/SystemBackupTest.php
new file mode 100644
index 0000000..9ee72ec
--- /dev/null
+++ b/tests/Feature/Domains/System/Actions/Backup/SystemBackupTest.php
@@ -0,0 +1,53 @@
+with('backup:run', [])
+ ->once()
+ ->andReturn(0);
+
+ Storage::disk('test-disk')->put('test-backup/test-file.zip', 'content');
+
+ $action = new SystemBackup();
+ $backup = $action->execute();
+
+ $this->assertInstanceOf(Backup::class, $backup);
+ $this->assertEquals('test-backup test-file.zip', $backup->file_name);
+ $this->assertEquals('test-disk', $backup->disk);
+ $this->assertEquals('test-backup/test-file.zip', $backup->path);
+
+ $this->assertEquals(7, $backup->getRawOriginal('size'));
+
+ $this->assertDatabaseHas('backups', [
+ 'file_name' => 'test-backup test-file.zip',
+ 'disk' => 'test-disk',
+ 'path' => 'test-backup/test-file.zip',
+ 'size' => 7,
+ 'type' => 'full',
+ ]);
+ }
+}
diff --git a/tests/Feature/Domains/System/Actions/Backup/SystemRestoreTest.php b/tests/Feature/Domains/System/Actions/Backup/SystemRestoreTest.php
new file mode 100644
index 0000000..42139fb
--- /dev/null
+++ b/tests/Feature/Domains/System/Actions/Backup/SystemRestoreTest.php
@@ -0,0 +1,82 @@
+ 'backup.zip',
+ 'disk' => 'test-disk',
+ 'path' => 'backups/backup.zip',
+ 'size' => 1024,
+ 'type' => 'full',
+ ]);
+
+ // Create a fake zip file
+ $zipPath = storage_path('temp-backup.zip');
+ $zip = new ZipArchive();
+ $zip->open($zipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE);
+ $zip->addFromString('db-dumps/mysql-'.config('database.connections.mysql.database').'.sql', 'SQL CONTENT');
+ $zip->addEmptyDir('storage');
+ $zip->close();
+
+ // Put it in fake storage
+ Storage::disk('test-disk')->put('backups/backup.zip', file_get_contents($zipPath));
+
+ // Ensure path exists for SystemRestore to find it (SystemRestore uses physical path)
+ // Since we are using Storage::fake, the path() might not point to where we put the file
+ // However, the code uses Storage::disk($backup->disk)->path($backup->path)
+ // Storage::fake() uses a local driver by default, so it should be fine if we mock the disk path or if it's pointing to a temp storage
+ // Actually, Storage::fake() usually puts files in a temporary directory.
+ // We need to make sure SystemRestore can find the file.
+ // Wait, SystemRestore does `file_exists($backupPath)`.
+ // In testing, I might need to make sure the file exists at the path returned by `path()`.
+
+ // Let's mock SyncBackupCatalog
+ $syncBackupCatalog = Mockery::mock(SyncBackupCatalog::class);
+ $syncBackupCatalog->shouldReceive('execute')->once();
+
+ // The action
+ $action = new SystemRestore($syncBackupCatalog);
+
+ // We need to bypass the file_exists check if Storage::fake() doesn't put it where expected
+ // Actually, we can just create the file where path() says it will be.
+ // But Storage::fake('test-disk') usually sets the root to a temp dir.
+ // Let's check the path.
+ $actualPath = Storage::disk('test-disk')->path('backups/backup.zip');
+ if (!file_exists(dirname($actualPath))) {
+ mkdir(dirname($actualPath), 0755, true);
+ }
+ copy($zipPath, $actualPath);
+
+ $action->execute($backup);
+
+ Process::assertRan(fn ($process) => in_array('mysql', (array) $process->command));
+
+ // Cleanup
+ unlink($zipPath);
+ File::deleteDirectory(storage_path('app/restore-temp'));
+ }
+}
diff --git a/tests/Feature/Domains/System/Actions/Settings/UpdateSettingsTest.php b/tests/Feature/Domains/System/Actions/Settings/UpdateSettingsTest.php
new file mode 100644
index 0000000..d5201b4
--- /dev/null
+++ b/tests/Feature/Domains/System/Actions/Settings/UpdateSettingsTest.php
@@ -0,0 +1,42 @@
+execute($dto);
+
+ expect(SystemSettings::where('key', SystemSettingKey::WEB_NAME->value)->value('value'))->toBe('New Name');
+});
+
+test('it updates existing setting', function () {
+ SystemSettings::create([
+ 'key' => SystemSettingKey::WEB_NAME->value,
+ 'value' => 'Old Name',
+ ]);
+
+ $action = new UpdateSettings();
+ $dto = new SystemSetingDTO(SystemSettingKey::WEB_NAME, 'New Name');
+
+ $action->execute($dto);
+
+ expect(SystemSettings::where('key', SystemSettingKey::WEB_NAME->value)->value('value'))->toBe('New Name');
+});
+
+test('it clears cache after update', function () {
+ Cache::shouldReceive('forget')->once()->with('system_settings');
+
+ $action = new UpdateSettings();
+ $dto = new SystemSetingDTO(SystemSettingKey::WEB_NAME, 'New Name');
+
+ $action->execute($dto);
+});
diff --git a/tests/Feature/Http/DataTables/Identity/RoleDataTableTest.php b/tests/Feature/Http/DataTables/Identity/RoleDataTableTest.php
new file mode 100644
index 0000000..863dd13
--- /dev/null
+++ b/tests/Feature/Http/DataTables/Identity/RoleDataTableTest.php
@@ -0,0 +1,35 @@
+seed(RoleSeeder::class);
+ $user = User::factory()->create();
+ $user->assignRole(RoleType::SYSTEM_ADMIN->value);
+ $response = $this->actingAs($user)->get(route('roles.index'));
+
+ $response->assertStatus(200);
+ $response->assertSee('role-table');
+});
+
+test('role datatable returns data in json format', function () {
+ $this->seed(RoleSeeder::class);
+ $user = User::factory()->create();
+ $user->assignRole(RoleType::SYSTEM_ADMIN->value);
+
+ $response = $this->actingAs($user)
+ ->withHeaders(['X-Requested-With' => 'XMLHttpRequest'])
+ ->getJson(route('roles.index', ['draw' => 1]));
+
+ $response->assertStatus(200)
+ ->assertJsonStructure([
+ 'draw',
+ 'recordsTotal',
+ 'recordsFiltered',
+ 'data',
+ ])
+ ->assertJsonPath('data.0.name', RoleType::SYSTEM_ADMIN->value);
+});
diff --git a/tests/Feature/Http/DataTables/Identity/UserDataTableTest.php b/tests/Feature/Http/DataTables/Identity/UserDataTableTest.php
new file mode 100644
index 0000000..c1cf3a3
--- /dev/null
+++ b/tests/Feature/Http/DataTables/Identity/UserDataTableTest.php
@@ -0,0 +1,83 @@
+seed(RoleSeeder::class);
+ $user = User::factory()->create();
+ $user->assignRole(RoleType::SYSTEM_ADMIN->value);
+ // Assuming the user needs permission to view users, but for now just acting as a user
+ $response = $this->actingAs($user)->get(route('users.index'));
+
+ $response->assertStatus(200);
+ $response->assertSee('user-table');
+});
+
+test('user datatable returns data in json format', function () {
+ $this->seed(RoleSeeder::class);
+ $user = User::factory()->create(['name' => 'John Doe']);
+ $user->assignRole(RoleType::SYSTEM_ADMIN->value);
+
+ $response = $this->actingAs($user)
+ ->withHeaders(['X-Requested-With' => 'XMLHttpRequest'])
+ ->getJson(route('users.index', ['draw' => 1]));
+
+ $response->assertStatus(200)
+ ->assertJsonStructure([
+ 'draw',
+ 'recordsTotal',
+ 'recordsFiltered',
+ 'data',
+ ])
+ ->assertJsonPath('recordsTotal', 1)
+ ->assertJsonPath('data.0.name', 'John Doe');
+});
+
+test('user datatable can be filtered by role', function () {
+ $this->seed(RoleSeeder::class);
+
+ $admin = User::factory()->create(['name' => 'Admin User']);
+ $admin->assignRole(RoleType::ADMIN->value);
+
+ $regularUser = User::factory()->create(['name' => 'Regular User']);
+ $regularUser->assignRole(RoleType::USER->value);
+
+ $response = $this->actingAs($admin)
+ ->withHeaders(['X-Requested-With' => 'XMLHttpRequest'])
+ ->getJson(route('users.index', [
+ 'draw' => 1,
+ 'role' => RoleType::ADMIN->value
+ ]));
+
+ $response->assertStatus(200)
+ ->assertJsonPath('recordsFiltered', 1)
+ ->assertJsonPath('data.0.name', 'Admin User');
+});
+
+test('user datatable can be filtered by status', function () {
+ $this->seed(RoleSeeder::class);
+ $activeUser = User::factory()->create([
+ 'name' => 'Active User',
+ 'status' => UserStatus::ACTIVE
+ ]);
+ $activeUser->assignRole(RoleType::SYSTEM_ADMIN->value);
+ $inactiveUser = User::factory()->create([
+ 'name' => 'Inactive User',
+ 'status' => UserStatus::INACTIVE
+ ]);
+
+ $response = $this->actingAs($activeUser)
+ ->withHeaders(['X-Requested-With' => 'XMLHttpRequest'])
+ ->getJson(route('users.index', [
+ 'draw' => 1,
+ 'status' => UserStatus::ACTIVE->value
+ ]));
+
+ $response->assertStatus(200)
+ ->assertJsonPath('recordsFiltered', 1)
+ ->assertJsonPath('data.0.name', 'Active User');
+});
diff --git a/tests/Pest.php b/tests/Pest.php
new file mode 100644
index 0000000..6885ad5
--- /dev/null
+++ b/tests/Pest.php
@@ -0,0 +1,50 @@
+extend(TestCase::class)
+ ->use(RefreshDatabase::class)
+ ->in('Feature');
+
+/*
+|--------------------------------------------------------------------------
+| Expectations
+|--------------------------------------------------------------------------
+|
+| When you're writing tests, you often need to check that values meet certain conditions. The
+| "expect()" function gives you access to a set of "expectations" methods that you can use
+| to assert different things. Of course, you may extend the Expectation API at any time.
+|
+*/
+
+expect()->extend('toBeOne', function () {
+ return $this->toBe(1);
+});
+
+/*
+|--------------------------------------------------------------------------
+| Functions
+|--------------------------------------------------------------------------
+|
+| While Pest is very powerful out-of-the-box, you may have some testing code specific to your
+| project that you don't want to repeat in every file. Here you can also expose helpers as
+| global functions to help you to reduce the number of lines of code in your test files.
+|
+*/
+
+function something()
+{
+ // ..
+}
diff --git a/tests/TestCase.php b/tests/TestCase.php
new file mode 100644
index 0000000..fe1ffc2
--- /dev/null
+++ b/tests/TestCase.php
@@ -0,0 +1,10 @@
+label())->toBe(__('domains/account/enum.gender.male'))
+ ->and(GenderOption::FEMALE->label())->toBe(__('domains/account/enum.gender.female'));
+});
diff --git a/tests/Unit/Domains/Account/Listeners/SyncImportedUserProfileTest.php b/tests/Unit/Domains/Account/Listeners/SyncImportedUserProfileTest.php
new file mode 100644
index 0000000..f253b37
--- /dev/null
+++ b/tests/Unit/Domains/Account/Listeners/SyncImportedUserProfileTest.php
@@ -0,0 +1,39 @@
+label(),
+ dateOfBirth: '1990-01-01',
+ phoneNumber: '1234567890'
+ );
+
+ $updateProfile->shouldReceive('execute')
+ ->once()
+ ->with(Mockery::on(function (UpdateProfileDTO $dto) use ($event) {
+ return $dto->userId === $event->userId &&
+ $dto->gender === GenderOption::MALE &&
+ $dto->dateOfBirth === $event->dateOfBirth &&
+ $dto->phoneNumber === $event->phoneNumber;
+ }));
+
+ $listener->handle($event);
+ }
+}
diff --git a/tests/Unit/Domains/Identity/Actions/IdentityMaintenance/UpdateUserAvatarTest.php b/tests/Unit/Domains/Identity/Actions/IdentityMaintenance/UpdateUserAvatarTest.php
new file mode 100644
index 0000000..bcce03b
--- /dev/null
+++ b/tests/Unit/Domains/Identity/Actions/IdentityMaintenance/UpdateUserAvatarTest.php
@@ -0,0 +1,34 @@
+create();
+ $file = UploadedFile::fake()->image('avatar.jpg');
+
+ $mockReplaceSingleFile = $this->mock(ReplaceSingleFile::class, function (MockInterface $mock) use ($user, $file) {
+ $mock->shouldReceive('execute')
+ ->once()
+ ->with($file, Mockery::on(function (FileDTO $dto) use ($user) {
+ return $dto->modelType === $user->getMorphClass() &&
+ $dto->modelId === $user->id &&
+ $dto->relationName === 'avatar' &&
+ $dto->disk === 'local' &&
+ $dto->directory === 'avatars';
+ }));
+ });
+
+ $action = new UpdateUserAvatar($mockReplaceSingleFile);
+ $action->execute($user, $file);
+});
diff --git a/tests/Unit/Domains/Identity/Actions/IdentityMaintenance/UpdateUserIdentityTest.php b/tests/Unit/Domains/Identity/Actions/IdentityMaintenance/UpdateUserIdentityTest.php
new file mode 100644
index 0000000..d6ca70a
--- /dev/null
+++ b/tests/Unit/Domains/Identity/Actions/IdentityMaintenance/UpdateUserIdentityTest.php
@@ -0,0 +1,64 @@
+create([
+ 'name' => 'Old Name',
+ 'email' => 'test@example.com',
+ 'email_verified_at' => now(),
+ ]);
+
+ $mockUpdateUserRole = $this->mock(UpdateUserRole::class);
+
+ $dto = new UpdateUserIdentityDTO(
+ name: 'New Name',
+ email: 'test@example.com',
+ );
+
+ $action = new UpdateUserIdentity($mockUpdateUserRole);
+ $action->execute($user, $dto);
+
+ expect($user->fresh()->name)->toBe('New Name')
+ ->and($user->fresh()->email)->toBe('test@example.com')
+ ->and($user->fresh()->email_verified_at)->not->toBeNull();
+
+ Notification::assertNothingSent();
+});
+
+test('it resets verification and sends notification when email changes', function () {
+ Notification::fake();
+
+ $user = User::factory()->create([
+ 'name' => 'Old Name',
+ 'email' => 'old@example.com',
+ 'email_verified_at' => now(),
+ ]);
+
+ $mockUpdateUserRole = $this->mock(UpdateUserRole::class);
+
+ $dto = new UpdateUserIdentityDTO(
+ name: 'New Name',
+ email: 'new@example.com',
+ );
+
+ $action = new UpdateUserIdentity($mockUpdateUserRole);
+ $action->execute($user, $dto);
+
+ $updatedUser = $user->fresh();
+ expect($updatedUser->name)->toBe('New Name')
+ ->and($updatedUser->email)->toBe('new@example.com')
+ ->and($updatedUser->email_verified_at)->toBeNull();
+
+ Notification::assertSentTo($updatedUser, VerifyEmailNotification::class);
+});
diff --git a/tests/Unit/Domains/Identity/Actions/IdentityMaintenance/UpdateUserSettingsTest.php b/tests/Unit/Domains/Identity/Actions/IdentityMaintenance/UpdateUserSettingsTest.php
new file mode 100644
index 0000000..829a18a
--- /dev/null
+++ b/tests/Unit/Domains/Identity/Actions/IdentityMaintenance/UpdateUserSettingsTest.php
@@ -0,0 +1,44 @@
+create([
+ 'settings' => [
+ UserSettingKey::LANGUAGE->value => 'id',
+ UserSettingKey::TIMEZONE->value => 'Asia/Jakarta',
+ ],
+ ]);
+
+ $newSettings = [
+ UserSettingKey::LANGUAGE->value => 'en',
+ ];
+
+ $action = new UpdateUserSettings();
+ $action->execute($user, $newSettings);
+
+ $updatedUser = $user->fresh();
+ expect($updatedUser->settings[UserSettingKey::LANGUAGE->value])->toBe('en')
+ ->and($updatedUser->settings[UserSettingKey::TIMEZONE->value])->toBe('Asia/Jakarta');
+});
+
+test('it ignores invalid settings keys', function () {
+ $user = User::factory()->create(['settings' => []]);
+
+ $newSettings = [
+ 'invalid_key' => 'some_value',
+ UserSettingKey::NOTIFICATION->value => 1,
+ ];
+
+ $action = new UpdateUserSettings();
+ $action->execute($user, $newSettings);
+
+ $updatedUser = $user->fresh();
+ expect($updatedUser->settings)->toHaveKey(UserSettingKey::NOTIFICATION->value)
+ ->and($updatedUser->settings)->not->toHaveKey('invalid_key');
+});
diff --git a/tests/Unit/Domains/Identity/Actions/Onboarding/ProvisionNewUserTest.php b/tests/Unit/Domains/Identity/Actions/Onboarding/ProvisionNewUserTest.php
new file mode 100644
index 0000000..f9e8cb6
--- /dev/null
+++ b/tests/Unit/Domains/Identity/Actions/Onboarding/ProvisionNewUserTest.php
@@ -0,0 +1,39 @@
+mock(UpdateUserRole::class, function (MockInterface $mock) {
+ $mock->shouldReceive('execute')
+ ->once()
+ ->with(Mockery::type(User::class), ['admin']);
+ });
+
+ $dto = new ProvisionUserDTO(
+ name: 'Admin User',
+ email: 'admin@example.com',
+ password: 'password',
+ role: 'admin'
+ );
+
+ $action = new ProvisionNewUser($mockUpdateUserRole);
+ $user = $action->execute($dto);
+
+ expect($user)->toBeInstanceOf(User::class)
+ ->and($user->name)->toBe('Admin User')
+ ->and($user->email)->toBe('admin@example.com');
+
+ Event::assertDispatched(UserWasProvisioned::class);
+});
diff --git a/tests/Unit/Domains/Identity/Actions/Onboarding/RegisterSelfServiceUserTest.php b/tests/Unit/Domains/Identity/Actions/Onboarding/RegisterSelfServiceUserTest.php
new file mode 100644
index 0000000..2cebcd4
--- /dev/null
+++ b/tests/Unit/Domains/Identity/Actions/Onboarding/RegisterSelfServiceUserTest.php
@@ -0,0 +1,40 @@
+mock(UpdateUserRole::class, function (MockInterface $mock) {
+ $mock->shouldReceive('execute')
+ ->once()
+ ->with(Mockery::type(User::class), [RoleType::USER]);
+ });
+
+ $dto = new RegisterSelfServiceUserDTO(
+ name: 'John Doe',
+ email: 'john@example.com',
+ password: 'password',
+ );
+
+ $action = new RegisterSelfServiceUser($mockUpdateUserRole);
+ $user = $action->execute($dto);
+
+ expect($user)->toBeInstanceOf(User::class)
+ ->and($user->name)->toBe('John Doe')
+ ->and($user->email)->toBe('john@example.com');
+
+ Event::assertDispatched(Registered::class);
+ Event::assertDispatched(UserWasRegistered::class);
+});
diff --git a/tests/Unit/Domains/Identity/Actions/Onboarding/ResendVerificationEmailTest.php b/tests/Unit/Domains/Identity/Actions/Onboarding/ResendVerificationEmailTest.php
new file mode 100644
index 0000000..3815b61
--- /dev/null
+++ b/tests/Unit/Domains/Identity/Actions/Onboarding/ResendVerificationEmailTest.php
@@ -0,0 +1,20 @@
+unverified()->create();
+
+ $action = new ResendVerificationEmail();
+ $action->execute($user);
+
+ Notification::assertSentTo($user, VerifyEmailNotification::class);
+});
diff --git a/tests/Unit/Domains/Identity/Actions/Onboarding/VerifyUserEmailTest.php b/tests/Unit/Domains/Identity/Actions/Onboarding/VerifyUserEmailTest.php
new file mode 100644
index 0000000..ff5031c
--- /dev/null
+++ b/tests/Unit/Domains/Identity/Actions/Onboarding/VerifyUserEmailTest.php
@@ -0,0 +1,41 @@
+unverified()->create();
+
+ $action = new VerifyUserEmail();
+ $result = $action->execute($user);
+
+ expect($result)->toBeTrue()
+ ->and($user->fresh()->hasVerifiedEmail())->toBeTrue();
+
+ Event::assertDispatched(Verified::class);
+ Event::assertDispatched(UserEmailWasVerified::class);
+});
+
+test('it returns false if email is already verified', function () {
+ Event::fake();
+
+ $user = User::factory()->create([
+ 'email_verified_at' => now(),
+ ]);
+
+ $action = new VerifyUserEmail();
+ $result = $action->execute($user);
+
+ expect($result)->toBeFalse();
+
+ Event::assertNotDispatched(Verified::class);
+ Event::assertNotDispatched(UserEmailWasVerified::class);
+});
diff --git a/tests/Unit/Domains/Identity/Actions/Passwords/ResetUserPasswordTest.php b/tests/Unit/Domains/Identity/Actions/Passwords/ResetUserPasswordTest.php
new file mode 100644
index 0000000..fe45292
--- /dev/null
+++ b/tests/Unit/Domains/Identity/Actions/Passwords/ResetUserPasswordTest.php
@@ -0,0 +1,60 @@
+create([
+ 'email' => 'test@example.com',
+ 'password' => 'old_password',
+ ]);
+
+ $token = Password::createToken($user);
+
+ $dto = new ResetPasswordDTO(
+ token: $token,
+ email: 'test@example.com',
+ password: 'new_password',
+ password_confirmation: 'new_password',
+ );
+
+ $action = new ResetUserPassword();
+ $status = $action->execute($dto);
+
+ expect($status)->toBe(Password::PASSWORD_RESET)
+ ->and(Hash::check('new_password', $user->fresh()->password))->toBeTrue();
+
+ Event::assertDispatched(PasswordReset::class);
+ Event::assertDispatched(UserPasswordReset::class);
+});
+
+test('it fails to reset password with invalid token', function () {
+ $user = User::factory()->create([
+ 'email' => 'test@example.com',
+ 'password' => 'old_password',
+ ]);
+
+ $dto = new ResetPasswordDTO(
+ token: 'invalid-token',
+ email: 'test@example.com',
+ password: 'new_password',
+ password_confirmation: 'new_password',
+ );
+
+ $action = new ResetUserPassword();
+ $status = $action->execute($dto);
+
+ expect($status)->toBe(Password::INVALID_TOKEN)
+ ->and(Hash::check('old_password', $user->fresh()->password))->toBeTrue();
+});
diff --git a/tests/Unit/Domains/Identity/Actions/Passwords/SendPasswordResetLinkTest.php b/tests/Unit/Domains/Identity/Actions/Passwords/SendPasswordResetLinkTest.php
new file mode 100644
index 0000000..f099b46
--- /dev/null
+++ b/tests/Unit/Domains/Identity/Actions/Passwords/SendPasswordResetLinkTest.php
@@ -0,0 +1,38 @@
+create([
+ 'email' => 'test@example.com',
+ ]);
+
+ $dto = new ForgotPasswordDTO(
+ email: 'test@example.com',
+ );
+
+ $action = new SendPasswordResetLink();
+ $status = $action->execute($dto);
+
+ expect($status)->toBe(Password::RESET_LINK_SENT);
+});
+
+test('it returns user not found if email does not exist', function () {
+ $dto = new ForgotPasswordDTO(
+ email: 'nonexistent@example.com',
+ );
+
+ $action = new SendPasswordResetLink();
+ $status = $action->execute($dto);
+
+ expect($status)->toBe(Password::INVALID_USER);
+});
diff --git a/tests/Unit/Domains/Identity/Actions/Passwords/UpdatePasswordTest.php b/tests/Unit/Domains/Identity/Actions/Passwords/UpdatePasswordTest.php
new file mode 100644
index 0000000..fc62d1c
--- /dev/null
+++ b/tests/Unit/Domains/Identity/Actions/Passwords/UpdatePasswordTest.php
@@ -0,0 +1,25 @@
+create([
+ 'password' => 'old_password',
+ ]);
+
+ $dto = new UpdatePasswordDTO(
+ new_password: 'new_password',
+ );
+
+ $action = new UpdatePassword();
+ $action->execute($user, $dto);
+
+ expect(Hash::check('new_password', $user->fresh()->password))->toBeTrue()
+ ->and(Hash::check('old_password', $user->fresh()->password))->toBeFalse();
+});
diff --git a/tests/Unit/Domains/Identity/Enums/RoleTypeTest.php b/tests/Unit/Domains/Identity/Enums/RoleTypeTest.php
new file mode 100644
index 0000000..ada8433
--- /dev/null
+++ b/tests/Unit/Domains/Identity/Enums/RoleTypeTest.php
@@ -0,0 +1,33 @@
+value)->toBe('System Administrator')
+ ->and(RoleType::ADMIN->value)->toBe('Administrator')
+ ->and(RoleType::USER->value)->toBe('User');
+});
+
+test('it returns permissions with correct structure', function () {
+ $permissions = RoleType::permissions();
+
+ expect($permissions)->toBeArray()->not->toBeEmpty();
+
+ foreach ($permissions as $permission) {
+ expect($permission)->toHaveKeys(['name', 'description', 'group', 'guard_name', 'roles'])
+ ->and($permission['roles'])->toBeArray();
+ }
+});
+
+test('it includes generated policies', function () {
+ $permissions = RoleType::permissions();
+ $names = array_column($permissions, 'name');
+
+ expect($names)->toContain('role.viewAny')
+ ->toContain('role.create')
+ ->toContain('user.viewAny')
+ ->toContain('user.delete');
+});
diff --git a/tests/Unit/Domains/Identity/Enums/UserSettingKeyTest.php b/tests/Unit/Domains/Identity/Enums/UserSettingKeyTest.php
new file mode 100644
index 0000000..7a570a4
--- /dev/null
+++ b/tests/Unit/Domains/Identity/Enums/UserSettingKeyTest.php
@@ -0,0 +1,48 @@
+label())->toBe(__('domains/account/enum.user_settings.notification'))
+ ->and(UserSettingKey::LANGUAGE->label())->toBe(__('domains/account/enum.user_settings.language'))
+ ->and(UserSettingKey::TIMEZONE->label())->toBe(__('domains/account/enum.user_settings.timezone'));
+});
+
+test('it has correct defaults', function () {
+ expect(UserSettingKey::NOTIFICATION->default())->toBe(0)
+ ->and(UserSettingKey::LANGUAGE->default())->toBe('en')
+ ->and(UserSettingKey::TIMEZONE->default())->toBe('UTC');
+});
+
+test('it has correct types', function () {
+ expect(UserSettingKey::NOTIFICATION->type())->toBe('option')
+ ->and(UserSettingKey::LANGUAGE->type())->toBe('option')
+ ->and(UserSettingKey::TIMEZONE->type())->toBe('option');
+});
+
+test('it has correct options', function () {
+ expect(UserSettingKey::LANGUAGE->options())->toHaveKeys(['en', 'id'])
+ ->and(UserSettingKey::TIMEZONE->options())->toHaveKey('UTC')
+ ->and(UserSettingKey::NOTIFICATION->options())->toHaveKeys([1, 0]);
+});
+
+test('it returns correct validation rules', function () {
+ expect(UserSettingKey::LANGUAGE->validation())->toContain('required');
+});
+
+test('it can apply effects', function () {
+ // Test Language effect
+ UserSettingKey::effect('language', 'id');
+ expect(app()->getLocale())->toBe('id');
+
+ // Test Timezone effect
+ UserSettingKey::effect('timezone', 'Asia/Jakarta');
+ expect(date_default_timezone_get())->toBe('Asia/Jakarta');
+
+ // Reset
+ UserSettingKey::effect('language', 'en');
+ UserSettingKey::effect('timezone', 'UTC');
+});
diff --git a/tests/Unit/Domains/Identity/Exports/UserExportTest.php b/tests/Unit/Domains/Identity/Exports/UserExportTest.php
new file mode 100644
index 0000000..0b02f93
--- /dev/null
+++ b/tests/Unit/Domains/Identity/Exports/UserExportTest.php
@@ -0,0 +1,40 @@
+create([
+ 'name' => 'John Doe',
+ 'email' => 'john@example.com',
+ ]);
+
+ $profile = new Profile([
+ 'gender' => GenderOption::MALE,
+ 'date_of_birth' => '1990-01-01',
+ 'phone_number' => '1234567890',
+ ]);
+ $user->profile()->save($profile);
+
+ $export = new UserExport();
+ $mapped = $export->map($user);
+
+ $this->assertEquals('=ROW()-1', $mapped[0]);
+ $this->assertEquals('John Doe', $mapped[1]);
+ $this->assertEquals('john@example.com', $mapped[2]);
+ $this->assertEquals($profile->gender->label(), $mapped[3]);
+ $this->assertEquals('1990-01-01', $mapped[4]->format('Y-m-d'));
+ $this->assertEquals('1234567890', $mapped[5]);
+ }
+}
diff --git a/tests/Unit/Domains/Identity/Queries/Dashboard/GetMonthlyNewUsersTest.php b/tests/Unit/Domains/Identity/Queries/Dashboard/GetMonthlyNewUsersTest.php
new file mode 100644
index 0000000..0ca7ed6
--- /dev/null
+++ b/tests/Unit/Domains/Identity/Queries/Dashboard/GetMonthlyNewUsersTest.php
@@ -0,0 +1,36 @@
+count(2)->create(['created_at' => '2024-04-10']);
+
+ // This month (May)
+ User::factory()->count(3)->create(['created_at' => '2024-05-01']);
+
+ $query = new GetMonthlyNewUsers();
+ $results = $query->fetch();
+
+ expect($results['new_users'])->toBe(3)
+ ->and($results['growth_rate'])->toBe('+50.00%'); // (3-2)/2 * 100 = 50
+});
+
+test('it returns 100% growth when no users joined last month', function () {
+ Carbon::setTestNow('2024-05-15');
+
+ User::factory()->count(3)->create(['created_at' => '2024-05-01']);
+
+ $query = new GetMonthlyNewUsers();
+ $results = $query->fetch();
+
+ expect($results['new_users'])->toBe(3)
+ ->and($results['growth_rate'])->toBe('+100%');
+});
diff --git a/tests/Unit/Domains/Identity/Queries/Dashboard/GetRoleDistributionsTest.php b/tests/Unit/Domains/Identity/Queries/Dashboard/GetRoleDistributionsTest.php
new file mode 100644
index 0000000..7a800db
--- /dev/null
+++ b/tests/Unit/Domains/Identity/Queries/Dashboard/GetRoleDistributionsTest.php
@@ -0,0 +1,28 @@
+ 'admin', 'guard_name' => 'web']);
+ $editor = Role::create(['name' => 'editor', 'guard_name' => 'web']);
+
+ $user1 = User::factory()->create();
+ $user1->assignRole($admin);
+
+ $user2 = User::factory()->create();
+ $user2->assignRole($editor);
+
+ $user3 = User::factory()->create();
+ $user3->assignRole($editor);
+
+ $query = new GetRoleDistributions();
+ $results = $query->fetch();
+
+ expect($results['categories'])->toContain('admin', 'editor')
+ ->and($results['series'])->toContain(1, 2);
+});
diff --git a/tests/Unit/Domains/Identity/Queries/Dashboard/GetTotalUsersTest.php b/tests/Unit/Domains/Identity/Queries/Dashboard/GetTotalUsersTest.php
new file mode 100644
index 0000000..139b967
--- /dev/null
+++ b/tests/Unit/Domains/Identity/Queries/Dashboard/GetTotalUsersTest.php
@@ -0,0 +1,36 @@
+count(2)->create(['created_at' => '2024-04-10']);
+
+ // This month (May)
+ User::factory()->count(3)->create(['created_at' => '2024-05-01']);
+
+ $query = new GetTotalUsers();
+ $results = $query->fetch();
+
+ expect($results['total_users'])->toBe(5)
+ ->and($results['growth_rate'])->toBe('+150.00%'); // (5-2)/2 * 100 = 150
+});
+
+test('it returns 100% growth when no users joined last month', function () {
+ Carbon::setTestNow('2024-05-15');
+
+ User::factory()->count(3)->create(['created_at' => '2024-05-01']);
+
+ $query = new GetTotalUsers();
+ $results = $query->fetch();
+
+ expect($results['total_users'])->toBe(3)
+ ->and($results['growth_rate'])->toBe('+100%');
+});
diff --git a/tests/Unit/Domains/Identity/Queries/Dashboard/GetUserGrowthTrendsTest.php b/tests/Unit/Domains/Identity/Queries/Dashboard/GetUserGrowthTrendsTest.php
new file mode 100644
index 0000000..c2c8882
--- /dev/null
+++ b/tests/Unit/Domains/Identity/Queries/Dashboard/GetUserGrowthTrendsTest.php
@@ -0,0 +1,34 @@
+create(['created_at' => '2024-01-01']);
+ User::factory()->create(['created_at' => '2024-01-15']);
+
+ // March
+ User::factory()->create(['created_at' => '2024-03-10']);
+
+ // May
+ User::factory()->create(['created_at' => '2024-05-01']);
+
+ $query = new GetUserGrowthTrends();
+ $results = $query->fetch();
+
+ expect($results['categories'])->toHaveCount(12)
+ ->and($results['categories'][0])->toBe('January')
+ ->and($results['series'])->toHaveCount(12)
+ ->and($results['series'][0])->toBe(2) // Jan
+ ->and($results['series'][1])->toBe(0) // Feb
+ ->and($results['series'][2])->toBe(1) // Mar
+ ->and($results['series'][3])->toBe(0) // Apr
+ ->and($results['series'][4])->toBe(1); // May
+});
diff --git a/tests/Unit/Domains/Identity/Queries/Dashboard/GetUserVerificationRatesTest.php b/tests/Unit/Domains/Identity/Queries/Dashboard/GetUserVerificationRatesTest.php
new file mode 100644
index 0000000..a883748
--- /dev/null
+++ b/tests/Unit/Domains/Identity/Queries/Dashboard/GetUserVerificationRatesTest.php
@@ -0,0 +1,39 @@
+count(3)->create([
+ 'email_verified_at' => now(),
+ ]);
+
+ // Create 2 unverified users
+ User::factory()->count(2)->create([
+ 'email_verified_at' => null,
+ ]);
+
+ $query = new GetUserVerificationRates();
+ $results = $query->fetch();
+
+ expect($results)->toBe([
+ 'verified' => 3,
+ 'unverified' => 2,
+ 'verification_rate' => 60.0, // (3/5) * 100
+ ]);
+});
+
+test('it returns zero rates when no users exist', function () {
+ $query = new GetUserVerificationRates();
+ $results = $query->fetch();
+
+ expect($results)->toBe([
+ 'verified' => 0,
+ 'unverified' => 0,
+ 'verification_rate' => 0,
+ ]);
+});
diff --git a/tests/Unit/Domains/Identity/Queries/Lookup/RoleLookupTest.php b/tests/Unit/Domains/Identity/Queries/Lookup/RoleLookupTest.php
new file mode 100644
index 0000000..c28c069
--- /dev/null
+++ b/tests/Unit/Domains/Identity/Queries/Lookup/RoleLookupTest.php
@@ -0,0 +1,43 @@
+ 'admin', 'guard_name' => 'web']);
+ Role::create(['name' => 'editor', 'guard_name' => 'web']);
+
+ $lookup = new RoleLookup();
+ $results = $lookup->fetch(null);
+
+ expect($results)->toHaveCount(2)
+ ->and($results->pluck('name'))->toContain('admin', 'editor');
+});
+
+test('it can filter roles by name', function () {
+ Role::create(['name' => 'admin', 'guard_name' => 'web']);
+ Role::create(['name' => 'editor', 'guard_name' => 'web']);
+ Role::create(['name' => 'viewer', 'guard_name' => 'web']);
+
+ $lookup = new RoleLookup();
+
+ $results = $lookup->fetch('edit');
+ expect($results)->toHaveCount(1)
+ ->and($results->first()->name)->toBe('editor');
+
+ $results = $lookup->fetch('v');
+ expect($results)->toHaveCount(1)
+ ->and($results->first()->name)->toBe('viewer');
+});
+
+test('it returns empty collection when no roles match', function () {
+ Role::create(['name' => 'admin', 'guard_name' => 'web']);
+
+ $lookup = new RoleLookup();
+ $results = $lookup->fetch('non-existent');
+
+ expect($results)->toBeEmpty();
+});
diff --git a/tests/Unit/Domains/System/Actions/Backup/UploadBackupFileTest.php b/tests/Unit/Domains/System/Actions/Backup/UploadBackupFileTest.php
new file mode 100644
index 0000000..d6f50e0
--- /dev/null
+++ b/tests/Unit/Domains/System/Actions/Backup/UploadBackupFileTest.php
@@ -0,0 +1,35 @@
+create('backup.zip');
+
+ $syncBackupCatalog = Mockery::mock(SyncBackupCatalog::class);
+ $syncBackupCatalog->shouldReceive('execute')->once();
+
+ $action = new UploadBackupFile($syncBackupCatalog);
+ $action->execute($file);
+
+ Storage::disk('test-disk')->assertExists('test-backup-dir/backup.zip');
+ }
+}
diff --git a/tests/Unit/Domains/System/Actions/Files/PruneOrphanedFilesTest.php b/tests/Unit/Domains/System/Actions/Files/PruneOrphanedFilesTest.php
new file mode 100644
index 0000000..0fc4261
--- /dev/null
+++ b/tests/Unit/Domains/System/Actions/Files/PruneOrphanedFilesTest.php
@@ -0,0 +1,62 @@
+shouldReceive('get')->andReturnNull();
+
+ // 1. Create a DB orphan: A record in 'files' table that references nothing
+ File::create([
+ 'fileable_type' => User::class,
+ 'fileable_id' => '999', // Non-existent
+ 'relation_name' => 'avatar',
+ 'name' => 'db_orphan.txt',
+ 'mime_type' => 'text/plain',
+ 'size' => 1024,
+ 'disk' => 'public',
+ 'path' => 'avatars/db_orphan.txt',
+ 'uploader_id' => 'user1',
+ ]);
+
+ // 2. Create a physical orphan: A file on disk that isn't in the DB
+ Storage::disk('public')->put('orphaned_file.txt', 'content');
+
+ // 3. Create a valid file (should not be deleted)
+ $user = User::factory()->create(['id' => '1']);
+ File::create([
+ 'fileable_type' => User::class,
+ 'fileable_id' => $user->id,
+ 'relation_name' => 'avatar',
+ 'name' => 'valid.txt',
+ 'mime_type' => 'text/plain',
+ 'size' => 1024,
+ 'disk' => 'public',
+ 'path' => 'avatars/valid.txt',
+ 'uploader_id' => 'user1',
+ ]);
+ Storage::disk('public')->put('avatars/valid.txt', 'content');
+
+ $action = new PruneOrphanedFiles($settingQuery);
+ $stats = $action->execute('public', '/');
+
+ // Assertions
+ expect($stats['db_orphans_removed'])->toBe(1)
+ ->and($stats['disk_orphans_removed'])->toBe(1);
+
+ expect(File::count())->toBe(1); // Only the valid one remains
+ Storage::disk('public')->assertMissing('orphaned_file.txt');
+ Storage::disk('public')->assertExists('avatars/valid.txt');
+});
diff --git a/tests/Unit/Domains/System/Actions/Files/RemoveModelFileTest.php b/tests/Unit/Domains/System/Actions/Files/RemoveModelFileTest.php
new file mode 100644
index 0000000..a57339e
--- /dev/null
+++ b/tests/Unit/Domains/System/Actions/Files/RemoveModelFileTest.php
@@ -0,0 +1,54 @@
+ 'User',
+ 'fileable_id' => '1',
+ 'relation_name' => 'avatar',
+ 'name' => 'file1.txt',
+ 'mime_type' => 'text/plain',
+ 'size' => 1024,
+ 'disk' => 'public',
+ 'path' => 'path/to/file1.txt',
+ 'uploader_id' => 'u1',
+ ]);
+
+ File::create([
+ 'fileable_type' => 'User',
+ 'fileable_id' => '1',
+ 'relation_name' => 'documents',
+ 'name' => 'file2.txt',
+ 'mime_type' => 'text/plain',
+ 'size' => 1024,
+ 'disk' => 'public',
+ 'path' => 'path/to/file2.txt',
+ 'uploader_id' => 'u1',
+ ]);
+
+ // Create a file for a different model/id to ensure no accidental deletion
+ File::create([
+ 'fileable_type' => 'Post',
+ 'fileable_id' => '2',
+ 'relation_name' => 'attachments',
+ 'name' => 'file3.txt',
+ 'mime_type' => 'text/plain',
+ 'size' => 1024,
+ 'disk' => 'public',
+ 'path' => 'path/to/file3.txt',
+ 'uploader_id' => 'u1',
+ ]);
+
+ $action = new RemoveModelFile();
+ $action->execute('User', '1');
+
+ expect(File::where('fileable_type', 'User')->where('fileable_id', '1')->count())->toBe(0)
+ ->and(File::where('fileable_type', 'Post')->where('fileable_id', '2')->count())->toBe(1);
+});
diff --git a/tests/Unit/Domains/System/Actions/Files/ReplaceSingleFileTest.php b/tests/Unit/Domains/System/Actions/Files/ReplaceSingleFileTest.php
new file mode 100644
index 0000000..20ced4f
--- /dev/null
+++ b/tests/Unit/Domains/System/Actions/Files/ReplaceSingleFileTest.php
@@ -0,0 +1,68 @@
+ 'User',
+ 'fileable_id' => '123',
+ 'relation_name' => 'avatar',
+ 'name' => 'old.txt',
+ 'mime_type' => 'text/plain',
+ 'size' => 1024,
+ 'disk' => 'public',
+ 'path' => 'avatars/old.txt',
+ 'uploader_id' => 'user1',
+ ]);
+
+ // Mock the upload action
+ $uploadAction = Mockery::mock(UploadAndAttachFile::class);
+
+ $newFile = UploadedFile::fake()->create('new.txt', 2048, 'text/plain');
+ $dto = new FileDTO(
+ modelType: 'User',
+ modelId: '123',
+ relationName: 'avatar',
+ directory: 'avatars',
+ disk: 'public',
+ options: [],
+ uploaderId: 'user1'
+ );
+
+ $newFileRecord = new File([
+ 'fileable_type' => 'User',
+ 'fileable_id' => '123',
+ 'relation_name' => 'avatar',
+ 'name' => 'new.txt',
+ 'mime_type' => 'text/plain',
+ 'size' => 2048,
+ 'disk' => 'public',
+ 'path' => 'avatars/new.txt',
+ 'uploader_id' => 'user1',
+ ]);
+
+ $uploadAction->shouldReceive('execute')
+ ->once()
+ ->with($newFile, $dto)
+ ->andReturn($newFileRecord);
+
+ $action = new ReplaceSingleFile($uploadAction);
+ $result = $action->execute($newFile, $dto);
+
+ // Check old file is deleted
+ expect(File::find($oldFile->id))->toBeNull();
+ // Check new file returned
+ expect($result->name)->toBe('new.txt');
+});
diff --git a/tests/Unit/Domains/System/Actions/Files/UploadAndAttachFileTest.php b/tests/Unit/Domains/System/Actions/Files/UploadAndAttachFileTest.php
new file mode 100644
index 0000000..72813a3
--- /dev/null
+++ b/tests/Unit/Domains/System/Actions/Files/UploadAndAttachFileTest.php
@@ -0,0 +1,38 @@
+create('test.txt', 1024, 'text/plain');
+ $dto = new FileDTO(
+ modelType: 'User',
+ modelId: '123',
+ relationName: 'avatar',
+ directory: 'avatars',
+ disk: 'public',
+ options: [],
+ uploaderId: 'user1'
+ );
+
+ // We expect File::create to be called. Since File is a model, we can mock it or use a database transaction if we had RefreshDatabase
+ // Let's use a simple mock for File if possible or just use the database
+
+ $action = new UploadAndAttachFile();
+ $result = $action->execute($file, $dto);
+
+ expect($result)->toBeInstanceOf(File::class)
+ ->and($result->name)->toBe('test.txt')
+ ->and($result->disk)->toBe('public');
+
+ Storage::disk('public')->assertExists($result->path);
+});
diff --git a/tests/Unit/Domains/System/Actions/Integration/RunGenericImportPipelineTest.php b/tests/Unit/Domains/System/Actions/Integration/RunGenericImportPipelineTest.php
new file mode 100644
index 0000000..301173a
--- /dev/null
+++ b/tests/Unit/Domains/System/Actions/Integration/RunGenericImportPipelineTest.php
@@ -0,0 +1,39 @@
+id();
+ $table->string('name');
+ $table->string('external_id')->unique();
+ });
+
+ $rows = collect([
+ ['external_id' => '1', 'name' => 'Item 1'],
+ ['external_id' => '2', 'name' => 'Item 2'],
+ ]);
+
+ $mapper = Mockery::mock(DataPayloadMapper::class);
+ $mapper->shouldReceive('getLookupKey')->andReturn('external_id');
+ $mapper->shouldReceive('transform')->twice()->andReturnUsing(fn($row) => $row);
+ $mapper->shouldReceive('updateOrCreateDomainState')->twice();
+
+ $action = new RunGenericImportPipeline();
+ $action->execute($rows, $mapper, get_class($dummyModel));
+});
diff --git a/tests/Unit/Domains/System/Actions/Settings/ResolveIpTimezoneTest.php b/tests/Unit/Domains/System/Actions/Settings/ResolveIpTimezoneTest.php
new file mode 100644
index 0000000..7ca47a3
--- /dev/null
+++ b/tests/Unit/Domains/System/Actions/Settings/ResolveIpTimezoneTest.php
@@ -0,0 +1,52 @@
+shouldReceive('get')->with(SystemSettingKey::TIMEZONE)->andReturn('Asia/Jakarta');
+
+ $action = new ResolveIpTimezone($getSystemSettings);
+
+ expect($action->execute('127.0.0.1'))->toBe('Asia/Jakarta');
+});
+
+test('it fetches timezone from API and caches it', function () {
+ $getSystemSettings = Mockery::mock(GetSystemSettings::class);
+ $getSystemSettings->shouldReceive('get')->with(SystemSettingKey::TIMEZONE)->andReturn('UTC');
+
+ Http::fake([
+ 'ip-api.com/*' => Http::response(['timezone' => 'Asia/Makassar'], 200),
+ ]);
+
+ $action = new ResolveIpTimezone($getSystemSettings);
+
+ expect($action->execute('1.1.1.1'))->toBe('Asia/Makassar');
+
+ // Verify cache is set
+ expect(Cache::get('timezone_ip_1.1.1.1'))->toBe('Asia/Makassar');
+});
+
+test('it falls back to system timezone if API fails', function () {
+ $getSystemSettings = Mockery::mock(GetSystemSettings::class);
+ $getSystemSettings->shouldReceive('get')->with(SystemSettingKey::TIMEZONE)->andReturn('UTC');
+
+ Http::fake([
+ 'ip-api.com/*' => Http::response(null, 500),
+ ]);
+
+ $action = new ResolveIpTimezone($getSystemSettings);
+
+ expect($action->execute('1.1.1.1'))->toBe('UTC');
+});
diff --git a/tests/Unit/Domains/System/Casts/ByteHumanReadableTest.php b/tests/Unit/Domains/System/Casts/ByteHumanReadableTest.php
new file mode 100644
index 0000000..1be7924
--- /dev/null
+++ b/tests/Unit/Domains/System/Casts/ByteHumanReadableTest.php
@@ -0,0 +1,51 @@
+get($model, 'bytes', 1024, []);
+
+ expect($result)->toBeInstanceOf(ByteUsage::class)
+ ->and($result->bytes)->toBe(1024);
+});
+
+test('it returns null for null value in get', function () {
+ $cast = new ByteHumanReadable();
+ $model = new class extends Model {};
+
+ expect($cast->get($model, 'bytes', null, []))->toBeNull();
+});
+
+test('it sets integer to integer', function () {
+ $cast = new ByteHumanReadable();
+ $model = new class extends Model {};
+
+ expect($cast->set($model, 'bytes', 1024, []))->toBe(1024);
+});
+
+test('it sets ByteUsage to bytes', function () {
+ $cast = new ByteHumanReadable();
+ $model = new class extends Model {};
+ $usage = new ByteUsage(1024);
+
+ expect($cast->set($model, 'bytes', $usage, []))->toBe(1024);
+});
+
+test('it returns null for null value in set', function () {
+ $cast = new ByteHumanReadable();
+ $model = new class extends Model {};
+
+ expect($cast->set($model, 'bytes', null, []))->toBeNull();
+});
+
+test('it throws exception for invalid type in set', function () {
+ $cast = new ByteHumanReadable();
+ $model = new class extends Model {};
+
+ expect(fn() => $cast->set($model, 'bytes', 'invalid', []))->toThrow(InvalidArgumentException::class);
+});
diff --git a/tests/Unit/Domains/System/Enums/SystemSettingKeyTest.php b/tests/Unit/Domains/System/Enums/SystemSettingKeyTest.php
new file mode 100644
index 0000000..38209ce
--- /dev/null
+++ b/tests/Unit/Domains/System/Enums/SystemSettingKeyTest.php
@@ -0,0 +1,34 @@
+inputType()->value)->toBe('file')
+ ->and(SystemSettingKey::WEB_DESCRIPTION->inputType()->value)->toBe('textarea')
+ ->and(SystemSettingKey::WEB_NAME->inputType()->value)->toBe('input');
+});
+
+test('it has correct defaults', function () {
+ expect(SystemSettingKey::DEFAULT_LANGUAGE->default())->toBe('en')
+ ->and(SystemSettingKey::TIMEZONE->default())->toBe('UTC')
+ ->and(SystemSettingKey::WEB_NAME->default())->toBe('Acme Inc');
+});
+
+test('it has correct options for select fields', function () {
+ expect(SystemSettingKey::DEFAULT_LANGUAGE->options())->toHaveKeys(['en', 'id'])
+ ->and(SystemSettingKey::TIMEZONE->options())->toHaveKey('UTC');
+});
+
+test('it identifies images correctly', function () {
+ expect(SystemSettingKey::WEB_LOGO->isImage())->toBeTrue()
+ ->and(SystemSettingKey::WEB_FAVICON->isImage())->toBeTrue()
+ ->and(SystemSettingKey::WEB_NAME->isImage())->toBeFalse();
+});
+
+test('it has correct validation for images', function () {
+ expect(SystemSettingKey::WEB_LOGO->validation())->toContain('file')
+ ->toContain('required');
+});
diff --git a/tests/Unit/Domains/System/Queries/GetModelAuditLogTest.php b/tests/Unit/Domains/System/Queries/GetModelAuditLogTest.php
new file mode 100644
index 0000000..8261d19
--- /dev/null
+++ b/tests/Unit/Domains/System/Queries/GetModelAuditLogTest.php
@@ -0,0 +1,25 @@
+shouldReceive('audits')->once()->andReturn($audits);
+ $audits->shouldReceive('with')->once()->with('user')->andReturnSelf();
+ $audits->shouldReceive('latest')->once()->andReturnSelf();
+ $audits->shouldReceive('limit')->once()->with(5)->andReturnSelf();
+ $audits->shouldReceive('get')->once()->andReturn(new Collection());
+
+ $query = new GetModelAuditLog();
+ $result = $query->get($model);
+
+ expect($result)->toBeInstanceOf(Collection::class);
+});
diff --git a/tests/Unit/Domains/System/Queries/GetSystemSettingsTest.php b/tests/Unit/Domains/System/Queries/GetSystemSettingsTest.php
new file mode 100644
index 0000000..0e12832
--- /dev/null
+++ b/tests/Unit/Domains/System/Queries/GetSystemSettingsTest.php
@@ -0,0 +1,79 @@
+get(SystemSettingKey::DEFAULT_LANGUAGE);
+
+ expect($value)->toBe(SystemSettingKey::DEFAULT_LANGUAGE->default());
+});
+
+test('it returns value from database when it exists', function () {
+ SystemSettings::create([
+ 'key' => SystemSettingKey::WEB_NAME->value,
+ 'value' => 'Custom Web Name',
+ ]);
+
+ $query = new GetSystemSettings();
+ $value = $query->get(SystemSettingKey::WEB_NAME);
+
+ expect($value)->toBe('Custom Web Name');
+});
+
+test('it caches settings', function () {
+ SystemSettings::create([
+ 'key' => SystemSettingKey::WEB_NAME->value,
+ 'value' => 'Cached Name',
+ ]);
+
+ $query = new GetSystemSettings();
+
+ // First call, should fetch from DB
+ expect($query->get(SystemSettingKey::WEB_NAME))->toBe('Cached Name');
+
+ // Update DB directly
+ SystemSettings::where('key', SystemSettingKey::WEB_NAME->value)->update(['value' => 'Updated Name']);
+
+ // Call again, should still return cached value
+ expect($query->get(SystemSettingKey::WEB_NAME))->toBe('Cached Name');
+
+ // Flush memory, should still be cached in Laravel Cache
+ $query->flushMemory();
+ expect($query->get(SystemSettingKey::WEB_NAME))->toBe('Cached Name');
+
+ // Clear Cache
+ Cache::flush();
+ $query->flushMemory();
+
+ // Should now fetch updated value
+ expect($query->get(SystemSettingKey::WEB_NAME))->toBe('Updated Name');
+});
+
+test('it flushes memory correctly', function () {
+ $query = new GetSystemSettings();
+
+ // Fill memory
+ $query->fetch();
+
+ $reflection = new ReflectionClass($query);
+ $property = $reflection->getProperty('settings');
+ $property->setAccessible(true);
+
+ expect($property->getValue($query))->not->toBeNull();
+
+ $query->flushMemory();
+
+ expect($property->getValue($query))->toBeNull();
+});
diff --git a/tests/Unit/Domains/System/Support/ValueObjects/ByteUsageTest.php b/tests/Unit/Domains/System/Support/ValueObjects/ByteUsageTest.php
new file mode 100644
index 0000000..e5167a1
--- /dev/null
+++ b/tests/Unit/Domains/System/Support/ValueObjects/ByteUsageTest.php
@@ -0,0 +1,42 @@
+ new ByteUsage(-1))->toThrow(InvalidArgumentException::class, 'Bytes cannot be negative.');
+});
+
+test('it formats 0 bytes correctly', function () {
+ $usage = new ByteUsage(0);
+ expect($usage->format())->toBe('0 B');
+});
+
+test('it formats bytes to B correctly', function () {
+ $usage = new ByteUsage(500);
+ expect($usage->format())->toBe('500 B');
+});
+
+test('it formats bytes to KB correctly', function () {
+ $usage = new ByteUsage(1024);
+ expect($usage->format())->toBe('1 KB');
+});
+
+test('it formats bytes to MB correctly', function () {
+ $usage = new ByteUsage(1024 * 1024);
+ expect($usage->format())->toBe('1 MB');
+});
+
+test('it formats bytes to GB correctly', function () {
+ $usage = new ByteUsage(1024 * 1024 * 1024);
+ expect($usage->format())->toBe('1 GB');
+});
+
+test('it formats with custom precision', function () {
+ $usage = new ByteUsage(1024 + 512); // 1.5 KB
+ expect($usage->format(1))->toBe('1.5 KB');
+});
+
+test('it implements Stringable', function () {
+ $usage = new ByteUsage(1024);
+ expect((string) $usage)->toBe('1 KB');
+});
diff --git a/tests/Unit/Domains/System/Traits/Model/HasFileTest.php b/tests/Unit/Domains/System/Traits/Model/HasFileTest.php
new file mode 100644
index 0000000..dcb350c
--- /dev/null
+++ b/tests/Unit/Domains/System/Traits/Model/HasFileTest.php
@@ -0,0 +1,29 @@
+hasSingleFile('avatar'))->toBeInstanceOf(MorphOne::class);
+});
+
+test('it returns correct morph relation for multi file', function () {
+ $model = new class extends Model {
+ use HasFile;
+ protected $table = 'test_models';
+ };
+
+ expect($model->hasMultiFile('documents'))->toBeInstanceOf(MorphMany::class);
+});
diff --git a/tests/Unit/Http/Ingestion/Excel/Identity/UserImportTest.php b/tests/Unit/Http/Ingestion/Excel/Identity/UserImportTest.php
new file mode 100644
index 0000000..163df75
--- /dev/null
+++ b/tests/Unit/Http/Ingestion/Excel/Identity/UserImportTest.php
@@ -0,0 +1,34 @@
+app->instance(RunGenericImportPipeline::class, $pipeline);
+ $this->app->instance(UserDataMapper::class, Mockery::mock(UserDataMapper::class));
+
+ $rows = new Collection([
+ ['name' => 'Test User', 'email' => 'test@example.com'],
+ ]);
+
+ $pipeline->shouldReceive('execute')
+ ->once()
+ ->with(
+ Mockery::on(fn($r) => $r === $rows),
+ Mockery::type(UserDataMapper::class),
+ User::class
+ );
+
+ $import = new UserImport();
+ $import->collection($rows);
+});
diff --git a/tests/Unit/Http/Middleware/HandleLayoutDataAttributesTest.php b/tests/Unit/Http/Middleware/HandleLayoutDataAttributesTest.php
new file mode 100644
index 0000000..4ca90ba
--- /dev/null
+++ b/tests/Unit/Http/Middleware/HandleLayoutDataAttributesTest.php
@@ -0,0 +1,53 @@
+shouldReceive('callAction'); // Method needed by ViewController
+
+ // Make it pass the ViewController check
+ $controller->shouldReceive('__destruct'); // Just to make it mockable
+
+ $route = Mockery::mock(Route::class);
+
+ // Set up route call chain properly
+ $route->shouldReceive('getController')
+ ->andReturn($controller);
+
+ $route->shouldReceive('getActionMethod')
+ ->andReturn('index');
+
+ $route->shouldReceive('parameters')
+ ->andReturn(['layout_data' => $layoutData]);
+
+ $request = Request::create('/', 'GET');
+ $request->setRouteResolver(fn() => $route);
+
+ $applyLayout->shouldReceive('execute')
+ ->with($layoutData, ['layout_data' => $layoutData]);
+
+ $next = fn($req) => new Response();
+
+ $response = $middleware->handle($request, $next);
+ expect($response)->toBeInstanceOf(Response::class);
+});
diff --git a/tests/Unit/Http/Middleware/HandlePreferredLanguageTest.php b/tests/Unit/Http/Middleware/HandlePreferredLanguageTest.php
new file mode 100644
index 0000000..80c9247
--- /dev/null
+++ b/tests/Unit/Http/Middleware/HandlePreferredLanguageTest.php
@@ -0,0 +1,61 @@
+shouldReceive('get')->with(SystemSettingKey::DEFAULT_LANGUAGE)->andReturn('en');
+
+ $middleware = new HandlePreferredLanguage($getSystemSettings);
+ $request = Request::create('/', 'GET');
+ $next = fn($req) => new Response();
+
+ $middleware->handle($request, $next);
+
+ expect(app()->getLocale())->toBe('en');
+});
+
+test('it uses session locale if set', function () {
+ session()->put('locale', 'id');
+ $getSystemSettings = Mockery::mock(GetSystemSettings::class);
+ // Should not call getSystemSettings because session has locale
+
+ $middleware = new HandlePreferredLanguage($getSystemSettings);
+ $request = Request::create('/', 'GET');
+ $next = fn($req) => new Response();
+
+ $middleware->handle($request, $next);
+
+ expect(app()->getLocale())->toBe('id');
+});
+
+test('it uses user preference if set', function () {
+ $getSystemSettings = Mockery::mock(GetSystemSettings::class);
+ $getSystemSettings->shouldReceive('get')->with(SystemSettingKey::DEFAULT_LANGUAGE)->andReturn('en');
+
+ $user = Mockery::mock(User::class);
+ $user->shouldReceive('getAttribute')->with('settings')->andReturn(collect([
+ UserSettingKey::LANGUAGE->value => 'id'
+ ]));
+
+ $middleware = new HandlePreferredLanguage($getSystemSettings);
+ $request = Request::create('/', 'GET');
+ $request->setUserResolver(fn() => $user);
+ $next = fn($req) => new Response();
+
+ $middleware->handle($request, $next);
+
+ expect(app()->getLocale())->toBe('id');
+});
diff --git a/tests/Unit/Http/Middleware/HandlePreferredTimezoneTest.php b/tests/Unit/Http/Middleware/HandlePreferredTimezoneTest.php
new file mode 100644
index 0000000..8211f66
--- /dev/null
+++ b/tests/Unit/Http/Middleware/HandlePreferredTimezoneTest.php
@@ -0,0 +1,47 @@
+shouldReceive('get')->with(SystemSettingKey::TIMEZONE)->andReturn('UTC');
+
+ $middleware = new HandlePreferredTimezone($getSystemSettings);
+ $request = Request::create('/', 'GET');
+ $next = fn($req) => new Response();
+
+ $middleware->handle($request, $next);
+
+ expect(config('app.display_timezone'))->toBe('UTC');
+});
+
+test('it uses user preference if set', function () {
+ $getSystemSettings = Mockery::mock(GetSystemSettings::class);
+ $getSystemSettings->shouldReceive('get')->with(SystemSettingKey::TIMEZONE)->andReturn('UTC');
+
+ $user = Mockery::mock(User::class);
+ $user->shouldReceive('getAttribute')->with('settings')->andReturn(collect([
+ UserSettingKey::TIMEZONE->value => 'Asia/Jakarta'
+ ]));
+
+ $middleware = new HandlePreferredTimezone($getSystemSettings);
+ $request = Request::create('/', 'GET');
+ $request->setUserResolver(fn() => $user);
+ $next = fn($req) => new Response();
+
+ $middleware->handle($request, $next);
+
+ expect(config('app.display_timezone'))->toBe('Asia/Jakarta');
+});
diff --git a/tests/Unit/Http/Middleware/HandleSeoSettingTest.php b/tests/Unit/Http/Middleware/HandleSeoSettingTest.php
new file mode 100644
index 0000000..46a5a41
--- /dev/null
+++ b/tests/Unit/Http/Middleware/HandleSeoSettingTest.php
@@ -0,0 +1,28 @@
+shouldReceive('get')->with(SystemSettingKey::WEB_NAME)->andReturn('My App');
+ $getSystemSettings->shouldReceive('get')->with(SystemSettingKey::WEB_DESCRIPTION)->andReturn('My App Description');
+
+ $middleware = new HandleSeoSetting($getSystemSettings);
+ $request = Request::create('/', 'GET');
+ $next = fn($req) => new Response();
+
+ $middleware->handle($request, $next);
+
+ expect(config('seotools.meta.defaults.title'))->toBe('My App')
+ ->and(config('seotools.meta.defaults.description'))->toBe('My App Description');
+});
diff --git a/tests/Unit/UI/Actions/ApplyLayoutMetadataTest.php b/tests/Unit/UI/Actions/ApplyLayoutMetadataTest.php
new file mode 100644
index 0000000..22345ca
--- /dev/null
+++ b/tests/Unit/UI/Actions/ApplyLayoutMetadataTest.php
@@ -0,0 +1,27 @@
+ 'home.index'], header: 'Dashboard');
+ $dataContext = [];
+
+ $textResolver->shouldReceive('execute')->andReturnUsing(fn($v) => $v);
+
+ View::shouldReceive('composer')->once();
+
+ $action->execute($attributeInstance, $dataContext);
+});
diff --git a/tests/Unit/UI/Actions/ResolveDynamicTextTest.php b/tests/Unit/UI/Actions/ResolveDynamicTextTest.php
new file mode 100644
index 0000000..89c23d3
--- /dev/null
+++ b/tests/Unit/UI/Actions/ResolveDynamicTextTest.php
@@ -0,0 +1,19 @@
+ 'John'];
+ };
+
+ $result = $action->execute('Hello {user.name}', $component, 'user');
+ expect($result)->toBe('Hello John');
+});
diff --git a/tests/Unit/UI/Actions/SetSeoMetadataTest.php b/tests/Unit/UI/Actions/SetSeoMetadataTest.php
new file mode 100644
index 0000000..70ed204
--- /dev/null
+++ b/tests/Unit/UI/Actions/SetSeoMetadataTest.php
@@ -0,0 +1,40 @@
+ 1];
+ $request = Request::create('/', 'GET');
+
+ $textResolver->shouldReceive('execute')->times(3)->andReturnUsing(fn($value) => $value);
+
+ $metatagsMock = Mockery::mock();
+ $metatagsMock->shouldReceive('setCanonical')->once();
+ $metatagsMock->shouldReceive('setKeywords')->once();
+
+ $opengraphMock = Mockery::mock();
+ $opengraphMock->shouldReceive('addProperty')->once();
+
+ SEOTools::shouldReceive('setTitle')->once();
+ SEOTools::shouldReceive('setDescription')->once();
+ SEOTools::shouldReceive('metatags')->andReturn($metatagsMock);
+ SEOTools::shouldReceive('addImages')->once();
+ SEOTools::shouldReceive('opengraph')->andReturn($opengraphMock);
+
+ $action->applySeo($seo, $viewData, $request);
+});
diff --git a/tests/Unit/UI/Enums/FileTypeTest.php b/tests/Unit/UI/Enums/FileTypeTest.php
new file mode 100644
index 0000000..3d2691e
--- /dev/null
+++ b/tests/Unit/UI/Enums/FileTypeTest.php
@@ -0,0 +1,12 @@
+mimeType())->toBe(['application/pdf', 'application/vnd.ms-word', 'application/vnd.oasis.opendocument.text'])
+ ->and(FileType::IMAGE->mimeType())->toBe(['image/jpeg', 'image/png', 'image/gif', 'image/svg+xml'])
+ ->and(FileType::AUDIO->mimeType())->toBe(['audio/mpeg', 'audio/mp4', 'audio/ogg', 'audio/webm', 'audio/x-wav', 'audio/aac']);
+});
diff --git a/tests/Unit/UI/Enums/InputTypeTest.php b/tests/Unit/UI/Enums/InputTypeTest.php
new file mode 100644
index 0000000..ff974d4
--- /dev/null
+++ b/tests/Unit/UI/Enums/InputTypeTest.php
@@ -0,0 +1,15 @@
+component())->toBe('form.textarea')
+ ->and(InputType::SELECT->component())->toBe('form.select')
+ ->and(InputType::FILE->component())->toBe('filepond::upload')
+ ->and(InputType::CHECKBOX->component())->toBe('form.checkbox')
+ ->and(InputType::NUMBER->component())->toBe('form.input')
+ ->and(InputType::TEXTLINE->component())->toBe('form.input');
+});
diff --git a/tests/Unit/UI/Support/Excel/StyledExportTest.php b/tests/Unit/UI/Support/Excel/StyledExportTest.php
new file mode 100644
index 0000000..e62384d
--- /dev/null
+++ b/tests/Unit/UI/Support/Excel/StyledExportTest.php
@@ -0,0 +1,28 @@
+shouldReceive('query')->once()->andReturn(Mockery::mock(Builder::class));
+ $domainExport->shouldReceive('headings')->once()->andReturn(['Header']);
+ $domainExport->shouldReceive('map')->once()->with('row')->andReturn(['Mapped']);
+ $domainExport->shouldReceive('columnFormats')->once()->andReturn(['A' => 'FORMAT']);
+
+ $export = new StyledExport($domainExport);
+
+ expect($export->query())->toBeInstanceOf(Builder::class)
+ ->and($export->headings())->toBe(['Header'])
+ ->and($export->map('row'))->toBe(['Mapped'])
+ ->and($export->columnFormats())->toBe(['A' => 'FORMAT']);
+});
diff --git a/tests/Unit/UI/Support/LayoutStateTest.php b/tests/Unit/UI/Support/LayoutStateTest.php
new file mode 100644
index 0000000..a529fc8
--- /dev/null
+++ b/tests/Unit/UI/Support/LayoutStateTest.php
@@ -0,0 +1,18 @@
+ 'Home', 'url' => '/'],
+ ['label' => 'Dashboard', 'url' => '/dashboard'],
+ ];
+
+ $layoutState->setBreadcrumbs($breadcrumbs);
+
+ expect($layoutState->getBreadcrumbs())->toBe($breadcrumbs);
+});
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 0000000..58a5dec
--- /dev/null
+++ b/vite.config.js
@@ -0,0 +1,34 @@
+import { defineConfig } from 'vite';
+import laravel from 'laravel-vite-plugin';
+import path from 'path'
+
+export default defineConfig({
+ build: {},
+ plugins: [
+ laravel({
+ input: [
+ 'resources/scss/app.scss',
+
+ 'resources/js/alpinejs.js',
+ 'resources/js/bootstrap.js',
+ 'resources/js/plugin/jquery.js',
+ 'resources/js/plugin/datatables.js',
+ 'resources/js/plugin/apexchart.js',
+ 'resources/js/plugin/select2.js',
+ 'resources/js/plugin/quill.js',
+ 'resources/js/plugin/sweetalert2.js',
+ 'resources/js/plugin/filepond.js',
+ ],
+ refresh: true,
+ }),
+ ],
+ resolve: {
+ alias: {
+ '~bootstrap': path.resolve(__dirname, 'node_modules/bootstrap'),
+ 'jquery': path.resolve(__dirname, 'node_modules/jquery/dist-module/jquery.module.js'),
+ }
+ },
+ optimizeDeps: {
+ include: ['jquery', 'datatables.net-dt', 'datatables.net-bs5', 'select2'],
+ },
+});