From 30965de94b2b3b81cec2577badc830a5b591412e Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Mon, 19 Oct 2026 19:20:08 +0100 Subject: [PATCH 1/2] Draw the graph's dashed lines as native dashed borders EDGE can now draw border-dashed, per-side borders and dashed rings (NativePHP/mobile-air#548), so each dashed line, ring, elbow and box is one view instead of a column of tiny ones. The lines get their length from layout, so Display::dashesFor() and Display::elbowDashes(), which guessed row heights and placed the elbow's dashes by hand, are gone. The partials now take the lane and draw it through Lane::token(), as the solid rail does. The dashed box gets the rounded corners the web gives it. Co-Authored-By: Claude Opus 5.5 --- app/Devnull/Display.php | 64 +------------------ .../partials/commit-row-content.blade.php | 1 - .../native/partials/dashed-box.blade.php | 21 +----- .../native/partials/dashed-line.blade.php | 13 +--- .../native/partials/dashed-ring.blade.php | 14 +--- .../native/partials/earlier-commits.blade.php | 2 +- .../native/partials/rail-avatar.blade.php | 2 +- .../views/native/partials/rail.blade.php | 8 +-- .../native/partials/thread-elbow.blade.php | 17 +---- .../native/partials/thread-links.blade.php | 6 +- .../native/partials/thread-reply.blade.php | 5 +- resources/views/native/this-device.blade.php | 2 +- tests/Feature/Screens/CommitDetailTest.php | 6 +- tests/Feature/Screens/ReplyPreviewTest.php | 9 ++- tests/Unit/Devnull/DisplayTest.php | 23 ------- 15 files changed, 31 insertions(+), 162 deletions(-) diff --git a/app/Devnull/Display.php b/app/Devnull/Display.php index de251c8..14ec171 100644 --- a/app/Devnull/Display.php +++ b/app/Devnull/Display.php @@ -2,7 +2,6 @@ namespace App\Devnull; -use App\Devnull\Data\Commit; use Carbon\CarbonImmutable; use Carbon\CarbonInterface; @@ -37,12 +36,10 @@ final class Display /** * How far below the top of a reply's row its elbow comes level with the - * middle of the reply's avatar, and the radius of the elbow's corner. + * middle of the reply's avatar. */ public const int ElbowLevel = 22; - public const int ElbowRadius = 12; - public static function relativeTime(?CarbonInterface $date, ?CarbonInterface $now = null): string { if ($date === null) { @@ -146,65 +143,6 @@ public static function maskedKey(string $openSsh): string return implode(' ', $parts); } - /** - * Enough 4pt dashes, with 3pt gaps, to run a dashed line down a commit's - * row, which is clipped where the row ends. PHP can't know how tall the - * row is drawn, so this errs long from the text, media and actions in it. - */ - public static function dashesFor(Commit $commit): int - { - $text = $commit->isFolded() ? Commit::PreviewLength : mb_strlen($commit->body); - $lines = (int) ceil($text / 32) + substr_count($commit->isFolded() ? '' : $commit->body, "\n"); - $height = 110 + $lines * 26 - + ($commit->images === [] ? 0 : 210) - + ($commit->poll === null ? 0 : 50 + 46 * count($commit->poll->options)) - + ($commit->isQuote() ? 230 : 0) - + ($commit->linkPreview === null ? 0 : 80); - - return min(250, (int) ceil($height / 7)); - } - - /** - * The dashes of a thread's elbow, as the web's from #121: down the line at - * $from, round a 12pt corner, and across to the reply's avatar at $to, - * level with its middle. They keep the 4pt dash and 3pt gap all the way - * round, with one on the middle of the bend so the corner reads the same - * from either side, and the ones on the bend turned to follow it. The last - * one may run past 1pt short of the avatar, where the elbow is cut off, as - * a dashed border would be. - * - * @return list each dash's middle, and how far it's turned from upright, in degrees - */ - public static function elbowDashes(float $from, float $to): array - { - $line = $from + 0.5; - $down = self::ElbowLevel - self::ElbowRadius; - $bend = M_PI * self::ElbowRadius / 2; - $length = $down + $bend + ($to - 1) - ($line + self::ElbowRadius); - - $dashes = []; - - for ($middle = fmod($down + $bend / 2, 7); $middle - 2 < $length; $middle += 7) { - if ($middle < 2) { - continue; - } - - $turn = min(M_PI / 2, max(0, ($middle - $down) / self::ElbowRadius)); - - $dashes[] = match (true) { - $middle <= $down => ['x' => $line, 'y' => round($middle, 2), 'rotate' => 0.0], - $middle >= $down + $bend => ['x' => round($line + self::ElbowRadius + $middle - $down - $bend, 2), 'y' => (float) self::ElbowLevel, 'rotate' => -90.0], - default => [ - 'x' => round($line + self::ElbowRadius - self::ElbowRadius * cos($turn), 2), - 'y' => round($down + self::ElbowRadius * sin($turn), 2), - 'rotate' => round(-rad2deg($turn), 2), - ], - }; - } - - return $dashes; - } - /** * How far a commit page has to scroll past its last reply to clear the * reply bar. A pushed screen is drawn under its bottom bar and the home diff --git a/resources/views/native/partials/commit-row-content.blade.php b/resources/views/native/partials/commit-row-content.blade.php index 3bcaa5a..21c8edd 100644 --- a/resources/views/native/partials/commit-row-content.blade.php +++ b/resources/views/native/partials/commit-row-content.blade.php @@ -92,7 +92,6 @@ 'tail' => ($inFeed ? $continuesNext || ! $continuesRun : $withTail) ? $commit->lane() : null, 'endsWithDot' => $endsWithDot, 'replyTail' => $threadBelow && ! ($inFeed && $continuesNext), - 'replyDashes' => \App\Devnull\Display::dashesFor($commit), 'node' => $continuesRun ? 'dot' : 'avatar', 'avatarSize' => $muted ? 36 : 48, 'user' => $author, diff --git a/resources/views/native/partials/dashed-box.blade.php b/resources/views/native/partials/dashed-box.blade.php index 7f73550..68e477c 100644 --- a/resources/views/native/partials/dashed-box.blade.php +++ b/resources/views/native/partials/dashed-box.blade.php @@ -1,20 +1,5 @@ {{-- - A 1pt dashed border drawn over the box it's placed last in, standing in for the web's `border-dashed`, since EDGE - has no dashed borders: a row of dashes along the top and bottom, a column down each side. The corners are square - where the web rounds them, as dashes can't follow a curve here without being placed one by one. + A 1pt dashed border, 8pt rounded, drawn over the box it's placed last in, as the web's `border border-dashed`. --}} -@php($colour ??= 'bg-theme-hairline') -@foreach (['top-0', 'bottom-0'] as $edge) - - @foreach (range(1, 40) as $dash) - - @endforeach - -@endforeach -@foreach (['left-0', 'right-0'] as $edge) - - @foreach (range(1, 6) as $dash) - - @endforeach - -@endforeach +@php($colour ??= 'border-theme-hairline') + diff --git a/resources/views/native/partials/dashed-line.blade.php b/resources/views/native/partials/dashed-line.blade.php index 19aabdf..c45ac3d 100644 --- a/resources/views/native/partials/dashed-line.blade.php +++ b/resources/views/native/partials/dashed-line.blade.php @@ -1,12 +1,5 @@ {{-- - A 1pt vertical line in dashes, standing in for the web's `border-l border-dashed`, since EDGE has no dashed borders. - $position places it (top, bottom or height classes) and $left is its x in points. Each dash is 4pt with a 3pt gap, - so a line of a height PHP can't know is drawn with $dashes enough for it and clipped to its box: any corner radius - makes EDGE clip a node to its shape, so a hairline radius is enough. + A 1pt vertical line in dashes of $lane's colour, as the web's `border-l border-dashed`: 4pt dashes with 3pt gaps, + starting at its top and running as far as $position (top, bottom or height classes) lets it. $left is its x in points. --}} -@php($dashes ??= 4) - - @foreach (range(1, max(1, $dashes)) as $dash) - - @endforeach - + diff --git a/resources/views/native/partials/dashed-ring.blade.php b/resources/views/native/partials/dashed-ring.blade.php index 2f29972..97d43a1 100644 --- a/resources/views/native/partials/dashed-ring.blade.php +++ b/resources/views/native/partials/dashed-ring.blade.php @@ -1,14 +1,4 @@ {{-- - A 2pt ring drawn in dashes, standing in for the web's `border-dashed` since EDGE has no dashed borders: each dash - is placed on the circle and turned to follow it. + A 2pt ring in dashes of $lane's colour, as the web's `border-2 border-dashed rounded-full`, $size across. --}} -@php($segments = 16) -@php($radius = $size / 2 - 1) - - @foreach (range(0, $segments - 1) as $index) - @php($angle = 360 / $segments * $index) - @php($x = $size / 2 + $radius * sin(deg2rad($angle))) - @php($y = $size / 2 - $radius * cos(deg2rad($angle))) - - @endforeach - + diff --git a/resources/views/native/partials/earlier-commits.blade.php b/resources/views/native/partials/earlier-commits.blade.php index 54f56dd..a33b471 100644 --- a/resources/views/native/partials/earlier-commits.blade.php +++ b/resources/views/native/partials/earlier-commits.blade.php @@ -19,7 +19,7 @@ - @include('native.partials.dashed-line', ['position' => 'top-0 h-[13]', 'left' => 31.5, 'colour' => 'bg-theme-rail', 'dashes' => 2]) + @include('native.partials.dashed-line', ['position' => 'top-0 h-[13]', 'left' => 31.5, 'lane' => \App\Devnull\Lane::Rail]) @include('native.partials.rail-node', ['lane' => $lane, 'filled' => false]) diff --git a/resources/views/native/partials/rail-avatar.blade.php b/resources/views/native/partials/rail-avatar.blade.php index 18a5407..38b2a80 100644 --- a/resources/views/native/partials/rail-avatar.blade.php +++ b/resources/views/native/partials/rail-avatar.blade.php @@ -7,7 +7,7 @@ @php($size ??= 48) @if ($staged) - @include('native.partials.dashed-ring', ['size' => 48, 'colour' => 'bg-theme-'.\App\Devnull\Lane::token($lane)]) + @include('native.partials.dashed-ring', ['size' => 48, 'lane' => $lane]) @include('native.partials.avatar', ['user' => $user, 'size' => 40]) diff --git a/resources/views/native/partials/rail.blade.php b/resources/views/native/partials/rail.blade.php index 6f5e3fe..0146fe1 100644 --- a/resources/views/native/partials/rail.blade.php +++ b/resources/views/native/partials/rail.blade.php @@ -12,7 +12,6 @@ null no node; a $previous segment then runs the full height of the row $endsWithDot stops the $tail 20pt above the bottom at a lane dot, as the web ends a commit that's alone in its run. $replyTail runs a dashed line in the lane on down from the node, or from that dot, to the replies under the row. - It's $replyDashes long when it starts at the node, enough for the row, since PHP can't know its height. $avatarSize draws the avatar's ring smaller than its 48pt, centred in the same place. --}} @php($previous ??= null) @@ -22,7 +21,6 @@ @php($tailDashes ??= 3) @php($endsWithDot ??= false) @php($replyTail ??= false) -@php($replyDashes ??= 40) @php($avatarSize ??= 48) @php($center = 32) @php($endDot = 20) @@ -48,9 +46,9 @@ @endif @if ($replyTail && $endsWithDot) - @include('native.partials.dashed-line', ['position' => 'bottom-0 h-['.$endDot.']', 'left' => 31.5, 'colour' => $segment($lane), 'dashes' => 3]) + @include('native.partials.dashed-line', ['position' => 'bottom-0 h-['.$endDot.']', 'left' => 31.5, 'lane' => $lane]) @elseif ($replyTail) - @include('native.partials.dashed-line', ['position' => 'top-['.($node === 'dot' ? $center - 9 : $center).'] bottom-0', 'left' => 31.5, 'colour' => $segment($lane), 'dashes' => $replyDashes]) + @include('native.partials.dashed-line', ['position' => 'top-['.($node === 'dot' ? $center - 9 : $center).'] bottom-0', 'left' => 31.5, 'lane' => $lane]) @endif @if ($endsWithDot && $tail !== null) @@ -81,7 +79,7 @@ @include('native.partials.rail-avatar', ['user' => $user, 'lane' => $lane, 'staged' => $node === 'staged']) @elseif ($node === 'tombstone') - @include('native.partials.dashed-ring', ['size' => 48, 'colour' => 'bg-theme-rail']) + @include('native.partials.dashed-ring', ['size' => 48, 'lane' => \App\Devnull\Lane::Rail]) @elseif ($node === 'skeleton') diff --git a/resources/views/native/partials/thread-elbow.blade.php b/resources/views/native/partials/thread-elbow.blade.php index db8ebbc..063ab96 100644 --- a/resources/views/native/partials/thread-elbow.blade.php +++ b/resources/views/native/partials/thread-elbow.blade.php @@ -1,21 +1,8 @@ {{-- Where a thread leaves the rail, as the web's thread elbow from #121: a dashed line in the commit's lane down from the rail ($from), round a 12pt corner, and across to the first reply's avatar ($to), level with its middle. A - nested reply's elbow leaves the spine of the reply above it the same way. EDGE has no dashed borders, so each dash - is placed along the way (see Display::elbowDashes), and the ones on the corner are turned to follow it, as in - dashed-ring. Any corner radius makes EDGE clip a node to its shape, so the last dash is cut 1pt short of the avatar. + nested reply's elbow leaves the spine of the reply above it the same way. It stops 1pt short of the avatar. --}} -@php($colour = 'bg-theme-'.\App\Devnull\Lane::token($lane)) @php($from ??= 31.5) @php($to ??= 64) - - @foreach (\App\Devnull\Display::elbowDashes($from, $to) as $dash) - @if ($dash['rotate'] === -90.0) - - @elseif ($dash['rotate'] === 0.0) - - @else - - @endif - @endforeach - + diff --git a/resources/views/native/partials/thread-links.blade.php b/resources/views/native/partials/thread-links.blade.php index 851bd74..858869c 100644 --- a/resources/views/native/partials/thread-links.blade.php +++ b/resources/views/native/partials/thread-links.blade.php @@ -7,16 +7,16 @@ @endif @if ($spine !== null) - @include('native.partials.dashed-line', ['position' => 'top-0 bottom-0', 'left' => 75.5, 'colour' => $segment($spine), 'dashes' => \App\Devnull\Display::dashesFor($reply)]) + @include('native.partials.dashed-line', ['position' => 'top-0 bottom-0', 'left' => 75.5, 'lane' => $spine]) @endif @if ($previousLane === null) @include('native.partials.thread-elbow', ['lane' => $lane, 'from' => $indent > 0 ? 75.5 : 31.5, 'to' => 64 + $indent]) @else - @include('native.partials.dashed-line', ['position' => 'top-0 h-[6]', 'left' => $avatarLine - 0.5, 'colour' => $segment($previousLane), 'dashes' => 1]) + @include('native.partials.dashed-line', ['position' => 'top-0 h-[6]', 'left' => $avatarLine - 0.5, 'lane' => $previousLane]) @endif @if ($hasNext) {{-- Down to the next reply, or to the top of Show more's dot: 12pt under it, its padding and the hairline below. --}} - @include('native.partials.dashed-line', ['position' => 'top-[38] '.($more !== null ? 'bottom-[34]' : 'bottom-0'), 'left' => $avatarLine - 0.5, 'colour' => $segment($reply->lane()), 'dashes' => \App\Devnull\Display::dashesFor($reply)]) + @include('native.partials.dashed-line', ['position' => 'top-[38] '.($more !== null ? 'bottom-[34]' : 'bottom-0'), 'left' => $avatarLine - 0.5, 'lane' => $reply->lane()]) @endif diff --git a/resources/views/native/partials/thread-reply.blade.php b/resources/views/native/partials/thread-reply.blade.php index a36b9c6..1c0027d 100644 --- a/resources/views/native/partials/thread-reply.blade.php +++ b/resources/views/native/partials/thread-reply.blade.php @@ -23,7 +23,6 @@ @php($spine ??= null) @php($tombstones ??= false) @php($more ??= null) -@php($segment = fn (int|string $lane): string => 'bg-theme-'.\App\Devnull\Lane::token($lane)) @php($avatarLine = 76 + $indent) @if ($reply->isDeleted()) @php($opened = $tombstones && array_key_exists($reply->id, $tombstoneReplies ?? [])) @@ -32,7 +31,7 @@ - @include('native.partials.dashed-ring', ['size' => 24, 'colour' => 'bg-theme-rail']) + @include('native.partials.dashed-ring', ['size' => 24, 'lane' => \App\Devnull\Lane::Rail]) @@ -58,7 +57,7 @@ @include('native.partials.thread-links') @if ($opened && ($tombstoneReplies[$reply->id] ?? []) !== []) {{-- The spine its own replies hang off, down from its ring. --}} - @include('native.partials.dashed-line', ['position' => 'top-[38] bottom-0', 'left' => $avatarLine - 0.5, 'colour' => 'bg-theme-rail', 'dashes' => 6]) + @include('native.partials.dashed-line', ['position' => 'top-[38] bottom-0', 'left' => $avatarLine - 0.5, 'lane' => \App\Devnull\Lane::Rail]) @endif @else diff --git a/resources/views/native/this-device.blade.php b/resources/views/native/this-device.blade.php index 2879e9d..b642413 100644 --- a/resources/views/native/this-device.blade.php +++ b/resources/views/native/this-device.blade.php @@ -89,7 +89,7 @@ @if ($option['auto']) - @include('native.partials.dashed-ring', ['size' => 28, 'colour' => 'bg-theme-'.Lane::token($option['lane'])]) + @include('native.partials.dashed-ring', ['size' => 28, 'lane' => $option['lane']]) @elseif ($option['lane'] === null) diff --git a/tests/Feature/Screens/CommitDetailTest.php b/tests/Feature/Screens/CommitDetailTest.php index 08719dc..fde4da3 100644 --- a/tests/Feature/Screens/CommitDetailTest.php +++ b/tests/Feature/Screens/CommitDetailTest.php @@ -279,10 +279,10 @@ function fakeThread(array $commit = [], array $replies = [], array $extra = []): // A dashed Rail Gray ring where the avatar was, as on the web, and a Rail Gray segment running on from it. $tombstone = nodeByRef($screen, 'tombstone-c1'); - $dashes = findNodes($tombstone, fn (array $node): bool => isset($node['props']['rotate'])); + $rings = findNodes($tombstone, fn (array $node): bool => ($node['props']['border_style'] ?? null) === 1); - expect($dashes)->toHaveCount(16) - ->and(array_unique(array_map(fn (array $dash): ?string => $dash['style']['bg_color'] ?? null, $dashes)))->toBe([$rail]) + expect($rings)->toHaveCount(1) + ->and($rings[0]['style'])->toMatchArray(['border_width' => 2.0, 'border_color' => $rail, 'border_radius' => 9999.0]) ->and(findNode($tombstone, fn (array $node): bool => ($node['type'] ?? null) === 'stack' && ($node['layout']['width'] ?? null) === 40.0))->toBeNull(); }); diff --git a/tests/Feature/Screens/ReplyPreviewTest.php b/tests/Feature/Screens/ReplyPreviewTest.php index 41ad87f..fa04a0a 100644 --- a/tests/Feature/Screens/ReplyPreviewTest.php +++ b/tests/Feature/Screens/ReplyPreviewTest.php @@ -34,15 +34,15 @@ function previewedJson(array $replies, bool $more = false, array $overrides = [] } /** - * The colours of the dashes in a node, in order. + * The colours of the dashed lines in a node, in order. * * @return list */ function dashColours(array $node): array { return array_values(array_unique(array_map( - fn (array $dash): string => $dash['style']['bg_color'] ?? '', - findNodes($node, fn (array $child): bool => in_array([$child['layout']['width'] ?? null, $child['layout']['height'] ?? null], [[1.0, 4.0], [4.0, 1.0]], true)), + fn (array $line): string => $line['style']['border_color'] ?? '', + findNodes($node, fn (array $child): bool => ($child['props']['border_style'] ?? null) === 1), ))); } @@ -62,6 +62,9 @@ function dashColours(array $node): array expect($elbow)->not->toBeNull() ->and(dashColours($elbow))->toBe([config('native-ui.theme.light.lane-1')]) + // One dashed border down from the rail, round a 12pt corner and across to 1pt short of the avatar. + ->and($elbow['props'])->toMatchArray(['border_l' => 1.0, 'border_b' => 1.0, 'border_t' => 0.0, 'border_r' => 0.0, 'radius_bl' => 12.0]) + ->and($elbow['layout'])->toMatchArray(['width' => 31.5, 'height' => 22.5, 'position' => [0.0, 0.0, 0.0, 31.5]]) // A reply's own actions: the star and Reply, but no Quote. ->and(findNode($reply, fn (array $node): bool => ($node['ref'] ?? null) === 'reply-to-r9'))->not->toBeNull() ->and(findNode($reply, fn (array $node): bool => ($node['ref'] ?? null) === 'quote-r9'))->toBeNull(); diff --git a/tests/Unit/Devnull/DisplayTest.php b/tests/Unit/Devnull/DisplayTest.php index 39bb488..f52c723 100644 --- a/tests/Unit/Devnull/DisplayTest.php +++ b/tests/Unit/Devnull/DisplayTest.php @@ -35,29 +35,6 @@ expect(Display::shortHash('9b2f6c1e-1d3a-4e5f-8a7b-0c1d2e3f4a5b'))->toBe('e3f4a5b'); }); -it("turns a thread elbow's dashes with its corner, one on the middle of the bend and the rest mirrored around it", function () { - $dashes = Display::elbowDashes(31.5, 64); - $bend = array_values(array_filter($dashes, fn (array $dash): bool => $dash['rotate'] < 0 && $dash['rotate'] > -90)); - - expect($dashes[0])->toBe(['x' => 32.0, 'y' => 5.42, 'rotate' => 0.0]) - // Turned along the bend, down and to the right, and never against it. - ->and(array_column($bend, 'rotate'))->toBe([-11.58, -45.0, -78.42]) - ->and($bend[1])->toBe(['x' => round(44 - 12 * cos(M_PI / 4), 2), 'y' => round(10 + 12 * sin(M_PI / 4), 2), 'rotate' => -45.0]) - ->and(end($dashes))->toBe(['x' => 62.58, 'y' => 22.0, 'rotate' => -90.0]); - - // A dash and a gap, 7pt, from the middle of one to the middle of the next on the straight runs. - $across = array_column(array_filter($dashes, fn (array $dash): bool => $dash['rotate'] === -90.0), 'x'); - - expect(array_map(fn (float $x, float $next): float => round($next - $x, 2), array_slice($across, 0, -1), array_slice($across, 1)))->toBe([7.0, 7.0]); -}); - -it("runs a nested reply's elbow from the spine above it to its avatar", function () { - $dashes = Display::elbowDashes(75.5, 96); - - expect($dashes[0]['x'])->toBe(76.0) - ->and(end($dashes))->toBe(['x' => 92.58, 'y' => 22.0, 'rotate' => -90.0]); -}); - it('prints full dates the way git log does', function () { expect(Display::gitDate(CarbonImmutable::parse('2026-10-14T20:13:05Z')))->toBe('Wed Oct 14 20:13:05 2026 +0000') ->and(Display::gitDate(null))->toBe(''); From eceefb1a0969468d203c5ef4327c23244272c192 Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Mon, 19 Oct 2026 20:34:50 +0100 Subject: [PATCH 2/2] Pull in mobile-air's native dashed borders The combined/dev-null-app branch now has NativePHP/mobile-air#548, so the lock moves to it (00a6bf1) and the dashed lines, rings and elbows draw. Co-Authored-By: Claude Opus 5.5 --- composer.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/composer.lock b/composer.lock index 0b36930..68f2b3c 100644 --- a/composer.lock +++ b/composer.lock @@ -2721,12 +2721,12 @@ "source": { "type": "git", "url": "https://github.com/NativePHP/mobile-air.git", - "reference": "8c53c6b097fbc050686f8e23dfd55d7d8146bd1a" + "reference": "00a6bf1451b70fa51eabd0d2fefa62db30d1e847" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/NativePHP/mobile-air/zipball/8c53c6b097fbc050686f8e23dfd55d7d8146bd1a", - "reference": "8c53c6b097fbc050686f8e23dfd55d7d8146bd1a", + "url": "https://api.github.com/repos/NativePHP/mobile-air/zipball/00a6bf1451b70fa51eabd0d2fefa62db30d1e847", + "reference": "00a6bf1451b70fa51eabd0d2fefa62db30d1e847", "shasum": "" }, "require": { @@ -2836,7 +2836,7 @@ "source": "https://github.com/NativePHP/mobile-air/tree/combined/dev-null-app", "issues": "https://github.com/NativePHP/mobile-air/issues" }, - "time": "2026-10-18T16:49:53+00:00" + "time": "2026-10-19T18:12:14+00:00" }, { "name": "nativephp/mobile-browser",