@ixfx/components 0.16.1 → 0.16.2

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 (89) hide show
  1. package/bundle/index.d.ts +216 -56
  2. package/bundle/index.js +370 -315
  3. package/dist/ac-text.d.ts +1 -1
  4. package/dist/{ac-token-qEs3FsQv.d.ts → ac-token-Cq7lsYf-.d.ts} +1 -1
  5. package/dist/ac-token.d.ts +1 -1
  6. package/dist/ac-token.js +1 -1
  7. package/dist/{button-N92DKoWb.d.ts → button-D1sPDUCM.d.ts} +1 -1
  8. package/dist/button.d.ts +1 -1
  9. package/dist/colour-picker.d.ts +1 -1
  10. package/dist/{crumb-navigation-YnFVFqrx.js → crumb-navigation-D3ndPgFg.js} +2 -2
  11. package/dist/{crumb-navigation-BbqfmkYs.d.ts → crumb-navigation-nnr881n-.d.ts} +1 -1
  12. package/dist/crumbs.d.ts +1 -1
  13. package/dist/crumbs.js +1 -1
  14. package/dist/{data-grid-Csh5EJW9.d.ts → data-grid-BAEpfTkb.d.ts} +22 -4
  15. package/dist/{data-grid-Cqxuwgbb.js → data-grid-CaPLWt5E.js} +138 -57
  16. package/dist/data-grid.d.ts +2 -2
  17. package/dist/data-grid.js +1 -1
  18. package/dist/{detail-list-Bo5ze3SN.js → detail-list-C3XKTRmE.js} +71 -20
  19. package/dist/{detail-list-DSZ1DDor.d.ts → detail-list-DK49TsJd.d.ts} +12 -4
  20. package/dist/detail-list.d.ts +2 -2
  21. package/dist/detail-list.js +1 -1
  22. package/dist/{flat-list-CT9kX3TQ.d.ts → flat-list-Bv4-qfjp.d.ts} +9 -4
  23. package/dist/flat-list.d.ts +1 -1
  24. package/dist/flat-list.js +6 -3
  25. package/dist/{grid-list-D0dJNVEF.js → grid-list-51XsR2X2.js} +55 -18
  26. package/dist/{grid-list-DDLrWRxm.d.ts → grid-list-DaeSHv9P.d.ts} +58 -14
  27. package/dist/grid-list.d.ts +2 -2
  28. package/dist/grid-list.js +1 -1
  29. package/dist/{grouped-item-lister-DETte0Dh.d.ts → grouped-item-lister-DZIjo9nO.d.ts} +2 -2
  30. package/dist/grouped-item-lister.d.ts +1 -1
  31. package/dist/{icon-CDGprSdE.d.ts → icon-DR_3rSyZ.d.ts} +1 -1
  32. package/dist/icons.d.ts +2 -2
  33. package/dist/{incr-search-CgbL-OlS.js → incr-search-2YaxUUA9.js} +10 -1
  34. package/dist/incr-search.d.ts +1 -1
  35. package/dist/incr-search.js +1 -1
  36. package/dist/{index-CoqtMSd9.d.ts → index-BUB5SICW.d.ts} +1 -1
  37. package/dist/{index-CUErGhCg.d.ts → index-BjRoPrwj.d.ts} +1 -1
  38. package/dist/{index-DN2BWctP.d.ts → index-Bz3loIAf.d.ts} +1 -1
  39. package/dist/{index-UvnoZUAo.d.ts → index-CJ5fshD3.d.ts} +1 -1
  40. package/dist/{index-DxyyVqzA.d.ts → index-DM9OLgTH.d.ts} +3 -2
  41. package/dist/{index-Bnm-kKGC.d.ts → index-DXDz60XH.d.ts} +20 -1
  42. package/dist/{index-CYndvTWH.d.ts → index-garxxqgV.d.ts} +1 -1
  43. package/dist/{index-D-lZNlHa.d.ts → index-vdbbGE7N.d.ts} +1 -1
  44. package/dist/index.d.ts +45 -25
  45. package/dist/index.js +15 -14
  46. package/dist/item-appearance-Bl6rMIID.js +60 -0
  47. package/dist/{labelled-input-base-DiTme1JO.d.ts → labelled-input-base-Dp_9KP0G.d.ts} +1 -1
  48. package/dist/labelled-radial-input.d.ts +2 -2
  49. package/dist/labelled-range-input.d.ts +1 -1
  50. package/dist/labelled-slider-input.d.ts +1 -1
  51. package/dist/list-group-header.d.ts +1 -1
  52. package/dist/{list-selection-controller-5L0Nj9tR.js → list-selection-controller-CJDdjH7E.js} +192 -54
  53. package/dist/list-selection-types-hj9Njei3.d.ts +83 -0
  54. package/dist/{menu-CU9KAm8s.js → menu-BX1FHxY8.js} +16 -1
  55. package/dist/{menu-item-bF6narIz.d.ts → menu-item-B0zqw2xp.d.ts} +1 -1
  56. package/dist/{menu-item-BUv8An2k.js → menu-item-oxU1VKiJ.js} +34 -9
  57. package/dist/menu.d.ts +2 -2
  58. package/dist/menu.js +2 -2
  59. package/dist/{miller-DUqiUAWQ.js → miller-BfrTcujx.js} +142 -11
  60. package/dist/miller.d.ts +1 -1
  61. package/dist/miller.js +1 -1
  62. package/dist/narrowed-text.js +1 -1
  63. package/dist/panel.d.ts +1 -1
  64. package/dist/{split-layout-CophkNeU.d.ts → split-layout-ChZ0Glrs.d.ts} +1 -1
  65. package/dist/split-layout.d.ts +1 -1
  66. package/dist/sunburst-tree.d.ts +1 -1
  67. package/dist/sunburst-tree.js +1 -1
  68. package/dist/{sunbursttree-list-B_M0YTEA.js → sunbursttree-list-906G33ME.js} +1 -1
  69. package/dist/{sunbursttree-list-DePuFYlU.d.ts → sunbursttree-list-BAS0KrDG.d.ts} +1 -1
  70. package/dist/{tab-list-Df371rqw.d.ts → tab-list-wC0xNz1b.d.ts} +1 -1
  71. package/dist/tabs.d.ts +1 -1
  72. package/dist/{titlebar-Tqu-VvMU.d.ts → titlebar-B5zSTZnF.d.ts} +1 -1
  73. package/dist/titlebar.d.ts +1 -1
  74. package/dist/{tree-DAlgvaHB.js → tree-C_H1COTf.js} +1 -1
  75. package/dist/tree.d.ts +1 -1
  76. package/dist/tree.js +1 -1
  77. package/dist/{vertical-list-SAx56U-l.d.ts → vertical-list-od4050Bo.d.ts} +11 -2
  78. package/dist/{vertical-list-DyKPKGxK.js → vertical-list-qMRIxZIT.js} +54 -20
  79. package/dist/vertical-list.d.ts +2 -2
  80. package/dist/vertical-list.js +1 -1
  81. package/docs-user/data-grid.md +10 -3
  82. package/docs-user/detail-list.md +1 -1
  83. package/docs-user/grid-list.md +4 -1
  84. package/docs-user/index.json +2 -2
  85. package/docs-user/menu.md +11 -1
  86. package/docs-user/vertical-list.md +9 -1
  87. package/package.json +1 -1
  88. package/dist/item-appearance-BjcMyauw.js +0 -29
  89. package/dist/list-selection-types-PjKdhF-a.d.ts +0 -45
