flexdesk 0.17.18 → 0.18.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 (118) hide show
  1. package/CHANGELOG.md +53 -0
  2. package/README.md +1 -0
  3. package/css/flexdesk.css +7544 -5916
  4. package/css/reset.css +1 -1
  5. package/css/tokens.css +23 -1
  6. package/dist/charts.js +10 -6
  7. package/dist/charts.js.map +1 -1
  8. package/dist/chunk-5W4EEKSK.js +778 -0
  9. package/dist/chunk-5W4EEKSK.js.map +7 -0
  10. package/dist/chunk-DNJ3N4T4.js +82 -0
  11. package/dist/chunk-DNJ3N4T4.js.map +7 -0
  12. package/dist/{chunk-REJ5HLIM.js → chunk-HYJYYEPK.js} +2 -2
  13. package/dist/{chunk-7U4IB65H.js → chunk-LLBJ2SJH.js} +5 -3
  14. package/dist/{chunk-7U4IB65H.js.map → chunk-LLBJ2SJH.js.map} +1 -1
  15. package/dist/{chunk-7OL6F2AM.js → chunk-P74F6I6P.js} +6 -4
  16. package/dist/{chunk-7OL6F2AM.js.map → chunk-P74F6I6P.js.map} +1 -1
  17. package/dist/chunk-PAI2ZUWV.js +260 -0
  18. package/dist/chunk-PAI2ZUWV.js.map +7 -0
  19. package/dist/chunk-Q7BALEPX.js +527 -0
  20. package/dist/chunk-Q7BALEPX.js.map +7 -0
  21. package/dist/chunk-QXAOTGCC.js +40 -0
  22. package/dist/chunk-QXAOTGCC.js.map +7 -0
  23. package/dist/{chunk-XJNADRGR.js → chunk-RKVYIXDC.js} +664 -91
  24. package/dist/chunk-RKVYIXDC.js.map +7 -0
  25. package/dist/{chunk-2Z2ENHR3.js → chunk-U5GKJUOC.js} +3 -3
  26. package/dist/{chunk-2Z2ENHR3.js.map → chunk-U5GKJUOC.js.map} +1 -1
  27. package/dist/chunk-UKP22RUG.js +219 -0
  28. package/dist/chunk-UKP22RUG.js.map +7 -0
  29. package/dist/chunk-VLOAI3HS.js +43 -0
  30. package/dist/chunk-VLOAI3HS.js.map +7 -0
  31. package/dist/{chunk-WUFLGETN.js → chunk-WVFGV5FT.js} +2 -39
  32. package/dist/{chunk-WUFLGETN.js.map → chunk-WVFGV5FT.js.map} +3 -3
  33. package/dist/{chunk-PGZ4EHSE.js → chunk-ZL3VKINP.js} +41 -390
  34. package/dist/chunk-ZL3VKINP.js.map +7 -0
  35. package/dist/editor.js +3 -2
  36. package/dist/editor.js.map +1 -1
  37. package/dist/flexdesk.css +7544 -5916
  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 +23 -1
  46. package/dist/widgets.js +3967 -1188
  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/chat-shell.md +198 -0
  53. package/docs/components/conversation.md +10 -0
  54. package/docs/components/filter-builder.md +246 -0
  55. package/docs/components/forms-and-dialogs.md +22 -0
  56. package/docs/components/git.md +355 -0
  57. package/docs/components/input-fields.md +167 -2
  58. package/docs/components/keymap.md +8 -0
  59. package/docs/components/menus.md +82 -2
  60. package/docs/components/panels-and-desktops.md +16 -5
  61. package/docs/components/shell.md +104 -1
  62. package/docs/components/workspace-picker.md +149 -0
  63. package/docs/concepts/building-and-packaging.md +2 -2
  64. package/docs/concepts/packages-and-entry-points.md +14 -13
  65. package/docs/concepts/state-guard.md +3 -3
  66. package/docs/quickstart.md +1 -1
  67. package/git.js +13 -0
  68. package/package.json +6 -1
  69. package/src/flow/kit/dom.js +28 -119
  70. package/src/flow/kit/step_picker.js +1 -1
  71. package/src/flow/kit/value_picker.js +1 -1
  72. package/src/flow/outline/menu.js +1 -1
  73. package/src/git/diff_viewer.js +212 -0
  74. package/src/git/diff_window.js +51 -0
  75. package/src/git/git_graph.js +401 -0
  76. package/src/git/git_ops.js +61 -0
  77. package/src/git/git_panel.js +755 -0
  78. package/src/git/index.js +9 -0
  79. package/src/git/line_diff.js +202 -0
  80. package/src/tiles/tile_base.js +5 -4
  81. package/src/tiling/desktops.js +10 -8
  82. package/src/tiling/keymap.js +6 -5
  83. package/src/tiling/kind_taxonomy.js +15 -2
  84. package/src/tiling/shell.js +124 -17
  85. package/src/tiling/tile_renderer.js +2 -3
  86. package/src/ui/components/action_dropdown.js +4 -78
  87. package/src/ui/components/anchored_layer.js +255 -0
  88. package/src/ui/components/chat_shell.js +920 -0
  89. package/src/ui/components/chat_state_machine.js +223 -0
  90. package/src/ui/components/choice_controls.js +45 -2
  91. package/src/ui/components/color_field.js +3 -2
  92. package/src/ui/components/color_list_field.js +3 -2
  93. package/src/ui/components/combobox.js +422 -0
  94. package/src/ui/components/conversation_view.js +17 -2
  95. package/src/ui/components/data_table.js +2 -2
  96. package/src/ui/components/filter_builder.js +766 -0
  97. package/src/ui/components/filter_model.js +614 -0
  98. package/src/ui/components/managed_window.js +24 -47
  99. package/src/ui/components/modal.js +79 -109
  100. package/src/ui/components/option_list.js +160 -0
  101. package/src/ui/components/popover.js +94 -0
  102. package/src/ui/components/recent_items.js +169 -0
  103. package/src/ui/components/select.js +322 -0
  104. package/src/ui/components/tag_input.js +77 -8
  105. package/src/ui/components/workspace_picker.js +1155 -0
  106. package/src/ui/utils/fire_event.js +17 -0
  107. package/src/ui/utils/focusable.js +25 -0
  108. package/src/ui/utils/modal_host.js +27 -0
  109. package/widgets.js +12 -2
  110. package/css/base.css +0 -10547
  111. package/css/overrides.css +0 -2596
  112. package/dist/chunk-7JI7HHSY.js +0 -107
  113. package/dist/chunk-7JI7HHSY.js.map +0 -7
  114. package/dist/chunk-MIW7D6WC.js +0 -526
  115. package/dist/chunk-MIW7D6WC.js.map +0 -7
  116. package/dist/chunk-PGZ4EHSE.js.map +0 -7
  117. package/dist/chunk-XJNADRGR.js.map +0 -7
  118. /package/dist/{chunk-REJ5HLIM.js.map → chunk-HYJYYEPK.js.map} +0 -0
