diff --git a/AGENTS.md b/AGENTS.md index fbf2d02..29b3186 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -107,13 +107,6 @@ This project has domain-specific skills available in `**/skills/**`. You MUST ac - Laravel can be deployed using [Laravel Cloud](https://cloud.laravel.com/), which is the fastest way to deploy and scale production Laravel applications. -=== herd rules === - -# Laravel Herd - -- The application is served by Laravel Herd at `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate valid URLs. Never run commands to serve the site. It is always available. -- Use the `herd` CLI to manage services, PHP versions, and sites (e.g. `herd sites`, `herd services:start `, `herd php:list`). Run `herd list` to discover all available commands. - === tests rules === # Test Enforcement diff --git a/CLAUDE.md b/CLAUDE.md index fbf2d02..29b3186 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -107,13 +107,6 @@ This project has domain-specific skills available in `**/skills/**`. You MUST ac - Laravel can be deployed using [Laravel Cloud](https://cloud.laravel.com/), which is the fastest way to deploy and scale production Laravel applications. -=== herd rules === - -# Laravel Herd - -- The application is served by Laravel Herd at `https?://[kebab-case-project-dir].test`. Use the `get-absolute-url` tool to generate valid URLs. Never run commands to serve the site. It is always available. -- Use the `herd` CLI to manage services, PHP versions, and sites (e.g. `herd sites`, `herd services:start `, `herd php:list`). Run `herd list` to discover all available commands. - === tests rules === # Test Enforcement diff --git a/app/NativeComponents/Counter.php b/app/NativeComponents/Counter.php index aa792f7..db962d3 100644 --- a/app/NativeComponents/Counter.php +++ b/app/NativeComponents/Counter.php @@ -3,9 +3,9 @@ namespace App\NativeComponents; use Illuminate\View\View; +use Native\Mobile\Attributes\Poll; use Native\Mobile\Edge\Layouts\Builders\NavBarOptions; use Native\Mobile\Edge\NativeComponent; -use Native\Mobile\Edge\Transition; use Native\Mobile\Facades\Camera; class Counter extends NativeComponent @@ -14,6 +14,12 @@ class Counter extends NativeComponent public $photo = ''; + /** Direction of an active press-and-hold: 'up', 'down', or null. */ + public ?string $holding = null; + + /** Ticks elapsed in the current hold — drives step acceleration. */ + public int $holdTicks = 0; + public function navTitle(): string { return 'Counter'; @@ -29,6 +35,53 @@ public function decrement() $this->count--; } + /** + * `@pressDown` — steps once immediately (so a plain tap counts), then + * arms the poll-driven repeat below for as long as the press is held. + */ + public function startHold(string $direction): void + { + $this->holding = $direction === 'down' ? 'down' : 'up'; + $this->holdTicks = 0; + $this->step(); + } + + /** `@pressUp` — fires on release or cancel; disarms the repeat. */ + public function stopHold(): void + { + $this->holding = null; + $this->holdTicks = 0; + } + + /** + * Repeat engine. The poll wakes every tick regardless of hold state, + * but idle ticks re-render an identical tree, which the shadow compare + * drops before it reaches the wire — only held ticks cost anything. + * The tick cap auto-releases a hold whose pressUp never arrived. + */ + #[Poll(110)] + public function holdTick(): void + { + if ($this->holding === null) { + return; + } + + if (++$this->holdTicks > 130) { + $this->stopHold(); + + return; + } + + $this->step(); + } + + /** Accelerates the longer the hold: ±1, then ±2, then ±5 per tick. */ + protected function step(): void + { + $delta = $this->holdTicks > 25 ? 5 : ($this->holdTicks > 8 ? 2 : 1); + $this->count += $this->holding === 'down' ? -$delta : $delta; + } + public function testCamera() { Camera::getPhoto()->photoTaken(function ($photo) { diff --git a/app/NativeComponents/DemoLauncher.php b/app/NativeComponents/DemoLauncher.php index b7d2c71..5784fc4 100644 --- a/app/NativeComponents/DemoLauncher.php +++ b/app/NativeComponents/DemoLauncher.php @@ -44,11 +44,12 @@ class DemoLauncher extends NativeComponent ['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'], + // ['id' => 'geo-watch', 'title' => 'Geo — Watch Position', 'subtitle' => 'Streaming GPS fixes (watchPosition/clearWatch)', 'icon' => 'location.fill', 'color' => '#F97316', 'url' => '/geo-watch'], ], ], [ @@ -57,7 +58,8 @@ class DemoLauncher extends NativeComponent ['id' => 'gestures', 'title' => 'Gestures', 'subtitle' => 'Double-tap (@doubleTap) ', 'icon' => 'hand.tap.fill', 'color' => '#6366F1', 'url' => '/gestures'], ['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' => 'glass', 'title' => 'Glass', 'subtitle' => 'Liquid Glass Demo', 'icon' => 'drop.fill', 'color' => '#0EA5E9', 'url' => '/glass'], + ['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'], diff --git a/app/NativeComponents/Layouts/DrawerLayout.php b/app/NativeComponents/Layouts/DrawerLayout.php index 4643c0d..8acf0bf 100644 --- a/app/NativeComponents/Layouts/DrawerLayout.php +++ b/app/NativeComponents/Layouts/DrawerLayout.php @@ -4,8 +4,8 @@ use Native\Mobile\Edge\Layouts\NativeLayout; use Native\Mobile\Edge\NativeComponent; -use Nativephp\NativeUi\Builders\Drawer; -use Nativephp\NativeUi\Concerns\HasLayoutDrawer; +use Native\Mobile\UI\Builders\Drawer; +use Native\Mobile\UI\Concerns\HasLayoutDrawer; /** * Demo layout for the content-agnostic side drawer (X-style side nav). diff --git a/app/NativeComponents/NumberSwitcherDemo.php b/app/NativeComponents/NumberSwitcherDemo.php new file mode 100644 index 0000000..2b305c5 --- /dev/null +++ b/app/NativeComponents/NumberSwitcherDemo.php @@ -0,0 +1,48 @@ + — the "number + * switcher": digits roll in place as the value changes (SwiftUI + * numericText on iOS, directional slide + fade on Android) instead of + * swapping cold. The side-by-side row renders the same value through + * each mode so the difference is visible at a glance. + */ +class NumberSwitcherDemo extends NativeComponent +{ + public int $value = 1024; + + public function increment(): void + { + $this->value++; + } + + public function decrement(): void + { + $this->value = max(0, $this->value - 1); + } + + public function jump(): void + { + $this->value += 111; + } + + public function randomize(): void + { + $this->value = random_int(0, 99999); + } + + public function navTitle(): string + { + return 'Number Switcher'; + } + + public function render(): View + { + return view('native.number-switcher'); + } +} diff --git a/app/NativeComponents/Phase4VirtualListTest.php b/app/NativeComponents/Phase4VirtualListTest.php index de31b06..e1419cd 100644 --- a/app/NativeComponents/Phase4VirtualListTest.php +++ b/app/NativeComponents/Phase4VirtualListTest.php @@ -11,7 +11,7 @@ use Native\Mobile\Edge\Layouts\Builders\NavBarOptions; use Native\Mobile\Edge\NativeComponent; use Native\Mobile\Edge\Traits\HasVirtualListWindow; -use Nativephp\NativeUi\Elements\NativeVirtualList; +use Native\Mobile\UI\Elements\NativeVirtualList; /** * Phase 4 — list virtualization verification screen. diff --git a/app/NativeComponents/SyncUpNative/Layouts/SyncUpNativeTabsLayout.php b/app/NativeComponents/SyncUpNative/Layouts/SyncUpNativeTabsLayout.php index 37df7e4..4fca713 100644 --- a/app/NativeComponents/SyncUpNative/Layouts/SyncUpNativeTabsLayout.php +++ b/app/NativeComponents/SyncUpNative/Layouts/SyncUpNativeTabsLayout.php @@ -9,8 +9,6 @@ use Native\Mobile\Edge\Layouts\Builders\TabBar; use Native\Mobile\Edge\Layouts\NativeLayout; use Native\Mobile\Edge\NativeComponent; -use Nativephp\NativeUi\Builders\FloatingOverlay; -use Nativephp\NativeUi\Concerns\HasFloatingOverlay; /** * Native-chrome variant of SyncUp's tabs layout. Same shape as the diff --git a/app/NativeComponents/WebviewDemo.php b/app/NativeComponents/WebviewDemo.php new file mode 100644 index 0000000..c0db3fd --- /dev/null +++ b/app/NativeComponents/WebviewDemo.php @@ -0,0 +1,78 @@ +mode = 'remote'; + } + + public function showLocal(): void + { + $this->mode = 'local'; + } + + public function showPhp(): void + { + $this->mode = 'php'; + } + + public function showFullscreen(): void + { + $this->mode = 'fullscreen'; + } + + public function onUrlChanged(string $url): void + { + $this->lastNavigatedUrl = $url; + } + + public function navTitle(): string + { + return 'Webview'; + } + + public function render(): View + { + return view('native.webview-demo', [ + 'localHtml' => $this->localHtml(), + ]); + } + + private function localHtml(): string + { + return <<<'HTML' + + + + + + + +

Inline HTML slot

+

This document was loaded with baseURL = null, so the + webview has an opaque origin and cannot reach back into the host app.

+

JavaScript is disabled by default — try clicking this:

+

example.org (will fire @navigated).

+ + +HTML; + } +} diff --git a/app/Providers/NativeServiceProvider.php b/app/Providers/NativeServiceProvider.php index 06d329f..f105639 100644 --- a/app/Providers/NativeServiceProvider.php +++ b/app/Providers/NativeServiceProvider.php @@ -3,12 +3,7 @@ namespace App\Providers; use Illuminate\Support\ServiceProvider; -use Native\Mobile\Providers\CameraServiceProvider; -use Native\Mobile\Providers\DeviceServiceProvider; -use Native\Mobile\Providers\DialogServiceProvider; -use Native\Mobile\Providers\GeolocationServiceProvider; -use Native\Mobile\Providers\SecureStorageServiceProvider; -use Nativephp\NativeUi\NativeUIServiceProvider; +use Native\Mobile\UI\NativeUIServiceProvider; class NativeServiceProvider extends ServiceProvider { @@ -40,7 +35,7 @@ public function boot(): void public function plugins(): array { return [ - NativeUIServiceProvider::class + NativeUIServiceProvider::class, ]; } } diff --git a/composer.json b/composer.json index 50e311f..afc7876 100644 --- a/composer.json +++ b/composer.json @@ -27,9 +27,9 @@ "php": "^8.4", "laravel/framework": "^13.0", "laravel/tinker": "^3.0", - "nativephp/mobile": "dev-element", + "nativephp/mobile": "dev-main as 4.0.x-dev", "nativephp/mobile-camera": "*", - "nativephp/native-ui": "*" + "nativephp/mobile-ui": "^0.2" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/composer.lock b/composer.lock index 34bf683..a437660 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": "e7b0dda36b28548b4a4d12ba69d4a897", + "content-hash": "1aacf0d8e0cc98f0588e238fe50a9080", "packages": [ { "name": "bacon/bacon-qr-code", @@ -2358,16 +2358,16 @@ }, { "name": "nativephp/mobile", - "version": "dev-element", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/NativePHP/mobile-air.git", - "reference": "b9e8d2fb271ff35add8a8066414191da914ba885" + "reference": "1afaf068f05918604b9fbaa63777bfc93c5f3785" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/NativePHP/mobile-air/zipball/b9e8d2fb271ff35add8a8066414191da914ba885", - "reference": "b9e8d2fb271ff35add8a8066414191da914ba885", + "url": "https://api.github.com/repos/NativePHP/mobile-air/zipball/1afaf068f05918604b9fbaa63777bfc93c5f3785", + "reference": "1afaf068f05918604b9fbaa63777bfc93c5f3785", "shasum": "" }, "require": { @@ -2469,10 +2469,10 @@ "nativephp" ], "support": { - "source": "https://github.com/NativePHP/mobile-air/tree/element", + "source": "https://github.com/NativePHP/mobile-air/tree/main", "issues": "https://github.com/NativePHP/mobile-air/issues" }, - "time": "2026-07-12T01:48:26+00:00" + "time": "2026-07-23T01:49:24+00:00" }, { "name": "nativephp/mobile-camera", @@ -2526,32 +2526,32 @@ "time": "2026-06-04T12:43:58+00:00" }, { - "name": "nativephp/native-ui", - "version": "dev-main", + "name": "nativephp/mobile-ui", + "version": "0.2.0", "source": { "type": "git", - "url": "https://github.com/NativePHP/native-ui.git", - "reference": "42bcf7a9e311fb0ac02ba8809d2846a485a5f51f" + "url": "https://github.com/NativePHP/mobile-ui.git", + "reference": "96e1c21760fd585c2df3bae96dd70b6a420d4199" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/NativePHP/native-ui/zipball/42bcf7a9e311fb0ac02ba8809d2846a485a5f51f", - "reference": "42bcf7a9e311fb0ac02ba8809d2846a485a5f51f", + "url": "https://api.github.com/repos/NativePHP/mobile-ui/zipball/96e1c21760fd585c2df3bae96dd70b6a420d4199", + "reference": "96e1c21760fd585c2df3bae96dd70b6a420d4199", "shasum": "" }, "require": { - "nativephp/mobile": "*", + "nativephp/mobile": "^4.0", "php": "^8.2" }, "require-dev": { + "orchestra/testbench": "^10.0", "pestphp/pest": "^3.0" }, - "default-branch": true, - "type": "nativephp-ui-plugin", + "type": "nativephp-plugin", "extra": { "laravel": { "providers": [ - "Nativephp\\NativeUi\\NativeUIServiceProvider" + "Native\\Mobile\\UI\\NativeUIServiceProvider" ] }, "nativephp": { @@ -2560,7 +2560,7 @@ }, "autoload": { "psr-4": { - "Nativephp\\NativeUi\\": "src/" + "Native\\Mobile\\UI\\": "src/" } }, "scripts": { @@ -2573,16 +2573,22 @@ ], "authors": [ { - "name": "Your Name", - "email": "you@example.com" + "name": "NativePHP", + "email": "support@nativephp.com" } ], - "description": "A NativePHP Mobile plugin", + "description": "Native SwiftUI and Jetpack Composer components for NativePHP", "support": { - "source": "https://github.com/NativePHP/native-ui/tree/main", - "issues": "https://github.com/NativePHP/native-ui/issues" + "source": "https://github.com/NativePHP/mobile-ui/tree/0.2.0", + "issues": "https://github.com/NativePHP/mobile-ui/issues" }, - "time": "2026-07-12T02:06:40+00:00" + "funding": [ + { + "type": "other", + "url": "https://nativephp.com/sponsor" + } + ], + "time": "2026-07-22T15:19:38+00:00" }, { "name": "nesbot/carbon", @@ -10641,7 +10647,14 @@ "time": "2026-06-15T15:31:57+00:00" } ], - "aliases": [], + "aliases": [ + { + "package": "nativephp/mobile", + "version": "dev-main", + "alias": "4.0.x-dev", + "alias_normalized": "4.0.9999999.9999999-dev" + } + ], "minimum-stability": "dev", "stability-flags": { "nativephp/mobile": 20 diff --git a/config/native-ui.php b/config/native-ui.php index 11ea93f..07440f0 100644 --- a/config/native-ui.php +++ b/config/native-ui.php @@ -6,7 +6,7 @@ * Published via `php artisan vendor:publish --tag=native-ui-config`. * Edit to customize your app's visual identity in one place. * - * For dynamic per-tenant theming, use Nativephp\NativeUi\Theme::merge([...]) + * For dynamic per-tenant theming, use Native\Mobile\UI\Theme::merge([...]) * from a service provider. Runtime merges deep-merge on top of these values. * * Decision log: /docs/NATIVE-UI-REWRITE-PLAN.md (D — theme layer) diff --git a/phpunit.xml b/phpunit.xml index d703241..99b8ade 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -19,6 +19,7 @@ + diff --git a/resources/views/native/counter.blade.php b/resources/views/native/counter.blade.php index dcf16d3..eebe9d0 100644 --- a/resources/views/native/counter.blade.php +++ b/resources/views/native/counter.blade.php @@ -1,15 +1,17 @@ - + {{ $count }} + {{-- Tap steps once; press-and-hold repeats with acceleration + (pressDown arms the poll-driven repeat, pressUp disarms it). --}} - + - + diff --git a/resources/views/native/number-switcher.blade.php b/resources/views/native/number-switcher.blade.php new file mode 100644 index 0000000..49c92c2 --- /dev/null +++ b/resources/views/native/number-switcher.blade.php @@ -0,0 +1,53 @@ + + + + {{-- Hero: the rolling number --}} + content-transition="numeric" + {{ number_format($value) }} + + + + + + + + + + + {{-- Same value through each mode, side by side --}} + Side by side + + + {{ $value }} + numeric + + + {{ $value }} + opacity + + + {{ $value }} + none + + + + + numeric rolls digits up as the value grows and down as it shrinks. + iOS uses SwiftUI numericText; Android approximates with a + directional slide + fade. opacity crossfades on both. + + + + diff --git a/resources/views/native/webview-demo.blade.php b/resources/views/native/webview-demo.blade.php new file mode 100644 index 0000000..570a3dd --- /dev/null +++ b/resources/views/native/webview-demo.blade.php @@ -0,0 +1,71 @@ +@if ($mode === 'fullscreen') + {{-- v3-style takeover: the webview is the entire screen content, extending + behind the safe areas. Navigate back (top-left chevron) to exit — + a fresh visit remounts in remote mode. --}} + +@else + + + + + Remote + + + + + Inline + + + + + PHP + + + + + Full + + + + + + Last @navigated → {{ $lastNavigatedUrl ?: '(none yet)' }} + + + @if ($mode === 'remote') + + @elseif ($mode === 'local') + + {!! $localHtml !!} + + @else + {{-- Enriched mode: the app's own Laravel webview — embedded runtime, + shared session, window.Native bridge. src is an app route path. --}} + + @endif + +@endif diff --git a/resources/views/webview-embedded.blade.php b/resources/views/webview-embedded.blade.php new file mode 100644 index 0000000..c64fedb --- /dev/null +++ b/resources/views/webview-embedded.blade.php @@ -0,0 +1,46 @@ + + + + + Embedded PHP runtime + + + +

