From 11837dbe3ffc70740dd7ed0d2fab79e85928c0a5 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 29 May 2026 12:46:37 +0000 Subject: [PATCH 01/22] feat: redesign branch divergence as a quiet branch-control signal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The full-width stock-Flux warning callout clipped its primary button off the right window edge (unbounded branch names in `inline` callout actions) and was the only off-palette surface in the app. Replace it: - Diverged/Detached (recoverable): a quiet amber/info marker + popover on the branch control via x-divergence.marker. No full-width band, no content reflow — the canvas stays calm. The popover names the stakes (N comments on the target) and keeps branch names out of button labels. - MissingTarget (genuinely blocking): an on-brand gh-* bar (x-divergence.missing-bar) instead of a stock-danger callout, now dismissible rather than forcing an un-undoable switch. - Empty state is branch-aware: when the review is empty because the checkout drifted, it explains why instead of a generic "clean" message. Behavioral hardening: - "Switch review" is now undoable through the central undo stack (Cmd+Z). - Divergence suppresses per branch identity, not per sha, so committing on the diverged branch no longer re-nags on every commit. data-testids preserved; tests cover undo, per-branch suppression, the missing-target dismiss, the comment count, and per-state rendering. https://claude.ai/code/session_01MjD6k8JXwGZjxXw1fajJ97 --- .../components/divergence/marker.blade.php | 91 ++++++++++++ .../divergence/missing-bar.blade.php | 38 +++++ .../pages/\342\232\241review-page.blade.php" | 135 ++++++++++++------ .../ReviewPageBranchDivergenceTest.php | 71 ++++++++- 4 files changed, 285 insertions(+), 50 deletions(-) create mode 100644 resources/views/components/divergence/marker.blade.php create mode 100644 resources/views/components/divergence/missing-bar.blade.php diff --git a/resources/views/components/divergence/marker.blade.php b/resources/views/components/divergence/marker.blade.php new file mode 100644 index 00000000..8b53f78b --- /dev/null +++ b/resources/views/components/divergence/marker.blade.php @@ -0,0 +1,91 @@ +{{-- + Branch-divergence marker for the header branch control. + + Renders a small dot on the branch pill when the repo's checked-out HEAD has + drifted from the review target, with a popover offering the choice. Replaces + the old full-width warning band so the canvas stays calm — the signal lives + where its cause does. Only shows for the quiet, recoverable states + (Diverged / Detached); MissingTarget is a blocking bar (). + + Buttons call ReviewPage actions directly (this is rendered inside its DOM). +--}} +@props(['state', 'context' => []]) + +@php + $isDiverged = $state === \App\Enums\DivergenceState::Diverged; + $isDetached = $state === \App\Enums\DivergenceState::Detached; +@endphp + +@if($isDiverged || $isDetached) + @php + $target = $context['target'] ?? ''; + $currentBranch = $context['currentBranch'] ?? null; + $shortSha = $context['shortSha'] ?? ''; + $commentCount = (int) ($context['commentCount'] ?? 0); + + // Literal class strings so Tailwind's source scan keeps them in the build. + $dotClasses = $isDiverged ? 'bg-gh-attention ring-gh-attention/30' : 'bg-gh-link ring-gh-link/30'; + $testid = $isDiverged ? 'divergence-banner-diverged' : 'divergence-banner-detached'; + @endphp + +
+ + + +
+@endif diff --git a/resources/views/components/divergence/missing-bar.blade.php b/resources/views/components/divergence/missing-bar.blade.php new file mode 100644 index 00000000..c4c0fdda --- /dev/null +++ b/resources/views/components/divergence/missing-bar.blade.php @@ -0,0 +1,38 @@ +{{-- + Branch-divergence "missing target" bar. + + The one genuinely blocking divergence state: the branch being reviewed no + longer exists (deleted / renamed / mid-rebase). Unlike Diverged/Detached + (quiet header marker), this stays a full-width bar — but on-brand (house + pattern from update-banner, gh-* tokens) rather than a stock-Flux callout, + and now dismissible. Buttons call ReviewPage actions directly. +--}} +@props(['state', 'context' => []]) + +@if($state === \App\Enums\DivergenceState::MissingTarget) + @php + $target = $context['target'] ?? ''; + $currentBranch = $context['currentBranch'] ?? null; + @endphp + + +@endif diff --git "a/resources/views/pages/\342\232\241review-page.blade.php" "b/resources/views/pages/\342\232\241review-page.blade.php" index 80b77030..5b4ee882 100644 --- "a/resources/views/pages/\342\232\241review-page.blade.php" +++ "b/resources/views/pages/\342\232\241review-page.blade.php" @@ -60,6 +60,8 @@ /** Undo-toast `type` for the "marked file as reviewed" action. */ private const UNDO_TYPE_MARK_REVIEWED = 'mark-reviewed'; + private const UNDO_TYPE_SWITCH_BRANCH = 'switch-branch'; + /** @var array> */ public array $files = []; @@ -140,9 +142,13 @@ /** @var array */ public array $divergenceContext = []; - /** HEAD sha at which the user last dismissed a divergence banner. */ + /** HEAD sha at which the user last dismissed a divergence banner (detached only). */ public ?string $dismissedAtHead = null; + /** Head branch the user last chose to keep reviewing past (diverged / missing-target). + * Suppresses by branch identity, not sha, so committing on that branch doesn't re-nag. */ + public ?string $dismissedAtBranch = null; + /** Guards `skipRender()` on poll ticks so the initial mount still renders. */ #[Locked] public bool $divergenceChecked = false; @@ -682,6 +688,12 @@ private function resolveDivergenceState(CurrentHeadResult $head): void } if ($target !== '' && $head->targetExists === false) { + if ($this->dismissedAtBranch === $head->branch) { + $this->markAligned(); + + return; + } + $this->divergenceState = DivergenceState::MissingTarget; $this->divergenceContext = [ 'target' => $target, @@ -699,7 +711,9 @@ private function resolveDivergenceState(CurrentHeadResult $head): void return; } - if ($this->dismissedAtHead === $head->sha) { + // Suppress by branch identity, not sha: once the user opts to keep reviewing + // their target, committing on the diverged branch shouldn't re-nag every commit. + if ($this->dismissedAtBranch === $head->branch) { $this->markAligned(); return; @@ -711,6 +725,7 @@ private function resolveDivergenceState(CurrentHeadResult $head): void 'currentBranch' => $head->branch, 'currentSha' => $head->sha, 'shortSha' => substr($head->sha, 0, 7), + 'commentCount' => $this->persistedCommentCount(), ]; } @@ -722,15 +737,38 @@ public function switchReviewToHead(): void return; } + // Capture before autoFollow clears state, so the switch stays undoable. + $wasDiverged = $this->divergenceState === DivergenceState::Diverged; + $fromBranch = $this->projectBranch; + $this->autoFollowToHead($head->branch); + + // Only offer undo when leaving a real, still-existing target (Diverged). + // MissingTarget's old branch is gone — undoing would re-point at nothing. + if ($wasDiverged && $fromBranch !== '' && $fromBranch !== $head->branch) { + $this->dispatch( + 'undo-available', + type: self::UNDO_TYPE_SWITCH_BRANCH, + payload: ['fromBranch' => $fromBranch], + message: 'Switched review to '.$head->branch, + ); + } } public function keepReviewing(): void { - $currentSha = $this->divergenceContext['currentSha'] ?? null; + $branch = $this->divergenceContext['currentBranch'] ?? null; - if (is_string($currentSha) && $currentSha !== '') { - $this->dismissedAtHead = $currentSha; + if (is_string($branch) && $branch !== '') { + // Diverged / missing-target: suppress by branch identity. + $this->dismissedAtBranch = $branch; + } else { + // Detached: no branch to key on, so fall back to the sha. + $sha = $this->divergenceContext['currentSha'] ?? null; + + if (is_string($sha) && $sha !== '') { + $this->dismissedAtHead = $sha; + } } $this->markAligned(); @@ -741,6 +779,21 @@ public function dismissDetachedBanner(): void $this->keepReviewing(); } + public function dismissMissingTarget(): void + { + $this->keepReviewing(); + } + + /** @param array{fromBranch?: string} $payload */ + public function restoreReviewBranch(array $payload): void + { + $branch = $payload['fromBranch'] ?? null; + + if (is_string($branch) && $branch !== '') { + $this->autoFollowToHead($branch); + } + } + private function autoFollowToHead(string $newBranch): void { // Race guard: overlapping polls during a slow rehydrate can re-enter here. @@ -754,6 +807,7 @@ private function autoFollowToHead(string $newBranch): void $this->projectBranch = $newBranch; $this->dismissedAtHead = null; + $this->dismissedAtBranch = null; $this->markAligned(); $this->rehydrateForTarget(); @@ -766,10 +820,21 @@ private function markAligned(): void } private function hasPersistedComments(): bool + { + return $this->persistedCommentQuery()->exists(); + } + + private function persistedCommentCount(): int + { + return $this->persistedCommentQuery()->count(); + } + + /** @return \Illuminate\Database\Eloquent\Builder<\App\Models\Comment> */ + private function persistedCommentQuery(): \Illuminate\Database\Eloquent\Builder { $projectId = $this->projectId === 0 ? null : $this->projectId; - return \App\Models\Comment::forProjectOrRepo($projectId, $this->repoPath)->exists(); + return \App\Models\Comment::forProjectOrRepo($projectId, $this->repoPath); } // endregion: Branch Divergence @@ -1011,6 +1076,7 @@ public function undo(string $type, mixed $payload): void 'delete', 'clear-all' => $this->restoreComments($payload), 'discard' => $this->restoreDiscardedFile($payload), self::UNDO_TYPE_MARK_REVIEWED => $this->unmarkReviewed($payload['filePaths'] ?? []), + self::UNDO_TYPE_SWITCH_BRANCH => $this->restoreReviewBranch(is_array($payload) ? $payload : []), default => null, }; } @@ -1618,6 +1684,9 @@ class="inline-flex" :selection-title="$selectionTitle" :default-base-branch="$defaultBaseBranch" /> + @if(! $this->isCommitMode()) + + @endif @endif @@ -1850,7 +1919,9 @@ class="flex-1" - {{-- Branch divergence banner + polling island (working-tree mode only) --}} + {{-- Branch divergence: HEAD poller + (missing-target only) blocking bar. + Diverged / detached surface as a quiet marker on the branch control in + the header (see above) so the canvas stays calm. --}} @if(! $this->isCommitMode()) - @if($divergenceState === DivergenceState::Diverged) -
- - Repo switched to {{ $divergenceContext['currentBranch'] }} - Still reviewing {{ $divergenceContext['target'] }}. - - - Switch review to {{ $divergenceContext['currentBranch'] }} - - - Keep reviewing {{ $divergenceContext['target'] }} - - - -
- @elseif($divergenceState === DivergenceState::Detached) -
- - Repo detached at {{ $divergenceContext['shortSha'] }} - Still reviewing {{ $divergenceContext['target'] }}. - - Dismiss - - -
- @elseif($divergenceState === DivergenceState::MissingTarget) - - @endif + @endif @if($commitInfo) @@ -2139,6 +2174,18 @@ class="opacity-0 group-hover:opacity-100 transition-opacity shrink-0" @if($this->isCommitMode())

