diff --git a/.DS_Store b/.DS_Store
index c69da35..9816c96 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/.claude/skills/laravel-best-practices/SKILL.md b/.claude/skills/laravel-best-practices/SKILL.md
index 965e267..d136d75 100644
--- a/.claude/skills/laravel-best-practices/SKILL.md
+++ b/.claude/skills/laravel-best-practices/SKILL.md
@@ -8,183 +8,52 @@ metadata:
# 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`.
+Best practices for Laravel, organized as an index of rule files. Each rule file 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.
+Before applying any rule, check what the application already does. Laravel offers multiple valid approaches, and 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
+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.
## 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
+1. Check the changed files, nearby code, project configuration, and relevant tests for established patterns. Deviate only for a correctness or security defect, and call the deviation out.
+2. Map every affected concern to the rule index below. Read each mapped rule file before editing. Skip unrelated rule files.
+3. Make the smallest coherent change. Keep the application's architecture and naming instead of introducing a second pattern for the same job.
+4. Verify version-sensitive Laravel APIs for the installed version with `search-docs`, or inspect the installed framework when it is unavailable.
+5. Run the narrowest relevant tests first, then the project's formatting and static-analysis checks when the change warrants them.
+6. Re-read the diff against every mapped rule before finishing.
+
+## Rule Index
+
+Cross-cutting changes often need more than one rule file.
+
+| Concern | Read |
+| --- | --- |
+| Query count, eager loading, indexes, large datasets | [`rules/db-performance.md`](rules/db-performance.md) |
+| Subqueries, aggregates, complex ordering and query plans | [`rules/advanced-queries.md`](rules/advanced-queries.md) |
+| Models, relationships, scopes, casts | [`rules/eloquent.md`](rules/eloquent.md) |
+| Authentication, authorization, input safety, secrets, uploads | [`rules/security.md`](rules/security.md) |
+| Form Requests and validation rules | [`rules/validation.md`](rules/validation.md) |
+| Controllers, route binding, resources, middleware | [`rules/routing.md`](rules/routing.md) |
+| Schema changes, columns, foreign keys, indexes | [`rules/migrations.md`](rules/migrations.md) |
+| Jobs, retries, uniqueness, batches, Horizon | [`rules/queue-jobs.md`](rules/queue-jobs.md) |
+| Cache lifetime, invalidation, locks, memoization | [`rules/caching.md`](rules/caching.md) |
+| Outbound requests, retries, timeouts, fakes | [`rules/http-client.md`](rules/http-client.md) |
+| Exceptions, reporting, rendering, log context | [`rules/error-handling.md`](rules/error-handling.md) |
+| Events and notifications | [`rules/events-notifications.md`](rules/events-notifications.md) |
+| Mailables and mail assertions | [`rules/mail.md`](rules/mail.md) |
+| Scheduled tasks and overlap protection | [`rules/scheduling.md`](rules/scheduling.md) |
+| Collections, lazy iteration, bulk operations | [`rules/collections.md`](rules/collections.md) |
+| Blade components, attributes, composers | [`rules/blade-views.md`](rules/blade-views.md) |
+| Environment values and application configuration | [`rules/config.md`](rules/config.md) |
+| Pest/PHPUnit patterns, factories, fakes | [`rules/testing.md`](rules/testing.md) |
+| Naming, helpers, file boundaries, PHP style | [`rules/style.md`](rules/style.md) |
+| Actions, services, dependencies, application structure | [`rules/architecture.md`](rules/architecture.md) |
+
+## Decision Rules
+
+- Prefer framework features and existing application abstractions over new helpers or dependencies.
+- Avoid speculative abstractions. Extract code when it creates a clear domain boundary, removes meaningful duplication, or makes behavior independently testable.
+- Keep database access out of Blade views and prevent hidden N+1 queries across controllers, resources, jobs, and serialization.
diff --git a/.claude/skills/laravel-best-practices/rules/style.md b/.claude/skills/laravel-best-practices/rules/style.md
index 64d1730..a8afb36 100644
--- a/.claude/skills/laravel-best-practices/rules/style.md
+++ b/.claude/skills/laravel-best-practices/rules/style.md
@@ -44,7 +44,7 @@ Strings — use `Str` and fluent `Str::of()` over raw PHP:
// Incorrect
$slug = strtolower(str_replace(' ', '-', $title));
$short = substr($text, 0, 100) . '...';
-$class = substr(strrchr('App\Models\User', '\'), 1);
+$class = substr(strrchr('App\Models\User', '\\'), 1);
// Correct
$slug = Str::slug($title);
diff --git a/.gitignore b/.gitignore
index c5b19de..f156ad2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -25,3 +25,6 @@ yarn-error.log
NUL
storage/.DS_Store
.DS_Store
+
+# Credential files (keystores, private keys, etc.)
+/credentials/
diff --git a/.junie/skills/laravel-best-practices/SKILL.md b/.junie/skills/laravel-best-practices/SKILL.md
index 965e267..d136d75 100644
--- a/.junie/skills/laravel-best-practices/SKILL.md
+++ b/.junie/skills/laravel-best-practices/SKILL.md
@@ -8,183 +8,52 @@ metadata:
# 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`.
+Best practices for Laravel, organized as an index of rule files. Each rule file 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.
+Before applying any rule, check what the application already does. Laravel offers multiple valid approaches, and 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
+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.
## 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
+1. Check the changed files, nearby code, project configuration, and relevant tests for established patterns. Deviate only for a correctness or security defect, and call the deviation out.
+2. Map every affected concern to the rule index below. Read each mapped rule file before editing. Skip unrelated rule files.
+3. Make the smallest coherent change. Keep the application's architecture and naming instead of introducing a second pattern for the same job.
+4. Verify version-sensitive Laravel APIs for the installed version with `search-docs`, or inspect the installed framework when it is unavailable.
+5. Run the narrowest relevant tests first, then the project's formatting and static-analysis checks when the change warrants them.
+6. Re-read the diff against every mapped rule before finishing.
+
+## Rule Index
+
+Cross-cutting changes often need more than one rule file.
+
+| Concern | Read |
+| --- | --- |
+| Query count, eager loading, indexes, large datasets | [`rules/db-performance.md`](rules/db-performance.md) |
+| Subqueries, aggregates, complex ordering and query plans | [`rules/advanced-queries.md`](rules/advanced-queries.md) |
+| Models, relationships, scopes, casts | [`rules/eloquent.md`](rules/eloquent.md) |
+| Authentication, authorization, input safety, secrets, uploads | [`rules/security.md`](rules/security.md) |
+| Form Requests and validation rules | [`rules/validation.md`](rules/validation.md) |
+| Controllers, route binding, resources, middleware | [`rules/routing.md`](rules/routing.md) |
+| Schema changes, columns, foreign keys, indexes | [`rules/migrations.md`](rules/migrations.md) |
+| Jobs, retries, uniqueness, batches, Horizon | [`rules/queue-jobs.md`](rules/queue-jobs.md) |
+| Cache lifetime, invalidation, locks, memoization | [`rules/caching.md`](rules/caching.md) |
+| Outbound requests, retries, timeouts, fakes | [`rules/http-client.md`](rules/http-client.md) |
+| Exceptions, reporting, rendering, log context | [`rules/error-handling.md`](rules/error-handling.md) |
+| Events and notifications | [`rules/events-notifications.md`](rules/events-notifications.md) |
+| Mailables and mail assertions | [`rules/mail.md`](rules/mail.md) |
+| Scheduled tasks and overlap protection | [`rules/scheduling.md`](rules/scheduling.md) |
+| Collections, lazy iteration, bulk operations | [`rules/collections.md`](rules/collections.md) |
+| Blade components, attributes, composers | [`rules/blade-views.md`](rules/blade-views.md) |
+| Environment values and application configuration | [`rules/config.md`](rules/config.md) |
+| Pest/PHPUnit patterns, factories, fakes | [`rules/testing.md`](rules/testing.md) |
+| Naming, helpers, file boundaries, PHP style | [`rules/style.md`](rules/style.md) |
+| Actions, services, dependencies, application structure | [`rules/architecture.md`](rules/architecture.md) |
+
+## Decision Rules
+
+- Prefer framework features and existing application abstractions over new helpers or dependencies.
+- Avoid speculative abstractions. Extract code when it creates a clear domain boundary, removes meaningful duplication, or makes behavior independently testable.
+- Keep database access out of Blade views and prevent hidden N+1 queries across controllers, resources, jobs, and serialization.
diff --git a/.junie/skills/laravel-best-practices/rules/style.md b/.junie/skills/laravel-best-practices/rules/style.md
index 64d1730..a8afb36 100644
--- a/.junie/skills/laravel-best-practices/rules/style.md
+++ b/.junie/skills/laravel-best-practices/rules/style.md
@@ -44,7 +44,7 @@ Strings — use `Str` and fluent `Str::of()` over raw PHP:
// Incorrect
$slug = strtolower(str_replace(' ', '-', $title));
$short = substr($text, 0, 100) . '...';
-$class = substr(strrchr('App\Models\User', '\'), 1);
+$class = substr(strrchr('App\Models\User', '\\'), 1);
// Correct
$slug = Str::slug($title);
diff --git a/app/NativeComponents/Concerns/HasSyncUpData.php b/app/NativeComponents/Concerns/HasSyncUpData.php
index f8d0aad..f0c0770 100644
--- a/app/NativeComponents/Concerns/HasSyncUpData.php
+++ b/app/NativeComponents/Concerns/HasSyncUpData.php
@@ -75,6 +75,41 @@ public static function suMessages(int $conversationId): array
'time' => '10:30 AM',
'status' => 'read',
],
+ [
+ 'id' => 5,
+ 'fromMe' => true,
+ 'text' => 'That looks incredible! 🚀',
+ 'time' => '10:30 AM',
+ 'status' => 'read',
+ ],
+ [
+ 'id' => 6,
+ 'fromMe' => true,
+ 'text' => 'That looks incredible! 🚀',
+ 'time' => '10:30 AM',
+ 'status' => 'read',
+ ],
+ [
+ 'id' => 7,
+ 'fromMe' => true,
+ 'text' => 'That looks incredible! 🚀',
+ 'time' => '10:30 AM',
+ 'status' => 'read',
+ ],
+ [
+ 'id' => 8,
+ 'fromMe' => true,
+ 'text' => 'That looks incredible! 🚀',
+ 'time' => '10:30 AM',
+ 'status' => 'read',
+ ],
+ [
+ 'id' => 9,
+ 'fromMe' => true,
+ 'text' => 'That looks incredible! 🚀',
+ 'time' => '10:30 AM',
+ 'status' => 'read',
+ ],
];
}
diff --git a/app/NativeComponents/Counter.php b/app/NativeComponents/Counter.php
index 638d93d..4fc3dde 100644
--- a/app/NativeComponents/Counter.php
+++ b/app/NativeComponents/Counter.php
@@ -4,6 +4,7 @@
use Illuminate\View\View;
use Native\Mobile\Edge\NativeComponent;
+use Native\Mobile\Edge\Transition;
use Native\Mobile\Facades\Camera;
class Counter extends NativeComponent
@@ -29,10 +30,9 @@ public function decrement()
public function testCamera()
{
- Camera::getPhoto()->photoTaken(function ($event) {
- $this->photo = $event->path;
+ Camera::getPhoto()->photoTaken(function($photo) {
+ $this->photo = $photo;
});
-
}
public function render(): View
diff --git a/app/NativeComponents/DemoLauncher.php b/app/NativeComponents/DemoLauncher.php
index d95d195..d7c8bd7 100644
--- a/app/NativeComponents/DemoLauncher.php
+++ b/app/NativeComponents/DemoLauncher.php
@@ -60,7 +60,7 @@ class DemoLauncher extends NativeComponent
['id' => 'glass', 'title' => 'Glass', 'subtitle' => 'Liquid Glass Demo', 'icon' => 'drop.fill', 'color' => '#0EA5E9', 'url' => '/glass'],
['id' => 'nativetabs', 'title' => 'Native Tabs', 'subtitle' => 'TabView-rendered bottom bar; Liquid Glass on iOS 26+', 'icon' => 'rectangle.split.3x1', 'color' => '#A855F7', 'url' => '/native-tabs'],
['id' => 'drawer', 'title' => 'Side Drawer', 'subtitle' => 'X-style slide-out nav — modal + reveal, declared once on the layout', 'icon' => 'line.3.horizontal', 'color' => '#6366F1', 'url' => '/drawer'],
- ['id' => 'syncupnative', 'title' => 'SyncUp Messaging', 'subtitle' => 'Login, chat threads, friends, profile (5 screens) — custom chrome', 'icon' => 'bubble.left.fill', 'color' => '#0891b2', 'url' => '/syncup-native/login'],
+ ['id' => 'syncupnative', 'title' => 'SyncUp Messaging', 'subtitle' => 'Login, chat threads, friends, profile (5 screens) — custom chrome', 'icon' => 'bubble.left.fill', 'color' => '#0891b2', 'url' => '/syncup-native'],
],
],
[
diff --git a/app/NativeComponents/ExploreIcons.php b/app/NativeComponents/ExploreIcons.php
index ae11dd1..e9f5e03 100644
--- a/app/NativeComponents/ExploreIcons.php
+++ b/app/NativeComponents/ExploreIcons.php
@@ -4,6 +4,7 @@
use App\Icons\Android;
use App\Icons\Ios;
+use Illuminate\Support\Str;
use Native\Mobile\Edge\Element;
use Native\Mobile\Edge\Elements\Column;
use Native\Mobile\Edge\Elements\Icon;
@@ -174,6 +175,7 @@ private function iconRow(array $cases, int $rowIndex, bool $ios): Element
$row->addChild(
Pressable::make($icon)
->onPress("showIcon('{$case->name}')")
+ ->a11yLabel(Str::headline($case->name))
->flexGrow(1)->flexBasis(0)->height(self::CELL_SIZE)
->center()->class('bg-theme-surface-variant rounded-lg')
);
diff --git a/app/NativeComponents/ExploreTypography.php b/app/NativeComponents/ExploreTypography.php
index 3516846..c76e2e4 100644
--- a/app/NativeComponents/ExploreTypography.php
+++ b/app/NativeComponents/ExploreTypography.php
@@ -13,6 +13,6 @@ public function navTitle(): string
public function render(): \Illuminate\View\View
{
- return view('explore.typography');
+ return view('native.explore.typography');
}
}
diff --git a/app/NativeComponents/InstagramFeed.php b/app/NativeComponents/InstagramFeed.php
index 4bc1ea9..ad1ef1f 100644
--- a/app/NativeComponents/InstagramFeed.php
+++ b/app/NativeComponents/InstagramFeed.php
@@ -27,8 +27,8 @@ public function navTitle(): string
public function navigationOptions(): ?NavBarOptions
{
return NavBarOptions::make()
- ->action(NavAction::make('activity')->icon('favorite_border')->press('viewActivity'))
- ->action(NavAction::make('messages')->icon('chat_bubble_outline')->press('viewMessages'));
+ ->action(NavAction::make('activity')->icon('favorite_border')->a11yLabel('Activity')->press('viewActivity'))
+ ->action(NavAction::make('messages')->icon('chat_bubble_outline')->a11yLabel('Messages')->press('viewMessages'));
}
public function viewActivity(): void
diff --git a/app/NativeComponents/Layouts/SyncUpTabsLayout.php b/app/NativeComponents/Layouts/SyncUpTabsLayout.php
index bf6f689..5aa62bd 100644
--- a/app/NativeComponents/Layouts/SyncUpTabsLayout.php
+++ b/app/NativeComponents/Layouts/SyncUpTabsLayout.php
@@ -41,6 +41,7 @@ public function navBar(NativeComponent $screen): ?NavBar
->elevation(8) // subtle shadow under bar
->action(NavAction::make('search')
->icon(ios: Ios::Magnifyingglass, android: Android::Search)
+ ->a11yLabel('Search')
->press('openSearch'));
}
diff --git a/app/NativeComponents/NativeChromeDemo.php b/app/NativeComponents/NativeChromeDemo.php
index 74e4c86..616e70b 100644
--- a/app/NativeComponents/NativeChromeDemo.php
+++ b/app/NativeComponents/NativeChromeDemo.php
@@ -37,7 +37,7 @@ public function navigationOptions(): ?NavBarOptions
return NavBarOptions::make()
->subtitle('NavigationStack toolbar')
// A plain icon — fires its press handler directly.
- ->action(NavAction::make('share')->icon('share')->press('shareIt'))
+ ->action(NavAction::make('share')->icon('share')->a11yLabel('Share')->press('shareIt'))
// A pull-down menu — tap to reveal sub-items. Each sub-item
// is itself a NavAction with its own icon / label / press
// handler, plus an optional `destructive()` flag for red
@@ -45,6 +45,7 @@ public function navigationOptions(): ?NavBarOptions
->action(
NavAction::make('more')
->icon('ellipsis')
+ ->a11yLabel('More options')
->items([
NavAction::make('mark_read')
->icon('checkmark.circle')
diff --git a/app/NativeComponents/NativeChromeDetail.php b/app/NativeComponents/NativeChromeDetail.php
index 6fed201..ff23cf9 100644
--- a/app/NativeComponents/NativeChromeDetail.php
+++ b/app/NativeComponents/NativeChromeDetail.php
@@ -30,7 +30,7 @@ public function navigationOptions(): ?NavBarOptions
{
return NavBarOptions::make()
->subtitle('Pushed via NavigationStack')
- ->action(NavAction::make('star')->icon('star')->press('toggleStar'));
+ ->action(NavAction::make('star')->icon('star')->a11yLabel('Favorite')->press('toggleStar'));
}
public bool $starred = false;
diff --git a/app/NativeComponents/Profile.php b/app/NativeComponents/Profile.php
index 88746d9..bbc2568 100644
--- a/app/NativeComponents/Profile.php
+++ b/app/NativeComponents/Profile.php
@@ -7,8 +7,8 @@
class Profile extends NativeComponent
{
public string $name = 'Shane Rosenthal';
- public string $email = 'srosenthal82@gmail.com';
- public int $followers = 1248;
+ public string $email = 'shane@nativephp.com';
+ public int $followers = 2850;
public int $following = 312;
public function navTitle(): string
diff --git a/app/NativeComponents/SyncUpChat.php b/app/NativeComponents/SyncUpChat.php
index 9f39398..1e8bc85 100644
--- a/app/NativeComponents/SyncUpChat.php
+++ b/app/NativeComponents/SyncUpChat.php
@@ -43,9 +43,9 @@ public function navTitle(): string
public function navigationOptions(): ?NavBarOptions
{
return NavBarOptions::make()
- ->action(NavAction::make('video')->icon('video.fill')->press('startVideo'))
- ->action(NavAction::make('call')->icon('phone.fill')->press('startCall'))
- ->action(NavAction::make('more')->icon('ellipsis')->press('openMenu'));
+ ->action(NavAction::make('video')->icon('video.fill')->a11yLabel('Video call')->press('startVideo'))
+ ->action(NavAction::make('call')->icon('phone.fill')->a11yLabel('Voice call')->press('startCall'))
+ ->action(NavAction::make('more')->icon('ellipsis')->a11yLabel('More options')->press('openMenu'));
}
public function setDraft(string $value): void
diff --git a/app/NativeComponents/SyncUpChats.php b/app/NativeComponents/SyncUpChats.php
index c5d6330..6ff3cfe 100644
--- a/app/NativeComponents/SyncUpChats.php
+++ b/app/NativeComponents/SyncUpChats.php
@@ -31,7 +31,7 @@ public function navTitle(): string
public function navigationOptions(): ?NavBarOptions
{
return NavBarOptions::make()
- ->action(NavAction::make('more')->icon('ellipsis')->press('openMenu'));
+ ->action(NavAction::make('more')->icon('ellipsis')->a11yLabel('More options')->press('openMenu'));
}
public function setFilter(string $name): void
diff --git a/app/NativeComponents/SyncUpNative/Layouts/SyncUpNativeTabsLayout.php b/app/NativeComponents/SyncUpNative/Layouts/SyncUpNativeTabsLayout.php
index a6f2fb4..37df7e4 100644
--- a/app/NativeComponents/SyncUpNative/Layouts/SyncUpNativeTabsLayout.php
+++ b/app/NativeComponents/SyncUpNative/Layouts/SyncUpNativeTabsLayout.php
@@ -9,6 +9,8 @@
use Native\Mobile\Edge\Layouts\Builders\TabBar;
use Native\Mobile\Edge\Layouts\NativeLayout;
use Native\Mobile\Edge\NativeComponent;
+use Nativephp\NativeUi\Builders\FloatingOverlay;
+use Nativephp\NativeUi\Concerns\HasFloatingOverlay;
/**
* Native-chrome variant of SyncUp's tabs layout. Same shape as the
@@ -38,7 +40,6 @@ public function navBar(NativeComponent $screen): ?NavBar
public function tabBar(NativeComponent $screen): ?TabBar
{
return TabBar::make()
- ->backgroundColor('0092b8')
->add(Tab::link('Messages', '/syncup-native', ios: Ios::Message, android: Android::ChatBubble)
->badge($this->getUnreadMessageCount()))
->add(Tab::link('Friends', '/syncup-native/friends', ios: Ios::Person3, android: Android::Group)->news($this->showNewsIndicator()))
diff --git a/app/NativeComponents/SyncUpNative/SyncUpNativeChat.php b/app/NativeComponents/SyncUpNative/SyncUpNativeChat.php
index 83bca19..747ea70 100644
--- a/app/NativeComponents/SyncUpNative/SyncUpNativeChat.php
+++ b/app/NativeComponents/SyncUpNative/SyncUpNativeChat.php
@@ -50,16 +50,19 @@ public function navigationOptions(): ?NavBarOptions
->action(
NavAction::make('video')
->icon(ios: Ios::Camera, android: AndroidOutlined::Camera)
+ ->a11yLabel('Video call')
->press('startVideo')
)
->action(
NavAction::make('call')
->icon(ios: Ios::Phone, android: AndroidOutlined::Phone)
+ ->a11yLabel('Voice call')
->press('startCall')
)
->action(
NavAction::make('more')
->icon(ios: Ios::Ellipsis, android: Android::MoreVert)
+ ->a11yLabel('More options')
->items([
NavAction::make('mark_read')
->icon(ios: Ios::CheckmarkCircle, android: Android::CheckCircle)
diff --git a/app/NativeComponents/SyncUpNative/SyncUpNativeChats.php b/app/NativeComponents/SyncUpNative/SyncUpNativeChats.php
index 737d1f4..e3f67d1 100644
--- a/app/NativeComponents/SyncUpNative/SyncUpNativeChats.php
+++ b/app/NativeComponents/SyncUpNative/SyncUpNativeChats.php
@@ -29,7 +29,7 @@ public function navTitle(): string
public function navigationOptions(): ?NavBarOptions
{
return NavBarOptions::make()
- ->action(NavAction::make('more')->icon('ellipsis')->press('openMenu'));
+ ->action(NavAction::make('more')->icon('ellipsis')->a11yLabel('More options')->press('openMenu'));
}
public function setFilter(string $name): void
diff --git a/app/NativeComponents/Testing.php b/app/NativeComponents/Testing.php
new file mode 100644
index 0000000..d3bec54
--- /dev/null
+++ b/app/NativeComponents/Testing.php
@@ -0,0 +1,14 @@
+action(NavAction::make('cast')->icon('cart')->press('castDevice'))
- ->action(NavAction::make('alerts')->icon('notifications')->press('viewNotifications'))
- ->action(NavAction::make('search')->icon('search')->press('viewSearch'));
+ ->action(NavAction::make('cast')->icon('cart')->a11yLabel('Cast')->press('castDevice'))
+ ->action(NavAction::make('alerts')->icon('notifications')->a11yLabel('Notifications')->press('viewNotifications'))
+ ->action(NavAction::make('search')->icon('search')->a11yLabel('Search')->press('viewSearch'));
}
public function castDevice(): void
diff --git a/app/Providers/NativeServiceProvider.php b/app/Providers/NativeServiceProvider.php
index e815146..06d329f 100644
--- a/app/Providers/NativeServiceProvider.php
+++ b/app/Providers/NativeServiceProvider.php
@@ -40,19 +40,7 @@ public function boot(): void
public function plugins(): array
{
return [
- NativeUIServiceProvider::class,
- DialogServiceProvider::class,
- DeviceServiceProvider::class,
- // \Local\DoomGame\DoomGameServiceProvider::class,
- // \Native\Mobile\Providers\BackgroundTasksServiceProvider::class,
- // \Native\Mobile\Providers\TimerServiceProvider::class,
- // \Native\Mobile\Providers\DebugLogServiceProvider::class,
- // \NativePhp\SkiaCanvas\SkiaCanvasServiceProvider::class,
- CameraServiceProvider::class,
- // \NativePHP\Vibe\VibeServiceProvider::class, // parked — vibe package currently uninstalled
- SecureStorageServiceProvider::class,
- GeolocationServiceProvider::class,
-
+ NativeUIServiceProvider::class
];
}
}
diff --git a/composer.json b/composer.json
index b697584..997e456 100644
--- a/composer.json
+++ b/composer.json
@@ -19,12 +19,7 @@
},
"plugins": {
"type": "composer",
- "url": "https://plugins.nativephp.com",
- "exclude": ["nativephp/mobile-geolocation"]
- },
- "geolocation": {
- "type": "vcs",
- "url": "https://github.com/nativephp/mobile-geolocation"
+ "url": "https://plugins.nativephp.com"
}
},
"license": "MIT",
@@ -33,9 +28,6 @@
"laravel/framework": "^13.0",
"laravel/tinker": "^3.0",
"nativephp/mobile": "dev-element",
- "nativephp/mobile-camera": "*",
- "nativephp/mobile-device": "^1.0",
- "nativephp/mobile-dialog": "^1.0",
"nativephp/native-ui": "*"
},
"require-dev": {
diff --git a/composer.lock b/composer.lock
index 051554b..acf2557 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "5bcadcd6df9e47161cfcaa9c80b2edf2",
+ "content-hash": "ded803ab38edb1139fdaf5b2ef4d2c70",
"packages": [
{
"name": "bacon/bacon-qr-code",
@@ -866,22 +866,22 @@
},
{
"name": "guzzlehttp/guzzle",
- "version": "7.13.1",
+ "version": "7.14.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
- "reference": "55901a76dfd2006a0cc012b9e3c5b487f796478d"
+ "reference": "aef242412e13128b5049864867bb49fc37dd39de"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/guzzle/zipball/55901a76dfd2006a0cc012b9e3c5b487f796478d",
- "reference": "55901a76dfd2006a0cc012b9e3c5b487f796478d",
+ "url": "https://api.github.com/repos/guzzle/guzzle/zipball/aef242412e13128b5049864867bb49fc37dd39de",
+ "reference": "aef242412e13128b5049864867bb49fc37dd39de",
"shasum": ""
},
"require": {
"ext-json": "*",
- "guzzlehttp/promises": "^2.5",
- "guzzlehttp/psr7": "^2.12.3",
+ "guzzlehttp/promises": "^2.5.1",
+ "guzzlehttp/psr7": "^2.12.4",
"php": "^7.2.5 || ^8.0",
"psr/http-client": "^1.0",
"symfony/deprecation-contracts": "^2.5 || ^3.0",
@@ -893,7 +893,7 @@
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8.2",
"ext-curl": "*",
- "guzzle/client-integration-tests": "3.0.2",
+ "guzzle/client-integration-tests": "3.0.3",
"guzzlehttp/test-server": "^0.6",
"php-http/message-factory": "^1.1",
"phpunit/phpunit": "^8.5.52 || ^9.6.34",
@@ -974,7 +974,7 @@
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
- "source": "https://github.com/guzzle/guzzle/tree/7.13.1"
+ "source": "https://github.com/guzzle/guzzle/tree/7.14.0"
},
"funding": [
{
@@ -990,20 +990,20 @@
"type": "tidelift"
}
],
- "time": "2026-06-29T20:14:18+00:00"
+ "time": "2026-07-08T22:54:09+00:00"
},
{
"name": "guzzlehttp/promises",
- "version": "2.5.0",
+ "version": "2.5.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
- "reference": "4360e982f87f5f258bf872d094647791db2f4c8e"
+ "reference": "9ad1e4fc607446a055b95870c7f668e93b5cff29"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/promises/zipball/4360e982f87f5f258bf872d094647791db2f4c8e",
- "reference": "4360e982f87f5f258bf872d094647791db2f4c8e",
+ "url": "https://api.github.com/repos/guzzle/promises/zipball/9ad1e4fc607446a055b95870c7f668e93b5cff29",
+ "reference": "9ad1e4fc607446a055b95870c7f668e93b5cff29",
"shasum": ""
},
"require": {
@@ -1058,7 +1058,7 @@
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
- "source": "https://github.com/guzzle/promises/tree/2.5.0"
+ "source": "https://github.com/guzzle/promises/tree/2.5.1"
},
"funding": [
{
@@ -1074,20 +1074,20 @@
"type": "tidelift"
}
],
- "time": "2026-06-02T12:23:43+00:00"
+ "time": "2026-07-08T15:48:39+00:00"
},
{
"name": "guzzlehttp/psr7",
- "version": "2.12.3",
+ "version": "2.12.4",
"source": {
"type": "git",
"url": "https://github.com/guzzle/psr7.git",
- "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d"
+ "reference": "51e27f9e2b332ab3e72f4520d5ff4f3c68c3577c"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/psr7/zipball/7ec62dc3f44aa218487dbed81a9bf9bc647be55d",
- "reference": "7ec62dc3f44aa218487dbed81a9bf9bc647be55d",
+ "url": "https://api.github.com/repos/guzzle/psr7/zipball/51e27f9e2b332ab3e72f4520d5ff4f3c68c3577c",
+ "reference": "51e27f9e2b332ab3e72f4520d5ff4f3c68c3577c",
"shasum": ""
},
"require": {
@@ -1177,7 +1177,7 @@
],
"support": {
"issues": "https://github.com/guzzle/psr7/issues",
- "source": "https://github.com/guzzle/psr7/tree/2.12.3"
+ "source": "https://github.com/guzzle/psr7/tree/2.12.4"
},
"funding": [
{
@@ -1193,20 +1193,20 @@
"type": "tidelift"
}
],
- "time": "2026-06-23T15:21:08+00:00"
+ "time": "2026-07-08T15:56:20+00:00"
},
{
"name": "guzzlehttp/uri-template",
- "version": "v1.0.8",
+ "version": "v1.0.9",
"source": {
"type": "git",
"url": "https://github.com/guzzle/uri-template.git",
- "reference": "9c19128923b05a5d7355e5d2318d7808b7e33bbd"
+ "reference": "d7580af6d3f8384325d9cd3e99b21c3ed1848176"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/guzzle/uri-template/zipball/9c19128923b05a5d7355e5d2318d7808b7e33bbd",
- "reference": "9c19128923b05a5d7355e5d2318d7808b7e33bbd",
+ "url": "https://api.github.com/repos/guzzle/uri-template/zipball/d7580af6d3f8384325d9cd3e99b21c3ed1848176",
+ "reference": "d7580af6d3f8384325d9cd3e99b21c3ed1848176",
"shasum": ""
},
"require": {
@@ -1263,7 +1263,7 @@
],
"support": {
"issues": "https://github.com/guzzle/uri-template/issues",
- "source": "https://github.com/guzzle/uri-template/tree/v1.0.8"
+ "source": "https://github.com/guzzle/uri-template/tree/v1.0.9"
},
"funding": [
{
@@ -1279,20 +1279,20 @@
"type": "tidelift"
}
],
- "time": "2026-06-23T13:02:23+00:00"
+ "time": "2026-07-08T16:19:22+00:00"
},
{
"name": "laravel/framework",
- "version": "v13.18.1",
+ "version": "v13.19.0",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
- "reference": "7d66044819e269f05924793a6800022dc181d850"
+ "reference": "514502b38e11bd676ecf83b271c9452cc7500f16"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/framework/zipball/7d66044819e269f05924793a6800022dc181d850",
- "reference": "7d66044819e269f05924793a6800022dc181d850",
+ "url": "https://api.github.com/repos/laravel/framework/zipball/514502b38e11bd676ecf83b271c9452cc7500f16",
+ "reference": "514502b38e11bd676ecf83b271c9452cc7500f16",
"shasum": ""
},
"require": {
@@ -1503,7 +1503,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
- "time": "2026-07-02T18:35:20+00:00"
+ "time": "2026-07-07T14:13:33+00:00"
},
{
"name": "laravel/prompts",
@@ -1885,16 +1885,16 @@
},
{
"name": "league/flysystem",
- "version": "3.35.1",
+ "version": "3.35.2",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem.git",
- "reference": "f23af6c5aafd958a7593029a271d77baf5ed793c"
+ "reference": "b277b5dc3d56650b68904117124e79c851e12376"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/f23af6c5aafd958a7593029a271d77baf5ed793c",
- "reference": "f23af6c5aafd958a7593029a271d77baf5ed793c",
+ "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/b277b5dc3d56650b68904117124e79c851e12376",
+ "reference": "b277b5dc3d56650b68904117124e79c851e12376",
"shasum": ""
},
"require": {
@@ -1962,9 +1962,9 @@
],
"support": {
"issues": "https://github.com/thephpleague/flysystem/issues",
- "source": "https://github.com/thephpleague/flysystem/tree/3.35.1"
+ "source": "https://github.com/thephpleague/flysystem/tree/3.35.2"
},
- "time": "2026-06-25T06:52:23+00:00"
+ "time": "2026-07-06T14:42:07+00:00"
},
{
"name": "league/flysystem-local",
@@ -2017,16 +2017,16 @@
},
{
"name": "league/mime-type-detection",
- "version": "1.16.0",
+ "version": "1.17.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/mime-type-detection.git",
- "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9"
+ "reference": "f5f47eff7c48ed1003069a2ca67f316fb4021c76"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/2d6702ff215bf922936ccc1ad31007edc76451b9",
- "reference": "2d6702ff215bf922936ccc1ad31007edc76451b9",
+ "url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/f5f47eff7c48ed1003069a2ca67f316fb4021c76",
+ "reference": "f5f47eff7c48ed1003069a2ca67f316fb4021c76",
"shasum": ""
},
"require": {
@@ -2036,7 +2036,7 @@
"require-dev": {
"friendsofphp/php-cs-fixer": "^3.2",
"phpstan/phpstan": "^0.12.68",
- "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0"
+ "phpunit/phpunit": "^8.5.8 || ^9.3 || ^10.0 || ^11.0 || ^12.0"
},
"type": "library",
"autoload": {
@@ -2057,7 +2057,7 @@
"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"
+ "source": "https://github.com/thephpleague/mime-type-detection/tree/1.17.0"
},
"funding": [
{
@@ -2069,7 +2069,7 @@
"type": "tidelift"
}
],
- "time": "2024-09-21T08:32:55+00:00"
+ "time": "2026-07-09T11:49:27+00:00"
},
{
"name": "league/uri",
@@ -2362,27 +2362,33 @@
"source": {
"type": "git",
"url": "https://github.com/NativePHP/mobile-air.git",
- "reference": "fd61339340a51a2266bee49a4179663826697882"
+ "reference": "35f98f0c541455bd4d513996c176d36fdee1680d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/NativePHP/mobile-air/zipball/fd61339340a51a2266bee49a4179663826697882",
- "reference": "fd61339340a51a2266bee49a4179663826697882",
+ "url": "https://api.github.com/repos/NativePHP/mobile-air/zipball/35f98f0c541455bd4d513996c176d36fdee1680d",
+ "reference": "35f98f0c541455bd4d513996c176d36fdee1680d",
"shasum": ""
},
"require": {
- "endroid/qr-code": "^6.1.3",
+ "endroid/qr-code": "^6.0.9",
"ext-dom": "*",
"ext-simplexml": "*",
"guzzlehttp/guzzle": "^7.9",
"illuminate/contracts": "^10.0|^11.0|^12.0|^13.0",
"illuminate/support": "^10.0|^11.0|^12.0|^13.0",
"laravel/prompts": "^0.1.1|^0.2|^0.3",
- "php": "^8.3",
+ "php": "^8.4",
"react/datagram": "^1.10",
"spatie/laravel-package-tools": "^1.14.0",
"workerman/workerman": "^4.1"
},
+ "conflict": {
+ "nativephp/mobile-device": "*",
+ "nativephp/mobile-dialog": "*",
+ "nativephp/mobile-file": "*",
+ "nativephp/mobile-system": "*"
+ },
"require-dev": {
"larastan/larastan": "^2.0",
"laravel/pint": "^1.0",
@@ -2407,7 +2413,10 @@
"autoload": {
"psr-4": {
"Native\\Mobile\\": "src/"
- }
+ },
+ "files": [
+ "src/helpers.php"
+ ]
},
"autoload-dev": {
"psr-4": {
@@ -2463,154 +2472,7 @@
"source": "https://github.com/NativePHP/mobile-air/tree/element",
"issues": "https://github.com/NativePHP/mobile-air/issues"
},
- "time": "2026-07-03T03:57:01+00:00"
- },
- {
- "name": "nativephp/mobile-camera",
- "version": "1.0.3",
- "source": {
- "type": "git",
- "url": "https://github.com/NativePHP/mobile-camera.git",
- "reference": "b01139ea47029b6eae695d1a16c41e00f265fd54"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/NativePHP/mobile-camera/zipball/b01139ea47029b6eae695d1a16c41e00f265fd54",
- "reference": "b01139ea47029b6eae695d1a16c41e00f265fd54",
- "shasum": ""
- },
- "require": {
- "nativephp/mobile": "*",
- "php": "^8.2"
- },
- "type": "nativephp-plugin",
- "extra": {
- "laravel": {
- "providers": [
- "Native\\Mobile\\Providers\\CameraServiceProvider"
- ]
- },
- "nativephp": {
- "manifest": "nativephp.json"
- }
- },
- "autoload": {
- "psr-4": {
- "Native\\Mobile\\Providers\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "NativePHP",
- "email": "info@nativephp.com"
- }
- ],
- "description": "Camera plugin for NativePHP Mobile (Photo capture, Video recording, Gallery picker)",
- "support": {
- "issues": "https://github.com/NativePHP/mobile-camera/issues",
- "source": "https://github.com/NativePHP/mobile-camera/tree/1.0.3"
- },
- "time": "2026-06-04T12:43:58+00:00"
- },
- {
- "name": "nativephp/mobile-device",
- "version": "1.0.1",
- "source": {
- "type": "git",
- "url": "https://github.com/NativePHP/mobile-device.git",
- "reference": "91f569fdc903961f06776e865b856369660de4c6"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/NativePHP/mobile-device/zipball/91f569fdc903961f06776e865b856369660de4c6",
- "reference": "91f569fdc903961f06776e865b856369660de4c6",
- "shasum": ""
- },
- "require": {
- "nativephp/mobile": "*",
- "php": "^8.2"
- },
- "type": "nativephp-plugin",
- "extra": {
- "laravel": {
- "providers": [
- "Native\\Mobile\\Providers\\DeviceServiceProvider"
- ]
- },
- "nativephp": {
- "manifest": "nativephp.json"
- }
- },
- "autoload": {
- "psr-4": {
- "Native\\Mobile\\Providers\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "description": "Device hardware operations for NativePHP Mobile",
- "support": {
- "issues": "https://github.com/NativePHP/mobile-device/issues",
- "source": "https://github.com/NativePHP/mobile-device/tree/1.0.1"
- },
- "time": "2026-03-12T22:12:09+00:00"
- },
- {
- "name": "nativephp/mobile-dialog",
- "version": "1.0.0",
- "source": {
- "type": "git",
- "url": "https://github.com/NativePHP/mobile-dialog.git",
- "reference": "ce1958fff0c32a500688b9bc4ca9c426b5bd2c4f"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/NativePHP/mobile-dialog/zipball/ce1958fff0c32a500688b9bc4ca9c426b5bd2c4f",
- "reference": "ce1958fff0c32a500688b9bc4ca9c426b5bd2c4f",
- "shasum": ""
- },
- "require": {
- "nativephp/mobile": "*",
- "php": "^8.2"
- },
- "type": "nativephp-plugin",
- "extra": {
- "laravel": {
- "providers": [
- "Native\\Mobile\\Providers\\DialogServiceProvider"
- ]
- },
- "nativephp": {
- "manifest": "nativephp.json"
- }
- },
- "autoload": {
- "psr-4": {
- "Native\\Mobile\\Providers\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "NativePHP",
- "email": "support@nativephp.com"
- }
- ],
- "description": "Native alert dialogs and toast notifications for NativePHP Mobile",
- "support": {
- "issues": "https://github.com/NativePHP/mobile-dialog/issues",
- "source": "https://github.com/NativePHP/mobile-dialog/tree/1.0.0"
- },
- "time": "2026-01-19T23:53:05+00:00"
+ "time": "2026-07-11T02:20:58+00:00"
},
{
"name": "nativephp/native-ui",
@@ -2618,12 +2480,12 @@
"source": {
"type": "git",
"url": "https://github.com/NativePHP/native-ui.git",
- "reference": "2913a0c0f4b72f583339845d719a34f8181e2570"
+ "reference": "03b6f3470ccb91ba9e780d79c51a7fd5bec5d05d"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/NativePHP/native-ui/zipball/2913a0c0f4b72f583339845d719a34f8181e2570",
- "reference": "2913a0c0f4b72f583339845d719a34f8181e2570",
+ "url": "https://api.github.com/repos/NativePHP/native-ui/zipball/03b6f3470ccb91ba9e780d79c51a7fd5bec5d05d",
+ "reference": "03b6f3470ccb91ba9e780d79c51a7fd5bec5d05d",
"shasum": ""
},
"require": {
@@ -2669,20 +2531,20 @@
"source": "https://github.com/NativePHP/native-ui/tree/main",
"issues": "https://github.com/NativePHP/native-ui/issues"
},
- "time": "2026-07-03T03:57:39+00:00"
+ "time": "2026-07-11T02:33:52+00:00"
},
{
"name": "nesbot/carbon",
- "version": "3.13.0",
+ "version": "3.13.1",
"source": {
"type": "git",
"url": "https://github.com/CarbonPHP/carbon.git",
- "reference": "40f6618f052df16b545f626fbf9a878e6497d16a"
+ "reference": "2937ad3d1d2c506fd2bc97d571438a95641f44e2"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/40f6618f052df16b545f626fbf9a878e6497d16a",
- "reference": "40f6618f052df16b545f626fbf9a878e6497d16a",
+ "url": "https://api.github.com/repos/CarbonPHP/carbon/zipball/2937ad3d1d2c506fd2bc97d571438a95641f44e2",
+ "reference": "2937ad3d1d2c506fd2bc97d571438a95641f44e2",
"shasum": ""
},
"require": {
@@ -2774,7 +2636,7 @@
"type": "tidelift"
}
],
- "time": "2026-06-18T13:49:15+00:00"
+ "time": "2026-07-09T18:23:49+00:00"
},
{
"name": "nette/schema",
@@ -2936,20 +2798,19 @@
},
{
"name": "nikic/php-parser",
- "version": "v5.7.0",
+ "version": "v5.8.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
- "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82"
+ "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82",
- "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82",
+ "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/044a6a392ff8ad0d61f14370a5fbbd0a0107152f",
+ "reference": "044a6a392ff8ad0d61f14370a5fbbd0a0107152f",
"shasum": ""
},
"require": {
- "ext-ctype": "*",
"ext-json": "*",
"ext-tokenizer": "*",
"php": ">=7.4"
@@ -2988,9 +2849,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
- "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0"
+ "source": "https://github.com/nikic/PHP-Parser/tree/v5.8.0"
},
- "time": "2025-12-06T11:56:16+00:00"
+ "time": "2026-07-04T14:30:18+00:00"
},
{
"name": "nunomaduro/termwind",
@@ -6814,16 +6675,16 @@
},
{
"name": "vlucas/phpdotenv",
- "version": "v5.6.3",
+ "version": "v5.6.4",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
- "reference": "955e7815d677a3eaa7075231212f2110983adecc"
+ "reference": "416df702837983f8d5ff48c9c3fee4f5f57b980b"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/955e7815d677a3eaa7075231212f2110983adecc",
- "reference": "955e7815d677a3eaa7075231212f2110983adecc",
+ "url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/416df702837983f8d5ff48c9c3fee4f5f57b980b",
+ "reference": "416df702837983f8d5ff48c9c3fee4f5f57b980b",
"shasum": ""
},
"require": {
@@ -6882,7 +6743,7 @@
],
"support": {
"issues": "https://github.com/vlucas/phpdotenv/issues",
- "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.3"
+ "source": "https://github.com/vlucas/phpdotenv/tree/v5.6.4"
},
"funding": [
{
@@ -6894,7 +6755,7 @@
"type": "tidelift"
}
],
- "time": "2025-12-27T19:49:13+00:00"
+ "time": "2026-07-06T19:11:50+00:00"
},
{
"name": "voku/portable-ascii",
@@ -7626,16 +7487,16 @@
},
{
"name": "laravel/boost",
- "version": "v2.4.11",
+ "version": "v2.4.12",
"source": {
"type": "git",
"url": "https://github.com/laravel/boost.git",
- "reference": "c98fc9d151de97a1864b51fc7c710fc03023ba17"
+ "reference": "d43bdf901fee8d216145cb062c9847c892844908"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/laravel/boost/zipball/c98fc9d151de97a1864b51fc7c710fc03023ba17",
- "reference": "c98fc9d151de97a1864b51fc7c710fc03023ba17",
+ "url": "https://api.github.com/repos/laravel/boost/zipball/d43bdf901fee8d216145cb062c9847c892844908",
+ "reference": "d43bdf901fee8d216145cb062c9847c892844908",
"shasum": ""
},
"require": {
@@ -7688,7 +7549,7 @@
"issues": "https://github.com/laravel/boost/issues",
"source": "https://github.com/laravel/boost"
},
- "time": "2026-06-26T08:21:49+00:00"
+ "time": "2026-07-08T09:53:34+00:00"
},
{
"name": "laravel/mcp",
@@ -8277,16 +8138,16 @@
},
{
"name": "pestphp/pest",
- "version": "v4.7.4",
+ "version": "v4.7.5",
"source": {
"type": "git",
"url": "https://github.com/pestphp/pest.git",
- "reference": "ee2e97e932d158faceeaa63a4dc17324b15152cb"
+ "reference": "5dc49a71d63602a9b98fed0f2017c4679ef9f8e0"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/pestphp/pest/zipball/ee2e97e932d158faceeaa63a4dc17324b15152cb",
- "reference": "ee2e97e932d158faceeaa63a4dc17324b15152cb",
+ "url": "https://api.github.com/repos/pestphp/pest/zipball/5dc49a71d63602a9b98fed0f2017c4679ef9f8e0",
+ "reference": "5dc49a71d63602a9b98fed0f2017c4679ef9f8e0",
"shasum": ""
},
"require": {
@@ -8309,11 +8170,11 @@
"webmozart/assert": "<1.11.0"
},
"require-dev": {
- "mrpunyapal/peststan": "^0.2.10",
+ "mrpunyapal/peststan": "^0.2.11",
"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"
+ "psy/psysh": "^0.12.24"
},
"bin": [
"bin/pest"
@@ -8380,7 +8241,7 @@
],
"support": {
"issues": "https://github.com/pestphp/pest/issues",
- "source": "https://github.com/pestphp/pest/tree/v4.7.4"
+ "source": "https://github.com/pestphp/pest/tree/v4.7.5"
},
"funding": [
{
@@ -8392,7 +8253,7 @@
"type": "github"
}
],
- "time": "2026-06-25T19:09:05+00:00"
+ "time": "2026-07-06T17:06:29+00:00"
},
{
"name": "pestphp/pest-plugin",
@@ -9036,16 +8897,16 @@
},
{
"name": "phpstan/phpdoc-parser",
- "version": "2.3.2",
+ "version": "2.3.3",
"source": {
"type": "git",
"url": "https://github.com/phpstan/phpdoc-parser.git",
- "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a"
+ "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/a004701b11273a26cd7955a61d67a7f1e525a45a",
- "reference": "a004701b11273a26cd7955a61d67a7f1e525a45a",
+ "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/fb19eedd2bb67ff8cf7a5502ad329e701d6398a3",
+ "reference": "fb19eedd2bb67ff8cf7a5502ad329e701d6398a3",
"shasum": ""
},
"require": {
@@ -9077,9 +8938,9 @@
"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"
+ "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.3"
},
- "time": "2026-01-25T14:56:51+00:00"
+ "time": "2026-07-08T07:01:06+00:00"
},
{
"name": "phpunit/php-code-coverage",
diff --git a/nativephp.json b/nativephp.lock
similarity index 60%
rename from nativephp.json
rename to nativephp.lock
index 9f864bf..a4a9436 100644
--- a/nativephp.json
+++ b/nativephp.lock
@@ -1,6 +1,6 @@
{
"php": {
- "version": "8.4",
+ "version": "8.4.23",
"icu": false
}
}
diff --git a/resources/fonts/RockSalt-Regular.ttf b/resources/fonts/RockSalt-Regular.ttf
new file mode 100644
index 0000000..0f72fe6
Binary files /dev/null and b/resources/fonts/RockSalt-Regular.ttf differ
diff --git a/resources/views/native/animate.blade.php b/resources/views/native/animate.blade.php
index 98d21de..26db5a9 100644
--- a/resources/views/native/animate.blade.php
+++ b/resources/views/native/animate.blade.php
@@ -476,6 +476,7 @@ class="w-[44] h-[44] rounded-full items-center justify-center bg-amber-500"
{{-- Main FAB — rotates 45° when open so + becomes × --}}
class="w-full h-full rounded-2xl bg-slate-800"
:opacity="$drag->interpolate([0, 180], [0, 0.5])" />
-
+
-
+
Shane Rosenthal
- srosenthal@example.com
+ shane@nativephp.com
diff --git a/resources/views/native/explore/forms.blade.php b/resources/views/native/explore/forms.blade.php
index 365a28c..22422dd 100644
--- a/resources/views/native/explore/forms.blade.php
+++ b/resources/views/native/explore/forms.blade.php
@@ -4,19 +4,19 @@
On release (blur)
-
+
{{ $slideBlur }}
Debounced (150ms)
-
+
{{ $slideDebounced }}
Live (every drag tick)
-
+
{{ $slideValue }}
diff --git a/resources/views/native/explore/menus.blade.php b/resources/views/native/explore/menus.blade.php
index 5eb963a..e430b3f 100644
--- a/resources/views/native/explore/menus.blade.php
+++ b/resources/views/native/explore/menus.blade.php
@@ -97,6 +97,7 @@ class="glass:interactive android:dark:bg-theme-surface-variant rounded-full p-3
supporting="Did you see the new design specs?"
leadingMonogram="SM"
leadingMonogramColor="#0EA5E9"
+ trailing-a11y-label="More options for Sarah Miller"
:trailing-menu="$listItemMenu"/>
diff --git a/resources/views/native/explore/typography.blade.php b/resources/views/native/explore/typography.blade.php
index 46ac123..5ea8be4 100644
--- a/resources/views/native/explore/typography.blade.php
+++ b/resources/views/native/explore/typography.blade.php
@@ -2,7 +2,7 @@
{{-- TYPOGRAPHY --}}
- Typography
+ Typography
text-xs (12pt) — The quick brown fox
text-sm (14pt) — The quick brown fox
@@ -32,6 +32,7 @@
font-sans — The quick brown fox 0123
font-serif — The quick brown fox 0123
font-mono — The quick brown fox 0123
+ Custom font: font="RockSalt-Regular"
{{-- TEXT TRANSFORM --}}
diff --git a/resources/views/native/facebook-create.blade.php b/resources/views/native/facebook-create.blade.php
index 995aa22..efccc23 100644
--- a/resources/views/native/facebook-create.blade.php
+++ b/resources/views/native/facebook-create.blade.php
@@ -3,7 +3,7 @@
{{-- Top Bar --}}
-
+
Create Post
@@ -23,6 +23,7 @@
diff --git a/resources/views/native/facebook-feed.blade.php b/resources/views/native/facebook-feed.blade.php
index e85fcc0..d8c83e8 100644
--- a/resources/views/native/facebook-feed.blade.php
+++ b/resources/views/native/facebook-feed.blade.php
@@ -21,6 +21,7 @@
@@ -63,6 +64,7 @@ class="w-[40] h-[40] rounded-full"
@@ -82,6 +84,7 @@ class="w-[54] h-[54] rounded-full"
@@ -102,6 +105,7 @@ class="w-[40] h-[40] rounded-full"
diff --git a/resources/views/native/facebook-post.blade.php b/resources/views/native/facebook-post.blade.php
index aea198e..7140736 100644
--- a/resources/views/native/facebook-post.blade.php
+++ b/resources/views/native/facebook-post.blade.php
@@ -3,7 +3,7 @@
{{-- Top Bar --}}
-
+
{{ $post['user']['name'] }}'s Post
@@ -16,6 +16,7 @@
@@ -35,6 +36,7 @@ class="w-[44] h-[44] rounded-full"
@@ -89,6 +91,7 @@ class="w-full h-[300]"
@@ -121,6 +124,7 @@ class="w-[32] h-[32] rounded-full"
diff --git a/resources/views/native/facebook-profile.blade.php b/resources/views/native/facebook-profile.blade.php
index 7e22fb7..fcc40af 100644
--- a/resources/views/native/facebook-profile.blade.php
+++ b/resources/views/native/facebook-profile.blade.php
@@ -3,7 +3,7 @@
{{-- Top Bar --}}
-
+
{{ $user['name'] }}
@@ -12,6 +12,7 @@
{{-- Cover Photo --}}
@@ -22,6 +23,7 @@ class="w-full h-[160]"
@@ -70,6 +72,7 @@ class="w-[80] h-[80] rounded-full mt-[-40]"
@@ -89,6 +92,7 @@ class="w-[40] h-[40] rounded-full"
diff --git a/resources/views/native/instagram-feed.blade.php b/resources/views/native/instagram-feed.blade.php
index f736e14..be57e07 100644
--- a/resources/views/native/instagram-feed.blade.php
+++ b/resources/views/native/instagram-feed.blade.php
@@ -14,6 +14,7 @@
@@ -29,6 +30,7 @@ class="w-[60] h-[60] rounded-full"
@@ -50,6 +52,7 @@ class="w-[56] h-[56] rounded-full"
@@ -71,6 +74,7 @@ class="w-[32] h-[32] rounded-full"
@@ -78,19 +82,19 @@ class="w-full h-[375]"
{{-- Action Bar --}}
-
+
-
+
-
+
-
+
Post
@@ -16,6 +16,7 @@
@@ -35,6 +36,7 @@ class="w-[36] h-[36] rounded-full"
{{-- Post Image --}}
@@ -42,7 +44,7 @@ class="w-full h-[375]"
{{-- Action Bar --}}
-
+
-
+
@@ -107,6 +110,7 @@ class="w-[32] h-[32] rounded-full"
diff --git a/resources/views/native/instagram-profile.blade.php b/resources/views/native/instagram-profile.blade.php
index 77147d3..502190d 100644
--- a/resources/views/native/instagram-profile.blade.php
+++ b/resources/views/native/instagram-profile.blade.php
@@ -4,7 +4,7 @@
{{-- Top Bar --}}
-
+
@@ -25,6 +25,7 @@
@@ -95,6 +96,7 @@ class="w-[76] h-[76] rounded-full"
diff --git a/resources/views/native/instagram-search.blade.php b/resources/views/native/instagram-search.blade.php
index f348a00..e55607d 100644
--- a/resources/views/native/instagram-search.blade.php
+++ b/resources/views/native/instagram-search.blade.php
@@ -28,6 +28,7 @@
@@ -41,6 +42,7 @@ class="w-[125] h-[125]"
@@ -51,6 +53,7 @@ class="w-[250] h-[251]"
@@ -60,6 +63,7 @@ class="w-[125] h-[125]"
diff --git a/resources/views/native/pill.blade.php b/resources/views/native/pill.blade.php
new file mode 100644
index 0000000..10ecefc
--- /dev/null
+++ b/resources/views/native/pill.blade.php
@@ -0,0 +1,3 @@
+
+ Hello
+
diff --git a/resources/views/native/syncup-native/chat.blade.php b/resources/views/native/syncup-native/chat.blade.php
index c22c842..bd3c158 100644
--- a/resources/views/native/syncup-native/chat.blade.php
+++ b/resources/views/native/syncup-native/chat.blade.php
@@ -19,17 +19,18 @@
->press('logSettings'),
];
@endphp
-{{-- Standard chat layout: scroll-view fills available height (flex-1),
- input row pinned at the bottom of the column. SwiftUI's automatic
- keyboard avoidance pushes the whole column up when the keyboard
- appears so the input stays visible above it. No layout-level
- bottomBar slot needed — the bar is part of the screen content. --}}
-
+{{-- Chat layout: a `flex-1` scroll-view of messages with an inline input
+ row beneath it, inside a full-height column (same structure as the
+ working chats-list screen). NOTE: keyboard avoidance is not yet solved
+ for this screen — the `` safeAreaInset seam collapsed
+ the content on this pushed tab-level config, so the input is inline for
+ now and the keyboard can overlap it. Revisit once the seam is fixed. --}}
+
- {{-- Messages — flex-1 so they fill the space between the navbar and
- the input bar. --}}
-
-
+ {{-- Messages — `flex-1` so the scroll-view fills the space above the
+ inline input row (same pattern as the working chats list). --}}
+
+
{{-- Date divider --}}
@@ -76,20 +77,14 @@ class="text-[10] text-theme-on-surface-variant">{{ $m['time'] }}
- {{-- Input bar — fully transparent so the column's bg-theme-background
- extends edge-to-edge through it (iMessage / WhatsApp / Telegram
- pattern: bg fills the screen, input pill floats glass on top).
- No safe-area-bottom — NavigationStack already insets content above
- the home indicator; adding it here doubles the gap. --}}
- {{-- Input bar in a column that fills the stack's height with
- `justify-end`, pushing the row to the bottom edge. The column is
- transparent so scroll content shows through everywhere except
- where the row's glass pills sit. --}}
-
-
+ {{-- Input bar — inline as the last child of the column, above the
+ `flex-1` scroll-view. Renders as a normal bottom row (no
+ safeAreaInset seam, which collapsed this pushed-tab-level screen). --}}
+
@@ -100,10 +95,12 @@ class="glass:interactive android:dark:bg-white text-slate-700 rounded-full p-1 i
(rounded-full + glass material), which is the iMessage /
WhatsApp / Telegram pattern exactly. --}}
{{-- Mic when field is empty; primary-tinted send when there's text.
@@ -111,18 +108,18 @@ class="flex-1 glass android:dark:bg-white rounded-full px-4 py-2 items-center te
@if (trim($draft) === '')
@else
-
@endif
-
{{-- More-actions modal — opened by the NavBar ellipsis. Dismissible. --}}
@@ -180,4 +177,4 @@ class="flex-1 px-4 py-3 rounded-xl bg-[#EF4444] items-center">
-
+
diff --git a/resources/views/native/syncup/chats.blade.php b/resources/views/native/syncup/chats.blade.php
index 47cbb3e..97065d8 100644
--- a/resources/views/native/syncup/chats.blade.php
+++ b/resources/views/native/syncup/chats.blade.php
@@ -114,7 +114,7 @@ class="px-5 py-2 rounded-full {{ $active ? 'bg-[#00b4d8]' : 'bg-theme-surface-va
{{-- Floating Action Button — absolutely positioned at the bottom-right
of the screen content. The absolute child only occupies its placed
(56x56) bounds, so it overlays without blocking scroll touches. --}}
-
diff --git a/resources/views/native/syncup/friends.blade.php b/resources/views/native/syncup/friends.blade.php
index 8d0f5c3..f1c85ad 100644
--- a/resources/views/native/syncup/friends.blade.php
+++ b/resources/views/native/syncup/friends.blade.php
@@ -65,7 +65,7 @@
{{ $f['statusText'] }}
-
+
diff --git a/resources/views/native/syncup/profile.blade.php b/resources/views/native/syncup/profile.blade.php
index 9a4d9e6..971bdf2 100644
--- a/resources/views/native/syncup/profile.blade.php
+++ b/resources/views/native/syncup/profile.blade.php
@@ -12,7 +12,7 @@
{{-- Camera FAB pinned bottom-right --}}
-
+
@@ -81,7 +81,7 @@ class="w-full"
{{-- Copy field --}}
syncup.me/elena_rod
-
+
diff --git a/resources/views/native/test-layout.blade.php b/resources/views/native/test-layout.blade.php
index c46a719..b15958a 100644
--- a/resources/views/native/test-layout.blade.php
+++ b/resources/views/native/test-layout.blade.php
@@ -4,8 +4,8 @@
-
-
+
+
-
+
diff --git a/resources/views/native/testing.blade.php b/resources/views/native/testing.blade.php
new file mode 100644
index 0000000..e572f1a
--- /dev/null
+++ b/resources/views/native/testing.blade.php
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/resources/views/native/tweet-detail.blade.php b/resources/views/native/tweet-detail.blade.php
index 0f9b904..f9c613e 100644
--- a/resources/views/native/tweet-detail.blade.php
+++ b/resources/views/native/tweet-detail.blade.php
@@ -3,7 +3,7 @@
{{-- Top Bar --}}
-
+
Post
@@ -16,6 +16,7 @@
@@ -40,6 +41,7 @@ class="w-[44] h-[44] rounded-full"
@@ -70,10 +72,10 @@ class="w-full h-[220] rounded-xl"
{{-- Action Bar --}}
-
+
-
+
diff --git a/resources/views/native/twitter-feed.blade.php b/resources/views/native/twitter-feed.blade.php
index 0813236..482d3be 100644
--- a/resources/views/native/twitter-feed.blade.php
+++ b/resources/views/native/twitter-feed.blade.php
@@ -7,11 +7,12 @@
𝕏
-
+
@@ -32,6 +33,7 @@ class="w-[32] h-[32] rounded-full"
@@ -55,6 +57,7 @@ class="w-[40] h-[40] rounded-full"
@if ($tweet['imageUrl'])
diff --git a/resources/views/native/twitter-profile.blade.php b/resources/views/native/twitter-profile.blade.php
index 4a0b885..43a3067 100644
--- a/resources/views/native/twitter-profile.blade.php
+++ b/resources/views/native/twitter-profile.blade.php
@@ -3,7 +3,7 @@
{{-- Top Bar --}}
-
+
@@ -18,6 +18,7 @@
@@ -29,6 +30,7 @@ class="w-full h-[220]"
@@ -79,6 +81,7 @@ class="w-[68] h-[68] rounded-full"
{{-- Avatar --}}
@@ -99,6 +102,7 @@ class="w-[40] h-[40] rounded-full"
@if ($tweet['imageUrl'])
diff --git a/resources/views/native/youtube-channel.blade.php b/resources/views/native/youtube-channel.blade.php
index 18f1471..a2022cc 100644
--- a/resources/views/native/youtube-channel.blade.php
+++ b/resources/views/native/youtube-channel.blade.php
@@ -3,7 +3,7 @@
{{-- Top Bar --}}
-
+
@@ -15,6 +15,7 @@
{{-- Banner --}}
@@ -24,6 +25,7 @@ class="w-full h-[90]"
diff --git a/resources/views/native/youtube-home.blade.php b/resources/views/native/youtube-home.blade.php
index 1b5d1f3..cb489a9 100644
--- a/resources/views/native/youtube-home.blade.php
+++ b/resources/views/native/youtube-home.blade.php
@@ -29,6 +29,7 @@ class="px-3 py-2 rounded-lg {{ $activeCategory === $name ? 'bg-white' : 'bg-[#27
@@ -46,6 +47,7 @@ class="w-full h-[210]"
@@ -98,6 +100,7 @@ class="w-[140] h-[248] rounded-lg"
@@ -113,6 +116,7 @@ class="w-full h-[210]"
diff --git a/resources/views/native/youtube-search.blade.php b/resources/views/native/youtube-search.blade.php
index 0e8129e..74d0c4c 100644
--- a/resources/views/native/youtube-search.blade.php
+++ b/resources/views/native/youtube-search.blade.php
@@ -4,7 +4,7 @@
{{-- Top Bar — search field flexes to fill remaining width between
the back arrow and the search button so it adapts to phone width. --}}
-
+
@@ -15,7 +15,7 @@
class="flex-1 text-[15] text-white bg-transparent"
/>
-
+
diff --git a/resources/views/native/youtube-video.blade.php b/resources/views/native/youtube-video.blade.php
index 0a91408..6a4c2a0 100644
--- a/resources/views/native/youtube-video.blade.php
+++ b/resources/views/native/youtube-video.blade.php
@@ -32,7 +32,7 @@ class="w-full h-[220]"
{{-- Top controls (back / settings) — declared LAST so they
sit above the other overlay layers and receive taps. --}}
-
+
@@ -72,7 +72,7 @@ class="w-full h-[220]"
/>
{{ $likesFormatted }}
|
-
+
diff --git a/tests/Feature/Native/AccessibilityTest.php b/tests/Feature/Native/AccessibilityTest.php
new file mode 100644
index 0000000..5e6f52a
--- /dev/null
+++ b/tests/Feature/Native/AccessibilityTest.php
@@ -0,0 +1,66 @@
+not->toBeEmpty();
+
+ $renderErrors = [];
+ $violations = [];
+ $audited = 0;
+
+ foreach ($uris as $uri) {
+ if (in_array($uri, a11ySkipList(), true)) {
+ continue;
+ }
+
+ $visit = preg_replace('/\{[^}]+\}/', '0', $uri);
+
+ try {
+ $found = Native::visit($visit)->accessibilityViolations();
+ } catch (\Throwable $e) {
+ $renderErrors[$uri] = get_class($e).': '.$e->getMessage();
+
+ continue;
+ }
+
+ if ($found !== []) {
+ $violations[$uri] = $found;
+ }
+
+ $audited++;
+ }
+
+ $report = '';
+
+ foreach ($violations as $uri => $list) {
+ $report .= "\n[$uri]\n - ".implode("\n - ", $list);
+ }
+ foreach ($renderErrors as $uri => $error) {
+ $report .= "\n[$uri] failed to render: $error";
+ }
+
+ expect($violations)->toBe([], "Accessibility violations:$report");
+ expect($renderErrors)->toBe([], "Screens failed to render:$report");
+ expect($audited)->toBeGreaterThan(50);
+});
diff --git a/tests/Feature/Native/NavigationTransitionsTest.php b/tests/Feature/Native/NavigationTransitionsTest.php
new file mode 100644
index 0000000..d172cd2
--- /dev/null
+++ b/tests/Feature/Native/NavigationTransitionsTest.php
@@ -0,0 +1,44 @@
+assertScreen(IkeaHome::class)
+ ->assertNoNavigation()
+ ->call('viewProduct', 3)
+ ->assertNavigatedTo('/ikea/product/3')
+ ->assertTransition(Transition::SlideFromRight);
+});
+
+it('resolves parameter-less named routes for the cart and search', function () {
+ Native::visit('/ikea')
+ ->call('viewCart')
+ ->assertNavigatedTo('/ikea/cart')
+ ->assertTransition(Transition::SlideFromRight);
+
+ Native::visit('/ikea')
+ ->call('viewSearch')
+ ->assertNavigatedTo('/ikea/search')
+ ->assertTransition(Transition::SlideFromRight);
+});
+
+// ── @navigate. directive path (TransitionsDemo) ──
+
+it('pushes with the slide-from-bottom transition via @navigate', function () {
+ Native::visit('/transitions')
+ ->tap('Slide from Bottom')
+ ->assertNavigatedTo('/transitions/detail')
+ ->assertTransition(Transition::SlideFromBottom);
+});
+
+it('pushes with the parallax transition via @navigate', function () {
+ Native::visit('/transitions')
+ ->tap('Parallax Push')
+ ->assertNavigatedTo('/transitions/detail')
+ ->assertTransition(Transition::ParallaxPush);
+});
diff --git a/tests/Pest.php b/tests/Pest.php
index 40d096b..9606e3c 100644
--- a/tests/Pest.php
+++ b/tests/Pest.php
@@ -15,6 +15,9 @@
->use(Illuminate\Foundation\Testing\RefreshDatabase::class)
->in('Feature');
+// Native testing suite sugar: expect($screen)->toSee(...)->toBeAccessible() etc.
+Native\Mobile\Testing\PestExpectations::register();
+
/*
|--------------------------------------------------------------------------
| Expectations