@design.estate/dees-catalog 10.0.0 → 11.0.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.
@@ -7,14 +7,16 @@ import '../dees-harness-question-card/dees-harness-question-card.js';
7
7
  import '../dees-harness-permission-card/dees-harness-permission-card.js';
8
8
  import type { DeesHarnessSessionList } from './dees-harness-session-list.js';
9
9
  import type {
10
+ IHarnessItemMoveDetail,
10
11
  IHarnessResourceAssociation,
11
12
  IHarnessResourceContextDetail,
12
13
  IHarnessResourceEventDetail,
13
14
  IHarnessResourceMeta,
15
+ IHarnessSessionEventDetail,
14
16
  IHarnessSessionGroup,
15
17
  IHarnessSessionContextDetail,
18
+ IHarnessSessionListItemRef,
16
19
  IHarnessSessionMeta,
17
- IHarnessSessionMoveDetail,
18
20
  IHarnessQuestionResponseDetail,
19
21
  IHarnessPermissionResponseDetail,
20
22
  THarnessResourceAssociationRequestDetail,
@@ -55,11 +57,39 @@ const demoSessions = (): IDemoSession[] => [
55
57
  preview: stages[session.stage].description,
56
58
  }));
57
59
 
60
+ // Resources are peers of sessions: `terminal:build` sits in Workspace while it
61
+ // stays attached to a session in Recent.
58
62
  const demoGroups: IHarnessSessionGroup[] = [
59
- { id: 'active', name: 'Workspace', sessionIds: ['session-0', 'session-1', 'session-2', 'session-6'] },
60
- { id: 'review', name: 'Recent', sessionIds: ['session-3', 'session-5', 'session-4'] },
63
+ {
64
+ id: 'active',
65
+ name: 'Workspace',
66
+ itemIds: [
67
+ { kind: 'session', id: 'session-0' },
68
+ { kind: 'resource', id: 'terminal:build' },
69
+ { kind: 'session', id: 'session-1' },
70
+ { kind: 'session', id: 'session-2' },
71
+ { kind: 'session', id: 'session-6' },
72
+ ],
73
+ },
74
+ {
75
+ id: 'review',
76
+ name: 'Recent',
77
+ itemIds: [
78
+ { kind: 'session', id: 'session-3' },
79
+ { kind: 'session', id: 'session-5' },
80
+ { kind: 'session', id: 'session-4' },
81
+ ],
82
+ },
61
83
  ];
62
84
 
