mirror of
https://seed.radicle.dev/rad:z4V1sjrXqjvFdnCUbxPFqd5p4DtH5.git
synced 2026-11-01 01:30:46 +08:00
When we land on a repo via notification inbox the repository wouldn't be visible if the section was collapsed. Now we expand it and scroll into view so we see which repository we're browsing.
702 lines
18 KiB
Svelte
702 lines
18 KiB
Svelte
<script lang="ts" module>
|
|
import { array, boolean, string } from "zod";
|
|
|
|
import useLocalStorage from "@app/lib/useLocalStorage.svelte";
|
|
|
|
let filterOpen = $state(false);
|
|
let filterQuery = $state("");
|
|
|
|
const reposExpanded = useLocalStorage(
|
|
"sidebarReposExpanded",
|
|
boolean(),
|
|
true,
|
|
!window.localStorage,
|
|
);
|
|
|
|
const pinnedRepoIds = useLocalStorage(
|
|
"sidebarPinnedRepos",
|
|
array(string()),
|
|
[],
|
|
!window.localStorage,
|
|
);
|
|
|
|
export function revealRepoInSidebar(rid: string) {
|
|
if (pinnedRepoIds.value.includes(rid)) return;
|
|
|
|
filterQuery = "";
|
|
filterOpen = false;
|
|
reposExpanded.value = true;
|
|
|
|
requestAnimationFrame(() => {
|
|
const row = document.querySelector(
|
|
`[data-unpinned-rid="${CSS.escape(rid)}"]`,
|
|
);
|
|
row?.scrollIntoView({ block: "nearest" });
|
|
});
|
|
}
|
|
</script>
|
|
|
|
<script lang="ts">
|
|
import type { RepoInfo } from "@bindings/repo/RepoInfo";
|
|
import type { RepoSummary } from "@bindings/repo/RepoSummary";
|
|
|
|
import { onMount } from "svelte";
|
|
import { flip } from "svelte/animate";
|
|
import { crossfade } from "svelte/transition";
|
|
|
|
import { nodeRunning } from "@app/lib/events";
|
|
import { dynamicInterval, resetDynamicInterval } from "@app/lib/interval";
|
|
import { cachedRepoCommitCount, invoke } from "@app/lib/invoke";
|
|
import * as router from "@app/lib/router";
|
|
import { formatRepositoryId } from "@app/lib/utils";
|
|
|
|
import AddRepoButton from "@app/components/AddRepoButton.svelte";
|
|
import Clipboard from "@app/components/Clipboard.svelte";
|
|
import Icon from "@app/components/Icon.svelte";
|
|
import RepoAvatar from "@app/components/RepoAvatar.svelte";
|
|
import ScrollArea from "@app/components/ScrollArea.svelte";
|
|
import usePinnedDragReorder, {
|
|
DRAG_RID_ATTRIBUTE,
|
|
PINNED_LIST_CLASS,
|
|
} from "@app/components/usePinnedDragReorder.svelte";
|
|
|
|
interface Props {
|
|
initialRepos: RepoSummary[];
|
|
initialSeededNotReplicated: string[];
|
|
activeRepo?: RepoInfo;
|
|
}
|
|
|
|
const {
|
|
initialRepos,
|
|
initialSeededNotReplicated,
|
|
activeRepo = undefined,
|
|
}: Props = $props();
|
|
|
|
let repos: RepoSummary[] = $derived(initialRepos);
|
|
let seededNotReplicated: string[] = $derived(initialSeededNotReplicated);
|
|
let activeCommitCount = $state<number | undefined>(undefined);
|
|
let filterInputElement: HTMLInputElement | undefined = $state(undefined);
|
|
|
|
$effect(() => {
|
|
const rid = activeRepo?.rid;
|
|
const head = activeRepo?.payloads["xyz.radicle.project"]?.meta.head;
|
|
|
|
activeCommitCount = undefined;
|
|
|
|
if (!rid || !head) return;
|
|
|
|
void cachedRepoCommitCount(rid, head)
|
|
.then(count => {
|
|
if (activeRepo?.rid === rid) {
|
|
activeCommitCount = count;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error("Failed to load commit count", error);
|
|
});
|
|
});
|
|
|
|
$effect(() => {
|
|
if (filterOpen && filterInputElement) {
|
|
filterInputElement.focus({ preventScroll: true });
|
|
}
|
|
});
|
|
|
|
$effect(() => {
|
|
if (seededNotReplicated.length > 0) {
|
|
dynamicInterval("seededNotReplicated", reloadRepos, 5_000);
|
|
} else {
|
|
resetDynamicInterval("seededNotReplicated");
|
|
}
|
|
});
|
|
|
|
onMount(() => {
|
|
return () => resetDynamicInterval("seededNotReplicated");
|
|
});
|
|
|
|
const filteredRepos = $derived(
|
|
filterQuery.trim()
|
|
? repos.filter(r =>
|
|
r.name.toLowerCase().includes(filterQuery.trim().toLowerCase()),
|
|
)
|
|
: repos,
|
|
);
|
|
|
|
const fetchingExpanded = useLocalStorage(
|
|
"sidebarFetchingExpanded",
|
|
boolean(),
|
|
true,
|
|
!window.localStorage,
|
|
);
|
|
|
|
const pinnedRepos = $derived.by(() => {
|
|
const byRid = new Map(repos.map(r => [r.rid, r]));
|
|
return pinnedRepoIds.value
|
|
.map(rid => byRid.get(rid))
|
|
.filter((r): r is RepoSummary => r !== undefined);
|
|
});
|
|
|
|
const unpinnedFilteredRepos = $derived(
|
|
filteredRepos.filter(r => !pinnedRepoIds.value.includes(r.rid)),
|
|
);
|
|
|
|
const unpinnedReposCount = $derived(
|
|
repos.filter(r => !pinnedRepoIds.value.includes(r.rid)).length,
|
|
);
|
|
|
|
const ANIMATION_DURATION_MS = 220;
|
|
let animatingPinnedList = $state(false);
|
|
let animationTimeout: ReturnType<typeof setTimeout> | undefined;
|
|
const animationDuration = $derived(
|
|
animatingPinnedList ? ANIMATION_DURATION_MS : 0,
|
|
);
|
|
const [send, receive] = crossfade({
|
|
duration: ANIMATION_DURATION_MS,
|
|
fallback: () => ({ duration: 0 }),
|
|
});
|
|
|
|
function withPinAnimation(fn: () => void) {
|
|
animatingPinnedList = true;
|
|
if (animationTimeout !== undefined) clearTimeout(animationTimeout);
|
|
animationTimeout = setTimeout(() => {
|
|
animatingPinnedList = false;
|
|
}, ANIMATION_DURATION_MS);
|
|
fn();
|
|
}
|
|
|
|
function togglePin(rid: string) {
|
|
withPinAnimation(() => {
|
|
if (pinnedRepoIds.value.includes(rid)) {
|
|
pinnedRepoIds.value = pinnedRepoIds.value.filter(r => r !== rid);
|
|
} else {
|
|
pinnedRepoIds.value = [rid, ...pinnedRepoIds.value];
|
|
}
|
|
});
|
|
}
|
|
|
|
const drag = usePinnedDragReorder({
|
|
pinnedRepos: () => pinnedRepos,
|
|
getOrder: () => pinnedRepoIds.value,
|
|
setOrder: rids => withPinAnimation(() => (pinnedRepoIds.value = rids)),
|
|
});
|
|
|
|
async function reloadRepos() {
|
|
[repos, seededNotReplicated] = await Promise.all([
|
|
invoke<RepoSummary[]>("list_repos_summary"),
|
|
invoke<string[]>("seeded_not_replicated"),
|
|
]);
|
|
}
|
|
|
|
async function unseed(rid: string) {
|
|
try {
|
|
await invoke<null>("unseed", { rid });
|
|
await reloadRepos();
|
|
} catch (error) {
|
|
console.error("Unseed failed", error);
|
|
}
|
|
}
|
|
|
|
const activeRoute = router.activeRouteStore;
|
|
|
|
function activeRid(): string | undefined {
|
|
return activeRepo?.rid;
|
|
}
|
|
|
|
function isRepoHome(rid: string): boolean {
|
|
return $activeRoute.resource === "repo.home" && activeRid() === rid;
|
|
}
|
|
|
|
function isIssues(rid: string): boolean {
|
|
return (
|
|
($activeRoute.resource === "repo.issues" ||
|
|
$activeRoute.resource === "repo.issue") &&
|
|
activeRid() === rid
|
|
);
|
|
}
|
|
|
|
function isCommits(rid: string): boolean {
|
|
return (
|
|
($activeRoute.resource === "repo.commits" ||
|
|
$activeRoute.resource === "repo.commit") &&
|
|
activeRid() === rid
|
|
);
|
|
}
|
|
|
|
function isPatches(rid: string): boolean {
|
|
return (
|
|
($activeRoute.resource === "repo.patches" ||
|
|
$activeRoute.resource === "repo.patch") &&
|
|
activeRid() === rid
|
|
);
|
|
}
|
|
</script>
|
|
|
|
<style>
|
|
.repos-list {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
padding: 0.5rem 0;
|
|
}
|
|
.pinned-list.empty {
|
|
height: 0;
|
|
padding: 0;
|
|
overflow: visible;
|
|
}
|
|
|
|
.section-header {
|
|
font: var(--txt-body-m-regular);
|
|
font-variant-ligatures: none;
|
|
color: var(--color-text-secondary);
|
|
padding: 0.5rem 0 0.25rem 0.5rem;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
justify-content: space-between;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
}
|
|
.section-header-label {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
.section-header-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.filter-button {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
background: none;
|
|
border: 0;
|
|
padding: 0.125rem;
|
|
margin-left: -0.125rem;
|
|
border-radius: var(--border-radius-sm);
|
|
color: var(--color-text-secondary);
|
|
cursor: pointer;
|
|
}
|
|
.filter-button:hover {
|
|
color: var(--color-text-primary);
|
|
background-color: var(--color-surface-subtle);
|
|
}
|
|
.filter-input {
|
|
background: none;
|
|
border: 0;
|
|
outline: none;
|
|
font: var(--txt-body-m-regular);
|
|
color: var(--color-text-primary);
|
|
flex: 1;
|
|
min-width: 0;
|
|
}
|
|
.filter-input::placeholder {
|
|
color: var(--color-text-secondary);
|
|
}
|
|
|
|
.nav-item {
|
|
position: relative;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.375rem 0.5rem;
|
|
border-radius: var(--border-radius-sm);
|
|
font: var(--txt-body-m-regular);
|
|
color: var(--color-text-primary);
|
|
cursor: pointer;
|
|
width: 100%;
|
|
text-decoration: none;
|
|
user-select: none;
|
|
-webkit-user-select: none;
|
|
}
|
|
.nav-item :global(img),
|
|
.nav-item :global(svg) {
|
|
-webkit-user-drag: none;
|
|
}
|
|
.nav-item:hover {
|
|
background-color: var(--color-surface-subtle);
|
|
}
|
|
.nav-item.active {
|
|
background-color: var(--color-surface-subtle);
|
|
}
|
|
.nav-item .global-counter-badge {
|
|
margin-left: auto;
|
|
}
|
|
.sub-item {
|
|
padding-left: 2rem;
|
|
}
|
|
|
|
.pending-item {
|
|
color: var(--color-text-secondary);
|
|
cursor: default;
|
|
}
|
|
.pending-avatar {
|
|
width: 1rem;
|
|
height: 1rem;
|
|
flex-shrink: 0;
|
|
border: 1px solid var(--color-border-subtle);
|
|
}
|
|
.pending-item .remove-icon {
|
|
display: none;
|
|
margin-left: auto;
|
|
color: var(--color-text-tertiary);
|
|
border-radius: var(--border-radius-sm);
|
|
}
|
|
.pending-item:hover .remove-icon {
|
|
display: flex;
|
|
}
|
|
.pending-item .remove-icon:hover {
|
|
background-color: var(--color-surface-mid);
|
|
}
|
|
|
|
.nav-item .row-actions {
|
|
visibility: hidden;
|
|
margin-left: auto;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.125rem;
|
|
color: var(--color-text-tertiary);
|
|
}
|
|
.nav-item:hover .row-actions,
|
|
.nav-item .row-actions:has(:focus-visible) {
|
|
visibility: visible;
|
|
}
|
|
.nav-item .row-actions :global(.clipboard),
|
|
.nav-item .row-actions .pin-button,
|
|
.nav-item .row-actions .drag-handle {
|
|
width: 1.5rem;
|
|
height: 1.5rem;
|
|
border-radius: var(--border-radius-sm);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.nav-item .row-actions :global(.clipboard):hover,
|
|
.nav-item .row-actions .pin-button:hover {
|
|
background-color: var(--color-surface-mid);
|
|
}
|
|
.pin-button {
|
|
background: none;
|
|
border: 0;
|
|
color: inherit;
|
|
cursor: pointer;
|
|
}
|
|
.drag-handle {
|
|
cursor: grab;
|
|
}
|
|
|
|
.repo-row-group {
|
|
position: relative;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 0.25rem;
|
|
}
|
|
.repo-row-group.drop-before::before,
|
|
.repo-row-group.drop-after::after {
|
|
content: "";
|
|
position: absolute;
|
|
left: 0;
|
|
right: 0;
|
|
height: 2px;
|
|
background-color: var(--color-border-mid);
|
|
border-radius: 1px;
|
|
pointer-events: none;
|
|
}
|
|
.repo-row-group.drop-before::before {
|
|
top: -3px;
|
|
}
|
|
.repo-row-group.drop-after::after {
|
|
bottom: -3px;
|
|
}
|
|
.nav-item.dragging {
|
|
opacity: 0.35;
|
|
background-color: var(--color-surface-subtle);
|
|
}
|
|
:global(body.dragging-pinned-repo),
|
|
:global(body.dragging-pinned-repo *) {
|
|
cursor: grabbing !important;
|
|
}
|
|
.drag-ghost {
|
|
position: fixed;
|
|
pointer-events: none;
|
|
z-index: 9999;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 0.5rem;
|
|
padding: 0.375rem 0.5rem;
|
|
background-color: var(--color-surface-strong);
|
|
border: 1px solid var(--color-border-mid);
|
|
border-radius: var(--border-radius-sm);
|
|
font: var(--txt-body-m-regular);
|
|
color: var(--color-text-primary);
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
|
max-width: 14rem;
|
|
}
|
|
|
|
.icon {
|
|
color: var(--color-text-tertiary);
|
|
}
|
|
</style>
|
|
|
|
{#if seededNotReplicated.length > 0}
|
|
<div
|
|
class="section-header"
|
|
onclick={() => (fetchingExpanded.value = !fetchingExpanded.value)}
|
|
role="button"
|
|
tabindex="0"
|
|
onkeydown={e => {
|
|
if (e.key === "Enter" || e.key === " ") {
|
|
fetchingExpanded.value = !fetchingExpanded.value;
|
|
}
|
|
}}>
|
|
<span class="section-header-label">
|
|
<span class="icon"><Icon name="hourglass" /></span>
|
|
Fetching {seededNotReplicated.length > 1
|
|
? ` (${seededNotReplicated.length})`
|
|
: ""}
|
|
<span class="icon">
|
|
<Icon name={fetchingExpanded.value ? "chevron-down" : "chevron-up"} />
|
|
</span>
|
|
</span>
|
|
</div>
|
|
|
|
{#if fetchingExpanded.value}
|
|
<div style:display="flex" style:flex-direction="column" style:gap="0.25rem">
|
|
{#each seededNotReplicated as rid (rid)}
|
|
<div
|
|
class="nav-item pending-item"
|
|
title="{$nodeRunning ? 'Fetching' : 'Queued'} {rid}">
|
|
<span class="pending-avatar"></span>
|
|
<span class="txt-overflow">{formatRepositoryId(rid)}</span>
|
|
<button
|
|
class="remove-icon filter-button"
|
|
title="Remove"
|
|
onclick={() => unseed(rid)}>
|
|
<span class="icon"><Icon name="trash" /></span>
|
|
</button>
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
|
|
<div
|
|
class="repos-list {PINNED_LIST_CLASS}"
|
|
class:empty={pinnedRepos.length === 0}>
|
|
{#each pinnedRepos as repo (repo.rid)}
|
|
<div
|
|
class="repo-row-group"
|
|
class:drop-before={drag.dropTargetRid === repo.rid &&
|
|
drag.dropPosition === "before" &&
|
|
drag.draggingRid !== repo.rid}
|
|
class:drop-after={drag.dropTargetRid === repo.rid &&
|
|
drag.dropPosition === "after" &&
|
|
drag.draggingRid !== repo.rid}
|
|
{...{ [DRAG_RID_ATTRIBUTE]: repo.rid }}
|
|
animate:flip={{ duration: animationDuration }}
|
|
in:receive={{ key: repo.rid, duration: animationDuration }}
|
|
out:send={{ key: repo.rid, duration: animationDuration }}>
|
|
{@render repoRowInner(repo, true)}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
|
|
<div
|
|
class="section-header"
|
|
onclick={() => {
|
|
if (!filterOpen) {
|
|
reposExpanded.value = !reposExpanded.value;
|
|
}
|
|
}}
|
|
role="button"
|
|
tabindex="0"
|
|
onkeydown={e => {
|
|
if (e.key === "Enter" || e.key === " ") {
|
|
if (!filterOpen) {
|
|
reposExpanded.value = !reposExpanded.value;
|
|
}
|
|
}
|
|
}}>
|
|
{#if filterOpen}
|
|
<span
|
|
class="section-header-label"
|
|
onclick={e => e.stopPropagation()}
|
|
role="none">
|
|
<button
|
|
class="filter-button"
|
|
title="Clear filter"
|
|
onclick={() => {
|
|
filterOpen = false;
|
|
filterQuery = "";
|
|
}}>
|
|
<span class="icon"><Icon name="search" /></span>
|
|
</button>
|
|
<input
|
|
bind:this={filterInputElement}
|
|
class="filter-input"
|
|
placeholder="Filter repos…"
|
|
bind:value={filterQuery}
|
|
onkeydown={e => {
|
|
if (e.key === "Escape") {
|
|
filterOpen = false;
|
|
filterQuery = "";
|
|
} else if (e.key === "Enter" && unpinnedFilteredRepos.length > 0) {
|
|
void router.push({
|
|
resource: "repo.home",
|
|
rid: unpinnedFilteredRepos[0].rid,
|
|
});
|
|
filterQuery = "";
|
|
}
|
|
}} />
|
|
</span>
|
|
{:else}
|
|
<span class="section-header-label">
|
|
<span onclick={e => e.stopPropagation()} role="none">
|
|
<button
|
|
class="filter-button"
|
|
title="Filter repos"
|
|
aria-keyshortcuts="ctrl+f"
|
|
onclick={() => {
|
|
filterOpen = true;
|
|
reposExpanded.value = true;
|
|
}}>
|
|
<span class="icon"><Icon name="filter" /></span>
|
|
</button>
|
|
</span>
|
|
All Repos{unpinnedReposCount > 1 ? ` (${unpinnedReposCount})` : ""}
|
|
<span class="icon">
|
|
<Icon name={reposExpanded.value ? "chevron-down" : "chevron-up"} />
|
|
</span>
|
|
</span>
|
|
{/if}
|
|
<span class="section-header-actions">
|
|
<span onclick={e => e.stopPropagation()} role="none">
|
|
<AddRepoButton
|
|
onOpen={() => (reposExpanded.value = true)}
|
|
reload={reloadRepos}
|
|
{repos}
|
|
{seededNotReplicated} />
|
|
</span>
|
|
</span>
|
|
</div>
|
|
|
|
{#snippet repoRowInner(repo: RepoSummary, pinned: boolean = false)}
|
|
{@const pinState = pinnedRepoIds.value.includes(repo.rid)}
|
|
<a
|
|
class="nav-item"
|
|
class:active={isRepoHome(repo.rid)}
|
|
class:dragging={pinned && drag.draggingRid === repo.rid}
|
|
draggable="false"
|
|
onmousedown={pinned ? e => drag.onMouseDown(e, repo.rid) : undefined}
|
|
onclick={pinned ? drag.onClick : undefined}
|
|
href={router.routeToPath({ resource: "repo.home", rid: repo.rid })}>
|
|
<RepoAvatar name={repo.name} rid={repo.rid} styleWidth="1rem" />
|
|
<span class="txt-overflow">{repo.name}</span>
|
|
<span
|
|
class="row-actions"
|
|
role="none"
|
|
onclick={e => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
}}>
|
|
<button
|
|
class="pin-button"
|
|
title={pinState ? "Unpin repository" : "Pin repository"}
|
|
onclick={() => togglePin(repo.rid)}>
|
|
<Icon name={pinState ? "pin-filled" : "pin-hollow"} />
|
|
</button>
|
|
<span title="Copy RID">
|
|
<Clipboard text={repo.rid} noPopover />
|
|
</span>
|
|
{#if pinned}
|
|
<span class="drag-handle" title="Drag to reorder">
|
|
<Icon name="drag-handle" />
|
|
</span>
|
|
{/if}
|
|
</span>
|
|
</a>
|
|
{#if activeRid() === repo.rid}
|
|
{@const activeProject = activeRepo?.payloads["xyz.radicle.project"]}
|
|
{@render subItem(
|
|
router.routeToPath({ resource: "repo.commits", rid: repo.rid }),
|
|
"branch",
|
|
"Commits",
|
|
isCommits(repo.rid),
|
|
activeCommitCount,
|
|
)}
|
|
{@render subItem(
|
|
router.routeToPath({
|
|
resource: "repo.issues",
|
|
rid: repo.rid,
|
|
status: "open",
|
|
}),
|
|
"issue",
|
|
"Issues",
|
|
isIssues(repo.rid),
|
|
activeProject?.meta.issues.open || undefined,
|
|
)}
|
|
{@render subItem(
|
|
router.routeToPath({
|
|
resource: "repo.patches",
|
|
rid: repo.rid,
|
|
status: "open",
|
|
}),
|
|
"patch",
|
|
"Patches",
|
|
isPatches(repo.rid),
|
|
activeProject?.meta.patches.open || undefined,
|
|
)}
|
|
{/if}
|
|
{/snippet}
|
|
|
|
{#snippet subItem(
|
|
href: string,
|
|
icon: "branch" | "issue" | "patch",
|
|
label: string,
|
|
active: boolean,
|
|
count: number | undefined,
|
|
)}
|
|
<a class="nav-item sub-item" class:active {href}>
|
|
<span class="icon"><Icon name={icon} /></span>
|
|
{label}
|
|
{#if count !== undefined}
|
|
<span class="global-counter-badge">{count}</span>
|
|
{/if}
|
|
</a>
|
|
{/snippet}
|
|
|
|
{#if reposExpanded.value}
|
|
<ScrollArea
|
|
style="flex: 1; min-height: 0; mask-image: linear-gradient(to bottom, transparent 0, black 0.5rem, black calc(100% - 0.5rem), transparent 100%);">
|
|
<div class="repos-list">
|
|
{#each unpinnedFilteredRepos as repo (repo.rid)}
|
|
<div
|
|
class="repo-row-group"
|
|
data-unpinned-rid={repo.rid}
|
|
animate:flip={{ duration: animationDuration }}
|
|
in:receive={{ key: repo.rid, duration: animationDuration }}
|
|
out:send={{ key: repo.rid, duration: animationDuration }}>
|
|
{@render repoRowInner(repo, false)}
|
|
</div>
|
|
{/each}
|
|
</div>
|
|
</ScrollArea>
|
|
{/if}
|
|
|
|
{#if drag.draggedRepo}
|
|
<div
|
|
class="drag-ghost"
|
|
style:left="{drag.ghostX + 12}px"
|
|
style:top="{drag.ghostY + 8}px">
|
|
<RepoAvatar
|
|
name={drag.draggedRepo.name}
|
|
rid={drag.draggedRepo.rid}
|
|
styleWidth="1rem" />
|
|
<span class="txt-overflow">{drag.draggedRepo.name}</span>
|
|
</div>
|
|
{/if}
|