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.
- package/LICENSE +21 -0
- package/README.md +195 -0
- package/THIRD_PARTY_LICENSES.md +53 -0
- package/css/base.css +3439 -0
- package/css/flexdesk.css +5797 -0
- package/css/overrides.css +2349 -0
- package/css/reset.css +46 -0
- package/css/tokens.css +137 -0
- package/dist/charts.js +780 -0
- package/dist/charts.js.map +7 -0
- package/dist/chunk-3PHCPZHT.js +222 -0
- package/dist/chunk-3PHCPZHT.js.map +7 -0
- package/dist/chunk-CT4YXXLP.js +1770 -0
- package/dist/chunk-CT4YXXLP.js.map +7 -0
- package/dist/chunk-DRYCDMEG.js +2217 -0
- package/dist/chunk-DRYCDMEG.js.map +7 -0
- package/dist/chunk-DVU44T77.js +855 -0
- package/dist/chunk-DVU44T77.js.map +7 -0
- package/dist/chunk-FL5KFNQH.js +640 -0
- package/dist/chunk-FL5KFNQH.js.map +7 -0
- package/dist/chunk-FOOS3T5L.js +117 -0
- package/dist/chunk-FOOS3T5L.js.map +7 -0
- package/dist/chunk-JYWURG5T.js +32 -0
- package/dist/chunk-JYWURG5T.js.map +7 -0
- package/dist/chunk-TLZUUFOE.js +276 -0
- package/dist/chunk-TLZUUFOE.js.map +7 -0
- package/dist/chunk-UCJ2WD4D.js +625 -0
- package/dist/chunk-UCJ2WD4D.js.map +7 -0
- package/dist/chunk-WVFGV5FT.js +205 -0
- package/dist/chunk-WVFGV5FT.js.map +7 -0
- package/dist/core.js +1098 -0
- package/dist/core.js.map +7 -0
- package/dist/editor.js +1661 -0
- package/dist/editor.js.map +7 -0
- package/dist/flexdesk.css +5797 -0
- package/dist/host.js +132 -0
- package/dist/host.js.map +7 -0
- package/dist/reset.css +46 -0
- package/dist/tile_registry-6WZPTUZV.js +28 -0
- package/dist/tile_registry-6WZPTUZV.js.map +7 -0
- package/dist/tiles.js +2068 -0
- package/dist/tiles.js.map +7 -0
- package/dist/tokens.css +137 -0
- package/dist/widgets.js +5438 -0
- package/dist/widgets.js.map +7 -0
- package/dist/wm.js +4830 -0
- package/dist/wm.js.map +7 -0
- package/package.json +80 -0
- package/src/charts/chart_types.js +699 -0
- package/src/charts/data_series_plot_window.js +761 -0
- package/src/charts/downsample.js +119 -0
- package/src/charts/plot_config.js +176 -0
- package/src/charts/plot_popout_window.js +1560 -0
- package/src/charts/plotly_wrapper.js +432 -0
- package/src/core/event_bus.js +230 -0
- package/src/core/logging.js +300 -0
- package/src/core/settings.js +813 -0
- package/src/core/state_guard.js +382 -0
- package/src/core/state_guard_config.js +309 -0
- package/src/core/state_machine.js +258 -0
- package/src/editor/editor_pane.js +264 -0
- package/src/editor/monaco_editor_factory.js +449 -0
- package/src/editor/monaco_loader.js +74 -0
- package/src/editor/notebook_search.js +460 -0
- package/src/editor/notebook_tab_bar.js +456 -0
- package/src/editor/notebook_undo_manager.js +108 -0
- package/src/editor/split_pane_container.js +197 -0
- package/src/help/help_modal.js +409 -0
- package/src/help/help_registry.js +65 -0
- package/src/host/host.js +69 -0
- package/src/host/pywebview_host.js +133 -0
- package/src/tiles/config_schema.js +256 -0
- package/src/tiles/layout_persistence.js +662 -0
- package/src/tiles/tile_base.js +785 -0
- package/src/tiles/tile_grid.js +905 -0
- package/src/tiles/tile_registry.js +136 -0
- package/src/tiling/command_palette.js +328 -0
- package/src/tiling/content_registry.js +110 -0
- package/src/tiling/desktops.js +133 -0
- package/src/tiling/entity_sources.js +179 -0
- package/src/tiling/keymap.js +156 -0
- package/src/tiling/kind_taxonomy.js +172 -0
- package/src/tiling/landing_table.js +717 -0
- package/src/tiling/loading_overlay.js +33 -0
- package/src/tiling/nav_panel.js +142 -0
- package/src/tiling/page_factory.js +215 -0
- package/src/tiling/panel_keys.js +145 -0
- package/src/tiling/shell.js +437 -0
- package/src/tiling/tile_breadcrumb.js +222 -0
- package/src/tiling/tile_renderer.js +459 -0
- package/src/tiling/tile_tab_menu.js +340 -0
- package/src/tiling/tile_tree.js +880 -0
- package/src/tiling/wm.js +1352 -0
- package/src/ui/base/component_base.js +30 -0
- package/src/ui/base/controller_base.js +48 -0
- package/src/ui/base/page_base.js +102 -0
- package/src/ui/components/about_dialog.js +224 -0
- package/src/ui/components/action_dropdown.js +389 -0
- package/src/ui/components/attribute_list_editor.js +652 -0
- package/src/ui/components/autocomplete_field.js +732 -0
- package/src/ui/components/computing_status_window.js +81 -0
- package/src/ui/components/confirm_dialog.js +307 -0
- package/src/ui/components/context_menu.js +87 -0
- package/src/ui/components/data_table.js +2228 -0
- package/src/ui/components/detail_header.js +177 -0
- package/src/ui/components/drag_reorder.js +0 -0
- package/src/ui/components/gallery_picker.js +147 -0
- package/src/ui/components/inline_renamer.js +129 -0
- package/src/ui/components/managed_window.js +804 -0
- package/src/ui/components/modal.js +512 -0
- package/src/ui/components/notification_history.js +264 -0
- package/src/ui/components/slide_out_panel.js +185 -0
- package/src/ui/components/slider_field.js +122 -0
- package/src/ui/components/sortable_list.js +309 -0
- package/src/ui/components/table_state_store.js +81 -0
- package/src/ui/components/toast.js +59 -0
- package/src/ui/components/tree_view.js +460 -0
- package/src/ui/components/window_taskbar.js +143 -0
- package/src/ui/controllers/panel_state_machine.js +634 -0
- package/src/ui/controllers/window_chrome_controller.js +659 -0
- package/src/ui/notification_center.js +576 -0
- package/src/ui/utils/overlay_scrollbar.js +492 -0
- package/src/ui/utils/raf_resize_observer.js +31 -0
- package/src/ui/utils/tooltip_service.js +406 -0
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Overlay scrollbar utilities for the js_new UI stack.
|
|
3
|
+
* Ports the unified hover-only scrollbar system from the legacy UI manager.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createRafResizeObserver } from './raf_resize_observer.js';
|
|
7
|
+
|
|
8
|
+
function normalizeOrientation(value) {
|
|
9
|
+
return String(value || 'horizontal').toLowerCase() === 'vertical' ? 'vertical' : 'horizontal';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function ensureInstallState(container) {
|
|
13
|
+
if (!container.__overlayScrollbarInstalled) {
|
|
14
|
+
Object.defineProperty(container, '__overlayScrollbarInstalled', {
|
|
15
|
+
value: {},
|
|
16
|
+
writable: true,
|
|
17
|
+
configurable: true,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function installOverlayScrollbar(container, options = {}) {
|
|
23
|
+
if (!container) return null;
|
|
24
|
+
|
|
25
|
+
const orientation = normalizeOrientation(options.orientation);
|
|
26
|
+
const className = options.className || 'overlay-scrollbar';
|
|
27
|
+
const isHorizontal = orientation !== 'vertical';
|
|
28
|
+
const orientationKey = isHorizontal ? 'horizontal' : 'vertical';
|
|
29
|
+
|
|
30
|
+
ensureInstallState(container);
|
|
31
|
+
if (container.__overlayScrollbarInstalled[orientationKey]) {
|
|
32
|
+
return container.__overlayScrollbarInstalled[orientationKey];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (options.setOverflow !== false) {
|
|
36
|
+
container.style.overflow = options.overflow || 'auto';
|
|
37
|
+
}
|
|
38
|
+
if (options.setPosition !== false && getComputedStyle(container).position === 'static') {
|
|
39
|
+
container.style.position = 'relative';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// When externalBar is true, place the bar on the parent element instead of
|
|
43
|
+
// inside the scroll container. This avoids overflow clipping issues in certain
|
|
44
|
+
// flex+overflow layouts (e.g. bottom-tabs-header .tabs).
|
|
45
|
+
const externalBar = !!options.externalBar;
|
|
46
|
+
const barHost = externalBar ? container.parentElement : container;
|
|
47
|
+
if (externalBar && barHost && getComputedStyle(barHost).position === 'static') {
|
|
48
|
+
barHost.style.position = 'relative';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const bar = document.createElement('div');
|
|
52
|
+
bar.className = className;
|
|
53
|
+
bar.classList.add(`overlay-scrollbar-${isHorizontal ? 'h' : 'v'}`);
|
|
54
|
+
const track = document.createElement('div');
|
|
55
|
+
track.className = 'track';
|
|
56
|
+
const thumb = document.createElement('div');
|
|
57
|
+
thumb.className = 'thumb';
|
|
58
|
+
bar.appendChild(track);
|
|
59
|
+
bar.appendChild(thumb);
|
|
60
|
+
(barHost || container).appendChild(bar);
|
|
61
|
+
|
|
62
|
+
const getScrollProps = () => ({
|
|
63
|
+
scrollSize: isHorizontal ? container.scrollWidth : container.scrollHeight,
|
|
64
|
+
clientSize: isHorizontal ? container.clientWidth : container.clientHeight,
|
|
65
|
+
scrollPos: isHorizontal ? (container.scrollLeft || 0) : (container.scrollTop || 0),
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Dragging and hover state - must be declared early so event handlers can access it
|
|
69
|
+
let dragging = false;
|
|
70
|
+
let hovered = false;
|
|
71
|
+
let startPos = 0;
|
|
72
|
+
let thumbStartPos = 0;
|
|
73
|
+
|
|
74
|
+
let previousHasOverflow = null;
|
|
75
|
+
let previousClientSize = null;
|
|
76
|
+
let isResettingScroll = false;
|
|
77
|
+
|
|
78
|
+
const update = () => {
|
|
79
|
+
if (isResettingScroll) return;
|
|
80
|
+
|
|
81
|
+
const { scrollSize, clientSize, scrollPos } = getScrollProps();
|
|
82
|
+
const scrollLeft = container.scrollLeft || 0;
|
|
83
|
+
const scrollTop = container.scrollTop || 0;
|
|
84
|
+
const maxScroll = scrollSize - clientSize;
|
|
85
|
+
const hasOverflow = maxScroll > 2;
|
|
86
|
+
|
|
87
|
+
if (previousClientSize !== null && clientSize > previousClientSize && hasOverflow && scrollPos > 0) {
|
|
88
|
+
const delta = clientSize - previousClientSize;
|
|
89
|
+
const targetScroll = Math.max(0, scrollPos - delta);
|
|
90
|
+
if (targetScroll !== scrollPos) {
|
|
91
|
+
isResettingScroll = true;
|
|
92
|
+
if (isHorizontal) {
|
|
93
|
+
container.scrollLeft = targetScroll;
|
|
94
|
+
} else {
|
|
95
|
+
container.scrollTop = targetScroll;
|
|
96
|
+
}
|
|
97
|
+
requestAnimationFrame(() => {
|
|
98
|
+
isResettingScroll = false;
|
|
99
|
+
update();
|
|
100
|
+
});
|
|
101
|
+
previousClientSize = clientSize;
|
|
102
|
+
previousHasOverflow = hasOverflow;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
previousClientSize = clientSize;
|
|
108
|
+
|
|
109
|
+
if (previousHasOverflow === true && !hasOverflow && scrollPos > 0) {
|
|
110
|
+
isResettingScroll = true;
|
|
111
|
+
if (isHorizontal) {
|
|
112
|
+
container.scrollLeft = 0;
|
|
113
|
+
} else {
|
|
114
|
+
container.scrollTop = 0;
|
|
115
|
+
}
|
|
116
|
+
requestAnimationFrame(() => {
|
|
117
|
+
isResettingScroll = false;
|
|
118
|
+
update();
|
|
119
|
+
});
|
|
120
|
+
previousHasOverflow = hasOverflow;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const clampedMaxScroll = Math.max(0, maxScroll);
|
|
125
|
+
if (hasOverflow && scrollPos > clampedMaxScroll) {
|
|
126
|
+
isResettingScroll = true;
|
|
127
|
+
if (isHorizontal) {
|
|
128
|
+
container.scrollLeft = clampedMaxScroll;
|
|
129
|
+
} else {
|
|
130
|
+
container.scrollTop = clampedMaxScroll;
|
|
131
|
+
}
|
|
132
|
+
requestAnimationFrame(() => {
|
|
133
|
+
isResettingScroll = false;
|
|
134
|
+
update();
|
|
135
|
+
});
|
|
136
|
+
previousHasOverflow = hasOverflow;
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
previousHasOverflow = hasOverflow;
|
|
140
|
+
|
|
141
|
+
container.dataset.scrollable = hasOverflow ? '1' : '0';
|
|
142
|
+
|
|
143
|
+
// Position the bar at the scroll viewport edge.
|
|
144
|
+
if (externalBar) {
|
|
145
|
+
// Bar lives on the parent element — position using container's offset.
|
|
146
|
+
// No scroll-offset compensation needed since bar is outside scroll context.
|
|
147
|
+
if (isHorizontal) {
|
|
148
|
+
bar.style.left = `${container.offsetLeft}px`;
|
|
149
|
+
bar.style.bottom = 'auto';
|
|
150
|
+
bar.style.top = `${container.offsetTop + container.offsetHeight - 6}px`;
|
|
151
|
+
bar.style.width = `${container.clientWidth}px`;
|
|
152
|
+
} else {
|
|
153
|
+
bar.style.top = `${container.offsetTop}px`;
|
|
154
|
+
bar.style.right = 'auto';
|
|
155
|
+
bar.style.left = `${container.offsetLeft + container.offsetWidth - 6}px`;
|
|
156
|
+
bar.style.height = `${container.clientHeight}px`;
|
|
157
|
+
}
|
|
158
|
+
} else {
|
|
159
|
+
// Bar lives inside the container — compensate for scroll offset
|
|
160
|
+
// on BOTH axes so the bar stays pinned to the viewport edge.
|
|
161
|
+
if (isHorizontal) {
|
|
162
|
+
bar.style.left = `${scrollLeft}px`;
|
|
163
|
+
bar.style.top = `${scrollTop + container.clientHeight - bar.offsetHeight}px`;
|
|
164
|
+
bar.style.bottom = 'auto';
|
|
165
|
+
bar.style.width = `${clientSize}px`;
|
|
166
|
+
} else {
|
|
167
|
+
bar.style.top = `${scrollTop}px`;
|
|
168
|
+
bar.style.left = `${scrollLeft + container.clientWidth - bar.offsetWidth}px`;
|
|
169
|
+
bar.style.right = 'auto';
|
|
170
|
+
bar.style.height = `${clientSize}px`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!hasOverflow) {
|
|
175
|
+
bar.style.opacity = '0';
|
|
176
|
+
bar.style.pointerEvents = 'none';
|
|
177
|
+
thumb.style[isHorizontal ? 'width' : 'height'] = '0px';
|
|
178
|
+
thumb.style[isHorizontal ? 'left' : 'top'] = '0px';
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Don't hide while dragging or hovering
|
|
183
|
+
if (!dragging && !hovered) {
|
|
184
|
+
bar.style.opacity = '0'; // hidden until hover
|
|
185
|
+
bar.style.pointerEvents = 'none';
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const ratio = clientSize / scrollSize;
|
|
189
|
+
const thumbSize = Math.max(30, Math.floor(ratio * clientSize));
|
|
190
|
+
const maxThumbPos = clientSize - thumbSize;
|
|
191
|
+
const scrollRatio = maxScroll > 0 ? scrollPos / maxScroll : 0;
|
|
192
|
+
const thumbPos = Math.round(maxThumbPos * scrollRatio);
|
|
193
|
+
|
|
194
|
+
if (isHorizontal) {
|
|
195
|
+
thumb.style.width = `${thumbSize}px`;
|
|
196
|
+
thumb.style.left = `${thumbPos}px`;
|
|
197
|
+
} else {
|
|
198
|
+
thumb.style.height = `${thumbSize}px`;
|
|
199
|
+
thumb.style.top = `${thumbPos}px`;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const onScroll = () => update();
|
|
204
|
+
container.addEventListener('scroll', onScroll, { passive: true });
|
|
205
|
+
|
|
206
|
+
// Manage hover visibility entirely in JS to avoid CSS specificity issues.
|
|
207
|
+
// Show bar on mouseenter if scrollable, hide on mouseleave.
|
|
208
|
+
const onMouseEnter = () => {
|
|
209
|
+
hovered = true;
|
|
210
|
+
update();
|
|
211
|
+
if (container.dataset.scrollable === '1') {
|
|
212
|
+
bar.style.opacity = '1';
|
|
213
|
+
bar.style.pointerEvents = '';
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
const onMouseLeave = () => {
|
|
217
|
+
hovered = false;
|
|
218
|
+
// Don't hide while dragging
|
|
219
|
+
if (dragging) return;
|
|
220
|
+
bar.style.opacity = '0';
|
|
221
|
+
bar.style.pointerEvents = 'none';
|
|
222
|
+
};
|
|
223
|
+
container.addEventListener('mouseenter', onMouseEnter, { passive: true });
|
|
224
|
+
container.addEventListener('mouseleave', onMouseLeave, { passive: true });
|
|
225
|
+
// When bar is external (sibling of container), also listen for hover on the bar
|
|
226
|
+
// itself so moving the mouse to the thumb doesn't trigger a hide.
|
|
227
|
+
if (externalBar) {
|
|
228
|
+
bar.addEventListener('mouseenter', onMouseEnter, { passive: true });
|
|
229
|
+
bar.addEventListener('mouseleave', onMouseLeave, { passive: true });
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Also handle focus-within for keyboard accessibility
|
|
233
|
+
container.addEventListener('focusin', onMouseEnter, { passive: true });
|
|
234
|
+
container.addEventListener('focusout', onMouseLeave, { passive: true });
|
|
235
|
+
|
|
236
|
+
const resizeObserver = createRafResizeObserver(update);
|
|
237
|
+
try { resizeObserver.observe(container); } catch (_) {}
|
|
238
|
+
|
|
239
|
+
const mo = new MutationObserver(update);
|
|
240
|
+
try { mo.observe(container, { childList: true, subtree: options.watchSubtree !== false }); } catch (_) {}
|
|
241
|
+
|
|
242
|
+
let resizeTimeout;
|
|
243
|
+
const onWindowResize = () => {
|
|
244
|
+
update();
|
|
245
|
+
clearTimeout(resizeTimeout);
|
|
246
|
+
resizeTimeout = setTimeout(update, 150);
|
|
247
|
+
};
|
|
248
|
+
window.addEventListener('resize', onWindowResize, { passive: true });
|
|
249
|
+
|
|
250
|
+
const onMouseDown = (e) => {
|
|
251
|
+
dragging = true;
|
|
252
|
+
startPos = isHorizontal ? e.clientX : e.clientY;
|
|
253
|
+
thumbStartPos = parseFloat(thumb.style[isHorizontal ? 'left' : 'top']) || 0;
|
|
254
|
+
// Keep bar visible while dragging
|
|
255
|
+
bar.style.opacity = '1';
|
|
256
|
+
bar.style.pointerEvents = '';
|
|
257
|
+
document.addEventListener('mousemove', onMouseMove);
|
|
258
|
+
document.addEventListener('mouseup', onMouseUp, { once: true });
|
|
259
|
+
e.preventDefault();
|
|
260
|
+
e.stopPropagation();
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const onMouseMove = (e) => {
|
|
264
|
+
if (!dragging) return;
|
|
265
|
+
e.preventDefault();
|
|
266
|
+
|
|
267
|
+
const { scrollSize, clientSize } = getScrollProps();
|
|
268
|
+
const maxScroll = scrollSize - clientSize;
|
|
269
|
+
if (maxScroll <= 0) return;
|
|
270
|
+
|
|
271
|
+
const thumbSize = parseFloat(thumb.style[isHorizontal ? 'width' : 'height']) || 30;
|
|
272
|
+
const maxThumbPos = clientSize - thumbSize;
|
|
273
|
+
|
|
274
|
+
const delta = (isHorizontal ? e.clientX : e.clientY) - startPos;
|
|
275
|
+
let newThumbPos = thumbStartPos + delta;
|
|
276
|
+
newThumbPos = Math.max(0, Math.min(maxThumbPos, newThumbPos));
|
|
277
|
+
|
|
278
|
+
const thumbRatio = maxThumbPos > 0 ? newThumbPos / maxThumbPos : 0;
|
|
279
|
+
if (isHorizontal) {
|
|
280
|
+
container.scrollLeft = thumbRatio * maxScroll;
|
|
281
|
+
} else {
|
|
282
|
+
container.scrollTop = thumbRatio * maxScroll;
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const onMouseUp = () => {
|
|
287
|
+
dragging = false;
|
|
288
|
+
document.removeEventListener('mousemove', onMouseMove);
|
|
289
|
+
// Check if mouse is still over container - if not, hide the bar
|
|
290
|
+
const rect = container.getBoundingClientRect();
|
|
291
|
+
const isMouseOver = document.elementsFromPoint(
|
|
292
|
+
(rect.left + rect.right) / 2,
|
|
293
|
+
(rect.top + rect.bottom) / 2
|
|
294
|
+
).includes(container);
|
|
295
|
+
if (!isMouseOver) {
|
|
296
|
+
bar.style.opacity = '0';
|
|
297
|
+
bar.style.pointerEvents = 'none';
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
thumb.addEventListener('mousedown', onMouseDown);
|
|
302
|
+
|
|
303
|
+
// Wheel-to-horizontal: convert vertical wheel into horizontal scroll,
|
|
304
|
+
// but only when the container has no vertical overflow (otherwise let
|
|
305
|
+
// the browser handle normal vertical scrolling).
|
|
306
|
+
let onWheel = null;
|
|
307
|
+
if (isHorizontal) {
|
|
308
|
+
onWheel = (e) => {
|
|
309
|
+
const { scrollSize, clientSize } = getScrollProps();
|
|
310
|
+
if (scrollSize <= clientSize) return;
|
|
311
|
+
if (Math.abs(e.deltaX) > Math.abs(e.deltaY)) return;
|
|
312
|
+
// If the container also scrolls vertically, don't hijack the wheel
|
|
313
|
+
if (container.scrollHeight > container.clientHeight) return;
|
|
314
|
+
e.preventDefault();
|
|
315
|
+
container.scrollLeft += e.deltaY;
|
|
316
|
+
};
|
|
317
|
+
container.addEventListener('wheel', onWheel, { passive: false });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
requestAnimationFrame(() => {
|
|
321
|
+
requestAnimationFrame(update);
|
|
322
|
+
});
|
|
323
|
+
setTimeout(update, 50);
|
|
324
|
+
|
|
325
|
+
const cleanup = () => {
|
|
326
|
+
container.removeEventListener('scroll', onScroll);
|
|
327
|
+
container.removeEventListener('mouseenter', onMouseEnter);
|
|
328
|
+
container.removeEventListener('mouseleave', onMouseLeave);
|
|
329
|
+
container.removeEventListener('focusin', onMouseEnter);
|
|
330
|
+
container.removeEventListener('focusout', onMouseLeave);
|
|
331
|
+
if (externalBar) {
|
|
332
|
+
bar.removeEventListener('mouseenter', onMouseEnter);
|
|
333
|
+
bar.removeEventListener('mouseleave', onMouseLeave);
|
|
334
|
+
}
|
|
335
|
+
if (onWheel) container.removeEventListener('wheel', onWheel);
|
|
336
|
+
window.removeEventListener('resize', onWindowResize);
|
|
337
|
+
try { resizeObserver.disconnect(); } catch (_) {}
|
|
338
|
+
try { mo.disconnect(); } catch (_) {}
|
|
339
|
+
bar.remove();
|
|
340
|
+
};
|
|
341
|
+
|
|
342
|
+
const handle = { update, cleanup, bar, thumb };
|
|
343
|
+
container.__overlayScrollbarInstalled[orientationKey] = handle;
|
|
344
|
+
return handle;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
export function installWorkspaceScrollbars(workspace, options = {}) {
|
|
348
|
+
if (!workspace) return null;
|
|
349
|
+
if (workspace.__workspaceOverlayScrollbarsInstalled) return workspace.__workspaceOverlayScrollbarsUpdate;
|
|
350
|
+
|
|
351
|
+
installOverlayScrollbar(workspace, {
|
|
352
|
+
orientation: 'vertical',
|
|
353
|
+
className: options.verticalClass || 'panel-scrollbar',
|
|
354
|
+
watchSubtree: options.watchSubtree ?? true,
|
|
355
|
+
});
|
|
356
|
+
installOverlayScrollbar(workspace, {
|
|
357
|
+
orientation: 'horizontal',
|
|
358
|
+
className: options.horizontalClass || 'tabs-scrollbar',
|
|
359
|
+
watchSubtree: options.watchSubtree ?? true,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const update = () => {
|
|
363
|
+
const maxX = Math.max(0, workspace.scrollWidth - workspace.clientWidth);
|
|
364
|
+
const maxY = Math.max(0, workspace.scrollHeight - workspace.clientHeight);
|
|
365
|
+
workspace.dataset.scrollableX = maxX > 1 ? '1' : '0';
|
|
366
|
+
workspace.dataset.scrollableY = maxY > 1 ? '1' : '0';
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
workspace.__workspaceOverlayScrollbarsInstalled = true;
|
|
370
|
+
workspace.__workspaceOverlayScrollbarsUpdate = update;
|
|
371
|
+
workspace.addEventListener('scroll', update, { passive: true });
|
|
372
|
+
const ro = createRafResizeObserver(update);
|
|
373
|
+
try { ro.observe(workspace); } catch (_) {}
|
|
374
|
+
const mo = new MutationObserver(update);
|
|
375
|
+
try { mo.observe(workspace, { childList: true, subtree: true }); } catch (_) {}
|
|
376
|
+
requestAnimationFrame(update);
|
|
377
|
+
return update;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Auto-discover and install vertical overlay scrollbars on `.collapsible-content.visible`
|
|
382
|
+
* elements inside `#fixed-200`. Uses a MutationObserver to handle dynamically created
|
|
383
|
+
* content (pages mount lazily) and class toggles (expand/collapse).
|
|
384
|
+
*
|
|
385
|
+
* Only adds the visual overlay bar — does NOT change overflow. Elements that are already
|
|
386
|
+
* scrollable via CSS (e.g. accordion `.collapsible-content.visible { overflow-y: auto }`)
|
|
387
|
+
* get the bar; non-scrollable elements get a no-op bar that stays hidden.
|
|
388
|
+
*
|
|
389
|
+
* @returns {Function} disconnect — stops watching
|
|
390
|
+
*/
|
|
391
|
+
export function installCollapsibleScrollbars() {
|
|
392
|
+
const installOnElement = (el) => {
|
|
393
|
+
installOverlayScrollbar(el, {
|
|
394
|
+
orientation: 'vertical',
|
|
395
|
+
setOverflow: false,
|
|
396
|
+
watchSubtree: true,
|
|
397
|
+
});
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const root = document.getElementById('twm-fixed-200');
|
|
401
|
+
if (root) {
|
|
402
|
+
root.querySelectorAll('.twm-collapsible-content.visible').forEach(installOnElement);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Watch for new elements AND class changes (visible toggled by toggleCategory)
|
|
406
|
+
const observer = new MutationObserver((mutations) => {
|
|
407
|
+
for (const mutation of mutations) {
|
|
408
|
+
// Class change — check if .collapsible-content just gained .visible
|
|
409
|
+
if (mutation.type === 'attributes' && mutation.attributeName === 'class') {
|
|
410
|
+
const el = mutation.target;
|
|
411
|
+
if (el.classList.contains('twm-collapsible-content') && el.classList.contains('visible')) {
|
|
412
|
+
installOnElement(el);
|
|
413
|
+
}
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
// New subtrees added (lazy page mount)
|
|
417
|
+
for (const node of mutation.addedNodes) {
|
|
418
|
+
if (node.nodeType !== 1) continue;
|
|
419
|
+
if (node.classList?.contains('twm-collapsible-content') && node.classList?.contains('visible')) {
|
|
420
|
+
installOnElement(node);
|
|
421
|
+
}
|
|
422
|
+
node.querySelectorAll?.('.twm-collapsible-content.visible')?.forEach(installOnElement);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
const target = root || document.body;
|
|
428
|
+
observer.observe(target, {
|
|
429
|
+
childList: true,
|
|
430
|
+
subtree: true,
|
|
431
|
+
attributes: true,
|
|
432
|
+
attributeFilter: ['class'],
|
|
433
|
+
});
|
|
434
|
+
return () => observer.disconnect();
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Auto-discover and install horizontal overlay scrollbars on tab strips.
|
|
439
|
+
*
|
|
440
|
+
* Uses a MutationObserver so dynamically created tab strips are picked up
|
|
441
|
+
* without per-page manual setup.
|
|
442
|
+
*
|
|
443
|
+
* The selector list used to be hardcoded as `.tabs, .scenario-detail-tabs`, so
|
|
444
|
+
* a generic scrollbar utility carried the name of one EcoAgent page's tab bar.
|
|
445
|
+
* The caller names its own strips now.
|
|
446
|
+
*
|
|
447
|
+
* @param {string[]} [selectors=['.tabs']] Which elements count as a tab strip.
|
|
448
|
+
* @returns {Function} disconnect — stops watching for new tabs
|
|
449
|
+
*/
|
|
450
|
+
export function installTabsScrollbars(selectors = ['.tabs']) {
|
|
451
|
+
const selector = selectors.join(', ');
|
|
452
|
+
const classes = selectors
|
|
453
|
+
.filter((s) => s.startsWith('.'))
|
|
454
|
+
.map((s) => s.slice(1));
|
|
455
|
+
const installOnElement = (el) => {
|
|
456
|
+
installOverlayScrollbar(el, {
|
|
457
|
+
orientation: 'horizontal',
|
|
458
|
+
className: 'tabs-scrollbar',
|
|
459
|
+
setOverflow: false,
|
|
460
|
+
externalBar: true,
|
|
461
|
+
watchSubtree: true,
|
|
462
|
+
});
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
const cleanupElement = (el) => {
|
|
466
|
+
el.__overlayScrollbarInstalled?.horizontal?.cleanup?.();
|
|
467
|
+
delete el.__overlayScrollbarInstalled;
|
|
468
|
+
};
|
|
469
|
+
|
|
470
|
+
const matches = (el) => classes.some((c) => el.classList?.contains(c));
|
|
471
|
+
|
|
472
|
+
document.querySelectorAll(selector).forEach(installOnElement);
|
|
473
|
+
|
|
474
|
+
// Watch for dynamically added/removed elements
|
|
475
|
+
const observer = new MutationObserver((mutations) => {
|
|
476
|
+
for (const mutation of mutations) {
|
|
477
|
+
for (const node of mutation.addedNodes) {
|
|
478
|
+
if (node.nodeType !== 1) continue;
|
|
479
|
+
if (matches(node)) installOnElement(node);
|
|
480
|
+
node.querySelectorAll?.(selector)?.forEach(installOnElement);
|
|
481
|
+
}
|
|
482
|
+
for (const node of mutation.removedNodes) {
|
|
483
|
+
if (node.nodeType !== 1) continue;
|
|
484
|
+
if (matches(node)) cleanupElement(node);
|
|
485
|
+
node.querySelectorAll?.(selector)?.forEach(cleanupElement);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
491
|
+
return () => observer.disconnect();
|
|
492
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* rAF-coalesced ResizeObserver.
|
|
3
|
+
*
|
|
4
|
+
* Running layout-mutating work — `Plotly.Plots.resize()`, Monaco's
|
|
5
|
+
* `editor.layout()`, a canvas resize, or a re-render — *synchronously* inside a
|
|
6
|
+
* ResizeObserver callback makes the engine emit the (spec-defined, benign but
|
|
7
|
+
* console-error-level) "ResizeObserver loop completed with undelivered
|
|
8
|
+
* notifications" warning: the callback mutates layout, which produces more
|
|
9
|
+
* resize notifications it can't deliver in the same dispatch.
|
|
10
|
+
*
|
|
11
|
+
* Deferring the work to the next animation frame is the root-cause fix — the
|
|
12
|
+
* engine delivers all pending notifications first, so there's nothing left
|
|
13
|
+
* "undelivered" — and coalescing collapses a burst of resize events into a
|
|
14
|
+
* single render.
|
|
15
|
+
*
|
|
16
|
+
* @param {(entries: ResizeObserverEntry[]) => void} callback
|
|
17
|
+
* @returns {ResizeObserver}
|
|
18
|
+
*/
|
|
19
|
+
export function createRafResizeObserver(callback) {
|
|
20
|
+
let scheduled = false;
|
|
21
|
+
let latestEntries = [];
|
|
22
|
+
return new ResizeObserver((entries) => {
|
|
23
|
+
latestEntries = entries;
|
|
24
|
+
if (scheduled) return;
|
|
25
|
+
scheduled = true;
|
|
26
|
+
requestAnimationFrame(() => {
|
|
27
|
+
scheduled = false;
|
|
28
|
+
callback(latestEntries);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
}
|