From 21fbb941770e6faa58bb39f35086d1d23c61c215 Mon Sep 17 00:00:00 2001 From: Kjell Kind Date: Sun, 25 Oct 2026 00:33:32 +0200 Subject: [PATCH 1/4] fix: land a thread's end dot on its parent's actions row When replies hang off the end dot, the commit has no padding below its actions, so the dot sat 20px up from the bottom while the actions row is centred 12px up (14px below md, where the secondary actions fold into a 28px menu button). The dot now sits on the row. Co-Authored-By: Claude Opus 5.5 --- resources/js/components/graph-rail.tsx | 17 ++++++++++++++--- tests/Unit/RailEntrance.test.tsx | 15 +++++++++++++++ 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/resources/js/components/graph-rail.tsx b/resources/js/components/graph-rail.tsx index c87095d6a..4388fb83b 100644 --- a/resources/js/components/graph-rail.tsx +++ b/resources/js/components/graph-rail.tsx @@ -74,6 +74,9 @@ export function GraphRail({ // A fresh commit going on below another one, like a reply chained under the last commit of a thread, scales in once // the trunk has reached it. const waitsForTrunk = isFresh && previousLane !== undefined; + // Replies hang off the end dot right under the commit's actions, with no padding below them, so the dot sits lower + // to land on their row: 24px tall, or 28px below md, where the secondary actions fold into a menu button. + const endsOnActions = endsWithDot && hasReplyTail; return (
@@ -266,11 +273,13 @@ export function RailEndDot({ isDimmed = false, delay = endDotDelay, isLeaving = false, + isOnActions = false, onLeft, }: { spec?: GraphNodeSpec | null; isFresh?: boolean; isDimmed?: boolean; + isOnActions?: boolean; delay?: number; isLeaving?: boolean; onLeft?: () => void; @@ -290,7 +299,9 @@ export function RailEndDot({ data-dimmed={isDimmed || undefined} isFresh={isFresh && !isLeaving} className={cn( - 'group/rail-node absolute bottom-5 left-1/2 flex -translate-x-1/2 translate-y-1/2 rounded-full', + 'group/rail-node absolute', + isOnActions ? 'bottom-3.5 md:bottom-3' : 'bottom-5', + 'left-1/2 flex -translate-x-1/2 translate-y-1/2 rounded-full', { 'motion-safe:animate-node-out': isLeaving }, )} style={ diff --git a/tests/Unit/RailEntrance.test.tsx b/tests/Unit/RailEntrance.test.tsx index 7fe427437..d2dbbe2db 100644 --- a/tests/Unit/RailEntrance.test.tsx +++ b/tests/Unit/RailEntrance.test.tsx @@ -106,3 +106,18 @@ test('leaves the end dot of a commit already there still', () => { assert.ok(dot); assert.doesNotMatch(dot, /animate-node-in/); }); + +test('lowers the end dot onto the actions row when replies hang off it', () => { + const onActions = endDot( + renderToStaticMarkup( + , + ), + ); + const alone = endDot( + renderToStaticMarkup(), + ); + + assert.match(onActions ?? '', /bottom-3\.5 md:bottom-3/); + assert.doesNotMatch(onActions ?? '', /bottom-5/); + assert.match(alone ?? '', /bottom-5/); +}); From 37ff01839a0f206e9aafe3393370d9a379d9bc53 Mon Sep 17 00:00:00 2001 From: Kjell Kind Date: Sun, 25 Oct 2026 00:46:29 +0200 Subject: [PATCH 2/4] fix: land every commit's end dot on its actions row A commit ending a thread with no replies open still had its dot 5px below the actions row (7px below md): the row sits above the commit's 12px padding and 1px border, centred 25px up, while the dot sat 20px up. Commit cards now tell the rail their actions row comes last, so the dot lands on it with or without replies; notifications and tombstones keep their dot where it was. The report button was 28px tall next to the 24px copy-link button, so rows on others' commits were 4px taller and their dot 2px off. It now takes the copy-link button's padding. Co-Authored-By: Claude Opus 5.5 --- resources/js/components/commit-card.tsx | 1 + .../js/components/commit-report-button.tsx | 2 +- resources/js/components/graph-rail.tsx | 28 ++++++++++++------- tests/Unit/RailEntrance.test.tsx | 24 +++++++++++----- 4 files changed, 37 insertions(+), 18 deletions(-) diff --git a/resources/js/components/commit-card.tsx b/resources/js/components/commit-card.tsx index d0e5a8e93..c83d82286 100644 --- a/resources/js/components/commit-card.tsx +++ b/resources/js/components/commit-card.tsx @@ -272,6 +272,7 @@ export function CommitCard({ lane={laneColor(commit.user.lane)} previousLane={previousLane} endsWithDot={endsWithDot} + endsOnActions nodePosition={railNodePosition} hasTail={hasRailTail && railNodePosition !== 'bottom'} hasReplyTail={ diff --git a/resources/js/components/commit-report-button.tsx b/resources/js/components/commit-report-button.tsx index e2567a628..4068fb4b6 100644 --- a/resources/js/components/commit-report-button.tsx +++ b/resources/js/components/commit-report-button.tsx @@ -54,7 +54,7 @@ export function CommitReportButton({ commit }: { commit: Commit }) { - -
- )} - + Cancel + + + + )} {commit.replies_count !== undefined && ( diff --git a/resources/js/components/commit-thread.tsx b/resources/js/components/commit-thread.tsx index 39d084fd9..857619da9 100644 --- a/resources/js/components/commit-thread.tsx +++ b/resources/js/components/commit-thread.tsx @@ -563,10 +563,13 @@ export function ThreadActions({ thread, actions, hasKeyboardShortcuts = false, + hasComposer = true, }: { thread: Thread; actions?: ReactNode; hasKeyboardShortcuts?: boolean; + /** False when the caller renders the reply composer itself, as ThreadReplyComposer. */ + hasComposer?: boolean; }) { const { auth } = usePage().props; const { commit } = thread; @@ -663,17 +666,23 @@ export function ThreadActions({ )} - {thread.isReplying && !isDeleted(commit) && ( - - )} + {hasComposer && } ); } +export function ThreadReplyComposer({ thread }: { thread: Thread }) { + const { commit } = thread; + + return thread.isReplying && !isDeleted(commit) ? ( + + ) : null; +} + export function ThreadSpine({ thread, className, diff --git a/resources/js/components/graph-rail.tsx b/resources/js/components/graph-rail.tsx index d3fe381e4..72223c5bc 100644 --- a/resources/js/components/graph-rail.tsx +++ b/resources/js/components/graph-rail.tsx @@ -77,19 +77,19 @@ export function GraphRail({ // A fresh commit going on below another one, like a reply chained under the last commit of a thread, scales in once // the trunk has reached it. const waitsForTrunk = isFresh && previousLane !== undefined; - // The actions row is 24px tall, or 28px below md, where the secondary actions fold into a menu button. Replies hang - // right under it; otherwise the commit's 12px padding and 1px border sit below it. - const endDotBottom = !endsOnActions - ? 'bottom-5' - : hasReplyTail - ? 'bottom-3.5 md:bottom-3' - : 'bottom-[27px] md:bottom-[25px]'; + // The actions row is 24px tall, or 28px below md, where the secondary actions fold into a menu button. It ends the + // rail's row: what opens below it, like the reply composer, has a row of its own. + const endDotBottom = endsOnActions ? 'bottom-3.5 md:bottom-3' : 'bottom-5'; return (