flexdesk 0.17.18 → 0.18.1

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 (126) hide show
  1. package/CHANGELOG.md +91 -0
  2. package/README.md +1 -0
  3. package/css/flexdesk.css +7547 -5891
  4. package/css/reset.css +1 -1
  5. package/css/tokens.css +30 -1
  6. package/dist/charts.js +83 -13
  7. package/dist/charts.js.map +2 -2
  8. package/dist/chunk-3BLIVOFD.js +778 -0
  9. package/dist/chunk-3BLIVOFD.js.map +7 -0
  10. package/dist/{chunk-7OL6F2AM.js → chunk-4X4SY3CP.js} +7 -5
  11. package/dist/chunk-4X4SY3CP.js.map +7 -0
  12. package/dist/chunk-DNJ3N4T4.js +82 -0
  13. package/dist/chunk-DNJ3N4T4.js.map +7 -0
  14. package/dist/{chunk-PGZ4EHSE.js → chunk-HL3IIHU3.js} +501 -359
  15. package/dist/chunk-HL3IIHU3.js.map +7 -0
  16. package/dist/{chunk-REJ5HLIM.js → chunk-HYJYYEPK.js} +2 -2
  17. package/dist/{chunk-7U4IB65H.js → chunk-LLBJ2SJH.js} +5 -3
  18. package/dist/{chunk-7U4IB65H.js.map → chunk-LLBJ2SJH.js.map} +1 -1
  19. package/dist/{chunk-XJNADRGR.js → chunk-NFZJ6P42.js} +188 -96
  20. package/dist/chunk-NFZJ6P42.js.map +7 -0
  21. package/dist/chunk-PAI2ZUWV.js +260 -0
  22. package/dist/chunk-PAI2ZUWV.js.map +7 -0
  23. package/dist/chunk-Q7BALEPX.js +527 -0
  24. package/dist/chunk-Q7BALEPX.js.map +7 -0
  25. package/dist/{chunk-2Z2ENHR3.js → chunk-QRBOIUU3.js} +7 -8
  26. package/dist/chunk-QRBOIUU3.js.map +7 -0
  27. package/dist/chunk-QXAOTGCC.js +40 -0
  28. package/dist/chunk-QXAOTGCC.js.map +7 -0
  29. package/dist/chunk-UKP22RUG.js +219 -0
  30. package/dist/chunk-UKP22RUG.js.map +7 -0
  31. package/dist/chunk-VLOAI3HS.js +43 -0
  32. package/dist/chunk-VLOAI3HS.js.map +7 -0
  33. package/dist/{chunk-WUFLGETN.js → chunk-WVFGV5FT.js} +2 -39
  34. package/dist/{chunk-WUFLGETN.js.map → chunk-WVFGV5FT.js.map} +3 -3
  35. package/dist/editor.js +3 -2
  36. package/dist/editor.js.map +1 -1
  37. package/dist/flexdesk.css +7547 -5891
  38. package/dist/flow.js +29 -97
  39. package/dist/flow.js.map +3 -3
  40. package/dist/git.js +1356 -0
  41. package/dist/git.js.map +7 -0
  42. package/dist/reset.css +1 -1
  43. package/dist/tiles.js +20 -15
  44. package/dist/tiles.js.map +2 -2
  45. package/dist/tokens.css +30 -1
  46. package/dist/widgets.js +4146 -1199
  47. package/dist/widgets.js.map +4 -4
  48. package/dist/wm.js +120 -32
  49. package/dist/wm.js.map +2 -2
  50. package/docs/README.md +9 -4
  51. package/docs/best-practices.md +2 -2
  52. package/docs/components/charts.md +40 -0
  53. package/docs/components/chat-shell.md +204 -0
  54. package/docs/components/conversation.md +10 -0
  55. package/docs/components/filter-builder.md +292 -0
  56. package/docs/components/forms-and-dialogs.md +30 -0
  57. package/docs/components/git.md +355 -0
  58. package/docs/components/input-fields.md +191 -2
  59. package/docs/components/keymap.md +8 -0
  60. package/docs/components/menus.md +86 -2
  61. package/docs/components/panels-and-desktops.md +16 -5
  62. package/docs/components/shell.md +108 -1
  63. package/docs/components/workspace-picker.md +193 -0
  64. package/docs/concepts/building-and-packaging.md +2 -2
  65. package/docs/concepts/packages-and-entry-points.md +14 -13
  66. package/docs/concepts/state-guard.md +3 -3
  67. package/docs/quickstart.md +1 -1
  68. package/git.js +13 -0
  69. package/package.json +6 -1
  70. package/src/charts/plot_config_editor.js +92 -6
  71. package/src/charts/plot_grid.js +6 -1
  72. package/src/flow/kit/dom.js +28 -119
  73. package/src/flow/kit/step_picker.js +1 -1
  74. package/src/flow/kit/value_picker.js +1 -1
  75. package/src/flow/outline/menu.js +1 -1
  76. package/src/git/diff_viewer.js +212 -0
  77. package/src/git/diff_window.js +51 -0
  78. package/src/git/git_graph.js +401 -0
  79. package/src/git/git_ops.js +61 -0
  80. package/src/git/git_panel.js +755 -0
  81. package/src/git/index.js +9 -0
  82. package/src/git/line_diff.js +202 -0
  83. package/src/tiles/tile_base.js +5 -4
  84. package/src/tiling/desktops.js +10 -8
  85. package/src/tiling/keymap.js +6 -5
  86. package/src/tiling/kind_taxonomy.js +15 -2
  87. package/src/tiling/shell.js +124 -17
  88. package/src/tiling/tile_renderer.js +2 -3
  89. package/src/ui/components/action_dropdown.js +4 -78
  90. package/src/ui/components/anchored_layer.js +255 -0
  91. package/src/ui/components/chat_shell.js +926 -0
  92. package/src/ui/components/chat_state_machine.js +223 -0
  93. package/src/ui/components/choice_controls.js +45 -2
  94. package/src/ui/components/color_field.js +3 -2
  95. package/src/ui/components/color_list_field.js +3 -2
  96. package/src/ui/components/combobox.js +422 -0
  97. package/src/ui/components/context_menu.js +8 -2
  98. package/src/ui/components/conversation_view.js +17 -2
  99. package/src/ui/components/data_table.js +2 -2
  100. package/src/ui/components/filter_builder.js +791 -0
  101. package/src/ui/components/filter_model.js +682 -0
  102. package/src/ui/components/inline_renamer.js +10 -5
  103. package/src/ui/components/managed_window.js +24 -47
  104. package/src/ui/components/modal.js +104 -117
  105. package/src/ui/components/option_list.js +160 -0
  106. package/src/ui/components/popover.js +94 -0
  107. package/src/ui/components/recent_items.js +169 -0
  108. package/src/ui/components/select.js +322 -0
  109. package/src/ui/components/tag_input.js +77 -8
  110. package/src/ui/components/workspace_picker.js +1253 -0
  111. package/src/ui/utils/fire_event.js +17 -0
  112. package/src/ui/utils/focusable.js +25 -0
  113. package/src/ui/utils/modal_host.js +27 -0
  114. package/src/ui/utils/tooltip_service.js +81 -14
  115. package/widgets.js +12 -2
  116. package/css/base.css +0 -10547
  117. package/css/overrides.css +0 -2596
  118. package/dist/chunk-2Z2ENHR3.js.map +0 -7
  119. package/dist/chunk-7JI7HHSY.js +0 -107
  120. package/dist/chunk-7JI7HHSY.js.map +0 -7
  121. package/dist/chunk-7OL6F2AM.js.map +0 -7
  122. package/dist/chunk-MIW7D6WC.js +0 -526
  123. package/dist/chunk-MIW7D6WC.js.map +0 -7
  124. package/dist/chunk-PGZ4EHSE.js.map +0 -7
  125. package/dist/chunk-XJNADRGR.js.map +0 -7
  126. /package/dist/{chunk-REJ5HLIM.js.map → chunk-HYJYYEPK.js.map} +0 -0