@@ -2,17 +2,30 @@ import {
2
2
  ColorField,
3
3
  ManagedWindow,
4
4
  ToggleField,
5
+ fireEvent,
6
+ isHexColor,
7
+ normalizeHex
8
+ } from "./chunk-ZL3VKINP.js";
9
+ import {
5
10
  choiceList,
11
+ choiceSections,
12
+ filterSections,
6
13
  findChoice,
7
14
  flatChoices,
8
- isHexColor,
9
- normalizeHex,
15
+ modalHost,
16
+ openAnchoredLayer,
10
17
  selectBox,
18
+ shownValue,
11
19
  toggleChips
12
- } from "./chunk-PGZ4EHSE.js";
20
+ } from "./chunk-Q7BALEPX.js";
13
21
  import {
14
22
  DragReorder
15
- } from "./chunk-WUFLGETN.js";
23
+ } from "./chunk-WVFGV5FT.js";
24
+ import {
25
+ focusMenuItem,
26
+ moveMenuFocus,
27
+ placeMenuInViewport
28
+ } from "./chunk-QXAOTGCC.js";
16
29
 
17
30
  // src/ui/components/sortable_list.js
18
31
  var ROW_CLASS = "twm-sortable-list__row";
@@ -428,21 +441,498 @@ var ColorListField = class {
428
441
  #emit(commit) {
429
442
  this.#writeInput();
430
443
  if (this.input) {
431
- this.input.dispatchEvent(new Event("input", { bubbles: true }));
432
- if (commit) this.input.dispatchEvent(new Event("change", { bubbles: true }));
444
+ fireEvent(this.input, "input", { bubbles: true });
445
+ if (commit) fireEvent(this.input, "change", { bubbles: true });
433
446
  }
434
447
  this.onChange?.(this.getValue(), { commit });
435
448
  }
436
449
  };
437
450
 