No file changes in this commit

This commit has no diff (empty or merge commit)

+ @elseif($divergenceState === DivergenceState::Diverged) + {{-- Empty *because* the checkout drifted: tell the one true story instead + of a generic "clean" message disconnected from the divergence marker. --}} +

Nothing to review on {{ $divergenceContext['target'] ?? $projectBranch }}

+

+ Your repo is on {{ $divergenceContext['currentBranch'] ?? '' }} right now. + Switch your review to it, or edit files on {{ $divergenceContext['target'] ?? $projectBranch }} to see changes here. +

+
+ + +
@else

Working tree is clean

Edit files to see them here

diff --git a/tests/Unit/Livewire/ReviewPageBranchDivergenceTest.php b/tests/Unit/Livewire/ReviewPageBranchDivergenceTest.php index 882dfdd3..a2fdc6f8 100644 --- a/tests/Unit/Livewire/ReviewPageBranchDivergenceTest.php +++ b/tests/Unit/Livewire/ReviewPageBranchDivergenceTest.php @@ -141,7 +141,9 @@ public function handle(int $projectId, string $repoPath): ?string expect($component->get('divergenceState'))->toBe(DivergenceState::Diverged); expect($component->get('divergenceContext.target'))->toBe('main'); expect($component->get('divergenceContext.currentBranch'))->toBe('feature-x'); + expect($component->get('divergenceContext.commentCount'))->toBe(1); expect($component->get('projectBranch'))->toBe('main'); + $component->assertSeeHtml('data-testid="divergence-banner-diverged"'); }); test('detached banner shown when HEAD is detached', function () { @@ -151,6 +153,7 @@ public function handle(int $projectId, string $repoPath): ?string expect($component->get('divergenceState'))->toBe(DivergenceState::Detached); expect($component->get('divergenceContext.shortSha'))->toBe('d'.str_repeat('0', 6)); + $component->assertSeeHtml('data-testid="divergence-banner-detached"'); }); test('missing_target banner shown when target branch no longer exists', function () { @@ -161,9 +164,10 @@ public function handle(int $projectId, string $repoPath): ?string expect($component->get('divergenceState'))->toBe(DivergenceState::MissingTarget); expect($component->get('divergenceContext.target'))->toBe('main'); expect($component->get('divergenceContext.currentBranch'))->toBe('feature-x'); + $component->assertSeeHtml('data-testid="divergence-banner-missing"'); }); -test('keepReviewing suppresses banner until HEAD moves', function () { +test('keepReviewing suppresses the banner for that branch, even across new commits', function () { Comment::create([ 'id' => 'c1', 'project_id' => $this->project->id, @@ -180,7 +184,8 @@ public function handle(int $projectId, string $repoPath): ?string ]); $firstSha = 'a'.str_repeat('0', 39); - $secondSha = 'b'.str_repeat('0', 39); + $sameBranchNewSha = 'a'.str_repeat('9', 39); + $otherBranchSha = 'b'.str_repeat('0', 39); $fake = bindFakeCurrentHeadAction(new CurrentHeadResult(branch: 'feature-x', sha: $firstSha, detached: false, targetExists: true)); @@ -189,14 +194,20 @@ public function handle(int $projectId, string $repoPath): ?string $component->call('keepReviewing'); expect($component->get('divergenceState'))->toBe(DivergenceState::Aligned); - expect($component->get('dismissedAtHead'))->toBe($firstSha); + // Suppression keys on the branch identity, not the sha. + expect($component->get('dismissedAtBranch'))->toBe('feature-x'); - // Another poll at the same HEAD: banner stays suppressed. + // Same HEAD: stays suppressed. $component->call('checkHeadDivergence'); expect($component->get('divergenceState'))->toBe(DivergenceState::Aligned); - // HEAD moves to a new SHA (still diverged): banner re-appears. - $fake->result = new CurrentHeadResult(branch: 'feature-y', sha: $secondSha, detached: false, targetExists: true); + // New commit on the SAME branch (sha changes, branch unchanged): still suppressed. + $fake->result = new CurrentHeadResult(branch: 'feature-x', sha: $sameBranchNewSha, detached: false, targetExists: true); + $component->call('checkHeadDivergence'); + expect($component->get('divergenceState'))->toBe(DivergenceState::Aligned); + + // HEAD moves to a DIFFERENT branch: banner re-appears. + $fake->result = new CurrentHeadResult(branch: 'feature-y', sha: $otherBranchSha, detached: false, targetExists: true); $component->call('checkHeadDivergence'); expect($component->get('divergenceState'))->toBe(DivergenceState::Diverged); expect($component->get('divergenceContext.currentBranch'))->toBe('feature-y'); @@ -309,3 +320,51 @@ public function handle(int $projectId, string $repoPath): ?string expect($component->get('divergenceState'))->toBe(DivergenceState::Aligned); }); + +test('switchReviewToHead is undoable and undo restores the original target', function () { + Comment::create([ + 'id' => 'c1', + 'project_id' => $this->project->id, + 'repo_path' => $this->project->path, + 'origin_ref' => 'main', + 'file_path' => 'src/Foo.php', + 'side' => 'new', + 'start_line' => 1, + 'end_line' => 1, + 'file_content_hash' => 'mock-hash', + 'body' => 'hello', + 'is_draft' => false, + 'submitted_at' => now(), + ]); + + bindFakeCurrentHeadAction(new CurrentHeadResult(branch: 'feature-x', sha: 'c'.str_repeat('0', 39), detached: false, targetExists: true)); + + $component = Livewire::test('pages::review-page', ['slug' => 'divergence-test']); + expect($component->get('divergenceState'))->toBe(DivergenceState::Diverged); + + $component->call('switchReviewToHead') + ->assertDispatched('undo-available', type: 'switch-branch', payload: ['fromBranch' => 'main']); + + expect($component->get('projectBranch'))->toBe('feature-x'); + expect($this->project->fresh()->branch)->toBe('feature-x'); + + // Undo re-points the review back to the branch it was on. + $component->call('undo', 'switch-branch', ['fromBranch' => 'main']); + expect($component->get('projectBranch'))->toBe('main'); + expect($this->project->fresh()->branch)->toBe('main'); +}); + +test('dismissMissingTarget suppresses the missing-target banner for that branch', function () { + bindFakeCurrentHeadAction(new CurrentHeadResult(branch: 'feature-x', sha: 'e'.str_repeat('0', 39), detached: false, targetExists: false)); + + $component = Livewire::test('pages::review-page', ['slug' => 'divergence-test']); + expect($component->get('divergenceState'))->toBe(DivergenceState::MissingTarget); + + $component->call('dismissMissingTarget'); + expect($component->get('divergenceState'))->toBe(DivergenceState::Aligned); + expect($component->get('dismissedAtBranch'))->toBe('feature-x'); + + // Same HEAD: stays suppressed. + $component->call('checkHeadDivergence'); + expect($component->get('divergenceState'))->toBe(DivergenceState::Aligned); +}); From 2df18f6ae2d4c02d7add257a3c5104c96f25c456 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 29 May 2026 12:59:46 +0000 Subject: [PATCH 02/22] refactor: enforce the gh-* palette; remove stray stock colors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The monochrome-brutalist system had no enforcement, so stock Tailwind/Flux colors had accreted (the divergence callout was just the loudest case). - Alias Flux's --color-accent → gh-accent in app.css, so variant="primary" buttons, checkboxes and focus rings render the brand near-black/near-white instead of Flux's stock zinc, and track the theme. - Replace stray utilities with gh-* tokens: review-pair "R" (purple→link), trash/restore + delete hovers (green-400/red-400/red-500 → gh-*), git-error glyphs (red-400 → gh-red). - Replace stock-color Flux badges (worktree yellow, untracked zinc, image before/after red/green) with on-brand tokened labels. - Fix the undefined `text-gh-fg` token on the first-run hero → gh-text. - Drop color="green" from the comment Save button (now accent). Add two arch guards in BladeConventionsTest: no stock Tailwind palette utilities and no stock Flux color="…" props in blade. https://claude.ai/code/session_01MjD6k8JXwGZjxXw1fajJ97 --- resources/css/app.css | 11 ++++++ .../components/comment-display.blade.php | 2 +- .../views/components/comment-form.blade.php | 2 +- .../views/components/project-list.blade.php | 4 +-- .../partials/context-tree-node.blade.php | 2 +- .../livewire/\342\232\241diff-file.blade.php" | 6 ++-- .../pages/\342\232\241review-page.blade.php" | 8 ++--- .../\342\232\241select-repo-page.blade.php" | 2 +- tests/Arch/BladeConventionsTest.php | 36 +++++++++++++++++++ 9 files changed, 60 insertions(+), 13 deletions(-) diff --git a/resources/css/app.css b/resources/css/app.css index 4cd82cc3..6299f6c0 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -35,6 +35,17 @@ --tracking-brutal-tight: -0.06em; } +/* Map Flux's accent token (used by variant="primary", checkboxes, focus rings) + onto the app's monochrome `gh-accent` so Flux components stay on-brand + (near-black in light, near-white in dark) instead of Flux's stock indigo. + Custom properties resolve at use-time, so referencing --gh-* (defined in the + layout's inline style) here is safe regardless of stylesheet order. */ +:root { + --color-accent: rgb(var(--gh-accent)); + --color-accent-content: rgb(var(--gh-accent)); + --color-accent-foreground: rgb(var(--gh-bg)); +} + * { scrollbar-width: thin; scrollbar-color: gray transparent; diff --git a/resources/views/components/comment-display.blade.php b/resources/views/components/comment-display.blade.php index 2e2cbe82..eeb01c1b 100644 --- a/resources/views/components/comment-display.blade.php +++ b/resources/views/components/comment-display.blade.php @@ -51,7 +51,7 @@ class="hover:!text-gh-accent" size="xs" aria-label="Delete comment" x-on:click.stop="$wire.dispatch('delete-comment', { commentId: '{{ $comment['id'] }}' })" - class="hover:!text-red-400" + class="hover:!text-gh-red" /> diff --git a/resources/views/components/comment-form.blade.php b/resources/views/components/comment-form.blade.php index 4c3ac5b5..91578424 100644 --- a/resources/views/components/comment-form.blade.php +++ b/resources/views/components/comment-form.blade.php @@ -16,6 +16,6 @@ class="font-mono text-xs"
Press Esc again to save as draft
Cancel - Save + Save
diff --git a/resources/views/components/project-list.blade.php b/resources/views/components/project-list.blade.php index a31d107d..623c60d3 100644 --- a/resources/views/components/project-list.blade.php +++ b/resources/views/components/project-list.blade.php @@ -98,7 +98,7 @@ 'text-gh-link' => $isCurrent, ]) title="{{ $project['name'] }}">{{ $project['name'] }} @if($project['is_worktree']) - worktree + worktree @endif @if($project['branch']) {{ $project['branch'] }} @@ -124,7 +124,7 @@ wire:click.stop="removeProject({{ $project['id'] }})" wire:confirm="{{ $confirmMessage }}" @class([ - 'text-gh-muted hover:text-red-500 transition-opacity', + 'text-gh-muted hover:text-gh-red transition-opacity', 'opacity-0 group-hover:opacity-100 focus-visible:opacity-100' => $isPicker, 'opacity-60 hover:opacity-100' => ! $isPicker, ]) diff --git a/resources/views/livewire/partials/context-tree-node.blade.php b/resources/views/livewire/partials/context-tree-node.blade.php index 360efcd9..f70b77db 100644 --- a/resources/views/livewire/partials/context-tree-node.blade.php +++ b/resources/views/livewire/partials/context-tree-node.blade.php @@ -42,7 +42,7 @@ class="w-full text-left flex items-center gap-2 py-1 rounded hover:bg-gh-border/ {{ $kind->badgeLabel() }} {{ $file['basename'] }} @if(! $file['isTracked']) - untracked + untracked @endif @if($file['isSymlink']) diff --git "a/resources/views/livewire/\342\232\241diff-file.blade.php" "b/resources/views/livewire/\342\232\241diff-file.blade.php" index 96f70e80..783bd3cf 100644 --- "a/resources/views/livewire/\342\232\241diff-file.blade.php" +++ "b/resources/views/livewire/\342\232\241diff-file.blade.php" @@ -462,7 +462,7 @@ class="group"
@if($hasBeforeImage)
- {{ $status === 'deleted' ? 'Deleted' : 'Before' }} + {{ $status === 'deleted' ? 'Deleted' : 'Before' }}
- {{ $status === 'added' ? 'New' : 'After' }} + {{ $status === 'added' ? 'New' : 'After' }}
@elseif($diffData['error'] ?? false)
- + Git error: {{ $diffData['error'] }}
@elseif(empty($diffData['hunks'])) diff --git "a/resources/views/pages/\342\232\241review-page.blade.php" "b/resources/views/pages/\342\232\241review-page.blade.php" index 5b4ee882..5dd75c70 100644 --- "a/resources/views/pages/\342\232\241review-page.blade.php" +++ "b/resources/views/pages/\342\232\241review-page.blade.php" @@ -1952,7 +1952,7 @@ class="flex-1"
@foreach($reviewPairs as $pair)
- R + R @@ -2143,7 +2143,7 @@ class="opacity-0 group-hover:opacity-100 transition-opacity text-gh-muted hover:
- +

Git error

{{ $gitError }}

@@ -2205,7 +2205,7 @@ class="text-gh-muted hover:text-gh-text transition-colors"> - R + R {{ $pair['displayName'] }} .md diff --git "a/resources/views/pages/\342\232\241select-repo-page.blade.php" "b/resources/views/pages/\342\232\241select-repo-page.blade.php" index e284cf0c..7c41acbd 100644 --- "a/resources/views/pages/\342\232\241select-repo-page.blade.php" +++ "b/resources/views/pages/\342\232\241select-repo-page.blade.php" @@ -132,7 +132,7 @@ class="sticky top-0 z-50"
@if($totalProjects === 0)
-

rfa

+

rfa

Be in the loop.

@native diff --git a/tests/Arch/BladeConventionsTest.php b/tests/Arch/BladeConventionsTest.php index d31dc391..4e85579e 100644 --- a/tests/Arch/BladeConventionsTest.php +++ b/tests/Arch/BladeConventionsTest.php @@ -237,3 +237,39 @@ function inlineAlpineObjects(string $content): array ->toContain('$this->imageUrl(') ->not->toContain('src="/api/image/'); }); + +test('blade templates use gh-* tokens, not stock Tailwind palette colors', function () { + // The app is monochrome-brutalist: color comes from the gh-* token system + // (config/theme.php), never stock Tailwind palette utilities. See resources/CLAUDE.md. + $pattern = '/\b(?:text|bg|border|ring|fill|stroke|divide|from|to|via|outline|decoration|shadow|accent|caret)-(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|slate|gray|zinc|neutral|stone)-(?:50|[1-9]00|950)\b/'; + + $violations = []; + foreach (bladeFiles() as $file) { + $content = file_get_contents($file); + if (preg_match_all($pattern, $content, $matches)) { + foreach ($matches[0] as $hit) { + $violations[] = bladeRelativePath($file).': '.$hit; + } + } + } + + expect($violations)->toBeEmpty(); +}); + +test('flux components do not use stock palette color props', function () { + // Flux color="red|green|…" bypasses the gh-* tokens. Use gh-* utilities, or the + // accent token (aliased to gh-accent in app.css) for primary emphasis. + $pattern = '/\bcolor="(?:red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose|slate|gray|zinc|neutral|stone)"/'; + + $violations = []; + foreach (bladeFiles() as $file) { + $content = file_get_contents($file); + if (preg_match_all($pattern, $content, $matches)) { + foreach ($matches[0] as $hit) { + $violations[] = bladeRelativePath($file).': '.$hit; + } + } + } + + expect($violations)->toBeEmpty(); +}); From d8b677691b2c3478b0f7091d1bfb5aaf51295e0b Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 29 May 2026 13:03:37 +0000 Subject: [PATCH 03/22] feat: unify empty/loading states behind x-empty-state + diff skeleton Four different empty-state visual languages collapse into one: - Add : rfa-wordmark watermark (or a Flux icon), display heading, body slot, optional actions; role/aria-live pass through for alert-grade states. One recipe, used everywhere. - Migrate review-page (git-error, no-commit-changes, diverged, clean-tree) and context-page (no context files) onto it. Headings now use an

with explicit font-display, fixing the div-as-heading and weight drift. - Add : a content-shaped placeholder (line-number + content bars in the diff rhythm) so lazy-loaded diffs don't pop in from a bare spinner. animate-pulse is collapsed by the global reduce-motion rule. https://claude.ai/code/session_01MjD6k8JXwGZjxXw1fajJ97 --- .../views/components/diff-skeleton.blade.php | 21 +++++++ .../views/components/empty-state.blade.php | 40 +++++++++++++ .../livewire/\342\232\241diff-file.blade.php" | 8 +-- .../pages/\342\232\241context-page.blade.php" | 9 ++- .../pages/\342\232\241review-page.blade.php" | 60 +++++++++---------- 5 files changed, 96 insertions(+), 42 deletions(-) create mode 100644 resources/views/components/diff-skeleton.blade.php create mode 100644 resources/views/components/empty-state.blade.php diff --git a/resources/views/components/diff-skeleton.blade.php b/resources/views/components/diff-skeleton.blade.php new file mode 100644 index 00000000..8a636fd4 --- /dev/null +++ b/resources/views/components/diff-skeleton.blade.php @@ -0,0 +1,21 @@ +{{-- + Content-shaped placeholder for a diff file while its hunks load — a few + line-number + content bars in the diff's own rhythm, so the layout doesn't + jump when the real diff arrives. `animate-pulse` is collapsed by the global + reduce-motion rule. Decorative, so aria-hidden. +--}} +@props(['rows' => 7]) + +@php + // Varied bar widths read as code rather than a flat block. + $widths = [86, 58, 72, 44, 90, 64, 50, 78, 38, 68]; +@endphp + + diff --git a/resources/views/components/empty-state.blade.php b/resources/views/components/empty-state.blade.php new file mode 100644 index 00000000..ec42a694 --- /dev/null +++ b/resources/views/components/empty-state.blade.php @@ -0,0 +1,40 @@ +{{-- + Shared full-page empty / zero state — one visual language across the app. + + Renders the rfa wordmark watermark (or a Flux icon), a display heading, a + free body slot, and an optional actions row. Pass role / aria-live through + as attributes for alert-grade states (e.g. git errors). + + Props: + glyph wordmark text (default "rfa"); set to null to omit + glyphClass color for the wordmark watermark (default muted/20) + icon Flux icon name to use instead of the wordmark + size "lg" (default, ~60vh centered) or "sm" (py-16) + Slots: heading, default (body), actions +--}} +@props([ + 'glyph' => 'rfa', + 'glyphClass' => 'text-gh-muted/20', + 'icon' => null, + 'size' => 'lg', +]) + +
class(['flex items-center justify-center', $size === 'sm' ? 'py-16' : 'h-[60vh]']) }}> +
+ @if($icon) + + @elseif($glyph) + + @endif + + @isset($heading) +