85
+ const cloneGroups = (groupsArg: IHarnessSessionGroup[]): IHarnessSessionGroup[] =>
86
+ groupsArg.map((group) => ({ ...group, itemIds: group.itemIds.map((ref) => ({ ...ref })) }));
87
+
88
+ const sameRef = (
89
+ leftArg: IHarnessSessionListItemRef,
90
+ rightArg: IHarnessSessionListItemRef,
91
+ ): boolean => leftArg.kind === rightArg.kind && leftArg.id === rightArg.id;
92
+
63
93
  const demoResources: IHarnessResourceMeta[] = [
64
94
  {
65
95
  id: 'terminal:build',
@@ -88,45 +118,61 @@ const demoResources: IHarnessResourceMeta[] = [
88
118
  },
89
119
  ];
90
120
 
121
+ // An association targets any other item: a session, or a resource such as a terminal.
91
122
  const demoResourceAssociations: IHarnessResourceAssociation[] = [
92
- { resourceId: 'terminal:build', sessionId: 'session-4' },
93
- { resourceId: 'file:release-notes', sessionId: 'session-4' },
123
+ { resourceId: 'terminal:build', target: { kind: 'session', id: 'session-4' } },
124
+ { resourceId: 'file:release-notes', target: { kind: 'resource', id: 'terminal:build' } },
125
+ ];
126
+
127
+ const demoEligibleSessionTargets: IHarnessSessionListItemRef[] = [
128
+ { kind: 'session', id: 'session-0' },
129
+ { kind: 'session', id: 'session-1' },
130
+ { kind: 'session', id: 'session-2' },
131
+ { kind: 'session', id: 'session-4' },
94
132
  ];
95
133
 
96
- const demoResourceEligibleSessionIds = Object.fromEntries(
97
- demoResources.map((resource) => [resource.id, ['session-0', 'session-1', 'session-2', 'session-4']]),
134
+ const demoResourceEligibleTargets: Record<string, IHarnessSessionListItemRef[]> = Object.fromEntries(
135
+ demoResources.map((resource) => [
136
+ resource.id,
137
+ resource.kind === 'terminal'
138
+ ? demoEligibleSessionTargets
139
+ : [...demoEligibleSessionTargets, { kind: 'resource' as const, id: 'terminal:build' }],
140
+ ]),
98
141
  );
99
142
 
100
143
  type TDemoSessionList = DeesHarnessSessionList;
101
144
 
102
- const handleSessionSelect = (event: CustomEvent<{ session: IHarnessSessionMeta }>): void => {
145
+ const handleSessionSelect = (event: CustomEvent<IHarnessSessionEventDetail>): void => {
103
146
  const list = event.currentTarget as TDemoSessionList;
104
147
  list.selectedSessionId = event.detail.session.id;
105
148
  };
106
149
 
107
- const handleSessionMove = (event: CustomEvent<IHarnessSessionMoveDetail>): void => {
150
+ /** The host owns order: one reducer moves sessions and resources alike. */
151
+ type TDemoItemMove = Pick<IHarnessItemMoveDetail, 'item' | 'toGroupId' | 'beforeItem'>;
152
+
153
+ const handleItemMove = (event: CustomEvent<IHarnessItemMoveDetail>): void => {
108
154
  const list = event.currentTarget as TDemoSessionList;
109
- applySessionMove(list, event.detail);
155
+ applyItemMove(list, event.detail);
110
156
  };
111
157
 
112
- const applySessionMove = (list: TDemoSessionList, detail: IHarnessSessionMoveDetail): void => {
113
- const { sessionId, groupId, beforeSessionId } = detail;
158
+ const applyItemMove = (list: TDemoSessionList, moveArg: TDemoItemMove): void => {
159
+ const { item, toGroupId, beforeItem } = moveArg;
114
160
  const groups = list.groups.map((group) => ({
115
161
  ...group,
116
- sessionIds: group.sessionIds.filter((id) => id !== sessionId),
162
+ itemIds: group.itemIds.filter((ref) => !sameRef(ref, item)),
117
163
  }));
118
- const ungroupedSessionIds = list.ungroupedSessionIds.filter((id) => id !== sessionId);
119
- const targetIds = groupId === null
120
- ? ungroupedSessionIds
121
- : groups.find((group) => group.id === groupId)?.sessionIds;
164
+ const ungroupedItemIds = list.ungroupedItemIds.filter((ref) => !sameRef(ref, item));
165
+ const targetIds = toGroupId === null
166
+ ? ungroupedItemIds
167
+ : groups.find((group) => group.id === toGroupId)?.itemIds;
122
168
  if (!targetIds) return;
123
- const anchorIndex = beforeSessionId ? targetIds.indexOf(beforeSessionId) : -1;
124
- targetIds.splice(anchorIndex >= 0 ? anchorIndex : targetIds.length, 0, sessionId);
169
+ const anchorIndex = beforeItem ? targetIds.findIndex((ref) => sameRef(ref, beforeItem)) : -1;
170
+ targetIds.splice(anchorIndex >= 0 ? anchorIndex : targetIds.length, 0, { ...item });
125
171
  list.groups = groups;
126
- list.ungroupedSessionIds = ungroupedSessionIds;
172
+ list.ungroupedItemIds = ungroupedItemIds;
127
173
  };
128
174
 
129
- const handleSessionOpen = (event: CustomEvent<{ session: IHarnessSessionMeta }>): void => {
175
+ const handleSessionOpen = (event: CustomEvent<IHarnessSessionEventDetail>): void => {
130
176
  const list = event.currentTarget as TDemoSessionList;
131
177
  list.selectedSessionId = event.detail.session.id;
132
178
  showDemoResult(list, `Opened “${event.detail.session.title}”.`);
@@ -141,7 +187,10 @@ const handleSessionContext = async (
141
187
  const invokingControl = originalEvent.composedPath().find(
142
188
  (target): target is HTMLButtonElement => target instanceof HTMLButtonElement,
143
189
  );
144
- const currentGroup = list.groups.find((group) => group.sessionIds.includes(session.id));
190
+ const sessionRef: IHarnessSessionListItemRef = { kind: 'session', id: session.id };
191
+ const currentGroup = list.groups.find(
192
+ (group) => group.itemIds.some((ref) => sameRef(ref, sessionRef)),
193
+ );
145
194
  // Capture dialog ownership before event dispatch ends: no await before opening.
146
195
  const menu = await DeesContextmenu.openContextMenuWithOptions(originalEvent, [
147
196
  {
@@ -159,7 +208,7 @@ const handleSessionContext = async (
159
208
  ...list.groups.filter((group) => group.id !== currentGroup?.id).map((group) => ({
160
209
  name: `Move to ${group.name}`, iconName: 'lucide:FolderInput',
161
210
  action: async () => {
162
- applySessionMove(list, { sessionId: session.id, groupId: group.id, index: group.sessionIds.length });
211
+ applyItemMove(list, { item: sessionRef, toGroupId: group.id, beforeItem: null });
163
212
  onChange();
164
213
  showDemoResult(list, `Moved “${session.title}” to ${group.name}.`);
165
214
  },
@@ -167,7 +216,7 @@ const handleSessionContext = async (
167
216
  ...(currentGroup ? [{
168
217
  name: 'Move to Ungrouped', iconName: 'lucide:FolderInput',
169
218
  action: async () => {
170
- applySessionMove(list, { sessionId: session.id, groupId: null, index: list.ungroupedSessionIds.length });
219
+ applyItemMove(list, { item: sessionRef, toGroupId: null, beforeItem: null });
171
220
  onChange();
172
221
  showDemoResult(list, `Moved “${session.title}” to Ungrouped.`);
173
222
  },
@@ -191,10 +240,10 @@ const handleResourceAssociation = (
191
240
  const next = list.resourceAssociations.filter(
192
241
  (association) => association.resourceId !== event.detail.resource.id,
193
242
  );
194
- if (event.detail.toSessionId) {
243
+ if (event.detail.toTarget) {
195
244
  next.push({
196
245
  resourceId: event.detail.resource.id,
197
- sessionId: event.detail.toSessionId,
246
+ target: { ...event.detail.toTarget },
198
247
  });
199
248
  }
200
249
  list.resourceAssociations = next;
@@ -204,27 +253,28 @@ const handleResourceContext = async (
204
253
  event: CustomEvent<IHarnessResourceContextDetail>,
205
254
  ): Promise<void> => {
206
255
  const list = event.currentTarget as TDemoSessionList;
207
- const currentSessionId = event.detail.sessionId;
208
- const eligibleSessionIds = list.eligibleSessionIdsByResourceId[event.detail.resource.id] ?? [];
256
+ const currentTarget = event.detail.target;
257
+ const eligibleTargets = list.eligibleTargetsByResourceId[event.detail.resource.id] ?? [];
258
+ const titleFor = (refArg: IHarnessSessionListItemRef): string => (refArg.kind === 'session'
259
+ ? list.sessions.find((session) => session.id === refArg.id)?.title
260
+ : list.resources.find((resource) => resource.id === refArg.id)?.title) ?? refArg.id;
209
261
  const menu = await DeesContextmenu.openContextMenuWithOptions(
210
262
  new MouseEvent('contextmenu', {
211
263
  clientX: event.detail.clientX,
212
264
  clientY: event.detail.clientY,
213
265
  }),
214
266
  [
215
- ...eligibleSessionIds
216
- .filter((sessionId) => sessionId !== currentSessionId)
217
- .map((sessionId) => ({
218
- name: `${currentSessionId ? 'Reassign' : 'Attach'} to ${
219
- list.sessions.find((session) => session.id === sessionId)?.title
220
- }`,
267
+ ...eligibleTargets
268
+ .filter((target) => !currentTarget || !sameRef(target, currentTarget))
269
+ .map((target) => ({
270
+ name: `${currentTarget ? 'Reassign' : 'Attach'} to ${titleFor(target)}`,
221
271
  iconName: 'lucide:Link',
222
272
  action: async () => {
223
- event.detail.requestAssociation(sessionId);
273
+ event.detail.requestAssociation(target);
224
274
  event.detail.close();
225
275
  },
226
276
  })),
227
- ...(currentSessionId
277
+ ...(currentTarget
228
278
  ? [{
229
279
  name: 'Detach',
230
280
  iconName: 'lucide:Unlink',
@@ -249,7 +299,7 @@ const openModal = async (sessions: IHarnessSessionMeta[], groups: IHarnessSessio
249
299
  const content = document.createElement('dees-harness-session-list');
250
300
  content.sessions = sessions;
251
301
  content.groups = groups;
252
- content.ungroupedSessionIds = [];
302
+ content.ungroupedItemIds = [];
253
303
  content.enableGrouping = true;
254
304
  content.style.height = '420px';
255
305
  content.addEventListener('harness-session-select', handleSessionSelect as EventListener);
@@ -257,7 +307,7 @@ const openModal = async (sessions: IHarnessSessionMeta[], groups: IHarnessSessio
257
307
  content.addEventListener('harness-session-context', (event) => {
258
308
  void handleSessionContext(event as CustomEvent<IHarnessSessionContextDetail>);
259
309
  });
260
- content.addEventListener('harness-session-move', handleSessionMove as EventListener);
310
+ content.addEventListener('harness-item-move', handleItemMove as EventListener);
261
311
  DeesModal.createAndShow({
262
312
  heading: 'Conversations',
263
313
  content: html`<style>${demoStyles}</style><div class="dees-demo-stack">${content}${demoOutput('Choose a conversation action.')}</div>`,
@@ -269,8 +319,8 @@ const openModal = async (sessions: IHarnessSessionMeta[], groups: IHarnessSessio
269
319
  class DeesDemoSessionWorkspace extends DeesElement {
270
320
  @state() private accessor sessions = demoSessions();
271
321
  @state() private accessor selectedSessionId = 'session-0';
272
- @state() private accessor groups = demoGroups.map((group) => ({ ...group, sessionIds: [...group.sessionIds] }));
273
- @state() private accessor ungroupedSessionIds: string[] = [];
322
+ @state() private accessor groups: IHarnessSessionGroup[] = cloneGroups(demoGroups);
323
+ @state() private accessor ungroupedItemIds: IHarnessSessionListItemRef[] = [];
274
324
  @state() private accessor associations = [...demoResourceAssociations];
275
325
  @state() private accessor notice = 'Interactive demo · Changes stay in this example.';
276
326
  @state() private accessor showDetails = false;
@@ -327,11 +377,11 @@ class DeesDemoSessionWorkspace extends DeesElement {
327
377
  if (!list) return;
328
378
  this.selectedSessionId = list.selectedSessionId;
329
379
  this.groups = list.groups;
330
- this.ungroupedSessionIds = list.ungroupedSessionIds;
380
+ this.ungroupedItemIds = list.ungroupedItemIds;
331
381
  this.associations = list.resourceAssociations;
332
382
  }
333
383
 
334
- private async select(event: CustomEvent<{ session: IHarnessSessionMeta }>): Promise<void> {
384
+ private async select(event: CustomEvent<IHarnessSessionEventDetail>): Promise<void> {
335
385
  handleSessionSelect(event);
336
386
  this.selectedSessionId = event.detail.session.id;
337
387
  await this.revealSession();
@@ -359,8 +409,8 @@ class DeesDemoSessionWorkspace extends DeesElement {
359
409
  private reset(): void {
360
410
  this.sessions = demoSessions();
361
411
  this.selectedSessionId = 'session-0';
362
- this.groups = demoGroups.map((group) => ({ ...group, sessionIds: [...group.sessionIds] }));
363
- this.ungroupedSessionIds = [];
412
+ this.groups = cloneGroups(demoGroups);
413
+ this.ungroupedItemIds = [];
364
414
  this.associations = [...demoResourceAssociations];
365
415
  this.notice = 'Demo reset. Choose a session or respond to the approval request.';
366
416
  if (this.list) {
@@ -390,20 +440,20 @@ class DeesDemoSessionWorkspace extends DeesElement {
390
440
  <span class="attentionSummary"><dees-icon icon="lucide:CircleAlert" iconSize="12"></dees-icon>${attentionCount} need attention</span>
391
441
  </div>
392
442
  <dees-harness-session-list
393
- .sessions=${this.sessions} .groups=${this.groups} .ungroupedSessionIds=${this.ungroupedSessionIds}
443
+ .sessions=${this.sessions} .groups=${this.groups} .ungroupedItemIds=${this.ungroupedItemIds}
394
444
  .resources=${demoResources} .resourceAssociations=${this.associations}
395
- .eligibleSessionIdsByResourceId=${demoResourceEligibleSessionIds} .enableGrouping=${true}
445
+ .eligibleTargetsByResourceId=${demoResourceEligibleTargets} .enableGrouping=${true}
396
446
  .selectedSessionId=${this.selectedSessionId}
397
447
  @harness-session-select=${this.select} @harness-session-open=${this.select}
398
448
  @harness-session-context=${(event: CustomEvent<IHarnessSessionContextDetail>) => handleSessionContext(event, (opened) => {
399
449
  this.syncList();
400
450
  if (opened) void this.revealSession();
401
451
  })}
402
- @harness-session-move=${(event: CustomEvent<IHarnessSessionMoveDetail>) => { handleSessionMove(event); this.syncList(); }}
452
+ @harness-item-move=${(event: CustomEvent<IHarnessItemMoveDetail>) => { handleItemMove(event); this.syncList(); }}
403
453
  @harness-group-create-request=${() => {
404
- this.groups = [...this.groups, { id: `group-${this.groups.length}`, name: `Group ${this.groups.length + 1}`, sessionIds: [] }];
454
+ this.groups = [...this.groups, { id: `group-${this.groups.length}`, name: `Group ${this.groups.length + 1}`, itemIds: [] }];
405
455
  if (this.list) this.list.search = '';
406
- this.notice = 'Group created. Drag a session here or use its Actions menu.';
456
+ this.notice = 'Group created. Drag a conversation or resource here, or use its Actions menu.';
407
457
  }}
408
458
  @harness-resource-select=${(event: CustomEvent<IHarnessResourceEventDetail>) => {
409
459
  (event.currentTarget as TDemoSessionList).selectedResourceId = event.detail.resource.id;