@design.estate/dees-catalog 3.101.0 → 3.103.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.
Files changed (21) hide show
  1. package/dist_bundle/bundle.js +1558 -979
  2. package/dist_ts_web/00_commitinfo_data.js +1 -1
  3. package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.d.ts +30 -1
  4. package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.js +330 -9
  5. package/dist_ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.d.ts +50 -0
  6. package/dist_ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.demo.d.ts +1 -0
  7. package/dist_ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.demo.js +26 -0
  8. package/dist_ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.js +347 -0
  9. package/dist_ts_web/elements/00group-harness/dees-terminal-view/index.d.ts +1 -0
  10. package/dist_ts_web/elements/00group-harness/dees-terminal-view/index.js +2 -0
  11. package/dist_ts_web/elements/00group-harness/index.d.ts +1 -0
  12. package/dist_ts_web/elements/00group-harness/index.js +2 -1
  13. package/dist_ts_web/elements/00group-harness/interfaces.d.ts +23 -0
  14. package/package.json +1 -1
  15. package/ts_web/00_commitinfo_data.ts +1 -1
  16. package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.ts +336 -7
  17. package/ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.demo.ts +26 -0
  18. package/ts_web/elements/00group-harness/dees-terminal-view/dees-terminal-view.ts +317 -0
  19. package/ts_web/elements/00group-harness/dees-terminal-view/index.ts +1 -0
  20. package/ts_web/elements/00group-harness/index.ts +1 -0
  21. package/ts_web/elements/00group-harness/interfaces.ts +26 -0
@@ -10,7 +10,13 @@ import {
10
10
  cssManager,
11
11
  } from '@design.estate/dees-element';
12
12
  import { themeDefaultStyles } from '../../00theme.js';
13
- import type { IHarnessSessionContextDetail, IHarnessSessionMeta } from '../interfaces.js';
13
+ import type {
14
+ IHarnessGroupContextDetail,
15
+ IHarnessSessionContextDetail,
16
+ IHarnessSessionGroup,
17
+ IHarnessSessionMeta,
18
+ IHarnessSessionMoveDetail,
19
+ } from '../interfaces.js';
14
20
  import { harnessFormatRelativeTime } from '../harness.helpers.js';
15
21
  import '../../00group-utility/dees-icon/dees-icon.js';
16
22
 
@@ -20,11 +26,24 @@ declare global {
20
26
  }
21
27
  }
22
28
 
29
+ interface IDropSlot {
30
+ groupId: string | null;
31
+ index: number;
32
+ }
33
+
23
34
  /**
24
35
  * Searchable conversation list, embeddable in a sidebar or a DeesModal.
25
36
  * Emits `harness-session-select` on click, `harness-session-open` on
26
37
  * double-click or Enter, and `harness-session-context` with viewport
27
38
  * pointer coordinates on right-click (the browser menu is suppressed).
39
+ *
40
+ * With `enableGrouping`, sessions render under user-defined `groups` and can
41
+ * be dragged between groups and reordered. The component never mutates its
42
+ * inputs: drops emit `harness-session-move`, the "new group" button emits
43
+ * `harness-group-create-request`, and right-clicking a group header emits
44
+ * `harness-group-context` — the host persists changes and passes fresh
45
+ * `groups` back in. Sessions in no group render in the trailing ungrouped
46
+ * section, recency-sorted. While a search query is active the list is flat.
28
47
  */
29
48
  @customElement('dees-harness-session-list')
