From ae2d3baba9306be6ea3120cd6add4e6dd73c909b Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Sat, 17 Oct 2026 16:21:48 +0100 Subject: [PATCH 1/2] Wear your lane colour on the main buttons and tab underlines Follow, Commit, Reply, Send report, Edit on GitHub and Save color are now drawn in your lane's colour, light or dark, as the tab bar already was. So is the underline under the Log's and Notifications' tabs. Before devnull has said who you are they stay Commit Blue. Save color takes the colour you've picked, so you see it before you save it. The buttons use the token variants from NativePHP/mobile-ui's feat/button-theme-token-variant, now in combined/dev-null-app. Each lane gets an on-lane token for its label. In light mode that's white, except ink on green and ochre, where white is under 4:1. In dark mode every lifted lane is too light for white, so labels are graphite, as on primary. Destructive, secondary, and the black and white Apple and GitHub buttons keep their colours. The full-width buttons that sit on their own are now large, like Sign in with GitHub: everything on the Account screen, and Send report and Done in the report sheet. Buttons in a row keep their size. Co-Authored-By: Claude Opus 5.5 --- app/Devnull/Lane.php | 13 +- app/NativeComponents/CommitDetail.php | 1 + .../Concerns/TalksToDevnull.php | 10 + app/NativeComponents/Layouts/TabsLayout.php | 4 +- app/NativeComponents/Log.php | 1 + app/NativeComponents/Notifications.php | 1 + app/NativeComponents/Profile.php | 1 + app/NativeComponents/ThisDevice.php | 1 + config/native-ui.php | 28 ++- .../views/native/commit-detail.blade.php | 2 +- resources/views/native/log.blade.php | 6 +- .../views/native/notifications.blade.php | 1 + .../native/partials/commit-sheet.blade.php | 6 +- .../views/native/partials/composer.blade.php | 2 +- .../native/partials/follow-back.blade.php | 4 +- .../native/partials/following-empty.blade.php | 2 +- resources/views/native/profile.blade.php | 2 +- resources/views/native/this-device.blade.php | 23 ++- tests/Feature/Screens/LaneButtonsTest.php | 173 ++++++++++++++++++ tests/Unit/Devnull/LaneTest.php | 50 +++++ 20 files changed, 305 insertions(+), 26 deletions(-) create mode 100644 tests/Feature/Screens/LaneButtonsTest.php diff --git a/app/Devnull/Lane.php b/app/Devnull/Lane.php index d04f580..456f71b 100644 --- a/app/Devnull/Lane.php +++ b/app/Devnull/Lane.php @@ -10,7 +10,8 @@ * for a server that doesn't send one. * * The colours themselves are `lane-0` to `lane-5` in the native-ui theme, so - * `bg-theme-lane-{n}` picks the light or dark lift automatically. + * `bg-theme-lane-{n}` picks the light or dark lift automatically. Each has an + * `on-lane-{n}` for a button's label. */ final class Lane { @@ -35,6 +36,16 @@ public static function from(mixed $value): ?int return $lane >= 0 && $lane < self::Count ? $lane : null; } + /** + * The theme token a lane is drawn in, which is also a button variant: your + * lane's is what the tab bar, the main buttons and the pager tab underlines + * wear. Commit Blue (primary) before devnull has said whose phone this is. + */ + public static function token(?int $lane): string + { + return $lane === null ? 'primary' : 'lane-'.$lane; + } + public static function for(string $id): int { $hash = 0; diff --git a/app/NativeComponents/CommitDetail.php b/app/NativeComponents/CommitDetail.php index 495c628..3bb817a 100644 --- a/app/NativeComponents/CommitDetail.php +++ b/app/NativeComponents/CommitDetail.php @@ -338,6 +338,7 @@ public function render(): View $earlier = $this->earlierShown ? [] : $this->ancestors; return view('native.commit-detail', [ + 'myColour' => $this->myColour(), 'viewer' => $this->me(), 'rows' => $this->commit === null ? [] : Timeline::thread( [...($this->earlierShown ? $this->ancestors : []), $this->commit, ...$this->series], diff --git a/app/NativeComponents/Concerns/TalksToDevnull.php b/app/NativeComponents/Concerns/TalksToDevnull.php index b687f73..246acbd 100644 --- a/app/NativeComponents/Concerns/TalksToDevnull.php +++ b/app/NativeComponents/Concerns/TalksToDevnull.php @@ -8,6 +8,7 @@ use App\Devnull\Exceptions\ApiException; use App\Devnull\Exceptions\KeychainUnavailable; use App\Devnull\Exceptions\Unauthenticated; +use App\Devnull\Lane; use Closure; /** @@ -60,4 +61,13 @@ protected function me(): ?User { return app(Account::class)->user(); } + + /** + * The theme token for your lane, which the main buttons and the pager tab + * underlines are drawn in. + */ + protected function myColour(): string + { + return Lane::token($this->me()?->lane()); + } } diff --git a/app/NativeComponents/Layouts/TabsLayout.php b/app/NativeComponents/Layouts/TabsLayout.php index 8548bae..11b9435 100644 --- a/app/NativeComponents/Layouts/TabsLayout.php +++ b/app/NativeComponents/Layouts/TabsLayout.php @@ -3,6 +3,7 @@ namespace App\NativeComponents\Layouts; use App\Devnull\Account; +use App\Devnull\Lane; use App\Icons\Lucide; use App\NativeComponents\Log; use Native\Mobile\Edge\Elements\Image; @@ -90,8 +91,7 @@ public function tabBar(NativeComponent $screen): ?TabBar public static function accent(): string { $primary = (string) theme('primary', '#3b6fe0'); - $me = app(Account::class)->user(); - return $me === null ? $primary : (string) theme('lane-'.$me->lane(), $primary); + return (string) theme(Lane::token(app(Account::class)->user()?->lane()), $primary); } } diff --git a/app/NativeComponents/Log.php b/app/NativeComponents/Log.php index 7af6cdb..864e558 100644 --- a/app/NativeComponents/Log.php +++ b/app/NativeComponents/Log.php @@ -698,6 +698,7 @@ public function render(): View $showing = $pages[array_search($this->log, self::Logs, true)]; return view('native.log', [ + 'myColour' => $this->myColour(), 'viewer' => $viewer, 'composerOpen' => $this->composing || $this->body !== '', 'remaining' => $this->remaining(), diff --git a/app/NativeComponents/Notifications.php b/app/NativeComponents/Notifications.php index 046198f..dc499f0 100644 --- a/app/NativeComponents/Notifications.php +++ b/app/NativeComponents/Notifications.php @@ -462,6 +462,7 @@ public function render(): View } return view('native.notifications', [ + 'myColour' => $this->myColour(), 'types' => self::Types, 'readAt' => $readAt, 'pages' => $pages, diff --git a/app/NativeComponents/Profile.php b/app/NativeComponents/Profile.php index b45731d..0a7dd6c 100644 --- a/app/NativeComponents/Profile.php +++ b/app/NativeComponents/Profile.php @@ -385,6 +385,7 @@ public function render(): View $pinnedCommit = $this->pinnedCommit(); return view('native.profile', [ + 'myColour' => $this->myColour(), 'isBlocked' => $this->unavailable === ErrorCode::UserBlocked->value, 'isMe' => $this->isMe(), 'newCommitsLabel' => $this->newCommitsLabel(), diff --git a/app/NativeComponents/ThisDevice.php b/app/NativeComponents/ThisDevice.php index 7809b26..aab8440 100644 --- a/app/NativeComponents/ThisDevice.php +++ b/app/NativeComponents/ThisDevice.php @@ -312,6 +312,7 @@ public function render(): View $current = $this->current; return view('native.this-device', [ + 'myColour' => $this->myColour(), 'user' => $current?->user, 'fields' => $current === null ? [] : self::fields($current), 'syncedAt' => $current?->githubSyncedAt === null ? 'Last synced when you signed in' : 'Last synced '.Display::ago($current->githubSyncedAt), diff --git a/config/native-ui.php b/config/native-ui.php index de629ca..1953780 100644 --- a/config/native-ui.php +++ b/config/native-ui.php @@ -40,9 +40,11 @@ 'theme' => [ // devnull's tokens from DESIGN.md: a cool, nearly colourless shell, one - // working blue, and six author lanes that only ever mark the graph. + // working blue, and six author lanes that mark the graph. Your own lane + // also colours your buttons. 'light' => [ - // Commit Blue: the primary button, links, the active tab underline. + // Commit Blue: links, the new commits pill, and the main buttons and + // tab underlines before devnull has said whose phone this is. 'primary' => '#3b6fe0', 'on-primary' => '#ffffff', @@ -80,13 +82,24 @@ 'pending' => '#b45309', 'pending-soft' => '#fef3c7', - // Author lanes. Identity only: graph nodes, rail segments, avatar rings. + // Author lanes: graph nodes, rail segments, avatar rings. Yours also + // colours the tab bar, the main buttons and the pager tab underlines. 'lane-0' => '#d9480f', 'lane-1' => '#2f9e44', 'lane-2' => '#9c36b5', 'lane-3' => '#0c8599', 'lane-4' => '#c2255c', 'lane-5' => '#b7791f', + + // A button's label on each lane: white, except ink on green and + // ochre, where white falls well short of 4.5:1. White on orange and + // teal is 4.3:1, a hair short, and ink is no better there. + 'on-lane-0' => '#ffffff', + 'on-lane-1' => '#121417', + 'on-lane-2' => '#ffffff', + 'on-lane-3' => '#ffffff', + 'on-lane-4' => '#ffffff', + 'on-lane-5' => '#121417', ], // A faithful mirror on graphite, with the lanes and primary lifted. @@ -129,6 +142,15 @@ 'lane-3' => '#3bc9db', 'lane-4' => '#f06595', 'lane-5' => '#fcc419', + + // The lifted lanes are all light enough that white won't read on + // them (3:1 at best), so labels are graphite, as on primary. + 'on-lane-0' => '#111112', + 'on-lane-1' => '#111112', + 'on-lane-2' => '#111112', + 'on-lane-3' => '#111112', + 'on-lane-4' => '#111112', + 'on-lane-5' => '#111112', ], // Corner radii (points / dp): 6px controls, 8px groups, circles for people and commits. diff --git a/resources/views/native/commit-detail.blade.php b/resources/views/native/commit-detail.blade.php index b853d7d..b8e1ce2 100644 --- a/resources/views/native/commit-detail.blade.php +++ b/resources/views/native/commit-detail.blade.php @@ -122,7 +122,7 @@ class="w-full py-2 text-theme-ink" /> @include('native.partials.body-counter', ['body' => $reply, 'ref' => 'reply-budget']) - Reply + Reply diff --git a/resources/views/native/log.blade.php b/resources/views/native/log.blade.php index 484ef5c..539809c 100644 --- a/resources/views/native/log.blade.php +++ b/resources/views/native/log.blade.php @@ -5,8 +5,9 @@ {{-- The logs as pages you swipe between, under DESIGN.md's page tabs, which the pager draws itself: a full-width - split, the active label in ink over a 3px blue underline. On the phone the underline follows your finger, the - label you're nearest lights up past halfway, and a tab tap scrolls the pager there, so none of it waits for PHP. + split, the active label in ink over a 3px underline in your colour. On the phone the underline follows your + finger, the label you're nearest lights up past halfway, and a tab tap scrolls the pager there, so none of it + waits for PHP. The pager says which log it settled on once the swipe has come to rest, not as the log changes under your finger: switching logs sends a frame big enough to make the swipe stutter if it lands mid-swipe. A tap on the @@ -27,6 +28,7 @@ class="w-full flex-1 bg-theme-surface" :tabs="$tabs" on-tab-reselect="backToTop" tab-active-color="theme-ink" + :tab-indicator-color="'theme-'.$myColour" tab-color="theme-ink-muted" tab-divider-color="theme-hairline" tab-font="semibold" diff --git a/resources/views/native/notifications.blade.php b/resources/views/native/notifications.blade.php index e2c536a..cf5f5af 100644 --- a/resources/views/native/notifications.blade.php +++ b/resources/views/native/notifications.blade.php @@ -25,6 +25,7 @@ class="w-full flex-1 bg-theme-surface" tabs-scrollable on-tab-reselect="backToTop" tab-active-color="theme-ink" + :tab-indicator-color="'theme-'.$myColour" tab-color="theme-ink-muted" tab-divider-color="theme-hairline" tab-font="semibold" diff --git a/resources/views/native/partials/commit-sheet.blade.php b/resources/views/native/partials/commit-sheet.blade.php index ce59e26..599ea69 100644 --- a/resources/views/native/partials/commit-sheet.blade.php +++ b/resources/views/native/partials/commit-sheet.blade.php @@ -44,7 +44,7 @@ class="w-full text-theme-ink" @include('native.partials.body-counter', ['body' => $sheetBody, 'ref' => 'sheet-budget']) - Commit + Commit @@ -84,7 +84,7 @@ class="w-full" {{ $sheetError }} @endif - Send report + Send report @elseif ($sheetView === 'reported') @@ -93,7 +93,7 @@ class="w-full" Report received. Moderators will review it privately. - Done + Done @endif @endif diff --git a/resources/views/native/partials/composer.blade.php b/resources/views/native/partials/composer.blade.php index 4429243..f6d782d 100644 --- a/resources/views/native/partials/composer.blade.php +++ b/resources/views/native/partials/composer.blade.php @@ -30,7 +30,7 @@ class="w-full text-theme-ink" @if ($composerOpen) @include('native.partials.body-counter', ['body' => $body, 'ref' => 'budget']) - Commit + Commit @endif diff --git a/resources/views/native/partials/follow-back.blade.php b/resources/views/native/partials/follow-back.blade.php index c1791a6..fc1471f 100644 --- a/resources/views/native/partials/follow-back.blade.php +++ b/resources/views/native/partials/follow-back.blade.php @@ -1,2 +1,2 @@ -{{-- The web's Follow button on a follow: Follow in Commit Blue, or Following once you do. A tap flips it. --}} -{{ $isFollowing ? 'Following' : 'Follow' }} +{{-- The web's Follow button on a follow: Follow in your colour, or Following once you do. A tap flips it. --}} +{{ $isFollowing ? 'Following' : 'Follow' }} diff --git a/resources/views/native/partials/following-empty.blade.php b/resources/views/native/partials/following-empty.blade.php index 33424bc..286beff 100644 --- a/resources/views/native/partials/following-empty.blade.php +++ b/resources/views/native/partials/following-empty.blade.php @@ -27,7 +27,7 @@ {{ $suggestion->plainText() }} - Follow + Follow @unless ($loop->last) diff --git a/resources/views/native/profile.blade.php b/resources/views/native/profile.blade.php index 34dc52e..4efd9ed 100644 --- a/resources/views/native/profile.blade.php +++ b/resources/views/native/profile.blade.php @@ -34,7 +34,7 @@ @elseif ($profile->isFollowing) Unfollow @else - Follow + Follow @endif diff --git a/resources/views/native/this-device.blade.php b/resources/views/native/this-device.blade.php index 3a72196..00962cb 100644 --- a/resources/views/native/this-device.blade.php +++ b/resources/views/native/this-device.blade.php @@ -1,8 +1,12 @@ +@use('App\Devnull\Lane') @use('App\Icons\Ios') @use('App\Icons\Android') @use('App\Icons\Lucide') -{{-- Laid out like the web's profile settings: a section each, with its heading, on the surface. --}} +{{-- + Laid out like the web's profile settings: a section each, with its heading, on the surface. The full-width buttons + are large, as Sign in with GitHub is, and the main ones wear your colour. +--}} @include('native.partials.error') @@ -41,9 +45,9 @@ - Edit on GitHub + Edit on GitHub {{-- The label stays put while it spins, so the button keeps its size. --}} - Sync from GitHub + Sync from GitHub {{ $syncedAt }} @if ($syncError) {{ $syncError }} @@ -94,8 +98,9 @@ @endforeach + {{-- In the colour picked, so you see it on a button before saving it. --}} - Save color + Save color @if ($laneError) {{ $laneError }} @endif @@ -121,11 +126,11 @@ @if ($appleLinked === false) {{-- Apple's black button (white in dark mode, the theme's accent) with its logo, as Apple's guidelines ask. --}} - Sign in with Apple + Sign in with Apple @elseif ($appleLinked === true) - Unlink Apple ID + Unlink Apple ID @endif @@ -153,12 +158,12 @@ - Your devices on dev-null.sh + Your devices on dev-null.sh - Sign out + Sign out Deletes the key from this phone and signs you out. @@ -169,7 +174,7 @@ ]) - Delete account + Delete account diff --git a/tests/Feature/Screens/LaneButtonsTest.php b/tests/Feature/Screens/LaneButtonsTest.php new file mode 100644 index 0000000..8525c66 --- /dev/null +++ b/tests/Feature/Screens/LaneButtonsTest.php @@ -0,0 +1,173 @@ + 'http://devnull.test']); +}); + +afterEach(fn () => Platform::set(null)); + +/** + * The colours a button in a lane is sent with: the lane's light and dark + * shades, and its label's. + * + * @return array + */ +function laneButtonColours(int $lane): array +{ + return [ + 'variant' => "lane-{$lane}", + 'tint' => config("native-ui.theme.light.lane-{$lane}"), + 'dark_tint' => config("native-ui.theme.dark.lane-{$lane}"), + 'on_tint' => config("native-ui.theme.light.on-lane-{$lane}"), + 'dark_on_tint' => config("native-ui.theme.dark.on-lane-{$lane}"), + ]; +} + +/** + * The props of the button with this ref, lower-cased for comparing colours. + * + * @return array + */ +function buttonProps(TestableComponent $screen, string $ref): array +{ + return array_map(fn (mixed $value): mixed => is_string($value) ? strtolower($value) : $value, nodeByRef($screen, $ref)['props']); +} + +/** + * A log with one commit of Ada's, which you can quote and report. + */ +function laneLog(): TestableComponent +{ + serveApi(fn (Request $request) => Http::response(pageJson([ + commitJson(['id' => 'c1', 'body' => 'Required reading.', 'author' => userJson(['id' => 'u2', 'username' => 'ada', 'name' => 'Ada'])]), + ]))); + + return Native::visit('/log', platform: 'ios'); +} + +dataset('main buttons', [ + 'Commit, in the composer' => [fn (): TestableComponent => laneLog()->pressUp('composer-field'), 'commit'], + 'Commit, quoting a commit' => [fn (): TestableComponent => laneLog()->tap('quote-c1'), 'send-quote'], + 'Send report' => [fn (): TestableComponent => laneLog()->tap('more-c1')->tap('sheet-report'), 'send-report'], + 'Follow, on a suggestion in an empty log' => [function (): TestableComponent { + serveApi(fn (Request $request) => Http::response([...pageJson([]), 'suggestions' => [commitJson(['id' => 's1', 'author' => userJson(['id' => 'u2', 'username' => 'ada', 'name' => 'Ada'])])]])); + + return Native::visit('/log'); + }, 'follow-ada'], + 'Follow, back from a follow' => [function (): TestableComponent { + $follow = notificationJson('followed', ['actor' => userJson(['id' => 'u-linus', 'username' => 'linus', 'name' => 'Linus', 'is_followed' => false])]); + serveApi(['GET /api/v1/notifications' => [[ + ...pageJson([$follow], path: 'http://devnull.test/api/v1/notifications'), + 'filters' => ['type' => 'all'], + 'read_at' => now()->toIso8601ZuluString(), + ]]]); + + return Native::visit('/notifications'); + }, 'follow-back-linus'], + 'Follow, on a profile' => [function (): TestableComponent { + serveApi([ + 'GET /api/v1/users/ada' => [['data' => userWithCountsJson(['id' => 'u2', 'username' => 'ada', 'name' => 'Ada']), 'is_following' => false]], + 'GET /api/v1/users/ada/commits' => [pageJson([])], + ]); + + return Native::visit('/log/users/ada'); + }, 'follow'], + 'Reply' => [function (): TestableComponent { + serveApi([ + 'GET /api/v1/commits/c2' => [commitPageJson(commitJson(['id' => 'c2']))], + 'GET /api/v1/commits/c2/replies' => [pageJson([])], + ]); + + return Native::visit('/log/commits/c2'); + }, 'send-reply'], + 'Edit on GitHub, on the Account screen' => [function (): TestableComponent { + serveApi(['GET /api/v1/me' => [currentUserJson(['lane' => app(Account::class)->user()?->lane()])]]); + + return Native::visit('/profile/device'); + }, 'edit-on-github'], +]); + +it('draws the main buttons in your lane, with a label that reads on it', function (Closure $screen, string $ref, int $lane) { + signIn(['lane' => $lane]); + + expect(buttonProps($screen(), $ref))->toMatchArray(array_map(strtolower(...), laneButtonColours($lane))); +})->with('main buttons')->with([ + 'green, labelled in ink' => 1, + 'rose, labelled in white' => 4, +]); + +it('draws the tab underline in your lane, in its light and dark shades', function (string $path, int $lane) { + signIn(['lane' => $lane]); + Http::fake(['devnull.test/*' => Http::response(pageJson([]))]); + + $pager = findNode(Native::visit($path)->tree(), fn (array $node): bool => ($node['type'] ?? null) === 'pager'); + + expect(strtolower($pager['props']['tab_indicator_color']))->toBe(strtolower(config("native-ui.theme.light.lane-{$lane}"))) + ->and(strtolower($pager['props']['dark_tab_indicator_color']))->toBe(strtolower(config("native-ui.theme.dark.lane-{$lane}"))) + // Only the underline: the labels stay ink. + ->and($pager['props']['tab_active_color'])->toBe(config('native-ui.theme.light.ink')); +})->with(['/log', '/notifications'])->with([2, 5]); + +it('draws Save color in the colour you picked, before you save it', function () { + signIn(['lane' => 3]); + serveApi(['GET /api/v1/me' => [currentUserJson(['lane' => 3, 'preferred_lane' => 3, 'automatic_lane' => 3])]]); + + $screen = Native::visit('/profile/device'); + + expect(buttonProps($screen, 'save-lane')['variant'])->toBe('lane-3'); + + $screen->tap('lane-1'); + + expect(buttonProps($screen, 'save-lane'))->toMatchArray(array_map(strtolower(...), laneButtonColours(1))) + // Everything else waits for the save. + ->and(buttonProps($screen, 'edit-on-github')['variant'])->toBe('lane-3'); +}); + +it('leaves the other buttons in their own colours', function () { + signIn(['lane' => 4])->respondTo('NativephpSocialAuth.AppleSignIn', ['pending' => true]); + serveApi(['GET /api/v1/me' => [currentUserJson(['lane' => 4], ['apple_linked' => false])]]); + + $screen = Native::visit('/profile/device', platform: 'ios'); + + foreach (['sync-github' => 'secondary', 'devices' => 'secondary', 'sign-out' => 'secondary', 'delete-account' => 'destructive', 'link-apple' => 'accent'] as $ref => $variant) { + expect(buttonProps($screen, $ref)) + ->toMatchArray(['variant' => $variant]) + ->not->toHaveKeys(['tint', 'on_tint']); + } +}); + +it('makes the full-width buttons large, and leaves the ones in a row their size', function () { + signIn(['lane' => 4])->respondTo('NativephpSocialAuth.AppleSignIn', ['pending' => true]); + serveApi(['GET /api/v1/me' => [currentUserJson(['lane' => 4], ['apple_linked' => false])]]); + + $account = Native::visit('/profile/device', platform: 'ios'); + + foreach (['edit-on-github', 'sync-github', 'save-lane', 'link-apple', 'devices', 'sign-out', 'delete-account'] as $ref) { + expect(buttonProps($account, $ref)['size'] ?? null)->toBe('lg', $ref); + } + + $report = laneLog()->tap('more-c1')->tap('sheet-report'); + + expect(buttonProps($report, 'send-report')['size'] ?? null)->toBe('lg'); + + $report->selectRadio('report-reason', 'spam')->tap('send-report'); + + expect(buttonProps($report, 'report-done')['size'] ?? null)->toBe('lg'); + + expect(buttonProps(laneLog()->pressUp('composer-field'), 'commit'))->not->toHaveKey('size') + ->and(buttonProps(laneLog()->tap('quote-c1'), 'send-quote'))->not->toHaveKey('size'); +}); + +it('makes Unlink Apple ID large too', function () { + signIn()->respondTo('NativephpSocialAuth.AppleSignIn', ['pending' => true]); + serveApi(['GET /api/v1/me' => [currentUserJson(extra: ['apple_linked' => true])]]); + + expect(buttonProps(Native::visit('/profile/device', platform: 'ios'), 'unlink-apple')['size'] ?? null)->toBe('lg'); +}); diff --git a/tests/Unit/Devnull/LaneTest.php b/tests/Unit/Devnull/LaneTest.php index 30583de..b65f622 100644 --- a/tests/Unit/Devnull/LaneTest.php +++ b/tests/Unit/Devnull/LaneTest.php @@ -27,3 +27,53 @@ ['blue', null], [2.5, null], ]); + +it('names the theme token your colour is drawn in, Commit Blue before devnull says who you are', function (?int $lane, string $token) { + expect(Lane::token($lane))->toBe($token); +})->with([ + [0, 'lane-0'], + [5, 'lane-5'], + [null, 'primary'], +]); + +/** + * WCAG 2's contrast ratio between two #rrggbb colours. + */ +function contrastRatio(string $first, string $second): float +{ + $luminance = function (string $hex): float { + $channels = array_map(function (string $pair): float { + $channel = hexdec($pair) / 255; + + return $channel <= 0.03928 ? $channel / 12.92 : (($channel + 0.055) / 1.055) ** 2.4; + }, str_split(ltrim($hex, '#'), 2)); + + return 0.2126 * $channels[0] + 0.7152 * $channels[1] + 0.0722 * $channels[2]; + }; + + [$lighter, $darker] = [max($luminance($first), $luminance($second)), min($luminance($first), $luminance($second))]; + + return ($lighter + 0.05) / ($darker + 0.05); +} + +it('labels a button on each lane in whichever of white and ink reads better', function (string $mode, string $ink) { + $theme = (require dirname(__DIR__, 3).'/config/native-ui.php')['theme'][$mode]; + + foreach (range(0, Lane::Count - 1) as $lane) { + $fill = $theme["lane-{$lane}"]; + $better = contrastRatio('#ffffff', $fill) >= contrastRatio($theme[$ink], $fill) ? '#ffffff' : $theme[$ink]; + + expect($theme["on-lane-{$lane}"])->toBe($better, "lane-{$lane} in {$mode}"); + } +})->with([ + 'light, against ink' => ['light', 'ink'], + 'dark, against graphite' => ['dark', 'on-primary'], +]); + +it('reads at 4.5:1 or better on every lifted lane in dark mode', function () { + $theme = (require dirname(__DIR__, 3).'/config/native-ui.php')['theme']['dark']; + + foreach (range(0, Lane::Count - 1) as $lane) { + expect(contrastRatio($theme["on-lane-{$lane}"], $theme["lane-{$lane}"]))->toBeGreaterThanOrEqual(4.5); + } +}); From 7191f024181f3b66fe6cda1232c00a37282161a3 Mon Sep 17 00:00:00 2001 From: Simon Hamp Date: Sat, 17 Oct 2026 16:59:35 +0100 Subject: [PATCH 2/2] Show button labels in light mode, and colour the profile's tab underline mobile-ui sent a token variant's label colour as on_tint, which the bridge took for a callback, so the labels were transparent in light mode. It's label_color now, and the lock picks that up. The underline under your profile's Commits and Replies is your colour too, as on the Log and Notifications. Co-Authored-By: Claude Opus 5.5 --- composer.lock | 8 ++--- .../views/native/partials/page-tabs.blade.php | 4 +-- tests/Feature/Screens/LaneButtonsTest.php | 31 +++++++++++++------ 3 files changed, 28 insertions(+), 15 deletions(-) diff --git a/composer.lock b/composer.lock index b3c76d6..d4af933 100644 --- a/composer.lock +++ b/composer.lock @@ -3009,12 +3009,12 @@ "source": { "type": "git", "url": "https://github.com/NativePHP/mobile-ui.git", - "reference": "b391517bbcd75e297ada2ef85f6c8ef9b4ae8b9f" + "reference": "ca9b7ef7cdc93f490e196862e33516855d3975f8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/NativePHP/mobile-ui/zipball/b391517bbcd75e297ada2ef85f6c8ef9b4ae8b9f", - "reference": "b391517bbcd75e297ada2ef85f6c8ef9b4ae8b9f", + "url": "https://api.github.com/repos/NativePHP/mobile-ui/zipball/ca9b7ef7cdc93f490e196862e33516855d3975f8", + "reference": "ca9b7ef7cdc93f490e196862e33516855d3975f8", "shasum": "" }, "require": { @@ -3073,7 +3073,7 @@ "url": "https://nativephp.com/sponsor" } ], - "time": "2026-10-17T16:18:24+00:00" + "time": "2026-10-17T16:26:36+00:00" }, { "name": "nesbot/carbon", diff --git a/resources/views/native/partials/page-tabs.blade.php b/resources/views/native/partials/page-tabs.blade.php index 8f0918e..a2b664f 100644 --- a/resources/views/native/partials/page-tabs.blade.php +++ b/resources/views/native/partials/page-tabs.blade.php @@ -1,11 +1,11 @@ -{{-- DESIGN.md's page tabs: a full-width split, the active label in ink over a 3px blue underline. --}} +{{-- DESIGN.md's page tabs: a full-width split, the active label in ink over a 3px underline in your colour. --}} @foreach ($tabs as $tab) {{ $tab['label'] }} - + @endforeach diff --git a/tests/Feature/Screens/LaneButtonsTest.php b/tests/Feature/Screens/LaneButtonsTest.php index 8525c66..46630a8 100644 --- a/tests/Feature/Screens/LaneButtonsTest.php +++ b/tests/Feature/Screens/LaneButtonsTest.php @@ -23,10 +23,10 @@ function laneButtonColours(int $lane): array { return [ 'variant' => "lane-{$lane}", - 'tint' => config("native-ui.theme.light.lane-{$lane}"), - 'dark_tint' => config("native-ui.theme.dark.lane-{$lane}"), - 'on_tint' => config("native-ui.theme.light.on-lane-{$lane}"), - 'dark_on_tint' => config("native-ui.theme.dark.on-lane-{$lane}"), + 'tint_color' => config("native-ui.theme.light.lane-{$lane}"), + 'dark_tint_color' => config("native-ui.theme.dark.lane-{$lane}"), + 'label_color' => config("native-ui.theme.light.on-lane-{$lane}"), + 'dark_label_color' => config("native-ui.theme.dark.on-lane-{$lane}"), ]; } @@ -54,8 +54,9 @@ function laneLog(): TestableComponent dataset('main buttons', [ 'Commit, in the composer' => [fn (): TestableComponent => laneLog()->pressUp('composer-field'), 'commit'], - 'Commit, quoting a commit' => [fn (): TestableComponent => laneLog()->tap('quote-c1'), 'send-quote'], - 'Send report' => [fn (): TestableComponent => laneLog()->tap('more-c1')->tap('sheet-report'), 'send-report'], + 'Commit, in the compose sheet' => [fn (): TestableComponent => laneLog()->press('composeInSheet'), 'sheet-commit'], + 'Commit, quoting a commit' => [fn (): TestableComponent => laneLog()->tap('quote-c1'), 'sheet-commit'], + 'Send report' => [fn (): TestableComponent => laneLog()->tap('report-c1'), 'send-report'], 'Follow, on a suggestion in an empty log' => [function (): TestableComponent { serveApi(fn (Request $request) => Http::response([...pageJson([]), 'suggestions' => [commitJson(['id' => 's1', 'author' => userJson(['id' => 'u2', 'username' => 'ada', 'name' => 'Ada'])])]])); @@ -115,6 +116,18 @@ function laneLog(): TestableComponent ->and($pager['props']['tab_active_color'])->toBe(config('native-ui.theme.light.ink')); })->with(['/log', '/notifications'])->with([2, 5]); +it('draws the underline under your profile\'s tabs in your lane too', function () { + signIn(['lane' => 5]); + serveApi([ + 'GET /api/v1/users/local_dev' => [['data' => userWithCountsJson(['lane' => 5]), 'is_following' => false]], + 'GET /api/v1/users/local_dev/commits' => [pageJson([])], + ]); + + $underline = findNode(nodeByRef(Native::visit('/profile'), 'tab-commits'), fn (array $node): bool => ($node['layout']['height'] ?? null) === 3.0); + + expect(strtolower($underline['style']['bg_color']))->toBe(strtolower(config('native-ui.theme.light.lane-5'))); +}); + it('draws Save color in the colour you picked, before you save it', function () { signIn(['lane' => 3]); serveApi(['GET /api/v1/me' => [currentUserJson(['lane' => 3, 'preferred_lane' => 3, 'automatic_lane' => 3])]]); @@ -139,7 +152,7 @@ function laneLog(): TestableComponent foreach (['sync-github' => 'secondary', 'devices' => 'secondary', 'sign-out' => 'secondary', 'delete-account' => 'destructive', 'link-apple' => 'accent'] as $ref => $variant) { expect(buttonProps($screen, $ref)) ->toMatchArray(['variant' => $variant]) - ->not->toHaveKeys(['tint', 'on_tint']); + ->not->toHaveKeys(['tint_color', 'label_color']); } }); @@ -153,7 +166,7 @@ function laneLog(): TestableComponent expect(buttonProps($account, $ref)['size'] ?? null)->toBe('lg', $ref); } - $report = laneLog()->tap('more-c1')->tap('sheet-report'); + $report = laneLog()->tap('report-c1'); expect(buttonProps($report, 'send-report')['size'] ?? null)->toBe('lg'); @@ -162,7 +175,7 @@ function laneLog(): TestableComponent expect(buttonProps($report, 'report-done')['size'] ?? null)->toBe('lg'); expect(buttonProps(laneLog()->pressUp('composer-field'), 'commit'))->not->toHaveKey('size') - ->and(buttonProps(laneLog()->tap('quote-c1'), 'send-quote'))->not->toHaveKey('size'); + ->and(buttonProps(laneLog()->tap('quote-c1'), 'sheet-commit'))->not->toHaveKey('size'); }); it('makes Unlink Apple ID large too', function () {