Embedded PHP runtime

+

This page is a plain Laravel web route rendered inside the demo's + php-mode webview — served by the app's own runtime, not + a sandboxed foreign page.

+ +
PHP {{ PHP_VERSION }} · Laravel {{ app()->version() }}
+
Session visits: {{ $hits }} — persists across reloads because the app session is shared.
+
Native bridge: checking…
+ +

Reload — bumps the counter and fires @navigated.

+ + + + diff --git a/routes/web.php b/routes/web.php index 4e28537..2c40f1c 100644 --- a/routes/web.php +++ b/routes/web.php @@ -45,6 +45,7 @@ use App\NativeComponents\NativeChromeDetail; use App\NativeComponents\NativeTabsHome; use App\NativeComponents\NativeTabsProfile; +use App\NativeComponents\NumberSwitcherDemo; use App\NativeComponents\PerfDemo; use App\NativeComponents\PerfShowdown; use App\NativeComponents\Phase1KeyTest; @@ -78,6 +79,7 @@ use App\NativeComponents\VibeLogin; use App\NativeComponents\VibePresenceDemo; use App\NativeComponents\VibePrivateDemo; +use App\NativeComponents\WebviewDemo; use App\NativeComponents\YouTubeChannel; use App\NativeComponents\YouTubeHome; use App\NativeComponents\YouTubeSearch; @@ -121,7 +123,9 @@ // Component showcases (broken out from explore) Route::native('/counter', Counter::class)->name('counter'); Route::native('/reactivity', ReactivityDemo::class)->name('reactivity.demo'); + Route::native('/webview-demo', WebviewDemo::class)->name('webview.demo'); Route::native('/animate', Animate::class)->name('animate'); + Route::native('/number-switcher', NumberSwitcherDemo::class)->name('number.switcher'); Route::native('/gestures', GestureDemo::class)->name('gestures'); Route::native('/mail-demo', MailDemo::class)->name('mail.demo'); Route::native('/refreshable-demo', RefreshableDemo::class)->name('refreshable.demo'); @@ -233,3 +237,11 @@ // ── Extras ── // (note: /benchmark is registered above inside the StackLayout group so it gets a back-chevron) + +// Plain web route — loaded by the webview demo's `php`-mode webview, where the +// embedded runtime serves it with the app session and window.Native bridge. +Route::get('/webview-embedded', function () { + return view('webview-embedded', [ + 'hits' => session()->increment('webview_embedded_hits'), + ]); +})->name('webview.embedded'); diff --git a/tests/Feature/Native/CounterTest.php b/tests/Feature/Native/CounterTest.php index ce6f385..77809bc 100644 --- a/tests/Feature/Native/CounterTest.php +++ b/tests/Feature/Native/CounterTest.php @@ -12,20 +12,59 @@ ->assertNavTitle('Counter'); }); -it('increments and decrements through the pressable icons', function () { +it('renders the count with the numeric content transition', function () { + $screen = Native::test(Counter::class); + + expect(json_encode($screen->tree()))->toContain('"content_transition":"numeric"'); +}); + +it('steps once per tap in both directions', function () { Native::test(Counter::class) - ->press('increment') + ->call('startHold', 'up') + ->call('stopHold') ->assertSet('count', 1) - ->press('increment') + ->call('startHold', 'up') + ->call('stopHold') ->assertSet('count', 2) - ->press('decrement') - ->assertSet('count', 1); + ->call('startHold', 'down') + ->call('stopHold') + ->assertSet('count', 1) + ->assertSet('holding', null); +}); + +it('repeats while held and stops on release', function () { + $screen = Native::test(Counter::class) + ->call('startHold', 'up') + ->assertSet('count', 1) + ->assertSet('holding', 'up'); + + foreach (range(1, 5) as $i) { + $screen->call('holdTick'); + } + + $screen->assertSet('count', 6) + ->call('stopHold') + ->call('holdTick') + ->call('holdTick') + ->assertSet('count', 6); +}); + +it('accelerates the longer the hold runs', function () { + $screen = Native::test(Counter::class)->call('startHold', 'up'); + + foreach (range(1, 30) as $i) { + $screen->call('holdTick'); + } + + // 8 ticks × 1 + 17 ticks × 2 + 5 ticks × 5 + the initial press step + $screen->assertSet('count', 1 + 8 + 34 + 25); }); -it('increments via the long-press binding too', function () { +it('ignores hold ticks when nothing is held', function () { Native::test(Counter::class) - ->longPress('increment') - ->assertSet('count', 1); + ->call('holdTick') + ->call('holdTick') + ->assertSet('count', 0); }); it('captures a photo through the camera bridge', function () { diff --git a/tests/Feature/Native/DemoLauncherTest.php b/tests/Feature/Native/DemoLauncherTest.php index 6c9be30..8e53ce5 100644 --- a/tests/Feature/Native/DemoLauncherTest.php +++ b/tests/Feature/Native/DemoLauncherTest.php @@ -8,6 +8,7 @@ ->assertScreen(DemoLauncher::class) ->assertNavTitle('SuperNative Demo') ->assertSee('Counter') + ->assertSee('Webview') ->assertSee('Twitter / X'); expect($screen->get('groups'))->toHaveCount(4); diff --git a/tests/Feature/Native/DemoScreensSmokeTest.php b/tests/Feature/Native/DemoScreensSmokeTest.php index 61a3a2f..441d566 100644 --- a/tests/Feature/Native/DemoScreensSmokeTest.php +++ b/tests/Feature/Native/DemoScreensSmokeTest.php @@ -23,6 +23,8 @@ 'menus' => '/explore/menus', 'mail inbox' => '/mail-demo', 'pull to refresh' => '/refreshable-demo', + 'webview' => '/webview-demo', + 'number switcher' => '/number-switcher', 'gestures' => '/gestures', 'transitions' => '/transitions', 'glass' => '/glass', diff --git a/tests/Feature/Native/NumberSwitcherDemoTest.php b/tests/Feature/Native/NumberSwitcherDemoTest.php new file mode 100644 index 0000000..2d9a3e6 --- /dev/null +++ b/tests/Feature/Native/NumberSwitcherDemoTest.php @@ -0,0 +1,32 @@ +assertSet('value', 1024) + ->assertSee('1,024'); + + $tree = json_encode($screen->tree()); + + expect($tree)->toContain('"content_transition":"numeric"') + ->toContain('"content_transition":"opacity"'); +}); + +it('steps and jumps the value', function () { + Native::test(NumberSwitcherDemo::class) + ->call('increment') + ->assertSet('value', 1025) + ->call('decrement') + ->call('decrement') + ->assertSet('value', 1023) + ->call('jump') + ->assertSet('value', 1134); +}); + +it('never decrements below zero', function () { + $screen = Native::test(NumberSwitcherDemo::class); + $screen->set('value', 0); + $screen->call('decrement')->assertSet('value', 0); +}); diff --git a/tests/Feature/Native/WebviewDemoTest.php b/tests/Feature/Native/WebviewDemoTest.php new file mode 100644 index 0000000..8b0f4f5 --- /dev/null +++ b/tests/Feature/Native/WebviewDemoTest.php @@ -0,0 +1,35 @@ +assertSet('mode', 'remote'); +}); + +it('renders the enriched php webview in php mode', function () { + $screen = Native::test(WebviewDemo::class)->call('showPhp'); + + $tree = json_encode($screen->tree()); + + expect($tree)->toContain('"php":true') + ->toContain('/webview-embedded'); +}); + +it('renders a fullscreen webview in fullscreen mode', function () { + $screen = Native::test(WebviewDemo::class)->call('showFullscreen'); + + expect(json_encode($screen->tree()))->toContain('"fullscreen":true'); +}); + +it('serves the embedded page with a session visit counter', function () { + $this->get('/webview-embedded') + ->assertOk() + ->assertSee('Embedded PHP runtime') + ->assertSee('Session visits: 1', escape: false); + + // The counter is session-backed, so a second request in the same + // test session increments it. + $this->get('/webview-embedded') + ->assertSee('Session visits: 2', escape: false); +}); diff --git a/tests/Feature/WebviewElementTest.php b/tests/Feature/WebviewElementTest.php new file mode 100644 index 0000000..0fb7232 --- /dev/null +++ b/tests/Feature/WebviewElementTest.php @@ -0,0 +1,104 @@ + NativeTagPrecompiler::setActive(true)); +afterEach(fn () => NativeTagPrecompiler::setActive(false)); + +it('exposes webview as a registered native element', function () { + expect(class_exists(Webview::class))->toBeTrue(); + expect((new Webview)->getType())->toBe('webview'); +}); + +it('captures src, html and opt-in js on a Webview element', function () { + $el = new Webview; + $el->applyAttributes([ + 'src' => 'https://example.com', + 'html' => '

