@design.estate/dees-catalog 4.10.0 → 4.11.0
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 +4473 -2293
- 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 +26 -2
- package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.js +597 -93
- 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 +36 -3
- package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.ts +651 -103
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 {
|
|
@@ -199,29 +280,64 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
199
280
|
|
|
200
281
|
.item:hover {
|
|
201
282
|
border-color: color-mix(in srgb, var(--card-accent) 42%, var(--dees-color-border-default));
|
|
202
|
-
background: color-mix(in srgb, var(--dees-color-bg-primary)
|
|
283
|
+
background: color-mix(in srgb, var(--dees-color-bg-primary) 90%, var(--card-accent) 10%);
|
|
284
|
+
box-shadow:
|
|
285
|
+
var(--dees-shadow-md),
|
|
286
|
+
inset 3px 0 0 var(--card-accent);
|
|
203
287
|
}
|
|
204
288
|
|
|
205
289
|
.item.selected {
|
|
206
|
-
border-color:
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
290
|
+
border-color: var(--dees-color-border-default);
|
|
291
|
+
border-left-color: var(--dees-color-accent-primary);
|
|
292
|
+
border-left-width: 3px;
|
|
293
|
+
background: color-mix(in srgb, var(--dees-color-bg-primary) 90%, transparent);
|
|
294
|
+
box-shadow: var(--dees-shadow-md);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.item.selected:hover {
|
|
298
|
+
border-color: var(--dees-color-border-default);
|
|
299
|
+
border-left-color: var(--dees-color-accent-primary);
|
|
300
|
+
background: color-mix(in srgb, var(--dees-color-bg-primary) 90%, transparent);
|
|
301
|
+
box-shadow: var(--dees-shadow-md);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.item.selected .titleText {
|
|
305
|
+
font-weight: 700;
|
|
211
306
|
}
|
|
212
307
|
|
|
213
308
|
.item.dragging {
|
|
214
|
-
position:
|
|
215
|
-
inset
|
|
216
|
-
|
|
217
|
-
|
|
309
|
+
position: fixed;
|
|
310
|
+
inset: 0 auto auto 0;
|
|
311
|
+
width: var(--drag-preview-width);
|
|
312
|
+
height: var(--drag-preview-height);
|
|
313
|
+
max-width: none;
|
|
314
|
+
margin: 0;
|
|
315
|
+
padding: 0;
|
|
316
|
+
opacity: 0.96;
|
|
218
317
|
pointer-events: none;
|
|
318
|
+
transform: translate3d(var(--drag-preview-x), var(--drag-preview-y), 0);
|
|
319
|
+
box-shadow: var(--dees-shadow-xl);
|
|
219
320
|
}
|
|
220
321
|
|
|
221
322
|
.item.state-working {
|
|
222
323
|
--card-accent: var(--session-working);
|
|
223
324
|
}
|
|
224
325
|
|
|
326
|
+
@keyframes workingDotPulse {
|
|
327
|
+
0%, 100% {
|
|
328
|
+
transform: scale(0.92);
|
|
329
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--session-working) 14%, transparent);
|
|
330
|
+
}
|
|
331
|
+
50% {
|
|
332
|
+
transform: scale(1.08);
|
|
333
|
+
box-shadow: 0 0 0 6px color-mix(in srgb, var(--session-working) 3%, transparent);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
.item.state-working .itemHeader .stateDot {
|
|
338
|
+
animation: workingDotPulse 2.4s ease-in-out infinite;
|
|
339
|
+
}
|
|
340
|
+
|
|
225
341
|
.item.state-finished {
|
|
226
342
|
--card-accent: var(--session-finished);
|
|
227
343
|
}
|
|
@@ -236,16 +352,10 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
236
352
|
|
|
237
353
|
@keyframes attentionPulse {
|
|
238
354
|
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);
|
|
355
|
+
background: color-mix(in srgb, var(--dees-color-bg-primary) 94%, var(--session-attention) 6%);
|
|
243
356
|
}
|
|
244
357
|
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);
|
|
358
|
+
background: color-mix(in srgb, var(--dees-color-bg-primary) 84%, var(--session-attention) 16%);
|
|
249
359
|
}
|
|
250
360
|
}
|
|
251
361
|
|
|
@@ -253,12 +363,43 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
253
363
|
animation: attentionPulse 1.15s ease-in-out infinite;
|
|
254
364
|
}
|
|
255
365
|
|
|
366
|
+
.item.state-attention:not(.selected):hover {
|
|
367
|
+
animation: none;
|
|
368
|
+
background: color-mix(in srgb, var(--dees-color-bg-primary) 82%, var(--session-attention) 18%);
|
|
369
|
+
}
|
|
370
|
+
|
|
256
371
|
.itemHeader {
|
|
257
372
|
display: grid;
|
|
258
373
|
grid-template-columns: minmax(0, 1fr) 30px;
|
|
259
374
|
min-height: 46px;
|
|
260
375
|
}
|
|
261
376
|
|
|
377
|
+
.itemHeader.draggable {
|
|
378
|
+
grid-template-columns: 24px minmax(0, 1fr) 30px;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
.dragHandle {
|
|
382
|
+
appearance: none;
|
|
383
|
+
display: grid;
|
|
384
|
+
place-items: center;
|
|
385
|
+
padding: 0;
|
|
386
|
+
border: 0;
|
|
387
|
+
background: transparent;
|
|
388
|
+
color: var(--dees-color-text-muted);
|
|
389
|
+
cursor: grab;
|
|
390
|
+
touch-action: none;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
.dragHandle:active {
|
|
394
|
+
cursor: grabbing;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
.dragHandle:focus-visible {
|
|
398
|
+
outline: 2px solid var(--dees-color-accent-primary);
|
|
399
|
+
outline-offset: -3px;
|
|
400
|
+
border-radius: 8px;
|
|
401
|
+
}
|
|
402
|
+
|
|
262
403
|
.itemPrimary,
|
|
263
404
|
.expandToggle {
|
|
264
405
|
appearance: none;
|
|
@@ -498,6 +639,8 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
498
639
|
@media (prefers-reduced-motion: reduce) {
|
|
499
640
|
.item,
|
|
500
641
|
.item.state-attention,
|
|
642
|
+
.item.state-working .itemHeader .stateDot,
|
|
643
|
+
.scrollShadow,
|
|
501
644
|
.expandToggle dees-icon {
|
|
502
645
|
animation: none;
|
|
503
646
|
transition: none;
|
|
@@ -557,15 +700,21 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
557
700
|
|
|
558
701
|
public updated(changedProperties: Map<PropertyKey, unknown>): void {
|
|
559
702
|
super.updated(changedProperties);
|
|
560
|
-
|
|
561
|
-
|
|
703
|
+
this.observeScrollGeometry();
|
|
704
|
+
this.syncScrollShadowState();
|
|
705
|
+
this.restoreKeyboardMoveFocus();
|
|
706
|
+
if (
|
|
707
|
+
this.pointerDragGesture
|
|
708
|
+
&& (!this.pointerDragGesture.source.isConnected || !this.pointerDragGesture.handle.isConnected)
|
|
709
|
+
) {
|
|
710
|
+
this.finishPointerDrag(false);
|
|
562
711
|
}
|
|
563
712
|
if (!changedProperties.has('sessions')) return;
|
|
564
713
|
const previousSessions = changedProperties.get('sessions') as IHarnessSessionMeta[] | undefined;
|
|
565
714
|
const previousById = new Map((previousSessions ?? []).map((session) => [session.id, session]));
|
|
566
715
|
const knownIds = new Set(this.sessions.map((session) => session.id));
|
|
567
|
-
if (this.
|
|
568
|
-
this.
|
|
716
|
+
if (this.pointerDragGesture && !knownIds.has(this.pointerDragGesture.sessionId)) {
|
|
717
|
+
this.finishPointerDrag(false);
|
|
569
718
|
}
|
|
570
719
|
const nextExpanded = new Set(
|
|
571
720
|
[...this.expandedSessionIds].filter((sessionId) => knownIds.has(sessionId)),
|
|
@@ -584,8 +733,20 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
584
733
|
}
|
|
585
734
|
}
|
|
586
735
|
|
|
736
|
+
public async connectedCallback(): Promise<void> {
|
|
737
|
+
await super.connectedCallback();
|
|
738
|
+
await this.updateComplete;
|
|
739
|
+
if (!this.isConnected) return;
|
|
740
|
+
this.observeScrollGeometry();
|
|
741
|
+
this.syncScrollShadowState();
|
|
742
|
+
}
|
|
743
|
+
|
|
587
744
|
public async disconnectedCallback(): Promise<void> {
|
|
588
|
-
this.
|
|
745
|
+
this.finishPointerDrag(false);
|
|
746
|
+
this.scrollResizeObserver?.disconnect();
|
|
747
|
+
this.scrollResizeObserver = undefined;
|
|
748
|
+
this.observedScrollContainer = undefined;
|
|
749
|
+
this.observedScrollContent = undefined;
|
|
589
750
|
await super.disconnectedCallback();
|
|
590
751
|
}
|
|
591
752
|
|
|
@@ -602,13 +763,23 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
602
763
|
}}
|
|
603
764
|
/>
|
|
604
765
|
</div>
|
|
605
|
-
<div class="
|
|
606
|
-
${this.
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
766
|
+
<div class="listFrame">
|
|
767
|
+
<div class="scrollShadow top ${this.topScrollShadowVisible ? 'visible' : ''}"></div>
|
|
768
|
+
<div
|
|
769
|
+
class="list"
|
|
770
|
+
@scroll=${this.syncScrollShadowState}
|
|
771
|
+
>
|
|
772
|
+
<div class="listContent">
|
|
773
|
+
${this.loading
|
|
774
|
+
? html`<div class="emptyNote">Loading conversations…</div>`
|
|
775
|
+
: groupingActive
|
|
776
|
+
? this.renderGroupedList()
|
|
777
|
+
: this.renderFlatList()}
|
|
778
|
+
</div>
|
|
779
|
+
</div>
|
|
780
|
+
<div class="scrollShadow bottom ${this.bottomScrollShadowVisible ? 'visible' : ''}"></div>
|
|
611
781
|
</div>
|
|
782
|
+
<div class="visuallyHidden" aria-live="polite">${this.moveAnnouncement}</div>
|
|
612
783
|
${groupingActive
|
|
613
784
|
? html`
|
|
614
785
|
<div class="newGroupRow">
|
|
@@ -670,6 +841,9 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
670
841
|
return html`
|
|
671
842
|
<button
|
|
672
843
|
class="groupHeader ${collapsed ? 'collapsed' : ''} ${isHeaderDrop ? 'dropTarget' : ''}"
|
|
844
|
+
data-drop-group-id=${group.id}
|
|
845
|
+
data-drop-index=${this.sortableSessions(members).length}
|
|
846
|
+
data-drop-kind="slot"
|
|
673
847
|
type="button"
|
|
674
848
|
@click=${() => this.toggleGroupCollapsed(group.id)}
|
|
675
849
|
@contextmenu=${(event: MouseEvent) => {
|
|
@@ -684,16 +858,6 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
684
858
|
new CustomEvent('harness-group-context', { detail, bubbles: true, composed: true }),
|
|
685
859
|
);
|
|
686
860
|
}}
|
|
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
861
|
>
|
|
698
862
|
<dees-icon class="chevron" icon="lucide:ChevronDown" iconSize="12"></dees-icon>
|
|
699
863
|
<span>${group.name}</span>
|
|
@@ -708,10 +872,11 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
708
872
|
? html`
|
|
709
873
|
<button
|
|
710
874
|
class="groupHeader"
|
|
875
|
+
data-drop-group-id=""
|
|
876
|
+
data-drop-index="0"
|
|
877
|
+
data-drop-kind="slot"
|
|
711
878
|
type="button"
|
|
712
879
|
tabindex="-1"
|
|
713
|
-
@dragover=${(event: DragEvent) => this.handleDragOverSlot(event, null, 0)}
|
|
714
|
-
@drop=${(event: DragEvent) => this.handleDrop(event, null, 0)}
|
|
715
880
|
>
|
|
716
881
|
<span>Ungrouped</span>
|
|
717
882
|
<span class="groupCount">${ungrouped.length}</span>
|
|
@@ -766,8 +931,9 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
766
931
|
? html`
|
|
767
932
|
<div
|
|
768
933
|
class="emptyDropZone"
|
|
769
|
-
|
|
770
|
-
|
|
934
|
+
data-drop-group-id=${groupId ?? ''}
|
|
935
|
+
data-drop-index="0"
|
|
936
|
+
data-drop-kind="slot"
|
|
771
937
|
></div>
|
|
772
938
|
`
|
|
773
939
|
: ''}
|
|
@@ -779,9 +945,10 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
779
945
|
return html`
|
|
780
946
|
<div
|
|
781
947
|
class="dropPlaceholder"
|
|
948
|
+
data-drop-group-id=${groupId ?? ''}
|
|
949
|
+
data-drop-index=${index}
|
|
950
|
+
data-drop-kind="slot"
|
|
782
951
|
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
952
|
></div>
|
|
786
953
|
`;
|
|
787
954
|
}
|
|
@@ -795,11 +962,11 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
795
962
|
this.enableGrouping
|
|
796
963
|
&& session.draggable !== false
|
|
797
964
|
&& index >= 0
|
|
798
|
-
&& !globalThis.matchMedia?.('(pointer: coarse)').matches
|
|
799
965
|
);
|
|
800
966
|
const expanded = this.expandedSessionIds.has(session.id);
|
|
801
967
|
const state = session.state ?? 'normal';
|
|
802
968
|
const detailsId = `session-details-${session.id}`;
|
|
969
|
+
const dropIndex = index < 0 ? 0 : index;
|
|
803
970
|
return html`
|
|
804
971
|
<article
|
|
805
972
|
class="item state-${state} ${expanded ? 'expanded' : ''} ${
|
|
@@ -808,9 +975,11 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
808
975
|
this.draggingSessionId === session.id ? 'dragging' : ''
|
|
809
976
|
}"
|
|
810
977
|
data-session-id=${session.id}
|
|
811
|
-
|
|
978
|
+
data-drop-group-id=${groupId ?? ''}
|
|
979
|
+
data-drop-index=${dropIndex}
|
|
980
|
+
data-drop-kind=${index < 0 ? 'slot' : 'card'}
|
|
812
981
|
@dblclick=${(event: MouseEvent) => {
|
|
813
|
-
if (this.eventTargetsExpandToggle(event)) return;
|
|
982
|
+
if (this.eventTargetsExpandToggle(event) || this.eventTargetsDragHandle(event)) return;
|
|
814
983
|
this.emit('harness-session-open', session);
|
|
815
984
|
}}
|
|
816
985
|
@contextmenu=${(event: MouseEvent) => {
|
|
@@ -826,25 +995,31 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
826
995
|
new CustomEvent('harness-session-context', { detail, bubbles: true, composed: true }),
|
|
827
996
|
);
|
|
828
997
|
}}
|
|
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
998
|
>
|
|
847
|
-
<div class="itemHeader">
|
|
999
|
+
<div class="itemHeader ${draggable ? 'draggable' : ''}">
|
|
1000
|
+
${draggable
|
|
1001
|
+
? html`
|
|
1002
|
+
<button
|
|
1003
|
+
class="dragHandle"
|
|
1004
|
+
type="button"
|
|
1005
|
+
title="Drag to reorder; use arrow, Home, or End keys for keyboard reordering"
|
|
1006
|
+
aria-label=${`Reorder ${session.title || session.id}`}
|
|
1007
|
+
aria-keyshortcuts="ArrowUp ArrowDown Home End"
|
|
1008
|
+
@pointerdown=${(event: PointerEvent) => {
|
|
1009
|
+
this.handlePointerDown(event, session, groupId, index);
|
|
1010
|
+
}}
|
|
1011
|
+
@pointermove=${this.handlePointerMove}
|
|
1012
|
+
@pointerup=${this.handlePointerUp}
|
|
1013
|
+
@pointercancel=${this.handlePointerCancel}
|
|
1014
|
+
@lostpointercapture=${this.handleLostPointerCapture}
|
|
1015
|
+
@keydown=${(event: KeyboardEvent) => {
|
|
1016
|
+
this.handleKeyboardMove(event, session, groupId);
|
|
1017
|
+
}}
|
|
1018
|
+
>
|
|
1019
|
+
<dees-icon icon="lucide:GripVertical" iconSize="14"></dees-icon>
|
|
1020
|
+
</button>
|
|
1021
|
+
`
|
|
1022
|
+
: ''}
|
|
848
1023
|
<button
|
|
849
1024
|
class="itemPrimary"
|
|
850
1025
|
type="button"
|
|
@@ -906,6 +1081,12 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
906
1081
|
);
|
|
907
1082
|
}
|
|
908
1083
|
|
|
1084
|
+
private eventTargetsDragHandle(event: Event): boolean {
|
|
1085
|
+
return event.composedPath().some(
|
|
1086
|
+
(target) => target instanceof HTMLElement && target.classList.contains('dragHandle'),
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
909
1090
|
private sessionStateLabel(state: NonNullable<IHarnessSessionMeta['state']>): string {
|
|
910
1091
|
switch (state) {
|
|
911
1092
|
case 'working': return 'Working';
|
|
@@ -953,27 +1134,139 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
953
1134
|
.filter((session): session is IHarnessSessionMeta => session !== undefined);
|
|
954
1135
|
}
|
|
955
1136
|
|
|
956
|
-
private
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
draggable: boolean,
|
|
1137
|
+
private handlePointerDown(
|
|
1138
|
+
eventArg: PointerEvent,
|
|
1139
|
+
sessionArg: IHarnessSessionMeta,
|
|
1140
|
+
groupIdArg: string | null,
|
|
1141
|
+
logicalIndexArg: number,
|
|
962
1142
|
): void {
|
|
963
|
-
if (
|
|
964
|
-
|
|
1143
|
+
if (
|
|
1144
|
+
this.pointerDragGesture
|
|
1145
|
+
|| eventArg.button !== 0
|
|
1146
|
+
|| !eventArg.isPrimary
|
|
1147
|
+
|| logicalIndexArg < 0
|
|
1148
|
+
) return;
|
|
1149
|
+
const handle = eventArg.currentTarget as HTMLElement;
|
|
1150
|
+
const source = handle.closest<HTMLElement>('.item');
|
|
1151
|
+
if (!source) return;
|
|
1152
|
+
const sourceRect = source.getBoundingClientRect();
|
|
1153
|
+
this.pointerDragGesture = {
|
|
1154
|
+
pointerId: eventArg.pointerId,
|
|
1155
|
+
handle,
|
|
1156
|
+
source,
|
|
1157
|
+
sessionId: sessionArg.id,
|
|
1158
|
+
sourceGroupId: groupIdArg,
|
|
1159
|
+
sourceLogicalIndex: logicalIndexArg,
|
|
1160
|
+
startClientX: eventArg.clientX,
|
|
1161
|
+
startClientY: eventArg.clientY,
|
|
1162
|
+
grabOffsetX: eventArg.clientX - sourceRect.left,
|
|
1163
|
+
grabOffsetY: eventArg.clientY - sourceRect.top,
|
|
1164
|
+
phase: 'pending',
|
|
1165
|
+
};
|
|
1166
|
+
window.addEventListener('keydown', this.handleDragKeyDown, true);
|
|
1167
|
+
window.addEventListener('blur', this.handleDragWindowBlur);
|
|
1168
|
+
try {
|
|
1169
|
+
handle.setPointerCapture(eventArg.pointerId);
|
|
1170
|
+
} catch {
|
|
1171
|
+
this.finishPointerDrag(false);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1175
|
+
private readonly handlePointerMove = (eventArg: PointerEvent): void => {
|
|
1176
|
+
const gesture = this.pointerDragGesture;
|
|
1177
|
+
if (!gesture || eventArg.pointerId !== gesture.pointerId) return;
|
|
1178
|
+
if (!gesture.source.isConnected || !gesture.handle.isConnected) {
|
|
1179
|
+
this.finishPointerDrag(false);
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
if (
|
|
1183
|
+
gesture.phase === 'pending'
|
|
1184
|
+
&& Math.hypot(
|
|
1185
|
+
eventArg.clientX - gesture.startClientX,
|
|
1186
|
+
eventArg.clientY - gesture.startClientY,
|
|
1187
|
+
) < dragThresholdPixels
|
|
1188
|
+
) return;
|
|
1189
|
+
if (gesture.phase === 'pending' && !this.startPointerDrag(eventArg, gesture)) return;
|
|
1190
|
+
eventArg.preventDefault();
|
|
1191
|
+
this.moveDragPreview(eventArg.clientX, eventArg.clientY, gesture);
|
|
1192
|
+
this.dragAutoScrollClientX = eventArg.clientX;
|
|
1193
|
+
this.dragAutoScrollClientY = eventArg.clientY;
|
|
1194
|
+
this.updateDragAutoScrollVelocity();
|
|
1195
|
+
this.updateDropSlotFromPointer();
|
|
1196
|
+
};
|
|
1197
|
+
|
|
1198
|
+
private readonly handlePointerUp = (eventArg: PointerEvent): void => {
|
|
1199
|
+
const gesture = this.pointerDragGesture;
|
|
1200
|
+
if (!gesture || eventArg.pointerId !== gesture.pointerId) return;
|
|
1201
|
+
if (gesture.phase === 'active') {
|
|
1202
|
+
this.dragAutoScrollClientX = eventArg.clientX;
|
|
1203
|
+
this.dragAutoScrollClientY = eventArg.clientY;
|
|
1204
|
+
this.moveDragPreview(eventArg.clientX, eventArg.clientY, gesture);
|
|
1205
|
+
this.updateDropSlotFromPointer();
|
|
1206
|
+
this.finishPointerDrag(true);
|
|
965
1207
|
return;
|
|
966
1208
|
}
|
|
967
|
-
|
|
1209
|
+
this.finishPointerDrag(false);
|
|
1210
|
+
};
|
|
1211
|
+
|
|
1212
|
+
private readonly handlePointerCancel = (eventArg: PointerEvent): void => {
|
|
1213
|
+
if (eventArg.pointerId === this.pointerDragGesture?.pointerId) this.finishPointerDrag(false);
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
private readonly handleLostPointerCapture = (eventArg: PointerEvent): void => {
|
|
1217
|
+
if (eventArg.pointerId === this.pointerDragGesture?.pointerId) this.finishPointerDrag(false);
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1220
|
+
private readonly handleDragKeyDown = (eventArg: KeyboardEvent): void => {
|
|
1221
|
+
if (eventArg.key !== 'Escape' || !this.pointerDragGesture) return;
|
|
1222
|
+
eventArg.preventDefault();
|
|
1223
|
+
eventArg.stopPropagation();
|
|
1224
|
+
this.finishPointerDrag(false);
|
|
1225
|
+
};
|
|
1226
|
+
|
|
1227
|
+
private readonly handleDragWindowBlur = (): void => {
|
|
1228
|
+
this.finishPointerDrag(false);
|
|
1229
|
+
};
|
|
1230
|
+
|
|
1231
|
+
private startPointerDrag(eventArg: PointerEvent, gestureArg: IPointerDragGesture): boolean {
|
|
1232
|
+
const sourceRect = gestureArg.source.getBoundingClientRect();
|
|
1233
|
+
gestureArg.source.classList.add('dragging');
|
|
1234
|
+
gestureArg.source.style.setProperty('--drag-preview-width', `${sourceRect.width}px`);
|
|
1235
|
+
gestureArg.source.style.setProperty('--drag-preview-height', `${sourceRect.height}px`);
|
|
1236
|
+
this.moveDragPreview(eventArg.clientX, eventArg.clientY, gestureArg);
|
|
1237
|
+
gestureArg.source.setAttribute('popover', 'manual');
|
|
1238
|
+
try {
|
|
1239
|
+
gestureArg.source.showPopover();
|
|
1240
|
+
} catch {
|
|
1241
|
+
this.finishPointerDrag(false);
|
|
1242
|
+
return false;
|
|
1243
|
+
}
|
|
1244
|
+
gestureArg.phase = 'active';
|
|
968
1245
|
this.reflowGeneration += 1;
|
|
969
1246
|
this.cancelReflowAnimations();
|
|
970
|
-
this.draggedItemHeight = Math.ceil(
|
|
971
|
-
this.
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
1247
|
+
this.draggedItemHeight = Math.ceil(sourceRect.height);
|
|
1248
|
+
this.draggingSessionId = gestureArg.sessionId;
|
|
1249
|
+
const remainingCount = this.sortableSessions(this.sessionsForGroup(gestureArg.sourceGroupId)).length;
|
|
1250
|
+
this.dropSlot = {
|
|
1251
|
+
groupId: gestureArg.sourceGroupId,
|
|
1252
|
+
index: Math.min(gestureArg.sourceLogicalIndex, remainingCount),
|
|
1253
|
+
};
|
|
1254
|
+
return true;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
private moveDragPreview(
|
|
1258
|
+
clientXArg: number,
|
|
1259
|
+
clientYArg: number,
|
|
1260
|
+
gestureArg: IPointerDragGesture,
|
|
1261
|
+
): void {
|
|
1262
|
+
gestureArg.source.style.setProperty(
|
|
1263
|
+
'--drag-preview-x',
|
|
1264
|
+
`${clientXArg - gestureArg.grabOffsetX}px`,
|
|
1265
|
+
);
|
|
1266
|
+
gestureArg.source.style.setProperty(
|
|
1267
|
+
'--drag-preview-y',
|
|
1268
|
+
`${clientYArg - gestureArg.grabOffsetY}px`,
|
|
1269
|
+
);
|
|
977
1270
|
}
|
|
978
1271
|
|
|
979
1272
|
private setDropSlot(groupIdArg: string | null, indexArg: number): void {
|
|
@@ -985,38 +1278,239 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
985
1278
|
void this.animateCardReflow(previousRects, generation);
|
|
986
1279
|
}
|
|
987
1280
|
|
|
988
|
-
private
|
|
989
|
-
if (!this.
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
this.
|
|
1281
|
+
private clearDropSlot(): void {
|
|
1282
|
+
if (!this.dropSlot) return;
|
|
1283
|
+
const previousRects = this.captureCardRects();
|
|
1284
|
+
const generation = ++this.reflowGeneration;
|
|
1285
|
+
this.dropSlot = undefined;
|
|
1286
|
+
void this.animateCardReflow(previousRects, generation);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
private updateDragAutoScrollVelocity(): void {
|
|
1290
|
+
const container = this.scrollContainer;
|
|
1291
|
+
if (!container || !this.draggingSessionId) return;
|
|
1292
|
+
const rect = container.getBoundingClientRect();
|
|
1293
|
+
if (this.dragAutoScrollClientX < rect.left || this.dragAutoScrollClientX > rect.right) {
|
|
1294
|
+
this.stopDragAutoScroll();
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
const edgeSize = Math.min(64, Math.max(32, rect.height * 0.25));
|
|
1298
|
+
const topDepth = Math.max(
|
|
1299
|
+
0,
|
|
1300
|
+
Math.min(1, (rect.top + edgeSize - this.dragAutoScrollClientY) / edgeSize),
|
|
1301
|
+
);
|
|
1302
|
+
const bottomDepth = Math.max(
|
|
1303
|
+
0,
|
|
1304
|
+
Math.min(1, (this.dragAutoScrollClientY - (rect.bottom - edgeSize)) / edgeSize),
|
|
1305
|
+
);
|
|
1306
|
+
this.dragAutoScrollVelocity = bottomDepth > 0
|
|
1307
|
+
? dragAutoScrollMaxPixelsPerSecond * bottomDepth * bottomDepth
|
|
1308
|
+
: topDepth > 0
|
|
1309
|
+
? -dragAutoScrollMaxPixelsPerSecond * topDepth * topDepth
|
|
1310
|
+
: 0;
|
|
1311
|
+
if (this.dragAutoScrollVelocity === 0) {
|
|
1312
|
+
this.stopDragAutoScroll();
|
|
1313
|
+
return;
|
|
1314
|
+
}
|
|
1315
|
+
if (this.dragAutoScrollFrame === undefined) {
|
|
1316
|
+
this.dragAutoScrollLastFrame = performance.now();
|
|
1317
|
+
this.dragAutoScrollFrame = requestAnimationFrame(this.runDragAutoScroll);
|
|
1318
|
+
}
|
|
993
1319
|
}
|
|
994
1320
|
|
|
995
|
-
private
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1321
|
+
private readonly runDragAutoScroll = (timestampArg: number): void => {
|
|
1322
|
+
this.dragAutoScrollFrame = undefined;
|
|
1323
|
+
const container = this.scrollContainer;
|
|
1324
|
+
if (!container || !this.draggingSessionId || this.dragAutoScrollVelocity === 0) return;
|
|
1325
|
+
const elapsedMs = Math.min(34, Math.max(0, timestampArg - this.dragAutoScrollLastFrame));
|
|
1326
|
+
this.dragAutoScrollLastFrame = timestampArg;
|
|
1327
|
+
const previousScrollTop = container.scrollTop;
|
|
1328
|
+
container.scrollTop += this.dragAutoScrollVelocity * elapsedMs / 1000;
|
|
1329
|
+
this.syncScrollShadowState();
|
|
1330
|
+
const atScrollBoundary = this.dragAutoScrollVelocity < 0
|
|
1331
|
+
? container.scrollTop <= 0
|
|
1332
|
+
: container.scrollTop + container.clientHeight >= container.scrollHeight - 1;
|
|
1333
|
+
if (container.scrollTop === previousScrollTop && atScrollBoundary) {
|
|
1334
|
+
this.stopDragAutoScroll();
|
|
1001
1335
|
return;
|
|
1002
1336
|
}
|
|
1337
|
+
this.updateDropSlotFromPointer();
|
|
1338
|
+
this.dragAutoScrollFrame = requestAnimationFrame(this.runDragAutoScroll);
|
|
1339
|
+
};
|
|
1340
|
+
|
|
1341
|
+
private updateDropSlotFromPointer(): void {
|
|
1342
|
+
const pointerX = this.dragAutoScrollClientX;
|
|
1343
|
+
const pointerY = this.dragAutoScrollClientY;
|
|
1344
|
+
const placeholder = this.shadowRoot?.querySelector<HTMLElement>('.dropPlaceholder');
|
|
1345
|
+
if (placeholder) {
|
|
1346
|
+
const rect = placeholder.getBoundingClientRect();
|
|
1347
|
+
if (
|
|
1348
|
+
pointerX >= rect.left
|
|
1349
|
+
&& pointerX <= rect.right
|
|
1350
|
+
&& pointerY >= rect.top
|
|
1351
|
+
&& pointerY <= rect.bottom
|
|
1352
|
+
) {
|
|
1353
|
+
this.setDropSlot(
|
|
1354
|
+
placeholder.dataset.dropGroupId || null,
|
|
1355
|
+
Number(placeholder.dataset.dropIndex),
|
|
1356
|
+
);
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
const selector = '[data-drop-group-id][data-drop-index][data-drop-kind]';
|
|
1361
|
+
let target: HTMLElement | undefined;
|
|
1362
|
+
const offsets = this.dragAutoScrollVelocity === 0
|
|
1363
|
+
? [0, -8, 8, -16, 16, -32, 32]
|
|
1364
|
+
: [0, 8, 16, 32, 48, 64, 96].map(
|
|
1365
|
+
(value) => value * (this.dragAutoScrollVelocity < 0 ? 1 : -1),
|
|
1366
|
+
);
|
|
1367
|
+
for (const offset of offsets) {
|
|
1368
|
+
const hit = this.shadowRoot?.elementFromPoint(pointerX, pointerY + offset);
|
|
1369
|
+
const candidate = hit?.closest<HTMLElement>(selector);
|
|
1370
|
+
if (
|
|
1371
|
+
candidate
|
|
1372
|
+
&& candidate.dataset.sessionId !== this.draggingSessionId
|
|
1373
|
+
&& Number(candidate.dataset.dropIndex) >= 0
|
|
1374
|
+
) {
|
|
1375
|
+
target = candidate;
|
|
1376
|
+
break;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
if (!target) {
|
|
1380
|
+
this.clearDropSlot();
|
|
1381
|
+
return;
|
|
1382
|
+
}
|
|
1383
|
+
const baseIndex = Number(target.dataset.dropIndex);
|
|
1384
|
+
const groupId = target.dataset.dropGroupId || null;
|
|
1385
|
+
const rect = target.getBoundingClientRect();
|
|
1386
|
+
const index = target.dataset.dropKind === 'card' && pointerY >= rect.top + rect.height / 2
|
|
1387
|
+
? baseIndex + 1
|
|
1388
|
+
: baseIndex;
|
|
1389
|
+
this.setDropSlot(groupId, index);
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
private get scrollContainer(): HTMLElement | undefined {
|
|
1393
|
+
return this.shadowRoot?.querySelector<HTMLElement>('.list') ?? undefined;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
private observeScrollGeometry(): void {
|
|
1397
|
+
if (!this.isConnected) return;
|
|
1398
|
+
const container = this.scrollContainer;
|
|
1399
|
+
const content = this.shadowRoot?.querySelector<HTMLElement>('.listContent') ?? undefined;
|
|
1400
|
+
if (!container || !content) return;
|
|
1401
|
+
if (
|
|
1402
|
+
this.scrollResizeObserver
|
|
1403
|
+
&& this.observedScrollContainer === container
|
|
1404
|
+
&& this.observedScrollContent === content
|
|
1405
|
+
) return;
|
|
1406
|
+
this.scrollResizeObserver?.disconnect();
|
|
1407
|
+
this.scrollResizeObserver = new ResizeObserver(this.syncScrollShadowState);
|
|
1408
|
+
this.scrollResizeObserver.observe(container);
|
|
1409
|
+
this.scrollResizeObserver.observe(content);
|
|
1410
|
+
this.observedScrollContainer = container;
|
|
1411
|
+
this.observedScrollContent = content;
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
private readonly syncScrollShadowState = (): void => {
|
|
1415
|
+
const container = this.scrollContainer;
|
|
1416
|
+
if (!container) return;
|
|
1417
|
+
const topVisible = container.scrollTop > 1;
|
|
1418
|
+
const bottomVisible = container.scrollTop + container.clientHeight < container.scrollHeight - 1;
|
|
1419
|
+
if (this.topScrollShadowVisible !== topVisible) this.topScrollShadowVisible = topVisible;
|
|
1420
|
+
if (this.bottomScrollShadowVisible !== bottomVisible) this.bottomScrollShadowVisible = bottomVisible;
|
|
1421
|
+
};
|
|
1422
|
+
|
|
1423
|
+
private stopDragAutoScroll(): void {
|
|
1424
|
+
if (this.dragAutoScrollFrame !== undefined) cancelAnimationFrame(this.dragAutoScrollFrame);
|
|
1425
|
+
this.dragAutoScrollFrame = undefined;
|
|
1426
|
+
this.dragAutoScrollVelocity = 0;
|
|
1427
|
+
this.dragAutoScrollLastFrame = 0;
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
private createMoveDetail(
|
|
1431
|
+
sessionIdArg: string,
|
|
1432
|
+
groupIdArg: string | null,
|
|
1433
|
+
indexArg: number,
|
|
1434
|
+
): IHarnessSessionMoveDetail {
|
|
1003
1435
|
const targetSessions = this.sortableSessions(this.sessionsForGroup(groupIdArg));
|
|
1004
1436
|
const beforeSessionId = targetSessions[Math.max(0, indexArg)]?.id;
|
|
1005
|
-
const sourceGroupId = this.groups.find((group) => group.sessionIds.includes(
|
|
1437
|
+
const sourceGroupId = this.groups.find((group) => group.sessionIds.includes(sessionIdArg))?.id ?? null;
|
|
1006
1438
|
const sourceSessions = this.sortableSessions(this.sessionsForGroup(sourceGroupId), false);
|
|
1007
|
-
const sourceIndex = sourceSessions.findIndex((session) => session.id ===
|
|
1439
|
+
const sourceIndex = sourceSessions.findIndex((session) => session.id === sessionIdArg);
|
|
1008
1440
|
const compatibilityIndex = (
|
|
1009
1441
|
sourceGroupId === groupIdArg
|
|
1010
1442
|
&& sourceIndex >= 0
|
|
1011
1443
|
&& indexArg > sourceIndex
|
|
1012
1444
|
) ? indexArg + 1 : indexArg;
|
|
1013
|
-
|
|
1014
|
-
sessionId,
|
|
1445
|
+
return {
|
|
1446
|
+
sessionId: sessionIdArg,
|
|
1015
1447
|
groupId: groupIdArg,
|
|
1016
1448
|
index: Math.max(0, compatibilityIndex),
|
|
1017
1449
|
...(beforeSessionId ? { beforeSessionId } : {}),
|
|
1018
1450
|
};
|
|
1019
|
-
|
|
1451
|
+
}
|
|
1452
|
+
|
|
1453
|
+
private handleKeyboardMove(
|
|
1454
|
+
eventArg: KeyboardEvent,
|
|
1455
|
+
sessionArg: IHarnessSessionMeta,
|
|
1456
|
+
groupIdArg: string | null,
|
|
1457
|
+
): void {
|
|
1458
|
+
if (!['ArrowUp', 'ArrowDown', 'Home', 'End'].includes(eventArg.key)) return;
|
|
1459
|
+
eventArg.preventDefault();
|
|
1460
|
+
eventArg.stopPropagation();
|
|
1461
|
+
const sessions = this.sortableSessions(this.sessionsForGroup(groupIdArg), false);
|
|
1462
|
+
const sourceIndex = sessions.findIndex((session) => session.id === sessionArg.id);
|
|
1463
|
+
if (sourceIndex < 0) return;
|
|
1464
|
+
const sectionIds: Array<string | null> = [...this.groups.map((group) => group.id), null];
|
|
1465
|
+
const sectionIndex = sectionIds.indexOf(groupIdArg);
|
|
1466
|
+
const adjacentGroupId = eventArg.key === 'ArrowUp' && sourceIndex === 0 && sectionIndex > 0
|
|
1467
|
+
? sectionIds[sectionIndex - 1]
|
|
1468
|
+
: eventArg.key === 'ArrowDown'
|
|
1469
|
+
&& sourceIndex === sessions.length - 1
|
|
1470
|
+
&& sectionIndex >= 0
|
|
1471
|
+
&& sectionIndex < sectionIds.length - 1
|
|
1472
|
+
? sectionIds[sectionIndex + 1]
|
|
1473
|
+
: undefined;
|
|
1474
|
+
if (adjacentGroupId !== undefined) {
|
|
1475
|
+
const targetSessions = this.sortableSessions(this.sessionsForGroup(adjacentGroupId), false);
|
|
1476
|
+
const targetIndex = eventArg.key === 'ArrowUp' ? targetSessions.length : 0;
|
|
1477
|
+
const beforeSession = targetSessions[targetIndex];
|
|
1478
|
+
const detail: IHarnessSessionMoveDetail = {
|
|
1479
|
+
sessionId: sessionArg.id,
|
|
1480
|
+
groupId: adjacentGroupId,
|
|
1481
|
+
index: targetIndex,
|
|
1482
|
+
...(beforeSession ? { beforeSessionId: beforeSession.id } : {}),
|
|
1483
|
+
};
|
|
1484
|
+
const groupName = adjacentGroupId === null
|
|
1485
|
+
? 'Ungrouped'
|
|
1486
|
+
: this.groups.find((group) => group.id === adjacentGroupId)?.name ?? adjacentGroupId;
|
|
1487
|
+
this.moveAnnouncement = `Moved ${sessionArg.title || sessionArg.id} to ${groupName}.`;
|
|
1488
|
+
this.keyboardMoveFocus = { sessionId: sessionArg.id, groupId: adjacentGroupId };
|
|
1489
|
+
this.dispatchEvent(
|
|
1490
|
+
new CustomEvent('harness-session-move', { detail, bubbles: true, composed: true }),
|
|
1491
|
+
);
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
const remaining = sessions.filter((session) => session.id !== sessionArg.id);
|
|
1495
|
+
const targetIndex = eventArg.key === 'Home'
|
|
1496
|
+
? 0
|
|
1497
|
+
: eventArg.key === 'End'
|
|
1498
|
+
? remaining.length
|
|
1499
|
+
: eventArg.key === 'ArrowUp'
|
|
1500
|
+
? Math.max(0, sourceIndex - 1)
|
|
1501
|
+
: Math.min(remaining.length, sourceIndex + 1);
|
|
1502
|
+
if (targetIndex === sourceIndex) return;
|
|
1503
|
+
const beforeSession = remaining[targetIndex];
|
|
1504
|
+
const compatibilityIndex = targetIndex > sourceIndex ? targetIndex + 1 : targetIndex;
|
|
1505
|
+
const detail: IHarnessSessionMoveDetail = {
|
|
1506
|
+
sessionId: sessionArg.id,
|
|
1507
|
+
groupId: groupIdArg,
|
|
1508
|
+
index: compatibilityIndex,
|
|
1509
|
+
...(beforeSession ? { beforeSessionId: beforeSession.id } : {}),
|
|
1510
|
+
};
|
|
1511
|
+
this.moveAnnouncement = beforeSession
|
|
1512
|
+
? `Moved ${sessionArg.title || sessionArg.id} before ${beforeSession.title || beforeSession.id}.`
|
|
1513
|
+
: `Moved ${sessionArg.title || sessionArg.id} to the end.`;
|
|
1020
1514
|
this.dispatchEvent(
|
|
1021
1515
|
new CustomEvent('harness-session-move', { detail, bubbles: true, composed: true }),
|
|
1022
1516
|
);
|
|
@@ -1031,6 +1525,31 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1031
1525
|
return result;
|
|
1032
1526
|
}
|
|
1033
1527
|
|
|
1528
|
+
private restoreKeyboardMoveFocus(): void {
|
|
1529
|
+
const pending = this.keyboardMoveFocus;
|
|
1530
|
+
if (!pending) return;
|
|
1531
|
+
const currentGroupId = this.groups.find(
|
|
1532
|
+
(group) => group.sessionIds.includes(pending.sessionId),
|
|
1533
|
+
)?.id ?? null;
|
|
1534
|
+
if (currentGroupId !== pending.groupId) return;
|
|
1535
|
+
const item = Array.from(
|
|
1536
|
+
this.shadowRoot?.querySelectorAll<HTMLElement>('.item[data-session-id]') ?? [],
|
|
1537
|
+
).find((candidate) => candidate.dataset.sessionId === pending.sessionId);
|
|
1538
|
+
const handle = item?.querySelector<HTMLElement>('.dragHandle');
|
|
1539
|
+
if (handle) {
|
|
1540
|
+
handle.focus();
|
|
1541
|
+
this.keyboardMoveFocus = undefined;
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
const groupHeader = Array.from(
|
|
1545
|
+
this.shadowRoot?.querySelectorAll<HTMLElement>('.groupHeader[data-drop-group-id]') ?? [],
|
|
1546
|
+
).find((candidate) => candidate.dataset.dropGroupId === (pending.groupId ?? ''));
|
|
1547
|
+
if (groupHeader) {
|
|
1548
|
+
groupHeader.focus();
|
|
1549
|
+
this.keyboardMoveFocus = undefined;
|
|
1550
|
+
}
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1034
1553
|
private async animateCardReflow(
|
|
1035
1554
|
previousRectsArg: Map<string, DOMRect>,
|
|
1036
1555
|
generationArg: number,
|
|
@@ -1069,13 +1588,42 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
1069
1588
|
this.reflowAnimations.clear();
|
|
1070
1589
|
}
|
|
1071
1590
|
|
|
1072
|
-
private
|
|
1591
|
+
private finishPointerDrag(commitArg: boolean): void {
|
|
1592
|
+
const gesture = this.pointerDragGesture;
|
|
1593
|
+
if (!gesture) return;
|
|
1594
|
+
const detail = commitArg && gesture.phase === 'active' && this.dropSlot
|
|
1595
|
+
? this.createMoveDetail(gesture.sessionId, this.dropSlot.groupId, this.dropSlot.index)
|
|
1596
|
+
: undefined;
|
|
1597
|
+
this.pointerDragGesture = undefined;
|
|
1598
|
+
window.removeEventListener('keydown', this.handleDragKeyDown, true);
|
|
1599
|
+
window.removeEventListener('blur', this.handleDragWindowBlur);
|
|
1600
|
+
this.stopDragAutoScroll();
|
|
1073
1601
|
this.reflowGeneration += 1;
|
|
1074
1602
|
this.cancelReflowAnimations();
|
|
1603
|
+
if (gesture.source.matches(':popover-open')) gesture.source.hidePopover();
|
|
1604
|
+
gesture.source.removeAttribute('popover');
|
|
1605
|
+
gesture.source.classList.remove('dragging');
|
|
1606
|
+
for (const property of [
|
|
1607
|
+
'--drag-preview-width',
|
|
1608
|
+
'--drag-preview-height',
|
|
1609
|
+
'--drag-preview-x',
|
|
1610
|
+
'--drag-preview-y',
|
|
1611
|
+
]) gesture.source.style.removeProperty(property);
|
|
1075
1612
|
this.draggingSessionId = '';
|
|
1076
1613
|
this.draggedItemHeight = 0;
|
|
1077
1614
|
this.dropSlot = undefined;
|
|
1078
|
-
|
|
1615
|
+
try {
|
|
1616
|
+
if (gesture.handle.hasPointerCapture(gesture.pointerId)) {
|
|
1617
|
+
gesture.handle.releasePointerCapture(gesture.pointerId);
|
|
1618
|
+
}
|
|
1619
|
+
} catch {
|
|
1620
|
+
// Capture may already be released or the handle may have disconnected.
|
|
1621
|
+
}
|
|
1622
|
+
if (detail) {
|
|
1623
|
+
this.dispatchEvent(
|
|
1624
|
+
new CustomEvent('harness-session-move', { detail, bubbles: true, composed: true }),
|
|
1625
|
+
);
|
|
1626
|
+
}
|
|
1079
1627
|
}
|
|
1080
1628
|
|
|
1081
1629
|
private emit(name: string, session: IHarnessSessionMeta): void {
|