From ad696ddaf443b9d7aa8b50f2363cb66c2f63c60a Mon Sep 17 00:00:00 2001 From: KaitoMuraoka <70003919+KaitoMuraoka@users.noreply.github.com> Date: Tue, 20 Oct 2026 02:00:38 +0900 Subject: [PATCH 1/2] Put Cancel and Commit in a bar at the top of the commit sheet The new commit and quote sheets have Cancel, the title and Commit in a bar along the top, pinned outside the scroll view so it stays while the field scrolls. native-ui's bottom sheet has no toolbar, so the bar is a row. On iOS, Cancel is a Liquid Glass button and Commit a prominent one in your colour, as the HIG's sheet toolbars float over content. On Android they're text buttons, as in Material 3's full-screen dialog header. The two buttons are the same width with the same padding either side, so the title sits in the middle, and both they and the 56pt bar grow with larger text rather than clipping. Android's sheet already has Material 3's drag handle above its content, so only iOS gets room above the bar. Cancel closes the sheet as a swipe does, and the draft stays stashed. Why devnull didn't take a commit now shows above the field, near Commit, rather than under the quoted card where the keyboard can hide it. --- .../native/partials/commit-sheet.blade.php | 106 ++++++++++-------- tests/Feature/Screens/ComposeSheetTest.php | 24 ++++ 2 files changed, 81 insertions(+), 49 deletions(-) diff --git a/resources/views/native/partials/commit-sheet.blade.php b/resources/views/native/partials/commit-sheet.blade.php index 5d97e7e..c6ec82c 100644 --- a/resources/views/native/partials/commit-sheet.blade.php +++ b/resources/views/native/partials/commit-sheet.blade.php @@ -1,9 +1,11 @@ {{-- - The sheet under a commit (see OpensCommitSheet). Quote opens it as a composer: the field, focused, with the commit - being quoted under it as a static card, then the counter and Commit, which waits for a body long enough to make. - If devnull says no, why shows here and the draft stays. The Log's compose button opens the same composer for a new - commit, without the card. A draft stashed last time comes back in it, with the web's stash@{0} and drop on the left - of Commit's row. + The sheet under a commit (see OpensCommitSheet). Quote opens it as a composer: a bar of Cancel, the title and + Commit, which waits for a body long enough to make, over the field, focused, with the commit being quoted under it + as a static card, then the counter. On iOS Cancel is a Liquid Glass button and Commit a prominent one in your + colour; on Android they're text buttons, as in Material 3's full-screen dialog header. The title is centred. + Cancel closes it as a swipe does, keeping the draft. If devnull says no, why shows here and the draft stays. The + Log's compose button opens the same composer for a new commit, without the card. A draft stashed last time comes + back in it, with the web's stash@{0} and drop on the left of the counter's row. Report commit, in someone else's published commit's More menu, opens it as the web's Report dialog: the private, not-hidden line, the commit's body clamped to three lines, the four reasons, optional details, and Send report, @@ -11,7 +13,8 @@ here and the form keeps what you wrote. A new commit can become a `/feature` or `/bug` issue, as on the web (see DraftsIssues): the command's badge over the - field, its placeholder in it, and Open issue for Commit. Opened, the sheet says so, with View issue and Done. + field, its placeholder in it, and Open issue in Commit's place in the bar. Opened, the sheet says so, with View + issue and Done. It's full height for the forms, and short for the receipts. It's always on the screen, shown or not, so iOS can slide it away and say when it's gone. @@ -22,55 +25,60 @@ @php($sheetLength = mb_strlen(trim($sheetBody))) @php($quoting = $sheetView === 'quote') @php($issueCommand = $sheetIssue === null ? null : \App\Devnull\IssueCommand::from($sheetIssue)) - - - {{ $quoting ? 'Quote '.$sheetCommit->author->handle() : 'New commit' }} - + + + Cancel + {{ $quoting ? 'Quote '.$sheetCommit->author->handle() : 'New commit' }} @if ($issueCommand) - @include('native.partials.issue-badge', ['command' => $issueCommand]) + {{-- The label stays put while it spins, so the button keeps its size. --}} + Open issue + @else + Commit @endif + + + + + @if ($sheetError) + {{ $sheetError }} + @endif - + @if ($issueCommand) + @include('native.partials.issue-badge', ['command' => $issueCommand]) + @endif - @if ($quoting) - @include('native.partials.quoted-commit', ['quoted' => $sheetCommit, 'static' => true]) - @elseif (! $issueCommand) - @include('native.partials.issue-suggestions', [ - 'suggestions' => $sheetTyped ? \App\Devnull\IssueCommand::suggested($sheetBody) : [], - 'hint' => $issueHintsDismissed ? null : \App\Devnull\IssueCommand::hinted($sheetBody), - ]) - @endif + - @if ($sheetError) - {{ $sheetError }} - @endif - - - @if ($sheetRestored) - @include('native.partials.stash-label', ['ref' => 'sheet-stash', 'drop' => 'dropSheetStash']) - + @if ($quoting) + @include('native.partials.quoted-commit', ['quoted' => $sheetCommit, 'static' => true]) + @elseif (! $issueCommand) + @include('native.partials.issue-suggestions', [ + 'suggestions' => $sheetTyped ? \App\Devnull\IssueCommand::suggested($sheetBody) : [], + 'hint' => $issueHintsDismissed ? null : \App\Devnull\IssueCommand::hinted($sheetBody), + ]) @endif - @include('native.partials.body-counter', ['body' => $sheetBody, 'ref' => 'sheet-budget']) - @if ($issueCommand) - {{-- The label stays put while it spins, so the button keeps its size. --}} - Open issue - @else - Commit - @endif - - - + + + @if ($sheetRestored) + @include('native.partials.stash-label', ['ref' => 'sheet-stash', 'drop' => 'dropSheetStash']) + + @endif + @include('native.partials.body-counter', ['body' => $sheetBody, 'ref' => 'sheet-budget']) + + + + @elseif ($sheetView === 'report') diff --git a/tests/Feature/Screens/ComposeSheetTest.php b/tests/Feature/Screens/ComposeSheetTest.php index f3f2aae..f5b3d66 100644 --- a/tests/Feature/Screens/ComposeSheetTest.php +++ b/tests/Feature/Screens/ComposeSheetTest.php @@ -138,6 +138,30 @@ function sheetCommitDisabled(TestableComponent $screen): bool Http::assertNotSent(fn (Request $request): bool => $request->method() === 'POST'); }); +it('closes the sheet from Cancel in its bar, keeping the draft as a swipe does', function () { + $screen = composeSheetOnLog()->input('sheet-body', 'Half a thought')->tap('sheet-cancel'); + + expect(nodeByRef($screen, 'sheet')['props']['visible'])->toBeFalse(); + + $screen->dismissSheet('sheet')->press('composeInSheet'); + + expect(nodeByRef($screen, 'sheet-body')['props']['value'])->toBe('Half a thought') + ->and(nodeByRef($screen, 'sheet-stash'))->not->toBeEmpty(); +}); + +it('draws the bar on Liquid Glass on iOS, and as a full-screen dialog header on Android', function () { + $commit = nodeByRef(composeSheetOnLog(), 'sheet-commit'); + + expect($commit['props']['glass'] ?? null)->toBe(3) + ->and($commit['props']['label'])->toBe('Commit'); + + fakeApi(['GET /api/v1/log/following' => [pageJson([])]]); + $android = Native::visit('/log', platform: 'android')->press('composeInSheet'); + + expect(nodeByRef($android, 'sheet-commit')['props'])->toMatchArray(['label' => 'Commit', 'variant' => 'ghost']) + ->and(nodeByRef($android, 'sheet-commit')['props'])->not->toHaveKey('glass'); +}); + it('only commits from a sheet that has a composer in it', function () { fakeApi(['GET /api/v1/log/following' => [pageJson([commitJson(['id' => 'c1', 'author' => userJson(['id' => 'u2', 'username' => 'ada'])])])]]); From 8b2bdab1a28f7402b57faf5646e6998d2f06fd96 Mon Sep 17 00:00:00 2001 From: KaitoMuraoka <70003919+KaitoMuraoka@users.noreply.github.com> Date: Tue, 20 Oct 2026 02:00:38 +0900 Subject: [PATCH 2/2] Say what Cancel keeps, and check a commit's length in one place Cancel tells VoiceOver and TalkBack that it keeps the draft for next time, since its name suggests otherwise. hideCommitSheet's docblock now says Cancel uses it too and that it mustn't forget the draft. Whether a body is long enough to commit is Commit::fits(), which the sheet's Commit button and commitFromSheet() both ask, so the button can't be enabled for a body the sheet won't send. --- app/Devnull/Data/Commit.php | 10 ++++++++++ app/NativeComponents/Concerns/DraftsIssues.php | 2 +- app/NativeComponents/Concerns/OpensCommitSheet.php | 5 +++-- resources/views/native/partials/commit-sheet.blade.php | 7 +++---- 4 files changed, 17 insertions(+), 7 deletions(-) diff --git a/app/Devnull/Data/Commit.php b/app/Devnull/Data/Commit.php index c4138a5..6c97c4a 100644 --- a/app/Devnull/Data/Commit.php +++ b/app/Devnull/Data/Commit.php @@ -171,6 +171,16 @@ public function withReactionsFrom(self $fresh): self ]); } + /** + * Whether a body is long enough to commit and short enough for devnull, once trimmed. + */ + public static function fits(string $body): bool + { + $length = mb_strlen(trim($body)); + + return $length >= self::MinLength && $length <= self::MaxLength; + } + /** * Whether a list would show only the start of a body, as the web's composer * works it out: more than PreviewLength characters or PreviewLines lines, diff --git a/app/NativeComponents/Concerns/DraftsIssues.php b/app/NativeComponents/Concerns/DraftsIssues.php index b3779f3..5f46a39 100644 --- a/app/NativeComponents/Concerns/DraftsIssues.php +++ b/app/NativeComponents/Concerns/DraftsIssues.php @@ -123,7 +123,7 @@ public function openIssueFromSheet(): void $command = $this->sheetIssue === null ? null : IssueCommand::tryFrom($this->sheetIssue); $body = trim($this->sheetBody); - if ($command === null || $this->openingIssue || ! $this->canDraftIssue() || mb_strlen($body) < Commit::MinLength || mb_strlen($body) > Commit::MaxLength) { + if ($command === null || $this->openingIssue || ! $this->canDraftIssue() || ! Commit::fits($body)) { return; } diff --git a/app/NativeComponents/Concerns/OpensCommitSheet.php b/app/NativeComponents/Concerns/OpensCommitSheet.php index 6ae0ff2..30b5282 100644 --- a/app/NativeComponents/Concerns/OpensCommitSheet.php +++ b/app/NativeComponents/Concerns/OpensCommitSheet.php @@ -180,7 +180,7 @@ public function commitFromSheet(): void $stashName = $this->sheetStashName(); $body = trim($this->sheetBody); - if ($stashName === null || $this->sheetIssue !== null || mb_strlen($body) < Commit::MinLength || mb_strlen($body) > Commit::MaxLength) { + if ($stashName === null || $this->sheetIssue !== null || ! Commit::fits($body)) { return; } @@ -196,7 +196,8 @@ public function commitFromSheet(): void } /** - * Done: the sheet goes. + * Done, after a report, and Cancel, in the composer's bar: the sheet goes. A draft in it stays stashed, as when + * it's swiped away, so this mustn't forget it. */ public function hideCommitSheet(): void { diff --git a/resources/views/native/partials/commit-sheet.blade.php b/resources/views/native/partials/commit-sheet.blade.php index c6ec82c..4e23606 100644 --- a/resources/views/native/partials/commit-sheet.blade.php +++ b/resources/views/native/partials/commit-sheet.blade.php @@ -22,18 +22,17 @@ @if ($sheetCommit || in_array($sheetView, ['compose', 'issued'], true)) @if (in_array($sheetView, ['quote', 'compose'], true)) - @php($sheetLength = mb_strlen(trim($sheetBody))) @php($quoting = $sheetView === 'quote') @php($issueCommand = $sheetIssue === null ? null : \App\Devnull\IssueCommand::from($sheetIssue)) - Cancel + Cancel {{ $quoting ? 'Quote '.$sheetCommit->author->handle() : 'New commit' }} @if ($issueCommand) {{-- The label stays put while it spins, so the button keeps its size. --}} - Open issue + Open issue @else - Commit + Commit @endif