hi

', + 'javascript' => 'true', + ]); + + $props = $el->getResolvedProps(new CallbackRegistry); + + expect($props['src'])->toBe('https://example.com'); + expect($props['html'])->toBe('

hi

'); + expect($props['javascript'])->toBeTrue(); +}); + +it('maps php and fullscreen attributes onto webview props', function () { + $el = new Webview; + $el->applyAttributes([ + 'php' => 'true', + 'fullscreen' => 'true', + ]); + + $props = $el->getResolvedProps(new CallbackRegistry); + + expect($props['php'])->toBeTrue(); + expect($props['fullscreen'])->toBeTrue(); + + // fullscreen implies fill layout so the element claims the whole screen. + $tree = $el->toArray(new CallbackRegistry); + expect($tree['layout'])->toMatchArray(['width' => 'fill', 'height' => 'fill']); +}); + +it('binds @navigated to a PHP method name via the callback registry', function () { + $el = new Webview; + $el->onNavigated('urlChanged'); + + $registry = new CallbackRegistry; + $props = $el->getResolvedProps($registry); + + expect($props['on_navigated'])->toBeInt()->toBeGreaterThan(0); + expect($registry->resolve($props['on_navigated']))->toMatchArray(['method' => 'urlChanged']); +}); + +it('compiles tags through the precompiler', function () { + $precompiler = new NativeTagPrecompiler; + + $compiled = $precompiler(''); + + expect($compiled)->toContain("leaf('webview'"); + expect($compiled)->toContain('_navigated'); +}); + +it('captures slot HTML as a raw html prop (no strip_tags)', function () { + $precompiler = new NativeTagPrecompiler; + + $template = '

Hi

Hello world

'; + $compiled = $precompiler($template); + + // Opening should start a buffer; closing should drop the captured + // slot into the attrs without stripping tags. + expect($compiled)->toContain('ob_start()'); + expect($compiled)->toContain("'html'"); +}); + +it('lets :html attribute take precedence over slot content', function () { + $precompiler = new NativeTagPrecompiler; + $template = '

fallback

'; + + $compiled = $precompiler($template); + + expect($compiled)->toContain("'html' => (\$x)"); + expect($compiled)->toContain("!isset(\$__nativeSlotAttrs['html'])"); +}); + +it('routes _navigated through onNavigated() on the Webview element', function () { + $el = new Webview; + NativeElementCollector::setCallbacks(new CallbackRegistry); + + // The collector wires _navigated → $element->onNavigated(...) only + // when the method exists; smoke-test that wiring directly. + $reflection = new ReflectionMethod(NativeElementCollector::class, 'applyCallbacks'); + $reflection->setAccessible(true); + $reflection->invoke(null, $el, ['_navigated' => 'urlChanged']); + + $props = $el->getResolvedProps(new CallbackRegistry); + expect($props)->toHaveKey('on_navigated'); +});