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
@@ -1,107 +0,0 @@
1
- import {
2
- modalHost
3
- } from "./chunk-XJNADRGR.js";
4
- import {
5
- focusMenuItem,
6
- moveMenuFocus,
7
- placeMenuInViewport
8
- } from "./chunk-WUFLGETN.js";
9
-
10
- // src/ui/components/context_menu.js
11
- var _activeMenu = null;
12
- var _returnFocusTo = null;
13
- function showContextMenu(x, y, items, onAction) {
14
- hideContextMenu();
15
- _returnFocusTo = document.activeElement;
16
- const menu = document.createElement("div");
17
- menu.className = "twm-context-menu ea-context-menu";
18
- menu.setAttribute("role", "menu");
19
- for (const it of items) {
20
- if (it.separator) {
21
- const sep = document.createElement("div");
22
- sep.className = "twm-context-menu__separator";
23
- sep.setAttribute("role", "separator");
24
- menu.appendChild(sep);
25
- continue;
26
- }
27
- const row = document.createElement("button");
28
- row.type = "button";
29
- row.setAttribute("role", "menuitem");
30
- let cls = "twm-context-menu-item";
31
- if (it.danger) cls += " twm-delete-node";
32
- if (it.disabled) cls += " disabled";
33
- row.className = cls;
34
- if (it.disabled) {
35
- row.disabled = true;
36
- row.setAttribute("aria-disabled", "true");
37
- }
38
- if (it.title) row.title = it.title;
39
- row.innerHTML = `
40
- <span class="material-symbols-outlined">${it.icon || ""}</span>
41
- <span>${escapeHtml(it.label)}</span>
42
- `;
43
- if (!it.disabled) {
44
- row.addEventListener("click", (e) => {
45
- e.stopPropagation();
46
- hideContextMenu();
47
- onAction?.(it.action);
48
- });
49
- }
50
- menu.appendChild(row);
51
- }
52
- (modalHost() || document.body).appendChild(menu);
53
- menu.style.display = "block";
54
- _activeMenu = menu;
55
- placeMenuInViewport(menu, x, y);
56
- setTimeout(() => {
57
- document.addEventListener("mousedown", _outsideHandler, { once: true, capture: true });
58
- }, 0);
59
- document.addEventListener("keydown", _keyHandler);
60
- window.addEventListener("scroll", _scrollHandler, { capture: true });
61
- focusMenuItem(_enabledItems(menu)[0]);
62
- }
63
- function hideContextMenu() {
64
- if (!_activeMenu) return;
65
- const returnTo = _returnFocusTo;
66
- const held = _activeMenu.contains(document.activeElement);
67
- _activeMenu.remove();
68
- _activeMenu = null;
69
- _returnFocusTo = null;
70
- document.removeEventListener("keydown", _keyHandler);
71
- window.removeEventListener("scroll", _scrollHandler, { capture: true });
72
- if (held && returnTo?.isConnected) returnTo.focus?.({ preventScroll: true });
73
- }
74
- function _enabledItems(menu) {
75
- return [...menu.querySelectorAll(".twm-context-menu-item:not(.disabled)")];
76
- }
77
- function _keyHandler(e) {
78
- if (!_activeMenu) return;
79
- if (e.key === "Escape") {
80
- e.preventDefault();
81
- hideContextMenu();
82
- return;
83
- }
84
- moveMenuFocus(e, _enabledItems(_activeMenu));
85
- }
86
- function _scrollHandler(e) {
87
- if (_activeMenu && e.target?.nodeType && _activeMenu.contains(e.target)) return;
88
- hideContextMenu();
89
- }
90
- function _outsideHandler(e) {
91
- if (_activeMenu && !_activeMenu.contains(e.target)) hideContextMenu();
92
- }
93
- function escapeHtml(s) {
94
- return String(s).replace(/[&<>"']/g, (c) => ({
95
- "&": "&amp;",
96
- "<": "&lt;",
97
- ">": "&gt;",
98
- '"': "&quot;",
99
- "'": "&#39;"
100
- })[c]);
101
- }
102
-
103
- export {
104
- showContextMenu,
105
- hideContextMenu
106
- };
107
- //# sourceMappingURL=chunk-7JI7HHSY.js.map
@@ -1,7 +0,0 @@
1
- {
2
- "version": 3,
3
- "sources": ["../src/ui/components/context_menu.js"],
4
- "sourcesContent": ["/**\n * context_menu.js \u2014 small popup menu in the `.context-menu` idiom the\n * DataTable's own right-click menu uses (./data_table.js).\n *\n * Usage:\n * showContextMenu(x, y, [\n * { label: 'Open', icon: 'open_in_new', action: 'open' },\n * { separator: true },\n * { label: 'Delete', icon: 'delete', action: 'delete', danger: true },\n * ], (action) => { ... });\n *\n * The menu auto-closes on outside click / scroll / Escape.\n *\n * \u2500\u2500 It is operable by keyboard \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * Items were bare `<div>`s with a click listener: no role, no tab stop, no\n * arrow-key movement, and `showContextMenu` never moved focus into the menu.\n * A consumer that opened this from Shift+F10 \u2014 the rail in Tables does \u2014 put a\n * menu on screen that the keyboard could only dismiss. Every command in it was\n * unreachable without a pointer, and nothing said so.\n *\n * So an item is a `<button role=\"menuitem\">`, the first enabled one takes focus\n * when the menu opens, Up/Down/Home/End move between them and wrap, Enter and\n * Space activate, Escape closes, and focus returns to whatever had it before.\n * Disabled items are skipped by the arrows rather than focusable-but-inert.\n * `<button>` rather than a div with `tabindex`: the browser then gives Enter and\n * Space for free and screen readers announce it without further help.\n *\n * \u2500\u2500 It stays inside the window (BUG-0040) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n * The top was clamped at 0 but nothing capped the height, so a long menu ran\n * off the bottom and could not scroll. `placeMenuInViewport` caps it at the\n * window's height less a margin; it scrolls inside, the arrows scroll the\n * focused item into view, and scrolling the menu itself does not close it.\n */\n\nimport { modalHost } from './modal.js';\nimport { placeMenuInViewport, moveMenuFocus, focusMenuItem } from '../utils/menu_viewport.js';\n\nlet _activeMenu = null;\nlet _returnFocusTo = null;\n\nexport function showContextMenu(x, y, items, onAction) {\n hideContextMenu();\n\n // Whatever had focus when the menu opened gets it back when the menu\n // closes. Without this a keyboard user who presses Escape is returned to\n // `document.body` and has to tab back to where they were.\n _returnFocusTo = document.activeElement;\n\n const menu = document.createElement('div');\n menu.className = 'twm-context-menu ea-context-menu';\n menu.setAttribute('role', 'menu');\n\n for (const it of items) {\n if (it.separator) {\n const sep = document.createElement('div');\n sep.className = 'twm-context-menu__separator';\n sep.setAttribute('role', 'separator');\n menu.appendChild(sep);\n continue;\n }\n const row = document.createElement('button');\n row.type = 'button';\n row.setAttribute('role', 'menuitem');\n let cls = 'twm-context-menu-item';\n if (it.danger) cls += ' twm-delete-node';\n if (it.disabled) cls += ' disabled';\n row.className = cls;\n if (it.disabled) {\n row.disabled = true;\n row.setAttribute('aria-disabled', 'true');\n }\n // A DISABLED ROW WITH NO EXPLANATION IS A DEAD CONTROL. `title` was\n // accepted by callers and rendered by nothing \u2014 the item was built with\n // one, the row silently dropped it, and the user got a greyed line with\n // no way to learn why. It is the same shape as the classes this\n // repository keeps finding: no error, no throw, and invisible to every\n // test that checks the row is disabled.\n if (it.title) row.title = it.title;\n row.innerHTML = `\n <span class=\"material-symbols-outlined\">${it.icon || ''}</span>\n <span>${escapeHtml(it.label)}</span>\n `;\n if (!it.disabled) {\n row.addEventListener('click', (e) => {\n e.stopPropagation();\n hideContextMenu();\n onAction?.(it.action);\n });\n }\n menu.appendChild(row);\n }\n\n // C25. THE SAME QUESTION A MODAL ASKS: which window is the user in? A\n // consumer spanning two browser windows sets the host when its focus moves;\n // null \u2014 every consumer today \u2014 is `document.body`, exactly as before. A\n // menu in the wrong window is worse than a modal in the wrong window,\n // because it is positioned at coordinates from the OTHER one.\n (modalHost() || document.body).appendChild(menu);\n menu.style.display = 'block';\n _activeMenu = menu;\n\n // Position inside the window, scrolling when taller than it (BUG-0040).\n placeMenuInViewport(menu, x, y);\n\n // Close on outside click / scroll / Escape. A scroll INSIDE the menu is\n // the menu being read, not the page moving under it.\n setTimeout(() => {\n document.addEventListener('mousedown', _outsideHandler, { once: true, capture: true });\n }, 0);\n document.addEventListener('keydown', _keyHandler);\n window.addEventListener('scroll', _scrollHandler, { capture: true });\n\n // Focus the first item the keyboard can actually use. `preventScroll` so a\n // menu opened near the bottom of a long page does not jump it.\n focusMenuItem(_enabledItems(menu)[0]);\n}\n\nexport function hideContextMenu() {\n if (!_activeMenu) return;\n const returnTo = _returnFocusTo;\n const held = _activeMenu.contains(document.activeElement);\n _activeMenu.remove();\n _activeMenu = null;\n _returnFocusTo = null;\n document.removeEventListener('keydown', _keyHandler);\n window.removeEventListener('scroll', _scrollHandler, { capture: true });\n // Only take focus back if the menu still had it. A click elsewhere has\n // already moved focus deliberately and must not be undone.\n if (held && returnTo?.isConnected) returnTo.focus?.({ preventScroll: true });\n}\n\nfunction _enabledItems(menu) {\n return [...menu.querySelectorAll('.twm-context-menu-item:not(.disabled)')];\n}\n\n/** Up/Down/Home/End move; the list WRAPS, which is what a menu of four items\n * wants and what every desktop menu does. Enter and Space are the button's\n * own, so they are not bound here. */\nfunction _keyHandler(e) {\n if (!_activeMenu) return;\n if (e.key === 'Escape') { e.preventDefault(); hideContextMenu(); return; }\n moveMenuFocus(e, _enabledItems(_activeMenu));\n}\n\nfunction _scrollHandler(e) {\n if (_activeMenu && e.target?.nodeType && _activeMenu.contains(e.target)) return;\n hideContextMenu();\n}\n\nfunction _outsideHandler(e) {\n if (_activeMenu && !_activeMenu.contains(e.target)) hideContextMenu();\n}\n\nfunction escapeHtml(s) {\n return String(s).replace(/[&<>\"']/g, (c) => ({\n '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;', \"'\": '&#39;',\n }[c]));\n}\n"],
5
- "mappings": "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",
6
- "names": []
7
- }
@@ -1,526 +0,0 @@
1
- // src/ui/base/component_base.js
2
- var ComponentBase = class _ComponentBase {
3
- constructor({ eventBus, logger } = {}) {
4
- if (new.target === _ComponentBase) {
5
- throw new Error("ComponentBase is abstract \u2013 extend it instead");
6
- }
7
- this.eventBus = eventBus;
8
- this.logger = logger;
9
- this._mounted = false;
10
- }
11
- mount() {
12
- throw new Error("ComponentBase.mount must be implemented by subclasses");
13
- }
14
- update() {
15
- }
16
- dispose() {
17
- this._mounted = false;
18
- }
19
- };
20
-
21
- // src/ui/components/action_dropdown.js
22
- var EDGE_MARGIN = 8;
23
- var GAP = 8;
24
- var ActionDropdown = class _ActionDropdown {
25
- /**
26
- * @param {Object} config
27
- * @param {HTMLElement} config.trigger - Button element that triggers the dropdown
28
- * @param {Array} config.options - Array of option objects with { type, label, icon, description? },
29
- * or `{ separator: true }` for a divider between them
30
- * @param {Function} config.onSelect - Callback when an option is selected
31
- * @param {string} [config.className=''] - Additional CSS class for the dropdown
32
- * @param {string} [config.menuId] - Optional ID for the menu element
33
- */
34
- constructor(config) {
35
- this.trigger = config.trigger;
36
- this.options = config.options || [];
37
- this.onSelect = config.onSelect;
38
- this.className = config.className || "";
39
- this.menuId = config.menuId;
40
- this.isOpen = false;
41
- this.menuEl = null;
42
- this._boundHandleDocumentClick = this._handleDocumentClick.bind(this);
43
- this._boundHandleKeydown = this._handleKeydown.bind(this);
44
- this._boundHandleTriggerClick = this._handleTriggerClick.bind(this);
45
- this._init();
46
- }
47
- /**
48
- * Initialize the dropdown.
49
- * @private
50
- */
51
- _init() {
52
- if (!this.trigger) {
53
- console.warn("[ActionDropdown] No trigger element provided");
54
- return;
55
- }
56
- this._createMenu();
57
- this.trigger.addEventListener("click", this._boundHandleTriggerClick);
58
- }
59
- /**
60
- * Create the dropdown menu element.
61
- * @private
62
- */
63
- _createMenu() {
64
- const menu = document.createElement("div");
65
- menu.className = `twm-action-dropdown-menu ${this.className}`.trim();
66
- menu.setAttribute("role", "menu");
67
- menu.hidden = true;
68
- if (this.menuId) {
69
- menu.id = this.menuId;
70
- }
71
- this._renderOptions(menu);
72
- document.body.appendChild(menu);
73
- this.menuEl = menu;
74
- }
75
- /**
76
- * Render option buttons, and dividers for separator entries, into a
77
- * container element. `data-index` stays the entry's index in `options`.
78
- * @param {HTMLElement} container
79
- * @private
80
- */
81
- _renderOptions(container) {
82
- container.innerHTML = "";
83
- let pendingSeparator = false;
84
- this.options.forEach((opt, index) => {
85
- if (opt?.separator) {
86
- pendingSeparator = container.childElementCount > 0;
87
- return;
88
- }
89
- if (pendingSeparator) {
90
- const sep = document.createElement("div");
91
- sep.className = "twm-action-dropdown-separator";
92
- sep.setAttribute("role", "separator");
93
- container.appendChild(sep);
94
- pendingSeparator = false;
95
- }
96
- const optionBtn = document.createElement("button");
97
- optionBtn.type = "button";
98
- optionBtn.className = "twm-action-dropdown-option";
99
- optionBtn.dataset.index = index;
100
- optionBtn.dataset.type = opt.type;
101
- optionBtn.setAttribute("role", "menuitem");
102
- const iconHtml = opt.icon ? `<span class="material-symbols-outlined twm-option-icon">${opt.icon}</span>` : "";
103
- const descHtml = opt.description ? `<span class="twm-option-desc">${opt.description}</span>` : "";
104
- optionBtn.innerHTML = `
105
- ${iconHtml}
106
- <span class="option-text">
107
- <span class="twm-option-label">${opt.label}</span>
108
- ${descHtml}
109
- </span>
110
- `;
111
- optionBtn.addEventListener("click", (e) => {
112
- e.preventDefault();
113
- e.stopPropagation();
114
- this._selectOption(opt);
115
- });
116
- container.appendChild(optionBtn);
117
- });
118
- }
119
- /**
120
- * Handle trigger button click.
121
- * @param {MouseEvent} e
122
- * @private
123
- */
124
- _handleTriggerClick(e) {
125
- e.preventDefault();
126
- e.stopPropagation();
127
- this.toggle();
128
- }
129
- /**
130
- * Toggle the dropdown open/closed.
131
- * @param {boolean} [forceState] - Optional forced state
132
- */
133
- toggle(forceState) {
134
- const shouldOpen = typeof forceState === "boolean" ? forceState : !this.isOpen;
135
- if (shouldOpen) {
136
- this.open();
137
- } else {
138
- this.close();
139
- }
140
- }
141
- /**
142
- * Open the dropdown with auto-positioning.
143
- */
144
- open() {
145
- if (this.isOpen || !this.menuEl) return;
146
- this.isOpen = true;
147
- this.menuEl.hidden = false;
148
- _ActionDropdown.position(this.trigger, this.menuEl);
149
- requestAnimationFrame(() => {
150
- this.menuEl?.classList.add("visible");
151
- });
152
- this.trigger?.classList.add("twm-is-open");
153
- if (this.trigger?.hasAttribute("aria-expanded")) {
154
- this.trigger.setAttribute("aria-expanded", "true");
155
- }
156
- document.addEventListener("click", this._boundHandleDocumentClick, true);
157
- document.addEventListener("keydown", this._boundHandleKeydown);
158
- const firstOption = this.menuEl.querySelector(".twm-action-dropdown-option");
159
- firstOption?.focus();
160
- }
161
- /**
162
- * Close the dropdown.
163
- */
164
- close() {
165
- if (!this.isOpen) return;
166
- this.isOpen = false;
167
- if (this.menuEl) {
168
- this.menuEl.classList.remove("visible");
169
- this.menuEl.hidden = true;
170
- }
171
- this.trigger?.classList.remove("twm-is-open");
172
- if (this.trigger?.hasAttribute("aria-expanded")) {
173
- this.trigger.setAttribute("aria-expanded", "false");
174
- }
175
- document.removeEventListener("click", this._boundHandleDocumentClick, true);
176
- document.removeEventListener("keydown", this._boundHandleKeydown);
177
- }
178
- /**
179
- * Handle document click (close on outside click).
180
- * @param {MouseEvent} e
181
- * @private
182
- */
183
- _handleDocumentClick(e) {
184
- if (!this.isOpen) return;
185
- if (this.menuEl?.contains(e.target) || this.trigger?.contains(e.target)) {
186
- return;
187
- }
188
- this.close();
189
- }
190
- /**
191
- * Handle keyboard events.
192
- * @param {KeyboardEvent} e
193
- * @private
194
- */
195
- _handleKeydown(e) {
196
- if (e.key === "Escape") {
197
- e.preventDefault();
198
- e.stopPropagation();
199
- this.close();
200
- this.trigger?.focus();
201
- return;
202
- }
203
- if (e.key === "ArrowDown" || e.key === "ArrowUp") {
204
- e.preventDefault();
205
- this._navigateOptions(e.key === "ArrowDown" ? 1 : -1);
206
- }
207
- }
208
- /**
209
- * Navigate options with arrow keys.
210
- * @param {number} direction - 1 for down, -1 for up
211
- * @private
212
- */
213
- _navigateOptions(direction) {
214
- const options = this.menuEl?.querySelectorAll(".twm-action-dropdown-option");
215
- if (!options || options.length === 0) return;
216
- const currentIndex = Array.from(options).findIndex((opt) => opt === document.activeElement);
217
- let nextIndex = currentIndex + direction;
218
- if (nextIndex < 0) nextIndex = options.length - 1;
219
- if (nextIndex >= options.length) nextIndex = 0;
220
- options[nextIndex]?.focus();
221
- }
222
- /**
223
- * Handle option selection.
224
- * @param {Object} option - The selected option
225
- * @private
226
- */
227
- _selectOption(option) {
228
- this.close();
229
- if (this.onSelect) {
230
- this.onSelect(option);
231
- }
232
- }
233
- /**
234
- * Update the options dynamically.
235
- * @param {Array} newOptions - New options array
236
- */
237
- setOptions(newOptions) {
238
- this.options = newOptions;
239
- if (this.menuEl) {
240
- this._renderOptions(this.menuEl);
241
- }
242
- }
243
- /**
244
- * Destroy the dropdown and clean up.
245
- */
246
- destroy() {
247
- this.close();
248
- this.trigger?.removeEventListener("click", this._boundHandleTriggerClick);
249
- this.menuEl?.remove();
250
- this.menuEl = null;
251
- this.trigger = null;
252
- this.options = [];
253
- this.onSelect = null;
254
- }
255
- // =========================================================================
256
- // STATIC — shared auto-positioning for any trigger + menu pair
257
- // =========================================================================
258
- /**
259
- * Position a dropdown menu relative to a trigger element.
260
- * Uses `position: fixed` to avoid overflow clipping.
261
- * Picks above/below based on available space, aligns left/right edge,
262
- * and applies `max-height` + `overflow-y: auto` when the menu is taller
263
- * than the available space.
264
- *
265
- * @param {HTMLElement} trigger - The element the dropdown opens from
266
- * @param {HTMLElement} menu - The dropdown menu element to position
267
- * @param {Object} [opts]
268
- * @param {'start'|'end'} [opts.align='start'] - Which trigger edge the menu
269
- * lines up with first: `'start'` its left edge, `'end'` its right edge.
270
- * Either flips to the other edge when it would leave the window.
271
- */
272
- static position(trigger, menu, { align = "start" } = {}) {
273
- if (!trigger || !menu) return;
274
- Object.assign(menu.style, {
275
- position: "fixed",
276
- top: "auto",
277
- bottom: "auto",
278
- left: "auto",
279
- right: "auto",
280
- maxHeight: "",
281
- overflowY: ""
282
- });
283
- const triggerRect = trigger.getBoundingClientRect();
284
- const menuRect = menu.getBoundingClientRect();
285
- const vw = window.innerWidth;
286
- const vh = window.innerHeight;
287
- const spaceBelow = vh - triggerRect.bottom - GAP - EDGE_MARGIN;
288
- const spaceAbove = triggerRect.top - GAP - EDGE_MARGIN;
289
- let top;
290
- let maxHeight;
291
- if (menuRect.height <= spaceBelow) {
292
- top = triggerRect.bottom + GAP;
293
- maxHeight = spaceBelow;
294
- } else if (menuRect.height <= spaceAbove) {
295
- top = triggerRect.top - GAP - menuRect.height;
296
- maxHeight = spaceAbove;
297
- } else if (spaceBelow >= spaceAbove) {
298
- top = triggerRect.bottom + GAP;
299
- maxHeight = spaceBelow;
300
- } else {
301
- maxHeight = spaceAbove;
302
- top = EDGE_MARGIN;
303
- }
304
- let left;
305
- if (align === "end") {
306
- left = triggerRect.right - menuRect.width;
307
- if (left < EDGE_MARGIN) {
308
- left = triggerRect.left;
309
- }
310
- } else {
311
- left = triggerRect.left;
312
- if (left + menuRect.width > vw - EDGE_MARGIN) {
313
- left = triggerRect.right - menuRect.width;
314
- }
315
- }
316
- left = Math.max(EDGE_MARGIN, Math.min(left, vw - menuRect.width - EDGE_MARGIN));
317
- Object.assign(menu.style, {
318
- position: "fixed",
319
- top: `${Math.round(top)}px`,
320
- left: `${Math.round(left)}px`,
321
- maxHeight: `${Math.round(maxHeight)}px`,
322
- overflowY: "auto"
323
- });
324
- }
325
- };
326
-
327
- // src/ui/components/slide_out_panel.js
328
- var SlideOutPanel = class extends ComponentBase {
329
- /** @type {HTMLElement} */
330
- #container = null;
331
- /** @type {HTMLElement} */
332
- #backdrop = null;
333
- /** @type {HTMLElement} */
334
- #panel = null;
335
- /** @type {HTMLElement} */
336
- #headerIcon = null;
337
- /** @type {HTMLElement} */
338
- #headerTitle = null;
339
- /** @type {HTMLElement} */
340
- #body = null;
341
- /** @type {number} */
342
- #width;
343
- /** @type {Function|null} */
344
- #onClose;
345
- /** @type {boolean} */
346
- #open = false;
347
- /** @type {ResizeObserver|null} */
348
- #hostObserver = null;
349
- /**
350
- * @param {object} opts
351
- * @param {number} [opts.width=380] Panel width in px.
352
- * @param {Function} [opts.onClose] Called after the panel closes.
353
- */
354
- constructor({ width = 700, onClose } = {}) {
355
- super();
356
- this.#width = width;
357
- this.#onClose = onClose ?? null;
358
- }
359
- // ─── Lifecycle ──────────────────────────────────────────────────────────
360
- /**
361
- * Mount the panel DOM into a host container.
362
- * @param {HTMLElement} container Must have position:relative; overflow:hidden.
363
- */
364
- mount(container) {
365
- if (this._mounted) return;
366
- this.#container = container;
367
- this.#backdrop = document.createElement("div");
368
- this.#backdrop.className = "twm-slide-out-panel__backdrop";
369
- this.#backdrop.addEventListener("click", this.#handleBackdropClick);
370
- this.#panel = document.createElement("div");
371
- this.#panel.className = "twm-slide-out-panel";
372
- this.#panel.style.width = `${this.#width}px`;
373
- const header = document.createElement("div");
374
- header.className = "twm-slide-out-panel__header";
375
- this.#headerIcon = document.createElement("span");
376
- this.#headerIcon.className = "twm-slide-out-panel__icon material-symbols-outlined";
377
- this.#headerTitle = document.createElement("span");
378
- this.#headerTitle.className = "twm-slide-out-panel__title";
379
- const closeBtn = document.createElement("button");
380
- closeBtn.className = "twm-slide-out-panel__close twm-btn-icon";
381
- closeBtn.type = "button";
382
- closeBtn.title = "Close";
383
- closeBtn.innerHTML = '<span class="material-symbols-outlined">close</span>';
384
- closeBtn.addEventListener("click", this.#handleCloseClick);
385
- header.append(this.#headerIcon, this.#headerTitle, closeBtn);
386
- this.#body = document.createElement("div");
387
- this.#body.className = "twm-slide-out-panel__body";
388
- this.#panel.append(header, this.#body);
389
- this.#container.append(this.#backdrop, this.#panel);
390
- this._mounted = true;
391
- }
392
- dispose() {
393
- this.close();
394
- this.#backdrop?.remove();
395
- this.#panel?.remove();
396
- this.#backdrop = null;
397
- this.#panel = null;
398
- this.#body = null;
399
- this.#container = null;
400
- super.dispose();
401
- }
402
- // ─── Public API ─────────────────────────────────────────────────────────
403
- /** The inner content element — callers render config into this. */
404
- get contentEl() {
405
- return this.#body;
406
- }
407
- get isOpen() {
408
- return this.#open;
409
- }
410
- /**
411
- * Open (or re-open) the panel with a new title/icon.
412
- * Clears previous content. Returns contentEl for rendering.
413
- * @param {string} title
414
- * @param {string} icon Material Symbols icon name.
415
- * @returns {HTMLElement} contentEl
416
- */
417
- open(title, icon) {
418
- if (!this._mounted) return null;
419
- this.#body.innerHTML = "";
420
- this.#headerIcon.textContent = icon || "";
421
- this.#headerIcon.style.display = icon ? "" : "none";
422
- this.#headerTitle.textContent = title || "";
423
- if (!this.#open) {
424
- this.#open = true;
425
- this.#backdrop.classList.add("twm-slide-out-panel__backdrop--visible");
426
- this.#panel.classList.add("twm-slide-out-panel--open");
427
- document.addEventListener("keydown", this.#handleEsc);
428
- this.#trackGeometry();
429
- window.addEventListener("scroll", this.#trackGeometry, true);
430
- window.addEventListener("resize", this.#trackGeometry);
431
- this.#observeHost();
432
- }
433
- return this.#body;
434
- }
435
- /** Close the panel and clear content. */
436
- close() {
437
- if (!this.#open) return;
438
- this.#open = false;
439
- this.#panel.classList.remove("twm-slide-out-panel--open");
440
- this.#backdrop.classList.remove("twm-slide-out-panel__backdrop--visible");
441
- document.removeEventListener("keydown", this.#handleEsc);
442
- window.removeEventListener("scroll", this.#trackGeometry, true);
443
- window.removeEventListener("resize", this.#trackGeometry);
444
- this.#hostObserver?.disconnect();
445
- this.#hostObserver = null;
446
- this.#body.innerHTML = "";
447
- this.#onClose?.();
448
- }
449
- // ─── Event handlers (arrow fns for stable `this`) ───────────────────────
450
- #handleBackdropClick = () => {
451
- this.close();
452
- };
453
- #handleCloseClick = () => {
454
- this.close();
455
- };
456
- #handleEsc = (e) => {
457
- if (e.key === "Escape") {
458
- e.stopPropagation();
459
- this.close();
460
- }
461
- };
462
- /** The host moves or resizes without any window event when a tile
463
- * splitter is dragged or a dock panel toggles, and the fixed geometry
464
- * would then stay at the old coordinates. Observe the host AND each
465
- * ancestor: a host can move without changing size of its own (a dock
466
- * panel toggling beside a fixed-width tile shrinks the content area,
467
- * an ancestor, and the tile only shifts). The callback runs
468
- * synchronously, after layout and before paint, so the panel follows
469
- * a drag without a frame of lag; it only writes the panel's own
470
- * `position: fixed` geometry, which is out of flow and resizes
471
- * nothing observed, so it cannot loop. */
472
- #observeHost() {
473
- this.#hostObserver = new ResizeObserver(this.#trackGeometry);
474
- for (let el = this.#container; el; el = el.parentElement) {
475
- this.#hostObserver.observe(el);
476
- }
477
- }
478
- /** Pin the panel to the right edge of its host's *visible* frame
479
- * using `position: fixed`. Re-run on every window scroll/resize and
480
- * every host resize (#observeHost) so the panel always overlays the
481
- * host correctly even when the host itself sits inside a scrolled
482
- * ancestor.
483
- *
484
- * UNDER A CSS `zoom` (the shell's content zoom scales a tile body),
485
- * `getBoundingClientRect()` answers in screen pixels, which already
486
- * carry the zoom, while the panel's own `top`/`left`/`height` are
487
- * CSS pixels that the same zoom multiplies again. Copied across
488
- * unchanged, every length was scaled twice: at 150% the panel landed
489
- * right of and below its host, at 50% it was half the host's height.
490
- * So the rect is divided by the panel's effective zoom
491
- * (`currentCSSZoom`: 1 when nothing above it zooms, `undefined` in an
492
- * engine that predates it), and the width subtracted is the panel's
493
- * laid-out one, which `min-width` may hold above the requested
494
- * width. Rounding happens in screen pixels, so at 100% the output is
495
- * exactly what it always was.
496
- *
497
- * NEVER WIDER THAN THE HOST (BUG-0035). The right edge is pinned, so a
498
- * panel wider than its host (700px by default, `min-width: 400px`)
499
- * reached past the host's left edge across the neighbouring pane. The
500
- * host's width, in the panel's CSS pixels, goes on the panel as
501
- * `--twm-slide-out-host-w`, which the stylesheet uses as `max-width`
502
- * and as the ceiling of the 400px `min-width`; it is set BEFORE the
503
- * width is read so the read sees the capped layout, and the width is
504
- * capped here too for an engine that lays nothing out. */
505
- #trackGeometry = () => {
506
- if (!this.#open || !this.#panel || !this.#container) return;
507
- const r = this.#container.getBoundingClientRect();
508
- const zoom = this.#panel.currentCSSZoom || 1;
509
- const hostWidth = r.width / zoom;
510
- const s = this.#panel.style;
511
- s.setProperty("--twm-slide-out-host-w", `${hostWidth}px`);
512
- const width = Math.min(this.#panel.offsetWidth || this.#width, hostWidth);
513
- s.position = "fixed";
514
- s.top = `${Math.round(r.top) / zoom}px`;
515
- s.left = `${Math.round(r.right - width * zoom) / zoom}px`;
516
- s.right = "auto";
517
- s.height = `${Math.round(r.height) / zoom}px`;
518
- };
519
- };
520
-
521
- export {
522
- ComponentBase,
523
- ActionDropdown,
524
- SlideOutPanel
525
- };
526
- //# sourceMappingURL=chunk-MIW7D6WC.js.map