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,389 @@
1
+ /**
2
+ * action_dropdown.js
3
+ *
4
+ * Reusable dropdown component for action buttons (Add Scenario, Add Widget, etc).
5
+ * Handles opening, closing, auto-positioning, keyboard navigation, and outside clicks.
6
+ *
7
+ * Auto-positioning: measures available viewport space around the trigger element
8
+ * and picks the best direction (above/below) and alignment (left/right).
9
+ * Adds scrolling when the menu would exceed available vertical space.
10
+ */
11
+
12
+ /** Viewport margin in px — dropdown stays this far from edges. */
13
+ const EDGE_MARGIN = 8;
14
+ /** Gap between trigger and dropdown in px. */
15
+ const GAP = 8;
16
+
17
+ /**
18
+ * ActionDropdown - A reusable dropdown component with smart auto-positioning.
19
+ *
20
+ * Usage:
21
+ * ```javascript
22
+ * const dropdown = new ActionDropdown({
23
+ * trigger: buttonElement,
24
+ * options: [
25
+ * { type: 'option1', label: 'Option 1', icon: 'icon_name', description: 'Description' },
26
+ * { type: 'option2', label: 'Option 2', icon: 'icon_name', description: 'Description' }
27
+ * ],
28
+ * onSelect: (option) => console.log('Selected:', option.type),
29
+ * });
30
+ *
31
+ * // Control programmatically
32
+ * dropdown.open();
33
+ * dropdown.close();
34
+ * dropdown.toggle();
35
+ * dropdown.destroy();
36
+ * ```
37
+ */
38
+ export class ActionDropdown {
39
+ /**
40
+ * @param {Object} config
41
+ * @param {HTMLElement} config.trigger - Button element that triggers the dropdown
42
+ * @param {Array} config.options - Array of option objects with { type, label, icon, description? }
43
+ * @param {Function} config.onSelect - Callback when an option is selected
44
+ * @param {string} [config.className=''] - Additional CSS class for the dropdown
45
+ * @param {string} [config.menuId] - Optional ID for the menu element
46
+ */
47
+ constructor(config) {
48
+ this.trigger = config.trigger;
49
+ this.options = config.options || [];
50
+ this.onSelect = config.onSelect;
51
+ this.className = config.className || '';
52
+ this.menuId = config.menuId;
53
+
54
+ this.isOpen = false;
55
+ this.menuEl = null;
56
+
57
+ this._boundHandleDocumentClick = this._handleDocumentClick.bind(this);
58
+ this._boundHandleKeydown = this._handleKeydown.bind(this);
59
+ this._boundHandleTriggerClick = this._handleTriggerClick.bind(this);
60
+
61
+ this._init();
62
+ }
63
+
64
+ /**
65
+ * Initialize the dropdown.
66
+ * @private
67
+ */
68
+ _init() {
69
+ if (!this.trigger) {
70
+ console.warn('[ActionDropdown] No trigger element provided');
71
+ return;
72
+ }
73
+
74
+ this._createMenu();
75
+ this.trigger.addEventListener('click', this._boundHandleTriggerClick);
76
+ }
77
+
78
+ /**
79
+ * Create the dropdown menu element.
80
+ * @private
81
+ */
82
+ _createMenu() {
83
+ const menu = document.createElement('div');
84
+ menu.className = `twm-action-dropdown-menu ${this.className}`.trim();
85
+ menu.setAttribute('role', 'menu');
86
+ menu.hidden = true;
87
+
88
+ if (this.menuId) {
89
+ menu.id = this.menuId;
90
+ }
91
+
92
+ this._renderOptions(menu);
93
+
94
+ // Append to body so position:fixed works without clipping.
95
+ document.body.appendChild(menu);
96
+
97
+ this.menuEl = menu;
98
+ }
99
+
100
+ /**
101
+ * Render option buttons into a container element.
102
+ * @param {HTMLElement} container
103
+ * @private
104
+ */
105
+ _renderOptions(container) {
106
+ container.innerHTML = '';
107
+ this.options.forEach((opt, index) => {
108
+ const optionBtn = document.createElement('button');
109
+ optionBtn.type = 'button';
110
+ optionBtn.className = 'twm-action-dropdown-option';
111
+ optionBtn.dataset.index = index;
112
+ optionBtn.dataset.type = opt.type;
113
+ optionBtn.setAttribute('role', 'menuitem');
114
+
115
+ const iconHtml = opt.icon
116
+ ? `<span class="material-symbols-outlined twm-option-icon">${opt.icon}</span>`
117
+ : '';
118
+
119
+ const descHtml = opt.description
120
+ ? `<span class="twm-option-desc">${opt.description}</span>`
121
+ : '';
122
+
123
+ optionBtn.innerHTML = `
124
+ ${iconHtml}
125
+ <span class="option-text">
126
+ <span class="twm-option-label">${opt.label}</span>
127
+ ${descHtml}
128
+ </span>
129
+ `;
130
+
131
+ optionBtn.addEventListener('click', (e) => {
132
+ e.preventDefault();
133
+ e.stopPropagation();
134
+ this._selectOption(opt);
135
+ });
136
+
137
+ container.appendChild(optionBtn);
138
+ });
139
+ }
140
+
141
+ /**
142
+ * Handle trigger button click.
143
+ * @param {MouseEvent} e
144
+ * @private
145
+ */
146
+ _handleTriggerClick(e) {
147
+ e.preventDefault();
148
+ e.stopPropagation();
149
+ this.toggle();
150
+ }
151
+
152
+ /**
153
+ * Toggle the dropdown open/closed.
154
+ * @param {boolean} [forceState] - Optional forced state
155
+ */
156
+ toggle(forceState) {
157
+ const shouldOpen = typeof forceState === 'boolean' ? forceState : !this.isOpen;
158
+ if (shouldOpen) {
159
+ this.open();
160
+ } else {
161
+ this.close();
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Open the dropdown with auto-positioning.
167
+ */
168
+ open() {
169
+ if (this.isOpen || !this.menuEl) return;
170
+
171
+ this.isOpen = true;
172
+ this.menuEl.hidden = false;
173
+
174
+ // Position against trigger
175
+ ActionDropdown.position(this.trigger, this.menuEl);
176
+
177
+ // Animate in
178
+ requestAnimationFrame(() => {
179
+ this.menuEl.classList.add('visible');
180
+ });
181
+
182
+ // Update trigger state
183
+ this.trigger?.classList.add('twm-is-open');
184
+
185
+ // Add document listeners
186
+ document.addEventListener('click', this._boundHandleDocumentClick, true);
187
+ document.addEventListener('keydown', this._boundHandleKeydown);
188
+
189
+ // Focus first option for keyboard accessibility
190
+ const firstOption = this.menuEl.querySelector('.twm-action-dropdown-option');
191
+ firstOption?.focus();
192
+ }
193
+
194
+ /**
195
+ * Close the dropdown.
196
+ */
197
+ close() {
198
+ if (!this.isOpen) return;
199
+
200
+ this.isOpen = false;
201
+
202
+ if (this.menuEl) {
203
+ this.menuEl.classList.remove('visible');
204
+ this.menuEl.hidden = true;
205
+ }
206
+
207
+ // Update trigger state
208
+ this.trigger?.classList.remove('twm-is-open');
209
+
210
+ // Remove document listeners
211
+ document.removeEventListener('click', this._boundHandleDocumentClick, true);
212
+ document.removeEventListener('keydown', this._boundHandleKeydown);
213
+ }
214
+
215
+ /**
216
+ * Handle document click (close on outside click).
217
+ * @param {MouseEvent} e
218
+ * @private
219
+ */
220
+ _handleDocumentClick(e) {
221
+ if (!this.isOpen) return;
222
+
223
+ // Don't close if clicking inside dropdown or trigger
224
+ if (this.menuEl?.contains(e.target) || this.trigger?.contains(e.target)) {
225
+ return;
226
+ }
227
+
228
+ this.close();
229
+ }
230
+
231
+ /**
232
+ * Handle keyboard events.
233
+ * @param {KeyboardEvent} e
234
+ * @private
235
+ */
236
+ _handleKeydown(e) {
237
+ if (e.key === 'Escape') {
238
+ this.close();
239
+ this.trigger?.focus();
240
+ return;
241
+ }
242
+
243
+ // Arrow key navigation
244
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
245
+ e.preventDefault();
246
+ this._navigateOptions(e.key === 'ArrowDown' ? 1 : -1);
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Navigate options with arrow keys.
252
+ * @param {number} direction - 1 for down, -1 for up
253
+ * @private
254
+ */
255
+ _navigateOptions(direction) {
256
+ const options = this.menuEl?.querySelectorAll('.twm-action-dropdown-option');
257
+ if (!options || options.length === 0) return;
258
+
259
+ const currentIndex = Array.from(options).findIndex(opt => opt === document.activeElement);
260
+ let nextIndex = currentIndex + direction;
261
+
262
+ if (nextIndex < 0) nextIndex = options.length - 1;
263
+ if (nextIndex >= options.length) nextIndex = 0;
264
+
265
+ options[nextIndex]?.focus();
266
+ }
267
+
268
+ /**
269
+ * Handle option selection.
270
+ * @param {Object} option - The selected option
271
+ * @private
272
+ */
273
+ _selectOption(option) {
274
+ this.close();
275
+
276
+ if (this.onSelect) {
277
+ this.onSelect(option);
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Update the options dynamically.
283
+ * @param {Array} newOptions - New options array
284
+ */
285
+ setOptions(newOptions) {
286
+ this.options = newOptions;
287
+ if (this.menuEl) {
288
+ this._renderOptions(this.menuEl);
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Destroy the dropdown and clean up.
294
+ */
295
+ destroy() {
296
+ this.close();
297
+
298
+ // Remove trigger listener
299
+ this.trigger?.removeEventListener('click', this._boundHandleTriggerClick);
300
+
301
+ // Remove menu element
302
+ this.menuEl?.remove();
303
+ this.menuEl = null;
304
+
305
+ this.trigger = null;
306
+ this.options = [];
307
+ this.onSelect = null;
308
+ }
309
+
310
+ // =========================================================================
311
+ // STATIC — shared auto-positioning for any trigger + menu pair
312
+ // =========================================================================
313
+
314
+ /**
315
+ * Position a dropdown menu relative to a trigger element.
316
+ * Uses `position: fixed` to avoid overflow clipping.
317
+ * Picks above/below based on available space, aligns left/right edge,
318
+ * and applies `max-height` + `overflow-y: auto` when the menu is taller
319
+ * than the available space.
320
+ *
321
+ * @param {HTMLElement} trigger - The element the dropdown opens from
322
+ * @param {HTMLElement} menu - The dropdown menu element to position
323
+ */
324
+ static position(trigger, menu) {
325
+ if (!trigger || !menu) return;
326
+
327
+ // Reset any prior inline position so natural size can be measured.
328
+ Object.assign(menu.style, {
329
+ position: 'fixed',
330
+ top: 'auto',
331
+ bottom: 'auto',
332
+ left: 'auto',
333
+ right: 'auto',
334
+ maxHeight: '',
335
+ overflowY: ''
336
+ });
337
+
338
+ const triggerRect = trigger.getBoundingClientRect();
339
+ const menuRect = menu.getBoundingClientRect();
340
+ const vw = window.innerWidth;
341
+ const vh = window.innerHeight;
342
+
343
+ // ── Vertical: prefer below, flip above if insufficient room ───────
344
+ const spaceBelow = vh - triggerRect.bottom - GAP - EDGE_MARGIN;
345
+ const spaceAbove = triggerRect.top - GAP - EDGE_MARGIN;
346
+
347
+ let top;
348
+ let maxHeight;
349
+
350
+ if (menuRect.height <= spaceBelow) {
351
+ // Fits below — use natural height.
352
+ top = triggerRect.bottom + GAP;
353
+ maxHeight = spaceBelow;
354
+ } else if (menuRect.height <= spaceAbove) {
355
+ // Fits above — use natural height.
356
+ top = triggerRect.top - GAP - menuRect.height;
357
+ maxHeight = spaceAbove;
358
+ } else if (spaceBelow >= spaceAbove) {
359
+ // More space below — scroll.
360
+ top = triggerRect.bottom + GAP;
361
+ maxHeight = spaceBelow;
362
+ } else {
363
+ // More space above — scroll.
364
+ maxHeight = spaceAbove;
365
+ top = EDGE_MARGIN;
366
+ }
367
+
368
+ // ── Horizontal: align left-edge with trigger, flip if off-screen ──
369
+ let left = triggerRect.left;
370
+
371
+ if (left + menuRect.width > vw - EDGE_MARGIN) {
372
+ // Align right edges instead.
373
+ left = triggerRect.right - menuRect.width;
374
+ }
375
+
376
+ // Still off-screen? Clamp to edges.
377
+ left = Math.max(EDGE_MARGIN, Math.min(left, vw - menuRect.width - EDGE_MARGIN));
378
+
379
+ Object.assign(menu.style, {
380
+ position: 'fixed',
381
+ top: `${Math.round(top)}px`,
382
+ left: `${Math.round(left)}px`,
383
+ maxHeight: `${Math.round(maxHeight)}px`,
384
+ overflowY: 'auto'
385
+ });
386
+ }
387
+ }
388
+
389
+ export default ActionDropdown;