@@ -1,3 +1,11 @@
1
+ import {
2
+ choiceSections,
3
+ collectFocusable,
4
+ filterSections,
5
+ isInAnchoredLayer,
6
+ openAnchoredLayer,
7
+ shownValue
8
+ } from "./chunk-Q7BALEPX.js";
1
9
  import {
2
10
  getSetting
3
11
  } from "./chunk-HNU6MM2B.js";
@@ -21,23 +29,6 @@ var DEFAULT_ICON = "web_asset";
21
29
  var _zIndexCounter = 0;
22
30
  var _activeWindows = /* @__PURE__ */ new Map();
23
31
  var _escAnswered = /* @__PURE__ */ new WeakSet();
24
- function _collectFocusable(root) {
25
- if (!root) return [];
26
- const sel = [
27
- "a[href]",
28
- "button:not([disabled])",
29
- 'input:not([disabled]):not([type="hidden"])',
30
- "select:not([disabled])",
31
- "textarea:not([disabled])",
32
- '[tabindex]:not([tabindex="-1"])'
33
- ].join(",");
34
- return Array.from(root.querySelectorAll(sel)).filter((el2) => {
35
- if (el2.hidden) return false;
36
- if (el2.closest("[hidden]")) return false;
37
- const r = el2.getBoundingClientRect();
38
- return r.width > 0 || r.height > 0;
39
- });
40
- }
41
32
  var _stateCache = null;
