@design.estate/dees-catalog 4.10.0 → 4.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist_bundle/bundle.js +4545 -2343
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.d.ts +36 -5
- package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.demo.js +32 -3
- package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.js +663 -142
- package/package.json +4 -4
- package/readme.md +1 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.demo.ts +44 -4
- package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.ts +728 -163
package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.ts
CHANGED
|
@@ -22,6 +22,8 @@ import { harnessFormatRelativeTime } from '../harness.helpers.js';
|
|
|
22
22
|
import '../../00group-utility/dees-icon/dees-icon.js';
|
|
23
23
|
|
|
24
24
|
const { repeat } = directives;
|
|
25
|
+
const dragAutoScrollMaxPixelsPerSecond = 900;
|
|
26
|
+
const dragThresholdPixels = 4;
|
|
25
27
|
|
|
26
28
|
declare global {
|
|
27
29
|
interface HTMLElementTagNameMap {
|
|
@@ -34,6 +36,25 @@ interface IDropSlot {
|
|
|
34
36
|
index: number;
|
|
35
37
|
}
|
|
36
38
|
|
|
39
|
+
interface IPointerDragGesture {
|
|
40
|
+
pointerId: number;
|
|
41
|
+
handle: HTMLElement;
|
|
42
|
+
source: HTMLElement;
|
|
43
|
+
sessionId: string;
|
|
44
|
+
sourceGroupId: string | null;
|
|
45
|
+
sourceLogicalIndex: number;
|
|
46
|
+
startClientX: number;
|
|
47
|
+
startClientY: number;
|
|
48
|
+
grabOffsetX: number;
|
|
49
|
+
grabOffsetY: number;
|
|
50
|
+
phase: 'pending' | 'active';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface IKeyboardMoveFocus {
|
|
54
|
+
sessionId: string;
|
|
55
|
+
groupId: string | null;
|
|
56
|
+
}
|
|
57
|
+
|
|
37
58
|
type TSectionRenderEntry =
|
|
38
59
|
| { type: 'session'; key: string; session: IHarnessSessionMeta; logicalIndex: number }
|
|
39
60
|
| { type: 'placeholder'; key: string };
|
|
@@ -98,9 +119,27 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
98
119
|
@state()
|
|
99
120
|
accessor collapsedGroupIds: ReadonlySet<string> = new Set<string>();
|
|
100
121
|
|
|
122
|
+
@state()
|
|
123
|
+
private accessor topScrollShadowVisible = false;
|
|
124
|
+
|
|
125
|
+
@state()
|
|
126
|
+
private accessor bottomScrollShadowVisible = false;
|
|
127
|
+
|
|
128
|
+
@state()
|
|
129
|
+
private accessor moveAnnouncement = '';
|
|
130
|
+
|
|
101
131
|
private reflowGeneration = 0;
|
|
102
132
|
private readonly reflowAnimations = new Set<Animation>();
|
|
103
|
-
private
|
|
133
|
+
private pointerDragGesture: IPointerDragGesture | undefined;
|
|
134
|
+
private keyboardMoveFocus: IKeyboardMoveFocus | undefined;
|
|
135
|
+
private scrollResizeObserver: ResizeObserver | undefined;
|
|
136
|
+
private observedScrollContainer: HTMLElement | undefined;
|
|
137
|
+
private observedScrollContent: HTMLElement | undefined;
|
|
138
|
+
private dragAutoScrollFrame: number | undefined;
|
|
139
|
+
private dragAutoScrollClientX = 0;
|
|
140
|
+
private dragAutoScrollClientY = 0;
|
|
141
|
+
private dragAutoScrollVelocity = 0;
|
|
142
|
+
private dragAutoScrollLastFrame = 0;
|
|
104
143
|
|
|
105
144
|
public static styles = [
|
|
106
145
|
themeDefaultStyles,
|
|
@@ -161,12 +200,54 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
161
200
|
}
|
|
162
201
|
|
|
163
202
|
.list {
|
|
164
|
-
|
|
165
|
-
|
|
203
|
+
width: 100%;
|
|
204
|
+
height: 100%;
|
|
166
205
|
overflow-y: auto;
|
|
167
206
|
scrollbar-width: thin;
|
|
168
207
|
scrollbar-color: var(--dees-color-scrollbar-thumb) transparent;
|
|
169
208
|
padding: 0 var(--dees-spacing-md) var(--dees-spacing-md);
|
|
209
|
+
box-sizing: border-box;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.listFrame {
|
|
213
|
+
position: relative;
|
|
214
|
+
flex: 1;
|
|
215
|
+
min-height: 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.scrollShadow {
|
|
219
|
+
position: absolute;
|
|
220
|
+
z-index: 1;
|
|
221
|
+
left: 0;
|
|
222
|
+
right: 0;
|
|
223
|
+
height: 16px;
|
|
224
|
+
opacity: 0;
|
|
225
|
+
pointer-events: none;
|
|
226
|
+
transition: opacity var(--dees-transition-fast) var(--dees-ease-standard);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
.scrollShadow.visible {
|
|
230
|
+
opacity: 1;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
.scrollShadow.top {
|
|
234
|
+
top: 0;
|
|
235
|
+
background: linear-gradient(
|
|
236
|
+
to bottom,
|
|
237
|
+
color-mix(in srgb, var(--dees-material-thin-bg-opaque) 74%, transparent),
|
|
238
|
+
transparent
|
|
239
|
+
);
|
|
240
|
+
box-shadow: var(--dees-shadow-sm);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.scrollShadow.bottom {
|
|
244
|
+
bottom: 0;
|
|
245
|
+
background: linear-gradient(
|
|
246
|
+
to top,
|
|
247
|
+
color-mix(in srgb, var(--dees-material-thin-bg-opaque) 74%, transparent),
|
|
248
|
+
transparent
|
|
249
|
+
);
|
|
250
|
+
box-shadow: var(--dees-shadow-up-sm);
|
|
170
251
|
}
|
|
171
252
|
|
|
172
253
|
.section {
|
|
@@ -178,50 +259,99 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
178
259
|
|
|
179
260
|
.item {
|
|
180
261
|
--card-accent: var(--session-normal);
|
|
262
|
+
--card-inner-background: color-mix(in srgb, var(--dees-color-bg-primary) 90%, transparent);
|
|
263
|
+
--card-inner-border: transparent;
|
|
264
|
+
--card-lip-width: 3px;
|
|
181
265
|
position: relative;
|
|
182
266
|
width: 100%;
|
|
183
267
|
min-width: 0;
|
|
184
268
|
overflow: hidden;
|
|
185
|
-
border:
|
|
269
|
+
border: 0;
|
|
186
270
|
border-radius: var(--dees-radius-xl);
|
|
187
271
|
corner-shape: var(--dees-corner-shape);
|
|
188
|
-
background:
|
|
272
|
+
background: var(--card-accent);
|
|
189
273
|
color: var(--dees-color-text-primary);
|
|
190
|
-
box-shadow:
|
|
191
|
-
var(--dees-shadow-sm),
|
|
192
|
-
inset 3px 0 0 var(--card-accent);
|
|
274
|
+
box-shadow: var(--dees-shadow-sm);
|
|
193
275
|
box-sizing: border-box;
|
|
276
|
+
padding-left: var(--card-lip-width);
|
|
194
277
|
transition:
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
box-shadow var(--dees-transition-fast) var(--dees-ease-standard);
|
|
278
|
+
box-shadow var(--dees-transition-fast) var(--dees-ease-standard),
|
|
279
|
+
padding-left var(--dees-transition-fast) var(--dees-ease-standard);
|
|
198
280
|
}
|
|
199
281
|
|
|
200
282
|
.item:hover {
|
|
201
|
-
|
|
202
|
-
|
|
283
|
+
--card-inner-background: color-mix(
|
|
284
|
+
in srgb,
|
|
285
|
+
var(--dees-color-bg-primary) 90%,
|
|
286
|
+
var(--card-accent) 10%
|
|
287
|
+
);
|
|
288
|
+
box-shadow: var(--dees-shadow-md);
|
|
203
289
|
}
|
|
204
290
|
|
|
205
291
|
.item.selected {
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
var(--
|
|
210
|
-
|
|
292
|
+
--card-inner-background: color-mix(
|
|
293
|
+
in srgb,
|
|
294
|
+
var(--dees-color-bg-primary) 86%,
|
|
295
|
+
var(--card-accent) 14%
|
|
296
|
+
);
|
|
297
|
+
--card-inner-border: var(--card-accent);
|
|
298
|
+
--card-lip-width: 16px;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.item.selected:hover {
|
|
302
|
+
--card-inner-background: color-mix(
|
|
303
|
+
in srgb,
|
|
304
|
+
var(--dees-color-bg-primary) 82%,
|
|
305
|
+
var(--card-accent) 18%
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
.itemInner {
|
|
310
|
+
min-width: 0;
|
|
311
|
+
overflow: hidden;
|
|
312
|
+
border: 1px solid var(--card-inner-border);
|
|
313
|
+
border-radius: calc(var(--dees-radius-xl) - 1px);
|
|
314
|
+
corner-shape: var(--dees-corner-shape);
|
|
315
|
+
background: var(--card-inner-background);
|
|
316
|
+
box-sizing: border-box;
|
|
317
|
+
transition:
|
|
318
|
+
background var(--dees-transition-fast) var(--dees-ease-standard),
|
|
319
|
+
border-color var(--dees-transition-fast) var(--dees-ease-standard);
|
|
211
320
|
}
|
|
212
321
|
|
|
213
322
|
.item.dragging {
|
|
214
|
-
position:
|
|
215
|
-
inset
|
|
216
|
-
|
|
217
|
-
|
|
323
|
+
position: fixed;
|
|
324
|
+
inset: 0 auto auto 0;
|
|
325
|
+
width: var(--drag-preview-width);
|
|
326
|
+
height: var(--drag-preview-height);
|
|
327
|
+
max-width: none;
|
|
328
|
+
margin: 0;
|
|
329
|
+
padding: 0 0 0 var(--card-lip-width);
|
|
330
|
+
opacity: 0.96;
|
|
218
331
|
pointer-events: none;
|
|
332
|
+
transform: translate3d(var(--drag-preview-x), var(--drag-preview-y), 0);
|
|
333
|
+
box-shadow: var(--dees-shadow-xl);
|
|
219
334
|
}
|
|
220
335
|
|
|
221
336
|
.item.state-working {
|
|
222
337
|
--card-accent: var(--session-working);
|
|
223
338
|
}
|
|
224
339
|
|
|
340
|
+
@keyframes workingDotPulse {
|
|
341
|
+
0%, 100% {
|
|
342
|
+
transform: scale(0.92);
|
|
343
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--session-working) 14%, transparent);
|
|
344
|
+
}
|
|
345
|
+
50% {
|
|
346
|
+
transform: scale(1.08);
|
|
347
|
+
box-shadow: 0 0 0 6px color-mix(in srgb, var(--session-working) 3%, transparent);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
.item.state-working .itemHeader .stateDot {
|
|
352
|
+
animation: workingDotPulse 2.4s ease-in-out infinite;
|
|
353
|
+
}
|
|
354
|
+
|
|
225
355
|
.item.state-finished {
|
|
226
356
|
--card-accent: var(--session-finished);
|
|
227
357
|
}
|
|
@@ -236,29 +366,54 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
236
366
|
|
|
237
367
|
@keyframes attentionPulse {
|
|
238
368
|
0%, 100% {
|
|
239
|
-
|
|
240
|
-
0 0 0 0 color-mix(in srgb, var(--session-attention) 0%, transparent),
|
|
241
|
-
var(--dees-shadow-sm),
|
|
242
|
-
inset 3px 0 0 var(--session-attention);
|
|
369
|
+
background: color-mix(in srgb, var(--dees-color-bg-primary) 94%, var(--session-attention) 6%);
|
|
243
370
|
}
|
|
244
371
|
50% {
|
|
245
|
-
|
|
246
|
-
0 0 0 4px color-mix(in srgb, var(--session-attention) 26%, transparent),
|
|
247
|
-
0 7px 18px color-mix(in srgb, var(--session-attention) 16%, transparent),
|
|
248
|
-
inset 3px 0 0 var(--session-attention);
|
|
372
|
+
background: color-mix(in srgb, var(--dees-color-bg-primary) 84%, var(--session-attention) 16%);
|
|
249
373
|
}
|
|
250
374
|
}
|
|
251
375
|
|
|
252
|
-
.item.state-attention
|
|
376
|
+
.item.state-attention .itemInner {
|
|
253
377
|
animation: attentionPulse 1.15s ease-in-out infinite;
|
|
254
378
|
}
|
|
255
379
|
|
|
380
|
+
.item.state-attention:hover .itemInner {
|
|
381
|
+
animation: none;
|
|
382
|
+
background: color-mix(in srgb, var(--dees-color-bg-primary) 82%, var(--session-attention) 18%);
|
|
383
|
+
}
|
|
384
|
+
|
|
256
385
|
.itemHeader {
|
|
257
386
|
display: grid;
|
|
258
387
|
grid-template-columns: minmax(0, 1fr) 30px;
|
|
259
388
|
min-height: 46px;
|
|
260
389
|
}
|
|
261
390
|
|
|
391
|
+
.itemHeader.draggable {
|
|
392
|
+
grid-template-columns: 24px minmax(0, 1fr) 30px;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
.dragHandle {
|
|
396
|
+
appearance: none;
|
|
397
|
+
display: grid;
|
|
398
|
+
place-items: center;
|
|
399
|
+
padding: 0;
|
|
400
|
+
border: 0;
|
|
401
|
+
background: transparent;
|
|
402
|
+
color: var(--dees-color-text-muted);
|
|
403
|
+
cursor: grab;
|
|
404
|
+
touch-action: none;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.dragHandle:active {
|
|
408
|
+
cursor: grabbing;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.dragHandle:focus-visible {
|
|
412
|
+
outline: 2px solid var(--dees-color-accent-primary);
|
|
413
|
+
outline-offset: -3px;
|
|
414
|
+
border-radius: 8px;
|
|
415
|
+
}
|
|
416
|
+
|
|
262
417
|
.itemPrimary,
|
|
263
418
|
.expandToggle {
|
|
264
419
|
appearance: none;
|
|
@@ -497,7 +652,10 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
497
652
|
|
|
498
653
|
@media (prefers-reduced-motion: reduce) {
|
|
499
654
|
.item,
|
|
500
|
-
.
|
|
655
|
+
.itemInner,
|
|
656
|
+
.item.state-attention .itemInner,
|
|
657
|
+
.item.state-working .itemHeader .stateDot,
|
|
658
|
+
.scrollShadow,
|
|
501
659
|
.expandToggle dees-icon {
|
|
502
660
|
animation: none;
|
|
503
661
|
transition: none;
|
|
@@ -557,15 +715,21 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
557
715
|
|
|
558
716
|
public updated(changedProperties: Map<PropertyKey, unknown>): void {
|
|
559
717
|
super.updated(changedProperties);
|
|
560
|
-
|
|
561
|
-
|
|
718
|
+
this.observeScrollGeometry();
|
|
719
|
+
this.syncScrollShadowState();
|
|
720
|
+
this.restoreKeyboardMoveFocus();
|
|
721
|
+
if (
|
|
722
|
+
this.pointerDragGesture
|
|
723
|
+
&& (!this.pointerDragGesture.source.isConnected || !this.pointerDragGesture.handle.isConnected)
|
|
724
|
+
) {
|
|
725
|
+
this.finishPointerDrag(false);
|
|
562
726
|
}
|
|
563
727
|
if (!changedProperties.has('sessions')) return;
|
|
564
728
|
const previousSessions = changedProperties.get('sessions') as IHarnessSessionMeta[] | undefined;
|
|
565
729
|
const previousById = new Map((previousSessions ?? []).map((session) => [session.id, session]));
|
|
566
730
|
const knownIds = new Set(this.sessions.map((session) => session.id));
|
|
567
|
-
if (this.
|
|
568
|
-
this.
|
|
731
|
+
if (this.pointerDragGesture && !knownIds.has(this.pointerDragGesture.sessionId)) {
|
|
732
|
+
this.finishPointerDrag(false);
|
|
569
733
|
}
|
|
570
734
|
const nextExpanded = new Set(
|
|
571
735
|
[...this.expandedSessionIds].filter((sessionId) => knownIds.has(sessionId)),
|
|
@@ -584,8 +748,20 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
584
748
|
}
|
|
585
749
|
}
|
|
586
750
|
|
|
751
|
+
public async connectedCallback(): Promise<void> {
|
|
752
|
+
await super.connectedCallback();
|
|
753
|
+
await this.updateComplete;
|
|
754
|
+
if (!this.isConnected) return;
|
|
755
|
+
this.observeScrollGeometry();
|
|
756
|
+
this.syncScrollShadowState();
|
|
757
|
+
}
|
|
758
|
+
|
|
587
759
|
public async disconnectedCallback(): Promise<void> {
|
|
588
|
-
this.
|
|
760
|
+
this.finishPointerDrag(false);
|
|
761
|
+
this.scrollResizeObserver?.disconnect();
|
|
762
|
+
this.scrollResizeObserver = undefined;
|
|
763
|
+
this.observedScrollContainer = undefined;
|
|
764
|
+
this.observedScrollContent = undefined;
|
|
589
765
|
await super.disconnectedCallback();
|
|
590
766
|
}
|
|
591
767
|
|
|
@@ -602,13 +778,23 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
602
778
|
}}
|
|
603
779
|
/>
|
|
604
780
|
</div>
|
|
605
|
-
<div class="
|
|
606
|
-
${this.
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
781
|
+
<div class="listFrame">
|
|
782
|
+
<div class="scrollShadow top ${this.topScrollShadowVisible ? 'visible' : ''}"></div>
|
|
783
|
+
<div
|
|
784
|
+
class="list"
|
|
785
|
+
@scroll=${this.syncScrollShadowState}
|
|
786
|
+
>
|
|
787
|
+
<div class="listContent">
|
|
788
|
+
${this.loading
|
|
789
|
+
? html`<div class="emptyNote">Loading conversations…</div>`
|
|
790
|
+
: groupingActive
|
|
791
|
+
? this.renderGroupedList()
|
|
792
|
+
: this.renderFlatList()}
|
|
793
|
+
</div>
|
|
794
|
+
</div>
|
|
795
|
+
<div class="scrollShadow bottom ${this.bottomScrollShadowVisible ? 'visible' : ''}"></div>
|
|
611
796
|
</div>
|
|
797
|
+
<div class="visuallyHidden" aria-live="polite">${this.moveAnnouncement}</div>
|
|
612
798
|
${groupingActive
|
|
613
799
|
? html`
|
|
614
800
|
<div class="newGroupRow">
|
|
@@ -670,6 +856,9 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
670
856
|
return html`
|
|
671
857
|
<button
|
|
672
858
|
class="groupHeader ${collapsed ? 'collapsed' : ''} ${isHeaderDrop ? 'dropTarget' : ''}"
|
|
859
|
+
data-drop-group-id=${group.id}
|
|
860
|
+
data-drop-index=${this.sortableSessions(members).length}
|
|
861
|
+
data-drop-kind="slot"
|
|
673
862
|
type="button"
|
|
674
863
|
@click=${() => this.toggleGroupCollapsed(group.id)}
|
|
675
864
|
@contextmenu=${(event: MouseEvent) => {
|
|
@@ -684,16 +873,6 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
684
873
|
new CustomEvent('harness-group-context', { detail, bubbles: true, composed: true }),
|
|
685
874
|
);
|
|
686
875
|
}}
|
|
687
|
-
@dragover=${(event: DragEvent) => this.handleDragOverSlot(
|
|
688
|
-
event,
|
|
689
|
-
group.id,
|
|
690
|
-
this.sortableSessions(members).length,
|
|
691
|
-
)}
|
|
692
|
-
@drop=${(event: DragEvent) => this.handleDrop(
|
|
693
|
-
event,
|
|
694
|
-
group.id,
|
|
695
|
-
this.sortableSessions(members).length,
|
|
696
|
-
)}
|
|
697
876
|
>
|
|
698
877
|
<dees-icon class="chevron" icon="lucide:ChevronDown" iconSize="12"></dees-icon>
|
|
699
878
|
<span>${group.name}</span>
|
|
@@ -708,10 +887,11 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
708
887
|
? html`
|
|
709
888
|
<button
|
|
710
889
|
class="groupHeader"
|
|
890
|
+
data-drop-group-id=""
|
|
891
|
+
data-drop-index="0"
|
|
892
|
+
data-drop-kind="slot"
|
|
711
893
|
type="button"
|
|
712
894
|
tabindex="-1"
|
|
713
|
-
@dragover=${(event: DragEvent) => this.handleDragOverSlot(event, null, 0)}
|
|
714
|
-
@drop=${(event: DragEvent) => this.handleDrop(event, null, 0)}
|
|
715
895
|
>
|
|
716
896
|
<span>Ungrouped</span>
|
|
717
897
|
<span class="groupCount">${ungrouped.length}</span>
|
|
@@ -766,8 +946,9 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
766
946
|
? html`
|
|
767
947
|
<div
|
|
768
948
|
class="emptyDropZone"
|
|
769
|
-
|
|
770
|
-
|
|
949
|
+
data-drop-group-id=${groupId ?? ''}
|
|
950
|
+
data-drop-index="0"
|
|
951
|
+
data-drop-kind="slot"
|
|
771
952
|
></div>
|
|
772
953
|
`
|
|
773
954
|
: ''}
|
|
@@ -779,9 +960,10 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
779
960
|
return html`
|
|
780
961
|
<div
|
|
781
962
|
class="dropPlaceholder"
|
|
963
|
+
data-drop-group-id=${groupId ?? ''}
|
|
964
|
+
data-drop-index=${index}
|
|
965
|
+
data-drop-kind="slot"
|
|
782
966
|
style=${`height: ${Math.max(46, this.draggedItemHeight)}px`}
|
|
783
|
-
@dragover=${(event: DragEvent) => this.handleDragOverSlot(event, groupId, index)}
|
|
784
|
-
@drop=${(event: DragEvent) => this.handleDrop(event, groupId, index)}
|
|
785
967
|
></div>
|
|
786
968
|
`;
|
|
787
969
|
}
|
|
@@ -795,11 +977,11 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
795
977
|
this.enableGrouping
|
|
796
978
|
&& session.draggable !== false
|
|
797
979
|
&& index >= 0
|
|
798
|
-
&& !globalThis.matchMedia?.('(pointer: coarse)').matches
|
|
799
980
|
);
|
|
800
981
|
const expanded = this.expandedSessionIds.has(session.id);
|
|
801
982
|
const state = session.state ?? 'normal';
|
|
802
983
|
const detailsId = `session-details-${session.id}`;
|
|
984
|
+
const dropIndex = index < 0 ? 0 : index;
|
|
803
985
|
return html`
|
|
804
986
|
<article
|
|
805
987
|
class="item state-${state} ${expanded ? 'expanded' : ''} ${
|
|
@@ -808,9 +990,11 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
808
990
|
this.draggingSessionId === session.id ? 'dragging' : ''
|
|
809
991
|
}"
|
|
810
992
|
data-session-id=${session.id}
|
|
811
|
-
|
|
993
|
+
data-drop-group-id=${groupId ?? ''}
|
|
994
|
+
data-drop-index=${dropIndex}
|
|
995
|
+
data-drop-kind=${index < 0 ? 'slot' : 'card'}
|
|
812
996
|
@dblclick=${(event: MouseEvent) => {
|
|
813
|
-
if (this.eventTargetsExpandToggle(event)) return;
|
|
997
|
+
if (this.eventTargetsExpandToggle(event) || this.eventTargetsDragHandle(event)) return;
|
|
814
998
|
this.emit('harness-session-open', session);
|
|
815
999
|
}}
|
|
816
1000
|
@contextmenu=${(event: MouseEvent) => {
|
|
@@ -826,76 +1010,84 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
826
1010
|
new CustomEvent('harness-session-context', { detail, bubbles: true, composed: true }),
|
|
827
1011
|
);
|
|
828
1012
|
}}
|
|
829
|
-
@dragstart=${(event: DragEvent) => {
|
|
830
|
-
this.handleDragStart(event, session, groupId, index, draggable);
|
|
831
|
-
}}
|
|
832
|
-
@dragend=${() => this.clearDragState()}
|
|
833
|
-
@dragover=${(event: DragEvent) => {
|
|
834
|
-
if (!this.draggingSessionId || index < 0 || session.id === this.draggingSessionId) return;
|
|
835
|
-
event.preventDefault();
|
|
836
|
-
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
|
837
|
-
const before = event.clientY < rect.top + rect.height / 2;
|
|
838
|
-
this.setDropSlot(groupId, before ? index : index + 1);
|
|
839
|
-
}}
|
|
840
|
-
@drop=${(event: DragEvent) => {
|
|
841
|
-
if (index < 0 || session.id === this.draggingSessionId) return;
|
|
842
|
-
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
|
843
|
-
const before = event.clientY < rect.top + rect.height / 2;
|
|
844
|
-
this.handleDrop(event, groupId, before ? index : index + 1);
|
|
845
|
-
}}
|
|
846
1013
|
>
|
|
847
|
-
<div class="
|
|
848
|
-
<
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
1014
|
+
<div class="itemInner">
|
|
1015
|
+
<div class="itemHeader ${draggable ? 'draggable' : ''}">
|
|
1016
|
+
${draggable
|
|
1017
|
+
? html`
|
|
1018
|
+
<button
|
|
1019
|
+
class="dragHandle"
|
|
1020
|
+
type="button"
|
|
1021
|
+
title="Drag to reorder; use arrow, Home, or End keys for keyboard reordering"
|
|
1022
|
+
aria-label=${`Reorder ${session.title || session.id}`}
|
|
1023
|
+
aria-keyshortcuts="ArrowUp ArrowDown Home End"
|
|
1024
|
+
@pointerdown=${(event: PointerEvent) => {
|
|
1025
|
+
this.handlePointerDown(event, session, groupId, index);
|
|
1026
|
+
}}
|
|
1027
|
+
@pointermove=${this.handlePointerMove}
|
|
1028
|
+
@pointerup=${this.handlePointerUp}
|
|
1029
|
+
@pointercancel=${this.handlePointerCancel}
|
|
1030
|
+
@lostpointercapture=${this.handleLostPointerCapture}
|
|
1031
|
+
@keydown=${(event: KeyboardEvent) => {
|
|
1032
|
+
this.handleKeyboardMove(event, session, groupId);
|
|
1033
|
+
}}
|
|
1034
|
+
>
|
|
1035
|
+
<dees-icon icon="lucide:GripVertical" iconSize="14"></dees-icon>
|
|
1036
|
+
</button>
|
|
1037
|
+
`
|
|
1038
|
+
: ''}
|
|
1039
|
+
<button
|
|
1040
|
+
class="itemPrimary"
|
|
1041
|
+
type="button"
|
|
1042
|
+
aria-current=${session.id === this.selectedSessionId ? 'true' : 'false'}
|
|
1043
|
+
@click=${() => this.emit('harness-session-select', session)}
|
|
1044
|
+
@keydown=${(event: KeyboardEvent) => {
|
|
1045
|
+
if (event.key !== 'Enter') return;
|
|
1046
|
+
event.preventDefault();
|
|
1047
|
+
this.emit('harness-session-open', session);
|
|
1048
|
+
}}
|
|
1049
|
+
>
|
|
1050
|
+
<span class="stateDot"></span>
|
|
1051
|
+
<span class="itemCopy">
|
|
1052
|
+
<span class="itemTitle">
|
|
1053
|
+
${session.icon
|
|
1054
|
+
? html`<dees-icon class="itemIcon" icon=${session.icon} iconSize="13"></dees-icon>`
|
|
1055
|
+
: ''}
|
|
1056
|
+
<span class="titleText">${session.title || session.id}</span>
|
|
1057
|
+
</span>
|
|
1058
|
+
<span class="itemTime">${harnessFormatRelativeTime(session.updatedAt ?? session.createdAt)}</span>
|
|
1059
|
+
<span class="visuallyHidden">Status: ${this.sessionStateLabel(state)}</span>
|
|
866
1060
|
</span>
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
>
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
<
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
<div class="itemMeta">
|
|
894
|
-
${session.messageCount ? `${session.messageCount} messages · ` : ''}${session.id}
|
|
1061
|
+
</button>
|
|
1062
|
+
<button
|
|
1063
|
+
class="expandToggle"
|
|
1064
|
+
type="button"
|
|
1065
|
+
aria-expanded=${expanded ? 'true' : 'false'}
|
|
1066
|
+
aria-controls=${detailsId}
|
|
1067
|
+
title=${expanded ? 'Collapse conversation card' : 'Expand conversation card'}
|
|
1068
|
+
@click=${(event: MouseEvent) => {
|
|
1069
|
+
event.stopPropagation();
|
|
1070
|
+
this.toggleSessionExpanded(session.id);
|
|
1071
|
+
}}
|
|
1072
|
+
>
|
|
1073
|
+
<dees-icon icon="lucide:ChevronDown" iconSize="15"></dees-icon>
|
|
1074
|
+
</button>
|
|
1075
|
+
</div>
|
|
1076
|
+
${expanded
|
|
1077
|
+
? html`
|
|
1078
|
+
<div class="itemDetails" id=${detailsId}>
|
|
1079
|
+
<div class="stateLine">
|
|
1080
|
+
<span class="stateDot"></span>
|
|
1081
|
+
<span>${this.sessionStateLabel(state)}</span>
|
|
1082
|
+
</div>
|
|
1083
|
+
${session.preview ? html`<div class="itemPreview">${session.preview}</div>` : ''}
|
|
1084
|
+
<div class="itemMeta">
|
|
1085
|
+
${session.messageCount ? `${session.messageCount} messages · ` : ''}${session.id}
|
|
1086
|
+
</div>
|
|
895
1087
|
</div>
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
1088
|
+
`
|
|
1089
|
+
: ''}
|
|
1090
|
+
</div>
|
|
899
1091
|
</article>
|
|
900
1092
|
`;
|
|
901
1093
|
}
|
|
@@ -906,6 +1098,12 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
906
1098
|
);
|
|
907
1099
|
}
|
|
908
1100
|
|
|
1101
|
+
private eventTargetsDragHandle(event: Event): boolean {
|
|
1102
|
+
return event.composedPath().some(
|
|
1103
|
+
(target) => target instanceof HTMLElement && target.classList.contains('dragHandle'),
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
|
|
909
1107
|
private sessionStateLabel(state: NonNullable<IHarnessSessionMeta['state']>): string {
|
|
910
1108
|
switch (state) {
|
|
911
1109
|
case 'working': return 'Working';
|
|
@@ -953,27 +1151,139 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
953
1151
|
.filter((session): session is IHarnessSessionMeta => session !== undefined);
|
|
954
1152
|
}
|
|
955
1153
|
|
|
956
|
-
private
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
draggable: boolean,
|
|
1154
|
+
private handlePointerDown(
|
|
1155
|
+
eventArg: PointerEvent,
|
|
1156
|
+
sessionArg: IHarnessSessionMeta,
|
|
1157
|
+
groupIdArg: string | null,
|
|
1158
|
+
logicalIndexArg: number,
|
|
962
1159
|
): void {
|
|
963
|
-
if (
|
|
964
|
-
|
|
1160
|
+
if (
|
|
1161
|
+
this.pointerDragGesture
|
|
1162
|
+
|| eventArg.button !== 0
|
|
1163
|
+
|| !eventArg.isPrimary
|
|
1164
|
+
|| logicalIndexArg < 0
|
|
1165
|
+
) return;
|
|
1166
|
+
const handle = eventArg.currentTarget as HTMLElement;
|
|
1167
|
+
const source = handle.closest<HTMLElement>('.item');
|
|
1168
|
+
if (!source) return;
|
|
1169
|
+
const sourceRect = source.getBoundingClientRect();
|
|
1170
|
+
this.pointerDragGesture = {
|
|
1171
|
+
pointerId: eventArg.pointerId,
|
|
1172
|
+
handle,
|
|
1173
|
+
source,
|
|
1174
|
+
sessionId: sessionArg.id,
|
|
1175
|
+
sourceGroupId: groupIdArg,
|
|
1176
|
+
sourceLogicalIndex: logicalIndexArg,
|
|
1177
|
+
startClientX: eventArg.clientX,
|
|
1178
|
+
startClientY: eventArg.clientY,
|
|
1179
|
+
grabOffsetX: eventArg.clientX - sourceRect.left,
|
|
1180
|
+
grabOffsetY: eventArg.clientY - sourceRect.top,
|
|
1181
|
+
phase: 'pending',
|
|
1182
|
+
};
|
|
1183
|
+
window.addEventListener('keydown', this.handleDragKeyDown, true);
|
|
1184
|
+
window.addEventListener('blur', this.handleDragWindowBlur);
|
|
1185
|
+
try {
|
|
1186
|
+
handle.setPointerCapture(eventArg.pointerId);
|
|
1187
|
+
} catch {
|
|
1188
|
+
this.finishPointerDrag(false);
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
private readonly handlePointerMove = (eventArg: PointerEvent): void => {
|
|
1193
|
+
const gesture = this.pointerDragGesture;
|
|
1194
|
+
if (!gesture || eventArg.pointerId !== gesture.pointerId) return;
|
|
1195
|
+
if (!gesture.source.isConnected || !gesture.handle.isConnected) {
|
|
1196
|
+
this.finishPointerDrag(false);
|
|
965
1197
|
return;
|
|
966
1198
|
}
|
|
967
|
-
|
|
1199
|
+
if (
|
|
1200
|
+
gesture.phase === 'pending'
|
|
1201
|
+
&& Math.hypot(
|
|
1202
|
+
eventArg.clientX - gesture.startClientX,
|
|
1203
|
+
eventArg.clientY - gesture.startClientY,
|
|
1204
|
+
) < dragThresholdPixels
|
|
1205
|
+
) return;
|
|
1206
|
+
if (gesture.phase === 'pending' && !this.startPointerDrag(eventArg, gesture)) return;
|
|
1207
|
+
eventArg.preventDefault();
|
|
1208
|
+
this.moveDragPreview(eventArg.clientX, eventArg.clientY, gesture);
|
|
1209
|
+
this.dragAutoScrollClientX = eventArg.clientX;
|
|
1210
|
+
this.dragAutoScrollClientY = eventArg.clientY;
|
|
1211
|
+
this.updateDragAutoScrollVelocity();
|
|
1212
|
+
this.updateDropSlotFromPointer();
|
|
1213
|
+
};
|
|
1214
|
+
|
|
1215
|
+
private readonly handlePointerUp = (eventArg: PointerEvent): void => {
|
|
1216
|
+
const gesture = this.pointerDragGesture;
|
|
1217
|
+
if (!gesture || eventArg.pointerId !== gesture.pointerId) return;
|
|
1218
|
+
if (gesture.phase === 'active') {
|
|
1219
|
+
this.dragAutoScrollClientX = eventArg.clientX;
|
|
1220
|
+
this.dragAutoScrollClientY = eventArg.clientY;
|
|
1221
|
+
this.moveDragPreview(eventArg.clientX, eventArg.clientY, gesture);
|
|
1222
|
+
this.updateDropSlotFromPointer();
|
|
1223
|
+
this.finishPointerDrag(true);
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
this.finishPointerDrag(false);
|
|
1227
|
+
};
|
|
1228
|
+
|
|
1229
|
+
private readonly handlePointerCancel = (eventArg: PointerEvent): void => {
|
|
1230
|
+
if (eventArg.pointerId === this.pointerDragGesture?.pointerId) this.finishPointerDrag(false);
|
|
1231
|
+
};
|
|
1232
|
+
|
|
1233
|
+
private readonly handleLostPointerCapture = (eventArg: PointerEvent): void => {
|
|
1234
|
+
if (eventArg.pointerId === this.pointerDragGesture?.pointerId) this.finishPointerDrag(false);
|
|
1235
|
+
};
|
|
1236
|
+
|
|
1237
|
+
private readonly handleDragKeyDown = (eventArg: KeyboardEvent): void => {
|
|
1238
|
+
if (eventArg.key !== 'Escape' || !this.pointerDragGesture) return;
|
|
1239
|
+
eventArg.preventDefault();
|
|
1240
|
+
eventArg.stopPropagation();
|
|
1241
|
+
this.finishPointerDrag(false);
|
|
1242
|
+
};
|
|
1243
|
+
|
|
1244
|
+
private readonly handleDragWindowBlur = (): void => {
|
|
1245
|
+
this.finishPointerDrag(false);
|
|
1246
|
+
};
|
|
1247
|
+
|
|
1248
|
+
private startPointerDrag(eventArg: PointerEvent, gestureArg: IPointerDragGesture): boolean {
|
|
1249
|
+
const sourceRect = gestureArg.source.getBoundingClientRect();
|
|
1250
|
+
gestureArg.source.classList.add('dragging');
|
|
1251
|
+
gestureArg.source.style.setProperty('--drag-preview-width', `${sourceRect.width}px`);
|
|
1252
|
+
gestureArg.source.style.setProperty('--drag-preview-height', `${sourceRect.height}px`);
|
|
1253
|
+
this.moveDragPreview(eventArg.clientX, eventArg.clientY, gestureArg);
|
|
1254
|
+
gestureArg.source.setAttribute('popover', 'manual');
|
|
1255
|
+
try {
|
|
1256
|
+
gestureArg.source.showPopover();
|
|
1257
|
+
} catch {
|
|
1258
|
+
this.finishPointerDrag(false);
|
|
1259
|
+
return false;
|
|
1260
|
+
}
|
|
1261
|
+
gestureArg.phase = 'active';
|
|
968
1262
|
this.reflowGeneration += 1;
|
|
969
1263
|
this.cancelReflowAnimations();
|
|
970
|
-
this.draggedItemHeight = Math.ceil(
|
|
971
|
-
this.
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1264
|
+
this.draggedItemHeight = Math.ceil(sourceRect.height);
|
|
1265
|
+
this.draggingSessionId = gestureArg.sessionId;
|
|
1266
|
+
const remainingCount = this.sortableSessions(this.sessionsForGroup(gestureArg.sourceGroupId)).length;
|
|
1267
|
+
this.dropSlot = {
|
|
1268
|
+
groupId: gestureArg.sourceGroupId,
|
|
1269
|
+
index: Math.min(gestureArg.sourceLogicalIndex, remainingCount),
|
|
1270
|
+
};
|
|
1271
|
+
return true;
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
private moveDragPreview(
|
|
1275
|
+
clientXArg: number,
|
|
1276
|
+
clientYArg: number,
|
|
1277
|
+
gestureArg: IPointerDragGesture,
|
|
1278
|
+
): void {
|
|
1279
|
+
gestureArg.source.style.setProperty(
|
|
1280
|
+
'--drag-preview-x',
|
|
1281
|
+
`${clientXArg - gestureArg.grabOffsetX}px`,
|
|
1282
|
+
);
|
|
1283
|
+
gestureArg.source.style.setProperty(
|
|
1284
|
+
'--drag-preview-y',
|
|
1285
|
+
`${clientYArg - gestureArg.grabOffsetY}px`,
|
|
1286
|
+
);
|
|
977
1287
|
}
|
|
978
1288
|
|
|
979
1289
|
private setDropSlot(groupIdArg: string | null, indexArg: number): void {
|
|
@@ -985,38 +1295,239 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
985
1295
|
void this.animateCardReflow(previousRects, generation);
|
|
986
1296
|
}
|
|
987
1297
|
|
|
988
|
-
private
|
|
989
|
-
if (!this.
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
this.
|
|
1298
|
+
private clearDropSlot(): void {
|
|
1299
|
+
if (!this.dropSlot) return;
|
|
1300
|
+
const previousRects = this.captureCardRects();
|
|
1301
|
+
const generation = ++this.reflowGeneration;
|
|
1302
|
+
this.dropSlot = undefined;
|
|
1303
|
+
void this.animateCardReflow(previousRects, generation);
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
private updateDragAutoScrollVelocity(): void {
|
|
1307
|
+
const container = this.scrollContainer;
|
|
1308
|
+
if (!container || !this.draggingSessionId) return;
|
|
1309
|
+
const rect = container.getBoundingClientRect();
|
|
1310
|
+
if (this.dragAutoScrollClientX < rect.left || this.dragAutoScrollClientX > rect.right) {
|
|
1311
|
+
this.stopDragAutoScroll();
|
|
1312
|
+
return;
|
|
1313
|
+
}
|
|
1314
|
+
const edgeSize = Math.min(64, Math.max(32, rect.height * 0.25));
|
|
1315
|
+
const topDepth = Math.max(
|
|
1316
|
+
0,
|
|
1317
|
+
Math.min(1, (rect.top + edgeSize - this.dragAutoScrollClientY) / edgeSize),
|
|
1318
|
+
);
|
|
1319
|
+
const bottomDepth = Math.max(
|
|
1320
|
+
0,
|
|
1321
|
+
Math.min(1, (this.dragAutoScrollClientY - (rect.bottom - edgeSize)) / edgeSize),
|
|
1322
|
+
);
|
|
1323
|
+
this.dragAutoScrollVelocity = bottomDepth > 0
|
|
1324
|
+
? dragAutoScrollMaxPixelsPerSecond * bottomDepth * bottomDepth
|
|
1325
|
+
: topDepth > 0
|
|
1326
|
+
? -dragAutoScrollMaxPixelsPerSecond * topDepth * topDepth
|
|
1327
|
+
: 0;
|
|
1328
|
+
if (this.dragAutoScrollVelocity === 0) {
|
|
1329
|
+
this.stopDragAutoScroll();
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
if (this.dragAutoScrollFrame === undefined) {
|
|
1333
|
+
this.dragAutoScrollLastFrame = performance.now();
|
|
1334
|
+
this.dragAutoScrollFrame = requestAnimationFrame(this.runDragAutoScroll);
|
|
1335
|
+
}
|
|
993
1336
|
}
|
|
994
1337
|
|
|
995
|
-
private
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1338
|
+
private readonly runDragAutoScroll = (timestampArg: number): void => {
|
|
1339
|
+
this.dragAutoScrollFrame = undefined;
|
|
1340
|
+
const container = this.scrollContainer;
|
|
1341
|
+
if (!container || !this.draggingSessionId || this.dragAutoScrollVelocity === 0) return;
|
|
1342
|
+
const elapsedMs = Math.min(34, Math.max(0, timestampArg - this.dragAutoScrollLastFrame));
|
|
1343
|
+
this.dragAutoScrollLastFrame = timestampArg;
|
|
1344
|
+
const previousScrollTop = container.scrollTop;
|
|
1345
|
+
container.scrollTop += this.dragAutoScrollVelocity * elapsedMs / 1000;
|
|
1346
|
+
this.syncScrollShadowState();
|
|
1347
|
+
const atScrollBoundary = this.dragAutoScrollVelocity < 0
|
|
1348
|
+
? container.scrollTop <= 0
|
|
1349
|
+
: container.scrollTop + container.clientHeight >= container.scrollHeight - 1;
|
|
1350
|
+
if (container.scrollTop === previousScrollTop && atScrollBoundary) {
|
|
1351
|
+
this.stopDragAutoScroll();
|
|
1001
1352
|
return;
|
|
1002
1353
|
}
|
|
1354
|
+
this.updateDropSlotFromPointer();
|
|
1355
|
+
this.dragAutoScrollFrame = requestAnimationFrame(this.runDragAutoScroll);
|
|
1356
|
+
};
|
|
1357
|
+
|
|
1358
|
+
private updateDropSlotFromPointer(): void {
|
|
1359
|
+
const pointerX = this.dragAutoScrollClientX;
|
|
1360
|
+
const pointerY = this.dragAutoScrollClientY;
|
|
1361
|
+
const placeholder = this.shadowRoot?.querySelector<HTMLElement>('.dropPlaceholder');
|
|
1362
|
+
if (placeholder) {
|
|
1363
|
+
const rect = placeholder.getBoundingClientRect();
|
|
1364
|
+
if (
|
|
1365
|
+
pointerX >= rect.left
|
|
1366
|
+
&& pointerX <= rect.right
|
|
1367
|
+
&& pointerY >= rect.top
|
|
1368
|
+
&& pointerY <= rect.bottom
|
|
1369
|
+
) {
|
|
1370
|
+
this.setDropSlot(
|
|
1371
|
+
placeholder.dataset.dropGroupId || null,
|
|
1372
|
+
Number(placeholder.dataset.dropIndex),
|
|
1373
|
+
);
|
|
1374
|
+
return;
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
const selector = '[data-drop-group-id][data-drop-index][data-drop-kind]';
|
|
1378
|
+
let target: HTMLElement | undefined;
|
|
1379
|
+
const offsets = this.dragAutoScrollVelocity === 0
|
|
1380
|
+
? [0, -8, 8, -16, 16, -32, 32]
|
|
1381
|
+
: [0, 8, 16, 32, 48, 64, 96].map(
|
|
1382
|
+
(value) => value * (this.dragAutoScrollVelocity < 0 ? 1 : -1),
|
|
1383
|
+
);
|
|
1384
|
+
for (const offset of offsets) {
|
|
1385
|
+
const hit = this.shadowRoot?.elementFromPoint(pointerX, pointerY + offset);
|
|
1386
|
+
const candidate = hit?.closest<HTMLElement>(selector);
|
|
1387
|
+
if (
|
|
1388
|
+
candidate
|
|
1389
|
+
&& candidate.dataset.sessionId !== this.draggingSessionId
|
|
1390
|
+
&& Number(candidate.dataset.dropIndex) >= 0
|
|
1391
|
+
) {
|
|
1392
|
+
target = candidate;
|
|
1393
|
+
break;
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
if (!target) {
|
|
1397
|
+
this.clearDropSlot();
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
const baseIndex = Number(target.dataset.dropIndex);
|
|
1401
|
+
const groupId = target.dataset.dropGroupId || null;
|
|
1402
|
+
const rect = target.getBoundingClientRect();
|
|
1403
|
+
const index = target.dataset.dropKind === 'card' && pointerY >= rect.top + rect.height / 2
|
|
1404
|
+
? baseIndex + 1
|
|
1405
|
+
: baseIndex;
|
|
1406
|
+
this.setDropSlot(groupId, index);
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
private get scrollContainer(): HTMLElement | undefined {
|
|
1410
|
+
return this.shadowRoot?.querySelector<HTMLElement>('.list') ?? undefined;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
private observeScrollGeometry(): void {
|
|
1414
|
+
if (!this.isConnected) return;
|
|
1415
|
+
const container = this.scrollContainer;
|
|
1416
|
+
const content = this.shadowRoot?.querySelector<HTMLElement>('.listContent') ?? undefined;
|
|
1417
|
+
if (!container || !content) return;
|
|
1418
|
+
if (
|
|
1419
|
+
this.scrollResizeObserver
|
|
1420
|
+
&& this.observedScrollContainer === container
|
|
1421
|
+
&& this.observedScrollContent === content
|
|
1422
|
+
) return;
|
|
1423
|
+
this.scrollResizeObserver?.disconnect();
|
|
1424
|
+
this.scrollResizeObserver = new ResizeObserver(this.syncScrollShadowState);
|
|
1425
|
+
this.scrollResizeObserver.observe(container);
|
|
1426
|
+
this.scrollResizeObserver.observe(content);
|
|
1427
|
+
this.observedScrollContainer = container;
|
|
1428
|
+
this.observedScrollContent = content;
|
|
1429
|
+
}
|
|
1430
|
+
|
|
1431
|
+
private readonly syncScrollShadowState = (): void => {
|
|
1432
|
+
const container = this.scrollContainer;
|
|
1433
|
+
if (!container) return;
|
|
1434
|
+
const topVisible = container.scrollTop > 1;
|
|
1435
|
+
const bottomVisible = container.scrollTop + container.clientHeight < container.scrollHeight - 1;
|
|
1436
|
+
if (this.topScrollShadowVisible !== topVisible) this.topScrollShadowVisible = topVisible;
|
|
1437
|
+
if (this.bottomScrollShadowVisible !== bottomVisible) this.bottomScrollShadowVisible = bottomVisible;
|
|
1438
|
+
};
|
|
1439
|
+
|
|
1440
|
+
private stopDragAutoScroll(): void {
|
|
1441
|
+
if (this.dragAutoScrollFrame !== undefined) cancelAnimationFrame(this.dragAutoScrollFrame);
|
|
1442
|
+
this.dragAutoScrollFrame = undefined;
|
|
1443
|
+
this.dragAutoScrollVelocity = 0;
|
|
1444
|
+
this.dragAutoScrollLastFrame = 0;
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
private createMoveDetail(
|
|
1448
|
+
sessionIdArg: string,
|
|
1449
|
+
groupIdArg: string | null,
|
|
1450
|
+
indexArg: number,
|
|
1451
|
+
): IHarnessSessionMoveDetail {
|
|
1003
1452
|
const targetSessions = this.sortableSessions(this.sessionsForGroup(groupIdArg));
|
|
1004
1453
|
const beforeSessionId = targetSessions[Math.max(0, indexArg)]?.id;
|
|
1005
|
-
const sourceGroupId = this.groups.find((group) => group.sessionIds.includes(
|
|
1454
|
+
const sourceGroupId = this.groups.find((group) => group.sessionIds.includes(sessionIdArg))?.id ?? null;
|
|
1006
1455
|
const sourceSessions = this.sortableSessions(this.sessionsForGroup(sourceGroupId), false);
|
|
1007
|
-
const sourceIndex = sourceSessions.findIndex((session) => session.id ===
|
|
1456
|
+
const sourceIndex = sourceSessions.findIndex((session) => session.id === sessionIdArg);
|
|
1008
1457
|
const compatibilityIndex = (
|
|
1009
1458
|
sourceGroupId === groupIdArg
|
|
1010
1459
|
&& sourceIndex >= 0
|
|
1011
1460
|
&& indexArg > sourceIndex
|
|
1012
1461
|
) ? indexArg + 1 : indexArg;
|
|
1013
|
-
|
|
1014
|
-
sessionId,
|
|
1462
|
+
return {
|
|
1463
|
+
sessionId: sessionIdArg,
|
|
1015
1464
|
groupId: groupIdArg,
|
|
1016
1465
|
index: Math.max(0, compatibilityIndex),
|
|
1017
1466
|
...(beforeSessionId ? { beforeSessionId } : {}),
|
|
1018
1467
|
};
|
|
1019
|
-
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
private handleKeyboardMove(
|
|
1471
|
+
eventArg: KeyboardEvent,
|
|
1472
|
+
sessionArg: IHarnessSessionMeta,
|
|
1473
|
+
groupIdArg: string | null,
|
|
1474
|
+
): void {
|
|
1475
|
+
if (!['ArrowUp', 'ArrowDown', 'Home', 'End'].includes(eventArg.key)) return;
|
|
1476
|
+
eventArg.preventDefault();
|
|
1477
|
+
eventArg.stopPropagation();
|
|
1478
|
+
const sessions = this.sortableSessions(this.sessionsForGroup(groupIdArg), false);
|
|
1479
|
+
const sourceIndex = sessions.findIndex((session) => session.id === sessionArg.id);
|
|
1480
|
+
if (sourceIndex < 0) return;
|
|
1481
|
+
const sectionIds: Array<string | null> = [...this.groups.map((group) => group.id), null];
|
|
1482
|
+
const sectionIndex = sectionIds.indexOf(groupIdArg);
|
|
1483
|
+
const adjacentGroupId = eventArg.key === 'ArrowUp' && sourceIndex === 0 && sectionIndex > 0
|
|
1484
|
+
? sectionIds[sectionIndex - 1]
|
|
1485
|
+
: eventArg.key === 'ArrowDown'
|
|
1486
|
+
&& sourceIndex === sessions.length - 1
|
|
1487
|
+
&& sectionIndex >= 0
|
|
1488
|
+
&& sectionIndex < sectionIds.length - 1
|
|
1489
|
+
? sectionIds[sectionIndex + 1]
|
|
1490
|
+
: undefined;
|
|
1491
|
+
if (adjacentGroupId !== undefined) {
|
|
1492
|
+
const targetSessions = this.sortableSessions(this.sessionsForGroup(adjacentGroupId), false);
|
|
1493
|
+
const targetIndex = eventArg.key === 'ArrowUp' ? targetSessions.length : 0;
|
|
1494
|
+
const beforeSession = targetSessions[targetIndex];
|
|
1495
|
+
const detail: IHarnessSessionMoveDetail = {
|
|
1496
|
+
sessionId: sessionArg.id,
|
|
1497
|
+
groupId: adjacentGroupId,
|
|
1498
|
+
index: targetIndex,
|
|
1499
|
+
...(beforeSession ? { beforeSessionId: beforeSession.id } : {}),
|
|
1500
|
+
};
|
|
1501
|
+
const groupName = adjacentGroupId === null
|
|
1502
|
+
? 'Ungrouped'
|
|
1503
|
+
: this.groups.find((group) => group.id === adjacentGroupId)?.name ?? adjacentGroupId;
|
|
1504
|
+
this.moveAnnouncement = `Moved ${sessionArg.title || sessionArg.id} to ${groupName}.`;
|
|
1505
|
+
this.keyboardMoveFocus = { sessionId: sessionArg.id, groupId: adjacentGroupId };
|
|
1506
|
+
this.dispatchEvent(
|
|
1507
|
+
new CustomEvent('harness-session-move', { detail, bubbles: true, composed: true }),
|
|
1508
|
+
);
|
|
1509
|
+
return;
|
|
1510
|
+
}
|
|
1511
|
+
const remaining = sessions.filter((session) => session.id !== sessionArg.id);
|
|
1512
|
+
const targetIndex = eventArg.key === 'Home'
|
|
1513
|
+
? 0
|
|
1514
|
+
: eventArg.key === 'End'
|
|
1515
|
+
? remaining.length
|
|
1516
|
+
: eventArg.key === 'ArrowUp'
|
|
1517
|
+
? Math.max(0, sourceIndex - 1)
|
|
1518
|
+
: Math.min(remaining.length, sourceIndex + 1);
|
|
1519
|
+
if (targetIndex === sourceIndex) return;
|
|
1520
|
+
const beforeSession = remaining[targetIndex];
|
|
1521
|
+
const compatibilityIndex = targetIndex > sourceIndex ? targetIndex + 1 : targetIndex;
|
|
1522
|
+
const detail: IHarnessSessionMoveDetail = {
|
|
1523
|
+
sessionId: sessionArg.id,
|
|
1524
|
+
groupId: groupIdArg,
|
|
1525
|
+
index: compatibilityIndex,
|
|
1526
|
+
...(beforeSession ? { beforeSessionId: beforeSession.id } : {}),
|
|
1527
|
+
};
|
|
1528
|
+
this.moveAnnouncement = beforeSession
|
|
1529
|
+
? `Moved ${sessionArg.title || sessionArg.id} before ${beforeSession.title || beforeSession.id}.`
|
|
1530
|
+
: `Moved ${sessionArg.title || sessionArg.id} to the end.`;
|
|
1020
1531
|
this.dispatchEvent(
|
|
1021
1532
|
new CustomEvent('harness-session-move', { detail, bubbles: true, composed: true }),
|
|
1022
1533
|
);
|
|
@@ -1031,6 +1542,31 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1031
1542
|
return result;
|
|
1032
1543
|
}
|
|
1033
1544
|
|
|
1545
|
+
private restoreKeyboardMoveFocus(): void {
|
|
1546
|
+
const pending = this.keyboardMoveFocus;
|
|
1547
|
+
if (!pending) return;
|
|
1548
|
+
const currentGroupId = this.groups.find(
|
|
1549
|
+
(group) => group.sessionIds.includes(pending.sessionId),
|
|
1550
|
+
)?.id ?? null;
|
|
1551
|
+
if (currentGroupId !== pending.groupId) return;
|
|
1552
|
+
const item = Array.from(
|
|
1553
|
+
this.shadowRoot?.querySelectorAll<HTMLElement>('.item[data-session-id]') ?? [],
|
|
1554
|
+
).find((candidate) => candidate.dataset.sessionId === pending.sessionId);
|
|
1555
|
+
const handle = item?.querySelector<HTMLElement>('.dragHandle');
|
|
1556
|
+
if (handle) {
|
|
1557
|
+
handle.focus();
|
|
1558
|
+
this.keyboardMoveFocus = undefined;
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
const groupHeader = Array.from(
|
|
1562
|
+
this.shadowRoot?.querySelectorAll<HTMLElement>('.groupHeader[data-drop-group-id]') ?? [],
|
|
1563
|
+
).find((candidate) => candidate.dataset.dropGroupId === (pending.groupId ?? ''));
|
|
1564
|
+
if (groupHeader) {
|
|
1565
|
+
groupHeader.focus();
|
|
1566
|
+
this.keyboardMoveFocus = undefined;
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1034
1570
|
private async animateCardReflow(
|
|
1035
1571
|
previousRectsArg: Map<string, DOMRect>,
|
|
1036
1572
|
generationArg: number,
|
|
@@ -1069,13 +1605,42 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1069
1605
|
this.reflowAnimations.clear();
|
|
1070
1606
|
}
|
|
1071
1607
|
|
|
1072
|
-
private
|
|
1608
|
+
private finishPointerDrag(commitArg: boolean): void {
|
|
1609
|
+
const gesture = this.pointerDragGesture;
|
|
1610
|
+
if (!gesture) return;
|
|
1611
|
+
const detail = commitArg && gesture.phase === 'active' && this.dropSlot
|
|
1612
|
+
? this.createMoveDetail(gesture.sessionId, this.dropSlot.groupId, this.dropSlot.index)
|
|
1613
|
+
: undefined;
|
|
1614
|
+
this.pointerDragGesture = undefined;
|
|
1615
|
+
window.removeEventListener('keydown', this.handleDragKeyDown, true);
|
|
1616
|
+
window.removeEventListener('blur', this.handleDragWindowBlur);
|
|
1617
|
+
this.stopDragAutoScroll();
|
|
1073
1618
|
this.reflowGeneration += 1;
|
|
1074
1619
|
this.cancelReflowAnimations();
|
|
1620
|
+
if (gesture.source.matches(':popover-open')) gesture.source.hidePopover();
|
|
1621
|
+
gesture.source.removeAttribute('popover');
|
|
1622
|
+
gesture.source.classList.remove('dragging');
|
|
1623
|
+
for (const property of [
|
|
1624
|
+
'--drag-preview-width',
|
|
1625
|
+
'--drag-preview-height',
|
|
1626
|
+
'--drag-preview-x',
|
|
1627
|
+
'--drag-preview-y',
|
|
1628
|
+
]) gesture.source.style.removeProperty(property);
|
|
1075
1629
|
this.draggingSessionId = '';
|
|
1076
1630
|
this.draggedItemHeight = 0;
|
|
1077
1631
|
this.dropSlot = undefined;
|
|
1078
|
-
|
|
1632
|
+
try {
|
|
1633
|
+
if (gesture.handle.hasPointerCapture(gesture.pointerId)) {
|
|
1634
|
+
gesture.handle.releasePointerCapture(gesture.pointerId);
|
|
1635
|
+
}
|
|
1636
|
+
} catch {
|
|
1637
|
+
// Capture may already be released or the handle may have disconnected.
|
|
1638
|
+
}
|
|
1639
|
+
if (detail) {
|
|
1640
|
+
this.dispatchEvent(
|
|
1641
|
+
new CustomEvent('harness-session-move', { detail, bubbles: true, composed: true }),
|
|
1642
|
+
);
|
|
1643
|
+
}
|
|
1079
1644
|
}
|
|
1080
1645
|
|
|
1081
1646
|
private emit(name: string, session: IHarnessSessionMeta): void {
|