@@ -42,6 +42,7 @@ var ListDragSelectController = class {
42
42
  /** Combine mode of the most recent preview (drives the commit flag). */
43
43
  #lastMode = `replace`;
44
44
  #preview = /* @__PURE__ */ new Set();
45
+ #covered = /* @__PURE__ */ new Set();
45
46
  #raf = 0;
46
47
  constructor(host, config) {
47
48
  this.#host = host;
@@ -129,6 +130,7 @@ var ListDragSelectController = class {
129
130
  this.#suppressClick = false;
130
131
  this.#base = /* @__PURE__ */ new Set();
131
132
  this.#lastMode = `replace`;
133
+ this.#covered = /* @__PURE__ */ new Set();
132
134
  this.#lastModifiers = {
133
135
  shiftKey: event.shiftKey,
134
136
  ctrlKey: event.ctrlKey,
@@ -211,6 +213,7 @@ var ListDragSelectController = class {
211
213
  preview = toggled;
212
214
  } else preview = hitSet;
213
215
  this.#preview = preview;
216
+ this.#covered = hitSet;
214
217
  try {
215
218
  this.#config.onPreview(preview, rect);
216
219
  } catch {}
@@ -236,7 +239,10 @@ var ListDragSelectController = class {
236
239
  this.#dragging = false;
237
240
  const final = this.#preview;
238
241
  try {
239
- this.#config.onCommit(final, { additive: this.#lastMode !== `replace` });
242
+ this.#config.onCommit(final, {
243
+ additive: this.#lastMode !== `replace`,
244
+ covered: this.#covered
245
+ });
240
246
  } catch {}
241
247
  try {
242
248
  this.#config.onPreview(/* @__PURE__ */ new Set(), void 0);
@@ -296,10 +302,20 @@ var ListDragSelectController = class {
296
302
  this.#base = /* @__PURE__ */ new Set();
297
303
  this.#lastMode = `replace`;
298
304
  this.#preview = /* @__PURE__ */ new Set();
305
+ this.#covered = /* @__PURE__ */ new Set();
299
306
  }
300
307
  };
301
308
  //#endregion
302
309
  //#region src/interaction/list-selection-controller.ts
310
+ /** Capture the modifier / pointer-type state of a click for `handleClick`. */
311
+ function gestureOf(e) {
312
+ return {
313
+ ctrlKey: e.ctrlKey,
314
+ metaKey: e.metaKey,
315
+ shiftKey: e.shiftKey,
316
+ pointerType: e.pointerType
317
+ };
318
+ }
303
319
  /** Whether the interaction mode allows building a multi-item selection. */
304
320
  function isMultiMode(mode) {
305
321
  return mode === `checked` || mode === `sticky` || mode === `vscode` || mode === `standard`;
@@ -399,36 +415,36 @@ var ListSelectionController = class {
399
415
  /** Select the contiguous ordered range from the anchor to `item`. Anchor unchanged. */
400
416
  rangeTo(item) {
401
417
  if (this.#config.getModes().selectionMode === `none`) return;
402
- if (this.#anchor === void 0) {
418
+ const range = this.#rangeItems(item);
419
+ if (range === void 0) {
403
420
  this.replace(item);
404
421
  return;
405
422
  }
423
+ this.#apply(new Set(range));
424
+ }
425
+ /**
426
+ * The selectable items between the anchor and `item` (inclusive), or
427
+ * `undefined` when there is no usable range (no anchor, an unresolved end,
428
+ * or a different group) and the gesture degrades to a plain `replace`.
429
+ */
430
+ #rangeItems(item) {
431
+ if (this.#anchor === void 0) return void 0;
406
432
  const ordered = this.#config.getOrderedItems();
407
433
  const anchorIdx = ordered.indexOf(this.#anchor);
408
434
  const targetIdx = ordered.indexOf(item);
409
- if (anchorIdx === -1 || targetIdx === -1) {
410
- this.replace(item);
411
- return;
412
- }
435
+ if (anchorIdx === -1 || targetIdx === -1) return void 0;
436
+ let scope = ordered;
437
+ let a = anchorIdx;
438
+ let b = targetIdx;
413
439
  if (this.#clamped()) {
414
440
  const group = this.#groupOf(this.#anchor);
415
- if (group !== this.#groupOf(item)) {
416
- this.replace(item);
417
- return;
418
- }
419
- const inGroup = ordered.filter((t) => this.#groupOf(t) === group);
420
- const a = inGroup.indexOf(this.#anchor);
421
- const b = inGroup.indexOf(item);
422
- if (a === -1 || b === -1) {
423
- this.replace(item);
424
- return;
425
- }
426
- this.#apply(new Set(inGroup.slice(Math.min(a, b), Math.max(a, b) + 1).filter((t) => this.#config.isSelectable(t))));
427
- return;
441
+ if (group !== this.#groupOf(item)) return void 0;
442
+ scope = ordered.filter((t) => this.#groupOf(t) === group);
443
+ a = scope.indexOf(this.#anchor);
444
+ b = scope.indexOf(item);
445
+ if (a === -1 || b === -1) return void 0;
428
446
  }
429
- const start = Math.min(anchorIdx, targetIdx);
430
- const end = Math.max(anchorIdx, targetIdx);
431
- this.#apply(new Set(ordered.slice(start, end + 1).filter((t) => this.#config.isSelectable(t))));
447
+ return scope.slice(Math.min(a, b), Math.max(a, b) + 1).filter((t) => this.#config.isSelectable(t));
432
448
  }
433
449
  /** Clear the selection. No change event when the selection is already empty. */
434
450
  clear() {
@@ -436,8 +452,16 @@ var ListSelectionController = class {
436
452
  if (this.#selected.size === 0) return;
437
453
  this.#apply(/* @__PURE__ */ new Set());
438
454
  }
439
- /** Select every ordered item that passes `isSelectable`. */
440
- selectAll() {
455
+ /**
456
+ * Select every ordered item that passes `isSelectable`. Fires an `all`
457
+ * intent first; the caller may take over.
458
+ */
459
+ selectAll(source = `api`) {
460
+ if (this.#intercepted({
461
+ kind: `all`,
462
+ keys: [],
463
+ source
464
+ })) return;
441
465
  let ordered = this.#config.getOrderedItems();
442
466
  if (ordered.length === 0) return;
443
467
  if (this.#clamped()) {
@@ -478,9 +502,20 @@ var ListSelectionController = class {
478
502
  * non-empty selection (e.g. a Cmd-drag that toggles everything off) but is
479
503
  * a no-op when nothing is selected.
480
504
  */
481
- applyDragSelection(items) {
505
+ applyDragSelection(items, info) {
482
506
  if (!this.canDragSelect()) return;
483
- const next = this.#clampToFirstGroup(new Set([...items].filter((t) => this.#config.isSelectable(t))));
507
+ const final = [...items];
508
+ const covered = info ? [...info.covered].filter((t) => this.#config.isSelectable(t)) : final;
509
+ if (this.#intercepted({
510
+ kind: `marquee`,
511
+ keys: this.#keysOf(covered),
512
+ additive: info?.additive ?? false,
513
+ source: `pointer`
514
+ })) {
515
+ this.#anchor = final.at(-1) ?? this.#anchor;
516
+ return;
517
+ }
518
+ const next = this.#clampToFirstGroup(new Set(final.filter((t) => this.#config.isSelectable(t))));
484
519
  if (next.size === 0 && this.#selected.size === 0) return;
485
520
  this.#anchor = [...next].at(-1);
486
521
  this.#apply(next);
@@ -503,54 +538,67 @@ var ListSelectionController = class {
503
538
  }
504
539
  /**
505
540
  * Replace the selection with the items resolved from `keys`. Keys that
506
- * `resolve` cannot map, or that resolve to a non-selectable item, are
507
- * ignored. `selectionMode="none"` is a no-op. When multi-select is not
508
- * available, only the last resolvable key is kept. `onChange` fires once.
541
+ * `resolve` cannot map yet are kept pending and selected when their items
542
+ * appear (see `resolvePending()`); keys that resolve to a non-selectable
543
+ * item are ignored. `selectionMode="none"` is a no-op. When multi-select is
544
+ * not available, only the last key is kept. `onChange` fires once.
509
545
  */
510
546
  selectKeys(keys, resolve) {
511
547
  if (this.#config.getModes().selectionMode === `none`) return;
512
548
  let next = /* @__PURE__ */ new Set();
549
+ const missing = /* @__PURE__ */ new Set();
513
550
  for (const key of new Set(keys)) {
514
551
  const item = resolve(key);
515
- if (item !== void 0 && this.#config.isSelectable(item)) next.add(item);
552
+ if (item === void 0) missing.add(key);
553
+ else if (this.#config.isSelectable(item)) next.add(item);
516
554
  }
517
- if (!this.#effectiveMulti() && next.size > 1) {
518
- const last = [...next].at(-1);
519
- next = last === void 0 ? /* @__PURE__ */ new Set() : /* @__PURE__ */ new Set([last]);
555
+ let pending = missing;
556
+ if (!this.#effectiveMulti()) {
557
+ if (next.size > 0) {
558
+ next = new Set([...next].slice(-1));
559
+ pending = EMPTY_KEYS;
560
+ } else pending = new Set([...missing].slice(-1));
520
561
  }
521
562
  this.#anchor = [...next].at(-1);
522
- this.#apply(next);
563
+ this.#pending = capKeys(pending);
564
+ this.#apply(next, true);
523
565
  }
524
566
  /**
525
567
  * Add the items resolved from `keys` to the selection (multi-select only;
526
- * a no-op otherwise). `onChange` fires once if anything changed.
568
+ * a no-op otherwise). Keys not present yet are kept pending. `onChange`
569
+ * fires once if anything changed.
527
570
  */
528
571
  addKeys(keys, resolve) {
529
572
  if (!this.#effectiveMulti()) return;
530
573
  const next = new Set(this.#selected);
574
+ const pending = new Set(this.#pending);
531
575
  let changed = false;
532
576
  for (const key of new Set(keys)) {
533
577
  const item = resolve(key);
534
- if (item !== void 0 && !next.has(item) && this.#config.isSelectable(item)) {
578
+ if (item === void 0) pending.add(key);
579
+ else if (!next.has(item) && this.#config.isSelectable(item)) {
535
580
  next.add(item);
536
581
  changed = true;
537
582
  }
538
583
  }
539
- if (changed) this.#apply(next);
584
+ if (changed) this.#apply(next, true);
585
+ this.#pending = capKeys(pending);
540
586
  }
541
587
  /**
542
- * Remove the items resolved from `keys` from the selection. `onChange`
543
- * fires once if anything changed.
588
+ * Remove the items resolved from `keys` from the selection, and drop them
589
+ * from the pending keys. `onChange` fires once if anything changed.
544
590
  */
545
591
  removeKeys(keys, resolve) {
592
+ const wanted = new Set(keys);
593
+ if (this.#pending.size > 0) this.#pending = new Set([...this.#pending].filter((k) => !wanted.has(k)));
546
594
  if (this.#selected.size === 0) return;
547
595
  const next = new Set(this.#selected);
548
596
  let changed = false;
549
- for (const key of new Set(keys)) {
597
+ for (const key of wanted) {
550
598
  const item = resolve(key);
551
599
  if (item !== void 0 && next.delete(item)) changed = true;
552
600
  }
553
- if (changed) this.#apply(next);
601
+ if (changed) this.#apply(next, true);
554
602
  }
555
603
  /** Selected keys waiting for their items to appear. */
556
604
  get pendingKeys() {
@@ -625,32 +673,122 @@ var ListSelectionController = class {
625
673
  const item = resolve(key);
626
674
  return item !== void 0 && this.#config.isSelectable(item) ? item : void 0;
627
675
  }
628
- /** Route a click on `item` according to the current interaction mode. */
676
+ /**
677
+ * Route a click on `item` according to the current interaction mode. In
678
+ * `manual` mode nothing is selected, but the intent is still reported so
679
+ * the caller can apply it.
680
+ */
629
681
  handleClick(item, gesture) {
630
682
  const { selectionMode, interactionMode: mode } = this.#config.getModes();
631
- if (mode === `manual`) return;
632
683
  if (selectionMode === `none` && mode !== `checked` && mode !== `sticky`) return;
684
+ const touch = gesture.pointerType === `touch`;
685
+ const modified = gesture.ctrlKey || gesture.metaKey;
686
+ const multi = selectionMode === `multiple` || mode === `vscode`;
633
687
  switch (mode) {
634
688
  case `implicit`:
635
- this.replace(item);
689
+ this.#replaceIntent(item, `pointer`);
690
+ break;
691
+ case `manual`:
692
+ if (modified) this.#toggleIntent(item, `pointer`);
693
+ else if (gesture.shiftKey) this.#extendIntent(item, `pointer`);
694
+ else this.#replaceIntent(item, `pointer`);
636
695
  break;
637
696
  case `standard`:
638
697
  case `vscode`:
639
- if (gesture.ctrlKey || gesture.metaKey) {
640
- if (selectionMode === `multiple` || mode === `vscode`) this.toggle(item);
641
- } else if (gesture.shiftKey && (selectionMode === `multiple` || mode === `vscode`)) this.rangeTo(item);
642
- else this.replace(item);
698
+ if (modified) {
699
+ if (multi) this.#toggleIntent(item, `pointer`);
700
+ } else if (gesture.shiftKey && multi) this.#extendIntent(item, `pointer`);
701
+ else this.#replaceIntent(item, `pointer`);
643
702
  break;
644
703
  case `checked`: break;
645
- case `sticky`: this.toggle(item);
704
+ case `sticky`: this.#toggleIntent(item, touch ? `touch` : `pointer`);
646
705
  }
647
706
  }
648
707
  /** Route a Space keypress on `item` according to the current interaction mode. */
649
708
  handleSpace(item) {
650
- const mode = this.#config.getModes().interactionMode;
651
- if (mode === `manual`) return;
652
- if (mode === `implicit`) this.replace(item);
653
- else this.toggle(item);
709
+ if (this.#config.getModes().interactionMode === `implicit`) this.#replaceIntent(item, `keyboard`);
710
+ else this.#toggleIntent(item, `keyboard`);
711
+ }
712
+ /** Route a checkbox (checked-mode) toggle on `item`. */
713
+ handleCheckbox(item, source = `pointer`) {
714
+ this.#toggleIntent(item, source);
715
+ }
716
+ /** Route a plain "select only this" gesture (Enter, double-click) on `item`. */
717
+ handleReplace(item, source = `pointer`) {
718
+ this.#replaceIntent(item, source);
719
+ }
720
+ /** Route Shift+Arrow: extend the range from the anchor to the cursor `item`. */
721
+ handleExtend(item) {
722
+ this.#extendIntent(item, `keyboard`);
723
+ }
724
+ /**
725
+ * Route Escape / empty-space click: clear the selection. `reportOnly`
726
+ * suppresses the default action in `manual` mode (the caller owns it).
727
+ */
728
+ handleClear(source = `keyboard`, reportOnly = false) {
729
+ if (this.#intercepted({
730
+ kind: `clear`,
731
+ keys: [],
732
+ source
733
+ })) return;
734
+ if (reportOnly && this.#config.getModes().interactionMode === `manual`) return;
735
+ this.clear();
736
+ }
737
+ #replaceIntent(item, source) {
738
+ if (this.#config.getModes().selectionMode === `none`) return;
739
+ if (!this.#config.isSelectable(item)) return;
740
+ if (this.#intercepted({
741
+ kind: `replace`,
742
+ keys: this.#keysOf([item]),
743
+ source
744
+ })) {
745
+ this.#anchor = item;
746
+ return;
747
+ }
748
+ if (this.#config.getModes().interactionMode === `manual`) return;
749
+ this.replace(item);
750
+ }
751
+ #toggleIntent(item, source) {
752
+ if (!this.#config.isSelectable(item)) return;
753
+ if (this.#intercepted({
754
+ kind: `toggle`,
755
+ keys: this.#keysOf([item]),
756
+ source
757
+ })) return;
758
+ if (this.#config.getModes().interactionMode === `manual`) return;
759
+ this.toggle(item);
760
+ }
761
+ #extendIntent(item, source) {
762
+ if (this.#config.getModes().selectionMode === `none`) return;
763
+ const range = this.#anchor === void 0 ? void 0 : this.#rangeItems(item);
764
+ const anchorKey = this.#anchor === void 0 ? void 0 : this.#config.getKey?.(this.#anchor);
765
+ const targetKey = this.#config.getKey?.(item);
766
+ if (this.#intercepted({
767
+ kind: `extend`,
768
+ keys: this.#keysOf(range ?? [item]),
769
+ ...anchorKey === void 0 ? {} : { anchorKey },
770
+ ...targetKey === void 0 ? {} : { targetKey },
771
+ source
772
+ })) {
773
+ if (this.#anchor === void 0) this.#anchor = item;
774
+ return;
775
+ }
776
+ if (this.#config.getModes().interactionMode === `manual`) return;
777
+ this.rangeTo(item);
778
+ }
779
+ #keysOf(items) {
780
+ const getKey = this.#config.getKey;
781
+ if (!getKey) return [];
782
+ const out = [];
783
+ for (const item of items) {
784
+ const key = getKey(item);
785
+ if (key !== void 0) out.push(key);
786
+ }
787
+ return out;
788
+ }
789
+ /** Report an intent; `true` when the caller took over. */
790
+ #intercepted(intent) {
791
+ return this.#config.onIntent?.(intent) ?? false;
654
792
  }
655
793
  /** Drop the range-selection anchor (e.g. when the interaction mode changes). */
656
794
  clearAnchor() {
@@ -707,4 +845,4 @@ var ListSelectionController = class {
707
845
  }
708
846
  };
709
847
  //#endregion
710
- export { isMultiMode as n, ListDragSelectController as r, ListSelectionController as t };
848
+ export { ListDragSelectController as i, gestureOf as n, isMultiMode as r, ListSelectionController as t };
@@ -0,0 +1,83 @@
1
+ //#region src/interaction/list-selection-types.d.ts
2
+ type ListSelectionMode = `none` | `single` | `multiple`;
3
+ /**
4
+ * Controls how pointer and keyboard gestures map to selection changes.
5
+ *
6
+ * - `implicit` — click always replaces selection (no modifier support)
7
+ * - `standard` — click replaces; Ctrl/Cmd+click toggles; Shift+click range-selects (multiple only)
8
+ * - `manual` — no auto-selection; drive state via select()/deselect()/clearSelection()
9
+ * - `checked` — every click toggles; implies multiple selection regardless of selectionMode
10
+ * - `sticky` — every tap/click toggles (no checkbox column); implies multiple selection, for touchscreens
11
+ * - `vscode` — like standard for clicks; Shift+Arrow extends range from the keyboard anchor
12
+ *
13
+ * Switching interactionMode clears the current selection.
14
+ */
15
+ type ListInteractionMode = `implicit` | `standard` | `manual` | `checked` | `sticky` | `vscode`;
16
+ type ListSelectDetail = {
17
+ readonly selected: ReadonlySet<Element>;
18
+ readonly previous: ReadonlySet<Element>;
19
+ /**
20
+ * The selection as stable key strings (`data-key` / `data-value`). Empty
21
+ * when items carry no key. Additive to `selected`.
22
+ */
23
+ readonly selectedKeys: ReadonlySet<string>;
24
+ /** The previous selection as keys. Additive to `previous`. */
25
+ readonly previousKeys: ReadonlySet<string>;
26
+ };
27
+ /**
28
+ * The key-based subset of `list-select` detail common to every list-like
29
+ * component (including `ixfx-data-grid`), so one adapter can read any of them.
30
+ */
31
+ type ListSelectKeysDetail = Pick<ListSelectDetail, `selectedKeys` | `previousKeys`>;
32
+ /**
33
+ * What the user (or API) is trying to do to the selection. See
34
+ * {@link ListSelectIntentDetail}.
35
+ */
36
+ type ListSelectIntentKind = `replace` | `toggle` | `extend` | `all` | `marquee` | `clear`;
37
+ type ListSelectIntentSource = `pointer` | `keyboard` | `touch` | `api`;
38
+ /**
39
+ * Detail of the cancelable `list-select-intent` event: fired *before* a
40
+ * component changes its selection, in every interaction mode (including
41
+ * `manual`). Calling `preventDefault()` means "the caller applies this
42
+ * itself" — the component then changes nothing (cursor and range anchor still
43
+ * move). Without `preventDefault()` behaviour is unchanged.
44
+ *
45
+ * Only gestures and `selectAll()` fire it. The imperative `select()` /
46
+ * `selectKeys()` / `clearSelection()` family never does, so a handler can
47
+ * apply its decision through them without re-entering itself.
48
+ */
49
+ type ListSelectIntentDetail = {
50
+ readonly kind: ListSelectIntentKind;
51
+ /** Keys directly affected. Empty for `all` / `clear`. For `extend`, the range (a convenience: callers should use the endpoints). */
52
+ readonly keys: readonly string[];
53
+ /** Range anchor, for `extend`. */
54
+ readonly anchorKey?: string;
55
+ /** Range end, for `extend`. */
56
+ readonly targetKey?: string;
57
+ /** Add to rather than replace the selection (a modifier was held). */
58
+ readonly additive?: boolean;
59
+ readonly source: ListSelectIntentSource;
60
+ };
61
+ type ListTickleDetail = {
62
+ readonly item: Element;
63
+ };
64
+ type ListActivateDetail = {
65
+ readonly item: Element;
66
+ };
67
+ type ListItemClickDetail = {
68
+ readonly item: Element;
69
+ };
70
+ type ListSelectEvent = CustomEvent<ListSelectDetail>;
71
+ type ListSelectIntentEvent = CustomEvent<ListSelectIntentDetail>;
72
+ type ListTickleEvent = CustomEvent<ListTickleDetail>;
73
+ type ListActivateEvent = CustomEvent<ListActivateDetail>;
74
+ type ListItemClickEvent = CustomEvent<ListItemClickDetail>;
75
+ interface ListElementEventMap {
76
+ 'list-select': ListSelectEvent;
77
+ 'list-select-intent': ListSelectIntentEvent;
78
+ 'list-tickle': ListTickleEvent;
79
+ 'list-activate': ListActivateEvent;
80
+ 'list-item-click': ListItemClickEvent;
81
+ }
82
+ //#endregion
83
+ export { ListItemClickDetail as a, ListSelectEvent as c, ListSelectIntentKind as d, ListSelectIntentSource as f, ListTickleEvent as g, ListTickleDetail as h, ListInteractionMode as i, ListSelectIntentDetail as l, ListSelectionMode as m, ListActivateEvent as n, ListItemClickEvent as o, ListSelectKeysDetail as p, ListElementEventMap as r, ListSelectDetail as s, ListActivateDetail as t, ListSelectIntentEvent as u };
@@ -4,7 +4,7 @@ import { t as themeFallbacks } from "./fallbacks-DQ_5OsmR.js";
4
4
  import { n as safeCustomElement, t as __decorate } from "./decorate-VRK8rslU.js";
5
5
  import "./icon-1gpwtPx8.js";
6
6
  import "./interaction-E-uMGHkJ.js";
7
- import { n as MenuTrigger, r as MenuContainer, t as MenuItem } from "./menu-item-BUv8An2k.js";
7
+ import { n as MenuTrigger, r as MenuContainer, t as MenuItem } from "./menu-item-oxU1VKiJ.js";
8
8
  import { LitElement, css, html, nothing } from "lit";
9
9
  import { property, query, state } from "lit/decorators.js";
10
10
  //#region src/menu/menu-bar.ts
@@ -16,6 +16,15 @@ let MenuBar = class MenuBar extends LitElement {
16
16
  this._handlePointerMove = () => {
17
17
  this._setInputMode(`mouse`);
18
18
  };
19
+ this._handlePointerOver = (e) => {
20
+ if (!this._openTrigger || e.pointerType === `touch`) return;
21
+ const triggers = this._getTriggers();
22
+ const path = e.composedPath();
23
+ const trigger = triggers.find((t) => path.includes(t));
24
+ if (!trigger || trigger === this._openTrigger) return;
25
+ this._openTrigger.closeMenu();
26
+ trigger.openMenu();
27
+ };
19
28
  this._handleTriggerOpen = (e) => {
20
29
  this._openTrigger = e.target;
21
30
  };
@@ -53,6 +62,7 @@ let MenuBar = class MenuBar extends LitElement {
53
62
  this.addEventListener(`focusin`, this._handleFocusIn);
54
63
  this.addEventListener(`keydown`, this._handleKeydown);
55
64
  this.addEventListener(`pointermove`, this._handlePointerMove);
65
+ this.addEventListener(`pointerover`, this._handlePointerOver);
56
66
  }
57
67
  disconnectedCallback() {
58
68
  super.disconnectedCallback();
@@ -61,6 +71,7 @@ let MenuBar = class MenuBar extends LitElement {
61
71
  this.removeEventListener(`focusin`, this._handleFocusIn);
62
72
  this.removeEventListener(`keydown`, this._handleKeydown);
63
73
  this.removeEventListener(`pointermove`, this._handlePointerMove);
74
+ this.removeEventListener(`pointerover`, this._handlePointerOver);
64
75
  }
65
76
  _setInputMode(mode) {
66
77
  if (this._inputMode === mode) return;
@@ -75,6 +86,8 @@ let MenuBar = class MenuBar extends LitElement {
75
86
  :host {
76
87
  display: flex;
77
88
  align-items: center;
89
+ gap: var(--menu-bar-gap, var(--space-xs));
90
+ padding: var(--menu-bar-padding, var(--space-xs));
78
91
  }
79
92
 
80
93
  /* Keyboard mode: suppress hover highlight on trigger buttons */
@@ -385,6 +398,7 @@ let MenuSubTrigger = class MenuSubTrigger extends LitElement {
385
398
  }
386
399
  _handleKeydown(e) {
387
400
  if (this.disabled) return;
401
+ if (e.target !== this !== (e.key === `ArrowLeft` || e.key === `Escape`)) return;
388
402
  switch (e.key) {
389
403
  case `ArrowRight`:
390
404
  case `Enter`:
@@ -408,6 +422,7 @@ let MenuSubTrigger = class MenuSubTrigger extends LitElement {
408
422
  e.preventDefault();
409
423
  e.stopPropagation();
410
424
  this._hideSubmenu();
425
+ this._itemEl?.focus();
411
426
  }
412
427
  }
413
428
  static {
@@ -1,4 +1,4 @@
1
- import "./icon-CDGprSdE.js";
1
+ import "./icon-DR_3rSyZ.js";
2
2
  import { LitElement } from "lit";
3
3
  import { CommandRegistry, KeyboardManager } from "@clinth/ui-commands";
4
4
  //#region src/menu/menu-container.d.ts
@@ -5,6 +5,7 @@ import { t as popupSurfaceStyles } from "./popup-D80sj2QC.js";
5
5
  import { n as safeCustomElement, t as __decorate } from "./decorate-VRK8rslU.js";
6
6
  import "./icon-1gpwtPx8.js";
7
7
  import "./interaction-E-uMGHkJ.js";
8
+ import { t as scrollFadeStyles } from "./scroll-fade-Xx28D7mH.js";
8
9
  import { LitElement, css, html } from "lit";
9
10
  import { property, query, state } from "lit/decorators.js";
10
11
  //#region src/menu/menu-container.ts
@@ -24,6 +25,7 @@ let MenuContainer = class MenuContainer extends LitElement {
24
25
  const item = items[index];
25
26
  item.tabIndex = 0;
26
27
  item.focus({ preventScroll: true });
28
+ if (this.tickled.inputMode === `keyboard`) item.scrollIntoView({ block: `nearest` });
27
29
  }
28
30
  },
29
31
  onStateChange: () => {
@@ -78,16 +80,29 @@ let MenuContainer = class MenuContainer extends LitElement {
78
80
  popupSurfaceStyles,
79
81
  themeFallbacks,
80
82
  tickledStyles,
83
+ scrollFadeStyles(`y`),
81
84
  css`
82
85
  :host {
83
86
  display: block;
84
87
  }
85
88
 
89
+ /* The surface (border, shadow) stays fixed; only the inner scroller
90
+ scrolls, so the scroll fade masks the items and not the surface edges. */
86
91
  .popover {
87
- padding: var(--space-xs) 0;
92
+ display: flex;
93
+ flex-direction: column;
94
+ max-height: var(--menu-max-height, min(60vh, 400px));
88
95
  min-width: 160px;
89
96
  }
90
97
 
98
+ .scroller {
99
+ min-height: 0;
100
+ padding: var(--space-s);
101
+ overflow-y: auto;
102
+ overscroll-behavior: contain;
103
+ --scroll-fade-width: var(--space-l);
104
+ }
105
+
91
106
  :host {
92
107
  opacity: 1;
93
108
  transform: translateY(0) scale(1);
@@ -159,7 +174,9 @@ let MenuContainer = class MenuContainer extends LitElement {
159
174
  @keydown=${this._handleKeydown}
160
175
  @mouseover=${this._handleMouseOver}
161
176
  >
162
- <slot @slotchange=${this._updateIconSections}></slot>
177
+ <div class="scroller scroll-fade-y">
178
+ <slot @slotchange=${this._updateIconSections}></slot>
179
+ </div>
163
180
  </div>
164
181
  `;
165
182
  }
@@ -541,7 +558,6 @@ let MenuTrigger = class MenuTrigger extends LitElement {
541
558
  :host {
542
559
  display: inline-block;
543
560
  position: relative;
544
- --ixfx-button-padding: var(--ixfx-button-padding, var(--space-s) var(--space-m));
545
561
  }
546
562
 
547
563
  /* Context mode with a context-target: dissolve the host's box so the
@@ -584,7 +600,7 @@ let MenuTrigger = class MenuTrigger extends LitElement {
584
600
  border: none;
585
601
  color: var(--surface-3-text);
586
602
  cursor: pointer;
587
- padding: var(--ixfx-button-padding);
603
+ padding: var(--ixfx-button-padding, var(--space-s) var(--space-m));
588
604
  font-family: var(--font-family, inherit);
589
605
  font-size: var(--text-m);
590
606
  font-weight: 600;
@@ -610,18 +626,24 @@ let MenuTrigger = class MenuTrigger extends LitElement {
610
626
  position: fixed;
611
627
  position-anchor: var(--anchor-name);
612
628
  inset: auto;
613
- margin: 8px;
629
+ /* Offset only along the placement axis so the popup's edge lines up
630
+ with the trigger's; flip fallbacks mirror the margin with it. */
631
+ margin: var(--menu-popup-offset, var(--space-xs)) 0;
614
632
  padding: 0;
615
633
  border: none;
616
634
  background: transparent;
617
635
  z-index: 100;
618
- max-height: min(60vh, 400px);
619
636
  max-width: min(90vw, 320px);
620
- overflow: auto;
621
- overscroll-behavior: contain;
637
+ /* Scrolling happens inside ixfx-menu-container (with a scroll fade).
638
+ Overriding the UA overflow:auto also stops the menu's shadow being clipped. */
639
+ overflow: visible;
622
640
  position-try-fallbacks: flip-block, flip-inline, flip-block flip-inline;
623
641
  }
624
642
 
643
+ :host([placement="right-start"]) .popup {
644
+ margin: 0 var(--menu-popup-offset, var(--space-xs));
645
+ }
646
+
625
647
  .popup[popover] {
626
648
  opacity: 1;
627
649
  transform: translateY(0) scale(1);
@@ -670,7 +692,10 @@ __decorate([property({
670
692
  })], MenuTrigger.prototype, "iconName", void 0);
671
693
  __decorate([property({ attribute: false })], MenuTrigger.prototype, "registry", void 0);
672
694
  __decorate([property({ attribute: false })], MenuTrigger.prototype, "keyboardManager", void 0);
673
- __decorate([property({ type: String })], MenuTrigger.prototype, "placement", void 0);
695
+ __decorate([property({
696
+ type: String,
697
+ reflect: true
698
+ })], MenuTrigger.prototype, "placement", void 0);
674
699
  __decorate([property({
675
700
  type: Boolean,
676
701
  attribute: `tab-stop`
package/dist/menu.d.ts CHANGED
@@ -1,3 +1,3 @@
1
- import { i as MenuContainer, n as MenuTrigger, t as MenuItem } from "./menu-item-bF6narIz.js";
2
- import { a as menuFromTree, c as MenuSeparator, d as MenuCheckItem, f as MenuBar, i as MenuFromTreeOptions, l as MenuSubTrigger, n as MenuRadioGroup, o as menuFromTreeToElements, r as MenuRadioItem, s as MenuHeader, u as MenuCommandController } from "./index-DxyyVqzA.js";
1
+ import { i as MenuContainer, n as MenuTrigger, t as MenuItem } from "./menu-item-B0zqw2xp.js";
2
+ import { a as menuFromTree, c as MenuSeparator, d as MenuCheckItem, f as MenuBar, i as MenuFromTreeOptions, l as MenuSubTrigger, n as MenuRadioGroup, o as menuFromTreeToElements, r as MenuRadioItem, s as MenuHeader, u as MenuCommandController } from "./index-DM9OLgTH.js";
3
3
  export { MenuBar, MenuCheckItem, MenuCommandController, MenuContainer, type MenuFromTreeOptions, MenuHeader, MenuItem, MenuRadioGroup, MenuRadioItem, MenuSeparator, MenuSubTrigger, MenuTrigger, menuFromTree, menuFromTreeToElements };
package/dist/menu.js CHANGED
@@ -1,3 +1,3 @@
1
- import { n as MenuTrigger, r as MenuContainer, t as MenuItem } from "./menu-item-BUv8An2k.js";
2
- import { a as menuFromTreeToElements, c as MenuSubTrigger, d as MenuBar, i as menuFromTree, l as MenuCommandController, n as MenuRadioGroup, o as MenuHeader, r as MenuRadioItem, s as MenuSeparator, u as MenuCheckItem } from "./menu-CU9KAm8s.js";
1
+ import { n as MenuTrigger, r as MenuContainer, t as MenuItem } from "./menu-item-oxU1VKiJ.js";
2
+ import { a as menuFromTreeToElements, c as MenuSubTrigger, d as MenuBar, i as menuFromTree, l as MenuCommandController, n as MenuRadioGroup, o as MenuHeader, r as MenuRadioItem, s as MenuSeparator, u as MenuCheckItem } from "./menu-BX1FHxY8.js";
3
3
  export { MenuBar, MenuCheckItem, MenuCommandController, MenuContainer, MenuHeader, MenuItem, MenuRadioGroup, MenuRadioItem, MenuSeparator, MenuSubTrigger, MenuTrigger, menuFromTree, menuFromTreeToElements };