Skip to content

Commit b54e84d

Browse files
authored
Redesign branhces reordering (#11390)
* feat(ui): add drag handle and refine branch header layout/styles * implement another branch line overlay * feat(overlay): dim other overlays when one is hovered to focus UX * style tweaks * "check" fixes * fix failing tests * branch header: remove unwanted focus here we handle focus with focusable * chore: replace="scss" with lang="postcss" in Svelte components We don’t use SCSS * fix failing tests * fix wrong css syntax * fix: wrong image directory
1 parent b76b1f0 commit b54e84d

25 files changed

+316
-151
lines changed

apps/desktop/src/components/BranchCard.svelte

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,10 +149,6 @@
149149
}
150150
</script>
151151

152-
{#if ((args.type === 'stack-branch' && !args.first) || (args.type === 'normal-branch' && !args.first)) && lineColor}
153-
<BranchDividerLine {lineColor} />
154-
{/if}
155-
156152
<div
157153
class="branch-card"
158154
class:selected

apps/desktop/src/components/BranchCommitList.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,10 @@
33
import CardOverlay from '$components/CardOverlay.svelte';
44
import CommitContextMenu from '$components/CommitContextMenu.svelte';
55
import CommitGoesHere from '$components/CommitGoesHere.svelte';
6+
import CommitLineOverlay from '$components/CommitLineOverlay.svelte';
67
import CommitRow from '$components/CommitRow.svelte';
78
import Dropzone from '$components/Dropzone.svelte';
89
9-
import LineOverlay from '$components/LineOverlay.svelte';
1010
import ReduxResult from '$components/ReduxResult.svelte';
1111
import UpstreamCommitsAction from '$components/UpstreamCommitsAction.svelte';
1212
import { isLocalAndRemoteCommit, isUpstreamCommit } from '$components/lib';
@@ -206,7 +206,7 @@
206206
{#if !isCommitting}
207207
<Dropzone handlers={[dropzone]}>
208208
{#snippet overlay({ hovered, activated })}
209-
<LineOverlay {hovered} {activated} />
209+
<CommitLineOverlay {hovered} {activated} />
210210
{/snippet}
211211
</Dropzone>
212212
{/if}

apps/desktop/src/components/BranchDividerLine.svelte

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
height: 12px;
1515
margin: 0 21px;
1616
background-color: var(--commit-color);
17+
pointer-events: none;
1718
1819
&.short {
1920
height: 6px;

apps/desktop/src/components/BranchHeader.svelte

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,8 @@
1515
import { BranchDropData } from '$lib/branches/dropHandler';
1616
import { draggableBranch, type DraggableConfig } from '$lib/dragging/draggable';
1717
import { DROPZONE_REGISTRY } from '$lib/dragging/registry';
18-
1918
import { inject } from '@gitbutler/core/context';
20-
import { Badge } from '@gitbutler/ui';
21-
import { TestId } from '@gitbutler/ui';
19+
import { Badge, TestId, Icon } from '@gitbutler/ui';
2220
import { DRAG_STATE_SERVICE } from '@gitbutler/ui/drag/dragStateService.svelte';
2321
import { focusable } from '@gitbutler/ui/focus/focusable';
2422
import { slide } from 'svelte/transition';
@@ -147,7 +145,7 @@
147145

148146
<div class="branch-header__content">
149147
<div class="branch-header__title text-14 text-bold">
150-
<div class="branch-header__title-content flex gap-6">
148+
<div class="branch-header__title-content">
151149
<BranchHeaderIcon color={lineColor} {iconName} />
152150
<BranchLabel
153151
name={branchName}
@@ -164,6 +162,10 @@
164162
<Badge style="error">Conflicts</Badge>
165163
</div>
166164
{/if}
165+
166+
<div class="branch-header__drag-handle" data-no-drag>
167+
<Icon name="draggable-narrow" />
168+
</div>
167169
</div>
168170

169171
{#if isEmpty}
@@ -177,7 +179,6 @@
177179
{/if}
178180
</div>
179181
</div>
180-
181182
{#if showCommitGoesHere}
182183
<CommitGoesHere
183184
{commitId}
@@ -228,24 +229,27 @@
228229
.branch-header {
229230
--branch-selected-bg: var(--clr-bg-1);
230231
--branch-selected-element-bg: var(--clr-selected-not-in-focus-element);
232+
--branch-side-padding: 12px;
231233
display: flex;
232-
233234
position: relative;
234235
flex-direction: column;
235236
align-items: center;
236237
justify-content: flex-start;
237-
padding-right: 12px;
238-
padding-left: 12px;
238+
padding-left: var(--branch-side-padding);
239239
overflow: hidden;
240240
border-bottom: none;
241241
background-color: var(--branch-selected-bg);
242242
243243
/* Selected but NOT in focus */
244244
&:hover {
245245
--branch-selected-bg: var(--clr-bg-1-muted);
246+
247+
& .branch-header__drag-handle {
248+
width: 16px;
249+
opacity: 0.4;
250+
}
246251
}
247252
248-
&:focus-within,
249253
&:not(:focus-within).selected {
250254
--branch-selected-bg: var(--clr-selected-not-in-focus-bg);
251255
}
@@ -260,6 +264,7 @@
260264
.branch-header__details {
261265
display: flex;
262266
align-items: center;
267+
padding-right: var(--branch-side-padding);
263268
overflow: hidden;
264269
gap: 6px;
265270
color: var(--clr-text-2);
@@ -290,18 +295,22 @@
290295
align-items: center;
291296
justify-content: space-between;
292297
min-width: 0;
293-
gap: 4px;
298+
/* margin-right: 12px; */
299+
/* gap: 4px; */
294300
}
295301
296302
.branch-header__title-content {
303+
display: flex;
297304
flex-grow: 1;
298305
align-items: center;
299306
min-width: 0;
307+
gap: 6px;
300308
}
301309
302310
.branch-header__top-badges {
303311
display: flex;
304312
align-items: center;
313+
margin-left: 6px;
305314
gap: 4px;
306315
transform: translateY(-2px);
307316
}
@@ -317,7 +326,23 @@
317326
text-overflow: ellipsis;
318327
}
319328
329+
.branch-header__drag-handle {
330+
display: flex;
331+
position: relative;
332+
align-items: center;
333+
justify-content: flex-end;
334+
width: 10px;
335+
margin-top: -20px;
336+
margin-right: 3px;
337+
color: var(--clr-text-1);
338+
opacity: 0;
339+
transition:
340+
width var(--transition-fast),
341+
opacity var(--transition-fast);
342+
}
343+
320344
.branch-header__empty-state {
345+
padding-right: var(--branch-side-padding);
321346
color: var(--clr-text-2);
322347
opacity: 0.8;
323348
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
<script lang="ts">
2+
import BranchDividerLine from '$components/BranchDividerLine.svelte';
3+
import BranchLineOverlay from '$components/BranchLineOverlay.svelte';
4+
import Dropzone from '$components/Dropzone.svelte';
5+
import { MoveBranchDzHandler } from '$lib/branches/dropHandler';
6+
import type { ForgePrService } from '$lib/forge/interface/forgePrService';
7+
import type { StackService } from '$lib/stacks/stackService.svelte';
8+
9+
interface Props {
10+
projectId: string;
11+
stackId: string;
12+
branchName: string;
13+
lineColor: string;
14+
isCommitting: boolean;
15+
baseBranchName: string | undefined;
16+
stackService: StackService;
17+
prService: ForgePrService | undefined;
18+
isFirst?: boolean;
19+
}
20+
21+
const {
22+
projectId,
23+
stackId,
24+
branchName,
25+
lineColor,
26+
isCommitting,
27+
baseBranchName,
28+
stackService,
29+
prService,
30+
isFirst = false
31+
}: Props = $props();
32+
</script>
33+
34+
{#if !isFirst}
35+
{#if !isCommitting && baseBranchName}
36+
{@const moveBranchHandler = new MoveBranchDzHandler(
37+
stackService,
38+
prService,
39+
projectId,
40+
stackId,
41+
branchName,
42+
baseBranchName
43+
)}
44+
<Dropzone handlers={[moveBranchHandler]}>
45+
{#snippet overlay({ hovered, activated })}
46+
<div data-testid="BranchListInsertionDropzone" class="dropzone-target">
47+
<BranchDividerLine {lineColor} />
48+
{#if activated}
49+
<BranchLineOverlay {hovered} />
50+
<BranchDividerLine {lineColor} />
51+
{/if}
52+
</div>
53+
{/snippet}
54+
</Dropzone>
55+
{/if}
56+
{:else if !isCommitting && baseBranchName}
57+
{@const moveBranchHandler = new MoveBranchDzHandler(
58+
stackService,
59+
prService,
60+
projectId,
61+
stackId,
62+
branchName,
63+
baseBranchName
64+
)}
65+
<Dropzone handlers={[moveBranchHandler]}>
66+
{#snippet overlay({ hovered, activated })}
67+
{#if activated}
68+
<div data-testid="BranchListInsertionDropzone" class="stack-v p-b-10 dropzone-target">
69+
<BranchLineOverlay {hovered} />
70+
</div>
71+
{/if}
72+
{/snippet}
73+
</Dropzone>
74+
{/if}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<script lang="ts" module>
2+
// Global state to track the currently hovered overlay
3+
let currentHoveredId: symbol | null = $state(null);
4+
</script>
5+
6+
<script lang="ts">
7+
interface Props {
8+
hovered: boolean;
9+
}
10+
11+
const { hovered }: Props = $props();
12+
13+
// Unique ID for this overlay instance
14+
const overlayId = Symbol('overlay');
15+
16+
// Update global state when this overlay is hovered
17+
$effect(() => {
18+
if (hovered) {
19+
currentHoveredId = overlayId;
20+
} else {
21+
// Always clear if this was the hovered one, or if no hover state
22+
if (currentHoveredId === overlayId) {
23+
currentHoveredId = null;
24+
}
25+
}
26+
});
27+
28+
// Ensure cleanup on unmount
29+
$effect(() => {
30+
return () => {
31+
if (currentHoveredId === overlayId) {
32+
currentHoveredId = null;
33+
}
34+
};
35+
});
36+
37+
// Check if other overlays are dimmed (another overlay is hovered, not this one)
38+
const isDimmed = $derived(currentHoveredId !== null && currentHoveredId !== overlayId);
39+
</script>
40+
41+
<div class="container" class:hovered class:dimmed={isDimmed}>
42+
<div class="indicator"></div>
43+
</div>
44+
45+
<style lang="postcss">
46+
.container {
47+
z-index: var(--z-floating);
48+
position: relative;
49+
top: var(--y-offset);
50+
align-items: center;
51+
width: 100%;
52+
pointer-events: none;
53+
54+
&.hovered {
55+
& .indicator {
56+
width: calc(100% - 70px);
57+
width: 100%;
58+
}
59+
}
60+
61+
&.dimmed {
62+
& .indicator {
63+
opacity: 0.4;
64+
}
65+
}
66+
}
67+
68+
.indicator {
69+
position: absolute;
70+
top: 50%;
71+
left: 50%;
72+
width: 60%;
73+
height: 3px;
74+
transform: translate(-50%, -50%);
75+
border-radius: var(--radius-m);
76+
background-color: transparent;
77+
background-color: var(--clr-theme-pop-element);
78+
79+
transition:
80+
width 0.08s ease,
81+
opacity 0.2s ease;
82+
}
83+
</style>

0 commit comments

Comments
 (0)