diff --git a/app/src/lib/branch/BranchCard.svelte b/app/src/lib/branch/BranchCard.svelte
index 6e29d1b41..086a2e08d 100644
--- a/app/src/lib/branch/BranchCard.svelte
+++ b/app/src/lib/branch/BranchCard.svelte
@@ -139,9 +139,9 @@
@@ -254,9 +258,6 @@
.card {
flex: 1;
- /* overflow: hidden; */
- /* border: 1px solid var(--clr-border-2);
- border-radius: var(--radius-m); */
}
.branch-card__files {
@@ -264,9 +265,6 @@
flex-direction: column;
flex: 1;
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 {
@@ -288,12 +286,6 @@
cursor: default; /* was defaulting to text cursor */
}
- .branch-card :global(.contents) {
- display: flex;
- flex-direction: column;
- min-height: 100%;
- }
-
/* COLLAPSED LANE */
.collapsed-lane-container {
display: flex;
diff --git a/app/src/lib/branch/BranchFooter.svelte b/app/src/lib/branch/BranchFooter.svelte
index effe66837..0c72dcc9e 100644
--- a/app/src/lib/branch/BranchFooter.svelte
+++ b/app/src/lib/branch/BranchFooter.svelte
@@ -54,7 +54,7 @@
options: {
root: null,
rootMargin: '-1px',
- threshold: 1
+ threshold: 0
}
}}
>
@@ -131,6 +131,5 @@
.not-in-viewport {
border-radius: 0;
- /* background-color: aquamarine; */
}
diff --git a/app/src/lib/branch/BranchLane.svelte b/app/src/lib/branch/BranchLane.svelte
index d4dcac9b3..426fe4d04 100644
--- a/app/src/lib/branch/BranchLane.svelte
+++ b/app/src/lib/branch/BranchLane.svelte
@@ -77,7 +77,7 @@
{#await $selectedFile then selected}
{#if selected}
diff --git a/app/src/lib/commit/CommitCard.svelte b/app/src/lib/commit/CommitCard.svelte
index 130b71f50..e6c02209a 100644
--- a/app/src/lib/commit/CommitCard.svelte
+++ b/app/src/lib/commit/CommitCard.svelte
@@ -3,7 +3,7 @@
import { Project } from '$lib/backend/projects';
import CommitMessageInput from '$lib/commit/CommitMessageInput.svelte';
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 BranchFilesList from '$lib/file/BranchFilesList.svelte';
import Button from '$lib/shared/Button.svelte';
@@ -25,7 +25,7 @@
BaseBranch,
type CommitStatus
} from '$lib/vbranches/types';
- import { createEventDispatcher, type Snippet } from 'svelte';
+ import { type Snippet } from 'svelte';
export let branch: Branch | undefined = undefined;
export let commit: Commit | RemoteCommit;
@@ -46,8 +46,7 @@
const currentCommitMessage = persistedCommitMessage(project.id, branch?.id || '');
- const dispatch = createEventDispatcher<{ toggle: void }>();
-
+ let draggableCommitElement: HTMLElement | null = null;
let files: RemoteFile[] = [];
let showDetails = false;
@@ -57,7 +56,6 @@
function toggleFiles() {
showDetails = !showDetails;
- dispatch('toggle');
if (showDetails) loadFiles();
}
@@ -102,6 +100,14 @@
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;
$: {
@@ -109,6 +115,9 @@
if (first) topHeightPx = 58;
if (showDetails && !first) topHeightPx += 12;
}
+
+ let dragDirection: 'up' | 'down' | undefined;
+ let isDragTargeted = false;
@@ -138,40 +147,77 @@
class:is-last={last}
class:has-lines={lines}
>
+ {#if dragDirection && isDragTargeted}
+
+ {/if}
+
{#if lines}
{@render lines(topHeightPx)}
{/if}
-
-
diff --git a/app/src/lib/commit/CommitDialog.svelte b/app/src/lib/commit/CommitDialog.svelte
index 1cd1607d5..7addfa831 100644
--- a/app/src/lib/commit/CommitDialog.svelte
+++ b/app/src/lib/commit/CommitDialog.svelte
@@ -38,6 +38,7 @@
$commitMessage = '';
} finally {
isCommitting = false;
+ $expanded = false;
}
}
diff --git a/app/src/lib/commit/CommitDragItem.svelte b/app/src/lib/commit/CommitDragItem.svelte
index caf3fa786..6f5f230b3 100644
--- a/app/src/lib/commit/CommitDragItem.svelte
+++ b/app/src/lib/commit/CommitDragItem.svelte
@@ -38,7 +38,14 @@
{@render squashDropzone()}
{#snippet overlay({ hovered, activated })}
-
+
{/snippet}
{/snippet}
@@ -48,7 +55,14 @@
{@render children()}
{#snippet overlay({ hovered, activated })}
-
+
{/snippet}
{/snippet}
@@ -57,6 +71,5 @@
.dropzone-wrapper {
position: relative;
width: 100%;
- overflow: hidden;
}
diff --git a/app/src/lib/commit/CommitList.svelte b/app/src/lib/commit/CommitList.svelte
index 9a78a0696..1f67252c1 100644
--- a/app/src/lib/commit/CommitList.svelte
+++ b/app/src/lib/commit/CommitList.svelte
@@ -149,8 +149,8 @@
{@render reorderDropzone(
reorderDropzoneManager.dropzoneBelowCommit(commit.id),
getReorderDropzoneOffset({
- isLast: $localAndRemoteCommits.length === 0 && idx + 1 === $localCommits.length,
- isMiddle: $localAndRemoteCommits.length > 0 && idx + 1 === $localCommits.length
+ isLast: idx + 1 === $localCommits.length,
+ isMiddle: idx + 1 === $localCommits.length
})
)}
@@ -181,7 +181,8 @@
{@render reorderDropzone(
reorderDropzoneManager.dropzoneBelowCommit(commit.id),
getReorderDropzoneOffset({
- isLast: idx + 1 === $localAndRemoteCommits.length
+ isMiddle: idx + 1 === $localAndRemoteCommits.length
+ // isLast: idx + 1 === $localAndRemoteCommits.length
})
)}
.commits {
+ position: relative;
display: flex;
flex-direction: column;
background-color: var(--clr-bg-2);
diff --git a/app/src/lib/components/Board.svelte b/app/src/lib/components/Board.svelte
index 536954441..4d76a592b 100644
--- a/app/src/lib/components/Board.svelte
+++ b/app/src/lib/components/Board.svelte
@@ -4,7 +4,7 @@
import { Project } from '$lib/backend/projects';
import BranchDropzone from '$lib/branch/BranchDropzone.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 Icon from '$lib/shared/Icon.svelte';
import { getContext, getContextStore } from '$lib/utils/context';
@@ -94,12 +94,12 @@
e.stopPropagation();
return;
}
- clone = cloneWithRotation(e.target);
+ clone = cloneElement(e.target as HTMLElement);
document.body.appendChild(clone);
// 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?.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;
priorPosition = Array.from(dropZone.children).indexOf(dragged);
}}
diff --git a/app/src/lib/dragging/draggable.ts b/app/src/lib/dragging/draggable.ts
index 3c3d1af77..8eb47fabc 100644
--- a/app/src/lib/dragging/draggable.ts
+++ b/app/src/lib/dragging/draggable.ts
@@ -1,82 +1,47 @@
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';
export interface DraggableConfig {
readonly selector?: string;
readonly disabled?: boolean;
+ readonly label?: string;
+ readonly filePath?: string;
+ readonly sha?: string;
+ readonly dateAndAuthor?: string;
+ readonly commitType?: CommitStatus;
readonly data?: Draggable | Promise;
readonly viewportId?: string;
- readonly extendWithClass?: string;
}
-export function applyContainerStyle(element: HTMLElement) {
- element.style.position = 'absolute';
- element.style.top = '-9999px'; // Element has to be in the DOM so we move it out of sight
- element.style.display = 'inline-block';
- element.style.padding = '30px'; // To prevent clipping of rotated element
+function createElement(
+ tag: string,
+ classNames: string[],
+ textContent?: string,
+ 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(
- children: Element[],
- extendWithClass: string | undefined
-): HTMLDivElement {
- const inner = document.createElement('div');
- inner.style.display = 'flex';
- inner.style.flexDirection = 'column';
- inner.style.gap = '0.125rem';
-
- 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;
+function setupDragHandlers(
+ node: HTMLElement,
+ opts: DraggableConfig,
+ createClone: (opts: DraggableConfig, selectedElements: HTMLElement[]) => HTMLElement,
+ params: {
+ handlerWidth: boolean;
+ maxHeight?: number;
+ } = {
+ handlerWidth: false
+ }
+) {
let dragHandle: HTMLElement | null;
- let clone: HTMLElement | undefined;
-
+ let clone: HTMLElement;
let selectedElements: HTMLElement[] = [];
function handleMouseDown(e: MouseEvent) {
@@ -84,84 +49,75 @@ export function draggable(node: HTMLElement, initialOpts: DraggableConfig) {
}
function handleDragStart(e: DragEvent) {
- let elt: HTMLElement | null = dragHandle;
+ e.stopPropagation();
- while (elt) {
- if (elt.dataset.noDrag !== undefined) {
- e.stopPropagation();
- e.preventDefault();
- return false;
- }
- elt = elt.parentElement;
+ if (dragHandle && dragHandle.dataset.noDrag !== undefined) {
+ e.preventDefault();
+ return false;
}
- // If the draggable specifies a selector then we check if we're dragging selected elements
if (opts.selector) {
- // Checking for selected siblings in the parent of the parent container likely works
- // for most use-cases but it was done here primarily for dragging multiple files.
- const parentNode = node.parentNode?.parentNode;
- selectedElements = parentNode
- ? Array.from(parentNode.querySelectorAll(opts.selector).values() as Iterable)
- : [];
-
- if (selectedElements.length > 0) {
- clone = createContainerForMultiDrag(selectedElements, opts.extendWithClass);
- // Dim the original element while dragging
- selectedElements.forEach((element) => {
- element.style.opacity = '0.5';
- });
+ const parentNode = node.parentElement?.parentElement;
+ if (!parentNode) {
+ console.error('draggable parent node not found');
+ return;
}
+ selectedElements = Array.from(
+ parentNode.querySelectorAll(opts.selector) as NodeListOf
+ );
}
- if (!clone) {
- clone = cloneWithRotation(node, opts.extendWithClass);
+ if (selectedElements.length === 0) {
+ 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);
Array.from(dropzoneRegistry.values()).forEach((dropzone) => {
dropzone.register(opts.data);
});
- // 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', 'placeholder copy'); // cannot be empty string
- e.dataTransfer?.setDragImage(clone, e.offsetX + 30, e.offsetY + 30); // Adds the padding
- e.stopPropagation();
+ if (e.dataTransfer) {
+ if (params.handlerWidth) {
+ e.dataTransfer.setDragImage(clone, e.offsetX, e.offsetY);
+ } else {
+ 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) {
- if (clone) {
- clone.remove();
- clone = undefined;
- }
-
- // reset the opacity of the selected elements
- selectedElements.forEach((element) => {
- element.style.opacity = '1';
- });
-
+ e.stopPropagation();
+ if (clone) clone.remove();
+ selectedElements.forEach((el) => el.classList.remove('drag-handle'));
Array.from(dropzoneRegistry.values()).forEach((dropzone) => {
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) {
+ e.preventDefault();
+ const viewport = opts.viewportId ? document.getElementById(opts.viewportId) : null;
if (!viewport) return;
- if (new Date().getTime() - lastDrag < 500) return;
- lastDrag = new Date().getTime();
-
+ const triggerRange = 150;
+ const scrollSpeed = (viewport.clientWidth || 500) / 2;
const viewportWidth = viewport.clientWidth;
const relativeX = e.clientX - viewport.getBoundingClientRect().left;
-
- // Scroll horizontally if the draggable is near the edge of the viewport
if (relativeX < triggerRange) {
viewport.scrollBy(-scrollSpeed, 0);
} else if (relativeX > viewportWidth - triggerRange) {
@@ -189,12 +145,123 @@ export function draggable(node: HTMLElement, initialOpts: DraggableConfig) {
setup(opts);
return {
- update(opts: DraggableConfig) {
+ update(newOpts: DraggableConfig) {
clean();
- setup(opts);
+ setup(newOpts);
},
destroy() {
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
+ });
+}
diff --git a/app/src/lib/dropzone/CardOverlay.svelte b/app/src/lib/dropzone/CardOverlay.svelte
index 76ae9e393..35e13dd87 100644
--- a/app/src/lib/dropzone/CardOverlay.svelte
+++ b/app/src/lib/dropzone/CardOverlay.svelte
@@ -1,48 +1,83 @@
-
-
-
-
{label}
+
diff --git a/app/src/lib/dropzone/LineOverlay.svelte b/app/src/lib/dropzone/LineOverlay.svelte
index 205d43050..d337b10a3 100644
--- a/app/src/lib/dropzone/LineOverlay.svelte
+++ b/app/src/lib/dropzone/LineOverlay.svelte
@@ -13,21 +13,21 @@
diff --git a/app/src/lib/file/FileCardHeader.svelte b/app/src/lib/file/FileCardHeader.svelte
index f62a2e28f..ce22a1ee7 100644
--- a/app/src/lib/file/FileCardHeader.svelte
+++ b/app/src/lib/file/FileCardHeader.svelte
@@ -2,6 +2,7 @@
import FileStatusTag from './FileStatusTag.svelte';
import { getVSIFileIcon } from '$lib/ext-icons';
import Button from '$lib/shared/Button.svelte';
+ import { splitFilePath } from '$lib/utils/filePath';
import { computeFileStatus } from '$lib/utils/fileStatus';
import { computeAddedRemovedByFiles } from '$lib/utils/metrics';
import { createEventDispatcher } from 'svelte';
@@ -14,22 +15,12 @@
$: fileStats = computeAddedRemovedByFiles(file);
$: fileStatus = computeFileStatus(file);
- function boldenFilename(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 };
- }
-
- $: fileTitle = boldenFilename(file.path);
+ $: fileTitle = splitFilePath(file.path);