diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 9816c96..0000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.claude/skills/pest-testing/SKILL.md b/.claude/skills/pest-testing/SKILL.md index ab27161..79cd0bc 100644 --- a/.claude/skills/pest-testing/SKILL.md +++ b/.claude/skills/pest-testing/SKILL.md @@ -1,16 +1,16 @@ --- 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." +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, architecture tests, or faster test runs with Test Impact Analysis. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, Tia (--tia), sharding, and all Pest 5 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code." license: MIT metadata: author: laravel --- -# Pest Testing 4 +# Pest Testing 5 ## Documentation -Use `search-docs` for detailed Pest 4 patterns and documentation. +Use `search-docs` for detailed Pest 5 patterns and documentation. ## Basic Usage @@ -46,6 +46,7 @@ it('is true', function () { - 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`. +- Run only tests affected by recent changes (Tia): `./vendor/bin/pest --parallel --tia`. ## Assertions @@ -82,16 +83,58 @@ it('has emails', function (string $email) { ]); ``` -## Pest 4 Features +## Pest 5 Features | Feature | Purpose | |---------|---------| +| Tia (Test Impact Analysis) | Rerun only tests affected by recent changes | +| Time-Balanced Sharding | Split tests across CI shards by execution time | +| New Validation Expectations | `toBeEmail()`, `toBeUlid()`, `toBeIpAddress()`, and more | | 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 | +### Tia (Test Impact Analysis) + +Tia reruns only tests affected by recent changes and replays cached results for the rest, dramatically reducing suite duration: + + +```shell +./vendor/bin/pest --parallel --tia +``` + +- Replayed tests are not skipped — cached tests store everything they produced, including covered lines and branches. +- Detects Laravel, Symfony, Livewire, and Inertia automatically. + +### New Validation Expectations + +Pest 5 ships eight new validation matchers, all supporting `.not` negation: + + +```php +expect('nuno@pestphp.com')->toBeEmail(); +expect('01ARZ3NDEKTSV4RRFFQ69G5FAV')->toBeUlid(); +expect('192.168.1.1')->toBeIpAddress(); +expect('00:1a:2b:3c:4d:5e')->toBeMacAddress(); +expect('example.com')->toBeHostname(); +expect('example.co.uk')->toBeDomain(); +expect('Zm9vYmFy')->toBeBase64(); +expect('deadbeef')->toBeHexadecimal(); +``` + +### Time-Balanced Sharding + +Distribute tests across CI shards by execution time rather than count: + + +```shell +./vendor/bin/pest --update-shards +./vendor/bin/pest --shard=1/4 +``` + +Commit `tests/.pest/shards.json` to the repository so CI shards stay consistent. + ### Browser Test Example Browser tests run in real browsers for full integration testing: @@ -140,14 +183,8 @@ $pages->assertNoJavaScriptErrors()->assertNoConsoleLogs(); 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') diff --git a/.junie/skills/pest-testing/SKILL.md b/.junie/skills/pest-testing/SKILL.md index ab27161..79cd0bc 100644 --- a/.junie/skills/pest-testing/SKILL.md +++ b/.junie/skills/pest-testing/SKILL.md @@ -1,16 +1,16 @@ --- 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." +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, architecture tests, or faster test runs with Test Impact Analysis. Covers: test()/it()/expect() syntax, datasets, mocking, browser testing (visit/click/fill), smoke testing, arch(), Livewire component tests, RefreshDatabase, Tia (--tia), sharding, and all Pest 5 features. Do not use for factories, seeders, migrations, controllers, models, or non-test PHP code." license: MIT metadata: author: laravel --- -# Pest Testing 4 +# Pest Testing 5 ## Documentation -Use `search-docs` for detailed Pest 4 patterns and documentation. +Use `search-docs` for detailed Pest 5 patterns and documentation. ## Basic Usage @@ -46,6 +46,7 @@ it('is true', function () { - 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`. +- Run only tests affected by recent changes (Tia): `./vendor/bin/pest --parallel --tia`. ## Assertions @@ -82,16 +83,58 @@ it('has emails', function (string $email) { ]); ``` -## Pest 4 Features +## Pest 5 Features | Feature | Purpose | |---------|---------| +| Tia (Test Impact Analysis) | Rerun only tests affected by recent changes | +| Time-Balanced Sharding | Split tests across CI shards by execution time | +| New Validation Expectations | `toBeEmail()`, `toBeUlid()`, `toBeIpAddress()`, and more | | 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 | +### Tia (Test Impact Analysis) + +Tia reruns only tests affected by recent changes and replays cached results for the rest, dramatically reducing suite duration: + + +```shell +./vendor/bin/pest --parallel --tia +``` + +- Replayed tests are not skipped — cached tests store everything they produced, including covered lines and branches. +- Detects Laravel, Symfony, Livewire, and Inertia automatically. + +### New Validation Expectations + +Pest 5 ships eight new validation matchers, all supporting `.not` negation: + + +```php +expect('nuno@pestphp.com')->toBeEmail(); +expect('01ARZ3NDEKTSV4RRFFQ69G5FAV')->toBeUlid(); +expect('192.168.1.1')->toBeIpAddress(); +expect('00:1a:2b:3c:4d:5e')->toBeMacAddress(); +expect('example.com')->toBeHostname(); +expect('example.co.uk')->toBeDomain(); +expect('Zm9vYmFy')->toBeBase64(); +expect('deadbeef')->toBeHexadecimal(); +``` + +### Time-Balanced Sharding + +Distribute tests across CI shards by execution time rather than count: + + +```shell +./vendor/bin/pest --update-shards +./vendor/bin/pest --shard=1/4 +``` + +Commit `tests/.pest/shards.json` to the repository so CI shards stay consistent. + ### Browser Test Example Browser tests run in real browsers for full integration testing: @@ -140,14 +183,8 @@ $pages->assertNoJavaScriptErrors()->assertNoConsoleLogs(); 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') diff --git a/AGENTS.md b/AGENTS.md index 29b3186..e4c2ca3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,8 +17,8 @@ This application is a Laravel application and its main Laravel ecosystems packag - laravel/pail (PAIL) - v1 - laravel/pint (PINT) - v1 - laravel/sail (SAIL) - v1 -- pestphp/pest (PEST) - v4 -- phpunit/phpunit (PHPUNIT) - v12 +- pestphp/pest (PEST) - v5 +- phpunit/phpunit (PHPUNIT) - v13 - tailwindcss (TAILWINDCSS) - v4 ## Skills Activation diff --git a/CLAUDE.md b/CLAUDE.md index 29b3186..e4c2ca3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -17,8 +17,8 @@ This application is a Laravel application and its main Laravel ecosystems packag - laravel/pail (PAIL) - v1 - laravel/pint (PINT) - v1 - laravel/sail (SAIL) - v1 -- pestphp/pest (PEST) - v4 -- phpunit/phpunit (PHPUNIT) - v12 +- pestphp/pest (PEST) - v5 +- phpunit/phpunit (PHPUNIT) - v13 - tailwindcss (TAILWINDCSS) - v4 ## Skills Activation diff --git a/README.md b/README.md index b49210c..c384cad 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ php artisan native:run android -W --no-vite ``` app/NativeComponents/Explore.php # The kitchen sink component resources/views/native/explore.blade.php # Blade template with <*> elements -routes/web.php # Single route: / → Explore +routes/mobile.php # All native routes (Route::native / nativeGroup) ``` ## License diff --git a/app/NativeComponents/Concerns/HasIkeaData.php b/app/NativeComponents/Concerns/HasIkeaData.php deleted file mode 100644 index 87126eb..0000000 --- a/app/NativeComponents/Concerns/HasIkeaData.php +++ /dev/null @@ -1,274 +0,0 @@ - - */ - public static function ikeaCategories(): array - { - return [ - ['name' => 'Living Room', 'icon' => 'weekend'], - ['name' => 'Bedroom', 'icon' => 'bed'], - ['name' => 'Kitchen', 'icon' => 'countertops'], - ['name' => 'Bathroom', 'icon' => 'bathroom'], - ['name' => 'Office', 'icon' => 'desk'], - ['name' => 'Dining', 'icon' => 'dining'], - ['name' => 'Outdoor', 'icon' => 'deck'], - ['name' => 'Storage', 'icon' => 'shelves'], - ]; - } - - /** - * @return array - */ - public static function ikeaProducts(): array - { - return [ - [ - 'name' => 'Sofa, 3-seat', - 'seriesName' => 'KLIPPAN', - 'imageUrl' => 'https://picsum.photos/seed/klippan/400/400', - 'price' => 349.00, - 'originalPrice' => null, - 'rating' => 4.5, - 'reviewCount' => 2847, - 'category' => 'Living Room', - 'badge' => null, - 'inStock' => true, - 'description' => 'A comfy sofa with a simple design that works well in various room settings. The cover is easy to keep clean since it is removable and machine washable.', - 'dimensions' => '180x88x66 cm', - 'material' => 'Cotton/polyester cover, wood frame', - 'color' => 'Vissle gray', - 'articleNumber' => '494.178.93', - 'isNew' => false, - ], - [ - 'name' => 'Bookcase', - 'seriesName' => 'KALLAX', - 'imageUrl' => 'https://picsum.photos/seed/kallax/400/400', - 'price' => 84.99, - 'originalPrice' => null, - 'rating' => 4.7, - 'reviewCount' => 5102, - 'category' => 'Storage', - 'badge' => 'Popular', - 'inStock' => true, - 'description' => 'You can use the furniture as a room divider because it looks good from every angle. Choose whether you want to hang it on the wall or stand it on the floor.', - 'dimensions' => '77x39x147 cm', - 'material' => 'Particleboard, fiberboard', - 'color' => 'White', - 'articleNumber' => '802.758.87', - 'isNew' => false, - ], - [ - 'name' => 'Bed frame with storage', - 'seriesName' => 'MALM', - 'imageUrl' => 'https://picsum.photos/seed/malm/400/400', - 'price' => 449.00, - 'originalPrice' => 549.00, - 'rating' => 4.3, - 'reviewCount' => 1893, - 'category' => 'Bedroom', - 'badge' => 'Last chance', - 'inStock' => true, - 'description' => 'Ample storage space is hidden neatly under the bed. A clean design that is just as beautiful on all sides. Place the bed on its own or with the headboard against a wall.', - 'dimensions' => '209x176x100 cm', - 'material' => 'Particleboard, fiberboard, solid wood', - 'color' => 'White stained oak veneer', - 'articleNumber' => '191.573.21', - 'isNew' => false, - ], - [ - 'name' => 'Desk', - 'seriesName' => 'BEKANT', - 'imageUrl' => 'https://picsum.photos/seed/bekant/400/400', - 'price' => 299.00, - 'originalPrice' => null, - 'rating' => 4.4, - 'reviewCount' => 1245, - 'category' => 'Office', - 'badge' => null, - 'inStock' => true, - 'description' => 'Sit/stand desk with a deep table top that provides a generous work surface. The net underneath keeps cables in place but is easily accessible.', - 'dimensions' => '160x80x65-125 cm', - 'material' => 'Particleboard, linoleum, steel', - 'color' => 'White/white', - 'articleNumber' => '490.228.08', - 'isNew' => false, - ], - [ - 'name' => 'Kitchen island', - 'seriesName' => 'VADHOLMA', - 'imageUrl' => 'https://picsum.photos/seed/vadholma/400/400', - 'price' => 499.00, - 'originalPrice' => null, - 'rating' => 4.6, - 'reviewCount' => 832, - 'category' => 'Kitchen', - 'badge' => null, - 'inStock' => true, - 'description' => 'Gives you extra storage, utility and work space. Perfect as a kitchen island in a bigger kitchen or against a wall in a smaller one.', - 'dimensions' => '126x79x90 cm', - 'material' => 'Solid birch, stainless steel', - 'color' => 'Black/oak', - 'articleNumber' => '303.508.22', - 'isNew' => false, - ], - [ - 'name' => 'LED floor lamp', - 'seriesName' => 'HEKTOGRAM', - 'imageUrl' => 'https://picsum.photos/seed/hekto/400/400', - 'price' => 39.99, - 'originalPrice' => 49.99, - 'rating' => 4.2, - 'reviewCount' => 612, - 'category' => 'Living Room', - 'badge' => 'Lower price', - 'inStock' => true, - 'description' => 'Provides soft ambient lighting. You can easily aim the light where you need it because the lamp head is adjustable. Uses LEDs, which consume up to 85% less energy.', - 'dimensions' => '176 cm', - 'material' => 'Steel, polycarbonate', - 'color' => 'Dark gray', - 'articleNumber' => '004.601.81', - 'isNew' => false, - ], - [ - 'name' => 'Dining table', - 'seriesName' => 'LISABO', - 'imageUrl' => 'https://picsum.photos/seed/lisabo/400/400', - 'price' => 199.00, - 'originalPrice' => null, - 'rating' => 4.5, - 'reviewCount' => 976, - 'category' => 'Dining', - 'badge' => null, - 'inStock' => true, - 'description' => 'Each table has its own unique character due to the distinctive grain pattern. The table top and legs are made of ash veneer and solid birch, durable materials.', - 'dimensions' => '140x78x74 cm', - 'material' => 'Ash veneer, solid birch', - 'color' => 'Ash veneer', - 'articleNumber' => '502.943.39', - 'isNew' => false, - ], - [ - 'name' => 'Outdoor lounge chair', - 'seriesName' => 'APPLARO', - 'imageUrl' => 'https://picsum.photos/seed/applaro/400/400', - 'price' => 130.00, - 'originalPrice' => 165.00, - 'rating' => 4.3, - 'reviewCount' => 543, - 'category' => 'Outdoor', - 'badge' => 'Lower price', - 'inStock' => true, - 'description' => 'Perfect for relaxing outdoors. The back can be adjusted to five different positions. Foldable, easy to store when not in use.', - 'dimensions' => '63x80x101 cm', - 'material' => 'Acacia wood', - 'color' => 'Brown stained', - 'articleNumber' => '202.085.29', - 'isNew' => false, - ], - [ - 'name' => 'Mirror cabinet with 2 doors', - 'seriesName' => 'HEMNES', - 'imageUrl' => 'https://picsum.photos/seed/hemnes/400/400', - 'price' => 189.00, - 'originalPrice' => null, - 'rating' => 4.6, - 'reviewCount' => 1134, - 'category' => 'Bathroom', - 'badge' => null, - 'inStock' => true, - 'description' => 'Shallow cabinet to keep all your smaller bathroom items organized. The mirror comes with safety film on the back, which reduces the risk of injury.', - 'dimensions' => '83x16x98 cm', - 'material' => 'Solid pine, mirror glass', - 'color' => 'White', - 'articleNumber' => '302.176.68', - 'isNew' => false, - ], - [ - 'name' => 'Armchair', - 'seriesName' => 'STRANDMON', - 'imageUrl' => 'https://picsum.photos/seed/strandmon/400/400', - 'price' => 329.00, - 'originalPrice' => null, - 'rating' => 4.8, - 'reviewCount' => 3421, - 'category' => 'Living Room', - 'badge' => 'Top seller', - 'inStock' => true, - 'description' => 'A classic wing chair with a timeless design. The high back provides extra support for your neck. Year after year it becomes one of the most beloved armchairs.', - 'dimensions' => '82x96x101 cm', - 'material' => 'Cotton/polyester, birch frame', - 'color' => 'Nordvalla dark gray', - 'articleNumber' => '903.432.24', - 'isNew' => false, - ], - [ - 'name' => 'Wardrobe', - 'seriesName' => 'PAX', - 'imageUrl' => 'https://picsum.photos/seed/pax/400/400', - 'price' => 570.00, - 'originalPrice' => null, - 'rating' => 4.4, - 'reviewCount' => 2156, - 'category' => 'Bedroom', - 'badge' => null, - 'inStock' => true, - 'description' => 'The PAX wardrobe can be customized to meet all your storage needs. Combine frames, doors, and interior organizers for a personalized solution.', - 'dimensions' => '150x60x236 cm', - 'material' => 'Particleboard, fiberboard', - 'color' => 'White', - 'articleNumber' => '290.235.17', - 'isNew' => false, - ], - [ - 'name' => 'Rug, flatwoven', - 'seriesName' => 'STOCKHOLM', - 'imageUrl' => 'https://picsum.photos/seed/stockholm/400/400', - 'price' => 249.00, - 'originalPrice' => null, - 'rating' => 4.7, - 'reviewCount' => 789, - 'category' => 'Living Room', - 'badge' => 'New', - 'inStock' => true, - 'description' => 'The rug is hand-woven by skilled craftspeople. The striped pattern with bold colors makes it a statement piece in any living room.', - 'dimensions' => '170x240 cm', - 'material' => '100% wool', - 'color' => 'Striped black/off-white', - 'articleNumber' => '902.290.06', - 'isNew' => true, - ], - ]; - } - - /** - * @return array - */ - public static function ikeaRoomIdeas(): array - { - return ['Small spaces', 'Scandinavian style', 'Budget friendly', 'Sustainable living', 'Student life', 'Family home']; - } - - public static function formatIkeaPrice(float $price): string - { - if ($price == floor($price)) { - return '$' . number_format($price, 0); - } - - return '$' . number_format($price, 2); - } - - public static function formatIkeaReviewCount(int $count): string - { - if ($count >= 1000) { - return number_format($count / 1000, 1) . 'k'; - } - - return (string) $count; - } -} diff --git a/app/NativeComponents/Counter.php b/app/NativeComponents/Counter.php index db962d3..c0d4ece 100644 --- a/app/NativeComponents/Counter.php +++ b/app/NativeComponents/Counter.php @@ -6,14 +6,11 @@ use Native\Mobile\Attributes\Poll; use Native\Mobile\Edge\Layouts\Builders\NavBarOptions; use Native\Mobile\Edge\NativeComponent; -use Native\Mobile\Facades\Camera; class Counter extends NativeComponent { public $count = 0; - public $photo = ''; - /** Direction of an active press-and-hold: 'up', 'down', or null. */ public ?string $holding = null; @@ -82,13 +79,6 @@ protected function step(): void $this->count += $this->holding === 'down' ? -$delta : $delta; } - public function testCamera() - { - Camera::getPhoto()->photoTaken(function ($photo) { - $this->photo = $photo->path; - }); - } - public function navigationOptions(): ?NavBarOptions { return NavBarOptions::make() diff --git a/app/NativeComponents/DemoLauncher.php b/app/NativeComponents/DemoLauncher.php index 5784fc4..5b72034 100644 --- a/app/NativeComponents/DemoLauncher.php +++ b/app/NativeComponents/DemoLauncher.php @@ -27,7 +27,9 @@ class DemoLauncher extends NativeComponent 'title' => 'Fundamentals', 'demos' => [ ['id' => 'counter', 'title' => 'Counter', 'subtitle' => 'Minimal Livewire-style counter', 'icon' => 'add', 'color' => '#10B981', 'url' => '/counter'], + ['id' => 'forms', 'title' => 'Slider', 'subtitle' => 'Text input, slider, toggle, checkbox, select, radio', 'icon' => 'square.text.square', 'color' => '#10B981', 'url' => '/explore/forms'], ['id' => 'typography', 'title' => 'Typography & Colors', 'subtitle' => 'Sizes, theme tokens, tailwind palette', 'icon' => 'textformat.alt', 'color' => '#A855F7', 'url' => '/explore/typography'], + ['id' => 'themelab', 'title' => 'Theme Lab', 'subtitle' => 'Design tokens, opacity ramps, variants, font aliases — live in light & dark', 'icon' => 'paintpalette.fill', 'color' => '#8B5CF6', 'url' => '/theme-lab'], ['id' => 'buttons', 'title' => 'Buttons', 'subtitle' => 'Variants, sizes, icons, states, pressable', 'icon' => 'square.and.pencil', 'color' => '#0EA5E9', 'url' => '/explore/buttons'], ['id' => 'buttonsform', 'title' => 'Buttons (Form)', 'subtitle' => 'Variants, sizes, extras in a NavigationStack + grouped form', 'icon' => 'list.bullet.rectangle', 'color' => '#0EA5E9', 'url' => '/buttons-form'], ['id' => 'icons', 'title' => 'Icons', 'subtitle' => 'IconHelper catalog + direct SF Symbols', 'icon' => 'star.fill', 'color' => '#EC4899', 'url' => '/explore/icons'], @@ -38,39 +40,29 @@ class DemoLauncher extends NativeComponent 'title' => 'Components & Patterns', 'demos' => [ ['id' => 'cards', 'title' => 'Cards & Chips', 'subtitle' => 'Card variants, chips, badges, list items', 'icon' => 'rectangle.stack.fill', 'color' => '#F59E0B', 'url' => '/explore/cards'], - ['id' => 'forms', 'title' => 'Slider', 'subtitle' => 'Text input, slider, toggle, checkbox, select, radio', 'icon' => 'square.text.square', 'color' => '#10B981', 'url' => '/explore/forms'], + ['id' => 'pickers', 'title' => 'Date & Time Pickers', 'subtitle' => 'Date/time/datetime, bounds, timezones, locales, inline & wheel', 'icon' => 'calendar', 'color' => '#F43F5E', 'url' => '/explore/pickers'], ['id' => 'sheets', 'title' => 'Sheets & Modals', 'subtitle' => 'Bottom-sheet detents, dismissible + blocking modals', 'icon' => 'square.on.square', 'color' => '#A855F7', 'url' => '/explore/sheets'], ['id' => 'menus', 'title' => 'Menus', 'subtitle' => 'Tap-to-open dropdowns on Pressable, Button, ListItem.trailing', 'icon' => 'ellipsis.circle', 'color' => '#0891B2', 'url' => '/explore/menus'], ['id' => 'mail', 'title' => 'Mail Inbox', 'subtitle' => 'Pull-to-refresh + leading/trailing swipe actions', 'icon' => 'envelope.fill', 'color' => '#0EA5E9', 'url' => '/mail-demo'], ['id' => 'refresh', 'title' => 'Pull to refresh', 'subtitle' => 'Native pull-to-refresh on custom card content', 'icon' => 'arrow.clockwise', 'color' => '#10B981', 'url' => '/refreshable-demo'], ['id' => 'reactivity', 'title' => 'Reactivity', 'subtitle' => '#[Computed], #[Poll] and #[Lazy] placeholder in one screen', 'icon' => 'bolt.fill', 'color' => '#8B5CF6', 'url' => '/reactivity'], ['id' => 'webview', 'title' => 'Webview', 'subtitle' => 'Embedded web content — remote URL + inline HTML, @navigated events', 'icon' => 'globe', 'color' => '#3B82F6', 'url' => '/webview-demo'], - // ['id' => 'eventchannel', 'title' => 'Event Channel Test', 'subtitle' => 'Native → PHP payload > 4KB (growable event buffer)', 'icon' => 'arrow.up.arrow.down', 'color' => '#EF4444', 'url' => '/event-channel-test'], - // ['id' => 'vibe', 'title' => 'Vibe — Live Events', 'subtitle' => 'Websocket broadcast events (Vask/Reverb) into a component', 'icon' => 'antenna.radiowaves.left.and.right', 'color' => '#22C55E', 'url' => '/vibe'], - // ['id' => 'vibe-private', 'title' => 'Vibe — Private Channel', 'subtitle' => 'Authenticated private-channel subscription (bearer + /broadcasting/auth)', 'icon' => 'lock.fill', 'color' => '#16A34A', 'url' => '/vibe-private'], - // ['id' => 'vibe-presence', 'title' => 'Vibe — Presence', 'subtitle' => 'Who\'s online + live join/leave (here/joining/leaving)', 'icon' => 'person.2.fill', 'color' => '#15803D', 'url' => '/vibe-presence'], - // ['id' => 'geo-watch', 'title' => 'Geo — Watch Position', 'subtitle' => 'Streaming GPS fixes (watchPosition/clearWatch)', 'icon' => 'location.fill', 'color' => '#F97316', 'url' => '/geo-watch'], ], ], [ 'title' => 'Motion & Chrome', 'demos' => [ ['id' => 'gestures', 'title' => 'Gestures', 'subtitle' => 'Double-tap (@doubleTap) ', 'icon' => 'hand.tap.fill', 'color' => '#6366F1', 'url' => '/gestures'], + ['id' => 'gamepad', 'title' => 'Game Pad', 'subtitle' => 'Held-press d-pad, auto-fire + shield (@tapDown / @tapUp)', 'icon' => 'gamecontroller.fill', 'color' => '#22C55E', 'url' => '/game-pad'], ['id' => 'transitions', 'title' => 'Page Transitions', 'subtitle' => 'Fade, slide, scale - every @navigate animation', 'icon' => 'arrow.left.arrow.right', 'color' => '#14B8A6', 'url' => '/transitions'], ['id' => 'animate', 'title' => 'Animations', 'subtitle' => 'Awesome native animations', 'icon' => 'sparkles', 'color' => '#F59E0B', 'url' => '/animate'], ['id' => 'numberswitcher', 'title' => 'Number Switcher', 'subtitle' => 'content-transition — rolling in-place digit changes', 'icon' => 'textformat.123', 'color' => '#14B8A6', 'url' => '/number-switcher'], - // ['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'], ], ], [ 'title' => 'Mini Apps', 'demos' => [ - ['id' => 'tabs', 'title' => 'Search Demo', 'subtitle' => 'TabsLayout + StackLayout (Home / Browse / Profile + push detail)', 'icon' => 'dashboard', 'color' => '#6366F1', 'url' => '/tabs'], ['id' => 'twitter', 'title' => 'Twitter / X', 'subtitle' => 'Feed, tweet detail, profile, compose', 'icon' => 'bubble.left.and.bubble.right.fill', 'color' => '#1D9BF0', 'url' => '/twitter'], - ['id' => 'ikea', 'title' => 'IKEA', 'subtitle' => 'Home, product detail, cart, search', 'icon' => 'bed.double.fill', 'color' => '#0058A3', 'url' => '/ikea'], ['id' => 'facebook', 'title' => 'Facebook', 'subtitle' => 'Feed, post, profile, create', 'icon' => 'person.2.fill', 'color' => '#1877F2', 'url' => '/facebook'], ['id' => 'instagram', 'title' => 'Instagram', 'subtitle' => 'Feed, post, profile, search', 'icon' => 'camera', 'color' => '#E1306C', 'url' => '/instagram'], ['id' => 'spotify', 'title' => 'Spotify', 'subtitle' => 'Home, playlist, artist, search', 'icon' => 'music.note', 'color' => '#1DB954', 'url' => '/spotify'], diff --git a/app/NativeComponents/DoomGame.php b/app/NativeComponents/DoomGame.php index c728083..712e5e9 100644 --- a/app/NativeComponents/DoomGame.php +++ b/app/NativeComponents/DoomGame.php @@ -2,6 +2,7 @@ namespace App\NativeComponents; +use Illuminate\View\View; use Local\DoomGame\Models\Enemy; use Local\DoomGame\Models\Player; use Local\DoomGame\Services\GameEngine; @@ -41,6 +42,16 @@ public function mount(): void $this->checkServer(); } + public function unmount(): void + { + // UI.SetBackground is app-global sticky native state — without + // this clear it leaks the window color into every screen visited + // after this one. Empty color = restore the platform default. + nativephp_call('UI.SetBackground', json_encode(['color' => null])); + + parent::unmount(); + } + public function checkServer(): void { $host = config('doom.server_host', '127.0.0.1'); @@ -196,7 +207,7 @@ public function checkResults(): void $this->isMultiplayer = false; } - public function render(): \Illuminate\View\View + public function render(): View { return view('doom-game'); } diff --git a/app/NativeComponents/DrawerHome.php b/app/NativeComponents/DrawerHome.php deleted file mode 100644 index 07f5792..0000000 --- a/app/NativeComponents/DrawerHome.php +++ /dev/null @@ -1,29 +0,0 @@ - 'modal']); - } -} diff --git a/app/NativeComponents/DrawerReveal.php b/app/NativeComponents/DrawerReveal.php deleted file mode 100644 index 8b5aceb..0000000 --- a/app/NativeComponents/DrawerReveal.php +++ /dev/null @@ -1,29 +0,0 @@ - 'reveal']); - } -} diff --git a/app/NativeComponents/EdgeChildTest.php b/app/NativeComponents/EdgeChildTest.php new file mode 100644 index 0000000..065ff9f --- /dev/null +++ b/app/NativeComponents/EdgeChildTest.php @@ -0,0 +1,33 @@ +) — a regression fixture for child-owned + * callback dispatch. Tapping the child's button must run + * EdgeChildTestCard::childTapped(), which emits back here and increments + * $childEvents (screen state, so it persists via the sealed snapshot). + */ +class EdgeChildTest extends NativeComponent +{ + public int $childEvents = 0; + + public function navTitle(): string + { + return 'Edge Child Test'; + } + + public function recordChildTap(int $taps = 0): void + { + $this->childEvents++; + } + + public function render(): View + { + return view('native.edge-child-test'); + } +} diff --git a/app/NativeComponents/EdgeChildTestCard.php b/app/NativeComponents/EdgeChildTestCard.php new file mode 100644 index 0000000..448edc1 --- /dev/null +++ b/app/NativeComponents/EdgeChildTestCard.php @@ -0,0 +1,43 @@ +. + * The @tap lives INSIDE this child, so its callback registers in the + * child's own registry rather than the mounting screen's. + */ +class EdgeChildTestCard extends NativeComponent +{ + /** Prop assigned from the mounting tag. */ + public string $label = 'Child'; + + /** Child-local state; bumps within the request's dispatch → render cycle. */ + public int $taps = 0; + + public function childTapped(): void + { + $this->taps++; + + // Bubbles to the parent's @child-tapped tag binding (screen state + // is snapshotted, so this survives across renders). + $this->emit('child-tapped', $this->taps); + } + + public function render(): Element + { + return Column::make( + Text::make("{$this->label}: child taps {$this->taps}")->fontSize(15), + Pressable::make( + Text::make('Tap child')->fontSize(16)->fontWeight(7), + )->onTap('childTapped')->padding(8), + )->gap(8)->padding(12); + } +} diff --git a/app/NativeComponents/Explore.php b/app/NativeComponents/Explore.php index a63ae0c..0370706 100644 --- a/app/NativeComponents/Explore.php +++ b/app/NativeComponents/Explore.php @@ -3,6 +3,7 @@ namespace App\NativeComponents; use Illuminate\Support\Str; +use Illuminate\View\View; use Native\Mobile\Attributes\OnNative; use Native\Mobile\Edge\NativeComponent; use Native\Mobile\Events\Alert\ButtonPressed; @@ -54,6 +55,16 @@ public function mount(): void $this->items = $this->fetchUsers($this->page); } + public function unmount(): void + { + // UI.SetBackground is app-global sticky native state — without + // this clear it leaks the window color into every screen visited + // after this one. Empty color = restore the platform default. + nativephp_call('UI.SetBackground', json_encode(['color' => null])); + + parent::unmount(); + } + public function showAlert() { Dialog::alert('Hello', 'Pick an option', ['Cancel', 'OK']); @@ -162,7 +173,7 @@ private function fetchUsers(int $page): array } } - public function render(): \Illuminate\View\View + public function render(): View { return view('explore'); } diff --git a/app/NativeComponents/ExploreForms.php b/app/NativeComponents/ExploreForms.php index d83ec6f..c1ec7ec 100644 --- a/app/NativeComponents/ExploreForms.php +++ b/app/NativeComponents/ExploreForms.php @@ -2,19 +2,29 @@ namespace App\NativeComponents; +use Illuminate\View\View; use Native\Mobile\Edge\NativeComponent; class ExploreForms extends NativeComponent { public string $name = ''; + public string $email = ''; + public float $slideValue = 0.0; + public float $slideDebounced = 50.0; - public float $slideBlur = 25.0; + + public float $slideBlur = 2.0; + public bool $subscribed = true; + public bool $termsAccepted = false; + public string $favoriteLanguage = 'PHP'; + public string $pricingPlan = 'pro'; + public bool $notificationsOn = true; public function navTitle(): string @@ -34,7 +44,7 @@ public function selectPricingPlan(string $value): void $this->pricingPlan = $value; } - public function render(): \Illuminate\View\View + public function render(): View { return view('native.explore.forms'); } diff --git a/app/NativeComponents/ExploreIcons.php b/app/NativeComponents/ExploreIcons.php index ce6d84b..c36c4ad 100644 --- a/app/NativeComponents/ExploreIcons.php +++ b/app/NativeComponents/ExploreIcons.php @@ -5,9 +5,9 @@ use App\Icons\Android; use App\Icons\Ios; use Illuminate\View\View; +use Native\Mobile\Edge\Concerns\HasVirtualListWindow; use Native\Mobile\Edge\Layouts\Builders\NavBarOptions; use Native\Mobile\Edge\NativeComponent; -use Native\Mobile\Edge\Traits\HasVirtualListWindow; use Native\Mobile\Platform; /** diff --git a/app/NativeComponents/ExplorePickers.php b/app/NativeComponents/ExplorePickers.php new file mode 100644 index 0000000..112c727 --- /dev/null +++ b/app/NativeComponents/ExplorePickers.php @@ -0,0 +1,54 @@ +lastEvent = $value === '' ? '(cleared)' : $value; + } + + public function render(): View + { + return view('native.explore.pickers'); + } +} diff --git a/app/NativeComponents/FacebookFeed.php b/app/NativeComponents/FacebookFeed.php index d20e809..851b666 100644 --- a/app/NativeComponents/FacebookFeed.php +++ b/app/NativeComponents/FacebookFeed.php @@ -3,6 +3,7 @@ namespace App\NativeComponents; use App\NativeComponents\Concerns\HasFacebookData; +use Illuminate\View\View; use Native\Mobile\Edge\NativeComponent; use Native\Mobile\Edge\Transition; @@ -10,17 +11,27 @@ class FacebookFeed extends NativeComponent { use HasFacebookData; - /** @var array */ + /** @var array Keyed by post id, stable across refresh rotation. */ public array $likedPosts = []; + public int $refreshCount = 0; + + /** Post id whose "more actions" sheet is open, or null. */ + public ?int $menuPostId = null; + public function navTitle(): string { return 'Facebook'; } - public function viewPost(int $index): void + public function refresh(): void + { + $this->refreshCount++; + } + + public function viewPost(int $id): void { - $this->navigate($this->route('facebook.post', $index)) + $this->navigate($this->route('facebook.post', $id)) ->transition(Transition::SlideFromRight); } @@ -36,32 +47,56 @@ public function createPost(): void ->transition(Transition::SlideFromBottom); } - public function toggleLike(int $index): void + public function toggleLike(int $id): void { - if (isset($this->likedPosts[$index])) { - unset($this->likedPosts[$index]); + if (isset($this->likedPosts[$id])) { + unset($this->likedPosts[$id]); } else { - $this->likedPosts[$index] = true; + $this->likedPosts[$id] = true; } } - public function render(): \Illuminate\View\View + public function openPostMenu(int $id): void + { + $this->menuPostId = $id; + } + + public function closePostMenu(): void + { + $this->menuPostId = null; + } + + public function render(): View { $users = self::fbUsers(); $posts = self::fbPosts(); - $stories = array_filter($users, fn (array $u): bool => $u['hasStory']); - foreach ($posts as $i => &$post) { + $stories = []; + foreach ($users as $userId => $user) { + if ($user['hasStory']) { + $user['id'] = $userId; + $stories[] = $user; + } + } + + foreach ($posts as $id => &$post) { + $post['id'] = $id; $post['user'] = $users[$post['userId']]; $post['reactionsFormatted'] = self::formatFbCount( - $post['reactions'] + (isset($this->likedPosts[$i]) ? 1 : 0) + $post['reactions'] + (isset($this->likedPosts[$id]) ? 1 : 0) ); - $post['isLiked'] = isset($this->likedPosts[$i]); + $post['isLiked'] = isset($this->likedPosts[$id]); + } + unset($post); + + if ($this->refreshCount > 0 && count($posts) > 1) { + $offset = $this->refreshCount % count($posts); + $posts = array_merge(array_slice($posts, $offset), array_slice($posts, 0, $offset)); } return view('facebook-feed', [ 'posts' => $posts, - 'stories' => array_values($stories), + 'stories' => $stories, ]); } } diff --git a/app/NativeComponents/FacebookProfile.php b/app/NativeComponents/FacebookProfile.php index a9549e2..1430c48 100644 --- a/app/NativeComponents/FacebookProfile.php +++ b/app/NativeComponents/FacebookProfile.php @@ -3,6 +3,7 @@ namespace App\NativeComponents; use App\NativeComponents\Concerns\HasFacebookData; +use Illuminate\View\View; use Native\Mobile\Edge\NativeComponent; use Native\Mobile\Edge\Transition; @@ -10,7 +11,6 @@ class FacebookProfile extends NativeComponent { use HasFacebookData; - /** @var array */ public array $user = []; /** @var array */ @@ -18,6 +18,8 @@ class FacebookProfile extends NativeComponent public int $selectedTab = 0; + public bool $friendRequested = false; + public function mount(): void { $id = (int) $this->param('id'); @@ -35,13 +37,18 @@ public function selectTab(int $index): void $this->selectedTab = $index; } + public function toggleFriendRequest(): void + { + $this->friendRequested = ! $this->friendRequested; + } + public function viewPost(int $postId): void { $this->navigate($this->route('facebook.post', $postId)) ->transition(Transition::SlideFromRight); } - public function render(): \Illuminate\View\View + public function render(): View { $users = self::fbUsers(); $allPosts = self::fbPosts(); @@ -55,9 +62,15 @@ public function render(): \Illuminate\View\View $postsWithMeta[] = $post; } + $photos = array_values(array_filter(array_map( + fn (array $post): ?string => $post['imageUrl'], + $allPosts + ))); + return view('facebook-profile', [ 'postsWithMeta' => $postsWithMeta, 'friendsFormatted' => self::formatFbCount($this->user['friendCount']), + 'photos' => $photos, ]); } } diff --git a/app/NativeComponents/GamePad.php b/app/NativeComponents/GamePad.php new file mode 100644 index 0000000..03a00dd --- /dev/null +++ b/app/NativeComponents/GamePad.php @@ -0,0 +1,204 @@ + + */ + public array $shots = []; + + public int $wallHits = 0; + + /** Wall side currently flashing from a hit (up/down/left/right), or null. */ + public ?string $flashingWall = null; + + public int $nextShotId = 1; + + public int $fireCooldown = 0; + + public int $flashTicksLeft = 0; + + public function navTitle(): string + { + return 'Game Pad'; + } + + /** `@tapDown` on a d-pad button — move immediately, then keep moving each tick. */ + public function startMove(string $direction): void + { + $this->moving = $direction; + $this->facing = $direction; + $this->stepPlayer(); + } + + /** `@tapUp` on every d-pad button. */ + public function stopMove(): void + { + $this->moving = null; + } + + /** `@tapDown` on FIRE — first shot is instant, the tick loop keeps firing. */ + public function startFire(): void + { + $this->firing = true; + $this->fireShot(); + $this->fireCooldown = self::FIRE_COOLDOWN; + } + + /** `@tapUp` on FIRE. */ + public function stopFire(): void + { + $this->firing = false; + } + + /** `@tapDown` on SHIELD. */ + public function raiseShield(): void + { + $this->shieldRaised = true; + } + + /** `@tapUp` on SHIELD. */ + public function lowerShield(): void + { + $this->shieldRaised = false; + } + + /** The game loop — held movement, held auto-fire, shot flight, wall flash decay. */ + #[Poll(50)] + public function tick(): void + { + if ($this->moving !== null) { + $this->stepPlayer(); + } + + if ($this->firing && --$this->fireCooldown <= 0) { + $this->fireShot(); + $this->fireCooldown = self::FIRE_COOLDOWN; + } + + $this->advanceShots(); + + if ($this->flashTicksLeft > 0 && --$this->flashTicksLeft === 0) { + $this->flashingWall = null; + } + } + + private function stepPlayer(): void + { + [$dx, $dy] = $this->vector($this->moving ?? $this->facing); + $limit = (self::ARENA - self::PLAYER_SIZE) / 2; + + $this->x = max(-$limit, min($limit, $this->x + $dx * self::PLAYER_SPEED)); + $this->y = max(-$limit, min($limit, $this->y + $dy * self::PLAYER_SPEED)); + } + + private function fireShot(): void + { + [$dx, $dy] = $this->vector($this->facing); + + $this->shots[] = [ + 'id' => $this->nextShotId++, + 'x' => $this->x, + 'y' => $this->y, + 'dx' => $dx * self::SHOT_SPEED, + 'dy' => $dy * self::SHOT_SPEED, + ]; + } + + private function advanceShots(): void + { + $limit = (self::ARENA - self::SHOT_SIZE) / 2; + $surviving = []; + + foreach ($this->shots as $shot) { + $shot['x'] += $shot['dx']; + $shot['y'] += $shot['dy']; + + if (abs($shot['x']) > $limit || abs($shot['y']) > $limit) { + $this->wallHits++; + $this->flashingWall = match (true) { + $shot['y'] < -$limit => 'up', + $shot['y'] > $limit => 'down', + $shot['x'] < -$limit => 'left', + default => 'right', + }; + $this->flashTicksLeft = self::FLASH_TICKS; + + continue; + } + + $surviving[] = $shot; + } + + $this->shots = $surviving; + } + + /** + * Unit vector for a d-pad direction (screen coordinates — up is negative y). + * + * @return array{float, float} + */ + private function vector(string $direction): array + { + return match ($direction) { + 'up' => [0.0, -1.0], + 'down' => [0.0, 1.0], + 'left' => [-1.0, 0.0], + default => [1.0, 0.0], + }; + } + + public function render(): View + { + return view('native.game-pad'); + } +} diff --git a/app/NativeComponents/GeoWatchDemo.php b/app/NativeComponents/GeoWatchDemo.php deleted file mode 100644 index 8d3d823..0000000 --- a/app/NativeComponents/GeoWatchDemo.php +++ /dev/null @@ -1,91 +0,0 @@ -locationUpdated() handler and re-renders. - * The watch auto-stops on unmount (walk away from the screen = GPS off); - * Stop exercises Geolocation::clearWatch() manually. - */ -class GeoWatchDemo extends NativeComponent -{ - public string $watchId = ''; - - public bool $watching = false; - - public int $updates = 0; - - public ?float $latitude = null; - - public ?float $longitude = null; - - public ?float $accuracy = null; - - public ?float $speed = null; - - public ?float $heading = null; - - public ?string $error = null; - - public function navTitle(): string - { - return 'Geo — Watch Position'; - } - - public function startSteaming() - { - $this->error = null; - - // Ask first; start streaming only once permission is settled. - Geolocation::requestPermissions() - ->permissionRequestResult(function ($event) { - if (($event->location ?? 'denied') !== 'granted') { - $this->error = 'Location permission '.($event->location ?? 'denied'); - - return; - } - - $this->watchId = Geolocation::watchPosition(fineAccuracy: true) - ->interval(2000) - ->minDistance(1) - ->locationUpdated(function ($event) { - if (! $event->success) { - $this->error = $event->error; - - return; - } - - $this->updates++; - $this->latitude = $event->latitude; - $this->longitude = $event->longitude; - $this->accuracy = $event->accuracy; - $this->speed = $event->speed ?? null; - $this->heading = $event->heading ?? null; - }) - ->getId(); - - $this->watching = true; - }); - } - - public function stopStreaming() - { - if ($this->watchId !== '') { - Geolocation::clearWatch($this->watchId); - } - - $this->watching = false; - } - - public function render(): View - { - return view('native.geo-watch-demo'); - } -} diff --git a/app/NativeComponents/IkeaCart.php b/app/NativeComponents/IkeaCart.php deleted file mode 100644 index 9e06db0..0000000 --- a/app/NativeComponents/IkeaCart.php +++ /dev/null @@ -1,80 +0,0 @@ - */ - public array $cartItems = [0 => 1, 1 => 1, 5 => 2]; - - public function incrementItem(int $productIndex): void - { - if (isset($this->cartItems[$productIndex]) && $this->cartItems[$productIndex] < 10) { - $this->cartItems[$productIndex]++; - } - } - - public function decrementItem(int $productIndex): void - { - if (isset($this->cartItems[$productIndex])) { - if ($this->cartItems[$productIndex] <= 1) { - unset($this->cartItems[$productIndex]); - } else { - $this->cartItems[$productIndex]--; - } - } - } - - public function removeItem(int $productIndex): void - { - unset($this->cartItems[$productIndex]); - } - - public function checkout(): void - { - Dialog::alert('Order Placed!', 'Your order has been submitted. You will receive a confirmation email shortly.'); - } - - public function render(): \Illuminate\View\View - { - $allProducts = self::ikeaProducts(); - $items = []; - $subtotal = 0.0; - - foreach ($this->cartItems as $productIndex => $qty) { - if (isset($allProducts[$productIndex])) { - $product = $allProducts[$productIndex]; - $lineTotal = $product['price'] * $qty; - $subtotal += $lineTotal; - $items[] = [ - 'productIndex' => $productIndex, - 'seriesName' => $product['seriesName'], - 'name' => $product['name'], - 'imageUrl' => $product['imageUrl'], - 'price' => $product['price'], - 'priceFormatted' => self::formatIkeaPrice($product['price']), - 'color' => $product['color'], - 'qty' => $qty, - 'lineTotalFormatted' => self::formatIkeaPrice($lineTotal), - ]; - } - } - - $deliveryFee = $subtotal >= 500 ? 0.0 : 49.00; - - return view('ikea-cart', [ - 'items' => $items, - 'subtotalFormatted' => self::formatIkeaPrice($subtotal), - 'deliveryFeeFormatted' => $deliveryFee > 0 ? self::formatIkeaPrice($deliveryFee) : 'FREE', - 'totalFormatted' => self::formatIkeaPrice($subtotal + $deliveryFee), - 'freeDelivery' => $deliveryFee === 0.0, - 'itemCount' => array_sum($this->cartItems), - ]); - } -} diff --git a/app/NativeComponents/IkeaHome.php b/app/NativeComponents/IkeaHome.php deleted file mode 100644 index 2dd5dab..0000000 --- a/app/NativeComponents/IkeaHome.php +++ /dev/null @@ -1,82 +0,0 @@ -selectedCategory = $category; - } - - public function viewProduct(int $index): void - { - $this->navigate($this->route('ikea.product', $index)) - ->transition(Transition::SlideFromRight); - } - - public function viewCart(): void - { - $this->navigate($this->route('ikea.cart')) - ->transition(Transition::SlideFromRight); - } - - public function viewSearch(): void - { - $this->navigate($this->route('ikea.search')) - ->transition(Transition::SlideFromRight); - } - - public function render(): \Illuminate\View\View - { - $products = self::ikeaProducts(); - $categories = self::ikeaCategories(); - - $newArrivals = []; - $categoryProducts = []; - - foreach ($products as $i => $product) { - $product['originalIndex'] = $i; - $product['priceFormatted'] = self::formatIkeaPrice($product['price']); - $product['originalPriceFormatted'] = $product['originalPrice'] ? self::formatIkeaPrice($product['originalPrice']) : null; - $product['reviewFormatted'] = self::formatIkeaReviewCount($product['reviewCount']); - - if ($product['isNew'] || $product['badge'] === 'Top seller') { - $newArrivals[] = $product; - } - - if ($product['category'] === $this->selectedCategory) { - $categoryProducts[] = $product; - } - } - - $popular = array_slice($products, 0, 6); - foreach ($popular as $i => &$p) { - $p['originalIndex'] = $i; - $p['priceFormatted'] = self::formatIkeaPrice($p['price']); - $p['originalPriceFormatted'] = $p['originalPrice'] ? self::formatIkeaPrice($p['originalPrice']) : null; - $p['reviewFormatted'] = self::formatIkeaReviewCount($p['reviewCount']); - } - - return view('ikea-home', [ - 'categories' => $categories, - 'popular' => $popular, - 'newArrivals' => $newArrivals, - 'categoryProducts' => $categoryProducts, - 'roomIdeas' => self::ikeaRoomIdeas(), - ]); - } -} diff --git a/app/NativeComponents/IkeaProduct.php b/app/NativeComponents/IkeaProduct.php deleted file mode 100644 index e24a9ae..0000000 --- a/app/NativeComponents/IkeaProduct.php +++ /dev/null @@ -1,60 +0,0 @@ -param('id'); - $products = self::ikeaProducts(); - $this->product = $products[$id] ?? $products[0]; - } - - public function incrementQty(): void - { - if ($this->quantity < 10) { - $this->quantity++; - } - } - - public function decrementQty(): void - { - if ($this->quantity > 1) { - $this->quantity--; - } - } - - public function toggleFavorite(): void - { - $this->isFavorited = ! $this->isFavorited; - } - - public function addToCart(): void - { - Dialog::alert('Added to bag', $this->quantity . 'x ' . $this->product['seriesName'] . ' ' . $this->product['name'] . ' added to your shopping bag.'); - } - - public function render(): \Illuminate\View\View - { - return view('ikea-product', [ - 'priceFormatted' => self::formatIkeaPrice($this->product['price']), - 'originalPriceFormatted' => $this->product['originalPrice'] ? self::formatIkeaPrice($this->product['originalPrice']) : null, - 'reviewFormatted' => self::formatIkeaReviewCount($this->product['reviewCount']), - 'totalFormatted' => self::formatIkeaPrice($this->product['price'] * $this->quantity), - ]); - } -} \ No newline at end of file diff --git a/app/NativeComponents/IkeaSearch.php b/app/NativeComponents/IkeaSearch.php deleted file mode 100644 index 2bf629d..0000000 --- a/app/NativeComponents/IkeaSearch.php +++ /dev/null @@ -1,62 +0,0 @@ -searchQuery = $value; - } - - public function selectRoom(string $room): void - { - $this->selectedRoom = $this->selectedRoom === $room ? '' : $room; - } - - public function viewProduct(int $index): void - { - $this->navigate($this->route('ikea.product', $index)) - ->transition(Transition::SlideFromRight); - } - - public function render(): \Illuminate\View\View - { - $allProducts = self::ikeaProducts(); - $categories = self::ikeaCategories(); - - $results = []; - foreach ($allProducts as $i => $product) { - $matchesSearch = $this->searchQuery === '' || - str_contains(strtolower($product['name']), strtolower($this->searchQuery)) || - str_contains(strtolower($product['seriesName']), strtolower($this->searchQuery)) || - str_contains(strtolower($product['category']), strtolower($this->searchQuery)); - - $matchesRoom = $this->selectedRoom === '' || $product['category'] === $this->selectedRoom; - - if ($matchesSearch && $matchesRoom) { - $product['originalIndex'] = $i; - $product['priceFormatted'] = self::formatIkeaPrice($product['price']); - $product['originalPriceFormatted'] = $product['originalPrice'] ? self::formatIkeaPrice($product['originalPrice']) : null; - $product['reviewFormatted'] = self::formatIkeaReviewCount($product['reviewCount']); - $results[] = $product; - } - } - - return view('ikea-search', [ - 'categories' => $categories, - 'results' => $results, - 'resultCount' => count($results), - ]); - } -} diff --git a/app/NativeComponents/InstagramFeed.php b/app/NativeComponents/InstagramFeed.php index ad1ef1f..998f638 100644 --- a/app/NativeComponents/InstagramFeed.php +++ b/app/NativeComponents/InstagramFeed.php @@ -3,6 +3,7 @@ namespace App\NativeComponents; use App\NativeComponents\Concerns\HasInstagramData; +use Illuminate\View\View; use Native\Mobile\Edge\Layouts\Builders\NavAction; use Native\Mobile\Edge\Layouts\Builders\NavBarOptions; use Native\Mobile\Edge\NativeComponent; @@ -44,6 +45,13 @@ public function viewMessages(): void /** @var array */ public array $savedPosts = []; + public int $refreshCount = 0; + + public function refresh(): void + { + $this->refreshCount++; + } + public function viewPost(int $index): void { $this->navigate($this->route('instagram.post', $index)) @@ -74,19 +82,35 @@ public function toggleSave(int $index): void } } - public function render(): \Illuminate\View\View + public function render(): View { $users = self::igUsers(); $posts = self::igPosts(); - $stories = array_values(array_filter($users, fn (array $u): bool => $u['hasStory'])); - foreach ($posts as $i => &$post) { + $stories = []; + foreach ($users as $userId => $user) { + if ($user['hasStory']) { + $user['id'] = $userId; + $stories[] = $user; + } + } + + foreach ($posts as $id => &$post) { + $post['id'] = $id; $post['user'] = $users[$post['userId']]; - $post['isLiked'] = isset($this->likedPosts[$i]); - $post['isSaved'] = isset($this->savedPosts[$i]); + $post['isLiked'] = isset($this->likedPosts[$id]); + $post['isSaved'] = isset($this->savedPosts[$id]); $post['likesFormatted'] = self::formatIgCount( $post['likes'] + ($post['isLiked'] ? 1 : 0) ); + $post['commentsFormatted'] = self::formatIgCount($post['commentCount']); + $post['sharesFormatted'] = self::formatIgCount(intdiv($post['likes'], 12)); + } + unset($post); + + if ($this->refreshCount > 0 && count($posts) > 1) { + $offset = $this->refreshCount % count($posts); + $posts = array_merge(array_slice($posts, $offset), array_slice($posts, 0, $offset)); } return view('instagram-feed', [ diff --git a/app/NativeComponents/InstagramProfile.php b/app/NativeComponents/InstagramProfile.php index 88bf2a6..8a63e05 100644 --- a/app/NativeComponents/InstagramProfile.php +++ b/app/NativeComponents/InstagramProfile.php @@ -3,6 +3,7 @@ namespace App\NativeComponents; use App\NativeComponents\Concerns\HasInstagramData; +use Illuminate\View\View; use Native\Mobile\Edge\NativeComponent; use Native\Mobile\Edge\Transition; @@ -10,12 +11,13 @@ class InstagramProfile extends NativeComponent { use HasInstagramData; - /** @var array */ public array $user = []; /** @var array */ public array $userPosts = []; + public bool $isFollowing = false; + public function mount(): void { $id = (int) $this->param('id'); @@ -28,13 +30,18 @@ public function mount(): void ); } + public function toggleFollow(): void + { + $this->isFollowing = ! $this->isFollowing; + } + public function viewPost(int $postId): void { $this->navigate($this->route('instagram.post', $postId)) ->transition(Transition::SlideFromRight); } - public function render(): \Illuminate\View\View + public function render(): View { $allPosts = self::igPosts(); @@ -48,7 +55,7 @@ public function render(): \Illuminate\View\View return view('instagram-profile', [ 'postsWithIndex' => $postsWithIndex, 'postsFormatted' => self::formatIgCount($this->user['postCount']), - 'followersFormatted' => self::formatIgCount($this->user['followers']), + 'followersFormatted' => self::formatIgCount($this->user['followers'] + ($this->isFollowing ? 1 : 0)), 'followingFormatted' => self::formatIgCount($this->user['following']), 'highlights' => self::igStoryHighlights(), ]); diff --git a/app/NativeComponents/InstagramSearch.php b/app/NativeComponents/InstagramSearch.php index 0e6bdf8..5c7e8a8 100644 --- a/app/NativeComponents/InstagramSearch.php +++ b/app/NativeComponents/InstagramSearch.php @@ -3,6 +3,7 @@ namespace App\NativeComponents; use App\NativeComponents\Concerns\HasInstagramData; +use Illuminate\View\View; use Native\Mobile\Edge\NativeComponent; use Native\Mobile\Edge\Transition; @@ -16,10 +17,15 @@ public function viewPost(int $index): void ->transition(Transition::SlideFromRight); } - public function render(): \Illuminate\View\View + public function render(): View { $posts = self::igPosts(); + foreach ($posts as $id => &$post) { + $post['id'] = $id; + } + unset($post); + return view('instagram-search', [ 'posts' => $posts, ]); diff --git a/app/NativeComponents/JustifyRepro.php b/app/NativeComponents/JustifyRepro.php new file mode 100644 index 0000000..2e78870 --- /dev/null +++ b/app/NativeComponents/JustifyRepro.php @@ -0,0 +1,33 @@ +long = ! $this->long; + } + + #[Poll(3000)] + public function tick() + { + $this->toggle(); + } + + public function render(): View + { + return view('native.justify-repro'); + } +} diff --git a/app/NativeComponents/Layouts/DrawerLayout.php b/app/NativeComponents/Layouts/DrawerLayout.php deleted file mode 100644 index 8acf0bf..0000000 --- a/app/NativeComponents/Layouts/DrawerLayout.php +++ /dev/null @@ -1,39 +0,0 @@ -width(300); - - $mode = method_exists($screen, 'drawerMode') ? $screen->drawerMode() : 'modal'; - - return $mode === 'reveal' ? $drawer->reveal() : $drawer->modal(); - } -} diff --git a/app/NativeComponents/Layouts/NativeTabsLayout.php b/app/NativeComponents/Layouts/NativeTabsLayout.php deleted file mode 100644 index d2f5476..0000000 --- a/app/NativeComponents/Layouts/NativeTabsLayout.php +++ /dev/null @@ -1,72 +0,0 @@ -back() - ->title($screen->navTitle()); - } - - public function tabBar(NativeComponent $screen): ?TabBar - { - return TabBar::make() - ->activeColor('#A855F7') - ->minimizeOnScroll() - ->add(Tab::link('Home', '/native-tabs', - ios: Ios::House, android: Android::Home)->badge('3')) - ->add(Tab::search( - 'Search', - placeholder: 'Search articles, songs, people…', - ios: Ios::Magnifyingglass, - android: Android::Search, - )) - ->add(Tab::link('Profile', '/native-tabs/profile', - ios: Ios::Person, android: Android::Person)); - } - - /** - * Persistent MiniPlayer-style accessory pinned above the tab bar. - * On iOS 26+ it inhabits the new `tabViewBottomAccessory` slot and - * tucks inline with the active tab on scroll-down (paired with the - * `minimizeOnScroll()` flag above). On older iOS this returns null - * so nothing renders. - */ - public function tabBarAccessory(NativeComponent $screen): ?Element - { - $row = Row::make()->center()->gap(8); - $row->addChild(Icon::make('music.note')); - $row->addChild(Text::make('Liquid Glass demo — scroll to minimize')); - - return $row; - } -} diff --git a/app/NativeComponents/ListingDetail.php b/app/NativeComponents/ListingDetail.php index 114b0ad..640ee36 100644 --- a/app/NativeComponents/ListingDetail.php +++ b/app/NativeComponents/ListingDetail.php @@ -3,6 +3,7 @@ namespace App\NativeComponents; use App\NativeComponents\Concerns\HasListingData; +use Illuminate\View\View; use Native\Mobile\Edge\NativeComponent; use Native\Mobile\Facades\Dialog; @@ -10,7 +11,6 @@ class ListingDetail extends NativeComponent { use HasListingData; - /** @var array */ public array $listing = []; /** @var array */ @@ -36,6 +36,16 @@ public function mount(): void $this->listingReviews = $allReviews[$this->listingId] ?? $allReviews[0] ?? []; } + public function unmount(): void + { + // UI.SetBackground is app-global sticky native state — without + // this clear it leaks the window color into every screen visited + // after this one. Empty color = restore the platform default. + nativephp_call('UI.SetBackground', json_encode(['color' => null])); + + parent::unmount(); + } + public function toggleWishlist(): void { $this->isWishlisted = ! $this->isWishlisted; @@ -60,10 +70,10 @@ public function prevImage(): void public function reserve(): void { - Dialog::alert('Reservation Requested', 'Your reservation for ' . $this->listing['title'] . ' has been submitted!'); + Dialog::alert('Reservation Requested', 'Your reservation for '.$this->listing['title'].' has been submitted!'); } - public function render(): \Illuminate\View\View + public function render(): View { $bathroomsFormatted = $this->listing['bathrooms'] == floor($this->listing['bathrooms']) ? (int) $this->listing['bathrooms'] diff --git a/app/NativeComponents/NativeChromeDemo.php b/app/NativeComponents/NativeChromeDemo.php deleted file mode 100644 index 616e70b..0000000 --- a/app/NativeComponents/NativeChromeDemo.php +++ /dev/null @@ -1,93 +0,0 @@ -subtitle('NavigationStack toolbar') - // A plain icon — fires its press handler directly. - ->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 - // styling on iOS. - ->action( - NavAction::make('more') - ->icon('ellipsis') - ->a11yLabel('More options') - ->items([ - NavAction::make('mark_read') - ->icon('checkmark.circle') - ->label('Mark all read') - ->press('markAllRead'), - NavAction::make('mute') - ->icon(ios: Ios::BellSlash, android: AndroidOutlined::VolumeMute) - ->label('Mute notifications') - ->press('mute'), - NavAction::make('archive') - ->icon('archivebox') - ->label('Archive') - ->press('archive'), - NavAction::make('delete') - ->icon('trash') - ->label('Delete') - ->destructive() - ->press('deleteIt'), - ]) - ); - } - - public function shareIt(): void - { - $this->shareCount++; - $this->lastAction = 'Share tapped'; - } - - public function markAllRead(): void { $this->lastAction = 'Mark all read'; } - public function mute(): void { $this->lastAction = 'Mute notifications'; } - public function archive(): void { $this->lastAction = 'Archive'; } - public function deleteIt(): void { $this->lastAction = 'Delete (destructive)'; } - - /** Push the detail screen — exercises the multi-level NavigationStack. */ - public function pushDetail(): void - { - $this->navigate('/native-chrome/detail') - ->transition(Transition::SlideFromRight); - } - - public function render(): \Illuminate\View\View - { - return view('native-chrome-demo'); - } -} diff --git a/app/NativeComponents/NativeChromeDetail.php b/app/NativeComponents/NativeChromeDetail.php deleted file mode 100644 index ff23cf9..0000000 --- a/app/NativeComponents/NativeChromeDetail.php +++ /dev/null @@ -1,47 +0,0 @@ -subtitle('Pushed via NavigationStack') - ->action(NavAction::make('star')->icon('star')->a11yLabel('Favorite')->press('toggleStar')); - } - - public bool $starred = false; - - public function toggleStar(): void - { - $this->starred = ! $this->starred; - } - - public function render(): \Illuminate\View\View - { - return view('native-chrome-detail'); - } -} diff --git a/app/NativeComponents/NativeTabsHome.php b/app/NativeComponents/NativeTabsHome.php deleted file mode 100644 index 48cb138..0000000 --- a/app/NativeComponents/NativeTabsHome.php +++ /dev/null @@ -1,145 +0,0 @@ - 'Ada Lovelace', 'subtitle' => 'Analytical Engine programmer', 'url' => '/'], -// ['title' => 'Brian Kernighan', 'subtitle' => 'Co-author of C', 'url' => '/'], -// ['title' => 'Charlie Munger', 'subtitle' => 'Investor', 'url' => '/'], -// ['title' => 'Dana Scott', 'subtitle' => 'Domain theory', 'url' => '/'], -// ['title' => 'Edsger Dijkstra', 'subtitle' => 'Algorithms', 'url' => '/'], -// ['title' => 'Fei-Fei Li', 'subtitle' => 'Computer vision', 'url' => '/'], -// ['title' => 'Grace Hopper', 'subtitle' => 'COBOL pioneer', 'url' => '/'], -// ['title' => 'Hedy Lamarr', 'subtitle' => 'Frequency hopping', 'url' => '/'], -// ['title' => 'Ivan Sutherland', 'subtitle' => 'Sketchpad / 3D graphics', 'url' => '/'], -// ['title' => 'James Gosling', 'subtitle' => 'Java', 'url' => '/'], -// ['title' => 'Katherine Johnson', 'subtitle' => 'NASA mathematician', 'url' => '/'], -// ['title' => 'Linus Torvalds', 'subtitle' => 'Linux & Git', 'url' => '/'], -// ]; -// } - - -// public function onSearchQuery(string $query): array -// { -// $needle = trim($query); -// if ($needle === '') { -// return []; -// } -// -// $posts = Http::timeout(5) -// ->get('https://jsonplaceholder.typicode.com/posts', ['q' => $needle]) -// ->json() ?? []; -// -// return collect($posts) -// ->take(20) -// ->map(fn($p) => Row::make() -// ->class('items-start gap-3 px-4 py-3') -// ->onPress("openPost({$p['id']})") -// ->addChild( -// // Tinted "P" avatar — could be Image::make($u->avatar_url) for real users. -// Column::make() -// ->class('w-10 h-10 rounded-full bg-purple-100 items-center justify-center') -// ->addChild(Text::make(str($p['title'])->limit(1, '')->upper())->class('text-purple-700 font-semibold')) -// ) -// ->addChild( -// Column::make() -// ->class('flex-1 gap-1') -// ->addChild( -// Text::make($p['title'] ?? '(no title)') -// ->class('font-semibold') -// ) -// ->addChild( -// Text::make(str(($p['body'] ?? ''))->limit(120)->value()) -// ->class('text-sm text-gray-500') -// ) -// ) -// ->addChild( -// Text::make("#{$p['id']}") -// ->class('text-xs text-gray-400 px-2 py-1 rounded bg-gray-100') -// ) -// ) -// ->values() -// ->toArray(); -// } - - - public function onSearchQuery(string $query): array - { - $needle = trim($query); - if ($needle === '') { - return []; - } - - $posts = Http::timeout(5) - ->get('https://jsonplaceholder.typicode.com/posts', ['q' => $needle]) - ->json() ?? []; - - return collect($posts) - ->take(20) - ->map(fn ($p) => view('search-post-row', ['post' => $p])) - ->values() - ->toArray(); - } - - - -// public function onSearchQuery(string $query): array -// { -// $needle = trim($query); -// if ($needle === '') { -// return []; -// } -// -// try { -// $posts = Http::timeout(5) -// ->get('https://jsonplaceholder.typicode.com/posts', ['q' => $needle]) -// ->json() ?? []; -// } catch (\Throwable) { -// return []; -// } -// -// return collect($posts) -// ->take(20) -// ->map(fn ($p) => [ -// 'title' => $p['title'] ?? '(no title)', -// 'subtitle' => str(($p['body'] ?? ''))->limit(90)->value(), -// 'leading' => 'article', -// 'url' => '/', -// ]) -// ->values() -// ->toArray(); -// } - - public function tap(): void - { - $this->taps++; - } - - public function render(): \Illuminate\View\View - { - return view('native.native-tabs-home'); - } -} diff --git a/app/NativeComponents/NativeTabsProfile.php b/app/NativeComponents/NativeTabsProfile.php deleted file mode 100644 index 17a28d3..0000000 --- a/app/NativeComponents/NativeTabsProfile.php +++ /dev/null @@ -1,18 +0,0 @@ -format(H:i:s.v)` for display. */ - public string $lastRenderAt = ''; - - /** Set when a user-initiated handler enters PHP (tap, setRows). - * Cleared after render so subsequent un-tracked renders don't - * show stale wall-clock numbers. */ - private float $actionStartMicro = 0.0; - - public function navTitle(): string - { - return 'Performance'; - } - - public function tap(): void - { - $this->actionStartMicro = microtime(true); - } - - public function setRows(int $count): void - { - $this->actionStartMicro = microtime(true); - $this->rowCount = $count; - // Reset the window when the count changes so we don't try to emit - // rows above the new total. Native will report the real visible - // range on first scroll/appear. - $this->virtualWindowFrom = 0; - $this->virtualWindowTo = min(79, $count - 1); - } - - public function reset(): void - { - $this->actionStartMicro = microtime(true); - $this->renderCount = 0; - $this->lastBuildMs = 0.0; - $this->lastActionToRenderMs = 0.0; - $this->lastElementCount = 0; - } - - public function render(): \Illuminate\View\View - { - $buildStart = microtime(true); - - $this->renderCount++; - $this->lastRenderAt = now()->format('H:i:s.v'); - - // Element count is deterministic from the tree we're about to - // render — header chrome + N list-items + 5 row-count buttons. - // Hardcoding the chrome (8 elements) keeps the count honest; - // it's the rowCount that dominates anyway. - // Element count is now bounded by the visible window — that's the - // whole point of . The full `rowCount` is a - // logical total ridden by the native LazyColumn, not a tree size. - $chromeElementCount = 8; - $buttonElementCount = 6; - $windowSize = max(0, $this->virtualWindowTo - $this->virtualWindowFrom + 1); - $this->lastElementCount = $chromeElementCount + $buttonElementCount + $windowSize; - - $view = view('native.perf-demo'); - - $this->lastBuildMs = round((microtime(true) - $buildStart) * 1000, 2); - - if ($this->actionStartMicro > 0) { - $this->lastActionToRenderMs = round((microtime(true) - $this->actionStartMicro) * 1000, 2); - $this->actionStartMicro = 0.0; - } - - return $view; - } -} diff --git a/app/NativeComponents/PerfShowdown.php b/app/NativeComponents/PerfShowdown.php deleted file mode 100644 index 33b9309..0000000 --- a/app/NativeComponents/PerfShowdown.php +++ /dev/null @@ -1,95 +0,0 @@ -subtitle('Real numbers from the C extension'); - } - - public function tap(): void - { - $this->counter++; - } - - public function tap10(): void - { - $this->counter += 10; - } - - public function toggleMemo(): void - { - $flags = nativephp_runtime_flags(); - nativephp_set_runtime_flags($flags ^ 0x02); - } - - public function render(): View - { - $stats = function_exists('nativephp_last_publish_stats') - ? nativephp_last_publish_stats() - : [ - 'node_count' => 0, 'full_count' => 0, 'reuse_count' => 0, - 'flat_size' => 0, 'was_skip' => false, - 'build_ms' => 0.0, 'total_ms' => 0.0, - 'runtime_flags' => function_exists('nativephp_runtime_flags') - ? nativephp_runtime_flags() - : 0, - ]; - - $memoOn = ((int) $stats['runtime_flags'] & 0x02) !== 0; - - $rows = []; - for ($i = 0; $i < self::ROW_COUNT; $i++) { - $rows[] = [ - 'key' => "row-{$i}", - 'label' => "row #{$i}", - 'value' => sprintf('%.2f ms', $i * 0.01), - ]; - } - - return view('native.perf-showdown', [ - 'stats' => $stats, - 'flatBytes' => $this->formatBytes((int) $stats['flat_size']), - 'memoOn' => $memoOn, - 'counter' => $this->counter, - 'rowCount' => self::ROW_COUNT, - 'rows' => $rows, - ]); - } - - private function formatBytes(int $bytes): string - { - if ($bytes < 1024) { - return $bytes.' B'; - } - if ($bytes < 1024 * 1024) { - return number_format($bytes / 1024, 1).' KB'; - } - - return number_format($bytes / 1024 / 1024, 2).' MB'; - } -} diff --git a/app/NativeComponents/Phase1KeyTest.php b/app/NativeComponents/Phase1KeyTest.php deleted file mode 100644 index 682db96..0000000 --- a/app/NativeComponents/Phase1KeyTest.php +++ /dev/null @@ -1,188 +0,0 @@ -key($domainId)` makes node ids - * stable across renders even when siblings reorder. Tinker can't drive - * the custom on-device libphp.a, so we run the same checks inside a - * demo screen and render their results. - * - * Tests: - * 1. Unkeyed children get sequential positional ids (legacy behavior). - * 2. Keyed children get hash ids (large, non-sequential). - * 3. Reordering keyed children preserves their ids (the Phase 1 win). - */ -class Phase1KeyTest extends NativeComponent -{ - public function navTitle(): string - { - return 'Phase 1 — Keys'; - } - - public function navigationOptions(): ?NavBarOptions - { - return NavBarOptions::make() - ->displayMode('large') - ->subtitle('Stable keyed-identity verification'); - } - - public function backToLauncher(): void - { - $this->back(); - } - - public function render(): Element - { - $results = $this->runTests(); - - $column = Column::make()->fillWidth()->padding(16)->gap(16); - - $column->addChild( - Pressable::make( - Row::make( - Text::make('<')->fontSize(20)->fontWeight(7)->color('#94A3B8'), - Spacer::make()->width(8), - Text::make('Back')->fontSize(15)->color('#94A3B8'), - )->gap(0) - )->onPress('backToLauncher')->padding(4, 0, 4, 0) - ); - - $column->addChild( - Text::make('Phase 1 verification') - ->fontSize(22)->fontWeight(7)->color('#F1F5F9') - ); - $column->addChild( - Text::make('Calls Element::toArray() three times and inspects the ids.') - ->fontSize(13)->color('#94A3B8') - ); - - foreach ($results as $r) { - $column->addChild($this->renderResultCard($r)); - } - - $allPassed = array_reduce($results, fn ($carry, $r) => $carry && $r['pass'], true); - $column->addChild( - Text::make($allPassed ? 'All tests passed.' : 'One or more tests failed — check ids above.') - ->fontSize(15) - ->fontWeight(6) - ->color($allPassed ? '#10B981' : '#EF4444') - ); - $column->addChild(Spacer::make()->height(24)); - - return ScrollView::make() - ->fill()->safeArea()->bg('#0F172A') - ->addChild($column); - } - - /** - * Runs the three Phase 1 checks and returns a normalized result - * list. Pure PHP — no native calls, no side effects on the wider - * runtime — so it's safe to call from render() every frame. - */ - private function runTests(): array - { - $results = []; - - // ── Test 1: unkeyed → sequential positional ids ── - $reg = new CallbackRegistry; - $t1 = Column::make( - Text::make('A'), - Text::make('B'), - Text::make('C'), - )->toArray($reg); - $unkeyedIds = array_map(fn ($c) => $c['id'], $t1['children']); - // Expect [2, 3, 4] — root is 1, children fall to $nextId++. - $results[] = [ - 'name' => 'Test 1 — unkeyed children get sequential ids', - 'detail' => 'Three unkeyed Text children under unkeyed Column. Expect [2, 3, 4].', - 'observed' => 'root='.$t1['id'].', children=['.implode(', ', $unkeyedIds).']', - 'pass' => $t1['id'] === 1 && $unkeyedIds === [2, 3, 4], - ]; - - // ── Test 2: keyed children → hash ids (non-sequential, > 2^16) ── - $reg = new CallbackRegistry; - $t2 = Column::make( - Text::make('A')->key('item-a'), - Text::make('B')->key('item-b'), - )->toArray($reg); - $keyedIds = array_map(fn ($c) => $c['id'], $t2['children']); - $hashShape = array_reduce( - $keyedIds, - fn ($carry, $id) => $carry && $id > 0xFFFF && $id <= 0xFFFFFFFF, - true, - ); - $results[] = [ - 'name' => 'Test 2 — keyed children get hash ids', - 'detail' => "Two Text children with ->key('item-a') and ->key('item-b').", - 'observed' => 'root='.$t2['id'].', children=['.implode(', ', $keyedIds).']', - 'pass' => $t2['id'] === 1 - && $hashShape - && $keyedIds[0] !== $keyedIds[1], - ]; - - // ── Test 3: reordering keyed children preserves ids ── - // Phase 1 win: same key → same id regardless of position. - $reg = new CallbackRegistry; - $t3 = Column::make( - Text::make('B')->key('item-b'), - Text::make('A')->key('item-a'), - )->toArray($reg); - $reorderedIds = array_map(fn ($c) => $c['id'], $t3['children']); - // 'item-b' was at index 1 in Test 2, now at index 0. Same id. - $itemBSame = $reorderedIds[0] === $keyedIds[1]; - $itemASame = $reorderedIds[1] === $keyedIds[0]; - $results[] = [ - 'name' => 'Test 3 — reorder preserves ids', - 'detail' => 'Same two keys as Test 2, swapped order. Each id follows its key.', - 'observed' => "before=[item-a:{$keyedIds[0]}, item-b:{$keyedIds[1]}], " - ."after=[item-b:{$reorderedIds[0]}, item-a:{$reorderedIds[1]}]", - 'pass' => $itemBSame && $itemASame, - ]; - - return $results; - } - - private function renderResultCard(array $r): Element - { - $passColor = $r['pass'] ? '#10B981' : '#EF4444'; - $passLabel = $r['pass'] ? 'PASS' : 'FAIL'; - - $card = Column::make() - ->fillWidth()->padding(14)->gap(6) - ->bg('#1E293B')->borderRadius(10); - - $card->addChild( - Row::make( - Text::make($passLabel) - ->fontSize(11)->fontWeight(7)->color('#FFFFFF') - ->bg($passColor)->borderRadius(4)->padding(2, 8), - Spacer::make()->width(10), - Text::make($r['name']) - ->fontSize(14)->fontWeight(6)->color('#F1F5F9'), - )->fillWidth()->alignItems(2) - ); - - $card->addChild( - Text::make($r['detail'])->fontSize(12)->color('#94A3B8') - ); - $card->addChild( - Text::make($r['observed'])->fontSize(12)->color('#CBD5E1') - ); - - return $card; - } -} diff --git a/app/NativeComponents/Phase2MemoTest.php b/app/NativeComponents/Phase2MemoTest.php deleted file mode 100644 index 6258426..0000000 --- a/app/NativeComponents/Phase2MemoTest.php +++ /dev/null @@ -1,207 +0,0 @@ -displayMode('large') - ->subtitle('REUSE-marker verification'); - } - - public function increment(): void - { - $this->counter++; - } - - public function decrement(): void - { - if ($this->counter > 0) { - $this->counter--; - } - } - - /** - * Toggle the NPHP_FLAG_SUBTREE_MEMO bit on the region. Effect kicks in - * on the next publish (this method's callback returns, runLoop loops, - * memoizedToArray() consults the new flag value). - */ - public function toggleMemo(): void - { - $now = nativephp_runtime_flags(); - $next = $now ^ 0x02; // flip bit 1 - nativephp_set_runtime_flags($next); - error_log(sprintf( - 'Phase2MemoTest: memo flipped 0x%08x → 0x%08x', - $now, - $next, - )); - } - - public function backToLauncher(): void - { - $this->back(); - } - - public function render(): Element - { - $this->currentFlags = nativephp_runtime_flags(); - $memoOn = ($this->currentFlags & 0x02) !== 0; - - $root = Column::make()->fillWidth()->padding(16)->gap(12); - - $root->addChild( - Pressable::make( - Row::make( - Text::make('<')->fontSize(20)->fontWeight(7)->color('#94A3B8'), - Spacer::make()->width(8), - Text::make('Back')->fontSize(15)->color('#94A3B8'), - )->gap(0) - )->onPress('backToLauncher')->padding(4, 0, 4, 0) - ); - - // ── Header / toggle ── - $root->addChild( - Text::make('Phase 2 verification') - ->fontSize(22)->fontWeight(7)->color('#F1F5F9') - ); - $root->addChild( - Text::make('Watch BENCH_C `flat=...` in the logs as you tap.') - ->fontSize(13)->color('#94A3B8') - ); - - $root->addChild($this->renderFlagsCard($memoOn)); - - // ── Counter ── - $root->addChild( - Row::make( - Pressable::make( - Text::make('-1')->fontSize(16)->fontWeight(7)->color('#FFFFFF') - )->onPress('decrement')->bg('#475569')->borderRadius(8)->padding(10, 18), - Spacer::make()->width(12), - Text::make((string) $this->counter) - ->fontSize(28)->fontWeight(7)->color('#38BDF8'), - Spacer::make()->width(12), - Pressable::make( - Text::make('+1')->fontSize(16)->fontWeight(7)->color('#FFFFFF') - )->onPress('increment')->bg('#0EA5E9')->borderRadius(8)->padding(10, 18), - )->fillWidth()->alignItems(2)->justifyContent(1) - ); - - // ── Big keyed list ── - // Keys are stable per Phase 1 — every row keeps its id across renders, - // so Phase 2's contentHash check can detect unchanged rows and emit - // REUSE markers. Without keys, every row would get a fresh positional - // id and never qualify as unchanged. - $list = Column::make()->fillWidth()->gap(2)->key('rows-container'); - for ($i = 0; $i < self::ROW_COUNT; $i++) { - // Static rows — content never changes. With memo on, these - // should all be REUSE on every publish after the first. - $list->addChild( - Row::make( - Text::make("row #{$i}")->fontSize(13)->color('#CBD5E1'), - Spacer::make()->flexGrow(1), - Text::make(sprintf('%.2f ms', $i * 0.1)) - ->fontSize(12)->color('#64748B'), - ) - ->key("row-{$i}") - ->fillWidth()->padding(8, 12)->bg('#1E293B')->borderRadius(6) - ); - } - $root->addChild($list); - - $root->addChild(Spacer::make()->height(24)); - - return ScrollView::make() - ->fill()->safeArea()->bg('#0F172A') - ->addChild($root); - } - - private function renderFlagsCard(bool $memoOn): Element - { - $statusColor = $memoOn ? '#10B981' : '#64748B'; - $statusLabel = $memoOn ? 'MEMO ON' : 'MEMO OFF'; - - $card = Column::make() - ->fillWidth()->padding(12)->gap(8) - ->bg('#1E293B')->borderRadius(10); - - $card->addChild( - Row::make( - Text::make($statusLabel) - ->fontSize(11)->fontWeight(7)->color('#FFFFFF') - ->bg($statusColor)->borderRadius(4)->padding(2, 8), - Spacer::make()->width(10), - Text::make(sprintf('runtime_flags = 0x%08x', $this->currentFlags)) - ->fontSize(12)->color('#CBD5E1')->fontWeight(6), - )->fillWidth()->alignItems(2) - ); - - $card->addChild( - Pressable::make( - Text::make('Toggle memo') - ->fontSize(13)->fontWeight(6)->color('#0F172A') - ) - ->onPress('toggleMemo') - ->bg('#F59E0B')->borderRadius(6)->padding(8, 14) - ); - - $card->addChild( - Text::make( - $memoOn - ? 'REUSE markers active. Static rows below should not re-serialize on +1.' - : 'All nodes emit FULL. Every +1 re-serializes the full tree.' - )->fontSize(11)->color('#94A3B8') - ); - - return $card; - } -} diff --git a/app/NativeComponents/Phase4VirtualListTest.php b/app/NativeComponents/Phase4VirtualListTest.php deleted file mode 100644 index e1419cd..0000000 --- a/app/NativeComponents/Phase4VirtualListTest.php +++ /dev/null @@ -1,126 +0,0 @@ -displayMode('large') - ->subtitle('Windowed materialization'); - } - - public function backToLauncher(): void - { - $this->back(); - } - - public function render(): Element - { - // The trait's defaults are [0..79] — clamp to our real total. - $from = max(0, $this->virtualWindowFrom); - $to = min(self::TOTAL_ITEMS - 1, $this->virtualWindowTo); - - // Materialize only the rows inside [from..to]. For 10k logical - // items with a window of ~80, that's ~80 Element constructions - // per render — vs. 10000 for a naive list. - $rows = []; - for ($i = $from; $i <= $to; $i++) { - $rows[] = Row::make( - Text::make("row #{$i}")->fontSize(13)->color('#CBD5E1'), - Spacer::make()->flexGrow(1), - Text::make(sprintf('%.2f ms', $i * 0.001)) - ->fontSize(12)->color('#64748B'), - ) - ->key("row-{$i}") - ->fillWidth()->padding(8, 12)->bg('#1E293B')->borderRadius(6); - } - - $list = NativeVirtualList::make(...$rows) - ->fillWidth() - ->flexGrow(1); - - // The element's window/count/handler attrs are set via the - // applyAttributes hook the Blade precompiler also uses — this is - // the canonical API; there's no fluent setter on the plugin - // element. - $list->applyAttributes([ - 'count' => self::TOTAL_ITEMS, - 'window_from' => $from, - 'window_to' => $to, - 'estimated_row_height' => 48, - 'overscan' => 40, - 'on_window_change' => 'setVirtualWindow', - ]); - - $header = Column::make()->fillWidth()->padding(16)->gap(12); - - $header->addChild( - Pressable::make( - Row::make( - Text::make('<')->fontSize(20)->fontWeight(7)->color('#94A3B8'), - Spacer::make()->width(8), - Text::make('Back')->fontSize(15)->color('#94A3B8'), - )->gap(0) - )->onPress('backToLauncher')->padding(4, 0, 4, 0) - ); - - $header->addChild( - Text::make('Phase 4 verification') - ->fontSize(22)->fontWeight(7)->color('#F1F5F9') - ); - $header->addChild( - Text::make(sprintf( - '%d logical items, current window = [%d..%d]. Scroll the list — '. - 'native fires on_window_change when the visible range drifts past '. - 'the materialized slice, and the next render emits the new slice.', - self::TOTAL_ITEMS, - $from, - $to, - ))->fontSize(13)->color('#94A3B8') - ); - - $root = Column::make()->fill()->safeArea()->bg('#0F172A'); - $root->addChild($header); - $root->addChild($list); - - return $root; - } -} diff --git a/app/NativeComponents/SkiaShowcase.php b/app/NativeComponents/SkiaShowcase.php deleted file mode 100644 index 7853d4b..0000000 --- a/app/NativeComponents/SkiaShowcase.php +++ /dev/null @@ -1,56 +0,0 @@ - '#0f0f1e'])); - } - - public function togglePulse(): void - { - $this->pulseRunning = ! $this->pulseRunning; - } - - public function toggleSpin(): void - { - $this->spinRunning = ! $this->spinRunning; - } - - public function toggleTimer(): void - { - $this->timerRunning = ! $this->timerRunning; - } - - public function setTimerSeconds(int $seconds): void - { - $this->timerSeconds = $seconds; - $this->timerRunning = false; - } - - public function toggleBars(): void - { - $this->barAnimRunning = $this->barAnimRunning === 1 ? 0 : 1; - } - - public function toggleWave(): void - { - $this->waveRunning = $this->waveRunning === 1 ? 0 : 1; - } - - public function render(): \Illuminate\View\View - { - return view('skia-showcase'); - } -} \ No newline at end of file diff --git a/app/NativeComponents/SpotifyArtist.php b/app/NativeComponents/SpotifyArtist.php index 116c12f..32f12aa 100644 --- a/app/NativeComponents/SpotifyArtist.php +++ b/app/NativeComponents/SpotifyArtist.php @@ -3,6 +3,7 @@ namespace App\NativeComponents; use App\NativeComponents\Concerns\HasSpotifyData; +use Illuminate\View\View; use Native\Mobile\Edge\NativeComponent; use Native\Mobile\Facades\Dialog; @@ -10,7 +11,6 @@ class SpotifyArtist extends NativeComponent { use HasSpotifyData; - /** @var array */ public array $artist = []; /** @var array */ @@ -20,6 +20,8 @@ class SpotifyArtist extends NativeComponent public function mount(): void { + nativephp_call('UI.SetBackground', json_encode(['color' => '#121212'])); + $id = (int) $this->param('id'); $artists = self::spotifyArtists(); $this->artist = $artists[$id] ?? $artists[0]; @@ -30,6 +32,16 @@ public function mount(): void ); } + public function unmount(): void + { + // UI.SetBackground is app-global sticky native state — without + // this clear it leaks the window color into every screen visited + // after this one. Empty color = restore the platform default. + nativephp_call('UI.SetBackground', json_encode(['color' => null])); + + parent::unmount(); + } + public function toggleFollow(): void { $this->isFollowing = ! $this->isFollowing; @@ -39,11 +51,11 @@ public function playTrack(int $index): void { $track = $this->topTracks[$index] ?? null; if ($track) { - Dialog::alert('Now Playing', $track['title'] . ' by ' . $this->artist['name']); + Dialog::alert('Now Playing', $track['title'].' by '.$this->artist['name']); } } - public function render(): \Illuminate\View\View + public function render(): View { $tracksWithMeta = []; foreach ($this->topTracks as $track) { diff --git a/app/NativeComponents/SpotifyHome.php b/app/NativeComponents/SpotifyHome.php index a88b25d..3a2c971 100644 --- a/app/NativeComponents/SpotifyHome.php +++ b/app/NativeComponents/SpotifyHome.php @@ -3,6 +3,7 @@ namespace App\NativeComponents; use App\NativeComponents\Concerns\HasSpotifyData; +use Illuminate\View\View; use Native\Mobile\Edge\NativeComponent; use Native\Mobile\Edge\Transition; @@ -10,6 +11,21 @@ class SpotifyHome extends NativeComponent { use HasSpotifyData; + public function mount(): void + { + nativephp_call('UI.SetBackground', json_encode(['color' => '#121212'])); + } + + public function unmount(): void + { + // UI.SetBackground is app-global sticky native state — without + // this clear it leaks the window color into every screen visited + // after this one. Empty color = restore the platform default. + nativephp_call('UI.SetBackground', json_encode(['color' => null])); + + parent::unmount(); + } + public function navTitle(): string { return 'Spotify'; @@ -33,7 +49,7 @@ public function viewSearch(): void ->transition(Transition::SlideFromRight); } - public function render(): \Illuminate\View\View + public function render(): View { $playlists = self::spotifyPlaylists(); $artists = self::spotifyArtists(); diff --git a/app/NativeComponents/SpotifyPlaylist.php b/app/NativeComponents/SpotifyPlaylist.php index 5922588..1107e8c 100644 --- a/app/NativeComponents/SpotifyPlaylist.php +++ b/app/NativeComponents/SpotifyPlaylist.php @@ -3,6 +3,7 @@ namespace App\NativeComponents; use App\NativeComponents\Concerns\HasSpotifyData; +use Illuminate\View\View; use Native\Mobile\Edge\NativeComponent; use Native\Mobile\Edge\Transition; use Native\Mobile\Facades\Dialog; @@ -11,7 +12,6 @@ class SpotifyPlaylist extends NativeComponent { use HasSpotifyData; - /** @var array */ public array $playlist = []; /** @var array */ @@ -19,6 +19,8 @@ class SpotifyPlaylist extends NativeComponent public function mount(): void { + nativephp_call('UI.SetBackground', json_encode(['color' => '#121212'])); + $id = (int) $this->param('id'); $playlists = self::spotifyPlaylists(); $this->playlist = $playlists[$id] ?? $playlists[0]; @@ -32,6 +34,16 @@ public function mount(): void } } + public function unmount(): void + { + // UI.SetBackground is app-global sticky native state — without + // this clear it leaks the window color into every screen visited + // after this one. Empty color = restore the platform default. + nativephp_call('UI.SetBackground', json_encode(['color' => null])); + + parent::unmount(); + } + public function viewArtist(int $artistId): void { $this->navigate($this->route('spotify.artist', $artistId)) @@ -42,16 +54,16 @@ public function playTrack(int $index): void { $track = $this->tracks[$index] ?? null; if ($track) { - Dialog::alert('Now Playing', $track['title'] . ' by ' . $track['artistName']); + Dialog::alert('Now Playing', $track['title'].' by '.$track['artistName']); } } public function shufflePlay(): void { - Dialog::alert('Shuffle Play', 'Playing ' . $this->playlist['name'] . ' on shuffle.'); + Dialog::alert('Shuffle Play', 'Playing '.$this->playlist['name'].' on shuffle.'); } - public function render(): \Illuminate\View\View + public function render(): View { $tracksWithMeta = []; foreach ($this->tracks as $track) { diff --git a/app/NativeComponents/SpotifySearch.php b/app/NativeComponents/SpotifySearch.php index 844c6c2..8e13631 100644 --- a/app/NativeComponents/SpotifySearch.php +++ b/app/NativeComponents/SpotifySearch.php @@ -3,6 +3,7 @@ namespace App\NativeComponents; use App\NativeComponents\Concerns\HasSpotifyData; +use Illuminate\View\View; use Native\Mobile\Edge\NativeComponent; use Native\Mobile\Edge\Transition; @@ -10,6 +11,21 @@ class SpotifySearch extends NativeComponent { use HasSpotifyData; + public function mount(): void + { + nativephp_call('UI.SetBackground', json_encode(['color' => '#121212'])); + } + + public function unmount(): void + { + // UI.SetBackground is app-global sticky native state — without + // this clear it leaks the window color into every screen visited + // after this one. Empty color = restore the platform default. + nativephp_call('UI.SetBackground', json_encode(['color' => null])); + + parent::unmount(); + } + public function viewPlaylist(int $index): void { $this->navigate($this->route('spotify.playlist', $index)) @@ -22,7 +38,7 @@ public function viewArtist(int $index): void ->transition(Transition::SlideFromRight); } - public function render(): \Illuminate\View\View + public function render(): View { return view('spotify-search', [ 'genres' => self::spotifyGenres(), diff --git a/app/NativeComponents/StackPositioningDemo.php b/app/NativeComponents/StackPositioningDemo.php new file mode 100644 index 0000000..8cad889 --- /dev/null +++ b/app/NativeComponents/StackPositioningDemo.php @@ -0,0 +1,71 @@ + + */ + public array $show = [11]; + + /** + * The nine anchor/origin points, in wire-enum order. Drives the anchor + * and origin grids so the blade stays declarative and we exercise the + * bound-attribute path (`:anchor="$point"`) as well as the literal one. + * + * @var array + */ + public array $points = [ + ['name' => 'top-left', 'enum' => 1], + ['name' => 'top', 'enum' => 2], + ['name' => 'top-right', 'enum' => 3], + ['name' => 'left', 'enum' => 4], + ['name' => 'center', 'enum' => 0], + ['name' => 'right', 'enum' => 5], + ['name' => 'bottom-left', 'enum' => 6], + ['name' => 'bottom', 'enum' => 7], + ['name' => 'bottom-right', 'enum' => 8], + ]; + + public function navTitle(): string + { + return 'Stack & Positioning'; + } + + public function toggleBounds(): void + { + $this->showBounds = ! $this->showBounds; + } + + /** Rows of three, so the 9-point grids lay out as a 3x3. */ + public function pointRows(): array + { + return array_chunk($this->points, 3); + } + + public function render(): View + { + return view('native.stack-positioning-demo'); + } +} diff --git a/app/NativeComponents/SyncUpChat.php b/app/NativeComponents/SyncUpChat.php deleted file mode 100644 index 1e8bc85..0000000 --- a/app/NativeComponents/SyncUpChat.php +++ /dev/null @@ -1,135 +0,0 @@ - */ - public array $messages = []; - - public string $draft = ''; - - /** Tracks whether we've seeded $messages from the conversation id yet. */ - public bool $seeded = false; - - /** Modal visibility for the more-actions overlay. */ - public bool $showMoreActions = false; - - /** Confirmation modal for clear-history (dismissible=false). */ - public bool $showClearConfirm = false; - - public bool $isMuted = false; - - public function mount(): void - { - $id = (int) $this->param('id', 1); - $this->messages = self::suMessages($id); - $this->seeded = true; - } - - public function navTitle(): string - { - return $this->friend()['name']; - } - - public function navigationOptions(): ?NavBarOptions - { - return NavBarOptions::make() - ->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 - { - $this->draft = $value; - } - - public function send(): void - { - $text = trim($this->draft); - if ($text === '') { - return; - } - - $this->messages[] = [ - 'id' => count($this->messages) + 1, - 'fromMe' => true, - 'text' => $text, - 'time' => date('g:i A'), - 'status' => 'sent', - ]; - $this->draft = ''; - } - - public function attachFile(): void { /* stub */ } - public function attachPhoto(): void { /* stub */ } - public function openEmoji(): void { /* stub */ } - public function openMore(): void { /* stub */ } - public function startCall(): void { /* stub */ } - public function startVideo(): void { - Dialog::toast('Starting Video'); - } - - /** Tapped from the NavBar ellipsis — opens the more-actions modal. */ - public function openMenu(): void - { - $this->showMoreActions = true; - } - - public function closeMoreActions(): void - { - $this->showMoreActions = false; - } - - public function toggleMute(): void - { - $this->isMuted = ! $this->isMuted; - $this->showMoreActions = false; - } - - /** Bridges from more-actions modal → blocking confirm modal. */ - public function askClearHistory(): void - { - $this->showMoreActions = false; - $this->showClearConfirm = true; - } - - public function confirmClearHistory(): void - { - $this->messages = []; - $this->showClearConfirm = false; - } - - public function cancelClearHistory(): void - { - $this->showClearConfirm = false; - } - - /** Resolve the friend (other party) for the conversation. */ - private function friend(): array - { - $id = (int) $this->param('id', 1); - $conv = self::suConversation($id); - if ($conv && ! $conv['isGroup']) { - return self::suUser($conv['userId']); - } - // Fallback for the demo — Alex Rivera matches the mockup. - return self::suUser(5); - } - - public function render(): \Illuminate\View\View - { - return view('syncup.chat', [ - 'friend' => $this->friend(), - ]); - } -} diff --git a/app/NativeComponents/SyncUpChats.php b/app/NativeComponents/SyncUpChats.php deleted file mode 100644 index 6ff3cfe..0000000 --- a/app/NativeComponents/SyncUpChats.php +++ /dev/null @@ -1,119 +0,0 @@ -action(NavAction::make('more')->icon('ellipsis')->a11yLabel('More options')->press('openMenu')); - } - - public function setFilter(string $name): void - { - $this->activeFilter = $name; - } - - public function updateSearch(string $value): void - { - $this->search = $value; - } - - public function openChat(int $id): void - { - $this->navigate("/syncup/chat/{$id}") - ->transition(Transition::SlideFromRight); - } - - public function newMessage(): void - { - $this->showNewMessage = true; - } - - public function closeNewMessage(): void - { - $this->showNewMessage = false; - } - - public function startChatWith(int $userId): void - { - $this->showNewMessage = false; - $this->navigate("/syncup/chat/{$userId}") - ->transition(Transition::SlideFromRight); - } - - public function addSuggestion(int $id): void - { - // Demo stub. - } - - public function openSearch(): void - { - // Demo stub. - } - - public function openMenu(): void - { - // Demo stub. - } - - public function render(): \Illuminate\View\View - { - $users = self::suUsers(); - $conversations = self::suConversations(); - - // Decorate each conversation with the related user (or a synthetic - // 'group' avatar for is-group rows). - $rows = []; - foreach ($conversations as $c) { - if ($c['isGroup']) { - $rows[] = $c + [ - 'displayName' => $c['name'], - 'avatarUrl' => null, - ]; - } else { - $u = self::suUser($c['userId']); - $rows[] = $c + [ - 'displayName' => $u['name'], - 'avatarUrl' => $u['avatarUrl'], - 'status' => $u['status'], - ]; - } - } - - // Friends for the "new message" bottom-sheet (skip "You" at id 0). - $friends = array_values(array_filter($users, fn ($u) => $u['id'] !== 0)); - - return view('native.syncup.chats', [ - 'rows' => $rows, - 'filters' => self::suFilters(), - 'suggestions' => self::suSuggestions(), - 'friends' => $friends, - ]); - } -} diff --git a/app/NativeComponents/SyncUpFriends.php b/app/NativeComponents/SyncUpFriends.php deleted file mode 100644 index d414111..0000000 --- a/app/NativeComponents/SyncUpFriends.php +++ /dev/null @@ -1,54 +0,0 @@ -navigate("/syncup/chat/{$id}") - ->transition(Transition::SlideFromRight); - } - - public function addSuggestion(int $id): void - { - // Stub. - } - - public function render(): \Illuminate\View\View - { - $users = self::suUsers(); - - // Take 3 friends for the list (skip "You" at id=0); the remaining are - // suggestions (overlap with suSuggestions for the demo). The mockup - // shows 3 visible friends and 2+ suggestions. - $friends = array_slice(array_filter($users, fn ($u) => $u['id'] !== 0), 0, 3); - $onlineCount = count(array_filter($users, fn ($u) => $u['status'] === 'online')); - - return view('syncup.friends', [ - 'friends' => array_values($friends), - 'onlineCount' => $onlineCount, - 'suggestions' => self::suSuggestions(), - ]); - } -} diff --git a/app/NativeComponents/SyncUpLogin.php b/app/NativeComponents/SyncUpLogin.php deleted file mode 100644 index 05814bf..0000000 --- a/app/NativeComponents/SyncUpLogin.php +++ /dev/null @@ -1,66 +0,0 @@ -email = $value; - } - - public function updatePassword(string $value): void - { - $this->password = $value; - } - - public function toggleVisibility(): void - { - $this->showPassword = ! $this->showPassword; - } - - public function login(): void - { - $this->replace('/syncup')->transition(Transition::Fade); - } - - /** Skip the form entirely — convenient for demo browsing. */ - public function skipLogin(): void - { - $this->login(); - } - - public function forgotPassword(): void - { - // Demo stub — would push a password reset flow. - } - - public function createAccount(): void - { - // Demo stub — would push the registration flow. - } - - public function loginWithGoogle(): void - { - $this->login(); - } - - public function loginWithApple(): void - { - $this->login(); - } - - public function render(): \Illuminate\View\View - { - return view('syncup.login'); - } -} diff --git a/app/NativeComponents/SyncUpProfile.php b/app/NativeComponents/SyncUpProfile.php deleted file mode 100644 index b865f7c..0000000 --- a/app/NativeComponents/SyncUpProfile.php +++ /dev/null @@ -1,53 +0,0 @@ -name = $value; - $this->saved = false; - } - - public function updateBio(string $value): void - { - $this->bio = $value; - $this->saved = false; - } - - public function editPhoto(): void { /* stub */ } - public function copyLink(): void { /* stub */ } - public function shareLink(): void { /* stub */ } - - public function saveProfile(): void - { - $this->saved = true; - } - - public function signOut(): void - { - $this->navigate('/syncup/login'); - } - - public function render(): \Illuminate\View\View - { - return view('syncup.profile'); - } -} diff --git a/app/NativeComponents/ThemeLab.php b/app/NativeComponents/ThemeLab.php new file mode 100644 index 0000000..104434c --- /dev/null +++ b/app/NativeComponents/ThemeLab.php @@ -0,0 +1,98 @@ +presses++; + } + + /** + * Token pairs rendered as swatches: bg token → its on-* companion. + * + * @return array + */ + public function swatchTokens(): array + { + return [ + 'primary' => 'Primary — brand violet', + 'secondary' => 'Secondary — teal / fuchsia', + 'surface' => 'Surface — cards & sheets', + 'surface-variant' => 'Surface Variant — tonal fills', + 'destructive' => 'Destructive — errors', + 'success' => 'Success — custom token', + 'accent' => 'Accent — highlights', + ]; + } + + /** + * Button variants including the success case. + * + * @return array + */ + public function buttonVariants(): array + { + return ['primary', 'secondary', 'success', 'destructive', 'ghost']; + } + + /** + * Badge variants including the success case. + * + * @return array + */ + public function badgeVariants(): array + { + return ['destructive', 'primary', 'success', 'accent']; + } + + /** + * Opacity steps for the theme-class tonal-fill ramp. + * + * @return array + */ + public function opacitySteps(): array + { + return [10, 25, 50, 75, 100]; + } + + /** + * Font aliases from config/native-ui.php with sample copy. + * + * @return array + */ + public function fontSamples(): array + { + return [ + 'display' => 'SUPERNATIVE', + 'grotesk' => 'Space Grotesk — clean UI copy with personality.', + 'grotesk-bold' => 'Space Grotesk Bold — headlines that mean it.', + 'accent' => 'Lobster — when the moment calls for flair.', + 'default' => 'Geist Pixel — the app-wide default face.', + ]; + } + + public function render(): View + { + return view('native.theme-lab'); + } +} diff --git a/app/NativeComponents/TweetDetail.php b/app/NativeComponents/TweetDetail.php index 22ef88a..001ea11 100644 --- a/app/NativeComponents/TweetDetail.php +++ b/app/NativeComponents/TweetDetail.php @@ -3,6 +3,7 @@ namespace App\NativeComponents; use App\NativeComponents\Concerns\HasTweetData; +use Illuminate\View\View; use Native\Mobile\Edge\NativeComponent; use Native\Mobile\Edge\Transition; @@ -20,6 +21,8 @@ class TweetDetail extends NativeComponent public bool $isRetweeted = false; + public bool $isBookmarked = false; + public function mount(): void { $id = (int) $this->param('id'); @@ -54,15 +57,23 @@ public function toggleRetweet(): void $this->isRetweeted = ! $this->isRetweeted; } - public function render(): \Illuminate\View\View + public function toggleBookmark(): void + { + $this->isBookmarked = ! $this->isBookmarked; + } + + public function render(): View { $likeCount = $this->tweet['likeCount'] + ($this->isLiked ? 1 : 0); $retweetCount = $this->tweet['retweetCount'] + ($this->isRetweeted ? 1 : 0); + $bookmarkCount = intdiv($this->tweet['likeCount'], 9) + ($this->isBookmarked ? 1 : 0); return view('tweet-detail', [ 'likeFormatted' => self::formatCount($likeCount), 'retweetFormatted' => self::formatCount($retweetCount), 'replyFormatted' => self::formatCount($this->tweet['replyCount']), + 'bookmarkFormatted' => self::formatCount($bookmarkCount), + 'viewFormatted' => self::formatCount($this->tweet['likeCount'] * 41 + $this->tweet['replyCount'] * 173), ]); } } diff --git a/app/NativeComponents/TwitterFeed.php b/app/NativeComponents/TwitterFeed.php index a065332..21cb1aa 100644 --- a/app/NativeComponents/TwitterFeed.php +++ b/app/NativeComponents/TwitterFeed.php @@ -3,6 +3,7 @@ namespace App\NativeComponents; use App\NativeComponents\Concerns\HasTweetData; +use Illuminate\View\View; use Native\Mobile\Edge\NativeComponent; use Native\Mobile\Edge\Transition; @@ -10,24 +11,34 @@ class TwitterFeed extends NativeComponent { use HasTweetData; + /** User ids shown on the Following tab. */ + private const FOLLOWING_USER_IDS = [0, 1, 2, 7]; + public int $selectedTab = 0; + public int $refreshCount = 0; + + /** @var array Keyed by tweet id, stable across tab filters and refresh rotation. */ + public array $likedTweets = []; + public function navTitle(): string { return 'Twitter / X'; } - /** @var array */ - public array $likedTweets = []; - public function selectTab(int $index): void { $this->selectedTab = $index; } - public function viewTweet(int $index): void + public function refresh(): void { - $this->navigate($this->route('twitter.tweet', $index)) + $this->refreshCount++; + } + + public function viewTweet(int $id): void + { + $this->navigate($this->route('twitter.tweet', $id)) ->transition(Transition::SlideFromRight); } @@ -43,28 +54,43 @@ public function composeTweet(): void ->transition(Transition::SlideFromBottom); } - public function toggleLike(int $index): void + public function toggleLike(int $id): void { - if (isset($this->likedTweets[$index])) { - unset($this->likedTweets[$index]); + if (isset($this->likedTweets[$id])) { + unset($this->likedTweets[$id]); } else { - $this->likedTweets[$index] = true; + $this->likedTweets[$id] = true; } } - public function render(): \Illuminate\View\View + public function render(): View { $users = self::tweetUsers(); $tweets = self::tweets(); - foreach ($tweets as $i => &$tweet) { + foreach ($tweets as $id => &$tweet) { + $tweet['id'] = $id; $tweet['user'] = $users[$tweet['userId']]; $tweet['replyFormatted'] = self::formatCount($tweet['replyCount']); $tweet['retweetFormatted'] = self::formatCount($tweet['retweetCount']); + $tweet['viewFormatted'] = self::formatCount($tweet['likeCount'] * 41 + $tweet['replyCount'] * 173); $tweet['likeFormatted'] = self::formatCount( - $tweet['likeCount'] + (isset($this->likedTweets[$i]) ? 1 : 0) + $tweet['likeCount'] + (isset($this->likedTweets[$id]) ? 1 : 0) ); - $tweet['isLiked'] = isset($this->likedTweets[$i]); + $tweet['isLiked'] = isset($this->likedTweets[$id]); + } + unset($tweet); + + if ($this->selectedTab === 1) { + $tweets = array_values(array_filter( + $tweets, + fn (array $tweet): bool => in_array($tweet['userId'], self::FOLLOWING_USER_IDS, true) + )); + } + + if ($this->refreshCount > 0 && count($tweets) > 1) { + $offset = $this->refreshCount % count($tweets); + $tweets = array_merge(array_slice($tweets, $offset), array_slice($tweets, 0, $offset)); } return view('twitter-feed', [ diff --git a/app/NativeComponents/TwitterProfile.php b/app/NativeComponents/TwitterProfile.php index 6ffb5ad..0c5fdbe 100644 --- a/app/NativeComponents/TwitterProfile.php +++ b/app/NativeComponents/TwitterProfile.php @@ -3,6 +3,7 @@ namespace App\NativeComponents; use App\NativeComponents\Concerns\HasTweetData; +use Illuminate\View\View; use Native\Mobile\Edge\NativeComponent; use Native\Mobile\Edge\Transition; @@ -18,6 +19,8 @@ class TwitterProfile extends NativeComponent public int $selectedTab = 0; + public bool $isFollowing = false; + public function mount(): void { $id = (int) $this->param('id'); @@ -35,13 +38,18 @@ public function selectTab(int $index): void $this->selectedTab = $index; } + public function toggleFollow(): void + { + $this->isFollowing = ! $this->isFollowing; + } + public function viewTweet(int $tweetId): void { $this->navigate($this->route('twitter.tweet', $tweetId)) ->transition(Transition::SlideFromRight); } - public function render(): \Illuminate\View\View + public function render(): View { $users = self::tweetUsers(); $allTweets = self::tweets(); @@ -59,7 +67,7 @@ public function render(): \Illuminate\View\View return view('twitter-profile', [ 'tweetsWithMeta' => $tweetsWithMeta, - 'followersFormatted' => self::formatCount($this->user['followers']), + 'followersFormatted' => self::formatCount($this->user['followers'] + ($this->isFollowing ? 1 : 0)), 'followingFormatted' => self::formatCount($this->user['following']), ]); } diff --git a/app/NativeComponents/VibeDemo.php b/app/NativeComponents/VibeDemo.php deleted file mode 100644 index f9dea09..0000000 --- a/app/NativeComponents/VibeDemo.php +++ /dev/null @@ -1,72 +0,0 @@ -on() closure and the #[OnEcho] attribute method — - * so this screen exercises the two listener paths side by side. PHP never - * broadcasts — it is purely a client subscriber. - * - * Also wires the connection lifecycle: a live/reconnecting badge - * (onDisconnect/onReconnect) and the last native error (onError — e.g. a - * failed subscription), which would otherwise be invisible on-device. - */ -class VibeDemo extends NativeComponent -{ - public string $channel = 'test-channel'; - - public string $lastMessage = '(waiting for a broadcast...)'; - - public int $count = 0; - - /** Same event, delivered via the #[OnEcho] attribute path. */ - public int $viaAttribute = 0; - - public bool $live = true; - - public ?string $lastError = null; - - public function navTitle(): string - { - return 'Vibe — Live Events'; - } - - public function mount(): void - { - Vibe::channel($this->channel) - ->on('TestEvent', function ($event) { - $this->lastMessage = $event->message; - $this->count++; - }) - ->onDisconnect(fn () => $this->live = false) - ->onReconnect(function () { - // Websockets don't replay missed messages — refetch - // authoritative state here in a real app. - $this->live = true; - $this->lastError = null; // transient errors are moot once we're back - }) - ->onError(fn ($event) => $this->lastError = "{$event->type}: {$event->message}"); - } - - /** The attribute path: channel-scoped, auto-torn-down on unmount. */ - #[OnEcho('TestEvent', channel: 'test-channel')] - public function onTestEvent(string $message): void - { - $this->viaAttribute++; - } - - public function render(): View - { - return view('native.vibe-demo'); - } -} diff --git a/app/NativeComponents/VibeLogin.php b/app/NativeComponents/VibeLogin.php deleted file mode 100644 index ad53c2e..0000000 --- a/app/NativeComponents/VibeLogin.php +++ /dev/null @@ -1,104 +0,0 @@ -loggedInAs = SecureStorage::get('vibe_user_name') ?: null; - } - - public function setEmail(string $value): void - { - $this->email = trim($value); - } - - public function setPassword(string $value): void - { - $this->password = $value; - } - - public function login(): void - { - if ($this->loading) { - return; - } - - // The HTTP call blocks the handler and the loop only re-renders after it - // returns — so flip to the loading state and publish this frame NOW, - // before the blocking call, so the spinner actually shows. - $this->loading = true; - $this->status = 'Logging in…'; - $this->publishFinalState(); - - // Reuse the single configured tunnel: swap the broadcasting-auth path - // for the login path so there's one URL to configure. - $loginUrl = str_replace('/broadcasting/auth', '/auth/login', (string) config('vibe.auth.endpoint')); - - try { - $response = Http::acceptJson() - ->withHeaders(['ngrok-skip-browser-warning' => 'true']) - ->post($loginUrl, ['email' => $this->email, 'password' => $this->password]); - - if ($response->successful()) { - SecureStorage::set('vibe_token', (string) $response->json('data.token')); - SecureStorage::set('vibe_user_name', (string) $response->json('data.user.name')); - - // Straight into the demo. replace() (not navigate()) so Back from - // the presence screen returns to the launcher, skipping login. - $this->replace('/vibe-presence'); - - return; - } else { - $this->status = 'Login failed ('.$response->status().'). Check your credentials.'; - } - } catch (\Throwable $e) { - $this->status = 'Error: '.$e->getMessage(); - } - - // Only reached on failure (success replace()s away above). - $this->loading = false; - } - - public function logout(): void - { - SecureStorage::delete('vibe_token'); - SecureStorage::delete('vibe_user_name'); - $this->loggedInAs = null; - $this->status = 'Logged out.'; - } - - public function render(): View - { - return view('native.vibe-login'); - } -} diff --git a/app/NativeComponents/VibePresenceDemo.php b/app/NativeComponents/VibePresenceDemo.php deleted file mode 100644 index 2f75e4a..0000000 --- a/app/NativeComponents/VibePresenceDemo.php +++ /dev/null @@ -1,111 +0,0 @@ - online members: user id => display name */ - public array $members = []; - - /** @var array ephemeral chat log ("Name: body") */ - public array $messages = []; - - public string $draft = ''; - - /** Ephemeral "X is typing" state — driven by whispers, auto-cleared by a poll. */ - public ?string $typistName = null; - - public int $typingUntil = 0; - - protected ?PendingSubscription $sub = null; - - public function navTitle(): string - { - return 'Vibe — Presence'; - } - - public function mount(): void - { - if (! $this->requireVibeAuth()) { - return; - } - - $this->sub = Vibe::presence($this->channel) - ->here(function (array $members) { - $this->members = []; - foreach ($members as $m) { - $this->members[(string) ($m['id'] ?? '')] = $this->name($m); - } - }) - ->joining(function (array $member) { - Dialog::toast($this->name($member).' has joined'); - $this->members[(string) ($member['id'] ?? '')] = $this->name($member); - }) - ->leaving(function (array $member) { - Dialog::toast($this->name($member).' has left'); - unset($this->members[(string) ($member['id'] ?? '')]); - }) - ->listenForWhisper('chat', function ($event) { - $this->messages[] = ['from' => $event->from ?? '?', 'body' => $event->body ?? '', 'mine' => false]; - $this->typingUntil = 0; // they sent → no longer typing - }) - ->listenForWhisper('typing', function ($event) { - $this->typistName = $event->from ?? 'Someone'; - $this->typingUntil = time() + 3; - }); - } - - /** Fires on each keystroke — whisper a transient "typing" signal to peers. */ - public function onType(string $text): void - { - $this->draft = $text; - $this->sub?->whisper('typing', ['from' => $this->vibeIdentity]); - } - - public function send(): void - { - $body = trim($this->draft); - if ($body === '' || $this->sub === null) { - return; - } - - // Whisper to the others; the sender never receives its own, so echo it. - $this->sub->whisper('chat', ['from' => $this->vibeIdentity, 'body' => $body]); - $this->messages[] = ['from' => $this->vibeIdentity ?? 'me', 'body' => $body, 'mine' => true]; - $this->draft = ''; - } - - /** Prefer the member's user_info name, fall back to their id. */ - protected function name(array $member): string - { - return $member['info']['name'] ?? ('user '.($member['id'] ?? '?')); - } - - public function render(): View - { - return view('native.vibe-presence-demo'); - } -} diff --git a/app/NativeComponents/VibePrivateDemo.php b/app/NativeComponents/VibePrivateDemo.php deleted file mode 100644 index bc09b45..0000000 --- a/app/NativeComponents/VibePrivateDemo.php +++ /dev/null @@ -1,56 +0,0 @@ -requireVibeAuth()) { - return; - } - - Vibe::private($this->channel) - ->on('OrderShipped', function ($event) { - $this->lastMessage = $event->message; - $this->count++; - }) - // Auth rejections (bad token, dead VIBE_AUTH_ENDPOINT tunnel) used - // to fail silently; now they land here as vibe:error. - ->onError(fn ($event) => $this->lastError = "{$event->type}: {$event->message}"); - } - - public function render(): View - { - return view('native.vibe-demo'); - } -} diff --git a/app/NativeComponents/YouTubeChannel.php b/app/NativeComponents/YouTubeChannel.php index 0992b62..4d45a6b 100644 --- a/app/NativeComponents/YouTubeChannel.php +++ b/app/NativeComponents/YouTubeChannel.php @@ -3,6 +3,7 @@ namespace App\NativeComponents; use App\NativeComponents\Concerns\HasYouTubeData; +use Illuminate\View\View; use Native\Mobile\Edge\NativeComponent; use Native\Mobile\Edge\Transition; @@ -10,7 +11,6 @@ class YouTubeChannel extends NativeComponent { use HasYouTubeData; - /** @var array */ public array $channel = []; /** @var array */ @@ -34,6 +34,16 @@ public function mount(): void ); } + public function unmount(): void + { + // UI.SetBackground is app-global sticky native state — without + // this clear it leaks the window color into every screen visited + // after this one. Empty color = restore the platform default. + nativephp_call('UI.SetBackground', json_encode(['color' => null])); + + parent::unmount(); + } + public function toggleSubscribe(): void { $this->isSubscribed = ! $this->isSubscribed; @@ -49,7 +59,7 @@ public function viewVideo(int $index): void } } - public function render(): \Illuminate\View\View + public function render(): View { $videosWithMeta = []; foreach ($this->channelVideos as $video) { @@ -62,4 +72,4 @@ public function render(): \Illuminate\View\View 'subscribersFormatted' => self::formatYtCount($this->channel['subscribers']), ]); } -} \ No newline at end of file +} diff --git a/app/NativeComponents/YouTubeHome.php b/app/NativeComponents/YouTubeHome.php index a1f9293..36733cc 100644 --- a/app/NativeComponents/YouTubeHome.php +++ b/app/NativeComponents/YouTubeHome.php @@ -3,8 +3,7 @@ namespace App\NativeComponents; use App\NativeComponents\Concerns\HasYouTubeData; -use Native\Mobile\Edge\Layouts\Builders\NavAction; -use Native\Mobile\Edge\Layouts\Builders\NavBarOptions; +use Illuminate\View\View; use Native\Mobile\Edge\NativeComponent; use Native\Mobile\Edge\Transition; @@ -14,22 +13,19 @@ class YouTubeHome extends NativeComponent public string $activeCategory = 'All'; - public function navTitle(): string + public function mount(): void { - return 'YouTube'; + nativephp_call('UI.SetBackground', json_encode(['color' => '#0F0F0F'])); } - /** - * Surface YouTube's right-side actions in the framework NavBar instead - * of a duplicate inline top row. StackLayout reads this and merges into - * its NavBar. - */ - public function navigationOptions(): ?NavBarOptions + public function unmount(): void { - return NavBarOptions::make() - ->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')); + // UI.SetBackground is app-global sticky native state — without + // this clear it leaks the window color into every screen visited + // after this one. Empty color = restore the platform default. + nativephp_call('UI.SetBackground', json_encode(['color' => null])); + + parent::unmount(); } public function castDevice(): void @@ -65,7 +61,7 @@ public function selectCategory(string $category): void $this->activeCategory = $category; } - public function render(): \Illuminate\View\View + public function render(): View { $videos = self::ytVideos(); $channels = self::ytChannels(); diff --git a/app/NativeComponents/YouTubeSearch.php b/app/NativeComponents/YouTubeSearch.php index 3aa3846..a8e8047 100644 --- a/app/NativeComponents/YouTubeSearch.php +++ b/app/NativeComponents/YouTubeSearch.php @@ -3,6 +3,7 @@ namespace App\NativeComponents; use App\NativeComponents\Concerns\HasYouTubeData; +use Illuminate\View\View; use Native\Mobile\Edge\NativeComponent; use Native\Mobile\Edge\Transition; @@ -20,6 +21,16 @@ public function mount(): void nativephp_call('UI.SetBackground', json_encode(['color' => '#0F0F0F'])); } + public function unmount(): void + { + // UI.SetBackground is app-global sticky native state — without + // this clear it leaks the window color into every screen visited + // after this one. Empty color = restore the platform default. + nativephp_call('UI.SetBackground', json_encode(['color' => null])); + + parent::unmount(); + } + public function search(): void { if (trim($this->query) === '') { @@ -56,7 +67,7 @@ public function viewChannel(int $channelId): void ->transition(Transition::SlideFromRight); } - public function render(): \Illuminate\View\View + public function render(): View { $videos = self::ytVideos(); $channels = self::ytChannels(); @@ -77,4 +88,4 @@ public function render(): \Illuminate\View\View 'channels' => $channels, ]); } -} \ No newline at end of file +} diff --git a/app/NativeComponents/YouTubeVideo.php b/app/NativeComponents/YouTubeVideo.php index b147b31..18c8197 100644 --- a/app/NativeComponents/YouTubeVideo.php +++ b/app/NativeComponents/YouTubeVideo.php @@ -3,6 +3,7 @@ namespace App\NativeComponents; use App\NativeComponents\Concerns\HasYouTubeData; +use Illuminate\View\View; use Native\Mobile\Edge\NativeComponent; use Native\Mobile\Edge\Transition; @@ -10,10 +11,8 @@ class YouTubeVideo extends NativeComponent { use HasYouTubeData; - /** @var array */ public array $video = []; - /** @var array */ public array $channel = []; /** @var array */ @@ -27,8 +26,12 @@ class YouTubeVideo extends NativeComponent public bool $showDescription = false; + public bool $showComments = false; + public function mount(): void { + nativephp_call('UI.SetBackground', json_encode(['color' => '#0F0F0F'])); + $id = (int) $this->param('id'); $videos = self::ytVideos(); $channels = self::ytChannels(); @@ -39,6 +42,16 @@ public function mount(): void $this->comments = $allComments[$id] ?? $allComments[0] ?? []; } + public function unmount(): void + { + // UI.SetBackground is app-global sticky native state — without + // this clear it leaks the window color into every screen visited + // after this one. Empty color = restore the platform default. + nativephp_call('UI.SetBackground', json_encode(['color' => null])); + + parent::unmount(); + } + public function toggleLike(): void { $this->isLiked = ! $this->isLiked; @@ -65,6 +78,11 @@ public function toggleDescription(): void $this->showDescription = ! $this->showDescription; } + public function toggleComments(): void + { + $this->showComments = ! $this->showComments; + } + public function viewChannel(int $id): void { $this->navigate($this->route('youtube.channel', $id)) @@ -76,7 +94,7 @@ public function viewVideo(int $index): void $this->replace($this->route('youtube.video', $index)); } - public function render(): \Illuminate\View\View + public function render(): View { $videos = self::ytVideos(); $channels = self::ytChannels(); diff --git a/app/Providers/NativeServiceProvider.php b/app/Providers/NativeServiceProvider.php index f105639..54b6e16 100644 --- a/app/Providers/NativeServiceProvider.php +++ b/app/Providers/NativeServiceProvider.php @@ -3,7 +3,9 @@ namespace App\Providers; use Illuminate\Support\ServiceProvider; +use Native\Mobile\Providers\CameraServiceProvider; use Native\Mobile\UI\NativeUIServiceProvider; +use S2BR\MobileSplashscreen\MobileSplashscreenServiceProvider; class NativeServiceProvider extends ServiceProvider { @@ -36,6 +38,9 @@ public function plugins(): array { return [ NativeUIServiceProvider::class, + MobileSplashscreenServiceProvider::class, + CameraServiceProvider::class, + ]; } } diff --git a/bootstrap/app.php b/bootstrap/app.php index c183276..0e1cfcc 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -6,7 +6,7 @@ return Application::configure(basePath: dirname(__DIR__)) ->withRouting( - web: __DIR__.'/../routes/web.php', + web: __DIR__.'/../routes/mobile.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) diff --git a/composer.json b/composer.json index afc7876..1894c5c 100644 --- a/composer.json +++ b/composer.json @@ -7,29 +7,13 @@ "laravel", "framework" ], - "repositories": { - - "mobile": { - "type": "vcs", - "url": "https://github.com/nativephp/mobile-air" - }, - "ui": { - "type": "vcs", - "url": "https://github.com/nativephp/native-ui" - }, - "plugins": { - "type": "composer", - "url": "https://plugins.nativephp.com" - } - }, "license": "MIT", "require": { "php": "^8.4", "laravel/framework": "^13.0", "laravel/tinker": "^3.0", - "nativephp/mobile": "dev-main as 4.0.x-dev", - "nativephp/mobile-camera": "*", - "nativephp/mobile-ui": "^0.2" + "nativephp/mobile": "dev-main as 4.9.9", + "nativephp/mobile-ui": "^0.3.0" }, "require-dev": { "fakerphp/faker": "^1.23", @@ -39,8 +23,8 @@ "laravel/sail": "^1.41", "mockery/mockery": "^1.6", "nunomaduro/collision": "^8.6", - "pestphp/pest": "^4.4", - "pestphp/pest-plugin-laravel": "^4.1" + "pestphp/pest": "^5.0", + "pestphp/pest-plugin-laravel": "^5.0" }, "autoload": { "psr-4": { @@ -112,6 +96,12 @@ "php-http/discovery": true } }, + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/NativePHP/mobile-air" + } + ], "minimum-stability": "dev", "prefer-stable": true } diff --git a/composer.lock b/composer.lock index a437660..52281e8 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": "1aacf0d8e0cc98f0588e238fe50a9080", + "content-hash": "1928a8c84cbfa8cad20150dfab7fe730", "packages": [ { "name": "bacon/bacon-qr-code", @@ -866,22 +866,22 @@ }, { "name": "guzzlehttp/guzzle", - "version": "7.14.0", + "version": "7.15.2", "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "aef242412e13128b5049864867bb49fc37dd39de" + "reference": "744101956d78b7c1384d0cbf379db13e859167bf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/aef242412e13128b5049864867bb49fc37dd39de", - "reference": "aef242412e13128b5049864867bb49fc37dd39de", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/744101956d78b7c1384d0cbf379db13e859167bf", + "reference": "744101956d78b7c1384d0cbf379db13e859167bf", "shasum": "" }, "require": { "ext-json": "*", "guzzlehttp/promises": "^2.5.1", - "guzzlehttp/psr7": "^2.12.4", + "guzzlehttp/psr7": "^2.13", "php": "^7.2.5 || ^8.0", "psr/http-client": "^1.0", "symfony/deprecation-contracts": "^2.5 || ^3.0", @@ -894,7 +894,7 @@ "bamarni/composer-bin-plugin": "^1.8.2", "ext-curl": "*", "guzzle/client-integration-tests": "3.0.3", - "guzzlehttp/test-server": "^0.6", + "guzzlehttp/test-server": "^0.7", "php-http/message-factory": "^1.1", "phpunit/phpunit": "^8.5.52 || ^9.6.34", "psr/log": "^1.1 || ^2.0 || ^3.0" @@ -974,7 +974,7 @@ ], "support": { "issues": "https://github.com/guzzle/guzzle/issues", - "source": "https://github.com/guzzle/guzzle/tree/7.14.0" + "source": "https://github.com/guzzle/guzzle/tree/7.15.2" }, "funding": [ { @@ -990,7 +990,7 @@ "type": "tidelift" } ], - "time": "2026-07-08T22:54:09+00:00" + "time": "2026-07-26T23:23:20+00:00" }, { "name": "guzzlehttp/promises", @@ -1078,16 +1078,16 @@ }, { "name": "guzzlehttp/psr7", - "version": "2.12.4", + "version": "2.13.0", "source": { "type": "git", "url": "https://github.com/guzzle/psr7.git", - "reference": "51e27f9e2b332ab3e72f4520d5ff4f3c68c3577c" + "reference": "dad89620b7a6edb60c15858442eb2e408b45d8f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/psr7/zipball/51e27f9e2b332ab3e72f4520d5ff4f3c68c3577c", - "reference": "51e27f9e2b332ab3e72f4520d5ff4f3c68c3577c", + "url": "https://api.github.com/repos/guzzle/psr7/zipball/dad89620b7a6edb60c15858442eb2e408b45d8f4", + "reference": "dad89620b7a6edb60c15858442eb2e408b45d8f4", "shasum": "" }, "require": { @@ -1177,7 +1177,7 @@ ], "support": { "issues": "https://github.com/guzzle/psr7/issues", - "source": "https://github.com/guzzle/psr7/tree/2.12.4" + "source": "https://github.com/guzzle/psr7/tree/2.13.0" }, "funding": [ { @@ -1193,20 +1193,20 @@ "type": "tidelift" } ], - "time": "2026-07-08T15:56:20+00:00" + "time": "2026-07-16T22:23:49+00:00" }, { "name": "guzzlehttp/uri-template", - "version": "v1.0.9", + "version": "v1.0.10", "source": { "type": "git", "url": "https://github.com/guzzle/uri-template.git", - "reference": "d7580af6d3f8384325d9cd3e99b21c3ed1848176" + "reference": "f6c24c21f42b990e9a58912b332d0874df6ba839" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/uri-template/zipball/d7580af6d3f8384325d9cd3e99b21c3ed1848176", - "reference": "d7580af6d3f8384325d9cd3e99b21c3ed1848176", + "url": "https://api.github.com/repos/guzzle/uri-template/zipball/f6c24c21f42b990e9a58912b332d0874df6ba839", + "reference": "f6c24c21f42b990e9a58912b332d0874df6ba839", "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.9" + "source": "https://github.com/guzzle/uri-template/tree/v1.0.10" }, "funding": [ { @@ -1279,20 +1279,20 @@ "type": "tidelift" } ], - "time": "2026-07-08T16:19:22+00:00" + "time": "2026-07-17T13:53:03+00:00" }, { "name": "laravel/framework", - "version": "v13.19.0", + "version": "v13.23.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "514502b38e11bd676ecf83b271c9452cc7500f16" + "reference": "92a707229148e57f08a249211c8a5a194159c619" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/514502b38e11bd676ecf83b271c9452cc7500f16", - "reference": "514502b38e11bd676ecf83b271c9452cc7500f16", + "url": "https://api.github.com/repos/laravel/framework/zipball/92a707229148e57f08a249211c8a5a194159c619", + "reference": "92a707229148e57f08a249211c8a5a194159c619", "shasum": "" }, "require": { @@ -1318,7 +1318,7 @@ "league/flysystem": "^3.25.1", "league/flysystem-local": "^3.25.1", "league/uri": "^7.5.1", - "monolog/monolog": "^3.0", + "monolog/monolog": "^3.10", "nesbot/carbon": "^3.8.4", "nunomaduro/termwind": "^2.0", "php": "^8.3", @@ -1371,6 +1371,7 @@ "illuminate/filesystem": "self.version", "illuminate/hashing": "self.version", "illuminate/http": "self.version", + "illuminate/image": "self.version", "illuminate/json-schema": "self.version", "illuminate/log": "self.version", "illuminate/macroable": "self.version", @@ -1397,6 +1398,7 @@ "ext-gmp": "*", "fakerphp/faker": "^1.24", "guzzlehttp/psr7": "^2.9", + "intervention/image": "^4.0", "laravel/pint": "^1.18", "league/flysystem-aws-s3-v3": "^3.25.1", "league/flysystem-ftp": "^3.25.1", @@ -1433,6 +1435,7 @@ "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).", + "intervention/image": "Required to use the image processing features (^4.0).", "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).", @@ -1503,7 +1506,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-07-07T14:13:33+00:00" + "time": "2026-07-27T14:48:58+00:00" }, { "name": "laravel/prompts", @@ -1566,16 +1569,16 @@ }, { "name": "laravel/serializable-closure", - "version": "v2.0.13", + "version": "v2.0.15", "source": { "type": "git", "url": "https://github.com/laravel/serializable-closure.git", - "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce" + "reference": "dccd8bcb851bb03fcc005df650b708b57cc52661" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/b566ee0dd251f3c4078bed003a7ce015f5ea6dce", - "reference": "b566ee0dd251f3c4078bed003a7ce015f5ea6dce", + "url": "https://api.github.com/repos/laravel/serializable-closure/zipball/dccd8bcb851bb03fcc005df650b708b57cc52661", + "reference": "dccd8bcb851bb03fcc005df650b708b57cc52661", "shasum": "" }, "require": { @@ -1623,7 +1626,7 @@ "issues": "https://github.com/laravel/serializable-closure/issues", "source": "https://github.com/laravel/serializable-closure" }, - "time": "2026-04-16T14:03:50+00:00" + "time": "2026-07-21T16:49:22+00:00" }, { "name": "laravel/tinker", @@ -1696,16 +1699,16 @@ }, { "name": "league/commonmark", - "version": "2.8.2", + "version": "2.9.0", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "59fb075d2101740c337c7216e3f32b36c204218b" + "reference": "5703d83ba3da3b2e356a5fedc848ed6d8ffb6529" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/59fb075d2101740c337c7216e3f32b36c204218b", - "reference": "59fb075d2101740c337c7216e3f32b36c204218b", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/5703d83ba3da3b2e356a5fedc848ed6d8ffb6529", + "reference": "5703d83ba3da3b2e356a5fedc848ed6d8ffb6529", "shasum": "" }, "require": { @@ -1727,8 +1730,8 @@ "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", + "phpstan/phpstan": "^2.0.0", + "phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0 || ^12.0.0 || ^13.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", @@ -1742,7 +1745,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.9-dev" + "dev-main": "2.10-dev" } }, "autoload": { @@ -1799,7 +1802,7 @@ "type": "tidelift" } ], - "time": "2026-03-19T13:16:38+00:00" + "time": "2026-08-03T13:42:31+00:00" }, { "name": "league/config", @@ -2362,12 +2365,12 @@ "source": { "type": "git", "url": "https://github.com/NativePHP/mobile-air.git", - "reference": "1afaf068f05918604b9fbaa63777bfc93c5f3785" + "reference": "eadf5e8f54617689048413b22a3779f6a3975813" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/NativePHP/mobile-air/zipball/1afaf068f05918604b9fbaa63777bfc93c5f3785", - "reference": "1afaf068f05918604b9fbaa63777bfc93c5f3785", + "url": "https://api.github.com/repos/NativePHP/mobile-air/zipball/eadf5e8f54617689048413b22a3779f6a3975813", + "reference": "eadf5e8f54617689048413b22a3779f6a3975813", "shasum": "" }, "require": { @@ -2402,6 +2405,10 @@ "phpstan/phpstan-phpunit": "^1.0", "spatie/laravel-ray": "^1.26" }, + "suggest": { + "ext-zip": "Required by native:install to validate and extract the downloaded Android PHP binary archives" + }, + "default-branch": true, "type": "library", "extra": { "laravel": { @@ -2472,71 +2479,20 @@ "source": "https://github.com/NativePHP/mobile-air/tree/main", "issues": "https://github.com/NativePHP/mobile-air/issues" }, - "time": "2026-07-23T01:49:24+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" + "time": "2026-08-03T18:34:16+00:00" }, { "name": "nativephp/mobile-ui", - "version": "0.2.0", + "version": "0.3.0", "source": { "type": "git", "url": "https://github.com/NativePHP/mobile-ui.git", - "reference": "96e1c21760fd585c2df3bae96dd70b6a420d4199" + "reference": "548b431ad5c7989c7da766aceb1a1bc02948606f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/NativePHP/mobile-ui/zipball/96e1c21760fd585c2df3bae96dd70b6a420d4199", - "reference": "96e1c21760fd585c2df3bae96dd70b6a420d4199", + "url": "https://api.github.com/repos/NativePHP/mobile-ui/zipball/548b431ad5c7989c7da766aceb1a1bc02948606f", + "reference": "548b431ad5c7989c7da766aceb1a1bc02948606f", "shasum": "" }, "require": { @@ -2563,11 +2519,7 @@ "Native\\Mobile\\UI\\": "src/" } }, - "scripts": { - "test": [ - "pest" - ] - }, + "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], @@ -2579,16 +2531,16 @@ ], "description": "Native SwiftUI and Jetpack Composer components for NativePHP", "support": { - "source": "https://github.com/NativePHP/mobile-ui/tree/0.2.0", - "issues": "https://github.com/NativePHP/mobile-ui/issues" + "issues": "https://github.com/NativePHP/mobile-ui/issues", + "source": "https://github.com/NativePHP/mobile-ui/tree/0.3.0" }, "funding": [ { - "type": "other", - "url": "https://nativephp.com/sponsor" + "url": "https://nativephp.com/sponsor", + "type": "other" } ], - "time": "2026-07-22T15:19:38+00:00" + "time": "2026-07-27T01:37:02+00:00" }, { "name": "nesbot/carbon", @@ -2764,16 +2716,16 @@ }, { "name": "nette/utils", - "version": "v4.1.4", + "version": "v4.1.5", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7" + "reference": "b043439dbdf954e6c28b5ea7e34b0100f83165e0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/7da6c396d7ebe142bc857c20479d5e70a5e1aac7", - "reference": "7da6c396d7ebe142bc857c20479d5e70a5e1aac7", + "url": "https://api.github.com/repos/nette/utils/zipball/b043439dbdf954e6c28b5ea7e34b0100f83165e0", + "reference": "b043439dbdf954e6c28b5ea7e34b0100f83165e0", "shasum": "" }, "require": { @@ -2793,7 +2745,7 @@ }, "suggest": { "ext-gd": "to use Image", - "ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()", + "ext-iconv": "to use Strings::chr(), ord() 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...", @@ -2849,9 +2801,9 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.1.4" + "source": "https://github.com/nette/utils/tree/v4.1.5" }, - "time": "2026-05-11T20:49:54+00:00" + "time": "2026-07-17T23:02:45+00:00" }, { "name": "nikic/php-parser", @@ -4275,16 +4227,16 @@ }, { "name": "symfony/console", - "version": "v8.1.1", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "b711a8ab808b6c074c6b8caef70d0fd8d6b6d07d" + "reference": "535e18a1b8925f6c01a55b171d157ab66c2ace15" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/b711a8ab808b6c074c6b8caef70d0fd8d6b6d07d", - "reference": "b711a8ab808b6c074c6b8caef70d0fd8d6b6d07d", + "url": "https://api.github.com/repos/symfony/console/zipball/535e18a1b8925f6c01a55b171d157ab66c2ace15", + "reference": "535e18a1b8925f6c01a55b171d157ab66c2ace15", "shasum": "" }, "require": { @@ -4351,7 +4303,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v8.1.1" + "source": "https://github.com/symfony/console/tree/v8.1.2" }, "funding": [ { @@ -4371,7 +4323,7 @@ "type": "tidelift" } ], - "time": "2026-06-16T12:55:20+00:00" + "time": "2026-07-27T13:58:19+00:00" }, { "name": "symfony/css-selector", @@ -4515,16 +4467,16 @@ }, { "name": "symfony/error-handler", - "version": "v8.1.0", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/error-handler.git", - "reference": "d8aeb1abd3fef84795567850d3a567bdb5945ee5" + "reference": "dc98404be5e8c949815e23fee1928f5de4f3f5d3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/error-handler/zipball/d8aeb1abd3fef84795567850d3a567bdb5945ee5", - "reference": "d8aeb1abd3fef84795567850d3a567bdb5945ee5", + "url": "https://api.github.com/repos/symfony/error-handler/zipball/dc98404be5e8c949815e23fee1928f5de4f3f5d3", + "reference": "dc98404be5e8c949815e23fee1928f5de4f3f5d3", "shasum": "" }, "require": { @@ -4572,7 +4524,7 @@ "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" + "source": "https://github.com/symfony/error-handler/tree/v8.1.2" }, "funding": [ { @@ -4592,20 +4544,20 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-07-22T15:42:13+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v8.1.1", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "abd6c11dc468725d1627302ad10f6cd486e9e3d0" + "reference": "c14c05a9e6da7f5e375e6efc28952c7e7dbddffb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/abd6c11dc468725d1627302ad10f6cd486e9e3d0", - "reference": "abd6c11dc468725d1627302ad10f6cd486e9e3d0", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/c14c05a9e6da7f5e375e6efc28952c7e7dbddffb", + "reference": "c14c05a9e6da7f5e375e6efc28952c7e7dbddffb", "shasum": "" }, "require": { @@ -4658,7 +4610,7 @@ "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.1" + "source": "https://github.com/symfony/event-dispatcher/tree/v8.1.2" }, "funding": [ { @@ -4678,7 +4630,7 @@ "type": "tidelift" } ], - "time": "2026-06-09T12:28:30+00:00" + "time": "2026-07-22T15:42:13+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -4830,16 +4782,16 @@ }, { "name": "symfony/http-foundation", - "version": "v8.1.1", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "6a168c8fcee806b57ac020244da14293d1f9a883" + "reference": "9943adbf5a64e2951a8d9eb0485310d55624f0e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/6a168c8fcee806b57ac020244da14293d1f9a883", - "reference": "6a168c8fcee806b57ac020244da14293d1f9a883", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/9943adbf5a64e2951a8d9eb0485310d55624f0e8", + "reference": "9943adbf5a64e2951a8d9eb0485310d55624f0e8", "shasum": "" }, "require": { @@ -4887,7 +4839,7 @@ "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.1" + "source": "https://github.com/symfony/http-foundation/tree/v8.1.2" }, "funding": [ { @@ -4907,20 +4859,20 @@ "type": "tidelift" } ], - "time": "2026-06-12T08:43:41+00:00" + "time": "2026-07-29T07:22:54+00:00" }, { "name": "symfony/http-kernel", - "version": "v8.1.1", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/http-kernel.git", - "reference": "89d8d6e7fbab3d9eda89ccb5ecdf44a74c4ec9d2" + "reference": "c7bb08dc26a7a7da68fb7ac1ce9de925e6464dbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-kernel/zipball/89d8d6e7fbab3d9eda89ccb5ecdf44a74c4ec9d2", - "reference": "89d8d6e7fbab3d9eda89ccb5ecdf44a74c4ec9d2", + "url": "https://api.github.com/repos/symfony/http-kernel/zipball/c7bb08dc26a7a7da68fb7ac1ce9de925e6464dbc", + "reference": "c7bb08dc26a7a7da68fb7ac1ce9de925e6464dbc", "shasum": "" }, "require": { @@ -4936,6 +4888,7 @@ "symfony/dependency-injection": "<8.1", "symfony/flex": "<2.10", "symfony/http-client-contracts": "<2.5", + "symfony/serializer": "<7.4.15|>=8.0,<8.0.15|>=8.1,<8.1.2", "symfony/translation-contracts": "<2.5", "symfony/var-dumper": "<8.1", "symfony/web-profiler-bundle": "<8.1", @@ -4968,7 +4921,7 @@ "symfony/validator": "^7.4|^8.0", "symfony/var-dumper": "^8.1", "symfony/var-exporter": "^7.4|^8.0", - "twig/twig": "^3.21" + "twig/twig": "^3.21|^4.0" }, "type": "library", "autoload": { @@ -4996,7 +4949,7 @@ "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.1" + "source": "https://github.com/symfony/http-kernel/tree/v8.1.2" }, "funding": [ { @@ -5016,20 +4969,20 @@ "type": "tidelift" } ], - "time": "2026-06-27T09:27:36+00:00" + "time": "2026-07-29T11:54:54+00:00" }, { "name": "symfony/mailer", - "version": "v8.1.1", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/mailer.git", - "reference": "4fa583a7377f28d54e4de442fba76375b2e20a12" + "reference": "221c7f326ace1ac2baee8331d829d5b7f04f4d53" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mailer/zipball/4fa583a7377f28d54e4de442fba76375b2e20a12", - "reference": "4fa583a7377f28d54e4de442fba76375b2e20a12", + "url": "https://api.github.com/repos/symfony/mailer/zipball/221c7f326ace1ac2baee8331d829d5b7f04f4d53", + "reference": "221c7f326ace1ac2baee8331d829d5b7f04f4d53", "shasum": "" }, "require": { @@ -5076,7 +5029,7 @@ "description": "Helps sending emails", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/mailer/tree/v8.1.1" + "source": "https://github.com/symfony/mailer/tree/v8.1.2" }, "funding": [ { @@ -5096,20 +5049,20 @@ "type": "tidelift" } ], - "time": "2026-06-16T12:55:20+00:00" + "time": "2026-07-28T07:35:25+00:00" }, { "name": "symfony/mime", - "version": "v8.1.0", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "b164ae7e3f7915aacfe9ee155f2f358502440664" + "reference": "75f4779d4ec2e13f24a3a7e5d0347c340c7ca627" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/b164ae7e3f7915aacfe9ee155f2f358502440664", - "reference": "b164ae7e3f7915aacfe9ee155f2f358502440664", + "url": "https://api.github.com/repos/symfony/mime/zipball/75f4779d4ec2e13f24a3a7e5d0347c340c7ca627", + "reference": "75f4779d4ec2e13f24a3a7e5d0347c340c7ca627", "shasum": "" }, "require": { @@ -5162,7 +5115,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v8.1.0" + "source": "https://github.com/symfony/mime/tree/v8.1.2" }, "funding": [ { @@ -5182,7 +5135,7 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-07-29T08:00:47+00:00" }, { "name": "symfony/polyfill-ctype", @@ -5269,16 +5222,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.38.1", + "version": "v1.41.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "e9247d281d694a5120554d9afaf54e070e88a603" + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/e9247d281d694a5120554d9afaf54e070e88a603", - "reference": "e9247d281d694a5120554d9afaf54e070e88a603", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", + "reference": "bb899c1db0aa8127dc3afe8cda4a67eb24915f8d", "shasum": "" }, "require": { @@ -5327,7 +5280,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.38.1" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.41.0" }, "funding": [ { @@ -5347,7 +5300,7 @@ "type": "tidelift" } ], - "time": "2026-05-26T05:58:03+00:00" + "time": "2026-07-28T08:25:59+00:00" }, { "name": "symfony/polyfill-intl-idn", @@ -5772,16 +5725,16 @@ }, { "name": "symfony/polyfill-php85", - "version": "v1.38.1", + "version": "v1.41.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php85.git", - "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1" + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", - "reference": "ba2ba04f3352cfa2dcbbcb90aee13ed967f505b1", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/255fab485aaa1006ed411040c42aecd7b5302d7a", + "reference": "255fab485aaa1006ed411040c42aecd7b5302d7a", "shasum": "" }, "require": { @@ -5828,7 +5781,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php85/tree/v1.38.1" + "source": "https://github.com/symfony/polyfill-php85/tree/v1.41.0" }, "funding": [ { @@ -5848,20 +5801,20 @@ "type": "tidelift" } ], - "time": "2026-05-26T02:25:22+00:00" + "time": "2026-07-01T12:47:55+00:00" }, { "name": "symfony/polyfill-php86", - "version": "v1.38.0", + "version": "v1.41.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php86.git", - "reference": "fcec68d64f46dc84e1f6ffcf2c6dda40ff3143ad" + "reference": "6bc356ed3d8dbfeea8f0de235e34d670704e880e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php86/zipball/fcec68d64f46dc84e1f6ffcf2c6dda40ff3143ad", - "reference": "fcec68d64f46dc84e1f6ffcf2c6dda40ff3143ad", + "url": "https://api.github.com/repos/symfony/polyfill-php86/zipball/6bc356ed3d8dbfeea8f0de235e34d670704e880e", + "reference": "6bc356ed3d8dbfeea8f0de235e34d670704e880e", "shasum": "" }, "require": { @@ -5908,7 +5861,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php86/tree/v1.38.0" + "source": "https://github.com/symfony/polyfill-php86/tree/v1.41.0" }, "funding": [ { @@ -5928,7 +5881,7 @@ "type": "tidelift" } ], - "time": "2026-05-25T11:52:35+00:00" + "time": "2026-07-02T13:42:24+00:00" }, { "name": "symfony/polyfill-uuid", @@ -6080,16 +6033,16 @@ }, { "name": "symfony/routing", - "version": "v8.1.0", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/routing.git", - "reference": "fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3" + "reference": "1058d4e13bb81dd9a6f7565686df7e13b880cdbd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/routing/zipball/fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3", - "reference": "fe0bfec72c8a806109fb9c3a5f2b898fe0c76eb3", + "url": "https://api.github.com/repos/symfony/routing/zipball/1058d4e13bb81dd9a6f7565686df7e13b880cdbd", + "reference": "1058d4e13bb81dd9a6f7565686df7e13b880cdbd", "shasum": "" }, "require": { @@ -6136,7 +6089,7 @@ "url" ], "support": { - "source": "https://github.com/symfony/routing/tree/v8.1.0" + "source": "https://github.com/symfony/routing/tree/v8.1.2" }, "funding": [ { @@ -6156,7 +6109,7 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-07-22T15:42:13+00:00" }, { "name": "symfony/service-contracts", @@ -6247,16 +6200,16 @@ }, { "name": "symfony/string", - "version": "v8.1.0", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9" + "reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/afd5944f4005862d961efb85c8bbd5c523c4e3c9", - "reference": "afd5944f4005862d961efb85c8bbd5c523c4e3c9", + "url": "https://api.github.com/repos/symfony/string/zipball/286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc", + "reference": "286a76b7255e5cc4bf0101a0bc5388ecf1c38ccc", "shasum": "" }, "require": { @@ -6313,7 +6266,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v8.1.0" + "source": "https://github.com/symfony/string/tree/v8.1.2" }, "funding": [ { @@ -6333,7 +6286,7 @@ "type": "tidelift" } ], - "time": "2026-05-29T05:06:50+00:00" + "time": "2026-07-28T07:35:25+00:00" }, { "name": "symfony/translation", @@ -6590,16 +6543,16 @@ }, { "name": "symfony/var-dumper", - "version": "v8.1.1", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/var-dumper.git", - "reference": "40096a2515a979f3125c5c928603995b8664c62a" + "reference": "865103cf742a039f34645b971fc3ace308d6c167" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/var-dumper/zipball/40096a2515a979f3125c5c928603995b8664c62a", - "reference": "40096a2515a979f3125c5c928603995b8664c62a", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/865103cf742a039f34645b971fc3ace308d6c167", + "reference": "865103cf742a039f34645b971fc3ace308d6c167", "shasum": "" }, "require": { @@ -6615,7 +6568,7 @@ "symfony/http-kernel": "^7.4|^8.0", "symfony/process": "^7.4|^8.0", "symfony/uid": "^7.4|^8.0", - "twig/twig": "^3.12" + "twig/twig": "^3.12|^4.0" }, "bin": [ "Resources/bin/var-dump-server" @@ -6653,7 +6606,7 @@ "dump" ], "support": { - "source": "https://github.com/symfony/var-dumper/tree/v8.1.1" + "source": "https://github.com/symfony/var-dumper/tree/v8.1.2" }, "funding": [ { @@ -6673,7 +6626,7 @@ "type": "tidelift" } ], - "time": "2026-06-09T10:54:51+00:00" + "time": "2026-07-22T15:42:13+00:00" }, { "name": "tijsverkoyen/css-to-inline-styles", @@ -6955,16 +6908,16 @@ "packages-dev": [ { "name": "brianium/paratest", - "version": "v7.20.0", + "version": "v7.23.1", "source": { "type": "git", "url": "https://github.com/paratestphp/paratest.git", - "reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d" + "reference": "6b2aaf7d0e8d5220710d78921f28e5464a816596" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/paratestphp/paratest/zipball/81c80677c9ec0ed4ef16b246167f11dec81a6e3d", - "reference": "81c80677c9ec0ed4ef16b246167f11dec81a6e3d", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/6b2aaf7d0e8d5220710d78921f28e5464a816596", + "reference": "6b2aaf7d0e8d5220710d78921f28e5464a816596", "shasum": "" }, "require": { @@ -6974,25 +6927,25 @@ "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" + "php": "~8.4.0 || ~8.5.0", + "phpunit/php-code-coverage": "^14.2.3", + "phpunit/php-file-iterator": "^7", + "phpunit/php-timer": "^9", + "phpunit/phpunit": "^13.2.5", + "sebastian/environment": "^9.3.2", + "symfony/console": "^7.4.8 || ^8.1.1", + "symfony/process": "^7.4.8 || ^8.1.0" }, "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" + "phpstan/phpstan": "^2.2.6", + "phpstan/phpstan-deprecation-rules": "^2.0.5", + "phpstan/phpstan-phpunit": "^2.0.18", + "phpstan/phpstan-strict-rules": "^2.0.12", + "symfony/filesystem": "^7.4.8 || ^8.1.0" }, "bin": [ "bin/paratest", @@ -7032,7 +6985,7 @@ ], "support": { "issues": "https://github.com/paratestphp/paratest/issues", - "source": "https://github.com/paratestphp/paratest/tree/v7.20.0" + "source": "https://github.com/paratestphp/paratest/tree/v7.23.1" }, "funding": [ { @@ -7044,149 +6997,7 @@ "type": "paypal" } ], - "time": "2026-03-29T15:46:14+00:00" - }, - { - "name": "composer/pcre", - "version": "3.4.0", - "source": { - "type": "git", - "url": "https://github.com/composer/pcre.git", - "reference": "d5a341b3fb61f3001970940afb1d332968a183ed" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/composer/pcre/zipball/d5a341b3fb61f3001970940afb1d332968a183ed", - "reference": "d5a341b3fb61f3001970940afb1d332968a183ed", - "shasum": "" - }, - "require": { - "php": "^7.4 || ^8.0" - }, - "conflict": { - "phpstan/phpstan": "<2.2.2" - }, - "require-dev": { - "phpstan/phpstan": "^2", - "phpstan/phpstan-deprecation-rules": "^2", - "phpstan/phpstan-strict-rules": "^2", - "phpunit/phpunit": "^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.4.0" - }, - "funding": [ - { - "url": "https://packagist.com", - "type": "custom" - }, - { - "url": "https://github.com/composer", - "type": "github" - } - ], - "time": "2026-06-07T11:47:49+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" + "time": "2026-07-28T13:47:02+00:00" }, { "name": "doctrine/deprecations", @@ -7544,16 +7355,16 @@ }, { "name": "laravel/boost", - "version": "v2.4.12", + "version": "v2.4.13", "source": { "type": "git", "url": "https://github.com/laravel/boost.git", - "reference": "d43bdf901fee8d216145cb062c9847c892844908" + "reference": "f55e08f5afa89ac72f23f574175005b67878f466" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/boost/zipball/d43bdf901fee8d216145cb062c9847c892844908", - "reference": "d43bdf901fee8d216145cb062c9847c892844908", + "url": "https://api.github.com/repos/laravel/boost/zipball/f55e08f5afa89ac72f23f574175005b67878f466", + "reference": "f55e08f5afa89ac72f23f574175005b67878f466", "shasum": "" }, "require": { @@ -7562,7 +7373,7 @@ "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|^0.8.0", + "laravel/mcp": "^0.7.1|^0.8.0|^0.9.0", "laravel/prompts": "^0.3.10", "laravel/roster": "^0.5.0", "php": "^8.2" @@ -7606,20 +7417,20 @@ "issues": "https://github.com/laravel/boost/issues", "source": "https://github.com/laravel/boost" }, - "time": "2026-07-08T09:53:34+00:00" + "time": "2026-07-17T14:28:57+00:00" }, { "name": "laravel/mcp", - "version": "v0.8.2", + "version": "v0.9.1", "source": { "type": "git", "url": "https://github.com/laravel/mcp.git", - "reference": "0c32bf369c6432cab21458f9f4479da33a49ba37" + "reference": "a08884d79a95c5143498507aec5badf751cdbec4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/mcp/zipball/0c32bf369c6432cab21458f9f4479da33a49ba37", - "reference": "0c32bf369c6432cab21458f9f4479da33a49ba37", + "url": "https://api.github.com/repos/laravel/mcp/zipball/a08884d79a95c5143498507aec5badf751cdbec4", + "reference": "a08884d79a95c5143498507aec5badf751cdbec4", "shasum": "" }, "require": { @@ -7680,7 +7491,7 @@ "issues": "https://github.com/laravel/mcp/issues", "source": "https://github.com/laravel/mcp" }, - "time": "2026-06-25T14:00:45+00:00" + "time": "2026-07-21T13:23:52+00:00" }, { "name": "laravel/pail", @@ -7764,16 +7575,16 @@ }, { "name": "laravel/pint", - "version": "v1.29.3", + "version": "v1.30.3", "source": { "type": "git", "url": "https://github.com/laravel/pint.git", - "reference": "da1d1111a6aa2e082d2a388b194afe1ba0a05d14" + "reference": "19ca6de4ce07869f61f09863e37e81562ebc0a9b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/pint/zipball/da1d1111a6aa2e082d2a388b194afe1ba0a05d14", - "reference": "da1d1111a6aa2e082d2a388b194afe1ba0a05d14", + "url": "https://api.github.com/repos/laravel/pint/zipball/19ca6de4ce07869f61f09863e37e81562ebc0a9b", + "reference": "19ca6de4ce07869f61f09863e37e81562ebc0a9b", "shasum": "" }, "require": { @@ -7784,14 +7595,16 @@ "php": "^8.2.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.95.8", - "illuminate/view": "^12.62.0", + "composer/semver": "^3.4.4", + "friendsofphp/php-cs-fixer": "^3.95.18", + "illuminate/view": "^12.64.0", "larastan/larastan": "^3.10.0", "laravel-zero/framework": "^12.1.0", "laravel/agent-detector": "^2.0.2", + "laravel/prompts": "^0.3.21", "mockery/mockery": "^1.6.12", "nunomaduro/termwind": "^2.4.0", - "pestphp/pest": "^3.8.6" + "pestphp/pest": "^3.8.7" }, "bin": [ "builds/pint" @@ -7828,7 +7641,7 @@ "issues": "https://github.com/laravel/pint/issues", "source": "https://github.com/laravel/pint" }, - "time": "2026-06-16T15:34:04+00:00" + "time": "2026-08-02T01:28:21+00:00" }, { "name": "laravel/roster", @@ -7893,16 +7706,16 @@ }, { "name": "laravel/sail", - "version": "v1.63.0", + "version": "v1.64.0", "source": { "type": "git", "url": "https://github.com/laravel/sail.git", - "reference": "51bbce3f803c1d386cabbb44e618c955a12ff5fc" + "reference": "08cacd3e72d6798df3fa8bd4b5d55d0f7f920625" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/sail/zipball/51bbce3f803c1d386cabbb44e618c955a12ff5fc", - "reference": "51bbce3f803c1d386cabbb44e618c955a12ff5fc", + "url": "https://api.github.com/repos/laravel/sail/zipball/08cacd3e72d6798df3fa8bd4b5d55d0f7f920625", + "reference": "08cacd3e72d6798df3fa8bd4b5d55d0f7f920625", "shasum": "" }, "require": { @@ -7952,7 +7765,7 @@ "issues": "https://github.com/laravel/sail/issues", "source": "https://github.com/laravel/sail" }, - "time": "2026-06-18T08:54:14+00:00" + "time": "2026-07-17T15:09:35+00:00" }, { "name": "mockery/mockery", @@ -8099,23 +7912,23 @@ }, { "name": "nunomaduro/collision", - "version": "v8.9.4", + "version": "v8.9.5", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "716af8f95a470e9094cfca09ed897b023be191a5" + "reference": "fb53eacd509a1d303858e2d20cfebf2d630254ec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/716af8f95a470e9094cfca09ed897b023be191a5", - "reference": "716af8f95a470e9094cfca09ed897b023be191a5", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/fb53eacd509a1d303858e2d20cfebf2d630254ec", + "reference": "fb53eacd509a1d303858e2d20cfebf2d630254ec", "shasum": "" }, "require": { "filp/whoops": "^2.18.4", "nunomaduro/termwind": "^2.4.0", "php": "^8.2.0", - "symfony/console": "^7.4.8 || ^8.0.8" + "symfony/console": "^7.4.14 || ^8.1.1" }, "conflict": { "laravel/framework": "<11.48.0 || >=14.0.0", @@ -8123,12 +7936,12 @@ }, "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" + "larastan/larastan": "^3.10.0", + "laravel/framework": "^11.48.0 || ^12.56.0 || ^13.20.0", + "laravel/pint": "^1.29.3", + "orchestra/testbench-core": "^9.12.0 || ^10.12.1 || ^11.3.5", + "pestphp/pest": "^3.8.5 || ^4.7.5 || ^5.0.0", + "sebastian/environment": "^7.2.1 || ^8.1.2 || ^9.3.2" }, "type": "library", "extra": { @@ -8191,46 +8004,47 @@ "type": "patreon" } ], - "time": "2026-04-21T14:04:20+00:00" + "time": "2026-07-15T19:09:14+00:00" }, { "name": "pestphp/pest", - "version": "v4.7.5", + "version": "v5.0.3", "source": { "type": "git", "url": "https://github.com/pestphp/pest.git", - "reference": "5dc49a71d63602a9b98fed0f2017c4679ef9f8e0" + "reference": "585de259a2e59d01ece1340f040d60bb52d2fb14" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/5dc49a71d63602a9b98fed0f2017c4679ef9f8e0", - "reference": "5dc49a71d63602a9b98fed0f2017c4679ef9f8e0", + "url": "https://api.github.com/repos/pestphp/pest/zipball/585de259a2e59d01ece1340f040d60bb52d2fb14", + "reference": "585de259a2e59d01ece1340f040d60bb52d2fb14", "shasum": "" }, "require": { - "brianium/paratest": "^7.20.0", - "composer/xdebug-handler": "^3.0.5", - "nunomaduro/collision": "^8.9.4", + "brianium/paratest": "^7.23.1", + "nunomaduro/collision": "^8.9.5", "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.30", - "symfony/process": "^7.4.13|^8.1.0" + "pestphp/pest-plugin": "^5.0.0", + "pestphp/pest-plugin-arch": "^5.0.0", + "pestphp/pest-plugin-mutate": "^5.0.0", + "pestphp/pest-plugin-profanity": "^5.0.0", + "php": "^8.4", + "phpunit/phpunit": "^13.2.6", + "symfony/process": "^8.1.0" }, "conflict": { "filp/whoops": "<2.18.3", - "phpunit/phpunit": ">12.5.30", + "phpunit/phpunit": ">13.2.6", "sebastian/exporter": "<7.0.0", "webmozart/assert": "<1.11.0" }, "require-dev": { - "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", + "laravel/pao": "^1.1.3", + "pestphp/pest-dev-tools": "^5.0.0", + "pestphp/pest-plugin-browser": "^5.0.0", + "pestphp/pest-plugin-phpstan": "^5.0.0", + "pestphp/pest-plugin-rector": "^5.0.0", + "pestphp/pest-plugin-type-coverage": "^5.0.0", "psy/psysh": "^0.12.24" }, "bin": [ @@ -8298,7 +8112,7 @@ ], "support": { "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v4.7.5" + "source": "https://github.com/pestphp/pest/tree/v5.0.3" }, "funding": [ { @@ -8310,34 +8124,34 @@ "type": "github" } ], - "time": "2026-07-06T17:06:29+00:00" + "time": "2026-08-03T02:26:34+00:00" }, { "name": "pestphp/pest-plugin", - "version": "v4.0.0", + "version": "v5.0.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin.git", - "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568" + "reference": "87283f41aafa2561b7618be53eab00f2d06f4140" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/9d4b93d7f73d3f9c3189bb22c220fef271cdf568", - "reference": "9d4b93d7f73d3f9c3189bb22c220fef271cdf568", + "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/87283f41aafa2561b7618be53eab00f2d06f4140", + "reference": "87283f41aafa2561b7618be53eab00f2d06f4140", "shasum": "" }, "require": { "composer-plugin-api": "^2.0.0", "composer-runtime-api": "^2.2.2", - "php": "^8.3" + "php": "^8.4" }, "conflict": { - "pestphp/pest": "<4.0.0" + "pestphp/pest": "<5.0.0" }, "require-dev": { - "composer/composer": "^2.8.10", - "pestphp/pest": "^4.0.0", - "pestphp/pest-dev-tools": "^4.0.0" + "composer/composer": "^2.10.2", + "pestphp/pest": "^5.0.0", + "pestphp/pest-dev-tools": "^5.0.0" }, "type": "composer-plugin", "extra": { @@ -8364,7 +8178,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin/tree/v4.0.0" + "source": "https://github.com/pestphp/pest-plugin/tree/v5.0.0" }, "funding": [ { @@ -8380,30 +8194,33 @@ "type": "patreon" } ], - "time": "2025-08-20T12:35:58+00:00" + "time": "2026-07-18T17:10:45+00:00" }, { "name": "pestphp/pest-plugin-arch", - "version": "v4.0.2", + "version": "v5.0.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin-arch.git", - "reference": "3fb0d02a91b9da504b139dc7ab2a31efb7c3215c" + "reference": "244465d5dc9ea9fb5ac5c9badb7eb47d1594e4c1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/3fb0d02a91b9da504b139dc7ab2a31efb7c3215c", - "reference": "3fb0d02a91b9da504b139dc7ab2a31efb7c3215c", + "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/244465d5dc9ea9fb5ac5c9badb7eb47d1594e4c1", + "reference": "244465d5dc9ea9fb5ac5c9badb7eb47d1594e4c1", "shasum": "" }, "require": { - "pestphp/pest-plugin": "^4.0.0", - "php": "^8.3", + "pestphp/pest-plugin": "^5.0.0", + "php": "^8.4", "ta-tikoma/phpunit-architecture-test": "^0.8.7" }, + "conflict": { + "pestphp/pest": "<5.0.0" + }, "require-dev": { - "pestphp/pest": "^4.4.6", - "pestphp/pest-dev-tools": "^4.1.0" + "pestphp/pest": "^5.0.0", + "pestphp/pest-dev-tools": "^5.0.0" }, "type": "library", "extra": { @@ -8438,7 +8255,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-arch/tree/v4.0.2" + "source": "https://github.com/pestphp/pest-plugin-arch/tree/v5.0.0" }, "funding": [ { @@ -8450,31 +8267,34 @@ "type": "github" } ], - "time": "2026-04-10T17:20:19+00:00" + "time": "2026-07-21T07:48:55+00:00" }, { "name": "pestphp/pest-plugin-laravel", - "version": "v4.1.0", + "version": "v5.0.1", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin-laravel.git", - "reference": "3057a36669ff11416cc0dc2b521b3aec58c488d0" + "reference": "d1564646e3198f1b607e64a36501d6edff7d104e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-laravel/zipball/3057a36669ff11416cc0dc2b521b3aec58c488d0", - "reference": "3057a36669ff11416cc0dc2b521b3aec58c488d0", + "url": "https://api.github.com/repos/pestphp/pest-plugin-laravel/zipball/d1564646e3198f1b607e64a36501d6edff7d104e", + "reference": "d1564646e3198f1b607e64a36501d6edff7d104e", "shasum": "" }, "require": { - "laravel/framework": "^11.45.2|^12.52.0|^13.0", - "pestphp/pest": "^4.4.1", - "php": "^8.3.0" + "laravel/framework": "^13.23.0", + "pestphp/pest": "^5.0.1", + "php": "^8.4" + }, + "conflict": { + "pestphp/pest": "<5.0.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" + "laravel/dusk": "^8.6.0", + "orchestra/testbench": "^11.1", + "pestphp/pest-dev-tools": "^5.0.0" }, "type": "library", "extra": { @@ -8512,7 +8332,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-laravel/tree/v4.1.0" + "source": "https://github.com/pestphp/pest-plugin-laravel/tree/v5.0.1" }, "funding": [ { @@ -8524,32 +8344,35 @@ "type": "github" } ], - "time": "2026-02-21T00:29:45+00:00" + "time": "2026-07-29T18:19:36+00:00" }, { "name": "pestphp/pest-plugin-mutate", - "version": "v4.0.1", + "version": "v5.0.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin-mutate.git", - "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c" + "reference": "fc4a0b3d3bc75bad1148d4c046cf0fa66c508f77" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-mutate/zipball/d9b32b60b2385e1688a68cc227594738ec26d96c", - "reference": "d9b32b60b2385e1688a68cc227594738ec26d96c", + "url": "https://api.github.com/repos/pestphp/pest-plugin-mutate/zipball/fc4a0b3d3bc75bad1148d4c046cf0fa66c508f77", + "reference": "fc4a0b3d3bc75bad1148d4c046cf0fa66c508f77", "shasum": "" }, "require": { - "nikic/php-parser": "^5.6.1", - "pestphp/pest-plugin": "^4.0.0", - "php": "^8.3", + "nikic/php-parser": "^5.8.0", + "pestphp/pest-plugin": "^5.0.0", + "php": "^8.4", "psr/simple-cache": "^3.0.0" }, + "conflict": { + "pestphp/pest": "<5.0.0" + }, "require-dev": { - "pestphp/pest": "^4.0.0", - "pestphp/pest-dev-tools": "^4.0.0", - "pestphp/pest-plugin-type-coverage": "^4.0.0" + "pestphp/pest": "^5.0.0", + "pestphp/pest-dev-tools": "^5.0.0", + "pestphp/pest-plugin-type-coverage": "^5.0.0" }, "type": "library", "autoload": { @@ -8584,7 +8407,7 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-mutate/tree/v4.0.1" + "source": "https://github.com/pestphp/pest-plugin-mutate/tree/v5.0.0" }, "funding": [ { @@ -8600,30 +8423,33 @@ "type": "github" } ], - "time": "2025-08-21T20:19:25+00:00" + "time": "2026-07-21T07:48:56+00:00" }, { "name": "pestphp/pest-plugin-profanity", - "version": "v4.2.1", + "version": "v5.0.0", "source": { "type": "git", "url": "https://github.com/pestphp/pest-plugin-profanity.git", - "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27" + "reference": "3dedf9ea6e2876b5b31f7733d3eacbd86f4653b9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin-profanity/zipball/343cfa6f3564b7e35df0ebb77b7fa97039f72b27", - "reference": "343cfa6f3564b7e35df0ebb77b7fa97039f72b27", + "url": "https://api.github.com/repos/pestphp/pest-plugin-profanity/zipball/3dedf9ea6e2876b5b31f7733d3eacbd86f4653b9", + "reference": "3dedf9ea6e2876b5b31f7733d3eacbd86f4653b9", "shasum": "" }, "require": { - "pestphp/pest-plugin": "^4.0.0", - "php": "^8.3" + "pestphp/pest-plugin": "^5.0.0", + "php": "^8.4" + }, + "conflict": { + "pestphp/pest": "<5.0.0" }, "require-dev": { - "faissaloux/pest-plugin-inside": "^1.9", - "pestphp/pest": "^4.0.0", - "pestphp/pest-dev-tools": "^4.0.0" + "faissaloux/pest-plugin-inside": "^1.11", + "pestphp/pest": "^5.0.0", + "pestphp/pest-dev-tools": "^5.0.0" }, "type": "library", "extra": { @@ -8654,9 +8480,9 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin-profanity/tree/v4.2.1" + "source": "https://github.com/pestphp/pest-plugin-profanity/tree/v5.0.0" }, - "time": "2025-12-08T00:13:17+00:00" + "time": "2026-07-21T07:48:56+00:00" }, { "name": "phar-io/manifest", @@ -9001,33 +8827,35 @@ }, { "name": "phpunit/php-code-coverage", - "version": "12.5.7", + "version": "14.2.4", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "186dab580576598076de6818596d12b61801880e" + "reference": "048a5c12bdb4580f4767ce2761793a16b170fbe4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/186dab580576598076de6818596d12b61801880e", - "reference": "186dab580576598076de6818596d12b61801880e", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/048a5c12bdb4580f4767ce2761793a16b170fbe4", + "reference": "048a5c12bdb4580f4767ce2761793a16b170fbe4", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", + "ext-mbstring": "*", "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", + "nikic/php-parser": "^5.8.0", + "php": ">=8.4", + "phpunit/php-text-template": "^6.0", + "sebastian/complexity": "^6.0", + "sebastian/environment": "^9.3.2", + "sebastian/git-state": "^1.0", + "sebastian/lines-of-code": "^5.0.1", + "sebastian/version": "^7.0", "theseer/tokenizer": "^2.0.1" }, "require-dev": { - "phpunit/phpunit": "^12.5.28" + "phpunit/phpunit": "^13.2.2" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -9036,7 +8864,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "12.5.x-dev" + "dev-main": "14.2.x-dev" } }, "autoload": { @@ -9065,7 +8893,7 @@ "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" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/14.2.4" }, "funding": [ { @@ -9085,32 +8913,32 @@ "type": "tidelift" } ], - "time": "2026-06-01T13:24:19+00:00" + "time": "2026-07-30T17:01:07+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "6.0.1", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" + "reference": "6e5aa1fb0a95b1703d83e721299ee18bb4e2de50" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", - "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6e5aa1fb0a95b1703d83e721299ee18bb4e2de50", + "reference": "6e5aa1fb0a95b1703d83e721299ee18bb4e2de50", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -9138,7 +8966,7 @@ "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" + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/7.0.0" }, "funding": [ { @@ -9158,28 +8986,28 @@ "type": "tidelift" } ], - "time": "2026-02-02T14:04:18+00:00" + "time": "2026-02-06T04:33:26+00:00" }, { "name": "phpunit/php-invoker", - "version": "6.0.0", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + "reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", - "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88", + "reference": "42e5c5cae0c65df12d1b1a3ab52bf3f50f244d88", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { "ext-pcntl": "*", - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "suggest": { "ext-pcntl": "*" @@ -9187,7 +9015,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -9214,40 +9042,52 @@ "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" + "source": "https://github.com/sebastianbergmann/php-invoker/tree/7.0.0" }, "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-invoker", + "type": "tidelift" } ], - "time": "2025-02-07T04:58:58+00:00" + "time": "2026-02-06T04:34:47+00:00" }, { "name": "phpunit/php-text-template", - "version": "5.0.0", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" + "reference": "a47af19f93f76aa3368303d752aa5272ca3299f4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", - "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/a47af19f93f76aa3368303d752aa5272ca3299f4", + "reference": "a47af19f93f76aa3368303d752aa5272ca3299f4", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -9274,40 +9114,52 @@ "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" + "source": "https://github.com/sebastianbergmann/php-text-template/tree/6.0.0" }, "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-text-template", + "type": "tidelift" } ], - "time": "2025-02-07T04:59:16+00:00" + "time": "2026-02-06T04:36:37+00:00" }, { "name": "phpunit/php-timer", - "version": "8.0.0", + "version": "9.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" + "reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", - "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/a0e12065831f6ab0d83120dc61513eb8d9a966f6", + "reference": "a0e12065831f6ab0d83120dc61513eb8d9a966f6", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "8.0-dev" + "dev-main": "9.0-dev" } }, "autoload": { @@ -9334,56 +9186,70 @@ "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" + "source": "https://github.com/sebastianbergmann/php-timer/tree/9.0.0" }, "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-timer", + "type": "tidelift" } ], - "time": "2025-02-07T04:59:38+00:00" + "time": "2026-02-06T04:37:53+00:00" }, { "name": "phpunit/phpunit", - "version": "12.5.30", + "version": "13.2.6", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "900400a5b616d6fb306f9549f6da33ba615d3fbb" + "reference": "5d2afe181339a56348ef9a80fa7eb806b7eae508" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/900400a5b616d6fb306f9549f6da33ba615d3fbb", - "reference": "900400a5b616d6fb306f9549f6da33ba615d3fbb", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/5d2afe181339a56348ef9a80fa7eb806b7eae508", + "reference": "5d2afe181339a56348ef9a80fa7eb806b7eae508", "shasum": "" }, "require": { "ext-dom": "*", + "ext-filter": "*", "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.7", - "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.3", - "sebastian/object-enumerator": "^7.0.0", - "sebastian/recursion-context": "^7.0.1", - "sebastian/type": "^6.0.4", - "sebastian/version": "^6.0.0", + "php": ">=8.4.1", + "phpunit/php-code-coverage": "^14.2.3", + "phpunit/php-file-iterator": "^7.0.0", + "phpunit/php-invoker": "^7.0.0", + "phpunit/php-text-template": "^6.0.0", + "phpunit/php-timer": "^9.0.0", + "sebastian/cli-parser": "^5.0.0", + "sebastian/comparator": "^8.3.0", + "sebastian/diff": "^9.0", + "sebastian/environment": "^9.3.2", + "sebastian/exporter": "^8.1.1", + "sebastian/file-filter": "^1.0", + "sebastian/git-state": "^1.0", + "sebastian/global-state": "^9.0.1", + "sebastian/object-enumerator": "^8.0.0", + "sebastian/recursion-context": "^8.0.0", + "sebastian/type": "^7.0.1", + "sebastian/version": "^7.0.0", "staabm/side-effects-detector": "^1.0.5" }, "bin": [ @@ -9392,7 +9258,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "12.5-dev" + "dev-main": "13.2-dev" } }, "autoload": { @@ -9424,7 +9290,7 @@ "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.30" + "source": "https://github.com/sebastianbergmann/phpunit/tree/13.2.6" }, "funding": [ { @@ -9432,32 +9298,32 @@ "type": "other" } ], - "time": "2026-06-15T13:12:30+00:00" + "time": "2026-07-28T14:00:09+00:00" }, { "name": "sebastian/cli-parser", - "version": "4.2.1", + "version": "5.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "7d05781b13f7dec9043a629a21d086ed74582a15" + "reference": "eeb759ad3146b7096fb59c3195d39e071cd409e3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/7d05781b13f7dec9043a629a21d086ed74582a15", - "reference": "7d05781b13f7dec9043a629a21d086ed74582a15", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/eeb759ad3146b7096fb59c3195d39e071cd409e3", + "reference": "eeb759ad3146b7096fb59c3195d39e071cd409e3", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.5.25" + "phpunit/phpunit": "^13.2.6" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.2-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -9481,7 +9347,7 @@ "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" + "source": "https://github.com/sebastianbergmann/cli-parser/tree/5.0.1" }, "funding": [ { @@ -9501,31 +9367,31 @@ "type": "tidelift" } ], - "time": "2026-05-17T05:29:34+00:00" + "time": "2026-08-01T04:27:14+00:00" }, { "name": "sebastian/comparator", - "version": "7.1.8", + "version": "8.3.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "7c65c1e79836812819705b473a90c12399542485" + "reference": "c025fc7604afab3f195fab7cdaf72327331af241" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/7c65c1e79836812819705b473a90c12399542485", - "reference": "7c65c1e79836812819705b473a90c12399542485", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/c025fc7604afab3f195fab7cdaf72327331af241", + "reference": "c025fc7604afab3f195fab7cdaf72327331af241", "shasum": "" }, "require": { "ext-dom": "*", "ext-mbstring": "*", - "php": ">=8.3", - "sebastian/diff": "^7.0", - "sebastian/exporter": "^7.0.3" + "php": ">=8.4", + "sebastian/diff": "^9.0", + "sebastian/exporter": "^8.1.0" }, "require-dev": { - "phpunit/phpunit": "^12.5.25" + "phpunit/phpunit": "^13.2" }, "suggest": { "ext-bcmath": "For comparing BcMath\\Number objects" @@ -9533,7 +9399,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "7.1-dev" + "dev-main": "8.3-dev" } }, "autoload": { @@ -9573,7 +9439,7 @@ "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" + "source": "https://github.com/sebastianbergmann/comparator/tree/8.3.0" }, "funding": [ { @@ -9593,33 +9459,33 @@ "type": "tidelift" } ], - "time": "2026-05-21T04:45:25+00:00" + "time": "2026-06-05T03:06:45+00:00" }, { "name": "sebastian/complexity", - "version": "5.0.0", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" + "reference": "c5651c795c98093480df79350cb050813fc7a2f3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", - "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/c5651c795c98093480df79350cb050813fc7a2f3", + "reference": "c5651c795c98093480df79350cb050813fc7a2f3", "shasum": "" }, "require": { "nikic/php-parser": "^5.0", - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -9643,41 +9509,53 @@ "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" + "source": "https://github.com/sebastianbergmann/complexity/tree/6.0.0" }, "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/complexity", + "type": "tidelift" } ], - "time": "2025-02-07T04:55:25+00:00" + "time": "2026-02-06T04:41:32+00:00" }, { "name": "sebastian/diff", - "version": "7.0.0", + "version": "9.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + "reference": "a3fb6a298a265ff487a91bbea46e03cd01dbb226" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", - "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/a3fb6a298a265ff487a91bbea46e03cd01dbb226", + "reference": "a3fb6a298a265ff487a91bbea46e03cd01dbb226", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0", - "symfony/process": "^7.2" + "phpunit/phpunit": "^13.2", + "symfony/process": "^7.4.13" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-main": "9.0-dev" } }, "autoload": { @@ -9710,35 +9588,47 @@ "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" + "source": "https://github.com/sebastianbergmann/diff/tree/9.0.0" }, "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/diff", + "type": "tidelift" } ], - "time": "2025-02-07T04:55:46+00:00" + "time": "2026-06-05T03:04:51+00:00" }, { "name": "sebastian/environment", - "version": "8.1.2", + "version": "9.3.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439" + "reference": "6c9e487c9eb706a8d258102a1c0b0a3e53e86c2e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/9d32c685773823b1983e256ae4ecd48a10d6e439", - "reference": "9d32c685773823b1983e256ae4ecd48a10d6e439", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/6c9e487c9eb706a8d258102a1c0b0a3e53e86c2e", + "reference": "6c9e487c9eb706a8d258102a1c0b0a3e53e86c2e", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.5.26" + "phpunit/phpunit": "^13.1.11" }, "suggest": { "ext-posix": "*" @@ -9746,7 +9636,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "8.1-dev" + "dev-main": "9.3-dev" } }, "autoload": { @@ -9774,7 +9664,7 @@ "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" + "source": "https://github.com/sebastianbergmann/environment/tree/9.3.2" }, "funding": [ { @@ -9794,34 +9684,34 @@ "type": "tidelift" } ], - "time": "2026-05-25T13:40:20+00:00" + "time": "2026-05-25T13:41:38+00:00" }, { "name": "sebastian/exporter", - "version": "7.0.3", + "version": "8.1.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23" + "reference": "cfaa77c750dcad6f44c9bac8f62ac486e1c82c26" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", - "reference": "c5e21b5de653ce0a769fb36f5cdfcb5e7a32cf23", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/cfaa77c750dcad6f44c9bac8f62ac486e1c82c26", + "reference": "cfaa77c750dcad6f44c9bac8f62ac486e1c82c26", "shasum": "" }, "require": { "ext-mbstring": "*", - "php": ">=8.3", - "sebastian/recursion-context": "^7.0.1" + "php": ">=8.4", + "sebastian/recursion-context": "^8.0" }, "require-dev": { - "phpunit/phpunit": "^12.5.25" + "phpunit/phpunit": "^13.2.4" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-main": "8.1-dev" } }, "autoload": { @@ -9864,7 +9754,7 @@ "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" + "source": "https://github.com/sebastianbergmann/exporter/tree/8.1.1" }, "funding": [ { @@ -9884,35 +9774,173 @@ "type": "tidelift" } ], - "time": "2026-05-20T04:37:17+00:00" + "time": "2026-07-13T11:35:11+00:00" + }, + { + "name": "sebastian/file-filter", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/file-filter.git", + "reference": "33a26f394330f6faa7684bb9cc73afb7727aae93" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/file-filter/zipball/33a26f394330f6faa7684bb9cc73afb7727aae93", + "reference": "33a26f394330f6faa7684bb9cc73afb7727aae93", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.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 filtering files", + "homepage": "https://github.com/sebastianbergmann/file-filter", + "support": { + "issues": "https://github.com/sebastianbergmann/file-filter/issues", + "security": "https://github.com/sebastianbergmann/file-filter/security/policy", + "source": "https://github.com/sebastianbergmann/file-filter/tree/1.0.0" + }, + "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/file-filter", + "type": "tidelift" + } + ], + "time": "2026-04-22T07:20:04+00:00" + }, + { + "name": "sebastian/git-state", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/git-state.git", + "reference": "792a952e0eba55b6960a48aeceb9f371aad1f76b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/git-state/zipball/792a952e0eba55b6960a48aeceb9f371aad1f76b", + "reference": "792a952e0eba55b6960a48aeceb9f371aad1f76b", + "shasum": "" + }, + "require": { + "php": ">=8.4" + }, + "require-dev": { + "phpunit/phpunit": "^13.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.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 describing the state of a Git checkout", + "homepage": "https://github.com/sebastianbergmann/git-state", + "support": { + "issues": "https://github.com/sebastianbergmann/git-state/issues", + "security": "https://github.com/sebastianbergmann/git-state/security/policy", + "source": "https://github.com/sebastianbergmann/git-state/tree/1.0.0" + }, + "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/git-state", + "type": "tidelift" + } + ], + "time": "2026-03-21T12:54:28+00:00" }, { "name": "sebastian/global-state", - "version": "8.0.3", + "version": "9.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9" + "reference": "ba68ba79da690cf7eddefd3ce5b78b20b9ba9945" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/b164d3274d6537ab462591c5755f76a8f5b1aae9", - "reference": "b164d3274d6537ab462591c5755f76a8f5b1aae9", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ba68ba79da690cf7eddefd3ce5b78b20b9ba9945", + "reference": "ba68ba79da690cf7eddefd3ce5b78b20b9ba9945", "shasum": "" }, "require": { - "php": ">=8.3", - "sebastian/object-reflector": "^5.0", - "sebastian/recursion-context": "^7.0.1" + "php": ">=8.4", + "sebastian/object-reflector": "^6.0", + "sebastian/recursion-context": "^8.0" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^12.5.28" + "phpunit/phpunit": "^13.1.13" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "8.0-dev" + "dev-main": "9.0-dev" } }, "autoload": { @@ -9938,7 +9966,7 @@ "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" + "source": "https://github.com/sebastianbergmann/global-state/tree/9.0.1" }, "funding": [ { @@ -9958,33 +9986,33 @@ "type": "tidelift" } ], - "time": "2026-06-01T15:10:33+00:00" + "time": "2026-06-01T15:11:33+00:00" }, { "name": "sebastian/lines-of-code", - "version": "4.0.1", + "version": "5.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e" + "reference": "d1b6f8fce682505dbd048977f1abedf1b8ad3ff8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d543b8ef219dcd8da262cbb958639a96bedba10e", - "reference": "d543b8ef219dcd8da262cbb958639a96bedba10e", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d1b6f8fce682505dbd048977f1abedf1b8ad3ff8", + "reference": "d1b6f8fce682505dbd048977f1abedf1b8ad3ff8", "shasum": "" }, "require": { - "nikic/php-parser": "^5.7.0", - "php": ">=8.3" + "nikic/php-parser": "^5.8.0", + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.5.25" + "phpunit/phpunit": "^13.2.4" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "4.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -10008,7 +10036,7 @@ "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" + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/5.0.2" }, "funding": [ { @@ -10028,34 +10056,34 @@ "type": "tidelift" } ], - "time": "2026-05-19T16:22:07+00:00" + "time": "2026-07-09T08:42:34+00:00" }, { "name": "sebastian/object-enumerator", - "version": "7.0.0", + "version": "8.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" + "reference": "b39ab125fd9a7434b0ecbc4202eebce11a98cfc5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", - "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/b39ab125fd9a7434b0ecbc4202eebce11a98cfc5", + "reference": "b39ab125fd9a7434b0ecbc4202eebce11a98cfc5", "shasum": "" }, "require": { - "php": ">=8.3", - "sebastian/object-reflector": "^5.0", - "sebastian/recursion-context": "^7.0" + "php": ">=8.4", + "sebastian/object-reflector": "^6.0", + "sebastian/recursion-context": "^8.0" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -10078,40 +10106,52 @@ "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" + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/8.0.0" }, "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/object-enumerator", + "type": "tidelift" } ], - "time": "2025-02-07T04:57:48+00:00" + "time": "2026-02-06T04:46:36+00:00" }, { "name": "sebastian/object-reflector", - "version": "5.0.0", + "version": "6.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "4bfa827c969c98be1e527abd576533293c634f6a" + "reference": "3ca042c2c60b0eab094f8a1b6a7093f4d4c72200" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", - "reference": "4bfa827c969c98be1e527abd576533293c634f6a", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/3ca042c2c60b0eab094f8a1b6a7093f4d4c72200", + "reference": "3ca042c2c60b0eab094f8a1b6a7093f4d4c72200", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.0" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "5.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -10134,40 +10174,52 @@ "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" + "source": "https://github.com/sebastianbergmann/object-reflector/tree/6.0.0" }, "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/object-reflector", + "type": "tidelift" } ], - "time": "2025-02-07T04:58:17+00:00" + "time": "2026-02-06T04:47:13+00:00" }, { "name": "sebastian/recursion-context", - "version": "7.0.1", + "version": "8.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" + "reference": "32dba72f2b4642d6a93db22d6c0a9280ff2e3ca0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", - "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/32dba72f2b4642d6a93db22d6c0a9280ff2e3ca0", + "reference": "32dba72f2b4642d6a93db22d6c0a9280ff2e3ca0", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.0" + "phpunit/phpunit": "^13.2.6" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "7.0-dev" + "dev-main": "8.0-dev" } }, "autoload": { @@ -10198,7 +10250,7 @@ "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" + "source": "https://github.com/sebastianbergmann/recursion-context/tree/8.0.1" }, "funding": [ { @@ -10218,32 +10270,32 @@ "type": "tidelift" } ], - "time": "2025-08-13T04:44:59+00:00" + "time": "2026-08-03T05:58:12+00:00" }, { "name": "sebastian/type", - "version": "6.0.4", + "version": "7.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/type.git", - "reference": "82ff822c2edc46724be9f7411d3163021f602773" + "reference": "fee0309275847fefd7636167085e379c1dbf6990" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/82ff822c2edc46724be9f7411d3163021f602773", - "reference": "82ff822c2edc46724be9f7411d3163021f602773", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/fee0309275847fefd7636167085e379c1dbf6990", + "reference": "fee0309275847fefd7636167085e379c1dbf6990", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "require-dev": { - "phpunit/phpunit": "^12.5.25" + "phpunit/phpunit": "^13.1.10" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -10267,7 +10319,7 @@ "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" + "source": "https://github.com/sebastianbergmann/type/tree/7.0.1" }, "funding": [ { @@ -10287,29 +10339,29 @@ "type": "tidelift" } ], - "time": "2026-05-20T06:45:45+00:00" + "time": "2026-05-20T06:49:11+00:00" }, { "name": "sebastian/version", - "version": "6.0.0", + "version": "7.0.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/version.git", - "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" + "reference": "ad37a5552c8e2b88572249fdc19b6da7792e021b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", - "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/ad37a5552c8e2b88572249fdc19b6da7792e021b", + "reference": "ad37a5552c8e2b88572249fdc19b6da7792e021b", "shasum": "" }, "require": { - "php": ">=8.3" + "php": ">=8.4" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "6.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -10333,15 +10385,27 @@ "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" + "source": "https://github.com/sebastianbergmann/version/tree/7.0.0" }, "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/version", + "type": "tidelift" } ], - "time": "2025-02-07T05:00:38+00:00" + "time": "2026-02-06T04:52:52+00:00" }, { "name": "staabm/side-effects-detector", @@ -10397,16 +10461,16 @@ }, { "name": "symfony/yaml", - "version": "v8.1.1", + "version": "v8.1.2", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "8e4cdd4311683516be06944f4b85244063cdb886" + "reference": "faabdbe998e8c5c599dceffa27aa265b185c0736" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/8e4cdd4311683516be06944f4b85244063cdb886", - "reference": "8e4cdd4311683516be06944f4b85244063cdb886", + "url": "https://api.github.com/repos/symfony/yaml/zipball/faabdbe998e8c5c599dceffa27aa265b185c0736", + "reference": "faabdbe998e8c5c599dceffa27aa265b185c0736", "shasum": "" }, "require": { @@ -10449,7 +10513,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v8.1.1" + "source": "https://github.com/symfony/yaml/tree/v8.1.2" }, "funding": [ { @@ -10469,7 +10533,7 @@ "type": "tidelift" } ], - "time": "2026-06-09T11:06:24+00:00" + "time": "2026-07-22T15:42:13+00:00" }, { "name": "ta-tikoma/phpunit-architecture-test", @@ -10651,8 +10715,8 @@ { "package": "nativephp/mobile", "version": "dev-main", - "alias": "4.0.x-dev", - "alias_normalized": "4.0.9999999.9999999-dev" + "alias": "4.9.9", + "alias_normalized": "4.9.9.0" } ], "minimum-stability": "dev", diff --git a/config/native-ui.php b/config/native-ui.php index 07440f0..7968f81 100644 --- a/config/native-ui.php +++ b/config/native-ui.php @@ -54,12 +54,18 @@ 'on-surface-variant' => 'slate-800', // Outline = neutral borders (text fields, dividers, cards). + // outline-variant = softer edges: hairline dividers, card seams. 'outline' => 'slate-200', + 'outline-variant' => 'slate-100', // Destructive / error actions and messages. 'destructive' => 'red-600', 'on-destructive' => 'white', + // Success / "all systems go" — confirmations, verified badges. + 'success' => 'emerald-600', + 'on-success' => 'white', + // Tertiary accent — for highlights, badges, emphasis not covered by primary. 'accent' => 'yellow-400', 'on-accent' => 'white', @@ -68,6 +74,7 @@ // Matched to nativephp.com's dark theme: #050714 page, slate-950 cards, // "cloud" navy (#2B2E53) tonal fills, violet-500 accents, teal #3EDAD7. 'dark' => [ + 'shane' => 'yellow-400', 'primary' => 'violet-500', 'on-primary' => 'white', @@ -83,10 +90,14 @@ 'on-surface-variant' => 'gray-400', 'outline' => 'slate-700', + 'outline-variant' => 'slate-800', 'destructive' => 'red-500', 'on-destructive' => 'white', + 'success' => 'emerald-400', + 'on-success' => '#052E16', + 'accent' => '#3EDAD7', 'on-accent' => '#050714', ], @@ -103,7 +114,16 @@ 'font-lg' => 20, 'font-xl' => 24, - 'font-family' => 'Geist+Pixel-Regular', + ], + + 'fonts' => [ + 'default' => 'Geist+Pixel-Regular', + 'accent' => 'Lobster-Regular', + + // Theme Lab display faces. + 'display' => 'Unbounded-Bold', + 'grotesk' => 'SpaceGrotesk-Regular', + 'grotesk-bold' => 'SpaceGrotesk-Bold', ], ]; diff --git a/config/view.php b/config/view.php new file mode 100644 index 0000000..52fb738 --- /dev/null +++ b/config/view.php @@ -0,0 +1,40 @@ + [ + resource_path('views'), + ], + + /* + |-------------------------------------------------------------------------- + | Compiled View Path + |-------------------------------------------------------------------------- + | + | This option determines where all the compiled Blade templates will be + | stored for your application. Unlike the framework default this does not + | wrap the path in realpath(), which returns false when the directory is + | absent and leaves Blade with a null cache path. The NativePHP build + | excludes storage/framework when it copies the app, so the directory does + | not exist when composer install boots the app inside the platform build + | directory. Blade creates the directory itself on first write. + | + */ + + 'compiled' => env( + 'VIEW_COMPILED_PATH', + storage_path('framework/views') + ), + +]; diff --git a/resources/animations/jump.lottie b/resources/animations/jump.lottie new file mode 100644 index 0000000..a0b4811 Binary files /dev/null and b/resources/animations/jump.lottie differ diff --git a/resources/animations/jump2.lottie b/resources/animations/jump2.lottie new file mode 100644 index 0000000..c2f79a4 Binary files /dev/null and b/resources/animations/jump2.lottie differ diff --git a/resources/fonts/SpaceGrotesk-Bold.ttf b/resources/fonts/SpaceGrotesk-Bold.ttf new file mode 100644 index 0000000..f4f8002 Binary files /dev/null and b/resources/fonts/SpaceGrotesk-Bold.ttf differ diff --git a/resources/fonts/SpaceGrotesk-Regular.ttf b/resources/fonts/SpaceGrotesk-Regular.ttf new file mode 100644 index 0000000..576f9b5 Binary files /dev/null and b/resources/fonts/SpaceGrotesk-Regular.ttf differ diff --git a/resources/fonts/Unbounded-Bold.ttf b/resources/fonts/Unbounded-Bold.ttf new file mode 100644 index 0000000..d4bb398 Binary files /dev/null and b/resources/fonts/Unbounded-Bold.ttf differ diff --git a/resources/views/native/_vibe-account.blade.php b/resources/views/native/_vibe-account.blade.php deleted file mode 100644 index 65653f0..0000000 --- a/resources/views/native/_vibe-account.blade.php +++ /dev/null @@ -1,14 +0,0 @@ -@if(! empty($vibeIdentity ?? null)) - - - - - Signed in as - {{ $vibeIdentity }} - - - - Log out - - -@endif diff --git a/resources/views/native/animate.blade.php b/resources/views/native/animate.blade.php index a631e54..739c2a9 100644 --- a/resources/views/native/animate.blade.php +++ b/resources/views/native/animate.blade.php @@ -28,13 +28,13 @@ class="flex-1 h-[120] rounded-2xl items-center justify-center bg-theme-surface-v - + Secondary - + Destructive - + Accent - + Ghost - + @@ -43,19 +43,19 @@ Small - + Medium - + Large - + @@ -66,19 +66,19 @@ With leading icon - + With trailing icon - + Loading - + @@ -90,31 +90,31 @@ glass - + glass:prominent - + glass:prominent + destructive - + glass:prominent + accent - + glass:prominent:interactive - + @@ -131,7 +131,7 @@ Pressable + interactive - + Tap me @@ -145,7 +145,7 @@ Glass · clear:interactive (pressable) - + Tap @@ -201,6 +201,5 @@ class="w-[2400] h-[1600]"/> @endios - diff --git a/resources/views/native/checkbox.blade.php b/resources/views/native/checkbox.blade.php index b5800ae..784e8dc 100644 --- a/resources/views/native/checkbox.blade.php +++ b/resources/views/native/checkbox.blade.php @@ -1,5 +1,5 @@ @ios - + diff --git a/resources/views/native/compose-tweet.blade.php b/resources/views/native/compose-tweet.blade.php index 1d768d1..f9a9ff8 100644 --- a/resources/views/native/compose-tweet.blade.php +++ b/resources/views/native/compose-tweet.blade.php @@ -1,21 +1,20 @@ +@php + $hasText = trim($tweetText) !== ''; + $remaining = 280 - mb_strlen($tweetText); +@endphp + {{-- Top Bar --}} - - - - - + + Taps: {{ $tapCount }} Long: {{ $longPressCount }} - Sheet visible: {{ $sheetVisible ? 'YES' : 'NO' }} @@ -601,7 +601,7 @@ leadingIcon="settings" trailingIconButton="more_vert" headlineColor="{{ $fg }}" supportingColor="{{ $muted }}" @trailingPress="onTrailingPress"/> - @@ -657,9 +657,9 @@ dd() & Exceptions - - diff --git a/resources/views/native/doom-game.blade.php b/resources/views/native/doom-game.blade.php deleted file mode 100644 index 5b08972..0000000 --- a/resources/views/native/doom-game.blade.php +++ /dev/null @@ -1,141 +0,0 @@ - - - - - - - {{-- Title --}} - - DOOM - PHP Edition - - - Every enemy is an Eloquent model - - - - {{-- Server Status --}} - - - SERVER - - REFRESH - - - - - - - {{ $serverOnline ? 'ONLINE' : 'OFFLINE' }} - {{ config('doom.server_host', '127.0.0.1') }}:{{ config('doom.server_port', 9001) }} - - - - - {{-- Player Name --}} - - PLAYER NAME - {{ $playerName }} - - - - - {{-- Multiplayer --}} - - FIND MATCH - @if(!$serverOnline) - Server offline - @endif - - - {{-- Solo --}} - - SOLO (vs AI) - - - @if($matchStatus !== '') - - {{ $matchStatus }} - - @endif - - @if($isMultiplayer) - - DISCONNECT - - @endif - - - - {{-- Stats --}} - - - - CAREER - - - - - {{ $gamesPlayed }} - Games - - - {{ $totalKills }} - Kills - - - - - - @if($mpKills > 0 || $mpDeaths > 0) - {{-- PvP Stats --}} - - - LAST MATCH (PvP) - - - - {{ $mpKills }} - Kills - - - {{ $mpDeaths }} - Deaths - - - {{ $lastScore }} - Score - - - - - @elseif($lastScore > 0) - {{-- Solo Stats --}} - - - LAST GAME - - - - {{ $lastScore }} - Score - - - {{ $lastKills }} - Kills - - - - - @endif - - {{-- Sync Button --}} - - SYNC RESULTS - - - - - - - diff --git a/resources/views/native/drawer-demo.blade.php b/resources/views/native/drawer-demo.blade.php deleted file mode 100644 index 65380a3..0000000 --- a/resources/views/native/drawer-demo.blade.php +++ /dev/null @@ -1,34 +0,0 @@ - - {{-- Header. The left padding (pl-16) clears the drawer host's floating - ☰ button, which is drawn as an overlay at the top-leading corner. --}} - - - {{ $mode === 'reveal' ? 'Reveal' : 'Modal' }} Drawer - - - - - - - Open the drawer with the ☰ button (top-left) or swipe in from the left edge. - - - - - {{ $mode === 'reveal' - ? 'Reveal mode pushes this content aside to expose the drawer sitting behind it.' - : 'Modal mode slides the drawer over this content with a dim scrim.' }} - - - - - Declared once - - The same drawer is defined on DrawerLayout::drawer() and appears on every screen - routed under it — no per-screen markup. Switch between Modal and Reveal from inside - the drawer to see both presentations share one declaration. - - - - - diff --git a/resources/views/native/drawer-menu.blade.php b/resources/views/native/drawer-menu.blade.php deleted file mode 100644 index 13f1000..0000000 --- a/resources/views/native/drawer-menu.blade.php +++ /dev/null @@ -1,31 +0,0 @@ -{{-- Arbitrary drawer content — any elements you want. Declared once on - DrawerLayout::drawer() and rendered into the side drawer on every screen. --}} - - {{-- Header --}} - - - - - Ada Lovelace - Side drawer demo - - - {{-- Menu items — plain rows that navigate() --}} - - - - Modal drawer - - - - Reveal drawer - - - - - - - All demos - - - diff --git a/resources/views/native/event-channel-test.blade.php b/resources/views/native/event-channel-test.blade.php index ffe429b..02579da 100644 --- a/resources/views/native/event-channel-test.blade.php +++ b/resources/views/native/event-channel-test.blade.php @@ -34,7 +34,7 @@ starts with: {{ $sample }}… @endif - + Reset counter diff --git a/resources/views/native/explore-icons-row.blade.php b/resources/views/native/explore-icons-row.blade.php index 7f9acaf..80bdd41 100644 --- a/resources/views/native/explore-icons-row.blade.php +++ b/resources/views/native/explore-icons-row.blade.php @@ -4,7 +4,7 @@ @php($cases = array_slice($iconCatalog['cases'], $index * 3, 3)) @foreach ($cases as $case) - ` component — see - resources/views/components/layouts/app.blade.php published from the - nativephp/native-ui plugin. --}} +{{-- Page wrapper: scroll-view with theme.background + safe-area. --}} @@ -45,10 +42,10 @@ {{-- Overlays--}} {{-- --}} -{{-- --}} -{{-- --}} {{-- --}} @@ -59,7 +56,7 @@ {{-- This is a full-screen modal overlay. Tap the X or press back to dismiss.--}} {{-- --}} {{-- You can put any content here — forms, lists, images, anything.--}} -{{-- --}} {{-- --}} @@ -84,7 +81,7 @@ {{-- --}} {{-- --}} {{-- @endforeach--}} -{{-- --}} +{{-- --}} {{-- --}} {{-- --}} @@ -445,18 +442,18 @@ {{-- --}}{{-- Variants (primary / secondary / destructive / ghost) --}} {{-- Variants--}} {{-- --}} -{{-- --}} -{{-- --}} -{{-- --}} -{{-- --}} +{{-- --}} +{{-- --}} +{{-- --}} +{{-- --}} {{-- --}} {{-- --}}{{-- Sizes --}} {{-- Sizes--}} {{-- --}} -{{-- --}} -{{-- --}} -{{-- --}} +{{-- --}} +{{-- --}} +{{-- --}} {{-- --}} {{-- --}}{{-- Icons (leading + trailing) --}} @@ -467,26 +464,26 @@ {{-- through directly. --}} {{-- With icons--}} {{-- --}} -{{-- --}} -{{-- --}} -{{-- --}} +{{-- --}} +{{-- --}} +{{-- --}} {{-- --}} {{-- --}}{{-- Icon-only (needs a11y-label) --}} {{-- Icon-only--}} {{-- --}} -{{-- --}} -{{-- --}} -{{-- --}} +{{-- --}} +{{-- --}} +{{-- --}} {{-- --}} {{-- --}}{{-- Custom look — escape hatch via --}} @@ -494,19 +491,19 @@ {{-- Custom look (via pressable)--}} {{-- --}} {{-- --}} -{{-- --}} +{{-- --}} {{-- Pink Pill--}} {{-- --}} -{{-- --}} +{{-- --}} {{-- Teal Pill--}} {{-- --}} -{{-- --}} +{{-- --}} {{-- Rose Pill--}} {{-- --}} -{{-- --}} +{{-- --}} {{-- Outlined blue--}} {{-- --}} -{{-- --}} +{{-- --}} {{-- Outlined red--}} {{-- --}} {{-- --}} @@ -514,23 +511,23 @@ {{-- --}}{{-- Icon circles — still via pressable (custom shape + color) --}} {{-- --}} -{{-- --}} {{-- --}} {{-- --}} -{{-- --}} {{-- --}} {{-- --}} -{{-- --}} {{-- --}} {{-- --}} -{{-- --}} {{-- --}} {{-- --}} -{{-- --}} {{-- --}} {{-- --}} @@ -590,18 +587,18 @@ {{-- --}}{{-- ============================================= --}} {{-- Interactive Counter--}} {{-- --}} -{{-- --}} +{{-- --}} {{-- @if($lastButton)--}} {{-- You pressed: {{ $lastButton }}--}} {{-- @endif--}} @@ -1007,7 +1004,7 @@ {{-- ];--}} {{-- @endphp--}} {{-- @foreach ($checkboxRows as $row)--}} -{{-- --}} +{{-- --}} {{-- --}} {{-- 'team', 'label' => 'Team — $49/mo'],--}} {{-- ] as $row)--}} {{-- @php $checked = $pricingPlan === $row['value']; @endphp--}} -{{-- --}} +{{-- --}} {{-- --}} {{-- 'termsAccepted', 'label' => 'Terms accepted', 'icon' => 'check'],--}} {{-- ] as $row)--}} {{-- @php $sel = $this->{$row['field']}; @endphp--}} -{{-- --}} +{{-- --}} {{-- --}} {{-- --}} {{-- {{ $row['label'] }}--}} @@ -1207,7 +1204,7 @@ {{-- ['label' => 'About', 'icon' => 'info'],--}} {{-- ] as $i => $tab)--}} {{-- @php $isActive = $activeTab === $i; @endphp--}} -{{-- --}} +{{-- --}} {{-- --}} {{-- --}} {{-- --}} @@ -1237,7 +1234,7 @@ {{-- $isFirst = $i === 0;--}} {{-- $isLast = $i === count($planOptions) - 1;--}} {{-- @endphp--}} -{{-- --}} +{{-- --}} {{-- --}} {{-- {{ $opt }}--}} {{-- --}} @@ -1417,10 +1414,10 @@ {{-- clean architecture, Tailwind, and strong coffee.--}} {{-- --}} {{-- --}} -{{-- --}} +{{-- --}} {{-- Follow--}} {{-- --}} -{{-- --}} {{-- Message--}} {{-- --}} @@ -1454,7 +1451,7 @@ {{-- List Items--}} {{-- --}} {{-- --}}{{-- Item 1 --}} -{{-- --}} +{{-- --}} {{-- --}} {{-- --}} {{-- --}} @@ -1471,7 +1468,7 @@ {{-- --}} {{-- --}} {{-- --}}{{-- Item 2 --}} -{{-- --}} +{{-- --}} {{-- --}} {{-- --}} {{-- --}} @@ -1485,7 +1482,7 @@ {{-- --}} {{-- --}} {{-- --}}{{-- Item 3 --}} -{{-- --}} +{{-- --}} {{-- --}} {{-- --}} {{-- --}} @@ -1499,7 +1496,7 @@ {{-- --}} {{-- --}} {{-- --}}{{-- Item 4 --}} -{{-- --}} +{{-- --}} {{-- --}} {{-- --}} {{-- --}} @@ -1620,19 +1617,19 @@ {{-- Bottom Navigation --}} {{----}} -{{-- --}} +{{-- --}} {{-- --}} {{-- Home--}} {{-- --}} -{{-- --}} +{{-- --}} {{-- --}} {{-- Search--}} {{-- --}} -{{-- --}} +{{-- --}} {{-- --}} {{-- Favorites--}} {{-- --}} -{{-- --}} +{{-- --}} {{-- --}} {{-- Profile--}} {{-- --}} diff --git a/resources/views/native/explore/buttons.blade.php b/resources/views/native/explore/buttons.blade.php index d2fd552..7e9e938 100644 --- a/resources/views/native/explore/buttons.blade.php +++ b/resources/views/native/explore/buttons.blade.php @@ -4,34 +4,34 @@ {{-- Variants --}} Variants - - - - + + + + {{-- Sizes --}} Sizes - - - + + + {{-- With icons --}} With icons - - - + + + {{-- States --}} States - - - + + + @@ -39,36 +39,36 @@ {{-- Pressable escape hatch — bright accent pills stay vivid in both modes --}} Pressable (custom) - + Pink Pill - + Teal Pill - Outlined - - - - - @@ -80,11 +80,11 @@ class="w-[48] h-[48] rounded-full bg-amber-500 items-center justify-center"> Interactive Counter - - {{-- Favorite Button --}} - - - {{ $isFavorited ? 'Saved to favorites' : 'Save to favorites' }} - - - - - - - diff --git a/resources/views/native/ikea-search.blade.php b/resources/views/native/ikea-search.blade.php deleted file mode 100644 index 4458aed..0000000 --- a/resources/views/native/ikea-search.blade.php +++ /dev/null @@ -1,94 +0,0 @@ - - - {{-- Top Bar --}} - - - - - - Search - - - - {{-- Search Input --}} - - - - - {{-- Room Filter Chips --}} - - - @foreach ($categories as $cat) - - @endforeach - - - - - - {{-- Result Count --}} - - {{ $resultCount }} {{ $resultCount === 1 ? 'result' : 'results' }} - - - {{-- Product Results --}} - - - @foreach ($results as $product) - - - {{-- Product Image --}} - - - - - {{-- Product Info --}} - - @if ($product['badge']) - {{ $product['badge'] }} - @endif - {{ $product['seriesName'] }} - {{ $product['name'] }} - {{ $product['color'] }} - - {{ $product['priceFormatted'] }} - @if ($product['originalPriceFormatted']) - {{ $product['originalPriceFormatted'] }} - @endif - - - - {{ $product['rating'] }} ({{ $product['reviewFormatted'] }}) - - - - - @endforeach - - - @if (count($results) === 0) - - - - No products found - Try a different search or room filter - - @endif - - - diff --git a/resources/views/native/instagram-feed.blade.php b/resources/views/native/instagram-feed.blade.php index be57e07..f24816b 100644 --- a/resources/views/native/instagram-feed.blade.php +++ b/resources/views/native/instagram-feed.blade.php @@ -1,43 +1,51 @@ -{{-- @include('native.partials.demo-nav', ['title' => 'Instagram']) --}} - {{-- Header is provided by the framework NavBar (StackLayout) — back arrow + "Instagram" title. - Action icons (heart, chat) move into navigationOptions() on the screen if needed. --}} + Action icons (heart, chat) live in navigationOptions() on the component. --}} - - + + {{-- Stories --}} - + {{-- Your Story --}} - - - Your story - - Your story + + + + Your story + + + + + + + + Your story + {{-- Other Stories — `linear-gradient` via inline style doesn't - render natively; use a chunky 4-px Instagram-pink ring + render natively; use a chunky Instagram-pink ring as a single-color approximation of the gradient. --}} @foreach ($stories as $story) - - - - {{ $story['username'] }}'s story + + + + + {{ $story['username'] }}'s story + + {{ explode('.', $story['username'])[0] }} - {{ explode('.', $story['username'])[0] }} - + @endforeach @@ -45,84 +53,94 @@ class="w-[56] h-[56] rounded-full" {{-- Posts --}} - @foreach ($posts as $index => $post) - + @foreach ($posts as $post) + {{-- Post Header --}} - {{ $post['user']['username'] }}'s profile - - - {{ $post['user']['username'] }} - @if ($post['user']['isVerified']) - + + + {{ $post['user']['username'] }}'s profile + + + + + + {{ $post['user']['username'] }} + @if ($post['user']['isVerified']) + + @endif + + @if ($post['location']) + {{ $post['location'] }} @endif - - @if ($post['location']) - {{ $post['location'] }} - @endif - - + + + {{-- Post Image --}} - Photo by {{ $post['user']['username'] }}: {{ \Illuminate\Support\Str::limit($post['caption'], 60) }} + + Photo by {{ $post['user']['username'] }}: {{ \Illuminate\Support\Str::limit($post['caption'], 60) }} + - {{-- Action Bar --}} - + {{-- Action Bar — counts sit beside their icons, reels-era style --}} + - - - - - - - + + + + {{ $post['likesFormatted'] }} + + + + + + {{ $post['commentsFormatted'] }} + + + + + {{ $post['sharesFormatted'] }} + - + - + - {{-- Likes --}} - - {{ $post['likesFormatted'] }} likes - - {{-- Caption --}} - - {{ $post['user']['username'] }} {{ $post['caption'] }} + + {{ $post['user']['username'] }} {{ $post['caption'] }} {{-- View Comments --}} @if ($post['commentCount'] > 0) - - View all {{ number_format($post['commentCount']) }} comments - + + View all {{ number_format($post['commentCount']) }} comments + @endif {{-- Time --}} - - {{ $post['time'] }} ago + + {{ $post['time'] }} ago @endforeach @@ -130,4 +148,4 @@ class="w-full h-[375]" - + diff --git a/resources/views/native/instagram-post.blade.php b/resources/views/native/instagram-post.blade.php index 0f564b4..d1b1ec3 100644 --- a/resources/views/native/instagram-post.blade.php +++ b/resources/views/native/instagram-post.blade.php @@ -1,123 +1,147 @@ - - + - {{-- Top Bar --}} - - - - - Post - + {{-- Top Bar --}} + + + + + + {{ strtoupper($post['user']['username']) }} + Posts + + + + + + + - + {{-- Post Header --}} + + + + {{ $post['user']['username'] }}'s profile + + + + + + {{ $post['user']['username'] }} + @if ($post['user']['isVerified']) + + @endif + + @if ($post['location']) + {{ $post['location'] }} + @endif + + + + - {{-- Post Header --}} - + {{-- Post Image --}} {{ $post['user']['username'] }}'s profile - - - {{ $post['user']['username'] }} - @if ($post['user']['isVerified']) - - @endif - - @if ($post['location']) - {{ $post['location'] }} - @endif - - - - {{-- Post Image --}} - Photo by {{ $post['user']['username'] }}: {{ \Illuminate\Support\Str::limit($post['caption'], 60) }} - {{-- Action Bar --}} - - - + {{-- Action Bar --}} + + + + + + {{ $likesFormatted }} + + + + + {{ count($comments) }} + + + + - - - + - - + + {{-- Caption --}} + + {{ $post['user']['username'] }} {{ $post['caption'] }} - - {{-- Likes --}} - - {{ $likesFormatted }} likes - + {{-- Time --}} + + {{ $post['time'] }} ago + - {{-- Caption --}} - - {{ $post['user']['username'] }} {{ $post['caption'] }} - + - {{-- Time --}} - - {{ $post['time'] }} ago - + {{-- Comments --}} + + @foreach ($comments as $comment) + + + {{ $comment['user']['username'] }}'s profile + + + {{ $comment['user']['username'] }} {{ $comment['text'] }} + + {{ $comment['time'] }} + {{ $comment['likes'] }} likes + Reply + + + + + @endforeach + - + - {{-- Comments --}} - - @foreach ($comments as $comment) - - {{ $comment['user']['username'] }}'s profile - - {{ $comment['user']['username'] }} {{ $comment['text'] }} - - {{ $comment['time'] }} - {{ $comment['likes'] }} likes - Reply - - - - - @endforeach + - - - {{-- Comment Input --}} - + {{-- Pinned comment composer --}} + + + {{-- Emoji quick-reactions --}} + + @foreach (['❤️', '🙌', '🔥', '👏', '😢', '😍', '😮', '😂'] as $emoji) + {{ $emoji }} + @endforeach + + Your profile - Add a comment... + + Add a comment for {{ $post['user']['username'] }}... + - - - - + + diff --git a/resources/views/native/instagram-profile.blade.php b/resources/views/native/instagram-profile.blade.php index 502190d..74d3e8a 100644 --- a/resources/views/native/instagram-profile.blade.php +++ b/resources/views/native/instagram-profile.blade.php @@ -1,32 +1,31 @@ - - + + {{-- Top Bar --}} - - - + + + - {{ $user['username'] }} + {{ $user['username'] }} @if ($user['isVerified']) @endif - + {{-- Profile Header — gradient classes don't render natively; fall back to a solid Instagram-pink ring around the avatar. --}} - {{-- Avatar — 4-px Instagram-pink ring matches the feed stories. --}} - - + + {{ $user['username'] }} @@ -35,33 +34,43 @@ class="w-[76] h-[76] rounded-full" {{-- Stats — flex-1 row so each column splits remaining width evenly. --}} - {{ $postsFormatted }} - Posts + {{ $postsFormatted }} + posts - {{ $followersFormatted }} - Followers + {{ $followersFormatted }} + followers - {{ $followingFormatted }} - Following + {{ $followingFormatted }} + following {{-- Bio --}} - {{ $user['displayName'] }} - {{ $user['bio'] }} + {{ $user['displayName'] }} + {{ $user['bio'] }} @if ($user['website']) - {{ $user['website'] }} + + + {{ $user['website'] }} + @endif {{-- Action Buttons --}} - + - + diff --git a/resources/views/native/search-post-row.blade.php b/resources/views/native/search-post-row.blade.php index d6f24c0..ba60a1d 100644 --- a/resources/views/native/search-post-row.blade.php +++ b/resources/views/native/search-post-row.blade.php @@ -1,4 +1,4 @@ - + P diff --git a/resources/views/native/skia-showcase.blade.php b/resources/views/native/skia-showcase.blade.php deleted file mode 100644 index 9b7d142..0000000 --- a/resources/views/native/skia-showcase.blade.php +++ /dev/null @@ -1,342 +0,0 @@ -@include('native.partials.demo-nav', ['title' => 'Skia Canvas']) - - - - - {{-- 1. BASIC SHAPES --}} - Basic Shapes - - - - - - - - - - {{-- 2. LINES & PATHS --}} - Lines & Paths - - - - - - - - - - {{-- 3. TEXT --}} - Text - - - - - - - - - {{-- 4. GRADIENTS --}} - Gradients - - - - - - - - - - - - - - - {{-- 5. ANIMATIONS --}} - Animations - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{-- 6. TIMER RING --}} - Timer Ring - - @php $totalSec = $timerSeconds; @endphp - - - - - - - - - - - - - - - - - - - - - - - - - - @if($timerRunning) - - @else - - @endif - - - {{-- 7. BAR CHART --}} - Animated Bar Chart - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{-- 8. GAUGE --}} - Gauge - - - - - - - - - - - - - - - - - - {{-- 9. PIE CHART --}} - Pie Chart - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{-- 10. GROUP TRANSFORMS --}} - Group Transforms - - - - - - - - - - - - - - - - - - - - - - - - - - - {{-- 11. COLOR ANIMATION --}} - Color Animation - - - - - - - - - - - - - - - - - - - - - - - {{-- React Native Skia "Hello World" — overlapping circles with multiply blend --}} - Multiply Blend - - - - - - - - - - - {{-- Screen blend --}} - Screen Blend - - - - - - - - - - - {{-- Overlay blend --}} - Overlay Blend - - - - - - - - - - - - diff --git a/resources/views/native/spotify-artist.blade.php b/resources/views/native/spotify-artist.blade.php index 6445fa5..ba3af29 100644 --- a/resources/views/native/spotify-artist.blade.php +++ b/resources/views/native/spotify-artist.blade.php @@ -1,62 +1,77 @@ +{{-- Full-bleed artist header with the name overlaid at the bottom of + the image, like the app. Overlays use `absolute` with NON-ZERO + insets only — both stack renderers read a zero bottom/right inset + as "unset" and pin to the top/left. `shuffle` isn't in the iOS + Material→SF icon map, so shuffle affordances use `repeat`. --}} - + - {{-- Top Bar --}} - - - + {{-- Artist Image + overlaid name + floating back button --}} + + + {{ $artist['name'] }} + + + + {{ $artist['name'] }} - - {{-- Artist Image --}} - + {{-- LAST stack child so it draws (and hit-tests) on top --}} + + + + - {{-- Artist Info --}} - - {{ $artist['name'] }} + {{-- Listeners + Action Row --}} + {{ $listenersFormatted }} monthly listeners - {{-- Action Row --}} - - {{ $isFollowing ? 'Following' : 'Follow' }} - + + {{ $isFollowing ? 'Following' : 'Follow' }} + - - - - + + + + - {{-- Popular Tracks --}} - + {{-- Popular Tracks — ranked, like the app's artist page --}} + Popular @foreach ($tracksWithMeta as $trackIndex => $track) - - - {{ $trackIndex + 1 }} - - - {{ $track['title'] }} - {{ $track['playsFormatted'] }} plays - - - {{ $track['duration'] }} - - + + + + {{ $trackIndex + 1 }} + + + {{ $track['title'] }} + {{ $track['playsFormatted'] }} plays + + {{ $track['duration'] }} + + + @endforeach - + {{-- About --}} @@ -70,7 +85,7 @@ class="w-full h-[280]" - + diff --git a/resources/views/native/spotify-home.blade.php b/resources/views/native/spotify-home.blade.php index 9403084..6916ef1 100644 --- a/resources/views/native/spotify-home.blade.php +++ b/resources/views/native/spotify-home.blade.php @@ -1,34 +1,59 @@ -{{-- @include('native.partials.demo-nav', ['title' => 'Spotify']) --}} +{{-- Icon names must exist in the iOS Material→SF map + (IconHelper.swift) as well as the Android set — e.g. `shuffle` and + `notifications_none` are Android-only and render blank on iOS. --}} +{{-- Plain full-screen root (outside the NavBar stack group — see + routes/mobile.php): stack screens sit inside a NavigationStack whose + container background is the white systemBackground with no override, + which showed as a white bar in the bottom inset. As a plain screen + the root fills the window edge-to-edge, so `safe-area` pads the + content while the dark bg covers the insets. --}} + + - - - - {{-- Top Bar --}} - - Good evening - - - - - - - + {{-- Top Bar — back + filter chips + search. Chips are + flex-shrink-0 with single-line labels: when the row runs out + of width, flex shrink squeezes them and the text wraps + mid-word ("Podcast s"). Keep the item set narrow enough for + small phones. --}} + + + + + + All + + + Music + + + Podcasts + + + + + {{-- Recently Played Grid --}} - @foreach (array_chunk($recentlyPlayed, 2) as $row) + @foreach (array_chunk($recentlyPlayed, 2, true) as $chunk) - @foreach ($row as $index => $playlist) - - - {{ $playlist['name'] }} - + @foreach ($chunk as $index => $playlist) + + + {{ $playlist['name'] }} + {{ $playlist['name'] }} + + @endforeach + @if (count($chunk) === 1) + + @endif @endforeach @@ -40,59 +65,68 @@ class="w-[56] h-[56] rounded-l" @foreach ($madeForYou as $index => $playlist) - - - {{ $playlist['description'] }} - + + + {{ $playlist['name'] }} + {{ $playlist['description'] }} + + @endforeach {{-- Popular Artists --}} - + Popular artists @foreach ($artists as $artistIndex => $artist) - - - {{ $artist['name'] }} - Artist - + + + {{ $artist['name'] }} + {{ $artist['name'] }} + Artist + + @endforeach {{-- Your Top Mixes --}} - + Your top mixes @foreach (array_slice($recentlyPlayed, 0, 4) as $pIndex => $playlist) - - - {{ $playlist['name'] }} - {{ $playlist['creator'] }} - + + + {{ $playlist['name'] }} + {{ $playlist['name'] }} + {{ $playlist['creator'] }} + + @endforeach - + diff --git a/resources/views/native/spotify-playlist.blade.php b/resources/views/native/spotify-playlist.blade.php index 0b87ba7..e6e7c9e 100644 --- a/resources/views/native/spotify-playlist.blade.php +++ b/resources/views/native/spotify-playlist.blade.php @@ -1,9 +1,12 @@ +{{-- Icon names must exist in the iOS Material→SF map + (IconHelper.swift) as well as the Android set — `shuffle` is + Android-only, so shuffle affordances use the mapped `repeat`. --}} - + {{-- Top Bar --}} - + @@ -12,56 +15,59 @@ {{ $playlist['name'] }} {{-- Playlist Info --}} - {{ $playlist['name'] }} + {{ $playlist['name'] }} {{ $playlist['description'] }} - - {{ $playlist['creator'] }} - · {{ $playlist['trackCount'] }} songs + + + + {{ $playlist['creator'] }} + {{ $playlist['trackCount'] }} songs {{-- Action Row --}} - + + - - - - - + + + + + - {{-- Track List --}} + {{-- Track List — title/artist flex, like the app (no numbers on + playlist pages) --}} @foreach ($tracksWithMeta as $trackIndex => $track) - - - {{ $trackIndex + 1 }} - - - {{ $track['title'] }} - {{ $track['artistName'] }} · {{ $track['playsFormatted'] }} plays - - - {{ $track['duration'] }} - - + + + + {{ $track['title'] }} + {{ $track['artistName'] }} · {{ $track['playsFormatted'] }} plays + + {{ $track['duration'] }} + + + @endforeach - + diff --git a/resources/views/native/spotify-search.blade.php b/resources/views/native/spotify-search.blade.php index d206054..b71fc3f 100644 --- a/resources/views/native/spotify-search.blade.php +++ b/resources/views/native/spotify-search.blade.php @@ -1,42 +1,43 @@ - + {{-- Top Bar --}} - - - - - - Search - + + + + + Search {{-- Search Bar --}} - + What do you want to listen to? - {{-- Top Genres --}} + {{-- Browse all — flexed 2-up colored genre cards --}} Browse all - - @foreach (array_chunk(array_keys($genres), 2) as $row) - - @foreach ($row as $genreName) - + + @foreach (array_chunk(array_keys($genres), 2) as $chunk) + + @foreach ($chunk as $genreName) + {{ $genreName }} @endforeach + @if (count($chunk) === 1) + + @endif @endforeach - {{-- Top Results / Popular --}} + {{-- Popular playlists --}} Popular playlists @@ -44,14 +45,17 @@ @foreach (array_slice($playlists, 0, 4) as $pIndex => $playlist) - - - {{ $playlist['name'] }} - + + + {{ $playlist['name'] }} + {{ $playlist['name'] }} + + @endforeach @@ -63,21 +67,25 @@ class="w-[140] h-[140] rounded" @foreach (array_slice($artists, 0, 4) as $aIndex => $artist) - - - - {{ $artist['name'] }} - Artist · {{ $artist['genre'] }} - - + + + {{ $artist['name'] }} + + {{ $artist['name'] }} + Artist · {{ $artist['genre'] }} + + + + @endforeach - + diff --git a/resources/views/native/stack-positioning-demo.blade.php b/resources/views/native/stack-positioning-demo.blade.php new file mode 100644 index 0000000..9c07edd --- /dev/null +++ b/resources/views/native/stack-positioning-demo.blade.php @@ -0,0 +1,406 @@ +@php + // Outline colour for the container being measured, and for the anchored child. + $box = $showBounds ? 'border border-red-500' : ''; + $chip = $showBounds ? 'border border-blue-500' : ''; +@endphp + + + + + + Stack & Positioning + + Red outline = the container's measured box. Blue = the anchored child. + Compare iOS and Android side by side — sections marked ⚠️ are expected to differ today. + + + + + @if($show === [] || in_array(1, $show, true)) + {{-- ───────────────────────────────────────────────────────────── + 1. Stack sizing — does a bare stack wrap its largest child? + This is the headline claim: "the stack sizes to its largest + child (ZStack/wrap-content), so no explicit size is needed." + + iOS `sizeThatFits` does `width: proposal.width ?? maxWidth`. + A column parent proposes (width: crossAvail, height: nil), so + width is FINITE and the union is discarded → the stack fills + the column's width. Height has a nil proposal, so it wraps. + Android's Box carries no size modifier in WRAP mode → wraps + on BOTH axes. + + Expected on device: the red outline hugs the text on Android + and spans the full width on iOS. + ───────────────────────────────────────────────────────────────── --}} + + 1. Stack sizing — wrap vs fill ⚠️ + + A bare stack with one child. If wrap-content holds, the red box hugs the text. + + + + Pinkary + + + + The README example — a dot anchored to the wordmark's top-right corner, + drawing half outside it. If the stack fills instead of wrapping, the dot + lands at the far right of the screen rather than on the letter "y". + + + + Pinkary + + + + Same stack, but given an explicit size — should fill regardless. + + + Pinkary + + + + + @endif + + @if($show === [] || in_array(2, $show, true)) + {{-- ───────────────────────────────────────────────────────────── + 2. Attribute ↔ class parity. All three rows must be identical. + NOTE: the short attribute spellings are read by + buildLayoutArray(), which only runs for the six builtin + streaming types (column/row/stack/scroll_view/pressable/canvas). + The row below therefore does NOT position — it's here + deliberately to show the gap. + ───────────────────────────────────────────────────────────────── --}} + + 2. Attribute ↔ class parity + Three spellings of the same thing — the blue chips must land identically. + + + + + + + + + + + + + + class="absolute top-0 right-0" + absolute top="0" right="0" + position="absolute" :top :right + + + + ⚠️ Same short attributes on a non-container element. `applyLayout()` (the Element + path) never learned them, so this one does NOT move — it should sit top-left. + + + text absolute + + + + @endif + + @if($show === [] || in_array(3, $show, true)) + {{-- ───────────────────────────────────────────────────────────── + 3. All nine anchors, origin defaulting to center. + The child's CENTRE lands on the parent's anchor point, so + every chip draws half outside the box. That's intended. + ───────────────────────────────────────────────────────────────── --}} + + 3. Nine anchors (origin = center) + + Child centre pinned to the parent point, so each chip straddles the edge. + + + @foreach($this->pointRows() as $rowIndex => $row) + + @foreach($row as $point) + + + + + {{ $point['name'] }} ({{ $point['enum'] }}) + + @endforeach + + @endforeach + + + @endif + + @if($show === [] || in_array(4, $show, true)) + {{-- ───────────────────────────────────────────────────────────── + 4. All nine origins with the anchor pinned to center. The chip + pivots around the parent's centre point. + ───────────────────────────────────────────────────────────────── --}} + + 4. Nine origins (anchor = center) + + Which point ON THE CHILD lands on the parent's centre. origin="top-left" + pushes the chip down-right; origin="bottom-right" pushes it up-left. + + + @foreach($this->pointRows() as $rowIndex => $row) + + @foreach($row as $point) + + + + + {{ $point['name'] }} + + @endforeach + + @endforeach + + + @endif + + @if($show === [] || in_array(5, $show, true)) + {{-- ───────────────────────────────────────────────────────────── + 5. The two-point combos that motivate the feature — a badge + hooked onto a corner three different ways. + ───────────────────────────────────────────────────────────────── --}} + + 5. Two-point badge combos + Same anchor (top-right), three different origins. + + + + + + + origin=top-right + fully inside + + + + + + origin=center + half outside (default) + + + + + + origin=bottom-left + fully outside + + + + + @endif + + @if($show === [] || in_array(6, $show, true)) + {{-- ───────────────────────────────────────────────────────────── + 6. Utility-class form must equal the attribute form. + ───────────────────────────────────────────────────────────────── --}} + + 6. anchor-* / origin-* classes + Class form vs attribute form — these two must be identical. + + + + + + + classes + + + + + + attributes + + + + + + mixed + + + + + @endif + + @if($show === [] || in_array(7, $show, true)) + {{-- ───────────────────────────────────────────────────────────── + 7. Insets as a nudge on top of the anchor. + + iOS placeAbsolute (two-point branch): x += left - right + Android stack: offsetX = if (right > 0) -right else left + + Identical when only one side is set. They diverge when BOTH + are set (iOS +6, Android −4 from centre) and on a negative + `right` (iOS shifts +6, Android ignores it entirely). + ───────────────────────────────────────────────────────────────── --}} + + 7. Inset nudge on an anchor ⚠️ + + Anchor is center for all four. The last two are where iOS and Android disagree. + + + + + + + + left=10 + agree + + + + + + right=4 + agree + + + + + + left=10 right=4 + iOS +6 / Android −4 + + + + + + right=-6 + iOS +6 / Android 0 + + + + + @endif + + @if($show === [] || in_array(8, $show, true)) + {{-- ───────────────────────────────────────────────────────────── + 8. Regression guard — a NON-stack container with absolute + children and no anchor/origin must keep the legacy + inset-corner behaviour byte for byte. The fourth chip opts + into the two-point model from inside a non-stack parent. + ───────────────────────────────────────────────────────────────── --}} + + 8. Legacy absolute (non-stack) — unchanged + + No anchor/origin anywhere, so these keep the historical corner logic. + + + + + + + + + + + Opting in from a non-stack parent — this one should centre, not corner. + + + + + + + @endif + + @if($show === [] || in_array(9, $show, true)) + {{-- ───────────────────────────────────────────────────────────── + 9. z-order should follow document order — first child at the + back. Offsets are staggered so the overlap is visible. + ───────────────────────────────────────────────────────────────── --}} + + 9. z-order = document order + Red is first in the markup, so it should be at the back; blue on top. + + + + + + + + + @endif + + @if($show === [] || in_array(10, $show, true)) + {{-- ───────────────────────────────────────────────────────────── + 10. A misspelt anchor resolves to null and the prop is dropped + silently, so the child falls back to centre. Both chips below + should look the same — which is exactly the problem. + ───────────────────────────────────────────────────────────────── --}} + + 10. Unknown anchor name ⚠️ + + A typo is dropped silently and centres. These two are indistinguishable at runtime. + + + + + + + + anchor="top-rigth" (typo) + + + + + + anchor="center" + + + + + @endif + + @if($show === [] || in_array(11, $show, true)) + {{-- Diagnostic: isolate WHY anchors centre inside a stack. --}} + + 11. Diagnostic + All four ask for top-right. Which ones obey? + + + + + + + A stack+anchor + + + + + + B stack+abs+anchor + + + + + + C col+abs+anchor + + + + + + D stack+legacy + + + + Two flow children, no anchor — does the stack overlay them? + + Pinkary + XX + + + + @endif + + + + diff --git a/resources/views/native/syncup-native/chat.blade.php b/resources/views/native/syncup-native/chat.blade.php index bd3c158..42f826f 100644 --- a/resources/views/native/syncup-native/chat.blade.php +++ b/resources/views/native/syncup-native/chat.blade.php @@ -113,7 +113,7 @@ class="glass:interactive android:dark:bg-white text-slate-700 rounded-full p-1 i @else - @@ -124,7 +124,7 @@ class="glass:interactive android:dark:bg-white text-slate-700 rounded-full p-1 {{-- More-actions modal — opened by the NavBar ellipsis. Dismissible. --}} - + @@ -137,7 +137,7 @@ class="text-[12] text-theme-on-surface-variant">{{ $isMuted ? 'You will hear not - + @@ -149,7 +149,7 @@ class="text-[12] text-theme-on-surface-variant">{{ $isMuted ? 'You will hear not - + Cancel @@ -165,11 +165,11 @@ class="text-[12] text-theme-on-surface-variant">{{ $isMuted ? 'You will hear not There's no undo. - Cancel - Delete diff --git a/resources/views/native/syncup/chat.blade.php b/resources/views/native/syncup/chat.blade.php deleted file mode 100644 index e021e40..0000000 --- a/resources/views/native/syncup/chat.blade.php +++ /dev/null @@ -1,120 +0,0 @@ - - - {{-- Message thread --}} - - - - {{-- Date divider --}} - - - TODAY - - - - {{-- Messages --}} - @foreach ($messages as $m) - @if ($m['fromMe']) - {{-- Outgoing — primary bubble, right-aligned --}} - - - {{ $m['text'] }} - - {{ $m['time'] }} - @if (($m['status'] ?? null) === 'read') - - @elseif (($m['status'] ?? null) === 'sent') - - @endif - - - - @else - {{-- Incoming — light gray bubble, left-aligned --}} - - - @if (! empty($m['imageUrl'])) - - @endif - {{ $m['text'] }} - - {{ $m['time'] }} - - - - @endif - @endforeach - - {{-- Typing indicator --}} - - - - - - - - - - - - - - - - {{-- More-actions modal — opened by the NavBar ellipsis. Dismissible - (tap backdrop to close). --}} - - - - - - - {{ $isMuted ? 'Unmute notifications' : 'Mute notifications' }} - {{ $isMuted ? 'You will hear notifications again.' : 'No banners or sounds for this chat.' }} - - - - - - - - - Clear history - Removes every message in this chat. Cannot be undone. - - - - - - - Cancel - - - - - - {{-- Blocking clear-history confirmation. dismissible=false so the user - must explicitly choose. --}} - - - Clear chat history? - This permanently deletes the message thread. There's no undo. - - - Cancel - - - Delete - - - - - - diff --git a/resources/views/native/syncup/chats.blade.php b/resources/views/native/syncup/chats.blade.php index 97065d8..d88bee2 100644 --- a/resources/views/native/syncup/chats.blade.php +++ b/resources/views/native/syncup/chats.blade.php @@ -21,7 +21,7 @@ class="flex-1 bg-white" @foreach ($filters as $name) @php $active = $activeFilter === $name; @endphp {{ $name }} @@ -42,7 +42,7 @@ class="px-5 py-2 rounded-full {{ $active ? 'bg-[#00b4d8]' : 'bg-theme-surface-va keep per-row state (any pending swipe, animations, focus) attached to the right chat instead of bleeding to whatever chat happens to occupy the same screen position next frame. --}} - + {{-- Avatar (or group icon) with status dot --}} @if ($row['isGroup']) @@ -99,7 +99,7 @@ class="px-5 py-2 rounded-full {{ $active ? 'bg-[#00b4d8]' : 'bg-theme-surface-va {{ $s['name'] }} - + Add @@ -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. --}} - @@ -131,7 +131,7 @@ class="absolute bottom-[20] right-[20] w-[56] h-[56] rounded-full shadow-xl bg-c @foreach ($friends as $f) - + @if ($f['status'] === 'online') diff --git a/resources/views/native/syncup/friends.blade.php b/resources/views/native/syncup/friends.blade.php index f1c85ad..4f9912d 100644 --- a/resources/views/native/syncup/friends.blade.php +++ b/resources/views/native/syncup/friends.blade.php @@ -8,7 +8,7 @@ {{-- Top row: QR + Find by ID --}} {{-- QR Code card --}} - + @@ -16,7 +16,7 @@ {{-- Find by ID card --}} - + @@ -35,7 +35,7 @@ syncup.me/u/johndoe - + Copy @@ -65,7 +65,7 @@ {{ $f['statusText'] }} - + @@ -85,7 +85,7 @@ {{ $s['name'] }} {{ $s['mutuals'] }} mutual friends - + Add diff --git a/resources/views/native/syncup/login.blade.php b/resources/views/native/syncup/login.blade.php index 746cc39..d90b365 100644 --- a/resources/views/native/syncup/login.blade.php +++ b/resources/views/native/syncup/login.blade.php @@ -32,7 +32,7 @@ class="flex-1" Password - Forgot? + Forgot? @@ -44,14 +44,14 @@ class="flex-1" :variant="0" class="flex-1" /> - + {{-- Login button --}} - + Login @@ -64,10 +64,10 @@ class="flex-1" {{-- Social buttons --}} - + Google - + Apple @@ -77,11 +77,11 @@ class="flex-1" Don't have an account? - Create Account + Create Account {{-- Demo-only: skip past login and jump straight into the app. --}} - Skip login → + Skip login → Privacy Policy diff --git a/resources/views/native/syncup/profile.blade.php b/resources/views/native/syncup/profile.blade.php index 971bdf2..ea3d464 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 --}} - + @@ -66,7 +66,7 @@ class="w-full" {{-- Share Link primary --}} - + Share Link @@ -81,19 +81,19 @@ class="w-full" {{-- Copy field --}} syncup.me/elena_rod - + {{-- Save button --}} - + {{ $saved ? 'Saved ✓' : 'Save Changes' }} {{-- Sign out --}} - + Sign out diff --git a/resources/views/native/test-layout.blade.php b/resources/views/native/test-layout.blade.php deleted file mode 100644 index b15958a..0000000 --- a/resources/views/native/test-layout.blade.php +++ /dev/null @@ -1,21 +0,0 @@ - - - Main area - - - - - - - - - - - diff --git a/resources/views/native/testing.blade.php b/resources/views/native/testing.blade.php deleted file mode 100644 index e572f1a..0000000 --- a/resources/views/native/testing.blade.php +++ /dev/null @@ -1,3 +0,0 @@ - - Testing - \ No newline at end of file diff --git a/resources/views/native/theme-lab.blade.php b/resources/views/native/theme-lab.blade.php new file mode 100644 index 0000000..6610bba --- /dev/null +++ b/resources/views/native/theme-lab.blade.php @@ -0,0 +1,134 @@ +@use('App\Icons\Ios') +@use('App\Icons\Android') + + + + + + {{-- ── Hero ────────────────────────────────────────────── --}} + + THEME LAB + + One config file drives every color, radius, and typeface on this screen. + Flip system dark mode right now — every token re-resolves live. + + + + {{-- ── Interactivity proof ─────────────────────────────── --}} + + Button presses recorded + + {{ $presses }} + + + {{-- ── Theme token swatches ────────────────────────────── --}} + Theme Tokens + + @foreach ($this->swatchTokens() as $token => $label) + + {{ $label }} + + bg-theme-{{ $token }} + + @endforeach + + + {{-- ── Outline vs outline-variant ──────────────────────── --}} + Outline vs Outline-Variant + + + outline + Field & card borders + + + outline-variant + Hairline seams + + + + {{-- ── Opacity modifiers on theme classes ──────────────── --}} + Opacity Ramps + + + @foreach ($this->opacitySteps() as $step) + + /{{ $step }} + + @endforeach + + + @foreach ($this->opacitySteps() as $step) + + /{{ $step }} + + @endforeach + + + bg-theme-primary/N and bg-theme-success/N — tonal fills straight from the tokens, + dark companions included. + + + + {{-- ── Button variants (incl. success) ─────────────────── --}} + Button Variants + + @foreach ($this->buttonVariants() as $buttonVariant) + + variant="{{ $buttonVariant }}" + + @endforeach + + success + disabled + + + success + glass:prominent (iOS 26) + + + + {{-- ── Badge variants (incl. success) ──────────────────── --}} + Badge Variants + + @foreach ($this->badgeVariants() as $badgeVariant) + + + + + @endforeach + + + {{-- ── Status pattern from the success token ───────────── --}} + Status Patterns + + + + ALL SYSTEMS GO + + + text-theme-success + status bar + + + + + LIVE + + + + + {{-- ── Font aliases ────────────────────────────────────── --}} + Font Aliases + + @foreach ($this->fontSamples() as $alias => $sample) + + font="{{ $alias }}" + {{ $sample }} + + @endforeach + + Semantic names from config/native-ui.php — swap a typeface app-wide with a one-line change. + + + + + diff --git a/resources/views/native/tweet-detail.blade.php b/resources/views/native/tweet-detail.blade.php index f9c613e..00b065d 100644 --- a/resources/views/native/tweet-detail.blade.php +++ b/resources/views/native/tweet-detail.blade.php @@ -1,136 +1,174 @@ - - + - {{-- Top Bar --}} - - - - - Post - - - - - {{-- Author Info --}} - - {{ $tweet['user']['name'] }}'s profile - - - {{ $tweet['user']['name'] }} - @if ($tweet['user']['isVerified']) - - @endif - - {{ $tweet['user']['handle'] }} - - + {{-- Top Bar --}} + + + + + Post + - {{-- Tweet Text --}} - - {{ $tweet['text'] }} - + - {{-- Optional Image --}} - @if ($tweet['imageUrl']) - - Photo by {{ $tweet['user']['name'] }} - - @endif + + - {{-- Timestamp --}} - - {{ $tweet['time'] }} ago - - - - - {{-- Engagement Stats --}} - - - {{ $retweetFormatted }} - Reposts - - - {{ $likeFormatted }} - Likes + {{-- Author Info --}} + + + {{ $tweet['user']['name'] }}'s profile + + + + + {{ $tweet['user']['name'] }} + @if ($tweet['user']['isVerified']) + + @endif + + {{ $tweet['user']['handle'] }} + + + - - - - {{-- Action Bar --}} - - - - - - - + {{-- Tweet Text --}} + + {{ $tweet['text'] }} - - - - - - {{-- Replies --}} - @foreach ($replies as $reply) - - - {{-- Reply Avatar --}} + {{-- Optional Image --}} + @if ($tweet['imageUrl']) + {{ $reply['user']['name'] }}'s profile + + @endif + + {{-- Timestamp + Views --}} + + {{ $tweet['time'] }} ago + · + {{ $viewFormatted }} + Views + - {{-- Reply Content --}} - - - {{ $reply['user']['name'] }} - @if ($reply['user']['isVerified']) - - @endif - {{ $reply['user']['handle'] }} - · {{ $reply['time'] }} - + + + {{-- Engagement Stats --}} + + + {{ $retweetFormatted }} + Reposts + + + {{ $likeFormatted }} + Likes + + + {{ $bookmarkFormatted }} + Bookmarks + + + + + + {{-- Action Bar --}} + + + + + + + + + + + + + - {{ $reply['text'] }} + + + {{-- Replies --}} + @foreach ($replies as $reply) + + + + {{ $reply['user']['name'] }}'s profile + + + + + {{ $reply['user']['name'] }} + @if ($reply['user']['isVerified']) + + @endif + {{ $reply['user']['handle'] }} · {{ $reply['time'] }} + - {{-- Reply Actions --}} - - - - - {{ $reply['likeFormatted'] }} + Replying to + {{ $tweet['user']['handle'] }} - - - - - - - @endforeach - + {{ $reply['text'] }} + + {{-- Reply Actions --}} + + + + + + {{ $reply['likeFormatted'] }} + + + + + + + + @endforeach + + + + + + + {{-- Pinned reply composer --}} + + + Your profile + + Post your reply + + - - + diff --git a/resources/views/native/twitter-feed.blade.php b/resources/views/native/twitter-feed.blade.php index 482d3be..13e69ba 100644 --- a/resources/views/native/twitter-feed.blade.php +++ b/resources/views/native/twitter-feed.blade.php @@ -1,99 +1,118 @@ -{{--@include('native.partials.demo-nav', ['title' => 'Twitter / X'])--}} + - - + + - {{-- Top Bar --}} - - Your profile - 𝕏 - - - - + {{-- Top Bar --}} + + Your profile + 𝕏 + + + + - {{-- Tab Row --}} - - - - + {{-- Tab Row --}} + + + + - + - {{-- Tweet Feed --}} - @foreach ($tweets as $index => $tweet) - - - {{-- Avatar --}} - {{ $tweet['user']['name'] }}'s profile - - {{-- Tweet Content --}} - - {{-- Name Row --}} - - {{ $tweet['user']['name'] }} - @if ($tweet['user']['isVerified']) - - @endif - {{ $tweet['user']['handle'] }} - · {{ $tweet['time'] }} - - - {{-- Tweet Text --}} - {{ $tweet['text'] }} - - {{-- Optional Image --}} - @if ($tweet['imageUrl']) + {{-- Tweet Feed --}} + @foreach ($tweets as $tweet) + + + {{-- Avatar --}} + Photo by {{ $tweet['user']['name'] }} - @endif + - {{-- Action Bar --}} - - {{-- Reply --}} - - - {{ $tweet['replyFormatted'] }} - - {{-- Retweet --}} - - - {{ $tweet['retweetFormatted'] }} - - {{-- Like --}} - - - {{ $tweet['likeFormatted'] }} + {{-- Tweet Content --}} + + + + {{-- Name Row --}} + + {{ $tweet['user']['name'] }} + @if ($tweet['user']['isVerified']) + + @endif + {{ $tweet['user']['handle'] }} · {{ $tweet['time'] }} + + + + + {{-- Tweet Text --}} + {{ $tweet['text'] }} + + {{-- Optional Image --}} + @if ($tweet['imageUrl']) + Photo by {{ $tweet['user']['name'] }} + @endif + + + + {{-- Action Bar --}} + + {{-- Reply --}} + + + {{ $tweet['replyFormatted'] }} + + {{-- Repost --}} + + + {{ $tweet['retweetFormatted'] }} + + {{-- Like --}} + + + {{ $tweet['likeFormatted'] }} + + {{-- Views --}} + + + {{ $tweet['viewFormatted'] }} + + {{-- Share --}} + - {{-- Share --}} - - - - - - - @endforeach + + + + + @endforeach + + + + + - + {{-- Compose FAB — `absolute` insets, not stack anchor/origin props, + which the current runtime drops (see StackPositioningDemoTest). --}} + + + - - + diff --git a/resources/views/native/twitter-profile.blade.php b/resources/views/native/twitter-profile.blade.php index 43a3067..f345755 100644 --- a/resources/views/native/twitter-profile.blade.php +++ b/resources/views/native/twitter-profile.blade.php @@ -1,66 +1,87 @@ - {{-- Top Bar --}} - - - - - - {{ $user['name'] }} - {{ count($tweetsWithMeta) }} posts - - - - {{-- Banner + Avatar Overlap --}} + {{-- Banner + floating back button + overlapping avatar. + `absolute` insets, not stack anchor/origin props, which the + current runtime drops (see StackPositioningDemoTest). Both + stack renderers only anchor to the bottom/right edges when + that inset is NON-ZERO (`bottom-0` reads as "unset" and pins + to the top), and zero opposing insets don't stretch — so + layers anchor from the top with computed offsets and size + themselves explicitly. --}} - {{-- Banner layer --}} + {{-- Banner layer — sizes the stack: 150 banner + 44 breathing room --}} {{ $user['name'] }}'s banner - + {{-- flex-grow-0: spacer defaults to flex_grow=1, and the + flex measurer sizes ANY grow child at 0 under the + indefinite proposal a stack measures its layers with — + the fixed height only sticks on a non-grow child. --}} + - {{-- Avatar layer --}} - - - + + {{-- Avatar + follow row: 82 tall (74 avatar + 4 ring pad each + side), top inset 112 = 194 stack − 82 row → flush with the + stack bottom. --}} + + {{ $user['name'] }} -