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:
Pavel Laptev 2024-07-02 20:49:17 +02:00 committed by GitHub
parent a1c591ffb6
commit 9ff735fd4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
22 changed files with 699 additions and 276 deletions

View File

@ -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;

View File

@ -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>

View File

@ -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`}

View File

@ -58,6 +58,5 @@
position: relative; position: relative;
flex-grow: 1; flex-grow: 1;
width: 100%; width: 100%;
/* overflow: hidden; */
} }
</style> </style>

View File

@ -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>

View File

@ -38,6 +38,7 @@
$commitMessage = ''; $commitMessage = '';
} finally { } finally {
isCommitting = false; isCommitting = false;
$expanded = false;
} }
} }
</script> </script>

View File

@ -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>

View File

@ -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);

View File

@ -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);
}} }}

View File

@ -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
});
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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>

View File

@ -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
}} }}

View File

@ -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

View File

@ -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 {

View File

@ -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

View 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 };
}

View 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);
}
}

View File

@ -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 {