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,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State Machine Base (js_new)
|
|
3
|
+
*
|
|
4
|
+
* Purpose
|
|
5
|
+
* -------
|
|
6
|
+
* Shared finite-state-machine utility that workspace, simulation, and other controllers extend.
|
|
7
|
+
*
|
|
8
|
+
* Responsibilities
|
|
9
|
+
* - Provide declarative state/transition definitions with guard + action hooks.
|
|
10
|
+
* - Emit lifecycle events via the injected EventBus for observability.
|
|
11
|
+
* - Supply debugging helpers (transition tracing) without referencing window globals.
|
|
12
|
+
*
|
|
13
|
+
* Source Material
|
|
14
|
+
* - html/js/state_machine.js (base implementation semantics).
|
|
15
|
+
* - html/js/workspace_state_machine.js (practical usage patterns to support).
|
|
16
|
+
*
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const BUS_EVENTS = Object.freeze({
|
|
20
|
+
ATTEMPT: 'fsm:transition:attempt',
|
|
21
|
+
BLOCKED: 'fsm:transition:blocked',
|
|
22
|
+
COMPLETE: 'fsm:transition:complete',
|
|
23
|
+
ERROR: 'fsm:transition:error',
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const isPromiseLike = (value) => Boolean(value) && typeof value.then === 'function';
|
|
27
|
+
|
|
28
|
+
export class StateMachine {
|
|
29
|
+
constructor({
|
|
30
|
+
name = 'state-machine',
|
|
31
|
+
initialState,
|
|
32
|
+
states = {},
|
|
33
|
+
eventBus = null,
|
|
34
|
+
logger = null,
|
|
35
|
+
trace = false,
|
|
36
|
+
} = {}) {
|
|
37
|
+
if (!initialState || typeof initialState !== 'string') {
|
|
38
|
+
throw new Error('StateMachine requires a non-empty initialState name');
|
|
39
|
+
}
|
|
40
|
+
if (!states[initialState]) {
|
|
41
|
+
throw new Error(`StateMachine initial state "${initialState}" is not defined in states map`);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
this.name = name;
|
|
45
|
+
this.states = this._normalizeStates(states);
|
|
46
|
+
this.initialState = initialState;
|
|
47
|
+
this.currentState = null;
|
|
48
|
+
this.previousState = null;
|
|
49
|
+
this.eventBus = eventBus;
|
|
50
|
+
this.logger = logger;
|
|
51
|
+
this.trace = trace;
|
|
52
|
+
this._isTransitioning = false;
|
|
53
|
+
this._ready = Promise.resolve().then(() => this._enterState(initialState, {
|
|
54
|
+
type: 'init',
|
|
55
|
+
payload: undefined,
|
|
56
|
+
fromState: null,
|
|
57
|
+
}));
|
|
58
|
+
|
|
59
|
+
this._ready.catch((err) => {
|
|
60
|
+
this.logger?.error?.('state-machine', 'Failed to enter initial state', {
|
|
61
|
+
machine: this.name,
|
|
62
|
+
error: err,
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
describeStates() {
|
|
68
|
+
return Object.keys(this.states);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
getState() {
|
|
72
|
+
return this.currentState;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
canTransition(eventName) {
|
|
76
|
+
const current = this.states[this.currentState];
|
|
77
|
+
return Boolean(current?.transitions?.[eventName]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async transition(eventName, payload = undefined) {
|
|
81
|
+
await this._ready;
|
|
82
|
+
|
|
83
|
+
if (this._isTransitioning) {
|
|
84
|
+
throw new Error(`StateMachine "${this.name}" is already processing a transition`);
|
|
85
|
+
}
|
|
86
|
+
const currentDef = this.states[this.currentState];
|
|
87
|
+
const transitionDef = currentDef?.transitions?.[eventName];
|
|
88
|
+
|
|
89
|
+
if (!transitionDef) {
|
|
90
|
+
this._emitBus(BUS_EVENTS.BLOCKED, { reason: 'missing-transition', event: eventName });
|
|
91
|
+
this.logger?.warn?.('state-machine', `No transition for event "${eventName}" in state "${this.currentState}"`, {
|
|
92
|
+
machine: this.name,
|
|
93
|
+
state: this.currentState,
|
|
94
|
+
event: eventName,
|
|
95
|
+
});
|
|
96
|
+
return { transitioned: false, reason: 'missing-transition' };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const targetState = transitionDef.target;
|
|
100
|
+
if (!targetState || !this.states[targetState]) {
|
|
101
|
+
throw new Error(`Transition from "${this.currentState}" via "${eventName}" references undefined target state "${targetState}"`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const context = {
|
|
105
|
+
machine: this,
|
|
106
|
+
event: eventName,
|
|
107
|
+
fromState: this.currentState,
|
|
108
|
+
toState: targetState,
|
|
109
|
+
payload,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const guardAllowed = await this._evaluateGuard(transitionDef.guard, context);
|
|
113
|
+
if (!guardAllowed) {
|
|
114
|
+
this._emitBus(BUS_EVENTS.BLOCKED, {
|
|
115
|
+
reason: 'guard-blocked',
|
|
116
|
+
event: eventName,
|
|
117
|
+
fromState: this.currentState,
|
|
118
|
+
toState: targetState,
|
|
119
|
+
});
|
|
120
|
+
return { transitioned: false, reason: 'guard-blocked' };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
this._emitBus(BUS_EVENTS.ATTEMPT, context);
|
|
124
|
+
this._isTransitioning = true;
|
|
125
|
+
|
|
126
|
+
try {
|
|
127
|
+
await this._runHook(this.states[this.currentState]?.onExit, context, 'onExit');
|
|
128
|
+
await this._runHook(transitionDef.action, context, 'transition-action');
|
|
129
|
+
await this._enterState(targetState, { type: eventName, payload, fromState: context.fromState });
|
|
130
|
+
this._emitBus(BUS_EVENTS.COMPLETE, context);
|
|
131
|
+
return { transitioned: true };
|
|
132
|
+
} catch (err) {
|
|
133
|
+
this._emitBus(BUS_EVENTS.ERROR, { ...context, error: err });
|
|
134
|
+
this.logger?.error?.('state-machine', 'Transition failed', {
|
|
135
|
+
machine: this.name,
|
|
136
|
+
event: eventName,
|
|
137
|
+
fromState: context.fromState,
|
|
138
|
+
toState: context.toState,
|
|
139
|
+
error: err,
|
|
140
|
+
});
|
|
141
|
+
throw err;
|
|
142
|
+
} finally {
|
|
143
|
+
this._isTransitioning = false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async reset(stateName = this.initialState) {
|
|
148
|
+
await this._ready;
|
|
149
|
+
if (!this.states[stateName]) {
|
|
150
|
+
throw new Error(`Cannot reset state machine to undefined state "${stateName}"`);
|
|
151
|
+
}
|
|
152
|
+
this.previousState = this.currentState;
|
|
153
|
+
await this._enterState(stateName, {
|
|
154
|
+
type: 'reset',
|
|
155
|
+
payload: undefined,
|
|
156
|
+
fromState: this.previousState,
|
|
157
|
+
});
|
|
158
|
+
this._emitBus(BUS_EVENTS.COMPLETE, {
|
|
159
|
+
machine: this,
|
|
160
|
+
event: 'reset',
|
|
161
|
+
fromState: this.previousState,
|
|
162
|
+
toState: stateName,
|
|
163
|
+
payload: undefined,
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
updateStates(partialStates = {}) {
|
|
168
|
+
this.states = this._normalizeStates({ ...this.states, ...partialStates });
|
|
169
|
+
if (!this.states[this.currentState]) {
|
|
170
|
+
throw new Error('Current state was removed during updateStates');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
_normalizeStates(definitions) {
|
|
175
|
+
const normalized = {};
|
|
176
|
+
for (const [stateName, config] of Object.entries(definitions)) {
|
|
177
|
+
if (!config || typeof config !== 'object') {
|
|
178
|
+
throw new Error(`Invalid state definition for "${stateName}"`);
|
|
179
|
+
}
|
|
180
|
+
normalized[stateName] = {
|
|
181
|
+
onEnter: config.onEnter ?? null,
|
|
182
|
+
onExit: config.onExit ?? null,
|
|
183
|
+
transitions: { ...(config.transitions ?? {}) },
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return normalized;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async _enterState(stateName, meta) {
|
|
190
|
+
const prev = this.currentState;
|
|
191
|
+
this.previousState = prev;
|
|
192
|
+
this.currentState = stateName;
|
|
193
|
+
|
|
194
|
+
const context = {
|
|
195
|
+
machine: this,
|
|
196
|
+
event: meta.type,
|
|
197
|
+
fromState: meta.fromState,
|
|
198
|
+
toState: stateName,
|
|
199
|
+
payload: meta.payload,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
await this._runHook(this.states[stateName]?.onEnter, context, 'onEnter');
|
|
203
|
+
if (this.trace) {
|
|
204
|
+
this.logger?.debug?.('state-machine', `State machine "${this.name}" entered state "${stateName}"`, {
|
|
205
|
+
machine: this.name,
|
|
206
|
+
fromState: prev,
|
|
207
|
+
toState: stateName,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
async _evaluateGuard(guardFn, context) {
|
|
213
|
+
if (!guardFn) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
const result = guardFn(context);
|
|
217
|
+
if (isPromiseLike(result)) {
|
|
218
|
+
return result;
|
|
219
|
+
}
|
|
220
|
+
return result !== false;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async _runHook(hook, context, hookName) {
|
|
224
|
+
if (typeof hook !== 'function') {
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const result = hook(context);
|
|
228
|
+
if (isPromiseLike(result)) {
|
|
229
|
+
await result;
|
|
230
|
+
}
|
|
231
|
+
if (this.trace) {
|
|
232
|
+
this.logger?.debug?.('state-machine', `${hookName} executed`, {
|
|
233
|
+
machine: this.name,
|
|
234
|
+
hook: hookName,
|
|
235
|
+
state: context.toState,
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
_emitBus(eventName, detail) {
|
|
241
|
+
if (!this.eventBus || !eventName) {
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
this.eventBus.emit(eventName, {
|
|
246
|
+
machine: this.name,
|
|
247
|
+
state: this.currentState,
|
|
248
|
+
detail,
|
|
249
|
+
});
|
|
250
|
+
} catch (err) {
|
|
251
|
+
this.logger?.warn?.('state-machine', 'Failed to emit state machine event', {
|
|
252
|
+
machine: this.name,
|
|
253
|
+
eventName,
|
|
254
|
+
error: err,
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EditorPane — one half (or whole) of the split editor.
|
|
3
|
+
*
|
|
4
|
+
* Each pane owns:
|
|
5
|
+
* - A NotebookTabBar instance
|
|
6
|
+
* - An editor container where the active file's editor is mounted
|
|
7
|
+
* - A list of file paths assigned to this pane
|
|
8
|
+
* - The active file path within this pane
|
|
9
|
+
*
|
|
10
|
+
* The pane does NOT mount editors itself — it delegates to the parent
|
|
11
|
+
* (NotebookPage) via callbacks, since editor mounting requires access to
|
|
12
|
+
* ProjectModel, symbol index, plot controller, etc.
|
|
13
|
+
*
|
|
14
|
+
* Layout:
|
|
15
|
+
* ┌──────────────────────────────┐
|
|
16
|
+
* │ [Tab1] [Tab2] │ ← NotebookTabBar
|
|
17
|
+
* ├──────────────────────────────┤
|
|
18
|
+
* │ │
|
|
19
|
+
* │ Editor content │ ← editorContainer
|
|
20
|
+
* │ │
|
|
21
|
+
* └──────────────────────────────┘
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { NotebookTabBar } from './notebook_tab_bar.js';
|
|
25
|
+
|
|
26
|
+
export class EditorPane {
|
|
27
|
+
/** @type {string} */
|
|
28
|
+
#id;
|
|
29
|
+
|
|
30
|
+
/** @type {HTMLElement} root element */
|
|
31
|
+
#el = null;
|
|
32
|
+
|
|
33
|
+
/** @type {NotebookTabBar} */
|
|
34
|
+
#tabBar = null;
|
|
35
|
+
|
|
36
|
+
/** @type {HTMLElement} */
|
|
37
|
+
#editorContainer = null;
|
|
38
|
+
|
|
39
|
+
/** @type {string[]} file paths in tab order */
|
|
40
|
+
#filePaths = [];
|
|
41
|
+
|
|
42
|
+
/** @type {string|null} */
|
|
43
|
+
#activeFilePath = null;
|
|
44
|
+
|
|
45
|
+
/** @type {string|null} file path of the preview (transient) tab, if any */
|
|
46
|
+
#previewFilePath = null;
|
|
47
|
+
|
|
48
|
+
/** @type {object} callbacks from parent */
|
|
49
|
+
#callbacks = {};
|
|
50
|
+
|
|
51
|
+
/** @type {boolean} whether this pane is the focused/active pane */
|
|
52
|
+
#focused = false;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @param {string} id — unique pane identifier ('left' or 'right')
|
|
56
|
+
* @param {object} callbacks
|
|
57
|
+
* @param {Function} callbacks.onActivate — (paneId, filePath) => void
|
|
58
|
+
* @param {Function} callbacks.onClose — (paneId, filePath) => void
|
|
59
|
+
* @param {Function} callbacks.onCloseOthers — (paneId, filePath) => void
|
|
60
|
+
* @param {Function} callbacks.onCloseToRight — (paneId, filePath) => void
|
|
61
|
+
* @param {Function} callbacks.onCloseAll — (paneId) => void
|
|
62
|
+
* @param {Function} callbacks.onCloseSaved — (paneId) => void
|
|
63
|
+
* @param {Function} callbacks.onRename — (oldPath, newPath) => void
|
|
64
|
+
* @param {Function} callbacks.onDuplicate — (filePath) => void
|
|
65
|
+
* @param {Function} callbacks.onReorder — (paneId, orderedPaths) => void
|
|
66
|
+
* @param {Function} callbacks.onRevealInExplorer — (filePath) => void
|
|
67
|
+
* @param {Function} callbacks.onFocus — (paneId) => void
|
|
68
|
+
* @param {Function} callbacks.onSplitRight — (paneId, filePath) => void
|
|
69
|
+
* @param {Function} callbacks.onMoveToOtherPane — (paneId, filePath) => void
|
|
70
|
+
*/
|
|
71
|
+
constructor(id, callbacks = {}) {
|
|
72
|
+
this.#id = id;
|
|
73
|
+
this.#callbacks = callbacks;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get id() { return this.#id; }
|
|
77
|
+
get activeFilePath() { return this.#activeFilePath; }
|
|
78
|
+
get filePaths() { return [...this.#filePaths]; }
|
|
79
|
+
get editorContainer() { return this.#editorContainer; }
|
|
80
|
+
get element() { return this.#el; }
|
|
81
|
+
get isEmpty() { return this.#filePaths.length === 0; }
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Build DOM and mount the tab bar.
|
|
85
|
+
* @returns {HTMLElement} the root element (to be placed in SplitPaneContainer)
|
|
86
|
+
*/
|
|
87
|
+
mount() {
|
|
88
|
+
this.#el = document.createElement('div');
|
|
89
|
+
this.#el.className = 'editor-pane';
|
|
90
|
+
this.#el.dataset.paneId = this.#id;
|
|
91
|
+
|
|
92
|
+
// Focus tracking — clicking anywhere in the pane focuses it
|
|
93
|
+
this.#el.addEventListener('mousedown', () => {
|
|
94
|
+
this.#callbacks.onFocus?.(this.#id);
|
|
95
|
+
}, true);
|
|
96
|
+
|
|
97
|
+
// Tab bar
|
|
98
|
+
const tabBarEl = document.createElement('div');
|
|
99
|
+
this.#el.appendChild(tabBarEl);
|
|
100
|
+
|
|
101
|
+
this.#tabBar = new NotebookTabBar();
|
|
102
|
+
this.#tabBar.mount(tabBarEl, {
|
|
103
|
+
onActivate: (path) => this.#callbacks.onActivate?.(this.#id, path),
|
|
104
|
+
onClose: (path) => this.#callbacks.onClose?.(this.#id, path),
|
|
105
|
+
onCloseOthers: (path) => this.#callbacks.onCloseOthers?.(this.#id, path),
|
|
106
|
+
onCloseToRight: (path) => this.#callbacks.onCloseToRight?.(this.#id, path),
|
|
107
|
+
onCloseAll: () => this.#callbacks.onCloseAll?.(this.#id),
|
|
108
|
+
onCloseSaved: () => this.#callbacks.onCloseSaved?.(this.#id),
|
|
109
|
+
onRename: (o, n) => this.#callbacks.onRename?.(o, n),
|
|
110
|
+
onDuplicate: (path) => this.#callbacks.onDuplicate?.(path),
|
|
111
|
+
onReorder: (paths) => this.#callbacks.onReorder?.(this.#id, paths),
|
|
112
|
+
onRevealInExplorer:(path) => this.#callbacks.onRevealInExplorer?.(path),
|
|
113
|
+
onSplitRight: (path) => this.#callbacks.onSplitRight?.(this.#id, path),
|
|
114
|
+
onMoveToOtherPane: (path) => this.#callbacks.onMoveToOtherPane?.(this.#id, path),
|
|
115
|
+
onDropFromOtherPane: (path) => this.#callbacks.onDropFromOtherPane?.(this.#id, path),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Editor body wrapper — positioned container that fills remaining
|
|
119
|
+
// flex space. The actual editor container is absolutely positioned
|
|
120
|
+
// inside, so its height is ALWAYS the wrapper's height, regardless
|
|
121
|
+
// of content. This makes the scroll chain bulletproof — no flex
|
|
122
|
+
// ancestor chain needed for the scrollbar to work.
|
|
123
|
+
const body = document.createElement('div');
|
|
124
|
+
body.className = 'editor-pane__body';
|
|
125
|
+
this.#el.appendChild(body);
|
|
126
|
+
|
|
127
|
+
// Editor container (absolutely positioned inside wrapper)
|
|
128
|
+
this.#editorContainer = document.createElement('div');
|
|
129
|
+
this.#editorContainer.className = 'notebook-editor-container';
|
|
130
|
+
body.appendChild(this.#editorContainer);
|
|
131
|
+
|
|
132
|
+
// Accept cross-pane tab drops on the editor area too
|
|
133
|
+
this.#editorContainer.addEventListener('dragover', (e) => {
|
|
134
|
+
if (this.#isExternalTabDrag(e)) {
|
|
135
|
+
e.preventDefault();
|
|
136
|
+
try { e.dataTransfer.dropEffect = 'move'; } catch {}
|
|
137
|
+
this.#editorContainer.classList.add('notebook-editor-container--drop-target');
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
this.#editorContainer.addEventListener('drop', (e) => {
|
|
141
|
+
if (this.#isExternalTabDrag(e)) {
|
|
142
|
+
e.preventDefault();
|
|
143
|
+
this.#editorContainer.classList.remove('notebook-editor-container--drop-target');
|
|
144
|
+
const filePath = e.dataTransfer.getData('application/x-ecosim-tab')
|
|
145
|
+
|| e.dataTransfer.getData('text/plain');
|
|
146
|
+
if (filePath) {
|
|
147
|
+
this.#callbacks.onDropFromOtherPane?.(this.#id, filePath);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
this.#editorContainer.addEventListener('dragleave', (e) => {
|
|
152
|
+
if (!this.#editorContainer.contains(e.relatedTarget)) {
|
|
153
|
+
this.#editorContainer.classList.remove('notebook-editor-container--drop-target');
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return this.#el;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Update the file list and active tab.
|
|
162
|
+
* @param {Array<{filePath: string, fileType: string, isDirty: boolean}>} tabs
|
|
163
|
+
* @param {string|null} activeFilePath
|
|
164
|
+
*/
|
|
165
|
+
syncTabBar(tabs, activeFilePath) {
|
|
166
|
+
this.#tabBar?.update(tabs, activeFilePath);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Set the files assigned to this pane.
|
|
171
|
+
* @param {string[]} filePaths
|
|
172
|
+
*/
|
|
173
|
+
setFilePaths(filePaths) {
|
|
174
|
+
this.#filePaths = [...filePaths];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Add a file to this pane's tabs (at end).
|
|
179
|
+
* @param {string} filePath
|
|
180
|
+
*/
|
|
181
|
+
addFile(filePath) {
|
|
182
|
+
if (!this.#filePaths.includes(filePath)) {
|
|
183
|
+
this.#filePaths.push(filePath);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Remove a file from this pane's tabs.
|
|
189
|
+
* @param {string} filePath
|
|
190
|
+
*/
|
|
191
|
+
removeFile(filePath) {
|
|
192
|
+
this.#filePaths = this.#filePaths.filter(p => p !== filePath);
|
|
193
|
+
if (this.#previewFilePath === filePath) this.#previewFilePath = null;
|
|
194
|
+
if (this.#activeFilePath === filePath) {
|
|
195
|
+
this.#activeFilePath = this.#filePaths.length > 0
|
|
196
|
+
? this.#filePaths[this.#filePaths.length - 1]
|
|
197
|
+
: null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
hasFile(filePath) {
|
|
202
|
+
return this.#filePaths.includes(filePath);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
setActiveFile(filePath) {
|
|
206
|
+
this.#activeFilePath = filePath;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
setFocused(focused) {
|
|
210
|
+
this.#focused = focused;
|
|
211
|
+
this.#el?.classList.toggle('editor-pane--focused', focused);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
markDirty(filePath, isDirty) {
|
|
215
|
+
this.#tabBar?.markDirty(filePath, isDirty);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
reorderFiles(orderedPaths) {
|
|
219
|
+
// Keep only paths that belong to this pane
|
|
220
|
+
const valid = orderedPaths.filter(p => this.#filePaths.includes(p));
|
|
221
|
+
// Append any missing (shouldn't happen)
|
|
222
|
+
for (const p of this.#filePaths) {
|
|
223
|
+
if (!valid.includes(p)) valid.push(p);
|
|
224
|
+
}
|
|
225
|
+
this.#filePaths = valid;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
renameFile(oldPath, newPath) {
|
|
229
|
+
const idx = this.#filePaths.indexOf(oldPath);
|
|
230
|
+
if (idx !== -1) this.#filePaths[idx] = newPath;
|
|
231
|
+
if (this.#activeFilePath === oldPath) this.#activeFilePath = newPath;
|
|
232
|
+
if (this.#previewFilePath === oldPath) this.#previewFilePath = newPath;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ─── Preview (transient) tab ──────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
/** @returns {string|null} the preview file path, or null if no preview tab */
|
|
238
|
+
get previewFilePath() { return this.#previewFilePath; }
|
|
239
|
+
|
|
240
|
+
/** Mark a file as the preview tab. Only one preview tab per pane. */
|
|
241
|
+
setPreview(filePath) { this.#previewFilePath = filePath; }
|
|
242
|
+
|
|
243
|
+
/** Whether a file is the preview (transient) tab. */
|
|
244
|
+
isPreview(filePath) { return this.#previewFilePath === filePath; }
|
|
245
|
+
|
|
246
|
+
/** Pin the preview tab (promote to permanent). */
|
|
247
|
+
pinPreview() { this.#previewFilePath = null; }
|
|
248
|
+
|
|
249
|
+
/** Check if a drag event is carrying a tab from another pane. */
|
|
250
|
+
#isExternalTabDrag(e) {
|
|
251
|
+
try { return e.dataTransfer.types.includes('application/x-ecosim-tab'); } catch { return false; }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
dispose() {
|
|
255
|
+
this.#tabBar?.dispose();
|
|
256
|
+
this.#tabBar = null;
|
|
257
|
+
this.#el?.remove();
|
|
258
|
+
this.#el = null;
|
|
259
|
+
this.#editorContainer = null;
|
|
260
|
+
this.#filePaths = [];
|
|
261
|
+
this.#activeFilePath = null;
|
|
262
|
+
this.#previewFilePath = null;
|
|
263
|
+
}
|
|
264
|
+
}
|