30
49
  export class DeesHarnessSessionList extends DeesElement {
@@ -43,9 +62,24 @@ export class DeesHarnessSessionList extends DeesElement {
43
62
  @property()
44
63
  accessor emptyText: string = 'No conversations yet.';
45
64
 
65
+ @property({ attribute: false })
66
+ accessor groups: IHarnessSessionGroup[] = [];
67
+
68
+ @property({ type: Boolean })
69
+ accessor enableGrouping: boolean = false;
70
+
46
71
  @state()
47
72
  accessor search: string = '';
48
73
 
74
+ @state()
75
+ accessor draggingSessionId: string = '';
76
+
77
+ @state()
78
+ accessor dropSlot: IDropSlot | undefined = undefined;
79
+
80
+ @state()
81
+ accessor collapsedGroupIds: ReadonlySet<string> = new Set<string>();
82
+
49
83
  public static styles = [
50
84
  themeDefaultStyles,
51
85
  cssManager.defaultStyles,
@@ -118,6 +152,10 @@ export class DeesHarnessSessionList extends DeesElement {
118
152
  box-shadow: inset 2px 0 0 var(--dees-color-accent-primary);
119
153
  }
120
154
 
155
+ .item.dragging {
156
+ opacity: 0.4;
157
+ }
158
+
121
159
  .itemTitle {
122
160
  display: flex;
123
161
  align-items: baseline;
@@ -133,6 +171,12 @@ export class DeesHarnessSessionList extends DeesElement {
133
171
  white-space: nowrap;
134
172
  }
135
173
 
174
+ .itemTitle .itemIcon {
175
+ flex: 0 0 auto;
176
+ align-self: center;
177
+ color: var(--dees-color-text-secondary);
178
+ }
179
+
136
180
  .itemTime {
137
181
  margin-left: auto;
138
182
  flex: 0 0 auto;
@@ -155,9 +199,109 @@ export class DeesHarnessSessionList extends DeesElement {
155
199
  font-size: var(--dees-font-control-size-sm, 12px);
156
200
  text-align: center;
157
201
  }
202
+
203
+ .groupHeader {
204
+ display: flex;
205
+ align-items: center;
206
+ gap: var(--dees-spacing-xs);
207
+ width: 100%;
208
+ margin-top: var(--dees-spacing-xs);
209
+ padding: 2px var(--dees-spacing-sm);
210
+ border: 0;
211
+ border-radius: var(--dees-radius-md);
212
+ background: transparent;
213
+ color: var(--dees-color-text-secondary);
214
+ cursor: pointer;
215
+ font-family: inherit;
216
+ font-size: var(--dees-font-caption1-size, 11px);
217
+ font-weight: 650;
218
+ text-transform: uppercase;
219
+ letter-spacing: 0.05em;
220
+ text-align: left;
221
+ }
222
+
223
+ .groupHeader:hover {
224
+ background: var(--dees-color-hover);
225
+ }
226
+
227
+ .groupHeader .chevron {
228
+ transition: transform var(--dees-transition-fast) var(--dees-ease-standard);
229
+ }
230
+
231
+ .groupHeader.collapsed .chevron {
232
+ transform: rotate(-90deg);
233
+ }
234
+
235
+ .groupHeader .groupCount {
236
+ margin-left: auto;
237
+ font-weight: 500;
238
+ text-transform: none;
239
+ letter-spacing: normal;
240
+ color: var(--dees-color-text-muted);
241
+ }
242
+
243
+ .groupHeader.dropTarget {
244
+ outline: 1px dashed var(--dees-color-accent-primary);
245
+ outline-offset: -1px;
246
+ }
247
+
248
+ .dropLine {
249
+ height: 2px;
250
+ margin: 0 var(--dees-spacing-sm);
251
+ border-radius: 1px;
252
+ background: var(--dees-color-accent-primary);
253
+ }
254
+
255
+ .newGroupRow {
256
+ flex: 0 0 auto;
257
+ padding: var(--dees-spacing-xs) var(--dees-spacing-sm) var(--dees-spacing-sm);
258
+ }
259
+
260
+ .newGroupButton {
261
+ display: flex;
262
+ align-items: center;
263
+ gap: var(--dees-spacing-xs);
264
+ width: 100%;
265
+ padding: var(--dees-spacing-xs) var(--dees-spacing-sm);
266
+ border: 1px dashed var(--dees-color-border-default);
267
+ border-radius: var(--dees-radius-md);
268
+ background: transparent;
269
+ color: var(--dees-color-text-secondary);
270
+ cursor: pointer;
271
+ font-family: inherit;
272
+ font-size: var(--dees-font-control-size-sm, 12px);
273
+ }
274
+
275
+ .newGroupButton:hover {
276
+ border-color: var(--dees-color-accent-primary);
277
+ color: var(--dees-color-text-primary);
278
+ }
158
279
  `,
159
280
  ];
160
281
 
282
+ private get sessionsById(): Map<string, IHarnessSessionMeta> {
283
+ return new Map(this.sessions.map((session) => [session.id, session]));
284
+ }
285
+
286
+ private get groupedSessionIds(): Set<string> {
287
+ const ids = new Set<string>();
288
+ for (const group of this.groups) {
289
+ for (const id of group.sessionIds) ids.add(id);
290
+ }
291
+ return ids;
292
+ }
293
+
294
+ /** Ungrouped sessions, most recent first. */
295
+ private get ungroupedSessions(): IHarnessSessionMeta[] {
296
+ const grouped = this.groupedSessionIds;
297
+ return [...this.sessions]
298
+ .filter((session) => !grouped.has(session.id))
299
+ .sort(
300
+ (left, right) =>
301
+ (right.updatedAt ?? right.createdAt ?? 0) - (left.updatedAt ?? left.createdAt ?? 0),
302
+ );
303
+ }
304
+
161
305
  private get filteredSessions(): IHarnessSessionMeta[] {
162
306
  const query = this.search.trim().toLowerCase();
163
307
  const sorted = [...this.sessions].sort(
@@ -174,7 +318,7 @@ export class DeesHarnessSessionList extends DeesElement {
174
318
  }
175
319
 
176
320
  public render(): TemplateResult {
177
- const sessions = this.filteredSessions;
321
+ const groupingActive = this.enableGrouping && !this.search.trim();
178
322
  return html`
179
323
  <div class="searchRow">
180
324
  <input
@@ -188,18 +332,138 @@ export class DeesHarnessSessionList extends DeesElement {
188
332
  <div class="list">
189
333
  ${this.loading
190
334
  ? html`<div class="emptyNote">Loading conversations…</div>`
191
- : sessions.length
192
- ? sessions.map((session) => this.renderItem(session))
193
- : html`<div class="emptyNote">${this.sessions.length ? 'No conversations match this search.' : this.emptyText}</div>`}
335
+ : groupingActive
336
+ ? this.renderGroupedList()
337
+ : this.renderFlatList()}
194
338
  </div>
339
+ ${groupingActive
340
+ ? html`
341
+ <div class="newGroupRow">
342
+ <button
343
+ class="newGroupButton"
344
+ type="button"
345
+ @click=${() => {
346
+ this.dispatchEvent(
347
+ new CustomEvent('harness-group-create-request', {
348
+ bubbles: true,
349
+ composed: true,
350
+ }),
351
+ );
352
+ }}
353
+ >
354
+ <dees-icon icon="lucide:FolderPlus" iconSize="13"></dees-icon>
355
+ New group
356
+ </button>
357
+ </div>
358
+ `
359
+ : ''}
360
+ `;
361
+ }
362
+
363
+ private renderFlatList(): TemplateResult {
364
+ const sessions = this.filteredSessions;
365
+ if (!sessions.length) {
366
+ return html`<div class="emptyNote">
367
+ ${this.sessions.length ? 'No conversations match this search.' : this.emptyText}
368
+ </div>`;
369
+ }
370
+ return html`${sessions.map((session) => this.renderItem(session, null, -1))}`;
371
+ }
372
+
373
+ private renderGroupedList(): TemplateResult {
374
+ const byId = this.sessionsById;
375
+ const ungrouped = this.ungroupedSessions;
376
+ if (!this.sessions.length && !this.groups.length) {
377
+ return html`<div class="emptyNote">${this.emptyText}</div>`;
378
+ }
379
+ return html`
380
+ ${this.groups.map((group) => {
381
+ const members = group.sessionIds
382
+ .map((id) => byId.get(id))
383
+ .filter((session): session is IHarnessSessionMeta => session !== undefined);
384
+ const collapsed = this.collapsedGroupIds.has(group.id);
385
+ const isHeaderDrop =
386
+ this.dropSlot?.groupId === group.id && this.dropSlot.index === members.length && collapsed;
387
+ return html`
388
+ <button
389
+ class="groupHeader ${collapsed ? 'collapsed' : ''} ${isHeaderDrop ? 'dropTarget' : ''}"
390
+ type="button"
391
+ @click=${() => this.toggleGroupCollapsed(group.id)}
392
+ @contextmenu=${(event: MouseEvent) => {
393
+ event.preventDefault();
394
+ const detail: IHarnessGroupContextDetail = {
395
+ group,
396
+ clientX: event.clientX,
397
+ clientY: event.clientY,
398
+ originalEvent: event,
399
+ };
400
+ this.dispatchEvent(
401
+ new CustomEvent('harness-group-context', { detail, bubbles: true, composed: true }),
402
+ );
403
+ }}
404
+ @dragover=${(event: DragEvent) => this.handleDragOverSlot(event, group.id, members.length)}
405
+ @drop=${(event: DragEvent) => this.handleDrop(event, group.id, members.length)}
406
+ >
407
+ <dees-icon class="chevron" icon="lucide:ChevronDown" iconSize="12"></dees-icon>
408
+ <span>${group.name}</span>
409
+ <span class="groupCount">${members.length}</span>
410
+ </button>
411
+ ${collapsed
412
+ ? ''
413
+ : this.renderSection(members, group.id)}
414
+ `;
415
+ })}
416
+ ${this.groups.length && ungrouped.length
417
+ ? html`
418
+ <button
419
+ class="groupHeader"
420
+ type="button"
421
+ tabindex="-1"
422
+ @dragover=${(event: DragEvent) => this.handleDragOverSlot(event, null, 0)}
423
+ @drop=${(event: DragEvent) => this.handleDrop(event, null, 0)}
424
+ >
425
+ <span>Ungrouped</span>
426
+ <span class="groupCount">${ungrouped.length}</span>
427
+ </button>
428
+ `
429
+ : ''}
430
+ ${this.renderSection(ungrouped, null)}
195
431
  `;
196
432
  }
197
433
 
198
- private renderItem(session: IHarnessSessionMeta): TemplateResult {
434
+ private renderSection(
435
+ sessions: IHarnessSessionMeta[],
436
+ groupId: string | null,
437
+ ): TemplateResult {
438
+ const slot = this.dropSlot;
439
+ return html`
440
+ ${sessions.map(
441
+ (session, index) => html`
442
+ ${slot && slot.groupId === groupId && slot.index === index
443
+ ? html`<div class="dropLine"></div>`
444
+ : ''}
445
+ ${this.renderItem(session, groupId, index)}
446
+ `,
447
+ )}
448
+ ${slot && slot.groupId === groupId && slot.index === sessions.length
449
+ ? html`<div class="dropLine"></div>`
450
+ : ''}
451
+ `;
452
+ }
453
+
454
+ private renderItem(
455
+ session: IHarnessSessionMeta,
456
+ groupId: string | null,
457
+ index: number,
458
+ ): TemplateResult {
459
+ const draggable = this.enableGrouping && index >= 0;
199
460
  return html`
200
461
  <button
201
- class="item ${session.id === this.selectedSessionId ? 'selected' : ''}"
462
+ class="item ${session.id === this.selectedSessionId ? 'selected' : ''} ${
463
+ this.draggingSessionId === session.id ? 'dragging' : ''
464
+ }"
202
465
  type="button"
466
+ draggable=${draggable ? 'true' : 'false'}
203
467
  @click=${() => this.emit('harness-session-select', session)}
204
468
  @dblclick=${() => this.emit('harness-session-open', session)}
205
469
  @keydown=${(event: KeyboardEvent) => {
@@ -217,8 +481,34 @@ export class DeesHarnessSessionList extends DeesElement {
217
481
  new CustomEvent('harness-session-context', { detail, bubbles: true, composed: true }),
218
482
  );
219
483
  }}
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
+ }}
220
507
  >
221
508
  <span class="itemTitle">
509
+ ${session.icon
510
+ ? html`<dees-icon class="itemIcon" icon=${session.icon} iconSize="12"></dees-icon>`
511
+ : ''}
222
512
  <span class="titleText">${session.title || session.id}</span>
223
513
  <span class="itemTime">${harnessFormatRelativeTime(session.updatedAt ?? session.createdAt)}</span>
224
514
  </span>
@@ -229,6 +519,45 @@ export class DeesHarnessSessionList extends DeesElement {
229
519
  `;
230
520
  }
231
521
 
522
+ private toggleGroupCollapsed(groupIdArg: string): void {
523
+ const next = new Set(this.collapsedGroupIds);
524
+ if (next.has(groupIdArg)) {
525
+ next.delete(groupIdArg);
526
+ } else {
527
+ next.add(groupIdArg);
528
+ }
529
+ this.collapsedGroupIds = next;
530
+ }
531
+
532
+ private setDropSlot(groupIdArg: string | null, indexArg: number): void {
533
+ const current = this.dropSlot;
534
+ if (current && current.groupId === groupIdArg && current.index === indexArg) return;
535
+ this.dropSlot = { groupId: groupIdArg, index: indexArg };
536
+ }
537
+
538
+ private handleDragOverSlot(event: DragEvent, groupIdArg: string | null, indexArg: number): void {
539
+ if (!this.draggingSessionId) return;
540
+ event.preventDefault();
541
+ this.setDropSlot(groupIdArg, indexArg);
542
+ }
543
+
544
+ private handleDrop(event: DragEvent, groupIdArg: string | null, indexArg: number): void {
545
+ event.preventDefault();
546
+ event.stopPropagation();
547
+ const sessionId = this.draggingSessionId || event.dataTransfer?.getData('text/plain') || '';
548
+ this.draggingSessionId = '';
549
+ this.dropSlot = undefined;
550
+ if (!sessionId) return;
551
+ const detail: IHarnessSessionMoveDetail = {
552
+ sessionId,
553
+ groupId: groupIdArg,
554
+ index: Math.max(0, indexArg),
555
+ };
556
+ this.dispatchEvent(
557
+ new CustomEvent('harness-session-move', { detail, bubbles: true, composed: true }),
558
+ );
559
+ }
560
+
232
561
  private emit(name: string, session: IHarnessSessionMeta): void {
233
562
  this.dispatchEvent(new CustomEvent(name, { detail: { session }, bubbles: true, composed: true }));
234
563
  }
@@ -0,0 +1,26 @@
1
+ import { html } from '@design.estate/dees-element';
2
+
3
+ // A loopback demo: keystrokes echo straight back into the terminal so the
4
+ // component is explorable without a live PTY behind it.
5
+ export const demoFunc = () => html`
6
+ <style>
7
+ .terminalDemo {
8
+ height: 380px;
9
+ border: 1px solid var(--dees-color-border-default);
10
+ border-radius: 8px;
11
+ overflow: hidden;
12
+ }
13
+ </style>
14
+ <div class="terminalDemo">
15
+ <dees-terminal-view
16
+ @terminal-input=${(event: CustomEvent<{ data: string }>) => {
17
+ const view = event.currentTarget as HTMLElementTagNameMap['dees-terminal-view'];
18
+ const data = event.detail.data;
19
+ view.write(data === '\r' ? '\r\n' : data);
20
+ }}
21
+ @terminal-resize=${(event: CustomEvent<{ rows: number; cols: number }>) => {
22
+ console.log('terminal size', event.detail);
23
+ }}
24
+ ></dees-terminal-view>
25
+ </div>
26
+ `;