438
- // src/ui/components/modal.js
439
- var _modalHost = null;
440
- function setModalHost(el) {
441
- _modalHost = el || null;
451
+ // src/ui/components/option_list.js
452
+ var SEQ = 0;
453
+ function node(tag, className, text) {
454
+ const n = document.createElement(tag);
455
+ if (className) n.className = className;
456
+ if (text !== void 0 && text !== null) n.textContent = String(text);
457
+ return n;
458
+ }
459
+ function marked(target, text, needle) {
460
+ const at = needle ? text.toLowerCase().indexOf(needle.toLowerCase()) : -1;
461
+ if (at < 0) {
462
+ target.textContent = text;
463
+ return;
464
+ }
465
+ target.append(
466
+ text.slice(0, at),
467
+ node("mark", "twm-option-list__match", text.slice(at, at + needle.length)),
468
+ text.slice(at + needle.length)
469
+ );
470
+ }
471
+ function createOptionList({ onPick, onActive = null }) {
472
+ const id = `twm-option-list-${++SEQ}`;
473
+ const el = node("div", "twm-option-list");
474
+ el.id = id;
475
+ el.setAttribute("role", "listbox");
476
+ let rows = [];
477
+ let active = -1;
478
+ const paintActive = () => {
479
+ rows.forEach(({ row: row2 }, i) => row2.classList.toggle("twm-option-list__row--active", i === active));
480
+ const row = rows[active]?.row;
481
+ row?.scrollIntoView?.({ block: "nearest" });
482
+ onActive?.(row ? row.id : null);
483
+ };
484
+ const rowFor = (option, { glyphs, mark, isCurrent }) => {
485
+ const row = node("div", "twm-option-list__row");
486
+ row.id = `${id}-${rows.length}`;
487
+ row.setAttribute("role", "option");
488
+ row.setAttribute("aria-selected", String(!!isCurrent?.(option)));
489
+ if (option.disabled) row.setAttribute("aria-disabled", "true");
490
+ row.title = option.description ? `${option.label} \u2014 ${option.description}` : option.label;
491
+ if (glyphs) {
492
+ const glyph = node("span", "twm-option-list__glyph material-symbols-outlined", option.icon || "");
493
+ glyph.setAttribute("aria-hidden", "true");
494
+ row.appendChild(glyph);
495
+ }
496
+ const words = node("span", "twm-option-list__words");
497
+ const label = node("span", "twm-option-list__label");
498
+ marked(label, option.label, mark);
499
+ words.appendChild(label);
500
+ if (option.description) words.appendChild(node("span", "twm-option-list__description", option.description));
501
+ if (option.disabled && option.reason) words.appendChild(node("span", "twm-choice__reason", option.reason));
502
+ row.appendChild(words);
503
+ if (option.hint) row.appendChild(node("span", "twm-option-list__hint", option.hint));
504
+ return row;
505
+ };
506
+ const enabled = () => rows.map((r, i) => r.option.disabled ? -1 : i).filter((i) => i >= 0);
507
+ el.addEventListener("mousedown", (ev) => ev.preventDefault());
508
+ el.addEventListener("click", (ev) => {
509
+ const row = ev.target.closest?.('[role="option"]');
510
+ const hit = rows.find((r) => r.row === row);
511
+ if (hit && !hit.option.disabled) onPick(hit.option);
512
+ });
513
+ return {
514
+ el,
515
+ id,
516
+ /**
517
+ * Draw `sections` (`[{ heading, items }]`, as `choiceSections` gives them).
518
+ * @param {object} [opts]
519
+ * @param {string} [opts.mark] The text to mark in each label.
520
+ * @param {(option: object) => boolean} [opts.isCurrent] Is this the current value?
521
+ */
522
+ setRows(sections, { mark = "", isCurrent = null } = {}) {
523
+ rows = [];
524
+ active = -1;
525
+ el.replaceChildren();
526
+ const glyphs = sections.some((s) => s.items.some((o) => o.icon));
527
+ for (const { heading, items } of sections) {
528
+ let parent = el;
529
+ if (heading != null) {
530
+ parent = node("div", "twm-option-list__section");
531
+ parent.setAttribute("role", "group");
532
+ parent.setAttribute("aria-label", heading);
533
+ const title = node("div", "twm-choice__group", heading);
534
+ title.setAttribute("aria-hidden", "true");
535
+ parent.appendChild(title);
536
+ el.appendChild(parent);
537
+ }
538
+ for (const option of items) {
539
+ const row = rowFor(option, { glyphs, mark, isCurrent });
540
+ rows.push({ option, row });
541
+ parent.appendChild(row);
542
+ }
543
+ }
544
+ paintActive();
545
+ },
546
+ /** One line of text in place of the rows: nothing matched, still loading, a failure. */
547
+ setMessage(text) {
548
+ rows = [];
549
+ active = -1;
550
+ el.replaceChildren(node("div", "twm-option-list__message", text));
551
+ paintActive();
552
+ },
553
+ /** How many rows are drawn. */
554
+ get size() {
555
+ return rows.length;
556
+ },
557
+ /** The drawn options, in order. */
558
+ options: () => rows.map((r) => r.option),
559
+ /** The active row's option, or null. */
560
+ activeOption: () => rows[active]?.option ?? null,
561
+ /** Make row `index` active; -1, or a row that is disabled, makes none active. */
562
+ setActive(index) {
563
+ active = rows[index] && !rows[index].option.disabled ? index : -1;
564
+ paintActive();
565
+ },
566
+ /** The next row that may be chosen, by `step`, wrapping. From none: the first or the last. */
567
+ move(step) {
568
+ const stops = enabled();
569
+ if (!stops.length) return;
570
+ const at = stops.indexOf(active);
571
+ const next = at < 0 ? step > 0 ? 0 : stops.length - 1 : (at + step + stops.length) % stops.length;
572
+ active = stops[next];
573
+ paintActive();
574
+ },
575
+ /** The first (`step` 1) or last (`step` -1) row that may be chosen. */
576
+ moveToEnd(step) {
577
+ const stops = enabled();
578
+ active = stops.length ? stops[step > 0 ? 0 : stops.length - 1] : -1;
579
+ paintActive();
580
+ }
581
+ };
582
+ }
583
+
584
+ // src/ui/components/combobox.js
585
+ var DEBOUNCE_MS = 150;
586
+ var SEQ2 = 0;
587
+ function attachInputOptions(input, {
588
+ options = [],
589
+ onPick,
590
+ anchor = null,
591
+ isCurrent = null,
592
+ autoHighlight = false,
593
+ isCandidate = null,
594
+ normalise = (text) => text,
595
+ openOnFocus = false,
596
+ passEnter = false,
597
+ loadingText = "Loading\u2026",
598
+ errorText = "Could not load the options.",
599
+ debounce = DEBOUNCE_MS,
600
+ panelClassName = "",
601
+ onSettle = null
602
+ } = {}) {
603
+ let source = options;
604
+ let layer = null;
605
+ let wanted = false;
606
+ let query = null;
607
+ let asked = 0;
608
+ let timer = null;
609
+ let controller = null;
610
+ let picking = false;
611
+ const list = createOptionList({
612
+ onPick: (option) => take(option),
613
+ onActive: (id) => {
614
+ if (id) input.setAttribute("aria-activedescendant", id);
615
+ else input.removeAttribute("aria-activedescendant");
616
+ }
617
+ });
618
+ const panel = document.createElement("div");
619
+ panel.className = `twm-option-panel${panelClassName ? ` ${panelClassName}` : ""}`;
620
+ panel.appendChild(list.el);
621
+ input.setAttribute("autocomplete", "off");
622
+ input.setAttribute("role", "combobox");
623
+ input.setAttribute("aria-autocomplete", "list");
624
+ input.setAttribute("aria-expanded", "false");
625
+ input.setAttribute("aria-controls", list.id);
626
+ const show = () => {
627
+ if (layer) {
628
+ layer.place();
629
+ return;
630
+ }
631
+ layer = openAnchoredLayer({
632
+ anchor: anchor || input,
633
+ el: panel,
634
+ gap: 4,
635
+ matchWidth: true,
636
+ returnFocus: false,
637
+ onClose() {
638
+ layer = null;
639
+ wanted = false;
640
+ input.setAttribute("aria-expanded", "false");
641
+ input.removeAttribute("aria-activedescendant");
642
+ }
643
+ });
644
+ if (!layer) {
645
+ wanted = false;
646
+ return;
647
+ }
648
+ input.setAttribute("aria-expanded", "true");
649
+ };
650
+ function forget() {
651
+ clearTimeout(timer);
652
+ asked += 1;
653
+ controller?.abort();
654
+ controller = null;
655
+ }
656
+ function close(reason = "closed") {
657
+ wanted = false;
658
+ forget();
659
+ layer?.close(reason);
660
+ }
661
+ function highlight(typed) {
662
+ const text = normalise(typed.trim());
663
+ if (!autoHighlight || !text) {
664
+ list.setActive(-1);
665
+ return;
666
+ }
667
+ const exact = list.options().findIndex((o) => normalise(shownValue(o.value)) === text);
668
+ if (exact >= 0) list.setActive(exact);
669
+ else if (isCandidate?.(text)) list.setActive(-1);
670
+ else list.moveToEnd(1);
671
+ }
672
+ function drawRows(sections) {
673
+ if (!wanted) return;
674
+ if (!sections.length) {
675
+ layer?.close("empty");
676
+ wanted = true;
677
+ return;
678
+ }
679
+ const text = query ?? "";
680
+ list.setRows(sections, { mark: normalise(text.trim()), isCurrent });
681
+ show();
682
+ highlight(text);
683
+ }
684
+ function ask(text) {
685
+ controller = new AbortController();
686
+ const token = asked;
687
+ const failed = (err) => {
688
+ if (token !== asked) return;
689
+ console.warn("[createCombobox] options failed", err);
690
+ if (!wanted) return;
691
+ list.setMessage(errorText);
692
+ show();
693
+ };
694
+ let answer;
695
+ try {
696
+ answer = source(text, { signal: controller.signal });
697
+ } catch (err) {
698
+ failed(err);
699
+ return;
700
+ }
701
+ if (typeof answer?.then !== "function") {
702
+ drawRows(choiceSections(answer));
703
+ return;
704
+ }
705
+ if (!list.size && wanted) {
706
+ list.setMessage(loadingText);
707
+ show();
708
+ }
709
+ answer.then((rows) => {
710
+ if (token === asked) drawRows(choiceSections(rows));
711
+ }, failed);
712
+ }
713
+ function refresh() {
714
+ forget();
715
+ if (typeof source !== "function") {
716
+ const all = choiceSections(source);
717
+ drawRows(query === null ? all : filterSections(all, query));
718
+ return;
719
+ }
720
+ const text = query ?? "";
721
+ if (query === null || debounce <= 0) {
722
+ ask(text);
723
+ return;
724
+ }
725
+ const token = asked;
726
+ timer = setTimeout(() => {
727
+ if (token === asked) ask(text);
728
+ }, debounce);
729
+ }
730
+ function take(option) {
731
+ close("pick");
732
+ picking = true;
733
+ onPick?.(option);
734
+ picking = false;
735
+ }
736
+ function takeActive() {
737
+ const option = layer && list.activeOption();
738
+ if (!option) return false;
739
+ take(option);
740
+ return true;
741
+ }
742
+ const openFor = (text) => {
743
+ query = text;
744
+ wanted = true;
745
+ refresh();
746
+ };
747
+ const onInput = () => openFor(input.value);
748
+ const onClick = () => openFor(null);
749
+ const onFocus = () => {
750
+ if (openOnFocus && !layer && !picking) openFor(input.value || null);
751
+ };
752
+ const onBlur = () => close("blur");
753
+ const onKey = (ev) => {
754
+ if (ev.isComposing) return;
755
+ if (ev.key === "ArrowDown" || ev.key === "ArrowUp") {
756
+ ev.preventDefault();
757
+ ev.stopPropagation();
758
+ if (!layer) openFor(null);
759
+ list.move(ev.key === "ArrowDown" ? 1 : -1);
760
+ return;
761
+ }
762
+ if (ev.key === "Enter") {
763
+ if (!layer) return;
764
+ if (takeActive()) {
765
+ ev.preventDefault();
766
+ ev.stopImmediatePropagation();
767
+ return;
768
+ }
769
+ close();
770
+ if (passEnter) return;
771
+ ev.preventDefault();
772
+ ev.stopPropagation();
773
+ onSettle?.();
774
+ return;
775
+ }
776
+ if (ev.key === "Tab") {
777
+ close("blur");
778
+ onSettle?.();
779
+ }
780
+ };
781
+ input.addEventListener("input", onInput);
782
+ input.addEventListener("click", onClick);
783
+ input.addEventListener("focus", onFocus);
784
+ input.addEventListener("blur", onBlur);
785
+ input.addEventListener("keydown", onKey);
786
+ return {
787
+ get isOpen() {
788
+ return !!layer;
789
+ },
790
+ /** Open it showing everything, or for `text`. */
791
+ open(text = null) {
792
+ openFor(text);
793
+ },
794
+ close,
795
+ takeActive,
796
+ /** Replace the options; an open list redraws. */
797
+ setOptions(next) {
798
+ source = next;
799
+ if (wanted) refresh();
800
+ },
801
+ destroy() {
802
+ close();
803
+ input.removeEventListener("input", onInput);
804
+ input.removeEventListener("click", onClick);
805
+ input.removeEventListener("focus", onFocus);
806
+ input.removeEventListener("blur", onBlur);
807
+ input.removeEventListener("keydown", onKey);
808
+ }
809
+ };
442
810
  }
443
- function modalHost() {
444
- return _modalHost;
811
+ function createCombobox({
812
+ options = [],
813
+ value = "",
814
+ onChange = null,
815
+ className = "",
816
+ panelClassName = "",
817
+ placeholder = "",
818
+ title = "",
819
+ ariaLabel = "",
820
+ name = null,
821
+ disabled = false,
822
+ normalise = (text) => text,
823
+ autoHighlight = true,
824
+ isCandidate = null,
825
+ validate = null,
826
+ loadingText,
827
+ errorText,
828
+ debounce
829
+ } = {}) {
830
+ const handed = String(value ?? "");
831
+ let current = normalise(handed);
832
+ const el = document.createElement("div");
833
+ el.className = `twm-combobox${className ? ` ${className}` : ""}`;
834
+ const input = document.createElement("input");
835
+ input.type = "text";
836
+ input.className = "twm-combobox__input";
837
+ input.spellcheck = false;
838
+ input.value = current;
839
+ input.disabled = !!disabled;
840
+ if (name) input.name = name;
841
+ if (placeholder) input.placeholder = placeholder;
842
+ if (title) input.title = title;
843
+ if (ariaLabel) input.setAttribute("aria-label", ariaLabel);
844
+ const error = document.createElement("p");
845
+ error.className = "twm-combobox__error";
846
+ error.hidden = true;
847
+ error.id = `twm-combobox-error-${++SEQ2}`;
848
+ el.append(input, error);
849
+ function report(next, option) {
850
+ const settled = normalise(String(next ?? ""));
851
+ if (settled === current) return;
852
+ current = settled;
853
+ onChange?.(current, option ?? null);
854
+ }
855
+ const clearValidity = () => {
856
+ error.hidden = true;
857
+ error.textContent = "";
858
+ input.removeAttribute("aria-invalid");
859
+ input.removeAttribute("aria-describedby");
860
+ };
861
+ function paintValidity() {
862
+ const message = validate && current ? validate(current) || null : null;
863
+ if (!message) {
864
+ clearValidity();
865
+ return;
866
+ }
867
+ error.textContent = message;
868
+ error.hidden = false;
869
+ input.setAttribute("aria-invalid", "true");
870
+ input.setAttribute("aria-describedby", error.id);
871
+ }
872
+ function settle() {
873
+ report(input.value, null);
874
+ if (input.value !== current) input.value = current;
875
+ paintValidity();
876
+ }
877
+ input.addEventListener("input", () => {
878
+ report(input.value, null);
879
+ clearValidity();
880
+ });
881
+ const menu = attachInputOptions(input, {
882
+ options,
883
+ autoHighlight,
884
+ isCandidate,
885
+ normalise,
886
+ panelClassName,
887
+ loadingText,
888
+ errorText,
889
+ debounce,
890
+ isCurrent: (o) => normalise(shownValue(o.value)) === current,
891
+ onSettle: settle,
892
+ onPick(option) {
893
+ input.value = shownValue(option.value);
894
+ report(option.value, option.source);
895
+ settle();
896
+ input.focus();
897
+ fireEvent(input, "change", { bubbles: true });
898
+ }
899
+ });
900
+ input.addEventListener("blur", settle);
901
+ paintValidity();
902
+ if (onChange && current !== handed) onChange(current, null);
903
+ return {
904
+ el,
905
+ input,
906
+ get value() {
907
+ return current;
908
+ },
909
+ set value(next) {
910
+ current = normalise(String(next ?? ""));
911
+ input.value = current;
912
+ paintValidity();
913
+ },
914
+ /** The complaint on screen, or `null`. A caller that wants to refuse Save has this. */
915
+ get error() {
916
+ return error.hidden ? null : error.textContent;
917
+ },
918
+ get isOpen() {
919
+ return menu.isOpen;
920
+ },
921
+ setOptions: menu.setOptions,
922
+ /** Open the list showing everything, or for `text`. */
923
+ open: menu.open,
924
+ close: () => menu.close(),
925
+ focus() {
926
+ input.focus();
927
+ },
928
+ destroy() {
929
+ menu.destroy();
930
+ el.remove();
931
+ }
932
+ };
445
933
  }
934
+
935
+ // src/ui/components/modal.js
446
936
  var _modalSeq = 1;
447
937
  function openForm({ title, fields = [], defaults = {}, submitLabel = "OK", onFieldChange = null } = {}) {
448
938
  return new Promise((resolve) => {
@@ -461,7 +951,7 @@ function openForm({ title, fields = [], defaults = {}, submitLabel = "OK", onFie
461
951
  </form>
462
952
  `;
463
953
  const win = new ManagedWindow({
464
- container: _modalHost,
954
+ container: modalHost(),
465
955
  id: `twm-modal-${_modalSeq++}`,
466
956
  title: title || "Dialog",
467
957
  icon: "edit_note",
@@ -501,12 +991,7 @@ function openForm({ title, fields = [], defaults = {}, submitLabel = "OK", onFie
501
991
  body.querySelector('[data-action="cancel"]')?.addEventListener("click", () => close(null));
502
992
  const formFields = _formFields(fields);
503
993
  const optionsOf = new Map(formFields.map((f) => [f.name, f.options]));
504
- const comboHints = /* @__PURE__ */ new Map();
505
994
  for (const f of formFields) {
506
- if (f.type === "select" && f.create) {
507
- const update = _wireComboboxHint(body, f, () => optionsOf.get(f.name));
508
- if (update) comboHints.set(f.name, update);
509
- }
510
995
  if (f.type === "picker") _wirePicker(body, f);
511
996
  }
512
997
  const rowHidden = (name) => !!body.querySelector(`[data-field-row="${name}"]`)?.hidden;
@@ -540,13 +1025,7 @@ function openForm({ title, fields = [], defaults = {}, submitLabel = "OK", onFie
540
1025
  }
541
1026
  win.show();
542
1027
  const comboOption = (f, text) => findChoice(flatChoices(optionsOf.get(f.name)), text, true);
543
- const unavailable = (f, value) => {
544
- const c = controls.get(f.name);
545
- if (c) return c.unavailable();
546
- if (f.type !== "select" || !f.create || value == null || value === "") return null;
547
- const o = comboOption(f, value);
548
- return o?.disabled ? o.reason || "" : null;
549
- };
1028
+ const unavailable = (f) => controls.get(f.name)?.unavailable() ?? null;
550
1029
  for (const f of formFields) {
551
1030
  const el = body.querySelector(`[name="${f.name}"]`);
552
1031
  el?.addEventListener("input", () => clearFieldError(f.name));
@@ -592,11 +1071,6 @@ function openForm({ title, fields = [], defaults = {}, submitLabel = "OK", onFie
592
1071
  const el = body.querySelector(`[name="${name}"]`);
593
1072
  if (!el) return void 0;
594
1073
  if (el.type === "checkbox") return !!el.checked;
595
- const f = fieldOf(name);
596
- if (f?.type === "select" && f.create) {
597
- const match = comboOption(f, el.value);
598
- return match ? match.value : el.value;
599
- }
600
1074
  return el.value;
601
1075
  },
602
1076
  set(name, value) {
@@ -611,28 +1085,17 @@ function openForm({ title, fields = [], defaults = {}, submitLabel = "OK", onFie
611
1085
  return;
612
1086
  }
613
1087
  const f = fieldOf(name);
614
- const shut = f?.type === "select" && f.create && comboOption(f, value)?.disabled;
615
- el.value = value == null || shut ? "" : String(value);
616
- comboHints.get(name)?.();
1088
+ el.value = value == null ? "" : String(value);
617
1089
  if (f?.type === "picker") {
618
1090
  const display = body.querySelector(`[data-picker-display="${name}"]`);
619
1091
  if (display) display.value = _pickerLabel(f, el.value);
620
1092
  }
621
1093
  },
622
1094
  setOptions(name, options) {
623
- const f = fieldOf(name);
624
- if (!f) return;
625
1095
  const c = controls.get(name);
626
- if (c?.setOptions) {
627
- optionsOf.set(name, options);
628
- c.setOptions(options);
629
- return;
630
- }
631
- if (f.type !== "select" || !f.create) return;
1096
+ if (!c?.setOptions) return;
632
1097
  optionsOf.set(name, options);
633
- const list = body.querySelector(`[data-datalist-for="${name}"]`);
634
- if (list) list.innerHTML = _datalistOptions(options);
635
- comboHints.get(name)?.();
1098
+ c.setOptions(options);
636
1099
  }
637
1100
  };
638
1101
  const fire = (name) => {
@@ -701,14 +1164,14 @@ function openForm({ title, fields = [], defaults = {}, submitLabel = "OK", onFie
701
1164
  for (const f of formFields) {
702
1165
  clearFieldError(f.name);
703
1166
  if (rowHidden(f.name)) {
704
- if (unavailable(f, data[f.name]) != null) data[f.name] = f.type === "radio" ? null : "";
1167
+ if (unavailable(f) != null) data[f.name] = f.type === "radio" ? null : "";
705
1168
  continue;
706
1169
  }
707
1170
  const v = data[f.name];
708
1171
  let msg = f.required && _missing(f, v) ? "Required." : null;
709
1172
  if (!msg && _outOfRange(f, v)) msg = _rangeMessage(f);
710
1173
  if (!msg) {
711
- const reason = unavailable(f, v);
1174
+ const reason = unavailable(f);
712
1175
  if (reason != null) msg = reason ? `Not available: ${reason}` : "Not available.";
713
1176
  }
714
1177
  if (!msg) msg = controls.get(f.name)?.error?.() ?? null;
@@ -767,7 +1230,7 @@ function openConfirm({
767
1230
  body.append(messageEl, actionsEl);
768
1231
  let _resolved = false;
769
1232
  const win = new ManagedWindow({
770
- container: _modalHost,
1233
+ container: modalHost(),
771
1234
  id: `twm-confirm-${_modalSeq++}`,
772
1235
  title: title || "Confirm",
773
1236
  icon: icon || (danger ? "warning" : "help"),
@@ -814,7 +1277,8 @@ function openModal({
814
1277
  backdropBlur = void 0,
815
1278
  backdropOpacity = void 0,
816
1279
  maximizable = false,
817
- onMaximizeChange = null
1280
+ onMaximizeChange = null,
1281
+ beforeClose = null
818
1282
  } = {}) {
819
1283
  return new Promise((resolve) => {
820
1284
  const body = document.createElement("div");
@@ -861,7 +1325,7 @@ function openModal({
861
1325
  _onMax = null;
862
1326
  };
863
1327
  const win = new ManagedWindow({
864
- container: _modalHost,
1328
+ container: modalHost(),
865
1329
  id: `twm-modal-${_modalSeq++}`,
866
1330
  title,
867
1331
  icon,
@@ -882,6 +1346,7 @@ function openModal({
882
1346
  canDrag: true,
883
1347
  defaultWidth: width,
884
1348
  defaultHeight: height,
1349
+ beforeClose,
885
1350
  // Esc, the X and the backdrop all land here without passing through
886
1351
  // `close`, so the listener is dropped here as well as there — the
887
1352
  // three dismissals a user reaches for most are exactly the ones
@@ -1023,8 +1488,54 @@ function _rangeMessage(f) {
1023
1488
  function _offered(options) {
1024
1489
  return flatChoices(options).filter((o) => !o.disabled);
1025
1490
  }
1026
- function _datalistOptions(options) {
1027
- return _offered(options).map((o) => `<option value="${_esc(o.value)}"></option>`).join("");
1491
+ function _mountCombobox(slot, f, value) {
1492
+ let options = f.options;
1493
+ const named = (text) => findChoice(flatChoices(options), text, true);
1494
+ const shown = (v) => v == null || named(v)?.disabled ? "" : String(v);
1495
+ const hint = document.createElement("span");
1496
+ hint.className = "twm-combobox__hint";
1497
+ hint.hidden = true;
1498
+ const noun = f.create === true ? null : _createSpec(f).hint || "new entry";
1499
+ const updateHint = () => {
1500
+ const v = c.value.trim();
1501
+ const isNew = noun && v && !flatChoices(options).some((o) => String(o.value) === v);
1502
+ if (isNew) hint.textContent = `\u21B3 will create ${noun} \u201C${v}\u201D`;
1503
+ hint.hidden = !isNew;
1504
+ };
1505
+ const c = createCombobox({
1506
+ options: _offered(options),
1507
+ value: shown(value),
1508
+ name: f.name,
1509
+ placeholder: f.placeholder ?? "type to pick or create\u2026",
1510
+ // Enter keeps what was typed: this field exists to accept a value nobody listed.
1511
+ autoHighlight: false,
1512
+ onChange: () => updateHint()
1513
+ });
1514
+ if (f.required) c.input.required = true;
1515
+ c.el.appendChild(hint);
1516
+ slot.replaceWith(c.el);
1517
+ updateHint();
1518
+ return {
1519
+ read() {
1520
+ const o = named(c.value);
1521
+ return o ? o.value : c.value;
1522
+ },
1523
+ write(v) {
1524
+ c.value = shown(v);
1525
+ updateHint();
1526
+ },
1527
+ focus: c.focus,
1528
+ unavailable() {
1529
+ const o = c.value === "" ? void 0 : named(c.value);
1530
+ return o?.disabled ? o.reason || "" : null;
1531
+ },
1532
+ setOptions(next) {
1533
+ options = next;
1534
+ c.setOptions(_offered(next));
1535
+ updateHint();
1536
+ },
1537
+ dispose: c.destroy
1538
+ };
1028
1539
  }
1029
1540
  function _mountControl(host, f, value, onChange) {
1030
1541
  const slot = host.querySelector(`[data-control-slot="${f.name}"]`);
@@ -1067,6 +1578,7 @@ function _mountControl(host, f, value, onChange) {
1067
1578
  dispose: () => c2.dispose()
1068
1579
  };
1069
1580
  }
1581
+ if (f.type === "select" && f.create) return _mountCombobox(slot, f, value);
1070
1582
  const common = { keepMissing: false, loose: true };
1071
1583
  const disabledReason = (entry) => entry?.disabled ? entry.reason || "" : null;
1072
1584
  if (f.type === "select") {
@@ -1173,31 +1685,11 @@ function _wirePicker(host, f) {
1173
1685
  const isPair = typeof picked === "object";
1174
1686
  hidden.value = String((isPair ? picked.value : picked) ?? "");
1175
1687
  display.value = isPair && picked.label != null ? String(picked.label) : _pickerLabel(f, hidden.value);
1176
- hidden.dispatchEvent(new Event("change", { bubbles: true }));
1688
+ fireEvent(hidden, "change", { bubbles: true });
1177
1689
  };
1178
1690
  btn.addEventListener("click", open);
1179
1691
  display.addEventListener("click", open);
1180
1692
  }
1181
- function _wireComboboxHint(host, f, optionsOf) {
1182
- const input = host.querySelector(`input[name="${f.name}"]`);
1183
- const hintEl = host.querySelector(`.twm-combobox__hint[data-for="${f.name}"]`);
1184
- if (!input || !hintEl) return;
1185
- if (f.create === true) return;
1186
- const label = _createSpec(f).hint || "new entry";
1187
- const update = () => {
1188
- const v = input.value.trim();
1189
- const known = new Set(flatChoices(optionsOf()).map((o) => String(o.value)));
1190
- if (v && !known.has(v)) {
1191
- hintEl.textContent = `\u21B3 will create ${label} \u201C${v}\u201D`;
1192
- hintEl.hidden = false;
1193
- } else {
1194
- hintEl.hidden = true;
1195
- }
1196
- };
1197
- input.addEventListener("input", update);
1198
- update();
1199
- return update;
1200
- }
1201
1693
  function _renderEntry(f, defaults) {
1202
1694
  if (f && f.section != null) {
1203
1695
  const attr = f.name ? ` data-field-row="${f.name}"` : "";
@@ -1273,21 +1765,6 @@ function _renderField(f, value) {
1273
1765
  </div>
1274
1766
  `);
1275
1767
  }
1276
- if (f.type === "select" && f.create) {
1277
- const shown = findChoice(flatChoices(f.options), value, true)?.disabled ? "" : v;
1278
- const dlId = `ea-dl-${f.name}-${_modalSeq}`;
1279
- const opts = _datalistOptions(f.options);
1280
- const placeholder2 = f.placeholder ? `placeholder="${_esc(f.placeholder)}"` : 'placeholder="type to pick or create\u2026"';
1281
- return wrap(`
1282
- <div class="twm-combobox">
1283
- <input name="${f.name}" type="text" list="${dlId}"
1284
- value="${_esc(shown)}" ${placeholder2} ${required}
1285
- autocomplete="off">
1286
- <datalist id="${dlId}" data-datalist-for="${f.name}">${opts}</datalist>
1287
- <span class="twm-combobox__hint" data-for="${f.name}" hidden></span>
1288
- </div>
1289
- `);
1290
- }
1291
1768
  if (f.type === "select") return wrap(slot);
1292
1769
  const step = f.step != null ? `step="${f.step}"` : "";
1293
1770
  const placeholder = f.placeholder ? `placeholder="${_esc(f.placeholder)}"` : "";
@@ -1301,13 +1778,109 @@ function _esc(s) {
1301
1778
  return String(s == null ? "" : s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1302
1779
  }
1303
1780
 
1781
+ // src/ui/components/context_menu.js
1782
+ var _activeMenu = null;
1783
+ var _returnFocusTo = null;
1784
+ function showContextMenu(x, y, items, onAction) {
1785
+ hideContextMenu();
1786
+ _returnFocusTo = document.activeElement;
1787
+ const menu = document.createElement("div");
1788
+ menu.className = "twm-context-menu ea-context-menu";
1789
+ menu.setAttribute("role", "menu");
1790
+ for (const it of items) {
1791
+ if (it.separator) {
1792
+ const sep = document.createElement("div");
1793
+ sep.className = "twm-context-menu__separator";
1794
+ sep.setAttribute("role", "separator");
1795
+ menu.appendChild(sep);
1796
+ continue;
1797
+ }
1798
+ const row = document.createElement("button");
1799
+ row.type = "button";
1800
+ row.setAttribute("role", "menuitem");
1801
+ let cls = "twm-context-menu-item";
1802
+ if (it.danger) cls += " twm-delete-node";
1803
+ if (it.disabled) cls += " disabled";
1804
+ row.className = cls;
1805
+ if (it.disabled) {
1806
+ row.disabled = true;
1807
+ row.setAttribute("aria-disabled", "true");
1808
+ }
1809
+ if (it.title) row.title = it.title;
1810
+ row.innerHTML = `
1811
+ <span class="material-symbols-outlined">${it.icon || ""}</span>
1812
+ <span>${escapeHtml(it.label)}</span>
1813
+ `;
1814
+ if (!it.disabled) {
1815
+ row.addEventListener("click", (e) => {
1816
+ e.stopPropagation();
1817
+ hideContextMenu();
1818
+ onAction?.(it.action);
1819
+ });
1820
+ }
1821
+ menu.appendChild(row);
1822
+ }
1823
+ (modalHost() || document.body).appendChild(menu);
1824
+ menu.style.display = "block";
1825
+ _activeMenu = menu;
1826
+ placeMenuInViewport(menu, x, y);
1827
+ setTimeout(() => {
1828
+ document.addEventListener("mousedown", _outsideHandler, { once: true, capture: true });
1829
+ }, 0);
1830
+ document.addEventListener("keydown", _keyHandler);
1831
+ window.addEventListener("scroll", _scrollHandler, { capture: true });
1832
+ focusMenuItem(_enabledItems(menu)[0]);
1833
+ }
1834
+ function hideContextMenu() {
1835
+ if (!_activeMenu) return;
1836
+ const returnTo = _returnFocusTo;
1837
+ const held = _activeMenu.contains(document.activeElement);
1838
+ _activeMenu.remove();
1839
+ _activeMenu = null;
1840
+ _returnFocusTo = null;
1841
+ document.removeEventListener("keydown", _keyHandler);
1842
+ window.removeEventListener("scroll", _scrollHandler, { capture: true });
1843
+ if (held && returnTo?.isConnected) returnTo.focus?.({ preventScroll: true });
1844
+ }
1845
+ function _enabledItems(menu) {
1846
+ return [...menu.querySelectorAll(".twm-context-menu-item:not(.disabled)")];
1847
+ }
1848
+ function _keyHandler(e) {
1849
+ if (!_activeMenu) return;
1850
+ if (e.key === "Escape") {
1851
+ e.preventDefault();
1852
+ hideContextMenu();
1853
+ return;
1854
+ }
1855
+ moveMenuFocus(e, _enabledItems(_activeMenu));
1856
+ }
1857
+ function _scrollHandler(e) {
1858
+ if (_activeMenu && e.target?.nodeType && _activeMenu.contains(e.target)) return;
1859
+ hideContextMenu();
1860
+ }
1861
+ function _outsideHandler(e) {
1862
+ if (_activeMenu && !_activeMenu.contains(e.target)) hideContextMenu();
1863
+ }
1864
+ function escapeHtml(s) {
1865
+ return String(s).replace(/[&<>"']/g, (c) => ({
1866
+ "&": "&amp;",
1867
+ "<": "&lt;",
1868
+ ">": "&gt;",
1869
+ '"': "&quot;",
1870
+ "'": "&#39;"
1871
+ })[c]);
1872
+ }
1873
+
1304
1874
  export {
1305
1875
  SortableList,
1306
1876
  ColorListField,
1307
- setModalHost,
1308
- modalHost,
1877
+ createOptionList,
1878
+ attachInputOptions,
1879
+ createCombobox,
1309
1880
  openForm,
1310
1881
  openConfirm,
1311
- openModal
1882
+ openModal,
1883
+ showContextMenu,
1884
+ hideContextMenu
1312
1885
  };
1313
- //# sourceMappingURL=chunk-XJNADRGR.js.map
1886
+ //# sourceMappingURL=chunk-RKVYIXDC.js.map