diff --git a/app/NativeComponents/DemoLauncher.php b/app/NativeComponents/DemoLauncher.php
index 5b72034..9ea603d 100644
--- a/app/NativeComponents/DemoLauncher.php
+++ b/app/NativeComponents/DemoLauncher.php
@@ -59,6 +59,20 @@ class DemoLauncher extends NativeComponent
['id' => 'numberswitcher', 'title' => 'Number Switcher', 'subtitle' => 'content-transition — rolling in-place digit changes', 'icon' => 'textformat.123', 'color' => '#14B8A6', 'url' => '/number-switcher'],
],
],
+ [
+ 'title' => 'Edge Components',
+ 'demos' => [
+ ['id' => 'edge-top-bar', 'title' => 'Top Bar', 'subtitle' => 'Title, subtitle, and trailing actions', 'icon' => 'rectangle.topthird.inset.filled', 'color' => '#0EA5E9', 'url' => '/edge-components/top-bar'],
+ ['id' => 'edge-top-bar-large-title', 'title' => 'Top Bar — Large Title', 'subtitle' => 'The large display mode', 'icon' => 'textformat.size.larger', 'color' => '#0EA5E9', 'url' => '/edge-components/top-bar-large-title'],
+ ['id' => 'edge-top-bar-search', 'title' => 'Top Bar — Search', 'subtitle' => 'An attached native search field', 'icon' => 'magnifyingglass', 'color' => '#0EA5E9', 'url' => '/edge-components/top-bar-search'],
+ ['id' => 'edge-top-bar-destructive-action', 'title' => 'Top Bar — Destructive Action', 'subtitle' => 'An action tinted for a destructive tap', 'icon' => 'trash', 'color' => '#EF4444', 'url' => '/edge-components/top-bar-destructive-action'],
+ ['id' => 'edge-top-bar-logo', 'title' => 'Top Bar — Logo', 'subtitle' => 'An image in the title slot', 'icon' => 'photo', 'color' => '#0EA5E9', 'url' => '/edge-components/top-bar-logo'],
+ ['id' => 'edge-bottom-nav', 'title' => 'Bottom Nav', 'subtitle' => 'Active, news, and badge items', 'icon' => 'square.grid.2x2', 'color' => '#22C55E', 'url' => '/edge-components/bottom-nav'],
+ ['id' => 'edge-bottom-nav-search-item', 'title' => 'Bottom Nav — Search Item', 'subtitle' => 'A dedicated search tab', 'icon' => 'magnifyingglass.circle', 'color' => '#22C55E', 'url' => '/edge-components/bottom-nav-search-item'],
+ ['id' => 'edge-side-nav', 'title' => 'Side Nav', 'subtitle' => 'Header, collapsed group, and a divider', 'icon' => 'sidebar.left', 'color' => '#A855F7', 'url' => '/edge-components/side-nav'],
+ ['id' => 'edge-side-nav-header-image', 'title' => 'Side Nav — Header Image', 'subtitle' => 'A background image behind the header', 'icon' => 'photo.on.rectangle', 'color' => '#A855F7', 'url' => '/edge-components/side-nav-header-image'],
+ ],
+ ],
[
'title' => 'Mini Apps',
'demos' => [
diff --git a/app/NativeComponents/EdgeComponents/BottomNav.php b/app/NativeComponents/EdgeComponents/BottomNav.php
new file mode 100644
index 0000000..27fa2f1
--- /dev/null
+++ b/app/NativeComponents/EdgeComponents/BottomNav.php
@@ -0,0 +1,21 @@
+ 'Edge Component showcase — this screen exists to show off the bottom nav below.'])
+
+
+
+
+
+
diff --git a/resources/views/native/edge-components/bottom-nav.blade.php b/resources/views/native/edge-components/bottom-nav.blade.php
new file mode 100644
index 0000000..1e034d6
--- /dev/null
+++ b/resources/views/native/edge-components/bottom-nav.blade.php
@@ -0,0 +1,7 @@
+@include('native.partials.edge-component-note', ['caption' => 'Edge Component showcase — this screen exists to show off the bottom nav below.'])
+
+
+
+
+
+
diff --git a/resources/views/native/edge-components/side-nav-body.blade.php b/resources/views/native/edge-components/side-nav-body.blade.php
new file mode 100644
index 0000000..acca81c
--- /dev/null
+++ b/resources/views/native/edge-components/side-nav-body.blade.php
@@ -0,0 +1,5 @@
+
+
+ Edge Component showcase — this screen exists to show off the side drawer. Its nav bar is hidden so the drawer's own ☰ affordance is the only leading icon on screen.
+
+
diff --git a/resources/views/native/edge-components/side-nav-drawer.blade.php b/resources/views/native/edge-components/side-nav-drawer.blade.php
new file mode 100644
index 0000000..e8f3dcf
--- /dev/null
+++ b/resources/views/native/edge-components/side-nav-drawer.blade.php
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/views/native/edge-components/side-nav-header-image.blade.php b/resources/views/native/edge-components/side-nav-header-image.blade.php
new file mode 100644
index 0000000..9f7117d
--- /dev/null
+++ b/resources/views/native/edge-components/side-nav-header-image.blade.php
@@ -0,0 +1,7 @@
+
+
+
diff --git a/resources/views/native/edge-components/top-bar-destructive-action.blade.php b/resources/views/native/edge-components/top-bar-destructive-action.blade.php
new file mode 100644
index 0000000..3a69cd3
--- /dev/null
+++ b/resources/views/native/edge-components/top-bar-destructive-action.blade.php
@@ -0,0 +1,5 @@
+
+
+
+
+@include('native.partials.edge-component-note', ['caption' => 'Edge Component showcase — this screen exists to show off the top bar above.'])
diff --git a/resources/views/native/edge-components/top-bar-large-title.blade.php b/resources/views/native/edge-components/top-bar-large-title.blade.php
new file mode 100644
index 0000000..f2e92db
--- /dev/null
+++ b/resources/views/native/edge-components/top-bar-large-title.blade.php
@@ -0,0 +1,5 @@
+
+
+
+
+@include('native.partials.edge-component-note', ['caption' => 'Edge Component showcase — this screen exists to show off the top bar above.'])
diff --git a/resources/views/native/edge-components/top-bar-logo.blade.php b/resources/views/native/edge-components/top-bar-logo.blade.php
new file mode 100644
index 0000000..aa2407d
--- /dev/null
+++ b/resources/views/native/edge-components/top-bar-logo.blade.php
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+@include('native.partials.edge-component-note', ['caption' => 'Edge Component showcase — this screen exists to show off the top bar above.'])
diff --git a/resources/views/native/edge-components/top-bar-search.blade.php b/resources/views/native/edge-components/top-bar-search.blade.php
new file mode 100644
index 0000000..3ccd3cb
--- /dev/null
+++ b/resources/views/native/edge-components/top-bar-search.blade.php
@@ -0,0 +1,3 @@
+
+
+@include('native.partials.edge-component-note', ['caption' => 'Edge Component showcase — this screen exists to show off the top bar above.'])
diff --git a/resources/views/native/edge-components/top-bar.blade.php b/resources/views/native/edge-components/top-bar.blade.php
new file mode 100644
index 0000000..3a1212f
--- /dev/null
+++ b/resources/views/native/edge-components/top-bar.blade.php
@@ -0,0 +1,16 @@
+
+
+
+
+
+@include('native.partials.edge-component-note', ['caption' => 'Edge Component showcase — this screen exists to show off the top bar above.'])
diff --git a/resources/views/native/explore/forms.blade.php b/resources/views/native/explore/forms.blade.php
index db1647a..f985882 100644
--- a/resources/views/native/explore/forms.blade.php
+++ b/resources/views/native/explore/forms.blade.php
@@ -1,3 +1,4 @@
+
Slider
@@ -20,119 +21,60 @@
{{ number_format($slideValue * 10000000, 2) }}
-
-
-
-
-
-
-
-
+
+
+ {{-- TOGGLE --}}
+ Toggle
+
+
+
+
+
+
+ {{-- CHECKBOX --}}
+ Checkbox
+
+
+
+ subscribed: {{ $subscribed ? 'yes' : 'no' }} - terms: {{ $termsAccepted ? 'yes' : 'no' }}
+
+
+
+
+ {{-- SELECT --}}
+ Select
+
+ Selected: {{ $favoriteLanguage }}
+
+
+
+ {{-- RADIO GROUP --}}
+ Radio Group
+
+
+
+
+
+ Chosen: {{ $pricingPlan }}
+
+
{{-- TEXT INPUT --}}
-{{-- Text Input — Outlined--}}
-{{-- --}}
-{{-- --}}
-{{-- --}}
-{{-- --}}
-
-{{-- Text Input — Filled--}}
-{{-- --}}
-{{-- --}}
-
-{{-- --}}
-
-{{-- --}}{{-- SLIDER --}}
-{{-- Slider--}}
-
-{{-- --}}
-{{-- Live (every drag tick)--}}
-{{-- --}}
-{{-- Value 1: {{ number_format($slideValue, 3) }}--}}
-{{-- Value 2: {{ number_format($slideValue * 2, 3) }}--}}
-{{-- --}}
-
-{{-- --}}
-{{-- Debounced (150ms)--}}
-{{-- --}}
-{{-- {{ number_format($slideDebounced, 1) }}--}}
-{{-- --}}
-
-{{-- --}}
-{{-- On release (blur)--}}
-{{-- --}}
-{{-- {{ number_format($slideBlur, 1) }}--}}
-{{-- --}}
-
-{{-- --}}
-
-{{-- --}}{{-- TOGGLE --}}
-{{-- Toggle--}}
-{{-- --}}
-{{-- --}}
-{{-- --}}
-
-{{-- --}}
-
-{{-- --}}{{-- CHECKBOX --}}
-{{-- Checkbox (composed)--}}
-{{-- @foreach ([--}}
-{{-- ['field' => 'subscribed', 'label' => 'Subscribe to newsletter'],--}}
-{{-- ['field' => 'termsAccepted', 'label' => 'I accept the terms and conditions'],--}}
-{{-- ] as $row)--}}
-{{-- --}}
-{{-- --}}
-{{-- --}}
-{{-- {{ $row['label'] }}--}}
-{{--
--}}
-{{-- --}}
-{{-- @endforeach--}}
-{{-- --}}
-{{-- subscribed: {{ $subscribed ? 'yes' : 'no' }} · terms: {{ $termsAccepted ? 'yes' : 'no' }}--}}
-{{-- --}}
-
-{{-- --}}
-
-{{-- --}}{{-- SELECT --}}
-{{-- Select--}}
-{{-- --}}
-{{-- Selected: {{ $favoriteLanguage }}--}}
-
-{{-- --}}
-
-{{-- --}}{{-- RADIO GROUP --}}
-{{-- Radio Group (composed)--}}
-{{-- --}}
-{{-- Pricing plan--}}
-
-{{-- @foreach ([--}}
-{{-- ['value' => 'free', 'label' => 'Free — $0/mo'],--}}
-{{-- ['value' => 'pro', 'label' => 'Pro — $19/mo'],--}}
-{{-- ['value' => 'team', 'label' => 'Team — $49/mo'],--}}
-{{-- ] as $row)--}}
-{{-- @php $checked = $pricingPlan === $row['value']; @endphp--}}
-{{-- --}}
-{{-- --}}
-{{-- --}}
-{{-- {{ $row['label'] }}--}}
-{{--
--}}
-{{-- --}}
-{{-- @endforeach--}}
-{{-- --}}
-{{-- Chosen: {{ $pricingPlan }}--}}
+ Text Input - Outlined
+
+
+
+
+
+ Text Input - Filled
+
+
+
diff --git a/resources/views/native/partials/edge-component-note.blade.php b/resources/views/native/partials/edge-component-note.blade.php
new file mode 100644
index 0000000..0170220
--- /dev/null
+++ b/resources/views/native/partials/edge-component-note.blade.php
@@ -0,0 +1,5 @@
+
+
+ {{ $caption }}
+
+
diff --git a/routes/mobile.php b/routes/mobile.php
index 2c6d5d2..e689eb7 100644
--- a/routes/mobile.php
+++ b/routes/mobile.php
@@ -6,6 +6,15 @@
use App\NativeComponents\Counter;
use App\NativeComponents\DemoLauncher;
use App\NativeComponents\EdgeChildTest;
+use App\NativeComponents\EdgeComponents\BottomNav;
+use App\NativeComponents\EdgeComponents\BottomNavSearchItem;
+use App\NativeComponents\EdgeComponents\SideNav;
+use App\NativeComponents\EdgeComponents\SideNavHeaderImage;
+use App\NativeComponents\EdgeComponents\TopBar;
+use App\NativeComponents\EdgeComponents\TopBarDestructiveAction;
+use App\NativeComponents\EdgeComponents\TopBarLargeTitle;
+use App\NativeComponents\EdgeComponents\TopBarLogo;
+use App\NativeComponents\EdgeComponents\TopBarSearch;
use App\NativeComponents\EventChannelTest;
use App\NativeComponents\ExploreButtons;
use App\NativeComponents\ExploreCards;
@@ -172,3 +181,17 @@
'hits' => session()->increment('webview_embedded_hits'),
]);
})->name('webview.embedded');
+
+// Edge Component showcase — one screen per component and prop variant,
+// listed in the demo launcher's "Edge Components" group.
+Route::nativeGroup(NativeStackLayout::class, function () {
+ Route::native('/edge-components/top-bar', TopBar::class)->name('edge-components.top-bar');
+ Route::native('/edge-components/top-bar-large-title', TopBarLargeTitle::class)->name('edge-components.top-bar-large-title');
+ Route::native('/edge-components/top-bar-search', TopBarSearch::class)->name('edge-components.top-bar-search');
+ Route::native('/edge-components/top-bar-destructive-action', TopBarDestructiveAction::class)->name('edge-components.top-bar-destructive-action');
+ Route::native('/edge-components/top-bar-logo', TopBarLogo::class)->name('edge-components.top-bar-logo');
+ Route::native('/edge-components/bottom-nav', BottomNav::class)->name('edge-components.bottom-nav');
+ Route::native('/edge-components/bottom-nav-search-item', BottomNavSearchItem::class)->name('edge-components.bottom-nav-search-item');
+ Route::native('/edge-components/side-nav', SideNav::class)->name('edge-components.side-nav');
+ Route::native('/edge-components/side-nav-header-image', SideNavHeaderImage::class)->name('edge-components.side-nav-header-image');
+});
diff --git a/tests/Feature/Native/DemoLauncherTest.php b/tests/Feature/Native/DemoLauncherTest.php
index 3e5beb9..dd877ff 100644
--- a/tests/Feature/Native/DemoLauncherTest.php
+++ b/tests/Feature/Native/DemoLauncherTest.php
@@ -11,7 +11,7 @@
->assertSee('Webview')
->assertSee('Twitter / X');
- expect($screen->get('groups'))->toHaveCount(4);
+ expect($screen->get('groups'))->toHaveCount(5);
});
it('filters the demo list by search query', function () {
@@ -37,7 +37,7 @@
->call('findADemo', 'spotify')
->call('findADemo', '');
- expect($screen->get('groups'))->toHaveCount(4);
+ expect($screen->get('groups'))->toHaveCount(5);
});
it('restores the full list when resuming from a pushed demo', function () {
@@ -45,5 +45,5 @@
->call('findADemo', 'counter')
->call('onResume');
- expect($screen->get('groups'))->toHaveCount(4);
+ expect($screen->get('groups'))->toHaveCount(5);
});
diff --git a/tests/Feature/Native/DemoScreensSmokeTest.php b/tests/Feature/Native/DemoScreensSmokeTest.php
index 6e9d307..6a49b55 100644
--- a/tests/Feature/Native/DemoScreensSmokeTest.php
+++ b/tests/Feature/Native/DemoScreensSmokeTest.php
@@ -45,4 +45,13 @@
'syncup login' => '/syncup-native/login',
'syncup friends' => '/syncup-native/friends',
'syncup profile' => '/syncup-native/profile',
+ 'edge components: top bar' => '/edge-components/top-bar',
+ 'edge components: top bar large title' => '/edge-components/top-bar-large-title',
+ 'edge components: top bar search' => '/edge-components/top-bar-search',
+ 'edge components: top bar destructive action' => '/edge-components/top-bar-destructive-action',
+ 'edge components: top bar logo' => '/edge-components/top-bar-logo',
+ 'edge components: bottom nav' => '/edge-components/bottom-nav',
+ 'edge components: bottom nav search item' => '/edge-components/bottom-nav-search-item',
+ 'edge components: side nav' => '/edge-components/side-nav',
+ 'edge components: side nav header image' => '/edge-components/side-nav-header-image',
]);
diff --git a/tests/Feature/Native/EdgeComponentsTest.php b/tests/Feature/Native/EdgeComponentsTest.php
new file mode 100644
index 0000000..4ccae3a
--- /dev/null
+++ b/tests/Feature/Native/EdgeComponentsTest.php
@@ -0,0 +1,102 @@
+assertScreen(TopBar::class)
+ ->assertNavTitle('Dashboard')
+ ->assertSee('Welcome back')
+ ->assertElement('top_bar_action', fn (array $node): bool => ($node['props']['id'] ?? null) === 'search')
+ ->assertElement('top_bar_action', fn (array $node): bool => ($node['props']['id'] ?? null) === 'settings')
+ ->assertElement('native_root_stack', fn (array $node): bool => $node['props']['back'] ?? false);
+});
+
+it('renders a top bar with the large-title display mode and a back chevron', function () {
+ Native::visit('/edge-components/top-bar-large-title')
+ ->assertScreen(TopBarLargeTitle::class)
+ ->assertNavTitle('Dashboard')
+ ->assertElement('native_root_stack', fn (array $node): bool => ($node['props']['display_mode'] ?? null) === 'large'
+ && ($node['props']['back'] ?? false));
+});
+
+it('renders a top bar with an inline search field and a back chevron', function () {
+ Native::visit('/edge-components/top-bar-search')
+ ->assertScreen(TopBarSearch::class)
+ ->assertElement('native_root_stack', fn (array $node): bool => ($node['props']['scroll_behavior'] ?? null) === 'pinned'
+ && ($node['props']['search_placeholder'] ?? null) === 'Search'
+ && ($node['props']['back'] ?? false));
+});
+
+it('renders a top bar with a destructive action and a back chevron', function () {
+ Native::visit('/edge-components/top-bar-destructive-action')
+ ->assertScreen(TopBarDestructiveAction::class)
+ ->assertNavTitle('Conversation')
+ ->assertElement('top_bar_action', fn (array $node): bool => $node['props']['destructive'] ?? false)
+ ->assertElement('native_root_stack', fn (array $node): bool => $node['props']['back'] ?? false);
+});
+
+it('renders a top bar with a logo in the title slot and a back chevron', function () {
+ Native::visit('/edge-components/top-bar-logo')
+ ->assertScreen(TopBarLogo::class)
+ ->assertElement('top_bar_title')
+ ->assertElement('image', fn (array $node): bool => ($node['props']['src'] ?? null) === 'https://nativephp.com/favicon.svg')
+ ->assertElement('native_root_stack', fn (array $node): bool => $node['props']['back'] ?? false)
+ ->assertMissingElement('top_bar_action');
+});
+
+it('renders a bottom nav with active, news, and badge items', function () {
+ Native::visit('/edge-components/bottom-nav')
+ ->assertScreen(BottomNav::class)
+ ->assertNavTitle('Bottom Nav')
+ ->assertElement('bottom_nav_item', fn (array $node): bool => ($node['props']['id'] ?? null) === 'home'
+ && ($node['props']['active'] ?? false))
+ ->assertElement('bottom_nav_item', fn (array $node): bool => ($node['props']['id'] ?? null) === 'friends'
+ && ($node['props']['news'] ?? false))
+ ->assertElement('bottom_nav_item', fn (array $node): bool => ($node['props']['id'] ?? null) === 'profile'
+ && ($node['props']['badge'] ?? null) === '3');
+});
+
+it('renders a bottom nav with a search tab item', function () {
+ Native::visit('/edge-components/bottom-nav-search-item')
+ ->assertScreen(BottomNavSearchItem::class)
+ ->assertElement('bottom_nav_item', fn (array $node): bool => ($node['props']['id'] ?? null) === 'home'
+ && ($node['props']['active'] ?? false))
+ ->assertElement('bottom_nav_item', fn (array $node): bool => ($node['props']['id'] ?? null) === 'search'
+ && ($node['props']['search'] ?? false)
+ && ($node['props']['search_placeholder'] ?? null) === 'Search')
+ ->assertElement('bottom_nav_item', fn (array $node): bool => ($node['props']['id'] ?? null) === 'profile');
+});
+
+it('renders a side nav drawer with a header, an active item, and a divider', function () {
+ Native::visit('/edge-components/side-nav')
+ ->assertScreen(SideNav::class)
+ ->assertElement('list_item', fn (array $node): bool => ($node['props']['headline'] ?? null) === 'My App'
+ && ($node['props']['supporting'] ?? null) === 'user@example.com'
+ && ($node['props']['leading_icon'] ?? null) === 'person')
+ ->assertElement('list_item', fn (array $node): bool => ($node['props']['headline'] ?? null) === 'Home'
+ && ($node['props']['leading_icon'] ?? null) === 'home')
+ ->assertElement('list_item', fn (array $node): bool => ($node['props']['headline'] ?? null) === 'Profile')
+ ->assertElement('list_item', fn (array $node): bool => ($node['props']['headline'] ?? null) === 'Settings')
+ ->assertElement('divider')
+ ->assertElement('list_item', fn (array $node): bool => ($node['props']['headline'] ?? null) === 'Help'
+ && ($node['props']['leading_icon'] ?? null) === 'help');
+});
+
+it('renders a side nav header with a background image', function () {
+ Native::visit('/edge-components/side-nav-header-image')
+ ->assertScreen(SideNavHeaderImage::class)
+ ->assertElement('side_nav_header', fn (array $node): bool => ($node['props']['image_url'] ?? null)
+ === 'https://images.nationalgeographic.org/image/upload/v1638892520/EducationHub/photos/stream-in-colorado.jpg')
+ ->assertElement('side_nav_item', fn (array $node): bool => ($node['props']['id'] ?? null) === 'home'
+ && ($node['props']['active'] ?? false));
+});