flexdesk 0.2.0 → 0.4.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 (58) hide show
  1. package/README.md +26 -0
  2. package/css/base.css +2243 -872
  3. package/css/flexdesk.css +1375 -14
  4. package/css/overrides.css +44 -0
  5. package/css/tokens.css +45 -0
  6. package/dist/charts.js +5 -3
  7. package/dist/charts.js.map +1 -1
  8. package/dist/{chunk-DVU44T77.js → chunk-ELXVW542.js} +196 -75
  9. package/dist/chunk-ELXVW542.js.map +7 -0
  10. package/dist/chunk-LH5TSOZW.js +1237 -0
  11. package/dist/chunk-LH5TSOZW.js.map +7 -0
  12. package/dist/{chunk-TLZUUFOE.js → chunk-O5OHMWBB.js} +10 -2
  13. package/dist/chunk-O5OHMWBB.js.map +7 -0
  14. package/dist/{chunk-CT4YXXLP.js → chunk-QIU5S2RU.js} +371 -73
  15. package/dist/chunk-QIU5S2RU.js.map +7 -0
  16. package/dist/chunk-QNQHQ24V.js +408 -0
  17. package/dist/chunk-QNQHQ24V.js.map +7 -0
  18. package/dist/{chunk-DRYCDMEG.js → chunk-XKDTIT4Q.js} +168 -12
  19. package/dist/chunk-XKDTIT4Q.js.map +7 -0
  20. package/dist/editor.js +3 -380
  21. package/dist/editor.js.map +3 -3
  22. package/dist/flexdesk.css +1375 -14
  23. package/dist/tiles.js +168 -41
  24. package/dist/tiles.js.map +2 -2
  25. package/dist/tokens.css +45 -0
  26. package/dist/widgets.js +44 -14
  27. package/dist/widgets.js.map +2 -2
  28. package/dist/wm.js +3140 -157
  29. package/dist/wm.js.map +4 -4
  30. package/package.json +3 -2
  31. package/src/charts/chart_types.js +167 -0
  32. package/src/charts/plotly_wrapper.js +178 -10
  33. package/src/editor/notebook_tab_bar.js +39 -3
  34. package/src/tiles/tile_base.js +143 -35
  35. package/src/tiles/tile_grid.js +52 -1
  36. package/src/tiling/command_palette.js +71 -18
  37. package/src/tiling/desktops.js +36 -12
  38. package/src/tiling/keymap.js +24 -4
  39. package/src/tiling/shell.js +156 -25
  40. package/src/tiling/tab_strip.js +184 -0
  41. package/src/tiling/tile_breadcrumb.js +34 -2
  42. package/src/tiling/tile_renderer.js +1386 -21
  43. package/src/tiling/tile_tab_menu.js +101 -0
  44. package/src/tiling/tile_tree.js +115 -11
  45. package/src/tiling/wm.js +2375 -84
  46. package/src/tiling/zoom.js +248 -0
  47. package/src/ui/components/action_dropdown.js +34 -3
  48. package/src/ui/components/autocomplete_field.js +65 -13
  49. package/src/ui/components/context_menu.js +79 -8
  50. package/src/ui/components/data_table.js +508 -84
  51. package/src/ui/components/managed_window.js +928 -36
  52. package/src/ui/components/modal.js +214 -8
  53. package/dist/chunk-CT4YXXLP.js.map +0 -7
  54. package/dist/chunk-DRYCDMEG.js.map +0 -7
  55. package/dist/chunk-DVU44T77.js.map +0 -7
  56. package/dist/chunk-TLZUUFOE.js.map +0 -7
  57. package/dist/chunk-UCJ2WD4D.js +0 -625
  58. package/dist/chunk-UCJ2WD4D.js.map +0 -7
package/dist/editor.js CHANGED
@@ -1,386 +1,9 @@
1
1
  import {
2
- DragReorder
3
- } from "./chunk-WVFGV5FT.js";
2
+ NotebookTabBar
3
+ } from "./chunk-QNQHQ24V.js";
4
+ import "./chunk-WVFGV5FT.js";
4
5
  import "./chunk-JYWURG5T.js";
5
6
 
