mirror of
https://github.com/gitbutlerapp/gitbutler.git
synced 2024-08-16 07:40:35 +03:00
Drag-n-drop update (#4220)
* Reuse `splitFilePath` function * unnecessary `width` and `height` removed * added utils for draggable file list items - added separate CSS * WIP new styles for the commit draggable * styles for draggable commit cards updated * Draggable hunk added * Draggable lanes updated * Dropzone design updated * Dropzones code refactor * reordering lines design update * Update logic for determining reorder shift * Remove unused CSS fix scrollable container prop * dropzone animations added * Dropzone hover state UI updated * CSS update: Card overlay labels * Fix: horizontal scroll wrong observer trigger * UX: Automatically close the commit message box after commit
This commit is contained in:
parent
a1c591ffb6
commit
9ff735fd4e
@ -139,9 +139,9 @@
|
|||||||
<PullRequestCard />
|
<PullRequestCard />
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<Dropzones>
|
{#if branch.files?.length > 0}
|
||||||
{#if branch.files?.length > 0}
|
<div class="branch-card__files">
|
||||||
<div class="branch-card__files">
|
<Dropzones>
|
||||||
<BranchFiles
|
<BranchFiles
|
||||||
files={branch.files}
|
files={branch.files}
|
||||||
{isUnapplied}
|
{isUnapplied}
|
||||||
@ -162,21 +162,23 @@
|
|||||||
</InfoMessage>
|
</InfoMessage>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
</Dropzones>
|
||||||
|
|
||||||
{#if branch.active}
|
{#if branch.active}
|
||||||
<CommitDialog
|
<CommitDialog
|
||||||
projectId={project.id}
|
projectId={project.id}
|
||||||
expanded={commitBoxOpen}
|
expanded={commitBoxOpen}
|
||||||
hasSectionsAfter={branch.commits.length > 0}
|
hasSectionsAfter={branch.commits.length > 0}
|
||||||
on:action={(e) => {
|
on:action={(e) => {
|
||||||
if (e.detail === 'generate-branch-name') {
|
if (e.detail === 'generate-branch-name') {
|
||||||
generateBranchName();
|
generateBranchName();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else if branch.commits.length === 0}
|
{:else if branch.commits.length === 0}
|
||||||
|
<Dropzones>
|
||||||
<div class="new-branch">
|
<div class="new-branch">
|
||||||
<EmptyStatePlaceholder image={laneNewSvg} width="11rem">
|
<EmptyStatePlaceholder image={laneNewSvg} width="11rem">
|
||||||
<svelte:fragment slot="title">This is a new branch</svelte:fragment>
|
<svelte:fragment slot="title">This is a new branch</svelte:fragment>
|
||||||
@ -185,7 +187,9 @@
|
|||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</EmptyStatePlaceholder>
|
</EmptyStatePlaceholder>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
</Dropzones>
|
||||||
|
{:else}
|
||||||
|
<Dropzones>
|
||||||
<div class="no-changes" data-dnd-ignore>
|
<div class="no-changes" data-dnd-ignore>
|
||||||
<EmptyStatePlaceholder image={noChangesSvg} width="11rem" hasBottomMargin={false}>
|
<EmptyStatePlaceholder image={noChangesSvg} width="11rem" hasBottomMargin={false}>
|
||||||
<svelte:fragment slot="caption"
|
<svelte:fragment slot="caption"
|
||||||
@ -193,8 +197,8 @@
|
|||||||
>
|
>
|
||||||
</EmptyStatePlaceholder>
|
</EmptyStatePlaceholder>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
</Dropzones>
|
||||||
</Dropzones>
|
{/if}
|
||||||
|
|
||||||
<div class="card-commits">
|
<div class="card-commits">
|
||||||
<CommitList {isUnapplied} />
|
<CommitList {isUnapplied} />
|
||||||
@ -254,9 +258,6 @@
|
|||||||
|
|
||||||
.card {
|
.card {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
/* overflow: hidden; */
|
|
||||||
/* border: 1px solid var(--clr-border-2);
|
|
||||||
border-radius: var(--radius-m); */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.branch-card__files {
|
.branch-card__files {
|
||||||
@ -264,9 +265,6 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
/* border-left: 1px solid var(--clr-border-2);
|
|
||||||
border-right: 1px solid var(--clr-border-2);
|
|
||||||
border-radius: var(--radius-m) var(--radius-m) 0 0; */
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.card-notifications {
|
.card-notifications {
|
||||||
@ -288,12 +286,6 @@
|
|||||||
cursor: default; /* was defaulting to text cursor */
|
cursor: default; /* was defaulting to text cursor */
|
||||||
}
|
}
|
||||||
|
|
||||||
.branch-card :global(.contents) {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
min-height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* COLLAPSED LANE */
|
/* COLLAPSED LANE */
|
||||||
.collapsed-lane-container {
|
.collapsed-lane-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
@ -54,7 +54,7 @@
|
|||||||
options: {
|
options: {
|
||||||
root: null,
|
root: null,
|
||||||
rootMargin: '-1px',
|
rootMargin: '-1px',
|
||||||
threshold: 1
|
threshold: 0
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -131,6 +131,5 @@
|
|||||||
|
|
||||||
.not-in-viewport {
|
.not-in-viewport {
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
/* background-color: aquamarine; */
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -77,7 +77,7 @@
|
|||||||
{#await $selectedFile then selected}
|
{#await $selectedFile then selected}
|
||||||
{#if selected}
|
{#if selected}
|
||||||
<div
|
<div
|
||||||
class="file-preview resize-viewport"
|
class="file-preview"
|
||||||
bind:this={rsViewport}
|
bind:this={rsViewport}
|
||||||
in:slide={{ duration: 180, easing: quintOut, axis: 'x' }}
|
in:slide={{ duration: 180, easing: quintOut, axis: 'x' }}
|
||||||
style:width={`${fileWidth || $defaultFileWidthRem}rem`}
|
style:width={`${fileWidth || $defaultFileWidthRem}rem`}
|
||||||
|
@ -58,6 +58,5 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
/* overflow: hidden; */
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
import { Project } from '$lib/backend/projects';
|
import { Project } from '$lib/backend/projects';
|
||||||
import CommitMessageInput from '$lib/commit/CommitMessageInput.svelte';
|
import CommitMessageInput from '$lib/commit/CommitMessageInput.svelte';
|
||||||
import { persistedCommitMessage } from '$lib/config/config';
|
import { persistedCommitMessage } from '$lib/config/config';
|
||||||
import { draggable } from '$lib/dragging/draggable';
|
import { draggableCommit } from '$lib/dragging/draggable';
|
||||||
import { DraggableCommit, nonDraggable } from '$lib/dragging/draggables';
|
import { DraggableCommit, nonDraggable } from '$lib/dragging/draggables';
|
||||||
import BranchFilesList from '$lib/file/BranchFilesList.svelte';
|
import BranchFilesList from '$lib/file/BranchFilesList.svelte';
|
||||||
import Button from '$lib/shared/Button.svelte';
|
import Button from '$lib/shared/Button.svelte';
|
||||||
@ -25,7 +25,7 @@
|
|||||||
BaseBranch,
|
BaseBranch,
|
||||||
type CommitStatus
|
type CommitStatus
|
||||||
} from '$lib/vbranches/types';
|
} from '$lib/vbranches/types';
|
||||||
import { createEventDispatcher, type Snippet } from 'svelte';
|
import { type Snippet } from 'svelte';
|
||||||
|
|
||||||
export let branch: Branch | undefined = undefined;
|
export let branch: Branch | undefined = undefined;
|
||||||
export let commit: Commit | RemoteCommit;
|
export let commit: Commit | RemoteCommit;
|
||||||
@ -46,8 +46,7 @@
|
|||||||
|
|
||||||
const currentCommitMessage = persistedCommitMessage(project.id, branch?.id || '');
|
const currentCommitMessage = persistedCommitMessage(project.id, branch?.id || '');
|
||||||
|
|
||||||
const dispatch = createEventDispatcher<{ toggle: void }>();
|
let draggableCommitElement: HTMLElement | null = null;
|
||||||
|
|
||||||
let files: RemoteFile[] = [];
|
let files: RemoteFile[] = [];
|
||||||
let showDetails = false;
|
let showDetails = false;
|
||||||
|
|
||||||
@ -57,7 +56,6 @@
|
|||||||
|
|
||||||
function toggleFiles() {
|
function toggleFiles() {
|
||||||
showDetails = !showDetails;
|
showDetails = !showDetails;
|
||||||
dispatch('toggle');
|
|
||||||
|
|
||||||
if (showDetails) loadFiles();
|
if (showDetails) loadFiles();
|
||||||
}
|
}
|
||||||
@ -102,6 +100,14 @@
|
|||||||
commitMessageModal.close();
|
commitMessageModal.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTimeAndAuthor() {
|
||||||
|
const timeAgo = getTimeAgo(commit.createdAt);
|
||||||
|
const author = type === 'localAndRemote' || type === 'remote' ? commit.author.name : 'you';
|
||||||
|
return `${timeAgo} by ${author}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const commitShortSha = commit.id.substring(0, 7);
|
||||||
|
|
||||||
let topHeightPx = 24;
|
let topHeightPx = 24;
|
||||||
|
|
||||||
$: {
|
$: {
|
||||||
@ -109,6 +115,9 @@
|
|||||||
if (first) topHeightPx = 58;
|
if (first) topHeightPx = 58;
|
||||||
if (showDetails && !first) topHeightPx += 12;
|
if (showDetails && !first) topHeightPx += 12;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let dragDirection: 'up' | 'down' | undefined;
|
||||||
|
let isDragTargeted = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Modal bind:this={commitMessageModal} width="small">
|
<Modal bind:this={commitMessageModal} width="small">
|
||||||
@ -138,40 +147,77 @@
|
|||||||
class:is-last={last}
|
class:is-last={last}
|
||||||
class:has-lines={lines}
|
class:has-lines={lines}
|
||||||
>
|
>
|
||||||
|
{#if dragDirection && isDragTargeted}
|
||||||
|
<div
|
||||||
|
class="pseudo-reorder-zone"
|
||||||
|
class:top={dragDirection === 'up'}
|
||||||
|
class:bottom={dragDirection === 'down'}
|
||||||
|
class:is-first={first}
|
||||||
|
class:is-last={last}
|
||||||
|
></div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if lines}
|
{#if lines}
|
||||||
<div>
|
<div>
|
||||||
{@render lines(topHeightPx)}
|
{@render lines(topHeightPx)}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<CommitDragItem {commit}>
|
|
||||||
<div class="commit-card" class:is-first={first} class:is-last={last}>
|
|
||||||
<div
|
|
||||||
class="accent-border-line"
|
|
||||||
class:is-first={first}
|
|
||||||
class:is-last={last}
|
|
||||||
class:local={type === 'local'}
|
|
||||||
class:local-and-remote={type === 'localAndRemote'}
|
|
||||||
class:upstream={type === 'remote'}
|
|
||||||
class:integrated={type === 'integrated'}
|
|
||||||
></div>
|
|
||||||
|
|
||||||
|
<div class="commit-card" class:is-first={first} class:is-last={last}>
|
||||||
|
<CommitDragItem {commit}>
|
||||||
<!-- GENERAL INFO -->
|
<!-- GENERAL INFO -->
|
||||||
<div
|
<div
|
||||||
|
bind:this={draggableCommitElement}
|
||||||
class="commit__header"
|
class="commit__header"
|
||||||
on:click={toggleFiles}
|
on:click={toggleFiles}
|
||||||
on:keyup={onKeyup}
|
on:keyup={onKeyup}
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
use:draggable={commit instanceof Commit
|
on:dragenter={() => {
|
||||||
|
isDragTargeted = true;
|
||||||
|
}}
|
||||||
|
on:dragleave={() => {
|
||||||
|
isDragTargeted = false;
|
||||||
|
}}
|
||||||
|
on:drop={() => {
|
||||||
|
isDragTargeted = false;
|
||||||
|
}}
|
||||||
|
on:drag={(e) => {
|
||||||
|
const target = e.target as HTMLElement;
|
||||||
|
const targetHeight = target.offsetHeight;
|
||||||
|
const targetTop = target.getBoundingClientRect().top;
|
||||||
|
const mouseY = e.clientY;
|
||||||
|
|
||||||
|
const isTop = mouseY < targetTop + targetHeight / 2;
|
||||||
|
|
||||||
|
dragDirection = isTop ? 'up' : 'down';
|
||||||
|
}}
|
||||||
|
use:draggableCommit={commit instanceof Commit && !isUnapplied && type !== 'integrated'
|
||||||
? {
|
? {
|
||||||
|
label: commit.descriptionTitle,
|
||||||
|
sha: commitShortSha,
|
||||||
|
dateAndAuthor: getTimeAndAuthor(),
|
||||||
|
commitType: type,
|
||||||
data: new DraggableCommit(commit.branchId, commit, isHeadCommit),
|
data: new DraggableCommit(commit.branchId, commit, isHeadCommit),
|
||||||
extendWithClass: 'commit_draggable'
|
viewportId: 'board-viewport'
|
||||||
}
|
}
|
||||||
: nonDraggable()}
|
: nonDraggable()}
|
||||||
>
|
>
|
||||||
<div class="commit__drag-icon">
|
<div
|
||||||
<Icon name="draggable-narrow" />
|
class="accent-border-line"
|
||||||
</div>
|
class:is-first={first}
|
||||||
|
class:is-last={last}
|
||||||
|
class:local={type === 'local'}
|
||||||
|
class:local-and-remote={type === 'localAndRemote'}
|
||||||
|
class:upstream={type === 'remote'}
|
||||||
|
class:integrated={type === 'integrated'}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
{#if type === 'local' || type === 'localAndRemote'}
|
||||||
|
<div class="commit__drag-icon">
|
||||||
|
<Icon name="draggable-narrow" />
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if first}
|
{#if first}
|
||||||
<div class="commit__type text-semibold text-base-12">
|
<div class="commit__type text-semibold text-base-12">
|
||||||
@ -209,7 +255,7 @@
|
|||||||
class="commit__subtitle-btn commit__subtitle-btn_dashed"
|
class="commit__subtitle-btn commit__subtitle-btn_dashed"
|
||||||
on:click|stopPropagation={() => copyToClipboard(commit.id)}
|
on:click|stopPropagation={() => copyToClipboard(commit.id)}
|
||||||
>
|
>
|
||||||
<span>{commit.id.substring(0, 7)}</span>
|
<span>{commitShortSha}</span>
|
||||||
|
|
||||||
<div class="commit__subtitle-btn__icon">
|
<div class="commit__subtitle-btn__icon">
|
||||||
<Icon name="copy-small" />
|
<Icon name="copy-small" />
|
||||||
@ -235,11 +281,7 @@
|
|||||||
|
|
||||||
<span class="commit__subtitle-divider">•</span>
|
<span class="commit__subtitle-divider">•</span>
|
||||||
|
|
||||||
<span
|
<span>{getTimeAndAuthor()}</span>
|
||||||
>{getTimeAgo(commit.createdAt)}{type === 'localAndRemote' || type === 'remote'
|
|
||||||
? ` by ${commit.author.name}`
|
|
||||||
: ' by you'}</span
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
@ -285,25 +327,11 @@
|
|||||||
<BranchFilesList {files} {isUnapplied} readonly={type === 'remote'} />
|
<BranchFilesList {files} {isUnapplied} readonly={type === 'remote'} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</CommitDragItem>
|
||||||
</CommitDragItem>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
/* amend drop zone */
|
|
||||||
:global(.amend-dz-active .amend-dz-marker) {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
:global(.amend-dz-hover .hover-text) {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
:global(.commit_draggable) {
|
|
||||||
cursor: grab;
|
|
||||||
background-color: var(--clr-bg-1);
|
|
||||||
border-radius: var(--radius-m);
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.commit-row {
|
.commit-row {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -321,6 +349,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
position: relative;
|
position: relative;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
background-color: var(--clr-bg-1);
|
background-color: var(--clr-bg-1);
|
||||||
border-right: 1px solid var(--clr-border-2);
|
border-right: 1px solid var(--clr-border-2);
|
||||||
@ -345,8 +374,12 @@
|
|||||||
|
|
||||||
.accent-border-line {
|
.accent-border-line {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
width: 4px;
|
width: 4px;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
z-index: var(--z-ground);
|
||||||
|
|
||||||
&.local {
|
&.local {
|
||||||
background-color: var(--clr-commit-local);
|
background-color: var(--clr-commit-local);
|
||||||
}
|
}
|
||||||
@ -517,4 +550,29 @@
|
|||||||
margin-left: 2px;
|
margin-left: 2px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* PSUEDO DROPZONE */
|
||||||
|
.pseudo-reorder-zone {
|
||||||
|
z-index: var(--z-lifted);
|
||||||
|
position: absolute;
|
||||||
|
height: 2px;
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--clr-theme-pop-element);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pseudo-reorder-zone.top {
|
||||||
|
top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pseudo-reorder-zone.bottom {
|
||||||
|
bottom: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pseudo-reorder-zone.top.is-first {
|
||||||
|
top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pseudo-reorder-zone.bottom.is-last {
|
||||||
|
bottom: -6px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -38,6 +38,7 @@
|
|||||||
$commitMessage = '';
|
$commitMessage = '';
|
||||||
} finally {
|
} finally {
|
||||||
isCommitting = false;
|
isCommitting = false;
|
||||||
|
$expanded = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
@ -38,7 +38,14 @@
|
|||||||
{@render squashDropzone()}
|
{@render squashDropzone()}
|
||||||
|
|
||||||
{#snippet overlay({ hovered, activated })}
|
{#snippet overlay({ hovered, activated })}
|
||||||
<CardOverlay {hovered} {activated} label="Amend commit" />
|
<CardOverlay
|
||||||
|
{hovered}
|
||||||
|
{activated}
|
||||||
|
label="Amend commit"
|
||||||
|
extraPaddings={{
|
||||||
|
left: 4
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
@ -48,7 +55,14 @@
|
|||||||
{@render children()}
|
{@render children()}
|
||||||
|
|
||||||
{#snippet overlay({ hovered, activated })}
|
{#snippet overlay({ hovered, activated })}
|
||||||
<CardOverlay {hovered} {activated} label="Squash commit" />
|
<CardOverlay
|
||||||
|
{hovered}
|
||||||
|
{activated}
|
||||||
|
label="Squash commit"
|
||||||
|
extraPaddings={{
|
||||||
|
left: 4
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
@ -57,6 +71,5 @@
|
|||||||
.dropzone-wrapper {
|
.dropzone-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -149,8 +149,8 @@
|
|||||||
{@render reorderDropzone(
|
{@render reorderDropzone(
|
||||||
reorderDropzoneManager.dropzoneBelowCommit(commit.id),
|
reorderDropzoneManager.dropzoneBelowCommit(commit.id),
|
||||||
getReorderDropzoneOffset({
|
getReorderDropzoneOffset({
|
||||||
isLast: $localAndRemoteCommits.length === 0 && idx + 1 === $localCommits.length,
|
isLast: idx + 1 === $localCommits.length,
|
||||||
isMiddle: $localAndRemoteCommits.length > 0 && idx + 1 === $localCommits.length
|
isMiddle: idx + 1 === $localCommits.length
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -181,7 +181,8 @@
|
|||||||
{@render reorderDropzone(
|
{@render reorderDropzone(
|
||||||
reorderDropzoneManager.dropzoneBelowCommit(commit.id),
|
reorderDropzoneManager.dropzoneBelowCommit(commit.id),
|
||||||
getReorderDropzoneOffset({
|
getReorderDropzoneOffset({
|
||||||
isLast: idx + 1 === $localAndRemoteCommits.length
|
isMiddle: idx + 1 === $localAndRemoteCommits.length
|
||||||
|
// isLast: idx + 1 === $localAndRemoteCommits.length
|
||||||
})
|
})
|
||||||
)}
|
)}
|
||||||
<InsertEmptyCommitAction
|
<InsertEmptyCommitAction
|
||||||
@ -240,6 +241,7 @@
|
|||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
.commits {
|
.commits {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background-color: var(--clr-bg-2);
|
background-color: var(--clr-bg-2);
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
import { Project } from '$lib/backend/projects';
|
import { Project } from '$lib/backend/projects';
|
||||||
import BranchDropzone from '$lib/branch/BranchDropzone.svelte';
|
import BranchDropzone from '$lib/branch/BranchDropzone.svelte';
|
||||||
import BranchLane from '$lib/branch/BranchLane.svelte';
|
import BranchLane from '$lib/branch/BranchLane.svelte';
|
||||||
import { cloneWithRotation } from '$lib/dragging/draggable';
|
import { cloneElement } from '$lib/dragging/draggable';
|
||||||
import { persisted } from '$lib/persisted/persisted';
|
import { persisted } from '$lib/persisted/persisted';
|
||||||
import Icon from '$lib/shared/Icon.svelte';
|
import Icon from '$lib/shared/Icon.svelte';
|
||||||
import { getContext, getContextStore } from '$lib/utils/context';
|
import { getContext, getContextStore } from '$lib/utils/context';
|
||||||
@ -94,12 +94,12 @@
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
clone = cloneWithRotation(e.target);
|
clone = cloneElement(e.target as HTMLElement);
|
||||||
document.body.appendChild(clone);
|
document.body.appendChild(clone);
|
||||||
// Get chromium to fire dragover & drop events
|
// Get chromium to fire dragover & drop events
|
||||||
// https://stackoverflow.com/questions/6481094/html5-drag-and-drop-ondragover-not-firing-in-chrome/6483205#6483205
|
// https://stackoverflow.com/questions/6481094/html5-drag-and-drop-ondragover-not-firing-in-chrome/6483205#6483205
|
||||||
e.dataTransfer?.setData('text/html', 'd'); // cannot be empty string
|
e.dataTransfer?.setData('text/html', 'd'); // cannot be empty string
|
||||||
e.dataTransfer?.setDragImage(clone, e.offsetX + 30, e.offsetY + 30); // Adds the padding
|
e.dataTransfer?.setDragImage(clone, e.offsetX, e.offsetY); // Adds the padding
|
||||||
dragged = e.currentTarget;
|
dragged = e.currentTarget;
|
||||||
priorPosition = Array.from(dropZone.children).indexOf(dragged);
|
priorPosition = Array.from(dropZone.children).indexOf(dragged);
|
||||||
}}
|
}}
|
||||||
|
@ -1,82 +1,47 @@
|
|||||||
import { dropzoneRegistry } from './dropzone';
|
import { dropzoneRegistry } from './dropzone';
|
||||||
|
import { getVSIFileIcon } from '$lib/ext-icons';
|
||||||
|
import { pxToRem } from '$lib/utils/pxToRem';
|
||||||
|
import { type CommitStatus } from '$lib/vbranches/types';
|
||||||
import type { Draggable } from './draggables';
|
import type { Draggable } from './draggables';
|
||||||
|
|
||||||
export interface DraggableConfig {
|
export interface DraggableConfig {
|
||||||
readonly selector?: string;
|
readonly selector?: string;
|
||||||
readonly disabled?: boolean;
|
readonly disabled?: boolean;
|
||||||
|
readonly label?: string;
|
||||||
|
readonly filePath?: string;
|
||||||
|
readonly sha?: string;
|
||||||
|
readonly dateAndAuthor?: string;
|
||||||
|
readonly commitType?: CommitStatus;
|
||||||
readonly data?: Draggable | Promise<Draggable>;
|
readonly data?: Draggable | Promise<Draggable>;
|
||||||
readonly viewportId?: string;
|
readonly viewportId?: string;
|
||||||
readonly extendWithClass?: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyContainerStyle(element: HTMLElement) {
|
function createElement(
|
||||||
element.style.position = 'absolute';
|
tag: string,
|
||||||
element.style.top = '-9999px'; // Element has to be in the DOM so we move it out of sight
|
classNames: string[],
|
||||||
element.style.display = 'inline-block';
|
textContent?: string,
|
||||||
element.style.padding = '30px'; // To prevent clipping of rotated element
|
src?: string
|
||||||
|
): HTMLElement {
|
||||||
|
const el = document.createElement(tag);
|
||||||
|
el.classList.add(...classNames);
|
||||||
|
if (textContent) el.textContent = textContent;
|
||||||
|
if (src) (el as HTMLImageElement).src = src;
|
||||||
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createContainerForMultiDrag(
|
function setupDragHandlers(
|
||||||
children: Element[],
|
node: HTMLElement,
|
||||||
extendWithClass: string | undefined
|
opts: DraggableConfig,
|
||||||
): HTMLDivElement {
|
createClone: (opts: DraggableConfig, selectedElements: HTMLElement[]) => HTMLElement,
|
||||||
const inner = document.createElement('div');
|
params: {
|
||||||
inner.style.display = 'flex';
|
handlerWidth: boolean;
|
||||||
inner.style.flexDirection = 'column';
|
maxHeight?: number;
|
||||||
inner.style.gap = '0.125rem';
|
} = {
|
||||||
|
handlerWidth: false
|
||||||
children.forEach((child) => {
|
}
|
||||||
inner.appendChild(cloneWithPreservedDimensions(child, extendWithClass));
|
) {
|
||||||
});
|
|
||||||
rotateElement(inner);
|
|
||||||
|
|
||||||
const container = document.createElement('div');
|
|
||||||
container.appendChild(inner);
|
|
||||||
applyContainerStyle(container);
|
|
||||||
|
|
||||||
return container;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cloneWithPreservedDimensions(node: any, extendWithClass: string | undefined) {
|
|
||||||
const clone = node.cloneNode(true) as HTMLElement;
|
|
||||||
clone.style.height = node.clientHeight + 'px';
|
|
||||||
clone.style.width = node.clientWidth + 'px';
|
|
||||||
clone.classList.remove('selected-draggable');
|
|
||||||
|
|
||||||
extendWithClass && clone.classList.add(extendWithClass);
|
|
||||||
|
|
||||||
return clone;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function cloneWithRotation(node: any, extendWithClass: string | undefined = undefined) {
|
|
||||||
const container = document.createElement('div');
|
|
||||||
const clone = cloneWithPreservedDimensions(node, extendWithClass) as HTMLElement;
|
|
||||||
container.appendChild(clone);
|
|
||||||
|
|
||||||
// exclude all ignored elements from the clone
|
|
||||||
const ignoredElements = container.querySelectorAll('[data-remove-from-draggable]');
|
|
||||||
ignoredElements.forEach((element) => {
|
|
||||||
element.remove();
|
|
||||||
});
|
|
||||||
|
|
||||||
applyContainerStyle(container);
|
|
||||||
|
|
||||||
// Style the inner node so it retains the shape and then rotate
|
|
||||||
// TODO: This rotation puts a requirement on draggables to have
|
|
||||||
// an outer container, which feels extra. Consider refactoring.
|
|
||||||
rotateElement(clone);
|
|
||||||
return container as HTMLElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
function rotateElement(element: HTMLElement) {
|
|
||||||
element.style.rotate = `${Math.floor(Math.random() * 3)}deg`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function draggable(node: HTMLElement, initialOpts: DraggableConfig) {
|
|
||||||
let opts = initialOpts;
|
|
||||||
let dragHandle: HTMLElement | null;
|
let dragHandle: HTMLElement | null;
|
||||||
let clone: HTMLElement | undefined;
|
let clone: HTMLElement;
|
||||||
|
|
||||||
let selectedElements: HTMLElement[] = [];
|
let selectedElements: HTMLElement[] = [];
|
||||||
|
|
||||||
function handleMouseDown(e: MouseEvent) {
|
function handleMouseDown(e: MouseEvent) {
|
||||||
@ -84,84 +49,75 @@ export function draggable(node: HTMLElement, initialOpts: DraggableConfig) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleDragStart(e: DragEvent) {
|
function handleDragStart(e: DragEvent) {
|
||||||
let elt: HTMLElement | null = dragHandle;
|
e.stopPropagation();
|
||||||
|
|
||||||
while (elt) {
|
if (dragHandle && dragHandle.dataset.noDrag !== undefined) {
|
||||||
if (elt.dataset.noDrag !== undefined) {
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
return false;
|
||||||
e.preventDefault();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
elt = elt.parentElement;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the draggable specifies a selector then we check if we're dragging selected elements
|
|
||||||
if (opts.selector) {
|
if (opts.selector) {
|
||||||
// Checking for selected siblings in the parent of the parent container likely works
|
const parentNode = node.parentElement?.parentElement;
|
||||||
// for most use-cases but it was done here primarily for dragging multiple files.
|
if (!parentNode) {
|
||||||
const parentNode = node.parentNode?.parentNode;
|
console.error('draggable parent node not found');
|
||||||
selectedElements = parentNode
|
return;
|
||||||
? Array.from(parentNode.querySelectorAll(opts.selector).values() as Iterable<HTMLElement>)
|
|
||||||
: [];
|
|
||||||
|
|
||||||
if (selectedElements.length > 0) {
|
|
||||||
clone = createContainerForMultiDrag(selectedElements, opts.extendWithClass);
|
|
||||||
// Dim the original element while dragging
|
|
||||||
selectedElements.forEach((element) => {
|
|
||||||
element.style.opacity = '0.5';
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
selectedElements = Array.from(
|
||||||
|
parentNode.querySelectorAll(opts.selector) as NodeListOf<HTMLElement>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!clone) {
|
if (selectedElements.length === 0) {
|
||||||
clone = cloneWithRotation(node, opts.extendWithClass);
|
selectedElements = [node];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clone = createClone(opts, selectedElements);
|
||||||
|
if (params.handlerWidth) {
|
||||||
|
clone.style.width = node.clientWidth + 'px';
|
||||||
|
}
|
||||||
|
if (params.maxHeight) {
|
||||||
|
clone.style.maxHeight = pxToRem(params.maxHeight);
|
||||||
|
}
|
||||||
|
|
||||||
|
// console.log('selectedElements', selectedElements);
|
||||||
|
selectedElements.forEach((el) => el.classList.add('drag-handle'));
|
||||||
document.body.appendChild(clone);
|
document.body.appendChild(clone);
|
||||||
|
|
||||||
Array.from(dropzoneRegistry.values()).forEach((dropzone) => {
|
Array.from(dropzoneRegistry.values()).forEach((dropzone) => {
|
||||||
dropzone.register(opts.data);
|
dropzone.register(opts.data);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Get chromium to fire dragover & drop events
|
if (e.dataTransfer) {
|
||||||
// https://stackoverflow.com/questions/6481094/html5-drag-and-drop-ondragover-not-firing-in-chrome/6483205#6483205
|
if (params.handlerWidth) {
|
||||||
e.dataTransfer?.setData('text/html', 'placeholder copy'); // cannot be empty string
|
e.dataTransfer.setDragImage(clone, e.offsetX, e.offsetY);
|
||||||
e.dataTransfer?.setDragImage(clone, e.offsetX + 30, e.offsetY + 30); // Adds the padding
|
} else {
|
||||||
e.stopPropagation();
|
e.dataTransfer.setDragImage(clone, clone.offsetWidth - 20, 25);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get chromium to fire dragover & drop events
|
||||||
|
// https://stackoverflow.com/questions/6481094/html5-drag-and-drop-ondragover-not-firing-in-chrome/6483205#6483205
|
||||||
|
e.dataTransfer?.setData('text/html', 'd'); // cannot be empty string
|
||||||
|
e.dataTransfer.effectAllowed = 'uninitialized';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleDragEnd(e: DragEvent) {
|
function handleDragEnd(e: DragEvent) {
|
||||||
if (clone) {
|
e.stopPropagation();
|
||||||
clone.remove();
|
if (clone) clone.remove();
|
||||||
clone = undefined;
|
selectedElements.forEach((el) => el.classList.remove('drag-handle'));
|
||||||
}
|
|
||||||
|
|
||||||
// reset the opacity of the selected elements
|
|
||||||
selectedElements.forEach((element) => {
|
|
||||||
element.style.opacity = '1';
|
|
||||||
});
|
|
||||||
|
|
||||||
Array.from(dropzoneRegistry.values()).forEach((dropzone) => {
|
Array.from(dropzoneRegistry.values()).forEach((dropzone) => {
|
||||||
dropzone.unregister();
|
dropzone.unregister();
|
||||||
});
|
});
|
||||||
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const viewport = opts.viewportId ? document.getElementById(opts.viewportId) : null;
|
|
||||||
const triggerRange = 150;
|
|
||||||
const scrollSpeed = (viewport?.clientWidth || 500) / 2;
|
|
||||||
let lastDrag = new Date().getTime();
|
|
||||||
|
|
||||||
function handleDrag(e: DragEvent) {
|
function handleDrag(e: DragEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
const viewport = opts.viewportId ? document.getElementById(opts.viewportId) : null;
|
||||||
if (!viewport) return;
|
if (!viewport) return;
|
||||||
if (new Date().getTime() - lastDrag < 500) return;
|
const triggerRange = 150;
|
||||||
lastDrag = new Date().getTime();
|
const scrollSpeed = (viewport.clientWidth || 500) / 2;
|
||||||
|
|
||||||
const viewportWidth = viewport.clientWidth;
|
const viewportWidth = viewport.clientWidth;
|
||||||
const relativeX = e.clientX - viewport.getBoundingClientRect().left;
|
const relativeX = e.clientX - viewport.getBoundingClientRect().left;
|
||||||
|
|
||||||
// Scroll horizontally if the draggable is near the edge of the viewport
|
|
||||||
if (relativeX < triggerRange) {
|
if (relativeX < triggerRange) {
|
||||||
viewport.scrollBy(-scrollSpeed, 0);
|
viewport.scrollBy(-scrollSpeed, 0);
|
||||||
} else if (relativeX > viewportWidth - triggerRange) {
|
} else if (relativeX > viewportWidth - triggerRange) {
|
||||||
@ -189,12 +145,123 @@ export function draggable(node: HTMLElement, initialOpts: DraggableConfig) {
|
|||||||
setup(opts);
|
setup(opts);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
update(opts: DraggableConfig) {
|
update(newOpts: DraggableConfig) {
|
||||||
clean();
|
clean();
|
||||||
setup(opts);
|
setup(newOpts);
|
||||||
},
|
},
|
||||||
destroy() {
|
destroy() {
|
||||||
clean();
|
clean();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//////////////////////////
|
||||||
|
//// COMMIT DRAGGABLE ////
|
||||||
|
//////////////////////////
|
||||||
|
|
||||||
|
export function createCommitElement(
|
||||||
|
commitType: CommitStatus | undefined,
|
||||||
|
label: string | undefined,
|
||||||
|
sha: string | undefined,
|
||||||
|
dateAndAuthor: string | undefined
|
||||||
|
): HTMLDivElement {
|
||||||
|
const cardEl = createElement('div', ['draggable-commit']) as HTMLDivElement;
|
||||||
|
const labelEl = createElement('span', ['text-base-13', 'text-bold'], label || 'Empty commit');
|
||||||
|
const infoEl = createElement('div', ['draggable-commit-info', 'text-base-11']);
|
||||||
|
const shaEl = createElement('span', ['draggable-commit-info-text'], sha);
|
||||||
|
const dateAndAuthorEl = createElement('span', ['draggable-commit-info-text'], dateAndAuthor);
|
||||||
|
|
||||||
|
if (commitType) {
|
||||||
|
const indicatorClass = `draggable-commit-${commitType}`;
|
||||||
|
labelEl.classList.add('draggable-commit-indicator', indicatorClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
cardEl.appendChild(labelEl);
|
||||||
|
infoEl.appendChild(shaEl);
|
||||||
|
infoEl.appendChild(dateAndAuthorEl);
|
||||||
|
cardEl.appendChild(infoEl);
|
||||||
|
return cardEl;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function draggableCommit(node: HTMLElement, initialOpts: DraggableConfig) {
|
||||||
|
function createClone(opts: DraggableConfig) {
|
||||||
|
return createCommitElement(opts.commitType, opts.label, opts.sha, opts.dateAndAuthor);
|
||||||
|
}
|
||||||
|
return setupDragHandlers(node, initialOpts, createClone, {
|
||||||
|
handlerWidth: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////
|
||||||
|
//// FILE DRAGGABLE ////
|
||||||
|
////////////////////////
|
||||||
|
|
||||||
|
export function createChipsElement(
|
||||||
|
childrenAmount: number,
|
||||||
|
label: string | undefined,
|
||||||
|
filePath: string | undefined
|
||||||
|
): HTMLDivElement {
|
||||||
|
const containerEl = createElement('div', ['draggable-chip-container']) as HTMLDivElement;
|
||||||
|
const chipEl = createElement('div', ['draggable-chip']);
|
||||||
|
containerEl.appendChild(chipEl);
|
||||||
|
|
||||||
|
if (filePath) {
|
||||||
|
const iconEl = createElement(
|
||||||
|
'img',
|
||||||
|
['draggable-chip-icon'],
|
||||||
|
undefined,
|
||||||
|
getVSIFileIcon(filePath)
|
||||||
|
);
|
||||||
|
chipEl.appendChild(iconEl);
|
||||||
|
}
|
||||||
|
|
||||||
|
const labelEl = createElement('span', ['text-base-12'], label);
|
||||||
|
chipEl.appendChild(labelEl);
|
||||||
|
|
||||||
|
if (childrenAmount > 1) {
|
||||||
|
const amountTag = createElement(
|
||||||
|
'div',
|
||||||
|
['text-base-11', 'text-bold', 'draggable-chip-amount'],
|
||||||
|
childrenAmount.toString()
|
||||||
|
);
|
||||||
|
chipEl.appendChild(amountTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (childrenAmount === 2) {
|
||||||
|
containerEl.classList.add('draggable-chip-two');
|
||||||
|
} else if (childrenAmount > 2) {
|
||||||
|
containerEl.classList.add('draggable-chip-multiple');
|
||||||
|
}
|
||||||
|
|
||||||
|
return containerEl;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function draggableChips(node: HTMLElement, initialOpts: DraggableConfig) {
|
||||||
|
function createClone(opts: DraggableConfig, selectedElements: HTMLElement[]) {
|
||||||
|
return createChipsElement(selectedElements.length, opts.label, opts.filePath);
|
||||||
|
}
|
||||||
|
return setupDragHandlers(node, initialOpts, createClone);
|
||||||
|
}
|
||||||
|
|
||||||
|
////////////////////////
|
||||||
|
//// HUNK DRAGGABLE ////
|
||||||
|
////////////////////////
|
||||||
|
|
||||||
|
export function cloneElement(node: HTMLElement) {
|
||||||
|
const cloneEl = node.cloneNode(true) as HTMLElement;
|
||||||
|
|
||||||
|
// exclude all ignored elements from the clone
|
||||||
|
const ignoredElements = Array.from(cloneEl.querySelectorAll('[data-remove-from-draggable]'));
|
||||||
|
ignoredElements.forEach((el) => el.remove());
|
||||||
|
|
||||||
|
return cloneEl;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function draggableElement(node: HTMLElement, initialOpts: DraggableConfig) {
|
||||||
|
function createClone() {
|
||||||
|
return cloneElement(node);
|
||||||
|
}
|
||||||
|
return setupDragHandlers(node, initialOpts, createClone, {
|
||||||
|
handlerWidth: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
@ -1,48 +1,83 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Icon from '$lib/shared/Icon.svelte';
|
import { pxToRem } from '$lib/utils/pxToRem';
|
||||||
|
import { scale } from 'svelte/transition';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
hovered: boolean;
|
hovered: boolean;
|
||||||
activated: boolean;
|
activated: boolean;
|
||||||
label?: string;
|
label?: string;
|
||||||
|
extraPaddings?: {
|
||||||
|
top?: number;
|
||||||
|
right?: number;
|
||||||
|
bottom?: number;
|
||||||
|
left?: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { hovered, activated, label = 'Drop here' }: Props = $props();
|
const { hovered, activated, label = 'Drop here', extraPaddings }: Props = $props();
|
||||||
|
let defaultPadding = $derived.by(() => {
|
||||||
|
if (hovered) return 2;
|
||||||
|
return 4;
|
||||||
|
});
|
||||||
|
|
||||||
|
const extraPaddingTop = extraPaddings?.top ?? 0;
|
||||||
|
const extraPaddingRight = extraPaddings?.right ?? 0;
|
||||||
|
const extraPaddingBottom = extraPaddings?.bottom ?? 0;
|
||||||
|
const extraPaddingLeft = extraPaddings?.left ?? 0;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="dropzone-target container" class:activated class:hovered>
|
<div
|
||||||
<div class="dropzone-content">
|
transition:scale={{ duration: 200, start: 0.9 }}
|
||||||
<Icon name="new-file-small-filled" />
|
class="dropzone-target dropzone-wrapper"
|
||||||
<p class="text-base-13">{label}</p>
|
class:activated
|
||||||
|
class:hovered
|
||||||
|
style="--padding-top: {pxToRem(defaultPadding + extraPaddingTop)}; --padding-right: {pxToRem(
|
||||||
|
defaultPadding + extraPaddingRight
|
||||||
|
)}; --padding-bottom: {pxToRem(defaultPadding + extraPaddingBottom)}; --padding-left: {pxToRem(
|
||||||
|
defaultPadding + extraPaddingLeft
|
||||||
|
)}"
|
||||||
|
>
|
||||||
|
<div class="container">
|
||||||
|
<div class="dropzone-label">
|
||||||
|
<svg
|
||||||
|
class="dropzone-label-icon"
|
||||||
|
viewBox="0 0 12 12"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path d="M11 7L6 2M6 2L1 7M6 2L6 12" stroke="white" stroke-width="1.5" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<span class="text-base-12 text-semibold">{label}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- add svg rectange -->
|
||||||
|
<svg width="100%" height="100%" class="animated-rectangle">
|
||||||
|
<rect width="100%" height="100%" rx="5" ry="5" vector-effect="non-scaling-stroke" />
|
||||||
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
:root {
|
.dropzone-wrapper {
|
||||||
--dropzone-height: 16px;
|
z-index: var(--z-ground);
|
||||||
--dropzone-overlap: calc(var(--dropzone-height) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
padding-top: var(--padding-top);
|
||||||
|
padding-right: var(--padding-right);
|
||||||
|
padding-bottom: var(--padding-bottom);
|
||||||
|
padding-left: var(--padding-left);
|
||||||
|
|
||||||
display: flex;
|
display: none;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
background-color: oklch(from var(--clr-scale-pop-70) l c h / 0.1);
|
transition:
|
||||||
|
transform 0.1s,
|
||||||
outline-color: var(--clr-scale-pop-40);
|
padding 0.1s;
|
||||||
outline-style: dashed;
|
|
||||||
outline-width: 1px;
|
|
||||||
outline-offset: -10px;
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
|
|
||||||
z-index: var(--z-lifted);
|
|
||||||
|
|
||||||
transition: background-color 0.1s;
|
|
||||||
|
|
||||||
/* It is very important that all children are pointer-events: none */
|
/* It is very important that all children are pointer-events: none */
|
||||||
/* https://stackoverflow.com/questions/7110353/html5-dragleave-fired-when-hovering-a-child-element */
|
/* https://stackoverflow.com/questions/7110353/html5-dragleave-fired-when-hovering-a-child-element */
|
||||||
@ -50,19 +85,98 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.activated) {
|
&.activated {
|
||||||
display: none;
|
display: flex;
|
||||||
|
animation: dropzone-scale 0.1s forwards;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.hovered {
|
&.hovered {
|
||||||
background-color: oklch(from var(--clr-scale-pop-20) l c h / 0.1);
|
transform: scale(1.01);
|
||||||
|
|
||||||
|
.animated-rectangle rect {
|
||||||
|
fill: oklch(from var(--clr-scale-pop-50) l c h / 0.14);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone-label {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.dropzone-content {
|
.container {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone-label {
|
||||||
|
opacity: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
color: var(--clr-scale-pop-40);
|
padding: 6px 10px;
|
||||||
|
border-radius: 100px;
|
||||||
|
color: var(--clr-theme-pop-on-element);
|
||||||
|
background-color: var(--clr-theme-pop-element);
|
||||||
|
transform: translateY(3px) scale(0.95);
|
||||||
|
|
||||||
|
transition:
|
||||||
|
opacity 0.1s,
|
||||||
|
transform 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropzone-label-icon {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
animation: icon-shifting 1s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes icon-shifting {
|
||||||
|
0% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animated-rectangle {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
& rect {
|
||||||
|
fill: oklch(from var(--clr-scale-pop-50) l c h / 0.1);
|
||||||
|
stroke: var(--clr-scale-pop-50);
|
||||||
|
|
||||||
|
stroke-width: 2px;
|
||||||
|
stroke-dasharray: 2;
|
||||||
|
stroke-dashoffset: 30;
|
||||||
|
transform-origin: center;
|
||||||
|
|
||||||
|
transition:
|
||||||
|
fill var(--transition-fast),
|
||||||
|
transform var(--transition-fast);
|
||||||
|
|
||||||
|
animation: dash 4s linear infinite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dash {
|
||||||
|
from {
|
||||||
|
stroke-dashoffset: 30;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
stroke-dashoffset: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -13,21 +13,21 @@
|
|||||||
<div
|
<div
|
||||||
class="dropzone-target container"
|
class="dropzone-target container"
|
||||||
class:activated
|
class:activated
|
||||||
style="--y-offset: {pxToRem(yOffsetPx) || 0}"
|
class:hovered
|
||||||
|
style:--y-offset={pxToRem(yOffsetPx)}
|
||||||
>
|
>
|
||||||
<div class="indicator" class:hovered></div>
|
<div class="indicator"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="postcss">
|
<style lang="postcss">
|
||||||
:root {
|
|
||||||
--dropzone-height: 16px;
|
|
||||||
--dropzone-overlap: calc(var(--dropzone-height) / 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
|
--dropzone-overlap: calc(var(--dropzone-height) / 2);
|
||||||
|
--dropzone-height: 16px;
|
||||||
|
|
||||||
height: var(--dropzone-height);
|
height: var(--dropzone-height);
|
||||||
margin-top: calc(var(--dropzone-overlap) * -1);
|
margin-top: calc(var(--dropzone-overlap) * -1);
|
||||||
margin-bottom: calc(var(--dropzone-overlap) * -1);
|
margin-bottom: calc(var(--dropzone-overlap) * -1);
|
||||||
|
/* background-color: rgba(0, 0, 0, 0.1); */
|
||||||
width: 100%;
|
width: 100%;
|
||||||
position: relative;
|
position: relative;
|
||||||
top: var(--y-offset);
|
top: var(--y-offset);
|
||||||
@ -35,7 +35,7 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
z-index: 101;
|
z-index: var(--z-floating);
|
||||||
|
|
||||||
/* It is very important that all children are pointer-events: none */
|
/* It is very important that all children are pointer-events: none */
|
||||||
/* https://stackoverflow.com/questions/7110353/html5-dragleave-fired-when-hovering-a-child-element */
|
/* https://stackoverflow.com/questions/7110353/html5-dragleave-fired-when-hovering-a-child-element */
|
||||||
@ -46,17 +46,19 @@
|
|||||||
&:not(.activated) {
|
&:not(.activated) {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.hovered {
|
||||||
|
& .indicator {
|
||||||
|
background-color: var(--clr-theme-pop-element);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.indicator {
|
.indicator {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 3px;
|
height: 2px;
|
||||||
|
margin-top: 1px;
|
||||||
transition: opacity 0.1s;
|
transition: opacity 0.1s;
|
||||||
background-color: var(--clr-border-2);
|
background-color: transparent;
|
||||||
opacity: 0;
|
|
||||||
|
|
||||||
&.hovered {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -2,6 +2,7 @@
|
|||||||
import FileStatusTag from './FileStatusTag.svelte';
|
import FileStatusTag from './FileStatusTag.svelte';
|
||||||
import { getVSIFileIcon } from '$lib/ext-icons';
|
import { getVSIFileIcon } from '$lib/ext-icons';
|
||||||
import Button from '$lib/shared/Button.svelte';
|
import Button from '$lib/shared/Button.svelte';
|
||||||
|
import { splitFilePath } from '$lib/utils/filePath';
|
||||||
import { computeFileStatus } from '$lib/utils/fileStatus';
|
import { computeFileStatus } from '$lib/utils/fileStatus';
|
||||||
import { computeAddedRemovedByFiles } from '$lib/utils/metrics';
|
import { computeAddedRemovedByFiles } from '$lib/utils/metrics';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
@ -14,22 +15,12 @@
|
|||||||
$: fileStats = computeAddedRemovedByFiles(file);
|
$: fileStats = computeAddedRemovedByFiles(file);
|
||||||
$: fileStatus = computeFileStatus(file);
|
$: fileStatus = computeFileStatus(file);
|
||||||
|
|
||||||
function boldenFilename(filepath: string): { filename: string; path: string } {
|
$: fileTitle = splitFilePath(file.path);
|
||||||
const parts = filepath.split('/');
|
|
||||||
if (parts.length === 0) return { filename: '', path: '' };
|
|
||||||
|
|
||||||
const filename = parts[parts.length - 1];
|
|
||||||
const path = parts.slice(0, -1).join('/');
|
|
||||||
|
|
||||||
return { filename, path };
|
|
||||||
}
|
|
||||||
|
|
||||||
$: fileTitle = boldenFilename(file.path);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="header__inner">
|
<div class="header__inner">
|
||||||
<img src={getVSIFileIcon(file.path)} alt="" width="13" height="13" class="icon" />
|
<img src={getVSIFileIcon(file.path)} alt="" class="icon" />
|
||||||
<div class="header__info truncate">
|
<div class="header__info truncate">
|
||||||
<div class="header__filetitle text-base-13 truncate">
|
<div class="header__filetitle text-base-13 truncate">
|
||||||
<span class="header__filename">{fileTitle.filename}</span>
|
<span class="header__filename">{fileTitle.filename}</span>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import FileContextMenu from './FileContextMenu.svelte';
|
import FileContextMenu from './FileContextMenu.svelte';
|
||||||
import FileStatusIcons from './FileStatusIcons.svelte';
|
import FileStatusIcons from './FileStatusIcons.svelte';
|
||||||
import { draggable } from '$lib/dragging/draggable';
|
import { draggableChips } from '$lib/dragging/draggable';
|
||||||
import { DraggableFile } from '$lib/dragging/draggables';
|
import { DraggableFile } from '$lib/dragging/draggables';
|
||||||
import { getVSIFileIcon } from '$lib/ext-icons';
|
import { getVSIFileIcon } from '$lib/ext-icons';
|
||||||
import Checkbox from '$lib/shared/Checkbox.svelte';
|
import Checkbox from '$lib/shared/Checkbox.svelte';
|
||||||
@ -116,7 +116,9 @@
|
|||||||
popupMenu.openByMouse(e, { files: [file] });
|
popupMenu.openByMouse(e, { files: [file] });
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
use:draggable={{
|
use:draggableChips={{
|
||||||
|
label: `${file.filename}`,
|
||||||
|
filePath: file.path,
|
||||||
data: $selectedFiles.then(
|
data: $selectedFiles.then(
|
||||||
(files) => new DraggableFile($branch?.id || '', file, $commit, files)
|
(files) => new DraggableFile($branch?.id || '', file, $commit, files)
|
||||||
),
|
),
|
||||||
@ -227,6 +229,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.info {
|
.info {
|
||||||
|
pointer-events: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
import { getVSIFileIcon } from '$lib/ext-icons';
|
import { getVSIFileIcon } from '$lib/ext-icons';
|
||||||
import { createdOnDay } from '$lib/history/history';
|
import { createdOnDay } from '$lib/history/history';
|
||||||
import Button from '$lib/shared/Button.svelte';
|
import Button from '$lib/shared/Button.svelte';
|
||||||
|
import { splitFilePath } from '$lib/utils/filePath';
|
||||||
import { toHumanReadableTime } from '$lib/utils/time';
|
import { toHumanReadableTime } from '$lib/utils/time';
|
||||||
import { createEventDispatcher } from 'svelte';
|
import { createEventDispatcher } from 'svelte';
|
||||||
import type { Snapshot, SnapshotDetails } from '$lib/history/types';
|
import type { Snapshot, SnapshotDetails } from '$lib/history/types';
|
||||||
@ -152,10 +153,6 @@
|
|||||||
const error = entry.details?.trailers.find((t) => t.key === 'error')?.value;
|
const error = entry.details?.trailers.find((t) => t.key === 'error')?.value;
|
||||||
|
|
||||||
const operation = mapOperation(entry.details);
|
const operation = mapOperation(entry.details);
|
||||||
|
|
||||||
function getPathOnly(path: string) {
|
|
||||||
return path.split('/').slice(0, -1).join('/');
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
@ -225,10 +222,10 @@
|
|||||||
/>
|
/>
|
||||||
<div class="text-base-12 files-attacment__file-path-and-name">
|
<div class="text-base-12 files-attacment__file-path-and-name">
|
||||||
<span class="files-attacment__file-name">
|
<span class="files-attacment__file-name">
|
||||||
{filePath.split('/').pop()}
|
{splitFilePath(filePath).filename}
|
||||||
</span>
|
</span>
|
||||||
<span class="files-attacment__file-path">
|
<span class="files-attacment__file-path">
|
||||||
{getPathOnly(filePath)}
|
{splitFilePath(filePath).path}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Project } from '$lib/backend/projects';
|
import { Project } from '$lib/backend/projects';
|
||||||
import { draggable } from '$lib/dragging/draggable';
|
import { draggableElement } from '$lib/dragging/draggable';
|
||||||
import { DraggableHunk } from '$lib/dragging/draggables';
|
import { DraggableHunk } from '$lib/dragging/draggables';
|
||||||
import HunkContextMenu from '$lib/hunk/HunkContextMenu.svelte';
|
import HunkContextMenu from '$lib/hunk/HunkContextMenu.svelte';
|
||||||
import HunkLines from '$lib/hunk/HunkLines.svelte';
|
import HunkLines from '$lib/hunk/HunkLines.svelte';
|
||||||
@ -63,7 +63,7 @@
|
|||||||
bind:this={viewport}
|
bind:this={viewport}
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
role="cell"
|
role="cell"
|
||||||
use:draggable={{
|
use:draggableElement={{
|
||||||
data: new DraggableHunk($branch?.id || '', section.hunk),
|
data: new DraggableHunk($branch?.id || '', section.hunk),
|
||||||
disabled: draggingDisabled
|
disabled: draggingDisabled
|
||||||
}}
|
}}
|
||||||
|
@ -97,6 +97,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
data-remove-from-draggable
|
||||||
on:mousedown={onMouseDown}
|
on:mousedown={onMouseDown}
|
||||||
on:click|stopPropagation
|
on:click|stopPropagation
|
||||||
on:dblclick|stopPropagation
|
on:dblclick|stopPropagation
|
||||||
|
@ -57,7 +57,13 @@
|
|||||||
{padding}
|
{padding}
|
||||||
{shift}
|
{shift}
|
||||||
{thickness}
|
{thickness}
|
||||||
on:dragging={(e) => dispatch('dragging', e.detail)}
|
on:dragging={(data) => dispatch('dragging', data.detail)}
|
||||||
|
on:scroll={(data) => {
|
||||||
|
const event = data.detail;
|
||||||
|
const target = event.target as HTMLDivElement;
|
||||||
|
|
||||||
|
scrolled = target.scrollTop > 0;
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -75,7 +81,9 @@
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
.contents {
|
.contents {
|
||||||
display: block;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100%;
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
}
|
}
|
||||||
.scrolled {
|
.scrolled {
|
||||||
|
@ -61,6 +61,7 @@
|
|||||||
|
|
||||||
const dispatch = createEventDispatcher<{
|
const dispatch = createEventDispatcher<{
|
||||||
dragging: boolean;
|
dragging: boolean;
|
||||||
|
scroll: Event;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
/////////////////////
|
/////////////////////
|
||||||
@ -190,9 +191,11 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function onScroll() {
|
function onScroll(e: Event) {
|
||||||
if (!isScrollable) return;
|
if (!isScrollable) return;
|
||||||
|
|
||||||
|
dispatch('scroll', e);
|
||||||
|
|
||||||
clearTimer();
|
clearTimer();
|
||||||
setupTimer();
|
setupTimer();
|
||||||
|
|
||||||
@ -276,6 +279,7 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={track}
|
bind:this={track}
|
||||||
|
data-remove-from-draggable
|
||||||
class="scrollbar-track"
|
class="scrollbar-track"
|
||||||
class:horz
|
class:horz
|
||||||
class:vert
|
class:vert
|
||||||
|
9
app/src/lib/utils/filePath.ts
Normal file
9
app/src/lib/utils/filePath.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
export function splitFilePath(filepath: string): { filename: string; path: string } {
|
||||||
|
const parts = filepath.split('/');
|
||||||
|
if (parts.length === 0) return { filename: '', path: '' };
|
||||||
|
|
||||||
|
const filename = parts[parts.length - 1];
|
||||||
|
const path = parts.slice(0, -1).join('/');
|
||||||
|
|
||||||
|
return { filename, path };
|
||||||
|
}
|
162
app/src/styles/draggable.css
Normal file
162
app/src/styles/draggable.css
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
.draggable-chip-container {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable-chip {
|
||||||
|
z-index: 3;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
background-color: var(--clr-bg-1);
|
||||||
|
border-radius: var(--radius-m);
|
||||||
|
border: 1px solid var(--clr-border-2);
|
||||||
|
padding: 8px;
|
||||||
|
min-width: 50px;
|
||||||
|
max-width: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable-chip-icon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable-chip-amount {
|
||||||
|
position: absolute;
|
||||||
|
top: -6px;
|
||||||
|
right: -8px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background-color: var(--clr-scale-ntrl-20);
|
||||||
|
color: var(--clr-scale-ntrl-100);
|
||||||
|
padding: 2px 4px;
|
||||||
|
min-width: 16px;
|
||||||
|
border-radius: 16px;
|
||||||
|
margin-left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* if dragging more then one item */
|
||||||
|
.draggable-chip-two {
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
z-index: 2;
|
||||||
|
top: 6px;
|
||||||
|
left: 6px;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 1px solid var(--clr-border-2);
|
||||||
|
background-color: var(--clr-bg-1);
|
||||||
|
border-radius: var(--radius-m);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable-chip-multiple {
|
||||||
|
&::after,
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border: 1px solid var(--clr-border-2);
|
||||||
|
background-color: var(--clr-bg-1);
|
||||||
|
border-radius: var(--radius-m);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
z-index: 2;
|
||||||
|
top: 6px;
|
||||||
|
left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
z-index: 1;
|
||||||
|
top: 12px;
|
||||||
|
left: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* COMMIT */
|
||||||
|
.draggable-commit {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
background-color: var(--clr-bg-1);
|
||||||
|
border-radius: var(--radius-m);
|
||||||
|
border: 1px solid var(--clr-border-2);
|
||||||
|
padding: 12px 12px 12px 16px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable-commit-indicator {
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 4px;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable-commit-localAndRemote {
|
||||||
|
&::before {
|
||||||
|
background-color: var(--clr-commit-remote);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable-commit-local {
|
||||||
|
color: var(--clr-text-1);
|
||||||
|
&::before {
|
||||||
|
background-color: var(--clr-commit-local);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable-commit-integrated {
|
||||||
|
color: var(--clr-text-1);
|
||||||
|
&::before {
|
||||||
|
background-color: var(--clr-commit-shadow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable-commit-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
/* gap: 8px; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.draggable-commit-info-text {
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
color: var(--clr-text-2);
|
||||||
|
|
||||||
|
&:not(:last-child):after {
|
||||||
|
content: '•';
|
||||||
|
margin: 0 5px;
|
||||||
|
color: var(--clr-text-3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-handle {
|
||||||
|
opacity: 0.5;
|
||||||
|
|
||||||
|
/* disable children events in order to prevent false dragleave events */
|
||||||
|
& > * {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dropzone-scale {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.96);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,7 @@
|
|||||||
@import './text-input.css';
|
@import './text-input.css';
|
||||||
@import './commit-lines.css';
|
@import './commit-lines.css';
|
||||||
@import './markdown.css';
|
@import './markdown.css';
|
||||||
|
@import './draggable.css';
|
||||||
|
|
||||||
/* CSS VARIABLES */
|
/* CSS VARIABLES */
|
||||||
:root {
|
:root {
|
||||||
|
Loading…
Reference in New Issue
Block a user