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/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/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/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/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..46630a8 --- /dev/null +++ b/tests/Feature/Screens/LaneButtonsTest.php @@ -0,0 +1,186 @@ + '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_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}"), + ]; +} + +/** + * 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, 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'])])]])); + + 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 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])]]); + + $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_color', 'label_color']); + } +}); + +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('report-c1'); + + 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'), 'sheet-commit'))->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); + } +});