42
33
  function _loadState() {
43
34
  if (_stateCache) return _stateCache;
@@ -93,14 +84,14 @@ function _focusFirstIn(root) {
93
84
  '[contenteditable="true"]',
94
85
  '[tabindex]:not([tabindex="-1"])'
95
86
  ].join(",");
96
- for (const el2 of root.querySelectorAll(sel)) {
97
- if (el2.hidden || el2.closest("[hidden]")) continue;
87
+ for (const el of root.querySelectorAll(sel)) {
88
+ if (el.hidden || el.closest("[hidden]")) continue;
98
89
  try {
99
- el2.focus({ preventScroll: true });
90
+ el.focus({ preventScroll: true });
100
91
  } catch {
101
92
  continue;
102
93
  }
103
- if (el2.ownerDocument.activeElement === el2) return true;
94
+ if (el.ownerDocument.activeElement === el) return true;
104
95
  }
105
96
  return false;
106
97
  }
@@ -327,11 +318,15 @@ var ManagedWindow = class {
327
318
  this._clearTargetProperties();
328
319
  document.removeEventListener("keydown", this._boundOnKeyDown);
329
320
  this._teardownContainerResizeObserver();
330
- window.dispatchEvent(new CustomEvent("managed-window-closed", {
331
- detail: { id: this.id, container: this.container }
332
- }));
321
+ this._announce("managed-window-closed", { id: this.id, container: this.container });
333
322
  this._release();
334
323
  }
324
+ /** Every window event goes out on the page's window, and a window takes only
325
+ * an event built by its own `CustomEvent`: Node's global one is refused
326
+ * under jsdom, in the middle of the close that fired it. */
327
+ _announce(name, detail) {
328
+ window.dispatchEvent(new window.CustomEvent(name, { detail }));
329
+ }
335
330
  /** BUG-0053. A closed window leaves the registry and lets go of what it
336
331
  * held: its content, its DOM and every consumer callback (an `onClose`
337
332
  * typically captures its owner). Until now the registry kept all of it
@@ -376,14 +371,12 @@ var ManagedWindow = class {
376
371
  if (this.backdropElement) {
377
372
  this.backdropElement.style.display = "none";
378
373
  }
379
- window.dispatchEvent(new CustomEvent("managed-window-minimized", {
380
- detail: {
381
- id: this.id,
382
- title: this.title,
383
- icon: this.icon,
384
- container: this.container
385
- }
386
- }));
374
+ this._announce("managed-window-minimized", {
375
+ id: this.id,
376
+ title: this.title,
377
+ icon: this.icon,
378
+ container: this.container
379
+ });
387
380
  if (!this.element) return;
388
381
  this._cancelRestoreAnimation();
389
382
  if (!getSetting("window.animateMinimize", true)) {
@@ -484,9 +477,7 @@ var ManagedWindow = class {
484
477
  }
485
478
  this.isMinimized = false;
486
479
  this.bringToFront();
487
- window.dispatchEvent(new CustomEvent("managed-window-restored", {
488
- detail: { id: this.id, container: this.container }
489
- }));
480
+ this._announce("managed-window-restored", { id: this.id, container: this.container });
490
481
  }
491
482
  /**
492
483
  * Toggle maximize state.
@@ -547,9 +538,7 @@ var ManagedWindow = class {
547
538
  this.element?.classList.toggle("twm-managed-window--maximized", this.isMaximized);
548
539
  this._applyPosition();
549
540
  this._saveCurrentState();
550
- window.dispatchEvent(new CustomEvent("managed-window-maximized", {
551
- detail: { id: this.id, maximized: this.isMaximized, container: this.container }
552
- }));
541
+ this._announce("managed-window-maximized", { id: this.id, maximized: this.isMaximized, container: this.container });
553
542
  }
554
543
  /**
555
544
  * Find the taskbar button for this window.
@@ -864,9 +853,7 @@ var ManagedWindow = class {
864
853
  this.height = bounds.height;
865
854
  }
866
855
  this._applyPosition();
867
- window.dispatchEvent(new CustomEvent("managed-window-moved", {
868
- detail: { id: this.id, container }
869
- }));
856
+ this._announce("managed-window-moved", { id: this.id, container });
870
857
  return true;
871
858
  }
872
859
  /** R1/R3. The container this drag started in, or null when the drag did
@@ -1243,9 +1230,7 @@ var ManagedWindow = class {
1243
1230
  this.height = rect.height;
1244
1231
  this._applyPosition();
1245
1232
  this.element?.classList.add("twm-managed-window--snapped");
1246
- window.dispatchEvent(new CustomEvent("managed-window-snapped", {
1247
- detail: { id: this.id, zone, container: this.container }
1248
- }));
1233
+ this._announce("managed-window-snapped", { id: this.id, zone, container: this.container });
1249
1234
  }
1250
1235
  /** Restore the geometry a snap replaced. Called when a snapped window is
1251
1236
  * picked up again, which is the gesture that means "un-snap". */
@@ -1329,7 +1314,8 @@ var ManagedWindow = class {
1329
1314
  }
1330
1315
  if (topWindow !== this) return;
1331
1316
  if (e.key === "Tab" && this.modal && this.element) {
1332
- const focusables = _collectFocusable(this.element);
1317
+ if (isInAnchoredLayer(document.activeElement)) return;
1318
+ const focusables = collectFocusable(this.element);
1333
1319
  if (focusables.length === 0) {
1334
1320
  e.preventDefault();
1335
1321
  return;
@@ -1551,6 +1537,12 @@ var ToggleField = class {
1551
1537
  }
1552
1538
  };
1553
1539
 
1540
+ // src/ui/utils/fire_event.js
1541
+ function fireEvent(target, type, init) {
1542
+ const view = target.ownerDocument.defaultView;
1543
+ target.dispatchEvent(new view.CustomEvent(type, init));
1544
+ }
1545
+
1554
1546
  // src/ui/components/color_field.js
1555
1547
  var HEX_RE = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i;
1556
1548
  var RGB_RE = /^rgba?\(\s*([^)]*)\)$/i;
@@ -1691,15 +1683,15 @@ var ColorField = class {
1691
1683
  this.swatch = null;
1692
1684
  this.hex = null;
1693
1685
  }
1694
- #listen(el2, type, fn) {
1695
- el2.addEventListener(type, fn);
1696
- this._disposers.push(() => el2.removeEventListener(type, fn));
1686
+ #listen(el, type, fn) {
1687
+ el.addEventListener(type, fn);
1688
+ this._disposers.push(() => el.removeEventListener(type, fn));
1697
1689
  }
1698
1690
  /** Put `text` in the box and fire the box's own events, as typing would. */
1699
1691
  #writeBox(text, commit) {
1700
1692
  this.hex.value = text;
1701
- this.hex.dispatchEvent(new Event("input", { bubbles: true }));
1702
- if (commit) this.hex.dispatchEvent(new Event("change", { bubbles: true }));
1693
+ fireEvent(this.hex, "input", { bubbles: true });
1694
+ if (commit) fireEvent(this.hex, "change", { bubbles: true });
1703
1695
  }
1704
1696
  /** Swatch and invalid mark follow the box; `commit === null` is silent. */
1705
1697
  #sync(commit) {
@@ -1710,332 +1702,488 @@ var ColorField = class {
1710
1702
  }
1711
1703
  };
1712
1704
 
