flexdesk 0.1.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 (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -0
  3. package/THIRD_PARTY_LICENSES.md +53 -0
  4. package/css/base.css +3439 -0
  5. package/css/flexdesk.css +5797 -0
  6. package/css/overrides.css +2349 -0
  7. package/css/reset.css +46 -0
  8. package/css/tokens.css +137 -0
  9. package/dist/charts.js +780 -0
  10. package/dist/charts.js.map +7 -0
  11. package/dist/chunk-3PHCPZHT.js +222 -0
  12. package/dist/chunk-3PHCPZHT.js.map +7 -0
  13. package/dist/chunk-CT4YXXLP.js +1770 -0
  14. package/dist/chunk-CT4YXXLP.js.map +7 -0
  15. package/dist/chunk-DRYCDMEG.js +2217 -0
  16. package/dist/chunk-DRYCDMEG.js.map +7 -0
  17. package/dist/chunk-DVU44T77.js +855 -0
  18. package/dist/chunk-DVU44T77.js.map +7 -0
  19. package/dist/chunk-FL5KFNQH.js +640 -0
  20. package/dist/chunk-FL5KFNQH.js.map +7 -0
  21. package/dist/chunk-FOOS3T5L.js +117 -0
  22. package/dist/chunk-FOOS3T5L.js.map +7 -0
  23. package/dist/chunk-JYWURG5T.js +32 -0
  24. package/dist/chunk-JYWURG5T.js.map +7 -0
  25. package/dist/chunk-TLZUUFOE.js +276 -0
  26. package/dist/chunk-TLZUUFOE.js.map +7 -0
  27. package/dist/chunk-UCJ2WD4D.js +625 -0
  28. package/dist/chunk-UCJ2WD4D.js.map +7 -0
  29. package/dist/chunk-WVFGV5FT.js +205 -0
  30. package/dist/chunk-WVFGV5FT.js.map +7 -0
  31. package/dist/core.js +1098 -0
  32. package/dist/core.js.map +7 -0
  33. package/dist/editor.js +1661 -0
  34. package/dist/editor.js.map +7 -0
  35. package/dist/flexdesk.css +5797 -0
  36. package/dist/host.js +132 -0
  37. package/dist/host.js.map +7 -0
  38. package/dist/reset.css +46 -0
  39. package/dist/tile_registry-6WZPTUZV.js +28 -0
  40. package/dist/tile_registry-6WZPTUZV.js.map +7 -0
  41. package/dist/tiles.js +2068 -0
  42. package/dist/tiles.js.map +7 -0
  43. package/dist/tokens.css +137 -0
  44. package/dist/widgets.js +5438 -0
  45. package/dist/widgets.js.map +7 -0
  46. package/dist/wm.js +4830 -0
  47. package/dist/wm.js.map +7 -0
  48. package/package.json +80 -0
  49. package/src/charts/chart_types.js +699 -0
  50. package/src/charts/data_series_plot_window.js +761 -0
  51. package/src/charts/downsample.js +119 -0
  52. package/src/charts/plot_config.js +176 -0
  53. package/src/charts/plot_popout_window.js +1560 -0
  54. package/src/charts/plotly_wrapper.js +432 -0
  55. package/src/core/event_bus.js +230 -0
  56. package/src/core/logging.js +300 -0
  57. package/src/core/settings.js +813 -0
  58. package/src/core/state_guard.js +382 -0
  59. package/src/core/state_guard_config.js +309 -0
  60. package/src/core/state_machine.js +258 -0
  61. package/src/editor/editor_pane.js +264 -0
  62. package/src/editor/monaco_editor_factory.js +449 -0
  63. package/src/editor/monaco_loader.js +74 -0
  64. package/src/editor/notebook_search.js +460 -0
  65. package/src/editor/notebook_tab_bar.js +456 -0
  66. package/src/editor/notebook_undo_manager.js +108 -0
  67. package/src/editor/split_pane_container.js +197 -0
  68. package/src/help/help_modal.js +409 -0
  69. package/src/help/help_registry.js +65 -0
  70. package/src/host/host.js +69 -0
  71. package/src/host/pywebview_host.js +133 -0
  72. package/src/tiles/config_schema.js +256 -0
  73. package/src/tiles/layout_persistence.js +662 -0
  74. package/src/tiles/tile_base.js +785 -0
  75. package/src/tiles/tile_grid.js +905 -0
  76. package/src/tiles/tile_registry.js +136 -0
  77. package/src/tiling/command_palette.js +328 -0
  78. package/src/tiling/content_registry.js +110 -0
  79. package/src/tiling/desktops.js +133 -0
  80. package/src/tiling/entity_sources.js +179 -0
  81. package/src/tiling/keymap.js +156 -0
  82. package/src/tiling/kind_taxonomy.js +172 -0
  83. package/src/tiling/landing_table.js +717 -0
  84. package/src/tiling/loading_overlay.js +33 -0
  85. package/src/tiling/nav_panel.js +142 -0
  86. package/src/tiling/page_factory.js +215 -0
  87. package/src/tiling/panel_keys.js +145 -0
  88. package/src/tiling/shell.js +437 -0
  89. package/src/tiling/tile_breadcrumb.js +222 -0
  90. package/src/tiling/tile_renderer.js +459 -0
  91. package/src/tiling/tile_tab_menu.js +340 -0
  92. package/src/tiling/tile_tree.js +880 -0
  93. package/src/tiling/wm.js +1352 -0
  94. package/src/ui/base/component_base.js +30 -0
  95. package/src/ui/base/controller_base.js +48 -0
  96. package/src/ui/base/page_base.js +102 -0
  97. package/src/ui/components/about_dialog.js +224 -0
  98. package/src/ui/components/action_dropdown.js +389 -0
  99. package/src/ui/components/attribute_list_editor.js +652 -0
  100. package/src/ui/components/autocomplete_field.js +732 -0
  101. package/src/ui/components/computing_status_window.js +81 -0
  102. package/src/ui/components/confirm_dialog.js +307 -0
  103. package/src/ui/components/context_menu.js +87 -0
  104. package/src/ui/components/data_table.js +2228 -0
  105. package/src/ui/components/detail_header.js +177 -0
  106. package/src/ui/components/drag_reorder.js +0 -0
  107. package/src/ui/components/gallery_picker.js +147 -0
  108. package/src/ui/components/inline_renamer.js +129 -0
  109. package/src/ui/components/managed_window.js +804 -0
  110. package/src/ui/components/modal.js +512 -0
  111. package/src/ui/components/notification_history.js +264 -0
  112. package/src/ui/components/slide_out_panel.js +185 -0
  113. package/src/ui/components/slider_field.js +122 -0
  114. package/src/ui/components/sortable_list.js +309 -0
  115. package/src/ui/components/table_state_store.js +81 -0
  116. package/src/ui/components/toast.js +59 -0
  117. package/src/ui/components/tree_view.js +460 -0
  118. package/src/ui/components/window_taskbar.js +143 -0
  119. package/src/ui/controllers/panel_state_machine.js +634 -0
  120. package/src/ui/controllers/window_chrome_controller.js +659 -0
  121. package/src/ui/notification_center.js +576 -0
  122. package/src/ui/utils/overlay_scrollbar.js +492 -0
  123. package/src/ui/utils/raf_resize_observer.js +31 -0
  124. package/src/ui/utils/tooltip_service.js +406 -0
@@ -0,0 +1,855 @@
1
+ import {
2
+ ManagedWindow
3
+ } from "./chunk-UCJ2WD4D.js";
4
+
5
+ // src/help/help_registry.js
6
+ var EMPTY_PROVIDER = Object.freeze({
7
+ getCategories: () => [],
8
+ getTopicsByCategory: () => [],
9
+ loadTopic: async () => null,
10
+ searchTopics: () => []
11
+ });
12
+ var DEFAULT_COPY = Object.freeze({
13
+ title: "Help",
14
+ welcome: "Select a topic from the sidebar or browse the categories below."
15
+ });
16
+ var _provider = EMPTY_PROVIDER;
17
+ var _categories = Object.freeze({});
18
+ var _copy = DEFAULT_COPY;
19
+ function setHelpProvider(provider, categories = {}, copy = {}) {
20
+ _provider = provider ?? EMPTY_PROVIDER;
21
+ _categories = Object.freeze({ ...categories });
22
+ _copy = Object.freeze({ ...DEFAULT_COPY, ...copy });
23
+ }
24
+ function helpProvider() {
25
+ return _provider;
26
+ }
27
+ function helpCategories() {
28
+ return _categories;
29
+ }
30
+ function helpCopy() {
31
+ return _copy;
32
+ }
33
+
34
+ // src/help/help_modal.js
35
+ var _activeModal = null;
36
+ var HelpModal = class _HelpModal {
37
+ /** The application's help, resolved at use time. */
38
+ get _service() {
39
+ return helpProvider();
40
+ }
41
+ get _categories() {
42
+ return helpCategories();
43
+ }
44
+ get _copy() {
45
+ return helpCopy();
46
+ }
47
+ constructor() {
48
+ this._overlay = null;
49
+ this._modal = null;
50
+ this._currentTopic = null;
51
+ this._searchQuery = "";
52
+ this._boundKeyHandler = this._handleKeyDown.bind(this);
53
+ }
54
+ /**
55
+ * Open the help modal.
56
+ * @param {string} [topicId] - Initial topic to display
57
+ */
58
+ async open(topicId = null) {
59
+ if (_activeModal && _activeModal !== this) {
60
+ _activeModal.close();
61
+ }
62
+ _activeModal = this;
63
+ this._createModal();
64
+ document.body.appendChild(this._overlay);
65
+ document.addEventListener("keydown", this._boundKeyHandler);
66
+ const searchInput = this._modal.querySelector(".help-search-input");
67
+ if (searchInput) {
68
+ setTimeout(() => searchInput.focus(), 100);
69
+ }
70
+ if (topicId) {
71
+ await this._loadTopic(topicId);
72
+ } else {
73
+ this._showIndex();
74
+ }
75
+ }
76
+ /**
77
+ * Close the help modal.
78
+ */
79
+ close() {
80
+ if (this._overlay) {
81
+ document.removeEventListener("keydown", this._boundKeyHandler);
82
+ this._overlay.remove();
83
+ this._overlay = null;
84
+ this._modal = null;
85
+ }
86
+ if (_activeModal === this) {
87
+ _activeModal = null;
88
+ }
89
+ }
90
+ /**
91
+ * Create the modal DOM structure.
92
+ * @private
93
+ */
94
+ _createModal() {
95
+ this._overlay = document.createElement("div");
96
+ this._overlay.className = "help-modal-overlay";
97
+ this._overlay.addEventListener("click", (e) => {
98
+ if (e.target === this._overlay) this.close();
99
+ });
100
+ this._modal = document.createElement("div");
101
+ this._modal.className = "help-modal";
102
+ this._modal.innerHTML = `
103
+ <div class="help-modal__header">
104
+ <div class="help-modal__title">
105
+ <span class="material-symbols-outlined">help</span>
106
+ <span>Help</span>
107
+ </div>
108
+ <button class="help-modal__close" title="Close (Esc)">
109
+ <span class="material-symbols-outlined">close</span>
110
+ </button>
111
+ </div>
112
+ <div class="help-modal__search">
113
+ <span class="material-symbols-outlined">search</span>
114
+ <input type="text" class="help-search-input" placeholder="Search help topics..." autocomplete="off">
115
+ </div>
116
+ <div class="help-modal__body">
117
+ <nav class="help-modal__sidebar">
118
+ <div class="help-sidebar__categories"></div>
119
+ </nav>
120
+ <main class="help-modal__content">
121
+ <div class="help-content__loading">Loading...</div>
122
+ </main>
123
+ </div>
124
+ `;
125
+ const closeBtn = this._modal.querySelector(".help-modal__close");
126
+ closeBtn.addEventListener("click", () => this.close());
127
+ const searchInput = this._modal.querySelector(".help-search-input");
128
+ searchInput.addEventListener("input", (e) => this._handleSearch(e.target.value));
129
+ searchInput.addEventListener("keydown", (e) => {
130
+ if (e.key === "Escape") {
131
+ if (e.target.value) {
132
+ e.target.value = "";
133
+ this._handleSearch("");
134
+ e.stopPropagation();
135
+ }
136
+ }
137
+ });
138
+ this._buildSidebar();
139
+ this._overlay.appendChild(this._modal);
140
+ }
141
+ /**
142
+ * Build the sidebar navigation.
143
+ * @private
144
+ */
145
+ _buildSidebar() {
146
+ const container = this._modal.querySelector(".help-sidebar__categories");
147
+ const categories = this._service.getCategories();
148
+ const categoryOrder = Object.keys(this._categories);
149
+ categories.sort((a, b) => {
150
+ const orderA = categoryOrder.indexOf(a);
151
+ const orderB = categoryOrder.indexOf(b);
152
+ return (orderA === -1 ? 99 : orderA) - (orderB === -1 ? 99 : orderB);
153
+ });
154
+ container.innerHTML = categories.map((category) => {
155
+ const topics = this._service.getTopicsByCategory(category);
156
+ const label = this._categories[category]?.label || category;
157
+ const icon = this._categories[category]?.icon || "folder";
158
+ return `
159
+ <div class="help-category" data-category="${category}">
160
+ <div class="help-category__header">
161
+ <span class="material-symbols-outlined">${icon}</span>
162
+ <span>${label}</span>
163
+ </div>
164
+ <ul class="help-category__topics">
165
+ ${topics.map((topic) => `
166
+ <li class="help-topic-item" data-topic="${topic.id}">
167
+ ${topic.title}
168
+ </li>
169
+ `).join("")}
170
+ </ul>
171
+ </div>
172
+ `;
173
+ }).join("");
174
+ container.querySelectorAll(".help-topic-item").forEach((item) => {
175
+ item.addEventListener("click", () => {
176
+ this._loadTopic(item.dataset.topic);
177
+ });
178
+ });
179
+ container.querySelectorAll(".help-category__header").forEach((header) => {
180
+ header.addEventListener("click", () => {
181
+ header.parentElement.classList.toggle("collapsed");
182
+ });
183
+ });
184
+ }
185
+ /**
186
+ * Show the help index page.
187
+ * @private
188
+ */
189
+ _showIndex() {
190
+ const content = this._modal.querySelector(".help-modal__content");
191
+ const categories = this._service.getCategories();
192
+ const categoryOrder = Object.keys(this._categories);
193
+ categories.sort((a, b) => {
194
+ const orderA = categoryOrder.indexOf(a);
195
+ const orderB = categoryOrder.indexOf(b);
196
+ return (orderA === -1 ? 99 : orderA) - (orderB === -1 ? 99 : orderB);
197
+ });
198
+ content.innerHTML = `
199
+ <div class="help-index">
200
+ <h1>${this._copy.title}</h1>
201
+ <p>${this._copy.welcome}</p>
202
+
203
+ <div class="help-index__grid">
204
+ ${categories.map((category) => {
205
+ const topics = this._service.getTopicsByCategory(category);
206
+ const label = this._categories[category]?.label || category;
207
+ const icon = this._categories[category]?.icon || "folder";
208
+ return `
209
+ <div class="help-index__category" data-category="${category}">
210
+ <div class="help-index__category-header">
211
+ <span class="material-symbols-outlined">${icon}</span>
212
+ <span>${label}</span>
213
+ </div>
214
+ <ul class="help-index__topics">
215
+ ${topics.slice(0, 3).map((topic) => `
216
+ <li class="help-index__topic" data-topic="${topic.id}">
217
+ ${topic.title}
218
+ </li>
219
+ `).join("")}
220
+ ${topics.length > 3 ? `<li class="help-index__more">+${topics.length - 3} more</li>` : ""}
221
+ </ul>
222
+ </div>
223
+ `;
224
+ }).join("")}
225
+ </div>
226
+ </div>
227
+ `;
228
+ content.querySelectorAll(".help-index__topic").forEach((item) => {
229
+ item.addEventListener("click", () => {
230
+ this._loadTopic(item.dataset.topic);
231
+ });
232
+ });
233
+ }
234
+ /**
235
+ * Load and display a topic.
236
+ * @param {string} topicId - Topic ID
237
+ * @private
238
+ */
239
+ async _loadTopic(topicId) {
240
+ const content = this._modal.querySelector(".help-modal__content");
241
+ content.innerHTML = '<div class="help-content__loading">Loading...</div>';
242
+ this._modal.querySelectorAll(".help-topic-item").forEach((item) => {
243
+ item.classList.toggle("active", item.dataset.topic === topicId);
244
+ });
245
+ try {
246
+ const topic = await this._service.loadTopic(topicId);
247
+ this._currentTopic = topicId;
248
+ content.innerHTML = `
249
+ <div class="help-content__article">
250
+ <div class="help-content__breadcrumb">
251
+ <a href="#" class="help-breadcrumb__home" title="Help Index">
252
+ <span class="material-symbols-outlined">home</span>
253
+ </a>
254
+ <span class="help-breadcrumb__separator">/</span>
255
+ <span class="help-breadcrumb__title">${topic.title}</span>
256
+ </div>
257
+ <article class="help-article">
258
+ ${topic.html}
259
+ </article>
260
+ </div>
261
+ `;
262
+ content.querySelector(".help-breadcrumb__home").addEventListener("click", (e) => {
263
+ e.preventDefault();
264
+ this._showIndex();
265
+ });
266
+ } catch (error) {
267
+ content.innerHTML = `
268
+ <div class="help-content__error">
269
+ <span class="material-symbols-outlined">error</span>
270
+ <p>Failed to load help topic.</p>
271
+ </div>
272
+ `;
273
+ }
274
+ }
275
+ /**
276
+ * Handle search input.
277
+ * @param {string} query - Search query
278
+ * @private
279
+ */
280
+ _handleSearch(query) {
281
+ this._searchQuery = query;
282
+ const content = this._modal.querySelector(".help-modal__content");
283
+ if (!query.trim()) {
284
+ this._showIndex();
285
+ return;
286
+ }
287
+ const results = this._service.searchTopics(query);
288
+ if (results.length === 0) {
289
+ content.innerHTML = `
290
+ <div class="help-search-results">
291
+ <h2>Search Results</h2>
292
+ <p class="help-search-empty">No results found for "${query}"</p>
293
+ </div>
294
+ `;
295
+ return;
296
+ }
297
+ content.innerHTML = `
298
+ <div class="help-search-results">
299
+ <h2>Search Results</h2>
300
+ <p class="help-search-count">${results.length} result${results.length !== 1 ? "s" : ""} for "${query}"</p>
301
+ <ul class="help-search-list">
302
+ ${results.map((topic) => `
303
+ <li class="help-search-item" data-topic="${topic.id}">
304
+ <span class="help-search-item__title">${topic.title}</span>
305
+ <span class="help-search-item__category">${this._categories[topic.category]?.label || topic.category}</span>
306
+ </li>
307
+ `).join("")}
308
+ </ul>
309
+ </div>
310
+ `;
311
+ content.querySelectorAll(".help-search-item").forEach((item) => {
312
+ item.addEventListener("click", () => {
313
+ this._loadTopic(item.dataset.topic);
314
+ });
315
+ });
316
+ }
317
+ /**
318
+ * Handle keyboard events.
319
+ * @param {KeyboardEvent} event
320
+ * @private
321
+ */
322
+ _handleKeyDown(event) {
323
+ if (event.key === "Escape") {
324
+ this.close();
325
+ }
326
+ }
327
+ /**
328
+ * Static method to open help modal.
329
+ * @param {string} [topicId] - Initial topic
330
+ * @returns {HelpModal}
331
+ */
332
+ static open(topicId = null) {
333
+ const modal = new _HelpModal();
334
+ modal.open(topicId);
335
+ return modal;
336
+ }
337
+ /**
338
+ * Get the currently active modal.
339
+ * @returns {HelpModal|null}
340
+ */
341
+ static getActive() {
342
+ return _activeModal;
343
+ }
344
+ };
345
+ function setupGlobalHelpShortcut() {
346
+ document.addEventListener("keydown", (event) => {
347
+ if (event.key !== "?") return;
348
+ const t = event.target;
349
+ if (t?.closest?.(
350
+ 'input, textarea, select, [contenteditable="true"]'
351
+ )) return;
352
+ event.preventDefault();
353
+ if (_activeModal) {
354
+ _activeModal.close();
355
+ } else {
356
+ HelpModal.open();
357
+ }
358
+ });
359
+ }
360
+ if (typeof document !== "undefined") {
361
+ setupGlobalHelpShortcut();
362
+ }
363
+
364
+ // src/ui/components/context_menu.js
365
+ var _activeMenu = null;
366
+ function showContextMenu(x, y, items, onAction) {
367
+ hideContextMenu();
368
+ const menu = document.createElement("div");
369
+ menu.className = "twm-context-menu ea-context-menu";
370
+ for (const it of items) {
371
+ if (it.separator) {
372
+ const sep = document.createElement("div");
373
+ sep.className = "twm-context-menu__separator";
374
+ menu.appendChild(sep);
375
+ continue;
376
+ }
377
+ const row = document.createElement("div");
378
+ let cls = "twm-context-menu-item";
379
+ if (it.danger) cls += " twm-delete-node";
380
+ if (it.disabled) cls += " disabled";
381
+ row.className = cls;
382
+ row.innerHTML = `
383
+ <span class="material-symbols-outlined">${it.icon || ""}</span>
384
+ <span>${escapeHtml(it.label)}</span>
385
+ `;
386
+ if (!it.disabled) {
387
+ row.addEventListener("click", (e) => {
388
+ e.stopPropagation();
389
+ hideContextMenu();
390
+ onAction?.(it.action);
391
+ });
392
+ }
393
+ menu.appendChild(row);
394
+ }
395
+ document.body.appendChild(menu);
396
+ menu.style.display = "block";
397
+ _activeMenu = menu;
398
+ const rect = menu.getBoundingClientRect();
399
+ const left = Math.min(x, window.innerWidth - rect.width - 8);
400
+ const top = Math.min(y, window.innerHeight - rect.height - 8);
401
+ menu.style.left = `${Math.max(0, left)}px`;
402
+ menu.style.top = `${Math.max(0, top)}px`;
403
+ setTimeout(() => {
404
+ document.addEventListener("mousedown", _outsideHandler, { once: true, capture: true });
405
+ }, 0);
406
+ document.addEventListener("keydown", _escHandler);
407
+ window.addEventListener("scroll", hideContextMenu, { once: true, capture: true });
408
+ }
409
+ function hideContextMenu() {
410
+ if (!_activeMenu) return;
411
+ _activeMenu.remove();
412
+ _activeMenu = null;
413
+ document.removeEventListener("keydown", _escHandler);
414
+ }
415
+ function _outsideHandler(e) {
416
+ if (_activeMenu && !_activeMenu.contains(e.target)) hideContextMenu();
417
+ }
418
+ function _escHandler(e) {
419
+ if (e.key === "Escape") hideContextMenu();
420
+ }
421
+ function escapeHtml(s) {
422
+ return String(s).replace(/[&<>"']/g, (c) => ({
423
+ "&": "&amp;",
424
+ "<": "&lt;",
425
+ ">": "&gt;",
426
+ '"': "&quot;",
427
+ "'": "&#39;"
428
+ })[c]);
429
+ }
430
+
431
+ // src/ui/components/modal.js
432
+ var _modalSeq = 1;
433
+ function openForm({ title, fields = [], defaults = {}, submitLabel = "OK" } = {}) {
434
+ return new Promise((resolve) => {
435
+ const body = document.createElement("div");
436
+ body.className = "twm-modal__body-host";
437
+ body.innerHTML = `
438
+ <form class="twm-modal__form">
439
+ ${fields.map((f) => _renderEntry(f, defaults[f.name] ?? f.default)).join("")}
440
+ <div class="twm-modal__error" data-role="form-error" hidden></div>
441
+ <div class="twm-modal__actions">
442
+ <button type="button" class="twm-btn" data-action="cancel">Cancel</button>
443
+ <button type="submit" class="twm-btn twm-btn--primary">${submitLabel}</button>
444
+ </div>
445
+ </form>
446
+ `;
447
+ const win = new ManagedWindow({
448
+ id: `twm-modal-${_modalSeq++}`,
449
+ title: title || "Dialog",
450
+ icon: "edit_note",
451
+ content: body,
452
+ modal: true,
453
+ canMinimize: false,
454
+ canMaximize: false,
455
+ canResize: false,
456
+ canDrag: true,
457
+ defaultWidth: _estimateWidth(fields),
458
+ defaultHeight: _estimateHeight(fields),
459
+ onClose: () => {
460
+ if (!_resolved) {
461
+ _resolved = true;
462
+ resolve(null);
463
+ }
464
+ }
465
+ });
466
+ win.show();
467
+ let _resolved = false;
468
+ const close = (result) => {
469
+ if (_resolved) return;
470
+ _resolved = true;
471
+ resolve(result);
472
+ try {
473
+ win.close({ force: true });
474
+ } catch {
475
+ }
476
+ };
477
+ const errEl = body.querySelector('[data-role="form-error"]');
478
+ const showError = (msg) => {
479
+ if (!errEl) return;
480
+ errEl.textContent = msg;
481
+ errEl.hidden = !msg;
482
+ };
483
+ body.querySelector('[data-action="cancel"]')?.addEventListener("click", () => close(null));
484
+ const formFields = fields.filter((f) => !f.section);
485
+ for (const f of formFields) {
486
+ if (f.type === "select" && f.create) _wireComboboxHint(body, f);
487
+ }
488
+ const clearFieldError = (name) => {
489
+ const row = body.querySelector(`[data-field-row="${name}"]`);
490
+ const errEl2 = body.querySelector(`[data-field-error="${name}"]`);
491
+ if (row) row.classList.remove("twm-is-invalid");
492
+ if (errEl2) {
493
+ errEl2.textContent = "";
494
+ errEl2.hidden = true;
495
+ }
496
+ };
497
+ const setFieldError = (name, msg) => {
498
+ const row = body.querySelector(`[data-field-row="${name}"]`);
499
+ const errEl2 = body.querySelector(`[data-field-error="${name}"]`);
500
+ if (row) row.classList.add("twm-is-invalid");
501
+ if (errEl2) {
502
+ errEl2.textContent = msg;
503
+ errEl2.hidden = false;
504
+ }
505
+ };
506
+ for (const f of formFields) {
507
+ const el = body.querySelector(`[name="${f.name}"]`);
508
+ el?.addEventListener("input", () => clearFieldError(f.name));
509
+ el?.addEventListener("change", () => clearFieldError(f.name));
510
+ }
511
+ body.querySelector("form").addEventListener("submit", async (e) => {
512
+ e.preventDefault();
513
+ const submitBtn = body.querySelector('button[type="submit"]');
514
+ showError("");
515
+ const data = {};
516
+ for (const f of formFields) {
517
+ const el = body.querySelector(`[name="${f.name}"]`);
518
+ if (!el) continue;
519
+ let v;
520
+ if (f.type === "checkbox") {
521
+ v = !!el.checked;
522
+ } else if (f.type === "number") {
523
+ v = el.value === "" ? null : Number(el.value);
524
+ } else {
525
+ v = el.value;
526
+ }
527
+ if (f.type === "select" && f.create) {
528
+ const known = new Set(_optionValues(f.options));
529
+ if (v && !known.has(v)) {
530
+ if (submitBtn) submitBtn.disabled = true;
531
+ let created;
532
+ let failMsg = null;
533
+ try {
534
+ created = await f.create.onCreate(v);
535
+ } catch (err) {
536
+ created = null;
537
+ failMsg = err?.message || String(err);
538
+ }
539
+ if (submitBtn) submitBtn.disabled = false;
540
+ if (created == null) {
541
+ const hint = f.create && f.create.hint || f.label.toLowerCase();
542
+ showError(failMsg ? `Couldn't create ${hint} "${v}": ${failMsg}` : `Couldn't create ${hint} "${v}". See the toast for details.`);
543
+ return;
544
+ }
545
+ v = created;
546
+ }
547
+ }
548
+ data[f.name] = v;
549
+ }
550
+ let firstBadName = null;
551
+ for (const f of formFields) {
552
+ clearFieldError(f.name);
553
+ if (typeof f.validator !== "function") continue;
554
+ let msg = null;
555
+ try {
556
+ msg = f.validator(data[f.name], data);
557
+ } catch (err) {
558
+ msg = err?.message || String(err);
559
+ }
560
+ if (msg) {
561
+ setFieldError(f.name, msg);
562
+ if (!firstBadName) firstBadName = f.name;
563
+ }
564
+ }
565
+ if (firstBadName) {
566
+ body.querySelector(`[name="${firstBadName}"]`)?.focus();
567
+ return;
568
+ }
569
+ close(data);
570
+ });
571
+ requestAnimationFrame(() => {
572
+ body.querySelector("input, select, textarea")?.focus();
573
+ });
574
+ });
575
+ }
576
+ function openConfirm({
577
+ title,
578
+ message,
579
+ confirmLabel = "OK",
580
+ cancelLabel = "Cancel",
581
+ danger = false,
582
+ icon = null
583
+ } = {}) {
584
+ return new Promise((resolve) => {
585
+ const body = document.createElement("div");
586
+ body.className = "twm-modal__body-host";
587
+ const cls = danger ? "twm-btn twm-btn--danger" : "twm-btn twm-btn--primary";
588
+ body.innerHTML = `
589
+ <div class="twm-modal__body">${message}</div>
590
+ <div class="twm-modal__actions">
591
+ <button type="button" class="twm-btn" data-action="cancel">${cancelLabel}</button>
592
+ <button type="button" class="${cls}" data-action="confirm">${confirmLabel}</button>
593
+ </div>
594
+ `;
595
+ let _resolved = false;
596
+ const win = new ManagedWindow({
597
+ id: `twm-confirm-${_modalSeq++}`,
598
+ title: title || "Confirm",
599
+ icon: icon || (danger ? "warning" : "help"),
600
+ content: body,
601
+ modal: true,
602
+ canMinimize: false,
603
+ canMaximize: false,
604
+ canResize: false,
605
+ canDrag: true,
606
+ defaultWidth: 380,
607
+ defaultHeight: 180,
608
+ onClose: () => {
609
+ if (!_resolved) {
610
+ _resolved = true;
611
+ resolve(false);
612
+ }
613
+ }
614
+ });
615
+ win.show();
616
+ const close = (v) => {
617
+ if (_resolved) return;
618
+ _resolved = true;
619
+ resolve(v);
620
+ try {
621
+ win.close({ force: true });
622
+ } catch {
623
+ }
624
+ };
625
+ body.querySelector('[data-action="cancel"]').addEventListener("click", () => close(false));
626
+ body.querySelector('[data-action="confirm"]').addEventListener("click", () => close(true));
627
+ requestAnimationFrame(() => {
628
+ body.querySelector(`[data-action="${danger ? "cancel" : "confirm"}"]`)?.focus();
629
+ });
630
+ });
631
+ }
632
+ function openModal({
633
+ title = "Dialog",
634
+ icon = "info",
635
+ content = null,
636
+ actions = null,
637
+ width = 640,
638
+ height = 480,
639
+ onMount = null,
640
+ backdropBlur = void 0,
641
+ backdropOpacity = void 0
642
+ } = {}) {
643
+ return new Promise((resolve) => {
644
+ const body = document.createElement("div");
645
+ body.className = "twm-modal__body-host";
646
+ const bodyInner = document.createElement("div");
647
+ bodyInner.className = "twm-modal__body twm-modal__body--rich";
648
+ if (content instanceof HTMLElement) bodyInner.appendChild(content);
649
+ body.appendChild(bodyInner);
650
+ const acts = Array.isArray(actions) && actions.length > 0 ? actions : [{ label: "Close", value: null }];
651
+ const actionsEl = document.createElement("div");
652
+ actionsEl.className = "twm-modal__actions";
653
+ acts.forEach((a, i) => {
654
+ const btn = document.createElement("button");
655
+ btn.type = "button";
656
+ let cls = "twm-btn";
657
+ if (a.primary) cls += " twm-btn--primary";
658
+ if (a.danger) cls += " twm-btn--danger";
659
+ btn.className = cls;
660
+ btn.textContent = String(a.label || "");
661
+ btn.dataset.actionIdx = String(i);
662
+ actionsEl.appendChild(btn);
663
+ });
664
+ body.appendChild(actionsEl);
665
+ let _resolved = false;
666
+ const win = new ManagedWindow({
667
+ id: `twm-modal-${_modalSeq++}`,
668
+ title,
669
+ icon,
670
+ content: body,
671
+ modal: true,
672
+ backdropBlur,
673
+ backdropOpacity,
674
+ canMinimize: false,
675
+ canMaximize: false,
676
+ canResize: true,
677
+ canDrag: true,
678
+ defaultWidth: width,
679
+ defaultHeight: height,
680
+ onClose: () => {
681
+ if (!_resolved) {
682
+ _resolved = true;
683
+ resolve(null);
684
+ }
685
+ }
686
+ });
687
+ win.show();
688
+ const close = (value) => {
689
+ if (_resolved) return;
690
+ _resolved = true;
691
+ resolve(value);
692
+ try {
693
+ win.close({ force: true });
694
+ } catch {
695
+ }
696
+ };
697
+ actionsEl.addEventListener("click", (e) => {
698
+ const btn = e.target.closest("[data-action-idx]");
699
+ if (!btn) return;
700
+ const idx = Number(btn.dataset.actionIdx);
701
+ const a = acts[idx];
702
+ if (!a) return;
703
+ if (a.keepOpen) {
704
+ try {
705
+ a.onClick?.(close);
706
+ } catch (err) {
707
+ console.warn(err);
708
+ }
709
+ return;
710
+ }
711
+ close(a.value);
712
+ });
713
+ if (typeof onMount === "function") {
714
+ requestAnimationFrame(() => {
715
+ try {
716
+ onMount(bodyInner);
717
+ } catch (err) {
718
+ console.warn(err);
719
+ }
720
+ });
721
+ }
722
+ requestAnimationFrame(() => {
723
+ const idx = acts.findIndex((a) => a.primary);
724
+ const which = idx >= 0 ? idx : acts.length - 1;
725
+ actionsEl.querySelector(`[data-action-idx="${which}"]`)?.focus();
726
+ });
727
+ });
728
+ }
729
+ function _estimateHeight(fields) {
730
+ let body = 0;
731
+ for (const f of fields) {
732
+ if (f.section) body += 32;
733
+ else if (f.type === "textarea") {
734
+ const rows = Number(f.rows) || 4;
735
+ body += 32 + rows * 18;
736
+ if (f.hint) body += 16;
737
+ } else if (f.type === "checkbox") body += 32;
738
+ else {
739
+ body += 46;
740
+ if (f.hint) body += 16;
741
+ }
742
+ }
743
+ const base = 36 + 46 + 28 + body;
744
+ const max = Math.floor(window.innerHeight * 0.8);
745
+ return Math.max(200, Math.min(base, max));
746
+ }
747
+ function _estimateWidth(fields) {
748
+ const wide = fields.some((f) => f && (f.type === "textarea" || f.section));
749
+ return wide ? 640 : 480;
750
+ }
751
+ function _optionValues(options) {
752
+ return (options || []).map((o) => typeof o === "object" ? o.value : o);
753
+ }
754
+ function _wireComboboxHint(host, f) {
755
+ const input = host.querySelector(`input[name="${f.name}"]`);
756
+ const hintEl = host.querySelector(`.twm-combobox__hint[data-for="${f.name}"]`);
757
+ if (!input || !hintEl) return;
758
+ const known = new Set(_optionValues(f.options));
759
+ const label = f.create && f.create.hint || "new entry";
760
+ const update = () => {
761
+ const v = input.value.trim();
762
+ if (v && !known.has(v)) {
763
+ hintEl.textContent = `\u21B3 will create ${label} \u201C${v}\u201D`;
764
+ hintEl.hidden = false;
765
+ } else {
766
+ hintEl.hidden = true;
767
+ }
768
+ };
769
+ input.addEventListener("input", update);
770
+ update();
771
+ }
772
+ function _renderEntry(f, value) {
773
+ if (f && f.section != null) {
774
+ return `<div class="twm-modal__section">${_esc(f.section)}</div>`;
775
+ }
776
+ return _renderField(f, value);
777
+ }
778
+ function _renderField(f, value) {
779
+ const required = f.required ? "required" : "";
780
+ const rowMod = f.type === "textarea" ? " twm-modal__row--multiline" : "";
781
+ const hintHtml = f.hint ? `<div class="twm-modal__hint--field">${_esc(f.hint)}</div>` : "";
782
+ const errHtml = `<div class="twm-modal__field-error" data-field-error="${f.name}" hidden></div>`;
783
+ const wrap = (inner) => `
784
+ <label class="twm-modal__row${rowMod}" data-field-row="${f.name}">
785
+ <span>${_esc(f.label || "")}</span>
786
+ ${inner}
787
+ ${hintHtml}
788
+ ${errHtml}
789
+ </label>
790
+ `;
791
+ if (f.type === "checkbox") {
792
+ const checked = value === true || value === "true" ? "checked" : "";
793
+ return `
794
+ <label class="twm-modal__row" data-field-row="${f.name}">
795
+ <span></span>
796
+ <span class="twm-modal__check">
797
+ <input name="${f.name}" type="checkbox" ${checked}>
798
+ <span>${_esc(f.label || "")}</span>
799
+ </span>
800
+ ${hintHtml}
801
+ ${errHtml}
802
+ </label>
803
+ `;
804
+ }
805
+ if (f.type === "textarea") {
806
+ const v2 = value == null ? "" : String(value);
807
+ const rows = f.rows != null ? `rows="${Number(f.rows) || 4}"` : 'rows="4"';
808
+ const placeholder2 = f.placeholder ? `placeholder="${_esc(f.placeholder)}"` : "";
809
+ return wrap(`<textarea name="${f.name}" ${rows} ${placeholder2} ${required}>${_esc(v2)}</textarea>`);
810
+ }
811
+ const v = value == null ? "" : String(value);
812
+ if (f.type === "select" && f.create) {
813
+ const dlId = `ea-dl-${f.name}-${_modalSeq}`;
814
+ const opts = _optionValues(f.options).map((ov) => `<option value="${_esc(ov)}"></option>`).join("");
815
+ const placeholder2 = f.placeholder ? `placeholder="${_esc(f.placeholder)}"` : 'placeholder="type to pick or create\u2026"';
816
+ return wrap(`
817
+ <div class="twm-combobox">
818
+ <input name="${f.name}" type="text" list="${dlId}"
819
+ value="${_esc(v)}" ${placeholder2} ${required}
820
+ autocomplete="off">
821
+ <datalist id="${dlId}">${opts}</datalist>
822
+ <span class="twm-combobox__hint" data-for="${f.name}" hidden></span>
823
+ </div>
824
+ `);
825
+ }
826
+ if (f.type === "select") {
827
+ const opts = (f.options || []).map((o) => {
828
+ const ov = typeof o === "object" ? o.value : o;
829
+ const ol = typeof o === "object" ? o.label : o;
830
+ return `<option value="${_esc(ov)}" ${ov === v ? "selected" : ""}>${_esc(ol)}</option>`;
831
+ }).join("");
832
+ return wrap(`<select name="${f.name}" ${required}>${opts}</select>`);
833
+ }
834
+ const step = f.step != null ? `step="${f.step}"` : "";
835
+ const placeholder = f.placeholder ? `placeholder="${_esc(f.placeholder)}"` : "";
836
+ const type = f.type === "number" || f.type === "password" ? f.type : "text";
837
+ return wrap(`<input name="${f.name}" type="${type}" value="${_esc(v)}" ${step} ${placeholder} ${required}>`);
838
+ }
839
+ function _esc(s) {
840
+ return String(s == null ? "" : s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
841
+ }
842
+
843
+ export {
844
+ setHelpProvider,
845
+ helpProvider,
846
+ helpCategories,
847
+ helpCopy,
848
+ HelpModal,
849
+ showContextMenu,
850
+ hideContextMenu,
851
+ openForm,
852
+ openConfirm,
853
+ openModal
854
+ };
855
+ //# sourceMappingURL=chunk-DVU44T77.js.map