From cc1958170e86660163820e52022253a8cd741b2e Mon Sep 17 00:00:00 2001 From: Shane Rosenthal Date: Sun, 13 Sep 2026 00:04:10 -0400 Subject: [PATCH 1/3] Document the reel component; list the media-player plugin The core-plugin pages redirect to the plugin directory, which renders each plugin's README, so the video-player details live in the plugin repo and this page is a sidebar entry like the others. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01T2gmYRYfSUFo2CjVG7DhZA --- .../docs/mobile/4/edge-components/reel.md | 260 ++++++++++++++++++ .../mobile/4/plugins/core/media-player.md | 4 + 2 files changed, 264 insertions(+) create mode 100644 resources/views/docs/mobile/4/edge-components/reel.md create mode 100644 resources/views/docs/mobile/4/plugins/core/media-player.md diff --git a/resources/views/docs/mobile/4/edge-components/reel.md b/resources/views/docs/mobile/4/edge-components/reel.md new file mode 100644 index 000000000..eb30f2810 --- /dev/null +++ b/resources/views/docs/mobile/4/edge-components/reel.md @@ -0,0 +1,260 @@ +--- +title: Reel +order: 312 +--- + +## Overview + +A full-screen snap pager: the TikTok / Reels / Shorts feed. Each page is sized to the reel's own frame, and a swipe +settles on exactly one page. Paging is vertical by default; pass `horizontal` for a stories-style strip. + +`` is a paired tag and its children are the pages. For a fixed set of pages that is all you need: + +@verbatim +```blade + + @foreach ($slides as $slide) + + {{ $slide->title }} + + @endforeach + +``` +@endverbatim + +A feed is different: it has no total and you do not want every page in the tree. Ship a window of pages and tell the +reel where that window sits. See [Windowed feeds](#windowed-feeds). + +## Props + +- `count` - Items loaded so far, which is how many logical pages native lays out (optional, int, default: the + number of inline children). This is not a total; a feed has none. Grow `count` as batches arrive and the pager + grows in place. +- `page` - Page the pager should show (optional, int, default: `0`). See [Pager position](#pager-position). +- `from` - Absolute index of the first page emitted as a child (optional, int, default: `0`). +- `to` - Absolute index of the last page emitted, inclusive (optional, int). Native lays the children out from `from` + upwards, so `from` is the value that has to agree with your loop; `to` travels with it to keep the two readable + together. +- `has-more` - Append a loading page after the last loaded item (optional, boolean, default: `false`). The user can + pull into it while the next batch is fetched, instead of hitting a wall at the end of the window. Settling on it + reports `index === count`. +- `placeholders` - One image URL per loaded page index, `''` for no image (optional, array). Shown for loaded pages + this render has not shipped. See [Placeholders](#placeholders). +- `horizontal` - Page sideways instead of vertically (optional, boolean, default: `false`). +- `on-page-change` - Component method called with `(int $index)` as the current page changes. +- `a11y-label` - Accessibility label (optional) +- `a11y-hint` - Accessibility hint (optional) [Android] + +## Every page needs a `native:key` + +The root element of every page must carry `native:key`, set to an id that does not depend on the page's slot in the +shipped window: + +@verbatim +```blade + +``` +@endverbatim + +The window slides by one on every swipe. Without a key, each page picks up a new positional node id on every render, +so native rebuilds the page's whole subtree as soon as the window shifts. A video on that page then restarts from +zero, one round trip after the swipe settled. See [Subtree Reuse](../architecture/subtree-reuse#helping-the-diff-keys) +for how keys give a subtree its identity. + + + +## Pager position + +Native owns the scroll position and keeps it across re-renders, so a render that lands mid-swipe never drags the +pager back. The `page` prop only moves the pager when PHP sends an index native never reported, which is how you jump +programmatically; an echo of native's own report is ignored. + +`on-page-change` fires as soon as a page becomes the nearest one during a swipe, not when the scroll comes to rest, +so PHP's round trip overlaps the swipe animation. The handler takes a single int, the absolute page index: + +```php +public function onReelPage(int $index): void +{ + $this->setReelPage($index); +} +``` + +## Placeholders + +`placeholders` is an image URL per loaded page index, covering every loaded item and not just the shipped window. A +page PHP has not shipped yet draws its image instead of nothing, so scrolling faster than the round trip lands on a +still rather than a blank. Twenty URLs is a trivial payload, and the images are fetched and cached natively. + +Pages past `count` (the `has-more` tail) show a loading indicator instead of a placeholder. A loaded page with no +placeholder URL is transparent, so the reel's own `bg-*` shows through. + +## Windowed feeds + +The `HasReelPage` trait holds the windowing state and leaves you the fetch. Native reports each page change, your +handler records it, and the next render emits the pages around it. + +```php +use App\Services\Feed; +use Illuminate\View\View; +use Native\Mobile\Edge\NativeComponent; +use Native\Mobile\UI\Concerns\HasReelPage; + +class ReelFeed extends NativeComponent +{ + use HasReelPage; + + /** Clips loaded so far, in feed order. */ + public array $items = []; + + /** Opaque cursor from your API. */ + public ?string $cursor = null; + + public function mount(): void + { + $this->loadMore(); + } + + public function onReelPage(int $index): void + { + $this->setReelPage($index); + + if ($this->reelNeedsMore()) { + $this->loadMore(); + } + } + + private function loadMore(): void + { + $batch = Feed::clips(cursor: $this->cursor); + + $this->items = [...$this->items, ...$batch->clips]; + $this->cursor = $batch->nextCursor; + $this->extendReel(count($batch->clips), hasMore: $batch->nextCursor !== null); + } + + public function render(): View + { + return view('native.reel-feed', [ + 'items' => $this->items, + 'placeholders' => array_map(fn ($clip) => $clip['poster'] ?? '', $this->items), + 'loaded' => $this->reelLoaded, + 'hasMore' => $this->reelHasMore, + 'page' => $this->reelPage, + 'from' => $this->reelWindowFrom(), + 'to' => $this->reelWindowTo(), + ]); + } +} +``` + +The view emits only the window, so the loop runs over indexes rather than the collection: + +@verbatim +```blade + + @for ($index = $from; $index <= $to; $index++) + @include('native.reel-feed-page', ['index' => $index]) + @endfor + +``` +@endverbatim + +And the page itself, keyed by its absolute index: + +@verbatim +```blade static +{{-- resources/views/native/reel-feed-page.blade.php --}} +@php $clip = $items[$index]; @endphp + + + + + + {{ $clip['handle'] }} + {{ $clip['caption'] }} + + +``` +@endverbatim + +### `HasReelPage` state + +- `$reelPage` - Absolute index of the page currently on screen (int, default: `0`) +- `$reelWindow` - Pages shipped either side of the current one (int, default: `2`). Two keeps a fast second swipe on + device: the page after next is already there while the round trip for the settle is still in flight. Each shipped + page is rendered Blade plus a buffering video, so keep it small. +- `$reelLoaded` - Items fetched so far, which is the reel's `count` (int, default: `0`) +- `$reelHasMore` - Whether another batch can be fetched, which drives the trailing loading page (bool, default: + `true`) + +### `HasReelPage` methods + +- `setReelPage(int $index)` - Record the page native reported +- `extendReel(int $added, bool $hasMore = true)` - Record a fetched batch: grows the pager and updates the tail state +- `reelNeedsMore(int $threshold = 3)` - True when the current page is within `$threshold` items of the end of what is + loaded, or on the loading page itself, and more can be fetched. Three is the usual feed default: the PHP round trip + plus the API call has to land before the user swipes there. +- `reelWindowFrom()` - First page index to emit +- `reelWindowTo()` - Last page index to emit, inclusive, clamped to what is loaded + +## Pairing with video + +A [``](../plugins/core/media-player) on a reel page needs no coordination with the pager. With +`autoplay` the surface plays while it is at least 45% on screen and pauses below that, so the page you are watching +plays and its shipped neighbours sit loaded and silent. Nothing in the reel renderer knows about playback. + +Give each page `:controls="false"` for a bare surface you can overlay your own EDGE elements on, and a `poster` so +the page shows a still the instant it exists rather than black. The playing surface is the one the `MediaPlayer` +facade drives, so `MediaPlayer::pause()` from a tap handler pauses the page on screen without passing a page id. + +## Element + +```php +use Native\Mobile\UI\Elements\Reel; + +Reel::make($page1, $page2, $page3) + ->count($loaded) + ->page($current) + ->hasMore() + ->placeholders($posters) + ->onPageChange('onReelPage'); +``` + +- `make(Element ...$children)` - Create a reel whose children are the pages +- `count(int $count)` - Items loaded so far +- `page(int $index)` - Page the pager should show +- `horizontal(bool $value = true)` - Page sideways +- `hasMore(bool $value = true)` - Append the trailing loading page +- `placeholders(array $urls)` - One image URL per loaded page index +- `onPageChange(string $method)` - Page-change handler +- `a11yLabel(string $value)` - Accessibility label +- `a11yHint(string $value)` - Accessibility hint + + diff --git a/resources/views/docs/mobile/4/plugins/core/media-player.md b/resources/views/docs/mobile/4/plugins/core/media-player.md new file mode 100644 index 000000000..78797c080 --- /dev/null +++ b/resources/views/docs/mobile/4/plugins/core/media-player.md @@ -0,0 +1,4 @@ +--- +title: Media Player +order: 800 +--- From 9c77c1600d51428a915f833c3b69209d02bdc6ed Mon Sep 17 00:00:00 2001 From: Shane Rosenthal Date: Tue, 15 Sep 2026 14:02:10 -0400 Subject: [PATCH 2/3] Rename the reel docs page to pager to match the component Co-Authored-By: Claude Fable 5.1 --- .../4/edge-components/{reel.md => pager.md} | 94 +++++++++---------- 1 file changed, 47 insertions(+), 47 deletions(-) rename resources/views/docs/mobile/4/edge-components/{reel.md => pager.md} (72%) diff --git a/resources/views/docs/mobile/4/edge-components/reel.md b/resources/views/docs/mobile/4/edge-components/pager.md similarity index 72% rename from resources/views/docs/mobile/4/edge-components/reel.md rename to resources/views/docs/mobile/4/edge-components/pager.md index eb30f2810..632d465d2 100644 --- a/resources/views/docs/mobile/4/edge-components/reel.md +++ b/resources/views/docs/mobile/4/edge-components/pager.md @@ -1,29 +1,29 @@ --- -title: Reel +title: Pager order: 312 --- ## Overview -A full-screen snap pager: the TikTok / Reels / Shorts feed. Each page is sized to the reel's own frame, and a swipe +A full-screen snap pager: the TikTok / Reels / Shorts feed. Each page is sized to the pager's own frame, and a swipe settles on exactly one page. Paging is vertical by default; pass `horizontal` for a stories-style strip. -`` is a paired tag and its children are the pages. For a fixed set of pages that is all you need: +`` is a paired tag and its children are the pages. For a fixed set of pages that is all you need: @verbatim ```blade - + @foreach ($slides as $slide) {{ $slide->title }} @endforeach - + ``` @endverbatim A feed is different: it has no total and you do not want every page in the tree. Ship a window of pages and tell the -reel where that window sits. See [Windowed feeds](#windowed-feeds). +pager where that window sits. See [Windowed feeds](#windowed-feeds). ## Props @@ -52,7 +52,7 @@ shipped window: @verbatim ```blade - + ``` @endverbatim @@ -78,9 +78,9 @@ programmatically; an echo of native's own report is ignored. so PHP's round trip overlaps the swipe animation. The handler takes a single int, the absolute page index: ```php -public function onReelPage(int $index): void +public function onPagerPage(int $index): void { - $this->setReelPage($index); + $this->setPagerPage($index); } ``` @@ -91,22 +91,22 @@ page PHP has not shipped yet draws its image instead of nothing, so scrolling fa still rather than a blank. Twenty URLs is a trivial payload, and the images are fetched and cached natively. Pages past `count` (the `has-more` tail) show a loading indicator instead of a placeholder. A loaded page with no -placeholder URL is transparent, so the reel's own `bg-*` shows through. +placeholder URL is transparent, so the pager's own `bg-*` shows through. ## Windowed feeds -The `HasReelPage` trait holds the windowing state and leaves you the fetch. Native reports each page change, your +The `HasPagerWindow` trait holds the windowing state and leaves you the fetch. Native reports each page change, your handler records it, and the next render emits the pages around it. ```php use App\Services\Feed; use Illuminate\View\View; use Native\Mobile\Edge\NativeComponent; -use Native\Mobile\UI\Concerns\HasReelPage; +use Native\Mobile\UI\Concerns\HasPagerWindow; class ReelFeed extends NativeComponent { - use HasReelPage; + use HasPagerWindow; /** Clips loaded so far, in feed order. */ public array $items = []; @@ -119,11 +119,11 @@ class ReelFeed extends NativeComponent $this->loadMore(); } - public function onReelPage(int $index): void + public function onPagerPage(int $index): void { - $this->setReelPage($index); + $this->setPagerPage($index); - if ($this->reelNeedsMore()) { + if ($this->pagerNeedsMore()) { $this->loadMore(); } } @@ -134,19 +134,19 @@ class ReelFeed extends NativeComponent $this->items = [...$this->items, ...$batch->clips]; $this->cursor = $batch->nextCursor; - $this->extendReel(count($batch->clips), hasMore: $batch->nextCursor !== null); + $this->extendPager(count($batch->clips), hasMore: $batch->nextCursor !== null); } public function render(): View { - return view('native.reel-feed', [ + return view('native.pager-feed', [ 'items' => $this->items, 'placeholders' => array_map(fn ($clip) => $clip['poster'] ?? '', $this->items), - 'loaded' => $this->reelLoaded, - 'hasMore' => $this->reelHasMore, - 'page' => $this->reelPage, - 'from' => $this->reelWindowFrom(), - 'to' => $this->reelWindowTo(), + 'loaded' => $this->pagerLoaded, + 'hasMore' => $this->pagerHasMore, + 'page' => $this->pagerPage, + 'from' => $this->pagerWindowFrom(), + 'to' => $this->pagerWindowTo(), ]); } } @@ -156,7 +156,7 @@ The view emits only the window, so the loop runs over indexes rather than the co @verbatim ```blade - @for ($index = $from; $index <= $to; $index++) - @include('native.reel-feed-page', ['index' => $index]) + @include('native.pager-feed-page', ['index' => $index]) @endfor - + ``` @endverbatim @@ -178,10 +178,10 @@ And the page itself, keyed by its absolute index: @verbatim ```blade static -{{-- resources/views/native/reel-feed-page.blade.php --}} +{{-- resources/views/native/pager-feed-page.blade.php --}} @php $clip = $items[$index]; @endphp - + `](../plugins/core/media-player) on a reel page needs no coordination with the pager. With +A [``](../plugins/core/media-player) on a pager page needs no coordination with the pager. With `autoplay` the surface plays while it is at least 45% on screen and pauses below that, so the page you are watching -plays and its shipped neighbours sit loaded and silent. Nothing in the reel renderer knows about playback. +plays and its shipped neighbours sit loaded and silent. Nothing in the pager renderer knows about playback. Give each page `:controls="false"` for a bare surface you can overlay your own EDGE elements on, and a `poster` so the page shows a still the instant it exists rather than black. The playing surface is the one the `MediaPlayer` @@ -232,17 +232,17 @@ facade drives, so `MediaPlayer::pause()` from a tap handler pauses the page on s ## Element ```php -use Native\Mobile\UI\Elements\Reel; +use Native\Mobile\UI\Elements\Pager; -Reel::make($page1, $page2, $page3) +Pager::make($page1, $page2, $page3) ->count($loaded) ->page($current) ->hasMore() ->placeholders($posters) - ->onPageChange('onReelPage'); + ->onPageChange('onPagerPage'); ``` -- `make(Element ...$children)` - Create a reel whose children are the pages +- `make(Element ...$children)` - Create a pager whose children are the pages - `count(int $count)` - Items loaded so far - `page(int $index)` - Page the pager should show - `horizontal(bool $value = true)` - Page sideways @@ -254,7 +254,7 @@ Reel::make($page1, $page2, $page3) From b8e1cca1631359893e69dfaaa96d6920fb41ba4c Mon Sep 17 00:00:00 2001 From: Shane Rosenthal Date: Tue, 15 Sep 2026 15:09:05 -0400 Subject: [PATCH 3/3] Pager docs: shorter sentences, fewer flourishes Co-Authored-By: Claude Fable 5.1 --- .../docs/mobile/4/edge-components/pager.md | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/resources/views/docs/mobile/4/edge-components/pager.md b/resources/views/docs/mobile/4/edge-components/pager.md index 632d465d2..348093f5e 100644 --- a/resources/views/docs/mobile/4/edge-components/pager.md +++ b/resources/views/docs/mobile/4/edge-components/pager.md @@ -5,8 +5,9 @@ order: 312 ## Overview -A full-screen snap pager: the TikTok / Reels / Shorts feed. Each page is sized to the pager's own frame, and a swipe -settles on exactly one page. Paging is vertical by default; pass `horizontal` for a stories-style strip. +A full-screen snap pager, the kind TikTok, Reels and Shorts are built on. Each page is the size of the pager's own +frame and a swipe settles on exactly one of them. Paging is vertical by default. Pass `horizontal` for a +stories-style strip. `` is a paired tag and its children are the pages. For a fixed set of pages that is all you need: @@ -22,8 +23,8 @@ settles on exactly one page. Paging is vertical by default; pass `horizontal` fo ``` @endverbatim -A feed is different: it has no total and you do not want every page in the tree. Ship a window of pages and tell the -pager where that window sits. See [Windowed feeds](#windowed-feeds). +A feed is different. It has no total, and you do not want every page in the tree. Ship a window of pages and tell +the pager where that window sits. See [Windowed feeds](#windowed-feeds). ## Props @@ -33,8 +34,7 @@ pager where that window sits. See [Windowed feeds](#windowed-feeds). - `page` - Page the pager should show (optional, int, default: `0`). See [Pager position](#pager-position). - `from` - Absolute index of the first page emitted as a child (optional, int, default: `0`). - `to` - Absolute index of the last page emitted, inclusive (optional, int). Native lays the children out from `from` - upwards, so `from` is the value that has to agree with your loop; `to` travels with it to keep the two readable - together. + upwards, so `from` is the value that has to agree with your loop. `to` is there so the pair reads as one range. - `has-more` - Append a loading page after the last loaded item (optional, boolean, default: `false`). The user can pull into it while the next batch is fetched, instead of hitting a wall at the end of the window. Settling on it reports `index === count`. @@ -70,9 +70,9 @@ A bare `key` attribute is not the same thing and is silently ignored. It has to ## Pager position -Native owns the scroll position and keeps it across re-renders, so a render that lands mid-swipe never drags the -pager back. The `page` prop only moves the pager when PHP sends an index native never reported, which is how you jump -programmatically; an echo of native's own report is ignored. +Native owns the scroll position and keeps it across re-renders. A render that lands mid-swipe never drags the pager +back. The `page` prop only moves the pager when PHP sends an index native never reported. That is how you jump +programmatically, and an echo of native's own report is ignored. `on-page-change` fires as soon as a page becomes the nearest one during a swipe, not when the scroll comes to rest, so PHP's round trip overlaps the swipe animation. The handler takes a single int, the absolute page index: @@ -86,9 +86,9 @@ public function onPagerPage(int $index): void ## Placeholders -`placeholders` is an image URL per loaded page index, covering every loaded item and not just the shipped window. A -page PHP has not shipped yet draws its image instead of nothing, so scrolling faster than the round trip lands on a -still rather than a blank. Twenty URLs is a trivial payload, and the images are fetched and cached natively. +`placeholders` is an image URL per loaded page index, for every loaded item and not just the shipped window. A page +PHP has not shipped yet shows its image, so flicking faster than the round trip lands on a still instead of a blank. +Twenty URLs is a trivial payload, and the images are fetched and cached natively. Pages past `count` (the `has-more` tail) show a loading indicator instead of a placeholder. A loaded page with no placeholder URL is transparent, so the pager's own `bg-*` shows through. @@ -222,12 +222,12 @@ And the page itself, keyed by its absolute index: ## Pairing with video A [``](../plugins/core/media-player) on a pager page needs no coordination with the pager. With -`autoplay` the surface plays while it is at least 45% on screen and pauses below that, so the page you are watching -plays and its shipped neighbours sit loaded and silent. Nothing in the pager renderer knows about playback. +`autoplay` the surface plays while it is at least 45% on screen and pauses below that. Only the page you are on +plays; the neighbours PHP shipped sit loaded and silent. Nothing in the pager renderer knows about playback. -Give each page `:controls="false"` for a bare surface you can overlay your own EDGE elements on, and a `poster` so -the page shows a still the instant it exists rather than black. The playing surface is the one the `MediaPlayer` -facade drives, so `MediaPlayer::pause()` from a tap handler pauses the page on screen without passing a page id. +Give each page `:controls="false"` for a bare surface you can overlay your own EDGE elements on, and a `poster` so a +page shows a still from its first frame instead of black. The playing surface is the one the `MediaPlayer` facade +drives, so `MediaPlayer::pause()` from a tap handler pauses the page on screen without passing a page id. ## Element