1713
- // src/ui/components/choice_controls.js
1714
- var shownValue = (v) => v === void 0 ? "" : typeof v === "string" ? v : JSON.stringify(v);
1715
- function el(tag, className, text) {
1705
+ // src/ui/components/option_list.js
1706
+ var SEQ = 0;
1707
+ function node(tag, className, text) {
1716
1708
  const n = document.createElement(tag);
1717
1709
  if (className) n.className = className;
1718
1710
  if (text !== void 0 && text !== null) n.textContent = String(text);
1719
1711
  return n;
1720
1712
  }
1721
- var isGroup = (o) => o !== null && typeof o === "object" && Array.isArray(o.options) && "group" in o;
1722
- function choiceOption(o) {
1723
- if (o === null || typeof o !== "object") {
1724
- return { value: o, label: shownValue(o), description: null, disabled: false, reason: null };
1725
- }
1713
+ function marked(target, text, needle) {
1714
+ const at = needle ? text.toLowerCase().indexOf(needle.toLowerCase()) : -1;
1715
+ if (at < 0) {
1716
+ target.textContent = text;
1717
+ return;
1718
+ }
1719
+ target.append(
1720
+ text.slice(0, at),
1721
+ node("mark", "twm-option-list__match", text.slice(at, at + needle.length)),
1722
+ text.slice(at + needle.length)
1723
+ );
1724
+ }
1725
+ function createOptionList({ onPick, onActive = null }) {
1726
+ const id = `twm-option-list-${++SEQ}`;
1727
+ const el = node("div", "twm-option-list");
1728
+ el.id = id;
1729
+ el.setAttribute("role", "listbox");
1730
+ let rows = [];
1731
+ let active = -1;
1732
+ const paintActive = () => {
1733
+ rows.forEach(({ row: row2 }, i) => row2.classList.toggle("twm-option-list__row--active", i === active));
1734
+ const row = rows[active]?.row;
1735
+ row?.scrollIntoView?.({ block: "nearest" });
1736
+ onActive?.(row ? row.id : null);
1737
+ };
1738
+ const rowFor = (option, { glyphs, mark, isCurrent }) => {
1739
+ const row = node("div", "twm-option-list__row");
1740
+ row.id = `${id}-${rows.length}`;
1741
+ row.setAttribute("role", "option");
1742
+ row.setAttribute("aria-selected", String(!!isCurrent?.(option)));
1743
+ if (option.disabled) row.setAttribute("aria-disabled", "true");
1744
+ row.title = option.description ? `${option.label} \u2014 ${option.description}` : option.label;
1745
+ if (glyphs) {
1746
+ const glyph = node("span", "twm-option-list__glyph material-symbols-outlined", option.icon || "");
1747
+ glyph.setAttribute("aria-hidden", "true");
1748
+ row.appendChild(glyph);
1749
+ }
1750
+ const words = node("span", "twm-option-list__words");
1751
+ const label = node("span", "twm-option-list__label");
1752
+ marked(label, option.label, mark);
1753
+ words.appendChild(label);
1754
+ if (option.description) words.appendChild(node("span", "twm-option-list__description", option.description));
1755
+ if (option.disabled && option.reason) words.appendChild(node("span", "twm-choice__reason", option.reason));
1756
+ row.appendChild(words);
1757
+ if (option.hint) row.appendChild(node("span", "twm-option-list__hint", option.hint));
1758
+ return row;
1759
+ };
1760
+ const enabled = () => rows.map((r, i) => r.option.disabled ? -1 : i).filter((i) => i >= 0);
1761
+ el.addEventListener("mousedown", (ev) => ev.preventDefault());
1762
+ el.addEventListener("click", (ev) => {
1763
+ const row = ev.target.closest?.('[role="option"]');
1764
+ const hit = rows.find((r) => r.row === row);
1765
+ if (hit && !hit.option.disabled) onPick(hit.option);
1766
+ });
1726
1767
  return {
1727
- value: o.value,
1728
- label: o.label != null ? String(o.label) : shownValue(o.value),
1729
- description: o.description != null ? String(o.description) : null,
1730
- disabled: Boolean(o.disabled),
1731
- reason: o.reason != null && o.reason !== "" ? String(o.reason) : null
1768
+ el,
1769
+ id,
1770
+ /**
1771
+ * Draw `sections` (`[{ heading, items }]`, as `choiceSections` gives them).
1772
+ * @param {object} [opts]
1773
+ * @param {string} [opts.mark] The text to mark in each label.
1774
+ * @param {(option: object) => boolean} [opts.isCurrent] Is this the current value?
1775
+ */
1776
+ setRows(sections, { mark = "", isCurrent = null } = {}) {
1777
+ rows = [];
1778
+ active = -1;
1779
+ el.replaceChildren();
1780
+ const glyphs = sections.some((s) => s.items.some((o) => o.icon));
1781
+ for (const { heading, items } of sections) {
1782
+ let parent = el;
1783
+ if (heading != null) {
1784
+ parent = node("div", "twm-option-list__section");
1785
+ parent.setAttribute("role", "group");
1786
+ parent.setAttribute("aria-label", heading);
1787
+ const title = node("div", "twm-choice__group", heading);
1788
+ title.setAttribute("aria-hidden", "true");
1789
+ parent.appendChild(title);
1790
+ el.appendChild(parent);
1791
+ }
1792
+ for (const option of items) {
1793
+ const row = rowFor(option, { glyphs, mark, isCurrent });
1794
+ rows.push({ option, row });
1795
+ parent.appendChild(row);
1796
+ }
1797
+ }
1798
+ paintActive();
1799
+ },
1800
+ /** One line of text in place of the rows: nothing matched, still loading, a failure. */
1801
+ setMessage(text) {
1802
+ rows = [];
1803
+ active = -1;
1804
+ el.replaceChildren(node("div", "twm-option-list__message", text));
1805
+ paintActive();
1806
+ },
1807
+ /** How many rows are drawn. */
1808
+ get size() {
1809
+ return rows.length;
1810
+ },
1811
+ /** The drawn options, in order. */
1812
+ options: () => rows.map((r) => r.option),
1813
+ /** The active row's option, or null. */
1814
+ activeOption: () => rows[active]?.option ?? null,
1815
+ /** Make row `index` active; -1, or a row that is disabled, makes none active. */
1816
+ setActive(index) {
1817
+ active = rows[index] && !rows[index].option.disabled ? index : -1;
1818
+ paintActive();
1819
+ },
1820
+ /** The next row that may be chosen, by `step`, wrapping. From none: the first or the last. */
1821
+ move(step) {
1822
+ const stops = enabled();
1823
+ if (!stops.length) return;
1824
+ const at = stops.indexOf(active);
1825
+ const next = at < 0 ? step > 0 ? 0 : stops.length - 1 : (at + step + stops.length) % stops.length;
1826
+ active = stops[next];
1827
+ paintActive();
1828
+ },
1829
+ /** The first (`step` 1) or last (`step` -1) row that may be chosen. */
1830
+ moveToEnd(step) {
1831
+ const stops = enabled();
1832
+ active = stops.length ? stops[step > 0 ? 0 : stops.length - 1] : -1;
1833
+ paintActive();
1834
+ }
1732
1835
  };
1733
1836
  }
1734
- function flatChoices(options) {
1735
- const out = [];
1736
- for (const o of options || []) {
1737
- if (isGroup(o)) for (const inner of o.options) out.push(choiceOption(inner));
1738
- else out.push(choiceOption(o));
1837
+
1838
+ // src/ui/components/combobox.js
1839
+ var DEBOUNCE_MS = 150;
1840
+ var SEQ2 = 0;
1841
+ function attachInputOptions(input, {
1842
+ options = [],
1843
+ onPick,
1844
+ anchor = null,
1845
+ isCurrent = null,
1846
+ autoHighlight = false,
1847
+ isCandidate = null,
1848
+ normalise = (text) => text,
1849
+ openOnFocus = false,
1850
+ passEnter = false,
1851
+ loadingText = "Loading\u2026",
1852
+ errorText = "Could not load the options.",
1853
+ debounce = DEBOUNCE_MS,
1854
+ panelClassName = "",
1855
+ onSettle = null
1856
+ } = {}) {
1857
+ let source = options;
1858
+ let layer = null;
1859
+ let wanted = false;
1860
+ let query = null;
1861
+ let asked = 0;
1862
+ let timer = null;
1863
+ let controller = null;
1864
+ let picking = false;
1865
+ const list = createOptionList({
1866
+ onPick: (option) => take(option),
1867
+ onActive: (id) => {
1868
+ if (id) input.setAttribute("aria-activedescendant", id);
1869
+ else input.removeAttribute("aria-activedescendant");
1870
+ }
1871
+ });
1872
+ const panel = document.createElement("div");
1873
+ panel.className = `twm-option-panel${panelClassName ? ` ${panelClassName}` : ""}`;
1874
+ panel.appendChild(list.el);
1875
+ input.setAttribute("autocomplete", "off");
1876
+ input.setAttribute("role", "combobox");
1877
+ input.setAttribute("aria-autocomplete", "list");
1878
+ input.setAttribute("aria-expanded", "false");
1879
+ input.setAttribute("aria-controls", list.id);
1880
+ const show = () => {
1881
+ if (layer) {
1882
+ layer.place();
1883
+ return;
1884
+ }
1885
+ layer = openAnchoredLayer({
1886
+ anchor: anchor || input,
1887
+ el: panel,
1888
+ gap: 4,
1889
+ matchWidth: true,
1890
+ returnFocus: false,
1891
+ onClose() {
1892
+ layer = null;
1893
+ wanted = false;
1894
+ input.setAttribute("aria-expanded", "false");
1895
+ input.removeAttribute("aria-activedescendant");
1896
+ }
1897
+ });
1898
+ if (!layer) {
1899
+ wanted = false;
1900
+ return;
1901
+ }
1902
+ input.setAttribute("aria-expanded", "true");
1903
+ };
1904
+ function forget() {
1905
+ clearTimeout(timer);
1906
+ asked += 1;
1907
+ controller?.abort();
1908
+ controller = null;
1909
+ }
1910
+ function close(reason = "closed") {
1911
+ wanted = false;
1912
+ forget();
1913
+ layer?.close(reason);
1914
+ }
1915
+ function highlight(typed) {
1916
+ const text = normalise(typed.trim());
1917
+ if (!autoHighlight || !text) {
1918
+ list.setActive(-1);
1919
+ return;
1920
+ }
1921
+ const exact = list.options().findIndex((o) => normalise(shownValue(o.value)) === text);
1922
+ if (exact >= 0) list.setActive(exact);
1923
+ else if (isCandidate?.(text)) list.setActive(-1);
1924
+ else list.moveToEnd(1);
1739
1925
  }
1740
- return out;
1741
- }
1742
- function groupHeads(options) {
1743
- const heads = /* @__PURE__ */ new Map();
1744
- let at = 0;
1745
- for (const o of options || []) {
1746
- if (!isGroup(o)) {
1747
- at += 1;
1748
- continue;
1926
+ function drawRows(sections) {
1927
+ if (!wanted) return;
1928
+ if (!sections.length) {
1929
+ layer?.close("empty");
1930
+ wanted = true;
1931
+ return;
1932
+ }
1933
+ const text = query ?? "";
1934
+ list.setRows(sections, { mark: normalise(text.trim()), isCurrent });
1935
+ show();
1936
+ highlight(text);
1937
+ }
1938
+ function ask(text) {
1939
+ controller = new AbortController();
1940
+ const token = asked;
1941
+ const failed = (err) => {
1942
+ if (token !== asked) return;
1943
+ console.warn("[createCombobox] options failed", err);
1944
+ if (!wanted) return;
1945
+ list.setMessage(errorText);
1946
+ show();
1947
+ };
1948
+ let answer;
1949
+ try {
1950
+ answer = source(text, { signal: controller.signal });
1951
+ } catch (err) {
1952
+ failed(err);
1953
+ return;
1954
+ }
1955
+ if (typeof answer?.then !== "function") {
1956
+ drawRows(choiceSections(answer));
1957
+ return;
1749
1958
  }
1750
- if (o.options.length) heads.set(at, String(o.group ?? ""));
1751
- at += o.options.length;
1959
+ if (!list.size && wanted) {
1960
+ list.setMessage(loadingText);
1961
+ show();
1962
+ }
1963
+ answer.then((rows) => {
1964
+ if (token === asked) drawRows(choiceSections(rows));
1965
+ }, failed);
1752
1966
  }
1753
- return heads;
1754
- }
1755
- var groupHeading = (text) => el("div", "twm-choice__group", text);
1756
- function sameChoice(v, value, loose = false) {
1757
- if (v === value) return true;
1758
- if (!loose || v === void 0 || value === void 0 || v === null || value === null) return false;
1759
- return String(v) === String(value);
1760
- }
1761
- function choiceIndex(list, v, loose = false, from = 0) {
1762
- for (let i = from; i < list.length; i += 1) if (list[i].value === v) return i;
1763
- if (!loose) return -1;
1764
- for (let i = from; i < list.length; i += 1) if (sameChoice(list[i].value, v, true)) return i;
1765
- return -1;
1766
- }
1767
- function findChoice(list, v, loose = false) {
1768
- const i = choiceIndex(list, v, loose);
1769
- return i >= 0 ? list[i] : void 0;
1770
- }
1771
- var optionText = (o) => o.disabled && o.reason ? `${o.label} \u2014 ${o.reason}` : o.label;
1772
- function selectBox(options, value, {
1773
- empty = null,
1774
- label = "",
1775
- notFound = (v) => `${v} (not found)`,
1776
- keepMissing = true,
1777
- valueAttr = "index",
1778
- loose = false,
1779
- className = "twm-flow-input twm-flow-input--select"
1780
- } = {}) {
1781
- const box = el("select", className);
1782
- if (label) box.setAttribute("aria-label", label);
1783
- let entries = [];
1784
- const add = (parent, o) => {
1785
- const attr = valueAttr === "index" ? String(entries.length) : o.value === void 0 ? "" : String(o.value);
1786
- const opt = el("option", "", optionText(o));
1787
- opt.value = attr;
1788
- if (o.disabled) {
1789
- opt.disabled = true;
1790
- if (o.reason) opt.title = o.reason;
1791
- }
1792
- parent.appendChild(opt);
1793
- entries.push(o);
1794
- };
1795
- const firstEnabled = () => {
1796
- const i = entries.findIndex((e) => !e.disabled);
1797
- return i >= 0 ? i : 0;
1798
- };
1799
- const select = (v) => {
1800
- const start = empty !== null ? 1 : 0;
1801
- const at = choiceIndex(entries, v, loose, start);
1802
- if (at < 0 || entries[at].disabled || entries[at].stray) return false;
1803
- box.selectedIndex = at;
1967
+ function refresh() {
1968
+ forget();
1969
+ if (typeof source !== "function") {
1970
+ const all = choiceSections(source);
1971
+ drawRows(query === null ? all : filterSections(all, query));
1972
+ return;
1973
+ }
1974
+ const text = query ?? "";
1975
+ if (query === null || debounce <= 0) {
1976
+ ask(text);
1977
+ return;
1978
+ }
1979
+ const token = asked;
1980
+ timer = setTimeout(() => {
1981
+ if (token === asked) ask(text);
1982
+ }, debounce);
1983
+ }
1984
+ function take(option) {
1985
+ close("pick");
1986
+ picking = true;
1987
+ onPick?.(option);
1988
+ picking = false;
1989
+ }
1990
+ function takeActive() {
1991
+ const option = layer && list.activeOption();
1992
+ if (!option) return false;
1993
+ take(option);
1804
1994
  return true;
1995
+ }
1996
+ const openFor = (text) => {
1997
+ query = text;
1998
+ wanted = true;
1999
+ refresh();
1805
2000
  };
1806
- const place = (v) => {
1807
- if (select(v)) return;
1808
- if (keepMissing && v !== void 0 && v !== null && v !== "") {
1809
- add(box, {
1810
- value: v,
1811
- label: notFound(shownValue(v)),
1812
- description: null,
1813
- disabled: false,
1814
- reason: null,
1815
- stray: true
1816
- });
1817
- box.selectedIndex = entries.length - 1;
2001
+ const onInput = () => openFor(input.value);
2002
+ const onClick = () => openFor(null);
2003
+ const onFocus = () => {
2004
+ if (openOnFocus && !layer && !picking) openFor(input.value || null);
2005
+ };
2006
+ const onBlur = () => close("blur");
2007
+ const onKey = (ev) => {
2008
+ if (ev.isComposing) return;
2009
+ if (ev.key === "ArrowDown" || ev.key === "ArrowUp") {
2010
+ ev.preventDefault();
2011
+ ev.stopPropagation();
2012
+ if (!layer) openFor(null);
2013
+ list.move(ev.key === "ArrowDown" ? 1 : -1);
1818
2014
  return;
1819
2015
  }
1820
- box.selectedIndex = empty !== null ? 0 : firstEnabled();
1821
- };
1822
- const fill = (opts, v) => {
1823
- box.replaceChildren();
1824
- entries = [];
1825
- if (empty !== null) add(box, {
1826
- value: void 0,
1827
- label: String(empty),
1828
- description: null,
1829
- disabled: false,
1830
- reason: null
1831
- });
1832
- for (const o of opts || []) {
1833
- if (isGroup(o)) {
1834
- const g = el("optgroup");
1835
- g.label = String(o.group ?? "");
1836
- for (const inner of o.options) add(g, choiceOption(inner));
1837
- box.appendChild(g);
1838
- } else {
1839
- add(box, choiceOption(o));
2016
+ if (ev.key === "Enter") {
2017
+ if (!layer) return;
2018
+ if (takeActive()) {
2019
+ ev.preventDefault();
2020
+ ev.stopImmediatePropagation();
2021
+ return;
1840
2022
  }
2023
+ close();
2024
+ if (passEnter) return;
2025
+ ev.preventDefault();
2026
+ ev.stopPropagation();
2027
+ onSettle?.();
2028
+ return;
2029
+ }
2030
+ if (ev.key === "Tab") {
2031
+ close("blur");
2032
+ onSettle?.();
1841
2033
  }
1842
- place(v);
1843
2034
  };
1844
- const entry = () => box.selectedIndex < 0 ? null : entries[box.selectedIndex] ?? null;
1845
- const valueOf = () => entry()?.value;
1846
- fill(options, value);
2035
+ input.addEventListener("input", onInput);
2036
+ input.addEventListener("click", onClick);
2037
+ input.addEventListener("focus", onFocus);
2038
+ input.addEventListener("blur", onBlur);
2039
+ input.addEventListener("keydown", onKey);
1847
2040
  return {
1848
- box,
1849
- valueOf,
1850
- entry,
1851
- select,
1852
- setOptions(opts, v = valueOf()) {
1853
- fill(opts, v);
2041
+ get isOpen() {
2042
+ return !!layer;
2043
+ },
2044
+ /** Open it showing everything, or for `text`. */
2045
+ open(text = null) {
2046
+ openFor(text);
2047
+ },
2048
+ close,
2049
+ takeActive,
2050
+ /** Replace the options; an open list redraws. */
2051
+ setOptions(next) {
2052
+ source = next;
2053
+ if (wanted) refresh();
2054
+ },
2055
+ destroy() {
2056
+ close();
2057
+ input.removeEventListener("input", onInput);
2058
+ input.removeEventListener("click", onClick);
2059
+ input.removeEventListener("focus", onFocus);
2060
+ input.removeEventListener("blur", onBlur);
2061
+ input.removeEventListener("keydown", onKey);
1854
2062
  }
1855
2063
  };
1856
2064
  }
1857
- function toggleChips({
1858
- options,
1859
- value,
1860
- label,
1861
- readOnly,
1862
- onChange,
1863
- notFound = (v) => `${v} (not found)`,
1864
- keepMissing = true,
1865
- loose = false
1866
- }) {
1867
- const root = el("div", "twm-flow-togglechips");
1868
- root.setAttribute("role", "group");
1869
- if (label) root.setAttribute("aria-label", label);
1870
- let shown = [];
1871
- let list = [];
1872
- let chosen = /* @__PURE__ */ new Set();
1873
- let strays = [];
1874
- const emit = () => {
1875
- const out = valueOf();
1876
- onChange?.(out.length ? out : void 0);
1877
- };
1878
- const chipFor = (text, pressed, toggle, { stray = false, reason = null, disabled = false } = {}) => {
1879
- const b = el("button", `twm-flow-togglechip${stray ? " twm-flow-togglechip--missing" : ""}`, text);
1880
- b.type = "button";
1881
- b.setAttribute("aria-pressed", pressed ? "true" : "false");
1882
- b.disabled = Boolean(readOnly) || disabled;
1883
- if (reason) {
1884
- b.title = reason;
1885
- b.appendChild(el("span", "twm-choice__reason", reason));
1886
- }
1887
- b.addEventListener("click", () => {
1888
- const on = toggle();
1889
- b.setAttribute("aria-pressed", on ? "true" : "false");
1890
- emit();
1891
- });
1892
- return b;
2065
+ function createCombobox({
2066
+ options = [],
2067
+ value = "",
2068
+ onChange = null,
2069
+ className = "",
2070
+ panelClassName = "",
2071
+ placeholder = "",
2072
+ title = "",
2073
+ ariaLabel = "",
2074
+ name = null,
2075
+ disabled = false,
2076
+ normalise = (text) => text,
2077
+ autoHighlight = true,
2078
+ isCandidate = null,
2079
+ validate = null,
2080
+ loadingText,
2081
+ errorText,
2082
+ debounce
2083
+ } = {}) {
2084
+ const handed = String(value ?? "");
2085
+ let current = normalise(handed);
2086
+ const el = document.createElement("div");
2087
+ el.className = `twm-combobox${className ? ` ${className}` : ""}`;
2088
+ const input = document.createElement("input");
2089
+ input.type = "text";
2090
+ input.className = "twm-combobox__input";
2091
+ input.spellcheck = false;
2092
+ input.value = current;
2093
+ input.disabled = !!disabled;
2094
+ if (name) input.name = name;
2095
+ if (placeholder) input.placeholder = placeholder;
2096
+ if (title) input.title = title;
2097
+ if (ariaLabel) input.setAttribute("aria-label", ariaLabel);
2098
+ const error = document.createElement("p");
2099
+ error.className = "twm-combobox__error";
2100
+ error.hidden = true;
2101
+ error.id = `twm-combobox-error-${++SEQ2}`;
2102
+ el.append(input, error);
2103
+ function report(next, option) {
2104
+ const settled = normalise(String(next ?? ""));
2105
+ if (settled === current) return;
2106
+ current = settled;
2107
+ onChange?.(current, option ?? null);
2108
+ }
2109
+ const clearValidity = () => {
2110
+ error.hidden = true;
2111
+ error.textContent = "";
2112
+ input.removeAttribute("aria-invalid");
2113
+ input.removeAttribute("aria-describedby");
1893
2114
  };
1894
- const draw = (opts, stored) => {
1895
- shown = opts || [];
1896
- list = flatChoices(shown);
1897
- const heads = groupHeads(shown);
1898
- const wanted = Array.isArray(stored) ? stored : [];
1899
- const offered = (v) => {
1900
- const o = findChoice(list, v, loose);
1901
- return o && !o.disabled ? o : void 0;
1902
- };
1903
- chosen = new Set(wanted.map(offered).filter(Boolean).map((o) => o.value));
1904
- strays = keepMissing ? wanted.filter((v) => !offered(v)) : [];
1905
- root.replaceChildren();
1906
- list.forEach((o, i) => {
1907
- if (heads.has(i)) root.appendChild(groupHeading(heads.get(i)));
1908
- root.appendChild(chipFor(o.label, chosen.has(o.value), () => {
1909
- if (o.disabled) return false;
1910
- if (chosen.has(o.value)) chosen.delete(o.value);
1911
- else chosen.add(o.value);
1912
- return chosen.has(o.value);
1913
- }, { disabled: o.disabled, reason: o.disabled ? o.reason : null }));
1914
- });
1915
- for (const v of [...strays]) {
1916
- root.appendChild(chipFor(notFound(shownValue(v)), true, () => {
1917
- const i = strays.indexOf(v);
1918
- if (i >= 0) {
1919
- strays.splice(i, 1);
1920
- return false;
1921
- }
1922
- strays.push(v);
1923
- return true;
1924
- }, { stray: true }));
2115
+ function paintValidity() {
2116
+ const message = validate && current ? validate(current) || null : null;
2117
+ if (!message) {
2118
+ clearValidity();
2119
+ return;
1925
2120
  }
1926
- };
1927
- const valueOf = () => [...list.filter((o) => chosen.has(o.value)).map((o) => o.value), ...strays];
1928
- draw(options, value);
1929
- return {
1930
- el: root,
1931
- valueOf,
1932
- setValue: (stored) => draw(shown, stored),
1933
- setOptions: (opts) => draw(opts, valueOf())
1934
- };
1935
- }
1936
- function choiceList({
1937
- options,
1938
- value,
1939
- name,
1940
- label,
1941
- readOnly,
1942
- onChange,
1943
- notFound = (v) => `${v} (not found)`,
1944
- keepMissing = true,
1945
- loose = false
1946
- }) {
1947
- const root = el("div", "twm-flow-choices");
1948
- root.setAttribute("role", "radiogroup");
1949
- if (label) root.setAttribute("aria-label", label);
1950
- let list = [];
1951
- let radios = [];
1952
- const draw = (opts, current) => {
1953
- list = flatChoices(opts);
1954
- const heads = groupHeads(opts);
1955
- radios = [];
1956
- root.replaceChildren();
1957
- const named = choiceIndex(list, current, loose);
1958
- const at = named >= 0 && !list[named].disabled ? named : -1;
1959
- list.forEach((o, i) => {
1960
- if (heads.has(i)) root.appendChild(groupHeading(heads.get(i)));
1961
- const row = el("label", `twm-flow-choice${o.disabled ? " twm-choice--disabled" : ""}`);
1962
- const radio = el("input", "twm-flow-choice__radio");
1963
- radio.type = "radio";
1964
- radio.name = name;
1965
- radio.checked = i === at;
1966
- radio.disabled = Boolean(readOnly) || o.disabled;
1967
- radio.addEventListener("change", () => {
1968
- if (radio.checked) onChange?.(o.value);
1969
- });
1970
- radios.push(radio);
1971
- const words = el("span", "twm-flow-choice__words");
1972
- words.appendChild(el("span", "twm-flow-choice__label", o.label));
1973
- if (o.description !== null) words.appendChild(el("span", "twm-flow-choice__sub", o.description));
1974
- if (o.disabled && o.reason) words.appendChild(el("span", "twm-choice__reason", o.reason));
1975
- row.append(radio, words);
1976
- root.appendChild(row);
1977
- });
1978
- if (keepMissing && current !== void 0 && named < 0) {
1979
- root.appendChild(el("p", "twm-flow-field__note", notFound(shownValue(current))));
2121
+ error.textContent = message;
2122
+ error.hidden = false;
2123
+ input.setAttribute("aria-invalid", "true");
2124
+ input.setAttribute("aria-describedby", error.id);
2125
+ }
2126
+ function settle() {
2127
+ report(input.value, null);
2128
+ if (input.value !== current) input.value = current;
2129
+ paintValidity();
2130
+ }
2131
+ input.addEventListener("input", () => {
2132
+ report(input.value, null);
2133
+ clearValidity();
2134
+ });
2135
+ const menu = attachInputOptions(input, {
2136
+ options,
2137
+ autoHighlight,
2138
+ isCandidate,
2139
+ normalise,
2140
+ panelClassName,
2141
+ loadingText,
2142
+ errorText,
2143
+ debounce,
2144
+ isCurrent: (o) => normalise(shownValue(o.value)) === current,
2145
+ onSettle: settle,
2146
+ onPick(option) {
2147
+ input.value = shownValue(option.value);
2148
+ report(option.value, option.source);
2149
+ settle();
2150
+ input.focus();
2151
+ fireEvent(input, "change", { bubbles: true });
1980
2152
  }
1981
- };
1982
- const checkedAt = () => radios.findIndex((r) => r.checked);
1983
- const entry = () => list[checkedAt()] ?? null;
1984
- const api = {
1985
- el: root,
1986
- focus: () => (radios.find((r) => r.checked && !r.disabled) || radios.find((r) => !r.disabled))?.focus({ preventScroll: true }),
1987
- entry,
1988
- valueOf: () => entry()?.value,
1989
- select(v) {
1990
- const named = choiceIndex(list, v, loose);
1991
- const at = named >= 0 && !list[named].disabled ? named : -1;
1992
- radios.forEach((r, i) => {
1993
- r.checked = i === at;
1994
- });
1995
- return at >= 0;
2153
+ });
2154
+ input.addEventListener("blur", settle);
2155
+ paintValidity();
2156
+ if (onChange && current !== handed) onChange(current, null);
2157
+ return {
2158
+ el,
2159
+ input,
2160
+ get value() {
2161
+ return current;
2162
+ },
2163
+ set value(next) {
2164
+ current = normalise(String(next ?? ""));
2165
+ input.value = current;
2166
+ paintValidity();
2167
+ },
2168
+ /** The complaint on screen, or `null`. A caller that wants to refuse Save has this. */
2169
+ get error() {
2170
+ return error.hidden ? null : error.textContent;
1996
2171
  },
1997
- setOptions(opts) {
1998
- draw(opts, api.valueOf());
2172
+ get isOpen() {
2173
+ return menu.isOpen;
2174
+ },
2175
+ setOptions: menu.setOptions,
2176
+ /** Open the list showing everything, or for `text`. */
2177
+ open: menu.open,
2178
+ close: () => menu.close(),
2179
+ focus() {
2180
+ input.focus();
2181
+ },
2182
+ destroy() {
2183
+ menu.destroy();
2184
+ el.remove();
1999
2185
  }
2000
2186
  };
2001
- draw(options, value);
2002
- return api;
2003
- }
2004
-
2005
- // src/ui/components/row_activation.js
2006
- var ROW_CONTROL_SELECTOR = [
2007
- "button",
2008
- "a",
2009
- "input",
2010
- "select",
2011
- "textarea",
2012
- "label",
2013
- "summary",
2014
- '[contenteditable=""]',
2015
- '[contenteditable="true"]',
2016
- "[data-twm-action]"
2017
- ].join(", ");
2018
- function isRowControl(target, scope = null) {
2019
- const hit = target?.closest?.(ROW_CONTROL_SELECTOR);
2020
- return !!hit && (!scope || scope.contains(hit));
2021
- }
2022
- function endsTextSelection(target, scope = null) {
2023
- const doc = target?.ownerDocument || (typeof document !== "undefined" ? document : null);
2024
- const sel = doc?.getSelection?.();
2025
- if (!sel || sel.isCollapsed || !String(sel)) return false;
2026
- if (!scope) return true;
2027
- const inside = (node) => !!node && scope.contains(node);
2028
- return inside(sel.anchorNode) || inside(sel.focusNode);
2029
- }
2030
- function isRowActivation(ev, gesture = "click", { scope = null } = {}) {
2031
- const target = ev?.target;
2032
- if (!target?.closest) return false;
2033
- if (isRowControl(target, scope)) return false;
2034
- if (gesture === "click") {
2035
- if ((ev.detail ?? 0) > 1) return false;
2036
- if (endsTextSelection(target, scope)) return false;
2037
- }
2038
- return true;
2039
2187
  }
2040
2188
 
2041
2189
  export {
@@ -2043,18 +2191,12 @@ export {
2043
2191
  DEFAULT_MAX_SAVED_WINDOWS,
2044
2192
  ManagedWindow,
2045
2193
  ToggleField,
2194
+ fireEvent,
2046
2195
  isHexColor,
2047
2196
  normalizeHex,
2048
2197
  ColorField,
2049
- shownValue,
2050
- flatChoices,
2051
- findChoice,
2052
- selectBox,
2053
- toggleChips,
2054
- choiceList,
2055
- ROW_CONTROL_SELECTOR,
2056
- isRowControl,
2057
- endsTextSelection,
2058
- isRowActivation
2198
+ createOptionList,
2199
+ attachInputOptions,
2200
+ createCombobox
2059
2201
  };
2060
- //# sourceMappingURL=chunk-PGZ4EHSE.js.map
2202
+ //# sourceMappingURL=chunk-HL3IIHU3.js.map