{{ $heading }}

+ @endisset + + {{ $slot }} + + @isset($actions) +
{{ $actions }}
+ @endisset +
+
diff --git "a/resources/views/livewire/\342\232\241diff-file.blade.php" "b/resources/views/livewire/\342\232\241diff-file.blade.php" index 783bd3cf..07108295 100644 --- "a/resources/views/livewire/\342\232\241diff-file.blade.php" +++ "b/resources/views/livewire/\342\232\241diff-file.blade.php" @@ -492,12 +492,8 @@ class="max-h-96 object-contain" @elseif($diffData === null) {{-- One spinner for both the pre-request setTimeout window and the in-flight request, so the visual doesn't swap mid-load. --}} -
- - Loading diff... +
+
@elseif($diffData['tooLarge'] ?? false)
diff --git "a/resources/views/pages/\342\232\241context-page.blade.php" "b/resources/views/pages/\342\232\241context-page.blade.php" index 29575838..3eafc699 100644 --- "a/resources/views/pages/\342\232\241context-page.blade.php" +++ "b/resources/views/pages/\342\232\241context-page.blade.php" @@ -515,15 +515,14 @@ class="min-h-screen flex flex-col" @if(empty($contextFiles)) -
- -
No context files found
-

+ + No context files found +

rfa scans for CLAUDE.md and AGENTS.md across this repo. Drop one in the repo root or any subdirectory and re-scan.

-
+ @else @foreach($contextFiles as $file)
diff --git "a/resources/views/pages/\342\232\241review-page.blade.php" "b/resources/views/pages/\342\232\241review-page.blade.php" index 5dd75c70..a18cedf7 100644 --- "a/resources/views/pages/\342\232\241review-page.blade.php" +++ "b/resources/views/pages/\342\232\241review-page.blade.php" @@ -2160,38 +2160,36 @@ class="opacity-0 group-hover:opacity-100 transition-opacity shrink-0" @if($gitError) - + + Git error +

{{ $gitError }}

+
@elseif(empty($files)) -
-
- - @if($this->isCommitMode()) -

No file changes in this commit

-

This commit has no diff (empty or merge commit)

- @elseif($divergenceState === DivergenceState::Diverged) - {{-- Empty *because* the checkout drifted: tell the one true story instead - of a generic "clean" message disconnected from the divergence marker. --}} -

Nothing to review on {{ $divergenceContext['target'] ?? $projectBranch }}

-

- Your repo is on {{ $divergenceContext['currentBranch'] ?? '' }} right now. - Switch your review to it, or edit files on {{ $divergenceContext['target'] ?? $projectBranch }} to see changes here. -

-
- - -
- @else -

Working tree is clean

-

Edit files to see them here

- @endif -
-
+ @if($this->isCommitMode()) + + No file changes in this commit +

This commit has no diff (empty or merge commit)

+
+ @elseif($divergenceState === DivergenceState::Diverged) + {{-- Empty *because* the checkout drifted: tell the one true story instead + of a generic "clean" message disconnected from the divergence marker. --}} + + Nothing to review on {{ $divergenceContext['target'] ?? $projectBranch }} +

+ Your repo is on {{ $divergenceContext['currentBranch'] ?? '' }} right now. + Switch your review to it, or edit files on {{ $divergenceContext['target'] ?? $projectBranch }} to see changes here. +

+ + + + +
+ @else + + Working tree is clean +

Edit files to see them here

+
+ @endif @else {{-- Review Pairs (working directory mode only) --}} @if(! $this->isCommitMode()) From f34ad1156fdbfdaf38d9bcac6a3bcbc34b4a4f8d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 29 May 2026 13:07:15 +0000 Subject: [PATCH 04/22] =?UTF-8?q?feat:=20theme=20the=20Flux=20layer=20?= =?UTF-8?q?=E2=80=94=20toast=20home=20+=20on-brand=20surface,=20no=20nativ?= =?UTF-8?q?e=20confirm()?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace the native browser confirm() on submit-with-drafts (an unthemeable OS modal in a brutalist app) with an inline arm-to-confirm: the primary button becomes "Submit without N drafts?" on first click, submits on the second, disarms on click-outside. Timer-free, matches the app's arm-commit-button vocabulary. - Give Flux toasts a single defined home (bottom-right) via , clear of the undo-toast and submit bar. - Override the Flux toast surface onto gh-surface/gh-border so it matches the hand-built undo-toast instead of stock white/zinc. https://claude.ai/code/session_01MjD6k8JXwGZjxXw1fajJ97 --- resources/css/app.css | 8 ++++++++ resources/views/components/feedback-submit-bar.blade.php | 8 ++++++-- resources/views/layouts/app.blade.php | 3 +++ 3 files changed, 17 insertions(+), 2 deletions(-) diff --git a/resources/css/app.css b/resources/css/app.css index 6299f6c0..b6a17918 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -46,6 +46,14 @@ --color-accent-foreground: rgb(var(--gh-bg)); } +/* Bring Flux toasts onto the app surface so they match the hand-built + undo-toast (radius/shadow already align). Targets the toast dialog body; + higher specificity than Flux's bg-white/zinc utilities, loaded after them. */ +[data-flux-toast-dialog] > div { + background-color: rgb(var(--gh-surface)); + border-color: rgb(var(--gh-border)); +} + * { scrollbar-width: thin; scrollbar-color: gray transparent; diff --git a/resources/views/components/feedback-submit-bar.blade.php b/resources/views/components/feedback-submit-bar.blade.php index e8b872b4..b9959481 100644 --- a/resources/views/components/feedback-submit-bar.blade.php +++ b/resources/views/components/feedback-submit-bar.blade.php @@ -68,6 +68,7 @@ @else
+ {{-- Drafts won't be included: arm-to-confirm inline (no native confirm() dialog). --}} - {{ $submitLabel }} + {{ $submitLabel }} +
diff --git a/resources/views/layouts/app.blade.php b/resources/views/layouts/app.blade.php index 2d2ae844..5083685b 100644 --- a/resources/views/layouts/app.blade.php +++ b/resources/views/layouts/app.blade.php @@ -268,6 +268,9 @@ class="text-gh-muted text-xs font-mono tabular-nums shrink-0 min-w-[3rem] text-r
+ {{-- Single defined home for Flux toasts (bottom-right), clear of the undo-toast + and submit bar. Surface styling is brought onto gh-* tokens in app.css. --}} + {{ $slot }}