@design.estate/dees-catalog 4.9.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 +4965 -2314
- package/dist_ts_web/00_commitinfo_data.js +2 -2
- package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.d.ts +56 -4
- package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.demo.js +35 -2
- package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.js +1107 -127
- package/dist_ts_web/elements/00group-harness/interfaces.d.ts +10 -1
- package/package.json +5 -5
- package/readme.md +3 -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 +45 -2
- package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.ts +1194 -126
- package/ts_web/elements/00group-harness/interfaces.ts +11 -1
- package/dist_watch/bundle.js +0 -235637
- package/dist_watch/bundle.js.map +0 -7
- package/dist_watch/index.html +0 -28
package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.ts
CHANGED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
property,
|
|
9
9
|
state,
|
|
10
10
|
cssManager,
|
|
11
|
+
directives,
|
|
11
12
|
} from '@design.estate/dees-element';
|
|
12
13
|
import { themeDefaultStyles } from '../../00theme.js';
|
|
13
14
|
import type {
|
|
@@ -20,6 +21,10 @@ import type {
|
|
|
20
21
|
import { harnessFormatRelativeTime } from '../harness.helpers.js';
|
|
21
22
|
import '../../00group-utility/dees-icon/dees-icon.js';
|
|
22
23
|
|
|
24
|
+
const { repeat } = directives;
|
|
25
|
+
const dragAutoScrollMaxPixelsPerSecond = 900;
|
|
26
|
+
const dragThresholdPixels = 4;
|
|
27
|
+
|
|
23
28
|
declare global {
|
|
24
29
|
interface HTMLElementTagNameMap {
|
|
25
30
|
'dees-harness-session-list': DeesHarnessSessionList;
|
|
@@ -31,6 +36,29 @@ interface IDropSlot {
|
|
|
31
36
|
index: number;
|
|
32
37
|
}
|
|
33
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
|
+
|
|
58
|
+
type TSectionRenderEntry =
|
|
59
|
+
| { type: 'session'; key: string; session: IHarnessSessionMeta; logicalIndex: number }
|
|
60
|
+
| { type: 'placeholder'; key: string };
|
|
61
|
+
|
|
34
62
|
/**
|
|
35
63
|
* Searchable conversation list, embeddable in a sidebar or a DeesModal.
|
|
36
64
|
* Emits `harness-session-select` on click, `harness-session-open` on
|
|
@@ -43,7 +71,8 @@ interface IDropSlot {
|
|
|
43
71
|
* `harness-group-create-request`, and right-clicking a group header emits
|
|
44
72
|
* `harness-group-context` — the host persists changes and passes fresh
|
|
45
73
|
* `groups` back in. Sessions in no group render in the trailing ungrouped
|
|
46
|
-
* section
|
|
74
|
+
* section with pinned entries first, then explicit order, then new sessions by
|
|
75
|
+
* recency. While a search query is active the list is flat.
|
|
47
76
|
*/
|
|
48
77
|
@customElement('dees-harness-session-list')
|
|
49
78
|
export class DeesHarnessSessionList extends DeesElement {
|
|
@@ -68,6 +97,10 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
68
97
|
@property({ type: Boolean })
|
|
69
98
|
accessor enableGrouping: boolean = false;
|
|
70
99
|
|
|
100
|
+
/** Explicit order for sortable sessions outside user-defined groups. */
|
|
101
|
+
@property({ attribute: false })
|
|
102
|
+
accessor ungroupedSessionIds: string[] = [];
|
|
103
|
+
|
|
71
104
|
@state()
|
|
72
105
|
accessor search: string = '';
|
|
73
106
|
|
|
@@ -77,9 +110,37 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
77
110
|
@state()
|
|
78
111
|
accessor dropSlot: IDropSlot | undefined = undefined;
|
|
79
112
|
|
|
113
|
+
@state()
|
|
114
|
+
accessor draggedItemHeight = 0;
|
|
115
|
+
|
|
116
|
+
@state()
|
|
117
|
+
accessor expandedSessionIds: ReadonlySet<string> = new Set<string>();
|
|
118
|
+
|
|
80
119
|
@state()
|
|
81
120
|
accessor collapsedGroupIds: ReadonlySet<string> = new Set<string>();
|
|
82
121
|
|
|
122
|
+
@state()
|
|
123
|
+
private accessor topScrollShadowVisible = false;
|
|
124
|
+
|
|
125
|
+
@state()
|
|
126
|
+
private accessor bottomScrollShadowVisible = false;
|
|
127
|
+
|
|
128
|
+
@state()
|
|
129
|
+
private accessor moveAnnouncement = '';
|
|
130
|
+
|
|
131
|
+
private reflowGeneration = 0;
|
|
132
|
+
private readonly reflowAnimations = new Set<Animation>();
|
|
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;
|
|
143
|
+
|
|
83
144
|
public static styles = [
|
|
84
145
|
themeDefaultStyles,
|
|
85
146
|
cssManager.defaultStyles,
|
|
@@ -89,26 +150,48 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
89
150
|
flex-direction: column;
|
|
90
151
|
min-height: 0;
|
|
91
152
|
font-family: var(--dees-font-family);
|
|
153
|
+
--session-normal: var(--dees-color-text-muted);
|
|
154
|
+
--session-working: var(--dees-color-accent-primary);
|
|
155
|
+
--session-finished: var(--dees-color-accent-success);
|
|
156
|
+
--session-attention: #bf5af2;
|
|
157
|
+
--session-error: var(--dees-color-accent-error);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
:host([gobright]) {
|
|
161
|
+
--session-attention: #af52de;
|
|
92
162
|
}
|
|
93
163
|
|
|
94
164
|
.searchRow {
|
|
165
|
+
position: relative;
|
|
95
166
|
flex: 0 0 auto;
|
|
96
|
-
padding: var(--dees-spacing-sm);
|
|
167
|
+
padding: var(--dees-spacing-sm) var(--dees-spacing-md) var(--dees-spacing-md);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.searchIcon {
|
|
171
|
+
position: absolute;
|
|
172
|
+
top: 50%;
|
|
173
|
+
left: calc(var(--dees-spacing-md) + 10px);
|
|
174
|
+
color: var(--dees-color-text-muted);
|
|
175
|
+
transform: translateY(-62%);
|
|
176
|
+
pointer-events: none;
|
|
97
177
|
}
|
|
98
178
|
|
|
99
179
|
.searchRow input {
|
|
100
180
|
width: 100%;
|
|
101
|
-
height: var(--dees-control-height-
|
|
102
|
-
padding: 0 var(--dees-spacing-md);
|
|
181
|
+
height: var(--dees-control-height-lg);
|
|
182
|
+
padding: 0 var(--dees-spacing-md) 0 34px;
|
|
103
183
|
border: 1px solid var(--dees-color-border-default);
|
|
104
|
-
border-radius: var(--dees-radius-
|
|
184
|
+
border-radius: var(--dees-radius-lg);
|
|
105
185
|
corner-shape: var(--dees-corner-shape);
|
|
106
|
-
background: var(--dees-color-bg-primary);
|
|
186
|
+
background: color-mix(in srgb, var(--dees-color-bg-primary) 78%, transparent);
|
|
107
187
|
color: var(--dees-color-text-primary);
|
|
108
188
|
font-family: inherit;
|
|
109
|
-
font-size: var(--dees-font-control-size,
|
|
189
|
+
font-size: var(--dees-font-control-size-sm, 12px);
|
|
110
190
|
outline: none;
|
|
111
191
|
box-sizing: border-box;
|
|
192
|
+
box-shadow: var(--dees-shadow-xs);
|
|
193
|
+
transition: border-color var(--dees-transition-fast) var(--dees-ease-standard),
|
|
194
|
+
box-shadow var(--dees-transition-fast) var(--dees-ease-standard);
|
|
112
195
|
}
|
|
113
196
|
|
|
114
197
|
.searchRow input:focus {
|
|
@@ -117,51 +200,253 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
117
200
|
}
|
|
118
201
|
|
|
119
202
|
.list {
|
|
120
|
-
|
|
121
|
-
|
|
203
|
+
width: 100%;
|
|
204
|
+
height: 100%;
|
|
122
205
|
overflow-y: auto;
|
|
123
206
|
scrollbar-width: thin;
|
|
124
207
|
scrollbar-color: var(--dees-color-scrollbar-thumb) transparent;
|
|
125
|
-
padding: 0 var(--dees-spacing-
|
|
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);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.section {
|
|
254
|
+
position: relative;
|
|
126
255
|
display: grid;
|
|
127
|
-
gap:
|
|
256
|
+
gap: var(--dees-spacing-sm);
|
|
128
257
|
align-content: start;
|
|
129
258
|
}
|
|
130
259
|
|
|
131
260
|
.item {
|
|
261
|
+
--card-accent: var(--session-normal);
|
|
262
|
+
position: relative;
|
|
132
263
|
width: 100%;
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
border:
|
|
136
|
-
border-radius: var(--dees-radius-
|
|
264
|
+
min-width: 0;
|
|
265
|
+
overflow: hidden;
|
|
266
|
+
border: 1px solid color-mix(in srgb, var(--card-accent) 24%, var(--dees-color-border-subtle));
|
|
267
|
+
border-radius: var(--dees-radius-xl);
|
|
137
268
|
corner-shape: var(--dees-corner-shape);
|
|
138
|
-
background: transparent;
|
|
269
|
+
background: color-mix(in srgb, var(--dees-color-bg-primary) 90%, transparent);
|
|
139
270
|
color: var(--dees-color-text-primary);
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
271
|
+
box-shadow:
|
|
272
|
+
var(--dees-shadow-sm),
|
|
273
|
+
inset 3px 0 0 var(--card-accent);
|
|
274
|
+
box-sizing: border-box;
|
|
275
|
+
transition:
|
|
276
|
+
border-color var(--dees-transition-fast) var(--dees-ease-standard),
|
|
277
|
+
background var(--dees-transition-fast) var(--dees-ease-standard),
|
|
278
|
+
box-shadow var(--dees-transition-fast) var(--dees-ease-standard);
|
|
144
279
|
}
|
|
145
280
|
|
|
146
281
|
.item:hover {
|
|
147
|
-
|
|
282
|
+
border-color: color-mix(in srgb, var(--card-accent) 42%, var(--dees-color-border-default));
|
|
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);
|
|
148
287
|
}
|
|
149
288
|
|
|
150
289
|
.item.selected {
|
|
151
|
-
|
|
152
|
-
|
|
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;
|
|
153
306
|
}
|
|
154
307
|
|
|
155
308
|
.item.dragging {
|
|
156
|
-
|
|
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;
|
|
317
|
+
pointer-events: none;
|
|
318
|
+
transform: translate3d(var(--drag-preview-x), var(--drag-preview-y), 0);
|
|
319
|
+
box-shadow: var(--dees-shadow-xl);
|
|
157
320
|
}
|
|
158
321
|
|
|
159
|
-
.
|
|
322
|
+
.item.state-working {
|
|
323
|
+
--card-accent: var(--session-working);
|
|
324
|
+
}
|
|
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
|
+
|
|
341
|
+
.item.state-finished {
|
|
342
|
+
--card-accent: var(--session-finished);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.item.state-attention {
|
|
346
|
+
--card-accent: var(--session-attention);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
.item.state-error {
|
|
350
|
+
--card-accent: var(--session-error);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
@keyframes attentionPulse {
|
|
354
|
+
0%, 100% {
|
|
355
|
+
background: color-mix(in srgb, var(--dees-color-bg-primary) 94%, var(--session-attention) 6%);
|
|
356
|
+
}
|
|
357
|
+
50% {
|
|
358
|
+
background: color-mix(in srgb, var(--dees-color-bg-primary) 84%, var(--session-attention) 16%);
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.item.state-attention:not(.selected) {
|
|
363
|
+
animation: attentionPulse 1.15s ease-in-out infinite;
|
|
364
|
+
}
|
|
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
|
+
|
|
371
|
+
.itemHeader {
|
|
372
|
+
display: grid;
|
|
373
|
+
grid-template-columns: minmax(0, 1fr) 30px;
|
|
374
|
+
min-height: 46px;
|
|
375
|
+
}
|
|
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
|
+
|
|
403
|
+
.itemPrimary,
|
|
404
|
+
.expandToggle {
|
|
405
|
+
appearance: none;
|
|
406
|
+
border: 0;
|
|
407
|
+
background: transparent;
|
|
408
|
+
color: inherit;
|
|
409
|
+
font-family: inherit;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
.itemPrimary {
|
|
160
413
|
display: flex;
|
|
161
|
-
|
|
414
|
+
min-width: 0;
|
|
415
|
+
align-items: center;
|
|
162
416
|
gap: var(--dees-spacing-sm);
|
|
417
|
+
padding: var(--dees-spacing-sm) var(--dees-spacing-xs) var(--dees-spacing-sm) var(--dees-spacing-md);
|
|
418
|
+
text-align: left;
|
|
419
|
+
cursor: pointer;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
.itemPrimary:focus-visible,
|
|
423
|
+
.expandToggle:focus-visible {
|
|
424
|
+
outline: 2px solid var(--dees-color-accent-primary);
|
|
425
|
+
outline-offset: -3px;
|
|
426
|
+
border-radius: 9px;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
.stateDot {
|
|
430
|
+
width: 8px;
|
|
431
|
+
height: 8px;
|
|
432
|
+
flex: 0 0 auto;
|
|
433
|
+
border-radius: 50%;
|
|
434
|
+
background: var(--card-accent);
|
|
435
|
+
box-shadow: 0 0 0 3px color-mix(in srgb, var(--card-accent) 15%, transparent);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
.itemCopy {
|
|
439
|
+
min-width: 0;
|
|
440
|
+
flex: 1;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.itemTitle {
|
|
444
|
+
display: flex;
|
|
445
|
+
min-width: 0;
|
|
446
|
+
align-items: center;
|
|
447
|
+
gap: 6px;
|
|
163
448
|
font-size: var(--dees-font-control-size-sm, 12px);
|
|
164
|
-
font-weight:
|
|
449
|
+
font-weight: 590;
|
|
165
450
|
}
|
|
166
451
|
|
|
167
452
|
.itemTitle .titleText {
|
|
@@ -173,24 +458,99 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
173
458
|
|
|
174
459
|
.itemTitle .itemIcon {
|
|
175
460
|
flex: 0 0 auto;
|
|
176
|
-
align-self: center;
|
|
177
461
|
color: var(--dees-color-text-secondary);
|
|
178
462
|
}
|
|
179
463
|
|
|
180
464
|
.itemTime {
|
|
181
|
-
margin-
|
|
182
|
-
flex: 0 0 auto;
|
|
465
|
+
margin-top: 2px;
|
|
183
466
|
color: var(--dees-color-text-muted);
|
|
184
467
|
font-size: var(--dees-font-caption2-size, 10px);
|
|
185
468
|
}
|
|
186
469
|
|
|
470
|
+
.expandToggle {
|
|
471
|
+
display: grid;
|
|
472
|
+
place-items: center;
|
|
473
|
+
margin: 7px 7px 7px 0;
|
|
474
|
+
border-radius: 9px;
|
|
475
|
+
color: var(--dees-color-text-muted);
|
|
476
|
+
cursor: pointer;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
.expandToggle:hover {
|
|
480
|
+
background: color-mix(in srgb, var(--card-accent) 11%, transparent);
|
|
481
|
+
color: var(--dees-color-text-primary);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
.expandToggle dees-icon {
|
|
485
|
+
transition: transform var(--dees-transition-fast) var(--dees-ease-standard);
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
.item.expanded .expandToggle dees-icon {
|
|
489
|
+
transform: rotate(180deg);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
.itemDetails {
|
|
493
|
+
display: grid;
|
|
494
|
+
gap: 6px;
|
|
495
|
+
margin: 0 10px 10px 28px;
|
|
496
|
+
padding: 9px 10px;
|
|
497
|
+
border-top: 1px solid color-mix(in srgb, var(--card-accent) 20%, var(--dees-color-border-subtle));
|
|
498
|
+
color: var(--dees-color-text-secondary);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
.stateLine {
|
|
502
|
+
display: flex;
|
|
503
|
+
align-items: center;
|
|
504
|
+
gap: 6px;
|
|
505
|
+
color: var(--card-accent);
|
|
506
|
+
font-size: var(--dees-font-caption1-size, 11px);
|
|
507
|
+
font-weight: 650;
|
|
508
|
+
}
|
|
509
|
+
|
|
187
510
|
.itemPreview {
|
|
188
|
-
margin-top: 1px;
|
|
189
511
|
color: var(--dees-color-text-secondary);
|
|
190
512
|
font-size: var(--dees-font-caption1-size, 11px);
|
|
191
513
|
overflow: hidden;
|
|
192
|
-
|
|
514
|
+
display: -webkit-box;
|
|
515
|
+
-webkit-line-clamp: 2;
|
|
516
|
+
-webkit-box-orient: vertical;
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
.itemMeta {
|
|
520
|
+
color: var(--dees-color-text-muted);
|
|
521
|
+
font-size: var(--dees-font-caption2-size, 10px);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
.visuallyHidden {
|
|
525
|
+
position: absolute;
|
|
526
|
+
width: 1px;
|
|
527
|
+
height: 1px;
|
|
528
|
+
padding: 0;
|
|
529
|
+
margin: -1px;
|
|
530
|
+
overflow: hidden;
|
|
531
|
+
clip: rect(0, 0, 0, 0);
|
|
193
532
|
white-space: nowrap;
|
|
533
|
+
border: 0;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.dropPlaceholder,
|
|
537
|
+
.emptyDropZone {
|
|
538
|
+
display: grid;
|
|
539
|
+
place-items: center;
|
|
540
|
+
min-height: 46px;
|
|
541
|
+
border: 1px dashed color-mix(in srgb, var(--dees-color-accent-primary) 68%, transparent);
|
|
542
|
+
border-radius: var(--dees-radius-xl);
|
|
543
|
+
background: color-mix(in srgb, var(--dees-color-accent-primary) 8%, transparent);
|
|
544
|
+
color: var(--dees-color-accent-primary);
|
|
545
|
+
box-sizing: border-box;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
.dropPlaceholder::after,
|
|
549
|
+
.emptyDropZone::after {
|
|
550
|
+
content: 'Drop conversation here';
|
|
551
|
+
font-size: var(--dees-font-caption2-size, 10px);
|
|
552
|
+
font-weight: 650;
|
|
553
|
+
letter-spacing: 0.02em;
|
|
194
554
|
}
|
|
195
555
|
|
|
196
556
|
.emptyNote {
|
|
@@ -205,8 +565,8 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
205
565
|
align-items: center;
|
|
206
566
|
gap: var(--dees-spacing-xs);
|
|
207
567
|
width: 100%;
|
|
208
|
-
margin
|
|
209
|
-
padding:
|
|
568
|
+
margin: var(--dees-spacing-md) 0 7px;
|
|
569
|
+
padding: var(--dees-spacing-xs) 6px;
|
|
210
570
|
border: 0;
|
|
211
571
|
border-radius: var(--dees-radius-md);
|
|
212
572
|
background: transparent;
|
|
@@ -246,15 +606,12 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
246
606
|
}
|
|
247
607
|
|
|
248
608
|
.dropLine {
|
|
249
|
-
|
|
250
|
-
margin: 0 var(--dees-spacing-sm);
|
|
251
|
-
border-radius: 1px;
|
|
252
|
-
background: var(--dees-color-accent-primary);
|
|
609
|
+
display: none;
|
|
253
610
|
}
|
|
254
611
|
|
|
255
612
|
.newGroupRow {
|
|
256
613
|
flex: 0 0 auto;
|
|
257
|
-
padding:
|
|
614
|
+
padding: 0 var(--dees-spacing-md) var(--dees-spacing-md);
|
|
258
615
|
}
|
|
259
616
|
|
|
260
617
|
.newGroupButton {
|
|
@@ -262,10 +619,12 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
262
619
|
align-items: center;
|
|
263
620
|
gap: var(--dees-spacing-xs);
|
|
264
621
|
width: 100%;
|
|
622
|
+
min-height: 34px;
|
|
623
|
+
justify-content: center;
|
|
265
624
|
padding: var(--dees-spacing-xs) var(--dees-spacing-sm);
|
|
266
625
|
border: 1px dashed var(--dees-color-border-default);
|
|
267
|
-
border-radius:
|
|
268
|
-
background: transparent;
|
|
626
|
+
border-radius: 11px;
|
|
627
|
+
background: color-mix(in srgb, var(--dees-color-bg-primary) 55%, transparent);
|
|
269
628
|
color: var(--dees-color-text-secondary);
|
|
270
629
|
cursor: pointer;
|
|
271
630
|
font-family: inherit;
|
|
@@ -276,6 +635,17 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
276
635
|
border-color: var(--dees-color-accent-primary);
|
|
277
636
|
color: var(--dees-color-text-primary);
|
|
278
637
|
}
|
|
638
|
+
|
|
639
|
+
@media (prefers-reduced-motion: reduce) {
|
|
640
|
+
.item,
|
|
641
|
+
.item.state-attention,
|
|
642
|
+
.item.state-working .itemHeader .stateDot,
|
|
643
|
+
.scrollShadow,
|
|
644
|
+
.expandToggle dees-icon {
|
|
645
|
+
animation: none;
|
|
646
|
+
transition: none;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
279
649
|
`,
|
|
280
650
|
];
|
|
281
651
|
|
|
@@ -291,15 +661,26 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
291
661
|
return ids;
|
|
292
662
|
}
|
|
293
663
|
|
|
294
|
-
/**
|
|
664
|
+
/** Pinned entries first, followed by explicitly ordered sortable sessions. */
|
|
295
665
|
private get ungroupedSessions(): IHarnessSessionMeta[] {
|
|
296
666
|
const grouped = this.groupedSessionIds;
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
667
|
+
const available = this.sessions.filter((session) => !grouped.has(session.id));
|
|
668
|
+
const pinned = available.filter((session) => session.draggable === false);
|
|
669
|
+
const sortableById = new Map(
|
|
670
|
+
available.filter((session) => session.draggable !== false).map((session) => [session.id, session]),
|
|
671
|
+
);
|
|
672
|
+
const ordered: IHarnessSessionMeta[] = [];
|
|
673
|
+
for (const sessionId of this.ungroupedSessionIds) {
|
|
674
|
+
const session = sortableById.get(sessionId);
|
|
675
|
+
if (!session) continue;
|
|
676
|
+
ordered.push(session);
|
|
677
|
+
sortableById.delete(sessionId);
|
|
678
|
+
}
|
|
679
|
+
const newSessions = [...sortableById.values()].sort(
|
|
680
|
+
(left, right) =>
|
|
681
|
+
(right.updatedAt ?? right.createdAt ?? 0) - (left.updatedAt ?? left.createdAt ?? 0),
|
|
682
|
+
);
|
|
683
|
+
return [...pinned, ...ordered, ...newSessions];
|
|
303
684
|
}
|
|
304
685
|
|
|
305
686
|
private get filteredSessions(): IHarnessSessionMeta[] {
|
|
@@ -317,10 +698,63 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
317
698
|
this.shadowRoot?.querySelector<HTMLInputElement>('.searchRow input')?.focus();
|
|
318
699
|
}
|
|
319
700
|
|
|
701
|
+
public updated(changedProperties: Map<PropertyKey, unknown>): void {
|
|
702
|
+
super.updated(changedProperties);
|
|
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);
|
|
711
|
+
}
|
|
712
|
+
if (!changedProperties.has('sessions')) return;
|
|
713
|
+
const previousSessions = changedProperties.get('sessions') as IHarnessSessionMeta[] | undefined;
|
|
714
|
+
const previousById = new Map((previousSessions ?? []).map((session) => [session.id, session]));
|
|
715
|
+
const knownIds = new Set(this.sessions.map((session) => session.id));
|
|
716
|
+
if (this.pointerDragGesture && !knownIds.has(this.pointerDragGesture.sessionId)) {
|
|
717
|
+
this.finishPointerDrag(false);
|
|
718
|
+
}
|
|
719
|
+
const nextExpanded = new Set(
|
|
720
|
+
[...this.expandedSessionIds].filter((sessionId) => knownIds.has(sessionId)),
|
|
721
|
+
);
|
|
722
|
+
for (const session of this.sessions) {
|
|
723
|
+
const previous = previousById.get(session.id);
|
|
724
|
+
if (previous && previous.working !== true && session.working === true) {
|
|
725
|
+
nextExpanded.add(session.id);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (
|
|
729
|
+
nextExpanded.size !== this.expandedSessionIds.size
|
|
730
|
+
|| [...nextExpanded].some((sessionId) => !this.expandedSessionIds.has(sessionId))
|
|
731
|
+
) {
|
|
732
|
+
this.expandedSessionIds = nextExpanded;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
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
|
+
|
|
744
|
+
public async disconnectedCallback(): Promise<void> {
|
|
745
|
+
this.finishPointerDrag(false);
|
|
746
|
+
this.scrollResizeObserver?.disconnect();
|
|
747
|
+
this.scrollResizeObserver = undefined;
|
|
748
|
+
this.observedScrollContainer = undefined;
|
|
749
|
+
this.observedScrollContent = undefined;
|
|
750
|
+
await super.disconnectedCallback();
|
|
751
|
+
}
|
|
752
|
+
|
|
320
753
|
public render(): TemplateResult {
|
|
321
754
|
const groupingActive = this.enableGrouping && !this.search.trim();
|
|
322
755
|
return html`
|
|
323
756
|
<div class="searchRow">
|
|
757
|
+
<dees-icon class="searchIcon" icon="lucide:Search" iconSize="13"></dees-icon>
|
|
324
758
|
<input
|
|
325
759
|
placeholder="Search conversations"
|
|
326
760
|
.value=${this.search}
|
|
@@ -329,13 +763,23 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
329
763
|
}}
|
|
330
764
|
/>
|
|
331
765
|
</div>
|
|
332
|
-
<div class="
|
|
333
|
-
${this.
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
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>
|
|
338
781
|
</div>
|
|
782
|
+
<div class="visuallyHidden" aria-live="polite">${this.moveAnnouncement}</div>
|
|
339
783
|
${groupingActive
|
|
340
784
|
? html`
|
|
341
785
|
<div class="newGroupRow">
|
|
@@ -367,7 +811,15 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
367
811
|
${this.sessions.length ? 'No conversations match this search.' : this.emptyText}
|
|
368
812
|
</div>`;
|
|
369
813
|
}
|
|
370
|
-
return html
|
|
814
|
+
return html`
|
|
815
|
+
<div class="section">
|
|
816
|
+
${repeat(
|
|
817
|
+
sessions,
|
|
818
|
+
(session) => session.id,
|
|
819
|
+
(session) => this.renderItem(session, null, -1),
|
|
820
|
+
)}
|
|
821
|
+
</div>
|
|
822
|
+
`;
|
|
371
823
|
}
|
|
372
824
|
|
|
373
825
|
private renderGroupedList(): TemplateResult {
|
|
@@ -383,10 +835,15 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
383
835
|
.filter((session): session is IHarnessSessionMeta => session !== undefined);
|
|
384
836
|
const collapsed = this.collapsedGroupIds.has(group.id);
|
|
385
837
|
const isHeaderDrop =
|
|
386
|
-
this.dropSlot?.groupId === group.id
|
|
838
|
+
this.dropSlot?.groupId === group.id
|
|
839
|
+
&& this.dropSlot.index === this.sortableSessions(members).length
|
|
840
|
+
&& collapsed;
|
|
387
841
|
return html`
|
|
388
842
|
<button
|
|
389
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"
|
|
390
847
|
type="button"
|
|
391
848
|
@click=${() => this.toggleGroupCollapsed(group.id)}
|
|
392
849
|
@contextmenu=${(event: MouseEvent) => {
|
|
@@ -401,8 +858,6 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
401
858
|
new CustomEvent('harness-group-context', { detail, bubbles: true, composed: true }),
|
|
402
859
|
);
|
|
403
860
|
}}
|
|
404
|
-
@dragover=${(event: DragEvent) => this.handleDragOverSlot(event, group.id, members.length)}
|
|
405
|
-
@drop=${(event: DragEvent) => this.handleDrop(event, group.id, members.length)}
|
|
406
861
|
>
|
|
407
862
|
<dees-icon class="chevron" icon="lucide:ChevronDown" iconSize="12"></dees-icon>
|
|
408
863
|
<span>${group.name}</span>
|
|
@@ -413,14 +868,15 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
413
868
|
: this.renderSection(members, group.id)}
|
|
414
869
|
`;
|
|
415
870
|
})}
|
|
416
|
-
${this.groups.length && ungrouped.length
|
|
871
|
+
${this.groups.length && (ungrouped.length || this.draggingSessionId)
|
|
417
872
|
? html`
|
|
418
873
|
<button
|
|
419
874
|
class="groupHeader"
|
|
875
|
+
data-drop-group-id=""
|
|
876
|
+
data-drop-index="0"
|
|
877
|
+
data-drop-kind="slot"
|
|
420
878
|
type="button"
|
|
421
879
|
tabindex="-1"
|
|
422
|
-
@dragover=${(event: DragEvent) => this.handleDragOverSlot(event, null, 0)}
|
|
423
|
-
@drop=${(event: DragEvent) => this.handleDrop(event, null, 0)}
|
|
424
880
|
>
|
|
425
881
|
<span>Ungrouped</span>
|
|
426
882
|
<span class="groupCount">${ungrouped.length}</span>
|
|
@@ -436,18 +892,64 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
436
892
|
groupId: string | null,
|
|
437
893
|
): TemplateResult {
|
|
438
894
|
const slot = this.dropSlot;
|
|
895
|
+
const entries: TSectionRenderEntry[] = [];
|
|
896
|
+
let logicalIndex = 0;
|
|
897
|
+
for (const session of sessions) {
|
|
898
|
+
const isDragging = session.id === this.draggingSessionId;
|
|
899
|
+
const sortable = session.draggable !== false;
|
|
900
|
+
if (
|
|
901
|
+
slot
|
|
902
|
+
&& slot.groupId === groupId
|
|
903
|
+
&& slot.index === logicalIndex
|
|
904
|
+
&& !isDragging
|
|
905
|
+
&& sortable
|
|
906
|
+
) {
|
|
907
|
+
entries.push({ type: 'placeholder', key: `placeholder:${groupId ?? 'ungrouped'}` });
|
|
908
|
+
}
|
|
909
|
+
entries.push({
|
|
910
|
+
type: 'session',
|
|
911
|
+
key: `session:${session.id}`,
|
|
912
|
+
session,
|
|
913
|
+
logicalIndex: sortable ? logicalIndex : -1,
|
|
914
|
+
});
|
|
915
|
+
if (sortable && !isDragging) logicalIndex += 1;
|
|
916
|
+
}
|
|
917
|
+
if (slot && slot.groupId === groupId && slot.index === logicalIndex) {
|
|
918
|
+
entries.push({ type: 'placeholder', key: `placeholder:${groupId ?? 'ungrouped'}` });
|
|
919
|
+
}
|
|
920
|
+
const hasPlaceholder = entries.some((entry) => entry.type === 'placeholder');
|
|
439
921
|
return html`
|
|
440
|
-
|
|
441
|
-
(
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
922
|
+
<div class="section" data-group-id=${groupId ?? ''}>
|
|
923
|
+
${repeat(
|
|
924
|
+
entries,
|
|
925
|
+
(entry) => entry.key,
|
|
926
|
+
(entry) => entry.type === 'placeholder'
|
|
927
|
+
? this.renderDropPlaceholder(groupId, slot?.index ?? 0)
|
|
928
|
+
: this.renderItem(entry.session, groupId, entry.logicalIndex),
|
|
929
|
+
)}
|
|
930
|
+
${this.draggingSessionId && !hasPlaceholder && this.sortableSessions(sessions).length === 0
|
|
931
|
+
? html`
|
|
932
|
+
<div
|
|
933
|
+
class="emptyDropZone"
|
|
934
|
+
data-drop-group-id=${groupId ?? ''}
|
|
935
|
+
data-drop-index="0"
|
|
936
|
+
data-drop-kind="slot"
|
|
937
|
+
></div>
|
|
938
|
+
`
|
|
939
|
+
: ''}
|
|
940
|
+
</div>
|
|
941
|
+
`;
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
private renderDropPlaceholder(groupId: string | null, index: number): TemplateResult {
|
|
945
|
+
return html`
|
|
946
|
+
<div
|
|
947
|
+
class="dropPlaceholder"
|
|
948
|
+
data-drop-group-id=${groupId ?? ''}
|
|
949
|
+
data-drop-index=${index}
|
|
950
|
+
data-drop-kind="slot"
|
|
951
|
+
style=${`height: ${Math.max(46, this.draggedItemHeight)}px`}
|
|
952
|
+
></div>
|
|
451
953
|
`;
|
|
452
954
|
}
|
|
453
955
|
|
|
@@ -456,20 +958,32 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
456
958
|
groupId: string | null,
|
|
457
959
|
index: number,
|
|
458
960
|
): TemplateResult {
|
|
459
|
-
const draggable =
|
|
961
|
+
const draggable = (
|
|
962
|
+
this.enableGrouping
|
|
963
|
+
&& session.draggable !== false
|
|
964
|
+
&& index >= 0
|
|
965
|
+
);
|
|
966
|
+
const expanded = this.expandedSessionIds.has(session.id);
|
|
967
|
+
const state = session.state ?? 'normal';
|
|
968
|
+
const detailsId = `session-details-${session.id}`;
|
|
969
|
+
const dropIndex = index < 0 ? 0 : index;
|
|
460
970
|
return html`
|
|
461
|
-
<
|
|
462
|
-
class="item ${
|
|
971
|
+
<article
|
|
972
|
+
class="item state-${state} ${expanded ? 'expanded' : ''} ${
|
|
973
|
+
session.id === this.selectedSessionId ? 'selected' : ''
|
|
974
|
+
} ${
|
|
463
975
|
this.draggingSessionId === session.id ? 'dragging' : ''
|
|
464
976
|
}"
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
@
|
|
470
|
-
if (event
|
|
977
|
+
data-session-id=${session.id}
|
|
978
|
+
data-drop-group-id=${groupId ?? ''}
|
|
979
|
+
data-drop-index=${dropIndex}
|
|
980
|
+
data-drop-kind=${index < 0 ? 'slot' : 'card'}
|
|
981
|
+
@dblclick=${(event: MouseEvent) => {
|
|
982
|
+
if (this.eventTargetsExpandToggle(event) || this.eventTargetsDragHandle(event)) return;
|
|
983
|
+
this.emit('harness-session-open', session);
|
|
471
984
|
}}
|
|
472
985
|
@contextmenu=${(event: MouseEvent) => {
|
|
986
|
+
if (this.eventTargetsExpandToggle(event)) return;
|
|
473
987
|
event.preventDefault();
|
|
474
988
|
const detail: IHarnessSessionContextDetail = {
|
|
475
989
|
session,
|
|
@@ -481,44 +995,115 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
481
995
|
new CustomEvent('harness-session-context', { detail, bubbles: true, composed: true }),
|
|
482
996
|
);
|
|
483
997
|
}}
|
|
484
|
-
@dragstart=${(event: DragEvent) => {
|
|
485
|
-
if (!draggable) return;
|
|
486
|
-
this.draggingSessionId = session.id;
|
|
487
|
-
event.dataTransfer?.setData('text/plain', session.id);
|
|
488
|
-
if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move';
|
|
489
|
-
}}
|
|
490
|
-
@dragend=${() => {
|
|
491
|
-
this.draggingSessionId = '';
|
|
492
|
-
this.dropSlot = undefined;
|
|
493
|
-
}}
|
|
494
|
-
@dragover=${(event: DragEvent) => {
|
|
495
|
-
if (!this.draggingSessionId || index < 0) return;
|
|
496
|
-
event.preventDefault();
|
|
497
|
-
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
|
498
|
-
const before = event.clientY < rect.top + rect.height / 2;
|
|
499
|
-
this.setDropSlot(groupId, before ? index : index + 1);
|
|
500
|
-
}}
|
|
501
|
-
@drop=${(event: DragEvent) => {
|
|
502
|
-
if (index < 0) return;
|
|
503
|
-
const rect = (event.currentTarget as HTMLElement).getBoundingClientRect();
|
|
504
|
-
const before = event.clientY < rect.top + rect.height / 2;
|
|
505
|
-
this.handleDrop(event, groupId, before ? index : index + 1);
|
|
506
|
-
}}
|
|
507
998
|
>
|
|
508
|
-
<
|
|
509
|
-
${
|
|
510
|
-
? html
|
|
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
|
+
`
|
|
511
1022
|
: ''}
|
|
512
|
-
<
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
1023
|
+
<button
|
|
1024
|
+
class="itemPrimary"
|
|
1025
|
+
type="button"
|
|
1026
|
+
aria-current=${session.id === this.selectedSessionId ? 'true' : 'false'}
|
|
1027
|
+
@click=${() => this.emit('harness-session-select', session)}
|
|
1028
|
+
@keydown=${(event: KeyboardEvent) => {
|
|
1029
|
+
if (event.key !== 'Enter') return;
|
|
1030
|
+
event.preventDefault();
|
|
1031
|
+
this.emit('harness-session-open', session);
|
|
1032
|
+
}}
|
|
1033
|
+
>
|
|
1034
|
+
<span class="stateDot"></span>
|
|
1035
|
+
<span class="itemCopy">
|
|
1036
|
+
<span class="itemTitle">
|
|
1037
|
+
${session.icon
|
|
1038
|
+
? html`<dees-icon class="itemIcon" icon=${session.icon} iconSize="13"></dees-icon>`
|
|
1039
|
+
: ''}
|
|
1040
|
+
<span class="titleText">${session.title || session.id}</span>
|
|
1041
|
+
</span>
|
|
1042
|
+
<span class="itemTime">${harnessFormatRelativeTime(session.updatedAt ?? session.createdAt)}</span>
|
|
1043
|
+
<span class="visuallyHidden">Status: ${this.sessionStateLabel(state)}</span>
|
|
1044
|
+
</span>
|
|
1045
|
+
</button>
|
|
1046
|
+
<button
|
|
1047
|
+
class="expandToggle"
|
|
1048
|
+
type="button"
|
|
1049
|
+
aria-expanded=${expanded ? 'true' : 'false'}
|
|
1050
|
+
aria-controls=${detailsId}
|
|
1051
|
+
title=${expanded ? 'Collapse conversation card' : 'Expand conversation card'}
|
|
1052
|
+
@click=${(event: MouseEvent) => {
|
|
1053
|
+
event.stopPropagation();
|
|
1054
|
+
this.toggleSessionExpanded(session.id);
|
|
1055
|
+
}}
|
|
1056
|
+
>
|
|
1057
|
+
<dees-icon icon="lucide:ChevronDown" iconSize="15"></dees-icon>
|
|
1058
|
+
</button>
|
|
1059
|
+
</div>
|
|
1060
|
+
${expanded
|
|
1061
|
+
? html`
|
|
1062
|
+
<div class="itemDetails" id=${detailsId}>
|
|
1063
|
+
<div class="stateLine">
|
|
1064
|
+
<span class="stateDot"></span>
|
|
1065
|
+
<span>${this.sessionStateLabel(state)}</span>
|
|
1066
|
+
</div>
|
|
1067
|
+
${session.preview ? html`<div class="itemPreview">${session.preview}</div>` : ''}
|
|
1068
|
+
<div class="itemMeta">
|
|
1069
|
+
${session.messageCount ? `${session.messageCount} messages · ` : ''}${session.id}
|
|
1070
|
+
</div>
|
|
1071
|
+
</div>
|
|
1072
|
+
`
|
|
517
1073
|
: ''}
|
|
518
|
-
</
|
|
1074
|
+
</article>
|
|
519
1075
|
`;
|
|
520
1076
|
}
|
|
521
1077
|
|
|
1078
|
+
private eventTargetsExpandToggle(event: Event): boolean {
|
|
1079
|
+
return event.composedPath().some(
|
|
1080
|
+
(target) => target instanceof HTMLElement && target.classList.contains('expandToggle'),
|
|
1081
|
+
);
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
private eventTargetsDragHandle(event: Event): boolean {
|
|
1085
|
+
return event.composedPath().some(
|
|
1086
|
+
(target) => target instanceof HTMLElement && target.classList.contains('dragHandle'),
|
|
1087
|
+
);
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
private sessionStateLabel(state: NonNullable<IHarnessSessionMeta['state']>): string {
|
|
1091
|
+
switch (state) {
|
|
1092
|
+
case 'working': return 'Working';
|
|
1093
|
+
case 'finished': return 'Finished';
|
|
1094
|
+
case 'attention': return 'Needs attention';
|
|
1095
|
+
case 'error': return 'Error';
|
|
1096
|
+
default: return 'Idle';
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
private toggleSessionExpanded(sessionIdArg: string): void {
|
|
1101
|
+
const next = new Set(this.expandedSessionIds);
|
|
1102
|
+
if (next.has(sessionIdArg)) next.delete(sessionIdArg);
|
|
1103
|
+
else next.add(sessionIdArg);
|
|
1104
|
+
this.expandedSessionIds = next;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
522
1107
|
private toggleGroupCollapsed(groupIdArg: string): void {
|
|
523
1108
|
const next = new Set(this.collapsedGroupIds);
|
|
524
1109
|
if (next.has(groupIdArg)) {
|
|
@@ -529,35 +1114,518 @@ export class DeesHarnessSessionList extends DeesElement {
|
|
|
529
1114
|
this.collapsedGroupIds = next;
|
|
530
1115
|
}
|
|
531
1116
|
|
|
1117
|
+
private sortableSessions(
|
|
1118
|
+
sessionsArg: IHarnessSessionMeta[],
|
|
1119
|
+
excludeDraggingArg = true,
|
|
1120
|
+
): IHarnessSessionMeta[] {
|
|
1121
|
+
return sessionsArg.filter((session) => (
|
|
1122
|
+
session.draggable !== false
|
|
1123
|
+
&& (!excludeDraggingArg || session.id !== this.draggingSessionId)
|
|
1124
|
+
));
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
private sessionsForGroup(groupIdArg: string | null): IHarnessSessionMeta[] {
|
|
1128
|
+
if (groupIdArg === null) return this.ungroupedSessions;
|
|
1129
|
+
const group = this.groups.find((candidate) => candidate.id === groupIdArg);
|
|
1130
|
+
if (!group) return [];
|
|
1131
|
+
const byId = this.sessionsById;
|
|
1132
|
+
return group.sessionIds
|
|
1133
|
+
.map((sessionId) => byId.get(sessionId))
|
|
1134
|
+
.filter((session): session is IHarnessSessionMeta => session !== undefined);
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
private handlePointerDown(
|
|
1138
|
+
eventArg: PointerEvent,
|
|
1139
|
+
sessionArg: IHarnessSessionMeta,
|
|
1140
|
+
groupIdArg: string | null,
|
|
1141
|
+
logicalIndexArg: number,
|
|
1142
|
+
): void {
|
|
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);
|
|
1207
|
+
return;
|
|
1208
|
+
}
|
|
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';
|
|
1245
|
+
this.reflowGeneration += 1;
|
|
1246
|
+
this.cancelReflowAnimations();
|
|
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
|
+
);
|
|
1270
|
+
}
|
|
1271
|
+
|
|
532
1272
|
private setDropSlot(groupIdArg: string | null, indexArg: number): void {
|
|
533
1273
|
const current = this.dropSlot;
|
|
534
1274
|
if (current && current.groupId === groupIdArg && current.index === indexArg) return;
|
|
1275
|
+
const previousRects = this.captureCardRects();
|
|
1276
|
+
const generation = ++this.reflowGeneration;
|
|
535
1277
|
this.dropSlot = { groupId: groupIdArg, index: indexArg };
|
|
1278
|
+
void this.animateCardReflow(previousRects, generation);
|
|
536
1279
|
}
|
|
537
1280
|
|
|
538
|
-
private
|
|
539
|
-
if (!this.
|
|
540
|
-
|
|
541
|
-
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);
|
|
542
1287
|
}
|
|
543
1288
|
|
|
544
|
-
private
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
const
|
|
548
|
-
this.
|
|
549
|
-
|
|
550
|
-
|
|
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
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
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();
|
|
1335
|
+
return;
|
|
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 {
|
|
1435
|
+
const targetSessions = this.sortableSessions(this.sessionsForGroup(groupIdArg));
|
|
1436
|
+
const beforeSessionId = targetSessions[Math.max(0, indexArg)]?.id;
|
|
1437
|
+
const sourceGroupId = this.groups.find((group) => group.sessionIds.includes(sessionIdArg))?.id ?? null;
|
|
1438
|
+
const sourceSessions = this.sortableSessions(this.sessionsForGroup(sourceGroupId), false);
|
|
1439
|
+
const sourceIndex = sourceSessions.findIndex((session) => session.id === sessionIdArg);
|
|
1440
|
+
const compatibilityIndex = (
|
|
1441
|
+
sourceGroupId === groupIdArg
|
|
1442
|
+
&& sourceIndex >= 0
|
|
1443
|
+
&& indexArg > sourceIndex
|
|
1444
|
+
) ? indexArg + 1 : indexArg;
|
|
1445
|
+
return {
|
|
1446
|
+
sessionId: sessionIdArg,
|
|
1447
|
+
groupId: groupIdArg,
|
|
1448
|
+
index: Math.max(0, compatibilityIndex),
|
|
1449
|
+
...(beforeSessionId ? { beforeSessionId } : {}),
|
|
1450
|
+
};
|
|
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;
|
|
551
1505
|
const detail: IHarnessSessionMoveDetail = {
|
|
552
|
-
sessionId,
|
|
1506
|
+
sessionId: sessionArg.id,
|
|
553
1507
|
groupId: groupIdArg,
|
|
554
|
-
index:
|
|
1508
|
+
index: compatibilityIndex,
|
|
1509
|
+
...(beforeSession ? { beforeSessionId: beforeSession.id } : {}),
|
|
555
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.`;
|
|
556
1514
|
this.dispatchEvent(
|
|
557
1515
|
new CustomEvent('harness-session-move', { detail, bubbles: true, composed: true }),
|
|
558
1516
|
);
|
|
559
1517
|
}
|
|
560
1518
|
|
|
1519
|
+
private captureCardRects(): Map<string, DOMRect> {
|
|
1520
|
+
const result = new Map<string, DOMRect>();
|
|
1521
|
+
for (const item of this.shadowRoot?.querySelectorAll<HTMLElement>('.item[data-session-id]') ?? []) {
|
|
1522
|
+
if (item.dataset.sessionId === this.draggingSessionId) continue;
|
|
1523
|
+
result.set(item.dataset.sessionId ?? '', item.getBoundingClientRect());
|
|
1524
|
+
}
|
|
1525
|
+
return result;
|
|
1526
|
+
}
|
|
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
|
+
|
|
1553
|
+
private async animateCardReflow(
|
|
1554
|
+
previousRectsArg: Map<string, DOMRect>,
|
|
1555
|
+
generationArg: number,
|
|
1556
|
+
): Promise<void> {
|
|
1557
|
+
await this.updateComplete;
|
|
1558
|
+
if (
|
|
1559
|
+
generationArg !== this.reflowGeneration
|
|
1560
|
+
|| !this.isConnected
|
|
1561
|
+
|| globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches
|
|
1562
|
+
) return;
|
|
1563
|
+
this.cancelReflowAnimations();
|
|
1564
|
+
for (const item of this.shadowRoot?.querySelectorAll<HTMLElement>('.item[data-session-id]') ?? []) {
|
|
1565
|
+
const sessionId = item.dataset.sessionId ?? '';
|
|
1566
|
+
if (!sessionId || sessionId === this.draggingSessionId) continue;
|
|
1567
|
+
const previous = previousRectsArg.get(sessionId);
|
|
1568
|
+
if (!previous) continue;
|
|
1569
|
+
const current = item.getBoundingClientRect();
|
|
1570
|
+
const deltaY = previous.top - current.top;
|
|
1571
|
+
if (Math.abs(deltaY) < 0.5) continue;
|
|
1572
|
+
const animation = item.animate(
|
|
1573
|
+
[
|
|
1574
|
+
{ transform: `translateY(${deltaY}px)` },
|
|
1575
|
+
{ transform: 'translateY(0)' },
|
|
1576
|
+
],
|
|
1577
|
+
{ duration: 180, easing: 'cubic-bezier(0.22, 1, 0.36, 1)' },
|
|
1578
|
+
);
|
|
1579
|
+
this.reflowAnimations.add(animation);
|
|
1580
|
+
void animation.finished.catch(() => undefined).finally(() => {
|
|
1581
|
+
this.reflowAnimations.delete(animation);
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
private cancelReflowAnimations(): void {
|
|
1587
|
+
for (const animation of this.reflowAnimations) animation.cancel();
|
|
1588
|
+
this.reflowAnimations.clear();
|
|
1589
|
+
}
|
|
1590
|
+
|
|
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();
|
|
1601
|
+
this.reflowGeneration += 1;
|
|
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);
|
|
1612
|
+
this.draggingSessionId = '';
|
|
1613
|
+
this.draggedItemHeight = 0;
|
|
1614
|
+
this.dropSlot = undefined;
|
|
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
|
+
}
|
|
1627
|
+
}
|
|
1628
|
+
|
|
561
1629
|
private emit(name: string, session: IHarnessSessionMeta): void {
|
|
562
1630
|
this.dispatchEvent(new CustomEvent(name, { detail: { session }, bubbles: true, composed: true }));
|
|
563
1631
|
}
|