@design.estate/dees-catalog 11.2.2 → 12.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.
Files changed (25) hide show
  1. package/dist_bundle/bundle.js +1861 -1290
  2. package/dist_bundle/bundle.js.map +1 -1
  3. package/dist_ts_web/00_commitinfo_data.js +1 -1
  4. package/dist_ts_web/elements/00group-harness/dees-harness-conversation-picker/dees-harness-conversation-picker.d.ts +62 -0
  5. package/dist_ts_web/elements/00group-harness/dees-harness-conversation-picker/dees-harness-conversation-picker.demo.d.ts +1 -0
  6. package/dist_ts_web/elements/00group-harness/dees-harness-conversation-picker/dees-harness-conversation-picker.demo.js +38 -0
  7. package/dist_ts_web/elements/00group-harness/dees-harness-conversation-picker/dees-harness-conversation-picker.js +455 -0
  8. package/dist_ts_web/elements/00group-harness/dees-harness-conversation-picker/index.d.ts +1 -0
  9. package/dist_ts_web/elements/00group-harness/dees-harness-conversation-picker/index.js +2 -0
  10. package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.d.ts +39 -10
  11. package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.demo.js +81 -27
  12. package/dist_ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.js +327 -115
  13. package/dist_ts_web/elements/00group-harness/index.d.ts +1 -0
  14. package/dist_ts_web/elements/00group-harness/index.js +2 -1
  15. package/dist_ts_web/elements/00group-harness/interfaces.d.ts +35 -16
  16. package/package.json +1 -1
  17. package/readme.md +54 -22
  18. package/ts_web/00_commitinfo_data.ts +1 -1
  19. package/ts_web/elements/00group-harness/dees-harness-conversation-picker/dees-harness-conversation-picker.demo.ts +46 -0
  20. package/ts_web/elements/00group-harness/dees-harness-conversation-picker/dees-harness-conversation-picker.ts +420 -0
  21. package/ts_web/elements/00group-harness/dees-harness-conversation-picker/index.ts +1 -0
  22. package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.demo.ts +94 -36
  23. package/ts_web/elements/00group-harness/dees-harness-session-list/dees-harness-session-list.ts +357 -120
  24. package/ts_web/elements/00group-harness/index.ts +1 -0
  25. package/ts_web/elements/00group-harness/interfaces.ts +41 -16
@@ -8,6 +8,7 @@ export * from './dees-harness-overflow-text.js';
8
8
  export * from './dees-harness-chat/index.js';
9
9
  export * from './dees-harness-composer/index.js';
10
10
  export * from './dees-harness-content-blocks/index.js';
11
+ export * from './dees-harness-conversation-picker/index.js';
11
12
  export * from './dees-harness-message/index.js';
12
13
  export * from './dees-harness-permission-card/index.js';
13
14
  export * from './dees-harness-question-card/index.js';
@@ -11,6 +11,7 @@ export * from './dees-harness-overflow-text.js';
11
11
  export * from './dees-harness-chat/index.js';
12
12
  export * from './dees-harness-composer/index.js';
13
13
  export * from './dees-harness-content-blocks/index.js';
14
+ export * from './dees-harness-conversation-picker/index.js';
14
15
  export * from './dees-harness-message/index.js';
15
16
  export * from './dees-harness-permission-card/index.js';
16
17
  export * from './dees-harness-question-card/index.js';
@@ -23,4 +24,4 @@ export * from './dees-harness-status/index.js';
23
24
  export * from './dees-harness-todos/index.js';
24
25
  export * from './dees-harness-tool-card/index.js';
25
26
  export * from './dees-harness-usage/index.js';
