Skip to content

Commit a89f18f

Browse files
committed
Drag: Add overlay, mod keys, fixes (phase 3)
- Canvas-rendered native drag image with file names, emoji icons, middle-truncation, fading edges, and count badge - In-app DOM overlay with cached macOS icons, action line("Copy to Documents"), and "can't drop here" feedback - Alt/Option modifier tracking: hold Alt to switch copy → move - Self-drag fingerprint for re-entry detection (fixes same-pane drop bug when cursor leaves window and re-enters) - Overlay file info uses icon cache for self-drags, extension-based lookup for external drags
1 parent 6207d8e commit a89f18f

13 files changed

Lines changed: 1145 additions & 53 deletions

apps/desktop/coverage-allowlist.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@
88
"app-status-store.ts": { "reason": "Depends on Tauri APIs" },
99
"benchmark.ts": { "reason": "Dev tooling, not critical path" },
1010
"file-explorer/drag-drop.ts": { "reason": "Needs integration testing with Tauri" },
11+
"file-explorer/drag-image-renderer.ts": { "reason": "Canvas API dependent, pure logic tested separately" },
12+
"file-explorer/DragOverlay.svelte": { "reason": "UI overlay component, state tested in drag-overlay tests" },
13+
"file-explorer/drag-overlay.svelte.ts": {
14+
"reason": "Reactive Svelte state, pure logic tested in drag-overlay tests"
15+
},
16+
"file-explorer/modifier-key-tracker.svelte.ts": {
17+
"reason": "Document event listeners, reactive Svelte state"
18+
},
1119
"file-explorer/views/BriefList.svelte": {
1220
"reason": "Logic tested in brief-list-utils.ts, component mounting heavy"
1321
},
Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
<script lang="ts">
2+
import {
3+
getOverlayVisible,
4+
getOverlayX,
5+
getOverlayY,
6+
getOverlayFileInfos,
7+
getOverlayTotalCount,
8+
getOverlayTargetName,
9+
getOverlayOperation,
10+
getOverlayCanDrop,
11+
buildOverlayNameLines,
12+
formatActionLine,
13+
} from './drag-overlay.svelte'
14+
15+
const cursorOffset = 16
16+
17+
const visible = $derived(getOverlayVisible())
18+
const x = $derived(getOverlayX())
19+
const y = $derived(getOverlayY())
20+
const fileInfos = $derived(getOverlayFileInfos())
21+
const totalCount = $derived(getOverlayTotalCount())
22+
const targetName = $derived(getOverlayTargetName())
23+
const operation = $derived(getOverlayOperation())
24+
const canDrop = $derived(getOverlayCanDrop())
25+
26+
const nameLines = $derived(buildOverlayNameLines(fileInfos, totalCount))
27+
const actionLine = $derived(formatActionLine(operation, targetName, canDrop))
28+
</script>
29+
30+
{#if visible}
31+
<div
32+
class="drag-overlay"
33+
class:cannot-drop={!canDrop}
34+
style="left: {String(x + cursorOffset)}px; top: {String(y + cursorOffset)}px;"
35+
aria-hidden="true"
36+
>
37+
<div class="name-list">
38+
{#each nameLines as line, i (i)}
39+
<div class="name-line" class:is-summary={line.isSummary}>
40+
{#if !line.isSummary}
41+
{#if line.iconUrl}
42+
<img class="name-icon" src={line.iconUrl} alt="" width="12" height="12" />
43+
{:else}
44+
<span class="name-icon-emoji">{line.isDirectory ? '\uD83D\uDCC1' : '\uD83D\uDCC4'}</span>
45+
{/if}
46+
{/if}
47+
<span class="name-text">{line.text}</span>
48+
</div>
49+
{/each}
50+
</div>
51+
<div class="action-line" class:is-warning={!canDrop}>
52+
{actionLine}
53+
</div>
54+
</div>
55+
{/if}
56+
57+
<style>
58+
.drag-overlay {
59+
position: fixed;
60+
z-index: 10000;
61+
pointer-events: none;
62+
max-width: 320px;
63+
padding: 10px 14px;
64+
border-radius: 8px;
65+
background: rgba(30, 30, 30, 0.9);
66+
color: rgba(255, 255, 255, 0.92);
67+
font-family: var(--font-system), sans-serif;
68+
font-size: var(--font-size-xs);
69+
line-height: 1.5;
70+
backdrop-filter: blur(8px);
71+
/* Fade edges via CSS mask-image */
72+
mask-image: linear-gradient(to bottom, transparent 0%, black 8px, black calc(100% - 8px), transparent 100%);
73+
/* Fade transition */
74+
opacity: 1;
75+
transition: opacity 0.15s ease-out;
76+
}
77+
78+
.drag-overlay.cannot-drop {
79+
opacity: 0.5;
80+
}
81+
82+
.name-list {
83+
max-height: 340px;
84+
overflow: hidden;
85+
}
86+
87+
.name-line {
88+
white-space: nowrap;
89+
overflow: hidden;
90+
text-overflow: ellipsis;
91+
padding: 1px 0;
92+
display: flex;
93+
align-items: center;
94+
gap: 4px;
95+
}
96+
97+
.name-icon {
98+
width: 12px;
99+
height: 12px;
100+
object-fit: contain;
101+
flex-shrink: 0;
102+
}
103+
104+
.name-icon-emoji {
105+
font-size: 10px;
106+
width: 12px;
107+
text-align: center;
108+
flex-shrink: 0;
109+
}
110+
111+
.name-text {
112+
overflow: hidden;
113+
text-overflow: ellipsis;
114+
}
115+
116+
.is-summary {
117+
color: rgba(255, 255, 255, 0.55);
118+
font-style: italic;
119+
}
120+
121+
.action-line {
122+
margin-top: 6px;
123+
padding-top: 6px;
124+
border-top: 1px solid rgba(255, 255, 255, 0.15);
125+
font-weight: 500;
126+
color: var(--color-accent);
127+
}
128+
129+
.action-line.is-warning {
130+
color: rgba(255, 255, 255, 0.4);
131+
}
132+
</style>

0 commit comments

Comments
 (0)