6
- // src/editor/notebook_tab_bar.js
7
- var TAB_MIME = "application/x-ecosim-tab";
8
- var NotebookTabBar = class _NotebookTabBar {
9
- /** @type {HTMLElement} */
10
- #container = null;
11
- /** @type {object[]} [{filePath, label, fileType, isDirty}] */
12
- #tabs = [];
13
- /** @type {string|null} */
14
- #activeTab = null;
15
- /** @type {object} callbacks */
16
- #callbacks = {};
17
- // ─── Context menu ────────────────────────────────────────────────────────
18
- /** @type {HTMLElement|null} */
19
- #contextMenuEl = null;
20
- /** @type {Function|null} */
21
- #boundHideContextMenu = null;
22
- // ─── Drag and drop ───────────────────────────────────────────────────────
23
- /** Same-pane reorder. Cross-pane drops stay in this class's own
24
- * container handlers (#onStripDrag*).
25
- * @type {DragReorder|null} */
26
- #dragReorder = null;
27
- mount(container, callbacks = {}) {
28
- this.#container = container;
29
- this.#callbacks = callbacks;
30
- this.#container.className = "tabs notebook-tabs";
31
- this.#boundHideContextMenu = () => this.#hideContextMenu();
32
- this.#installStripDnDHandlers();
33
- this.#dragReorder = new DragReorder({
34
- container: this.#container,
35
- itemSelector: ".tab",
36
- keyAttr: "path",
37
- axis: "x",
38
- draggableGuard: (e) => {
39
- if (e.target.closest(".twm-tab-close")) return false;
40
- const title = e.target.closest(".tab")?.querySelector(".twm-tab-title");
41
- return title?.getAttribute("contenteditable") !== "true";
42
- },
43
- onDragStart: (e, key) => {
44
- try {
45
- e.dataTransfer.setData(TAB_MIME, key);
46
- } catch (_) {
47
- }
48
- },
49
- onReorder: (order) => this.#callbacks.onReorder?.(order)
50
- });
51
- this.#render();
52
- }
53
- /**
54
- * Sync tabs from ProjectModel state.
55
- * @param {Array<{ filePath: string, fileType: string, isDirty: boolean }>} tabs
56
- * @param {string|null} activeFilePath
57
- */
58
- update(tabs, activeFilePath) {
59
- this.#tabs = tabs;
60
- this.#activeTab = activeFilePath;
61
- this.#render();
62
- }
63
- markDirty(filePath, isDirty) {
64
- const tab = this.#tabs.find((t) => t.filePath === filePath);
65
- if (tab) {
66
- tab.isDirty = isDirty;
67
- this.#render();
68
- }
69
- }
70
- dispose() {
71
- this.#hideContextMenu();
72
- this.#removeStripDnDHandlers();
73
- this.#dragReorder?.destroy();
74
- this.#dragReorder = null;
75
- this.#container = null;
76
- }
77
- // Material symbol per file kind, merged over the generic set. The map used to
78
- // hardcode EcoAgent's kinds — `scenario`, `dashboard`, `model` — so a generic
79
- // tab bar knew one application's file taxonomy. Pass `fileTypeIcons` to add
80
- // your own.
81
- static #FILE_TYPE_ICONS = {
82
- module: "extension",
83
- test: "fact_check",
84
- diff: "difference",
85
- unknown: "description"
86
- };
87
- static #EXTRA_ICONS = {};
88
- /**
89
- * Add your application's file kinds to the tab-bar icon map.
90
- *
91
- * The map used to hardcode `scenario: 'science'`, `dashboard: 'dashboard'` and
92
- * `model: 'schema'` — EcoAgent's file taxonomy, baked into a generic tab bar.
93
- *
94
- * @param {Object.<string, string>} icons fileType -> material symbol name
95
- */
96
- static setFileTypeIcons(icons = {}) {
97
- _NotebookTabBar.#EXTRA_ICONS = { ...icons };
98
- }
99
- static #iconFor(fileType) {
100
- return _NotebookTabBar.#EXTRA_ICONS[fileType] ?? _NotebookTabBar.#FILE_TYPE_ICONS[fileType] ?? _NotebookTabBar.#FILE_TYPE_ICONS.unknown;
101
- }
102
- // ═════════════════════════════════════════════════════════════════════════
103
- // Rendering
104
- // ═════════════════════════════════════════════════════════════════════════
105
- #render() {
106
- if (!this.#container) return;
107
- this.#container.innerHTML = "";
108
- for (const tab of this.#tabs) {
109
- const isActive = tab.filePath === this.#activeTab;
110
- const rawPath = tab.filePath.replace(/^(diff|builtin):\/\//, "");
111
- const label = tab.label ?? (tab.fileType === "diff" ? `\u2194 ${rawPath.split("/").pop()}` : rawPath.split("/").pop().replace(/\.[^.]+$/, ""));
112
- const icon = _NotebookTabBar.#iconFor(tab.fileType);
113
- const tabEl = document.createElement("div");
114
- tabEl.className = `tab${isActive ? " active" : ""}${tab.isPreview ? " tab--preview" : ""}`;
115
- tabEl.dataset.path = tab.filePath;
116
- tabEl.title = tab.filePath;
117
- tabEl.innerHTML = `
118
- <span class="material-symbols-outlined tab-icon">${icon}</span>
119
- <span class="twm-tab-title">${this.#escHtml(label)}</span>
120
- ${tab.isDirty ? '<span class="tab-dirty">\u25CF</span>' : ""}
121
- ${tab.readonly ? "" : '<button class="twm-tab-close" title="Close tab" aria-label="Close tab">\xD7</button>'}
122
- `;
123
- tabEl.addEventListener("click", (e) => {
124
- if (e.target.closest(".twm-tab-close")) return;
125
- if (tabEl.querySelector(".twm-tab-title")?.getAttribute("contenteditable") === "true") return;
126
- this.#callbacks.onActivate?.(tab.filePath);
127
- });
128
- tabEl.querySelector(".twm-tab-close")?.addEventListener("click", (e) => {
129
- e.stopPropagation();
130
- this.#callbacks.onClose?.(tab.filePath);
131
- });
132
- tabEl.addEventListener("dblclick", (e) => {
133
- if (e.target.closest(".twm-tab-close")) return;
134
- this.#beginInlineRename(tab.filePath);
135
- });
136
- tabEl.addEventListener("keydown", (e) => {
137
- if (e.key === "F2") {
138
- e.preventDefault();
139
- this.#beginInlineRename(tab.filePath);
140
- }
141
- });
142
- tabEl.addEventListener("contextmenu", (e) => {
143
- e.preventDefault();
144
- e.stopPropagation();
145
- this.#showContextMenu(e.clientX, e.clientY, tab.filePath);
146
- });
147
- this.#container.appendChild(tabEl);
148
- }
149
- this.#dragReorder?.attach();
150
- }
151
- // ═════════════════════════════════════════════════════════════════════════
152
- // Drag and drop reordering (same-pane + cross-pane)
153
- // ═════════════════════════════════════════════════════════════════════════
154
- #installStripDnDHandlers() {
155
- if (!this.#container) return;
156
- this.#container.addEventListener("dragover", this.#onStripDragOver);
157
- this.#container.addEventListener("drop", this.#onStripDrop);
158
- this.#container.addEventListener("dragleave", this.#onStripDragLeave);
159
- }
160
- #removeStripDnDHandlers() {
161
- if (!this.#container) return;
162
- this.#container.removeEventListener("dragover", this.#onStripDragOver);
163
- this.#container.removeEventListener("drop", this.#onStripDrop);
164
- this.#container.removeEventListener("dragleave", this.#onStripDragLeave);
165
- }
166
- /** Check whether a drag event carries a cross-pane tab (not from this bar). */
167
- #isExternalTabDrag(e) {
168
- if (this.#dragReorder?.isActive()) return false;
169
- try {
170
- return e.dataTransfer.types.includes(TAB_MIME);
171
- } catch {
172
- return false;
173
- }
174
- }
175
- // Same-pane reorder is handled by the shared DragReorder module (set
176
- // up in mount()). These container handlers cover ONLY cross-pane tab
177
- // drops — gated on `#isExternalTabDrag`, which defers while our own
178
- // reorder drag is active.
179
- #onStripDragOver = (e) => {
180
- if (this.#isExternalTabDrag(e)) {
181
- e.preventDefault();
182
- try {
183
- e.dataTransfer.dropEffect = "move";
184
- } catch (_) {
185
- }
186
- this.#container.classList.add("notebook-tabs--drop-target");
187
- }
188
- };
189
- #onStripDrop = (e) => {
190
- if (this.#isExternalTabDrag(e)) {
191
- e.preventDefault();
192
- this.#container.classList.remove("notebook-tabs--drop-target");
193
- const filePath = e.dataTransfer.getData(TAB_MIME) || e.dataTransfer.getData("text/plain");
194
- if (filePath) {
195
- this.#callbacks.onDropFromOtherPane?.(filePath);
196
- }
197
- }
198
- };
199
- #onStripDragLeave = (e) => {
200
- if (!this.#container.contains(e.relatedTarget)) {
201
- this.#container.classList.remove("notebook-tabs--drop-target");
202
- }
203
- };
204
- // ═════════════════════════════════════════════════════════════════════════
205
- // Context menu
206
- // ═════════════════════════════════════════════════════════════════════════
207
- #showContextMenu(x, y, filePath) {
208
- this.#hideContextMenu();
209
- const idx = this.#tabs.findIndex((t) => t.filePath === filePath);
210
- const total = this.#tabs.length;
211
- const allItems = [
212
- { label: "Rename", icon: "edit", action: "rename", shortcut: "F2" },
213
- { label: "Duplicate", icon: "content_copy", action: "duplicate" },
214
- { separator: true },
215
- { label: "Split Right", icon: "vertical_split", action: "split-right", shortcut: "Ctrl+\\" },
216
- { label: "Move to Other Pane", icon: "swap_horiz", action: "move-to-other" },
217
- { separator: true },
218
- { label: "Close", icon: "close", action: "close", danger: true },
219
- { label: "Close Others", icon: "close_fullscreen", action: "close-others", danger: true, disabled: total <= 1 },
220
- { label: "Close to the Right", icon: "tab_close", action: "close-right", danger: true, disabled: idx === -1 || idx >= total - 1 },
221
- { label: "Close All", icon: "tab_close_inactive", action: "close-all", danger: true },
222
- { label: "Close Saved", icon: "check_circle", action: "close-saved", danger: true },
223
- { separator: true },
224
- { label: "Reveal in Explorer", icon: "folder_open", action: "reveal" }
225
- ];
226
- const menuFilter = this.#callbacks.menuFilter;
227
- const items = menuFilter ? allItems.filter((item) => item.separator || menuFilter(item.action, filePath) !== false) : allItems;
228
- const cleaned = [];
229
- for (const item of items) {
230
- if (item.separator) {
231
- if (cleaned.length > 0 && !cleaned[cleaned.length - 1].separator) cleaned.push(item);
232
- } else {
233
- cleaned.push(item);
234
- }
235
- }
236
- while (cleaned.length > 0 && cleaned[cleaned.length - 1].separator) cleaned.pop();
237
- const menu = document.createElement("div");
238
- menu.className = "nb-context-menu";
239
- for (const item of cleaned) {
240
- if (item.separator) {
241
- const sep = document.createElement("div");
242
- sep.className = "nb-context-menu__separator";
243
- menu.appendChild(sep);
244
- continue;
245
- }
246
- const btn = document.createElement("button");
247
- btn.className = "nb-context-menu__item";
248
- if (item.danger) btn.classList.add("nb-context-menu__item--danger");
249
- if (item.disabled) btn.classList.add("nb-context-menu__item--disabled");
250
- btn.innerHTML = `<span class="material-symbols-outlined">${item.icon}</span><span>${this.#escHtml(item.label)}</span>${item.shortcut ? `<span class="nb-context-menu__shortcut">${item.shortcut}</span>` : ""}`;
251
- if (!item.disabled) {
252
- btn.addEventListener("click", (e) => {
253
- e.stopPropagation();
254
- this.#hideContextMenu();
255
- this.#handleContextMenuAction(item.action, filePath);
256
- });
257
- }
258
- menu.appendChild(btn);
259
- }
260
- document.body.appendChild(menu);
261
- this.#contextMenuEl = menu;
262
- const rect = menu.getBoundingClientRect();
263
- const left = Math.min(x, window.innerWidth - rect.width - 8);
264
- const top = Math.min(y, window.innerHeight - rect.height - 8);
265
- menu.style.left = `${Math.max(0, left)}px`;
266
- menu.style.top = `${Math.max(0, top)}px`;
267
- requestAnimationFrame(() => menu.classList.add("visible"));
268
- setTimeout(() => {
269
- document.addEventListener("click", this.#boundHideContextMenu, true);
270
- document.addEventListener("contextmenu", this.#boundHideContextMenu, true);
271
- }, 0);
272
- document.addEventListener("keydown", this.#onKeyDown);
273
- }
274
- #hideContextMenu() {
275
- if (!this.#contextMenuEl) return;
276
- this.#contextMenuEl.remove();
277
- this.#contextMenuEl = null;
278
- document.removeEventListener("click", this.#boundHideContextMenu, true);
279
- document.removeEventListener("contextmenu", this.#boundHideContextMenu, true);
280
- document.removeEventListener("keydown", this.#onKeyDown);
281
- }
282
- #onKeyDown = (e) => {
283
- if (e.key === "Escape") this.#hideContextMenu();
284
- };
285
- #handleContextMenuAction(action, filePath) {
286
- switch (action) {
287
- case "rename":
288
- this.#beginInlineRename(filePath);
289
- break;
290
- case "duplicate":
291
- this.#callbacks.onDuplicate?.(filePath);
292
- break;
293
- case "close":
294
- this.#callbacks.onClose?.(filePath);
295
- break;
296
- case "close-others":
297
- this.#callbacks.onCloseOthers?.(filePath);
298
- break;
299
- case "close-right":
300
- this.#callbacks.onCloseToRight?.(filePath);
301
- break;
302
- case "close-all":
303
- this.#callbacks.onCloseAll?.();
304
- break;
305
- case "close-saved":
306
- this.#callbacks.onCloseSaved?.();
307
- break;
308
- case "split-right":
309
- this.#callbacks.onSplitRight?.(filePath);
310
- break;
311
- case "move-to-other":
312
- this.#callbacks.onMoveToOtherPane?.(filePath);
313
- break;
314
- case "reveal":
315
- this.#callbacks.onRevealInExplorer?.(filePath);
316
- break;
317
- }
318
- }
319
- // ═════════════════════════════════════════════════════════════════════════
320
- // Inline rename (contenteditable on tab title)
321
- // ═════════════════════════════════════════════════════════════════════════
322
- #beginInlineRename(filePath) {
323
- const tabEl = this.#container?.querySelector(`.tab[data-path="${CSS.escape(filePath)}"]`);
324
- if (!tabEl) return;
325
- const title = tabEl.querySelector(".twm-tab-title");
326
- if (!title || title.getAttribute("contenteditable") === "true") return;
327
- const tab = this.#tabs.find((t) => t.filePath === filePath);
328
- if (!tab) return;
329
- if (tab.fileType === "model") return;
330
- if (filePath.startsWith("builtin://")) return;
331
- if (tab.readonly) return;
332
- const original = title.textContent || "";
333
- title.setAttribute("contenteditable", "true");
334
- title.focus();
335
- try {
336
- const range = document.createRange();
337
- range.selectNodeContents(title);
338
- const sel = window.getSelection();
339
- sel.removeAllRanges();
340
- sel.addRange(range);
341
- } catch (_) {
342
- }
343
- let committed = false;
344
- const finish = (commit) => {
345
- if (committed) return;
346
- committed = true;
347
- const newName = (title.textContent || "").trim();
348
- title.removeAttribute("contenteditable");
349
- title.removeEventListener("keydown", onKeyDown);
350
- title.removeEventListener("blur", onBlur, true);
351
- try {
352
- window.getSelection()?.removeAllRanges?.();
353
- } catch (_) {
354
- }
355
- if (commit && newName && newName !== original && /^[\w][\w.-]*$/.test(newName)) {
356
- title.textContent = newName;
357
- const dir = filePath.split("/").slice(0, -1).join("/");
358
- const ext = filePath.split(".").pop();
359
- const newPath = `${dir}/${newName}.${ext}`;
360
- this.#callbacks.onRename?.(filePath, newPath);
361
- } else {
362
- title.textContent = original;
363
- }
364
- };
365
- const onKeyDown = (e) => {
366
- if (e.key === "Enter") {
367
- e.preventDefault();
368
- finish(true);
369
- } else if (e.key === "Escape") {
370
- e.preventDefault();
371
- finish(false);
372
- }
373
- e.stopPropagation();
374
- };
375
- const onBlur = () => finish(true);
376
- title.addEventListener("keydown", onKeyDown);
377
- title.addEventListener("blur", onBlur, true);
378
- }
379
- #escHtml(s) {
380
- return String(s).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
381
- }
382
- };
383
-
384
7
  // src/editor/editor_pane.js
385
8
  var EditorPane = class {
386
9
  /** @type {string} */