26
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi90c193ZWIvZWxlbWVudHMvMDBncm91cC1oYXJuZXNzL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLG9DQUFvQztBQUNwQywyRUFBMkU7QUFDM0UsNkVBQTZFO0FBQzdFLGNBQWMsaUJBQWlCLENBQUM7QUFDaEMsY0FBYyxtQkFBbUIsQ0FBQztBQUNsQyxjQUFjLHNCQUFzQixDQUFDO0FBQ3JDLE9BQU8sRUFBRSxlQUFlLEVBQUUsY0FBYyxFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFDeEUsT0FBTyxFQUFFLGlCQUFpQixFQUFFLGlCQUFpQixFQUFFLGlCQUFpQixFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFDOUYsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLGlDQUFpQyxDQUFDO0FBRWhELGNBQWMsOEJBQThCLENBQUM7QUFDN0MsY0FBYyxrQ0FBa0MsQ0FBQztBQUNqRCxjQUFjLHdDQUF3QyxDQUFDO0FBQ3ZELGNBQWMsaUNBQWlDLENBQUM7QUFDaEQsY0FBYyx5Q0FBeUMsQ0FBQztBQUN4RCxjQUFjLHVDQUF1QyxDQUFDO0FBQ3RELGNBQWMsc0NBQXNDLENBQUM7QUFDckQsY0FBYyxtQ0FBbUMsQ0FBQztBQUNsRCxjQUFjLHNDQUFzQyxDQUFDO0FBQ3JELGNBQWMseUNBQXlDLENBQUM7QUFDeEQsY0FBYywrQkFBK0IsQ0FBQztBQUM5QyxjQUFjLGdDQUFnQyxDQUFDO0FBQy9DLGNBQWMsK0JBQStCLENBQUM7QUFDOUMsY0FBYyxtQ0FBbUMsQ0FBQztBQUNsRCxjQUFjLCtCQUErQixDQUFDIn0=
27
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi90c193ZWIvZWxlbWVudHMvMDBncm91cC1oYXJuZXNzL2luZGV4LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLG9DQUFvQztBQUNwQywyRUFBMkU7QUFDM0UsNkVBQTZFO0FBQzdFLGNBQWMsaUJBQWlCLENBQUM7QUFDaEMsY0FBYyxtQkFBbUIsQ0FBQztBQUNsQyxjQUFjLHNCQUFzQixDQUFDO0FBQ3JDLE9BQU8sRUFBRSxlQUFlLEVBQUUsY0FBYyxFQUFFLE1BQU0sdUJBQXVCLENBQUM7QUFDeEUsT0FBTyxFQUFFLGlCQUFpQixFQUFFLGlCQUFpQixFQUFFLGlCQUFpQixFQUFFLE1BQU0scUJBQXFCLENBQUM7QUFDOUYsY0FBYyx1QkFBdUIsQ0FBQztBQUN0QyxjQUFjLGlDQUFpQyxDQUFDO0FBRWhELGNBQWMsOEJBQThCLENBQUM7QUFDN0MsY0FBYyxrQ0FBa0MsQ0FBQztBQUNqRCxjQUFjLHdDQUF3QyxDQUFDO0FBQ3ZELGNBQWMsNkNBQTZDLENBQUM7QUFDNUQsY0FBYyxpQ0FBaUMsQ0FBQztBQUNoRCxjQUFjLHlDQUF5QyxDQUFDO0FBQ3hELGNBQWMsdUNBQXVDLENBQUM7QUFDdEQsY0FBYyxzQ0FBc0MsQ0FBQztBQUNyRCxjQUFjLG1DQUFtQyxDQUFDO0FBQ2xELGNBQWMsc0NBQXNDLENBQUM7QUFDckQsY0FBYyx5Q0FBeUMsQ0FBQztBQUN4RCxjQUFjLCtCQUErQixDQUFDO0FBQzlDLGNBQWMsZ0NBQWdDLENBQUM7QUFDL0MsY0FBYywrQkFBK0IsQ0FBQztBQUM5QyxjQUFjLG1DQUFtQyxDQUFDO0FBQ2xELGNBQWMsK0JBQStCLENBQUMifQ==
@@ -364,42 +364,57 @@ export interface IHarnessResourceMeta<TState extends string = string> {
364
364
  draggable?: boolean;
365
365
  }
366
366
  /**
367
- * A controlled zero-or-one association for one resource. The target is any
368
- * other list item — a session or another resource such as a terminal.
367
+ * The controlled set of associations for one resource. A resource can be attached to several
368
+ * items at once, and a target is any other list item — a session or another resource such as a
369
+ * terminal. An empty array is an unattached resource.
369
370
  */
370
371
  export interface IHarnessResourceAssociation {
371
372
  resourceId: string;
372
- target: IHarnessSessionListItemRef;
373
+ targets: IHarnessSessionListItemRef[];
373
374
  }
374
375
  export interface IHarnessResourceEventDetail<TState extends string = string> {
375
376
  resource: IHarnessResourceMeta<TState>;
376
- /** The controlled association target, or null when the resource is unattached. */
377
- target: IHarnessSessionListItemRef | null;
377
+ /** The controlled association targets; empty when the resource is unattached. */
378
+ targets: IHarnessSessionListItemRef[];
378
379
  }
379
380
  /**
380
- * Association is an explicit host action, never a drop: dragging a resource
381
- * only moves it inside the ordered list.
381
+ * Attaching is an explicit choice, never an accident of reordering: a drag only attaches when it
382
+ * is released on the centre of a row, and the edges of that same row stay positional. Keyboard
383
+ * moves are always positional; keyboard attaching goes through the host's menu.
382
384
  */
383
- export type THarnessResourceAssociationInteractionSource = 'action' | 'keyboard';
384
- export type THarnessResourceAssociationRequestDetail<TState extends string = string> = {
385
- intent: 'attach';
385
+ export type THarnessResourceAssociationInteractionSource = 'action' | 'keyboard' | 'pointer';
386
+ /**
387
+ * A validated association request. `attach` adds the first target, `attach-additional` adds
388
+ * another one beside existing targets, `move` replaces one specific target, and `detach` removes
389
+ * one. `fromTarget` always names the attachment being replaced or removed, never the whole set.
390
+ */
391
+ export type THarnessResourceAssociateDetail<TState extends string = string> = {
392
+ mode: 'attach';
386
393
  resource: IHarnessResourceMeta<TState>;
387
394
  fromTarget: null;
388
395
  toTarget: IHarnessSessionListItemRef;
389
396
  source: THarnessResourceAssociationInteractionSource;
390
397
  } | {
391
- intent: 'detach';
398
+ mode: 'attach-additional';
392
399
  resource: IHarnessResourceMeta<TState>;
393
- fromTarget: IHarnessSessionListItemRef;
394
- toTarget: null;
400
+ fromTarget: null;
401
+ toTarget: IHarnessSessionListItemRef;
395
402
  source: THarnessResourceAssociationInteractionSource;
396
403
  } | {
397
- intent: 'reassign';
404
+ mode: 'move';
398
405
  resource: IHarnessResourceMeta<TState>;
399
406
  fromTarget: IHarnessSessionListItemRef;
400
407
  toTarget: IHarnessSessionListItemRef;
401
408
  source: THarnessResourceAssociationInteractionSource;
409
+ } | {
410
+ mode: 'detach';
411
+ resource: IHarnessResourceMeta<TState>;
412
+ fromTarget: IHarnessSessionListItemRef;
413
+ toTarget: null;
414
+ source: THarnessResourceAssociationInteractionSource;
402
415
  };
416
+ /** Explicit mode for `requestAssociation`; omitted lets the list infer it from the current set. */
417
+ export type THarnessResourceAssociateMode = THarnessResourceAssociateDetail['mode'];
403
418
  export type THarnessResourceContextSource = 'pointer' | 'action' | 'keyboard';
404
419
  /** Detail of `harness-resource-context`; the host owns the actual menu. */
405
420
  export interface IHarnessResourceContextDetail<TState extends string = string> extends IHarnessResourceEventDetail<TState> {
@@ -407,8 +422,12 @@ export interface IHarnessResourceContextDetail<TState extends string = string> e
407
422
  clientY: number;
408
423
  source: THarnessResourceContextSource;
409
424
  originalEvent: MouseEvent | KeyboardEvent;
410
- /** Routes a host menu choice through the component's validated controlled request event. */
411
- requestAssociation: (toTarget: IHarnessSessionListItemRef | null) => boolean;
425
+ /**
426
+ * Routes a host menu choice through the component's validated controlled request event. With no
427
+ * mode, an unattached resource attaches and a singly attached one moves; a resource with several
428
+ * attachments is refused, because only the host knows which attachment was meant.
429
+ */
430
+ requestAssociation: (toTarget: IHarnessSessionListItemRef | null, mode?: THarnessResourceAssociateMode, fromTarget?: IHarnessSessionListItemRef | null) => boolean;
412
431
  /** Closes the external-menu disclosure state and restores focus to its invoking control. */
413
432
  close: () => void;
414
433
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@design.estate/dees-catalog",
3
- "version": "11.2.2",
3
+ "version": "12.0.0",
4
4
  "private": false,
5
5
  "description": "A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.",
6
6
  "main": "dist_ts_web/index.js",
package/readme.md CHANGED
@@ -165,6 +165,7 @@ task guide and examples deliberately; the generated table does not replace them.
165
165
  | `dees-harness-chat` | harness | [DeesHarnessChat](#deesharnesschat) | [Source](ts_web/elements/00group-harness/dees-harness-chat/dees-harness-chat.ts) |
166
166
  | `dees-harness-composer` | harness | [DeesHarnessComposer](#deesharnesscomposer) | [Source](ts_web/elements/00group-harness/dees-harness-composer/dees-harness-composer.ts) |
167
167
  | `dees-harness-content-blocks` | harness | [DeesHarnessContentBlocks](#deesharnesscontentblocks) | [Source](ts_web/elements/00group-harness/dees-harness-content-blocks/dees-harness-content-blocks.ts) |
168
+ | `dees-harness-conversation-picker` | harness | [DeesHarnessConversationPicker](#deesharnessconversationpicker) | [Source](ts_web/elements/00group-harness/dees-harness-conversation-picker/dees-harness-conversation-picker.ts) |
168
169
  | `dees-harness-message` | harness | [DeesHarnessMessage](#deesharnessmessage) | [Source](ts_web/elements/00group-harness/dees-harness-message/dees-harness-message.ts) |
169
170
  | `dees-harness-message-list` | harness | [DeesHarnessMessageList](#deesharnessmessagelist) | [Source](ts_web/elements/00group-harness/dees-harness-message-list/dees-harness-message-list.ts) |
170
171
  | `dees-harness-overflow-text` | harness | [DeesHarnessOverflowText](#deesharnessoverflowtext) | [Source](ts_web/elements/00group-harness/dees-harness-overflow-text.ts) |
@@ -2767,6 +2768,25 @@ Scratchpad edits emit `harness-scratchpad-save` with `{ text, expectedRevision }
2767
2768
 
2768
2769
  Direct sidebar hosts can set `intelligenceAvailabilityStatus`, `intelligenceUnavailableReason`, and `intelligenceHeading`; `intelligenceEnabled` still controls whether the section exists. The properties default to `available`, an empty reason, and `Session Intelligence`. A blank heading falls back to `Session Intelligence`, and unavailable state without a reason renders `<heading> is unavailable.` Only `available` permits `harness-session-intelligence-ask`; checking and unavailable states retain history and expose accessible status text. The availability union is exported as `THarnessIntelligenceAvailabilityStatus`.
2769
2770
 
2771
+ #### `DeesHarnessConversationPicker`
2772
+
2773
+ Keyboard-first picker for one conversation, or one attachable resource, used wherever a menu would otherwise list every possible target. It is presentation only: it never mutates a session, a resource, or an association — it resolves one choice and leaves the change to the host.
2774
+
2775
+ Set `.sessions`, `.groups` and `.resources` to the same models the list renders. `.excludeTargets` removes targets that would be a no-op (a resource's existing attachments), and `.eligibleTargets` restricts the offer to a host-declared allow-list; absent means unrestricted. Typing filters on title, project label and group name — the same fields the session list searches. Up/Down move the active row, Enter picks it, Escape cancels. `harness-picker-choose` carries `{ target: IHarnessSessionListItemRef }`; `harness-picker-cancel` carries nothing.
2776
+
2777
+ `DeesHarnessConversationPicker.pick(options)` is the one-call form: it opens the picker inside `DeesModal`, resolves with the chosen ref or `null` on cancel, closes the modal, and returns focus to whatever had it before. Reuse it for disambiguation as well — pass a resource's current attachments as `eligibleTargets` to ask which attachment a Move or Detach applies to.
2778
+
2779
+ ```typescript
2780
+ const target = await DeesHarnessConversationPicker.pick({
2781
+ sessions: list.sessions,
2782
+ groups: list.groups,
2783
+ excludeTargets: currentTargets,
2784
+ eligibleTargets: list.eligibleTargetsByResourceId[resource.id],
2785
+ heading: `Attach ${resource.title} to…`,
2786
+ });
2787
+ if (target) detail.requestAssociation(target, currentTargets.length ? 'attach-additional' : 'attach');
2788
+ ```
2789
+
2770
2790
  #### `DeesHarnessSessionList`
2771
2791
 
2772
2792
  Searchable, card-based conversation column for sidebars or a `DeesModal` (`harness-session-select` on click, `harness-session-open` on double-click/Enter). Cards stay collapsed until their independent accessible expansion control is used. Selection and working-state changes do not affect expansion.
@@ -2785,22 +2805,29 @@ The compact **New group** button sits beside search whenever `enableGrouping` is
2785
2805
 
2786
2806
  The Sessions demo pairs the list with a selected-session workspace. Its controls step through starting, working, feedback, approval, completion and failure without timers or backend operations. Question and permission responses update the selected session; declining approval pauses it. The narrow layout switches between the preserved list and detail panes with an **All sessions** control that restores row focus.
2787
2807
 
2788
- Generic host resources use the exported `IHarnessResourceMeta` and `IHarnessResourceAssociation` contracts. Set `.resources`, `.resourceAssociations`, `.selectedResourceId`, and `.eligibleTargetsByResourceId`. Resource rows render as top-level rows in the ordered list, never as children of a session. Optional `projectLabel` renders on the resource metadata line after the kind and is searched like the title and preview. Resource metadata is presentation-only: the list never mounts a terminal, browser, file viewer, or other runtime.
2808
+ Generic host resources use the exported `IHarnessResourceMeta` and `IHarnessResourceAssociation` contracts. Set `.resources`, `.resourceAssociations`, `.selectedResourceId`, and `.eligibleTargetsByResourceId`. Resource rows render as top-level rows in the ordered list, never as children of a session. Optional `projectLabel` renders on the resource metadata line after the kind and is searched like the title and preview. A resource can be attached to several items at once: `IHarnessResourceAssociation.targets` is a set of refs, de-duplicated on projection, and the row shows one marker per attachment — two by name, the rest as a `+N` count with every name in the row's title. `IHarnessResourceEventDetail.targets` carries the same set on select, open and context events. Resource metadata is presentation-only: the list never mounts a terminal, browser, file viewer, or other runtime.
2789
2809
 
2790
- **Attaching is a separate explicit action, not a drop.** An association is an access grant: it shows as a decorative marker on the resource row (a link icon plus the target's title, resolved from `sessions` or `resources` regardless of search or collapse) and never moves the row. Dropping is purely positional and never attaches. The association stays single-valued — a resource has at most one target — and the target is any other item, so a resource may be attached to a session **or** to another resource such as a terminal:
2810
+ **Attaching is a deliberate gesture, never an accident of reordering.** An association is an access grant: it shows as a decorative marker on the resource row (a link icon plus each target's title, resolved from `sessions` or `resources` regardless of search or collapse) and never moves the row. A resource dropped on the **centre half of a row** attaches to it; the outer quarters of that same row, and every gap between rows, stay purely positional. The row being targeted shows a ring, a tint and a link badge, and no insertion gap opens while it does. A drag attaches an unattached resource and moves a singly attached one. A resource attached to several conversations is only repositioned by drag — it offers no attach affordance on any row, because a drag cannot say which of its attachments was meant; use **Move to…** for that. Keyboard moves stay positional; attaching by keyboard goes through the host's menu. The target is any other item, so a resource may be attached to a session **or** to another resource such as a terminal, and to several at once:
2791
2811
 
2792
2812
  ```typescript
2793
2813
  list.resourceAssociations = [
2794
- { resourceId: 'file:notes', target: { kind: 'resource', id: 'terminal:build' } },
2814
+ { resourceId: 'file:notes', targets: [{ kind: 'resource', id: 'terminal:build' }] },
2815
+ {
2816
+ resourceId: 'terminal:build',
2817
+ targets: [{ kind: 'session', id: 's-4' }, { kind: 'session', id: 's-0' }],
2818
+ },
2795
2819
  ];
2796
2820
  ```
2797
2821
 
2798
- Association records are projected only when the resource exists, the target exists, the target is not the resource itself, and no earlier record claimed that resource. `eligibleTargetsByResourceId` restricts attach/reassign targets for host menus: a missing entry leaves the resource unrestricted, an empty array means no eligible target. Which resource kinds may host an attachment is the host's rule, not the component's.
2822
+ Targets are projected only when the resource exists, the target exists, the target is not the resource itself, and no earlier target in the same record already claimed it. `eligibleTargetsByResourceId` restricts targets for both the host menu and the drop affordance: a missing entry leaves the resource unrestricted, an empty array means no eligible target, and an ineligible row offers no attach affordance and commits nothing. Which resource kinds may host an attachment is the host's rule, not the component's.
2823
+
2824
+ `requestResourceAssociation(resourceId, toTarget, source?, mode?, fromTarget?)` and the context detail's `requestAssociation(toTarget, mode?, fromTarget?)` emit `harness-resource-associate` with `THarnessResourceAssociateDetail`: `mode` is `attach`, `attach-additional`, `move` or `detach`, and `fromTarget` names the single attachment being replaced or removed. With no mode, an unattached resource attaches and a singly attached one moves; a resource with several attachments is refused, because only the host knows which one the user meant — pass a mode and a `fromTarget`, or ask with `DeesHarnessConversationPicker`.
2799
2825
 
2800
2826
  ```typescript
2801
- import type {
2802
- IHarnessResourceContextDetail,
2803
- THarnessResourceAssociationRequestDetail,
2827
+ import {
2828
+ DeesHarnessConversationPicker,
2829
+ type IHarnessResourceContextDetail,
2830
+ type THarnessResourceAssociateDetail,
2804
2831
  } from '@design.estate/dees-catalog';
2805
2832
 
2806
2833
  const list = document.querySelector('dees-harness-session-list')!;
@@ -2810,37 +2837,42 @@ list.resources = [
2810
2837
  { id: 'file:notes', kind: 'file', title: 'Release notes', preview: '6.7.0 draft' },
2811
2838
  ];
2812
2839
  list.resourceAssociations = [
2813
- { resourceId: 'shell:build', target: { kind: 'session', id: 'session-1' } },
2840
+ { resourceId: 'shell:build', targets: [{ kind: 'session', id: 'session-1' }] },
2814
2841
  ];
2815
2842
  list.eligibleTargetsByResourceId = {
2816
2843
  'shell:build': [{ kind: 'session', id: 'session-1' }, { kind: 'session', id: 'session-2' }],
2817
2844
  'file:notes': [{ kind: 'session', id: 'session-2' }, { kind: 'resource', id: 'shell:build' }],
2818
2845
  };
2819
2846
 
2820
- list.addEventListener('harness-resource-association-request', async (event) => {
2821
- const resourceEvent = event as CustomEvent<THarnessResourceAssociationRequestDetail>;
2822
- const { resource, toTarget } = resourceEvent.detail;
2823
- await persistAssociation(resource.id, toTarget);
2847
+ list.addEventListener('harness-resource-associate', async (event) => {
2848
+ const resourceEvent = event as CustomEvent<THarnessResourceAssociateDetail>;
2849
+ const { resource, mode, fromTarget, toTarget } = resourceEvent.detail;
2850
+ await persistAssociation(resource.id, { mode, fromTarget, toTarget });
2824
2851
  list.resourceAssociations = await loadAssociations();
2825
2852
  });
2826
2853
 
2827
- list.addEventListener('harness-resource-context', (event) => {
2854
+ list.addEventListener('harness-resource-context', async (event) => {
2828
2855
  const resourceEvent = event as CustomEvent<IHarnessResourceContextDetail>;
2829
- openHostMenu({
2830
- x: resourceEvent.detail.clientX,
2831
- y: resourceEvent.detail.clientY,
2832
- attach: (target) => resourceEvent.detail.requestAssociation(target),
2833
- detach: () => resourceEvent.detail.requestAssociation(null),
2834
- onClose: resourceEvent.detail.close,
2856
+ const { resource, targets } = resourceEvent.detail;
2857
+ resourceEvent.detail.close();
2858
+ const target = await DeesHarnessConversationPicker.pick({
2859
+ sessions: list.sessions,
2860
+ groups: list.groups,
2861
+ excludeTargets: targets,
2862
+ eligibleTargets: list.eligibleTargetsByResourceId[resource.id],
2863
+ heading: `Attach ${resource.title} to…`,
2835
2864
  });
2865
+ if (target) {
2866
+ resourceEvent.detail.requestAssociation(target, targets.length ? 'attach-additional' : 'attach');
2867
+ }
2836
2868
  });
2837
2869
  ```
2838
2870
 
2839
- `harness-resource-select` and `harness-resource-open` carry `{ resource, target }`, where `target` is the controlled association ref or null. `harness-resource-context` adds viewport coordinates, the pointer/Actions/keyboard source, the original event, `requestAssociation(toTarget)`, and `close()` for focus restoration. The visible Actions button has menu disclosure semantics; native `ContextMenu` and `Shift+F10` invoke the same host-owned path. The component does not require `DeesContextmenu` and does not own menu choices.
2871
+ `harness-resource-select` and `harness-resource-open` carry `{ resource, targets }`, where `targets` is the controlled set of association refs and is empty for an unattached resource. `harness-resource-context` adds viewport coordinates, the pointer/Actions/keyboard source, the original event, `requestAssociation(toTarget, mode?, fromTarget?)`, and `close()` for focus restoration. The visible Actions button has menu disclosure semantics; native `ContextMenu` and `Shift+F10` invoke the same host-owned path. The component does not require `DeesContextmenu` and does not own menu choices.
2840
2872
 
2841
- All component-originated association request paths converge on the public `requestResourceAssociation(resourceId, toTarget, source?)` method. A valid change emits one bubbling/composed `harness-resource-association-request` discriminated by `intent: 'attach' | 'detach' | 'reassign'` and carrying `resource`, `fromTarget`, `toTarget`, and `source: 'action' | 'keyboard'`. The method returns `false` and emits nothing for unknown resources, unknown or ineligible targets, a target that is the resource itself, detach from an unattached resource, or the current target. It never mutates resources, associations, selection, groups, or order.
2873
+ All component-originated association paths converge on the public `requestResourceAssociation(resourceId, toTarget, source?, mode?, fromTarget?)` method. A valid change emits one bubbling/composed `harness-resource-associate` discriminated by `mode: 'attach' | 'attach-additional' | 'move' | 'detach'` and carrying `resource`, `fromTarget`, `toTarget`, and `source: 'action' | 'keyboard' | 'pointer'`. The method returns `false` and emits nothing for unknown resources, unknown or ineligible targets, a target that is the resource itself, a target already in the set, a `move`/`detach` whose `fromTarget` is not in the set, an `attach` on an already attached resource, an `attach-additional` on an unattached one, or an ambiguous request with no mode. It never mutates resources, associations, selection, groups, or order.
2842
2874
 
2843
- Association projection is deterministic and non-destructive: resource records without a non-empty ID, kind, or title, unknown resource/target references, self-targets, and later duplicate records for a resource are ignored; the first valid association wins. Duplicate resource metadata IDs likewise use the first resource. Eligibility does not hide an existing authoritative association, which remains visible and detachable. Requested changes are announced separately from completion; completion is announced only after the controlled association input reflects the requested target.
2875
+ Association projection is deterministic and non-destructive: resource records without a non-empty ID, kind, or title, unknown resource/target references, self-targets, and repeated targets are ignored; several records for one resource merge into one de-duplicated set. Duplicate resource metadata IDs likewise use the first resource. Eligibility does not hide an existing authoritative association, which remains visible and detachable. Requested changes are announced separately from completion; completion is announced only after the controlled association input reflects the requested set.
2844
2876
 
2845
2877
  Search covers resource ID, kind, title, and preview, and matches sessions and resources as peers — a matching session no longer reveals its attached resources, and a matching resource no longer reveals its target. Resources render with zero sessions. Resource dragging edge-scrolls the list exactly like session dragging and emits only `harness-item-move`. Use the Actions/context path for keyboard and mobile association workflows; coarse-pointer grip and Actions targets are at least 44px.
2846
2878
 
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@design.estate/dees-catalog',
6
- version: '11.2.2',
6
+ version: '12.0.0',
7
7
  description: 'A comprehensive library that provides dynamic web components for building sophisticated and modern web applications using JavaScript and TypeScript.'
8
8
  }
@@ -0,0 +1,46 @@
1
+ import { html } from '@design.estate/dees-element';
2
+ import { demoPage } from '../../../demos/templates.js';
3
+ import type { IHarnessSessionGroup, IHarnessSessionMeta, IHarnessResourceMeta } from '../interfaces.js';
4
+
5
+ const demoSessions: IHarnessSessionMeta[] = [
6
+ { id: 's1', title: 'Fix sitemap cache key', projectLabel: 'storefront', updatedAt: Date.now() - 1_000 },
7
+ { id: 's2', title: 'Investigate model rate limits', projectLabel: 'gateway', updatedAt: Date.now() - 2_000 },
8
+ { id: 's3', title: 'Prepare the catalog release', projectLabel: 'catalog', updatedAt: Date.now() - 3_000 },
9
+ { id: 's4', title: 'Improve agent onboarding', updatedAt: Date.now() - 4_000 },
10
+ ];
11
+
12
+ const demoResources: IHarnessResourceMeta[] = [
13
+ { id: 'terminal:build', kind: 'terminal', title: 'Build shell', icon: 'lucide:Terminal', projectLabel: 'storefront' },
14
+ ];
15
+
16
+ const demoGroups: IHarnessSessionGroup[] = [
17
+ { id: 'active', name: 'Workspace', itemIds: [{ kind: 'session', id: 's1' }, { kind: 'session', id: 's2' }] },
18
+ { id: 'review', name: 'Review', itemIds: [{ kind: 'session', id: 's3' }] },
19
+ ];
20
+
21
+ export const demoFunc = () => demoPage(
22
+ 'Conversation picker',
23
+ 'Keyboard-first target picker: type to filter on title, project label or group, Enter picks, Escape cancels.',
24
+ html`
25
+ <style>
26
+ .pickerDemo {
27
+ max-width: 420px;
28
+ padding: 16px;
29
+ border: 1px solid var(--dees-color-border-subtle);
30
+ border-radius: var(--dees-radius-xl);
31
+ background: var(--dees-color-bg-secondary);
32
+ }
33
+ </style>
34
+ <div class="pickerDemo">
35
+ <dees-harness-conversation-picker
36
+ heading="Attach Build shell to…"
37
+ .sessions=${demoSessions}
38
+ .groups=${demoGroups}
39
+ .resources=${demoResources}
40
+ .excludeTargets=${[{ kind: 'session' as const, id: 's4' }]}
41
+ @harness-picker-choose=${(event: CustomEvent) => console.log('picked', event.detail.target)}
42
+ @harness-picker-cancel=${() => console.log('cancelled')}
43
+ ></dees-harness-conversation-picker>
44
+ </div>
45
+ `,
46
+ );