flexdesk 0.4.5 → 0.4.6

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.
@@ -29,6 +29,7 @@ var NotebookTabBar = class _NotebookTabBar {
29
29
  this.#container.className = "tabs notebook-tabs";
30
30
  this.#boundHideContextMenu = () => this.#hideContextMenu();
31
31
  this.#installStripDnDHandlers();
32
+ this.#container.addEventListener("wheel", this.#onWheel, { passive: false });
32
33
  this.#dragReorder = new DragReorder({
33
34
  container: this.#container,
34
35
  itemSelector: ".tab",
@@ -74,6 +75,9 @@ var NotebookTabBar = class _NotebookTabBar {
74
75
  dispose() {
75
76
  this.#hideContextMenu();
76
77
  this.#removeStripDnDHandlers();
78
+ this.#container?.removeEventListener("wheel", this.#onWheel);
79
+ if (this.#revealFrame) cancelAnimationFrame(this.#revealFrame);
80
+ this.#revealFrame = 0;
77
81
  this.#dragReorder?.destroy();
78
82
  this.#dragReorder = null;
79
83
  this.#container = null;
@@ -108,6 +112,7 @@ var NotebookTabBar = class _NotebookTabBar {
108
112
  // ═════════════════════════════════════════════════════════════════════════
109
113
  #render() {
110
114
  if (!this.#container) return;
115
+ const keepLeft = this.#container.scrollLeft;
111
116
  this.#container.innerHTML = "";
112
117
  for (const tab of this.#tabs) {
113
118
  const isActive = tab.filePath === this.#activeTab;
@@ -151,8 +156,74 @@ var NotebookTabBar = class _NotebookTabBar {
151
156
  this.#container.appendChild(tabEl);
152
157
  }
153
158
  this.#dragReorder?.attach();
159
+ this.#container.scrollLeft = keepLeft;
160
+ const revealKey = `${this.#activeTab}\0${this.#tabs.length}`;
161
+ if (revealKey !== this.#revealedKey) {
162
+ this.#revealedKey = revealKey;
163
+ this.#revealActive(true);
164
+ }
154
165
  }
155
166
  // ═════════════════════════════════════════════════════════════════════════
167
+ // Scrolling (1.5) — the strip scrolls rather than wrapping or shrinking its
168
+ // tabs, and hides its scrollbar (a bar in a 26px strip eats a third of every
169
+ // label). So it owes a mouse user every other way in: a plain wheel, the
170
+ // active tab brought into view, and — in `DragReorder._autoScrollEdge` —
171
+ // the edges scrolling while a tab is dragged toward them.
172
+ // ═════════════════════════════════════════════════════════════════════════
173
+ /** @type {string|null} the active tab + count last scrolled into view */
174
+ #revealedKey = null;
175
+ /** @type {number} a pending retry, for a strip not yet laid out */
176
+ #revealFrame = 0;
177
+ /** Bring the active tab fully into the strip — and ONLY the strip. Not
178
+ * `scrollIntoView`, which scrolls every scrollable ancestor as well and
179
+ * would drag the tile, the window or the page along with the tab. */
180
+ #revealActive(retry = false) {
181
+ const el = this.#container;
182
+ if (!el) return;
183
+ const active = el.querySelector(":scope > .tab.active");
184
+ if (!active) return;
185
+ if (!el.isConnected || el.clientWidth === 0) {
186
+ if (retry && !this.#revealFrame && typeof requestAnimationFrame === "function") {
187
+ this.#revealFrame = requestAnimationFrame(() => {
188
+ this.#revealFrame = 0;
189
+ this.#revealActive(false);
190
+ });
191
+ }
192
+ return;
193
+ }
194
+ const box = el.getBoundingClientRect();
195
+ const tab = active.getBoundingClientRect();
196
+ const scale = (el.offsetWidth ? box.width / el.offsetWidth : 1) || 1;
197
+ const left = (tab.left - box.left) / scale + el.scrollLeft;
198
+ const right = left + tab.width / scale;
199
+ if (left < el.scrollLeft) el.scrollLeft = left;
200
+ else if (right > el.scrollLeft + el.clientWidth) el.scrollLeft = right - el.clientWidth;
201
+ }
202
+ /**
203
+ * A VERTICAL WHEEL SCROLLS THE STRIP SIDEWAYS. Chromium and Firefox scroll
204
+ * an element that overflows only horizontally with Shift+wheel or a
205
+ * touchpad, and not with the plain wheel most people have — so with the
206
+ * scrollbar hidden, every tab past the edge was unreachable with a mouse.
207
+ *
208
+ * The wheel is taken from the page only while the strip can actually move
209
+ * that way: at either end it is left alone, so a wheel over a strip that
210
+ * has nothing more to show still scrolls whatever is behind it. A gesture
211
+ * that is already horizontal (a touchpad, Shift+wheel) and Ctrl+wheel (zoom)
212
+ * are the browser's, untouched.
213
+ */
214
+ #onWheel = (e) => {
215
+ const el = this.#container;
216
+ if (!el || e.ctrlKey || e.shiftKey) return;
217
+ if (Math.abs(e.deltaX) >= Math.abs(e.deltaY)) return;
218
+ const room = el.scrollWidth - el.clientWidth;
219
+ if (room <= 0) return;
220
+ const unit = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? el.clientWidth : 1;
221
+ const next = Math.max(0, Math.min(room, el.scrollLeft + e.deltaY * unit));
222
+ if (Math.abs(next - el.scrollLeft) < 0.5) return;
223
+ e.preventDefault();
224
+ el.scrollLeft = next;
225
+ };
226
+ // ═════════════════════════════════════════════════════════════════════════
156
227
  // Drag and drop reordering (same-pane + cross-pane)
157
228
  // ═════════════════════════════════════════════════════════════════════════
158
229
  #installStripDnDHandlers() {
@@ -405,4 +476,4 @@ var NotebookTabBar = class _NotebookTabBar {
405
476
  export {
406
477
  NotebookTabBar
407
478
  };
408
- //# sourceMappingURL=chunk-QNQHQ24V.js.map
479
+ //# sourceMappingURL=chunk-NLTOY57M.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/editor/notebook_tab_bar.js"],
4
+ "sourcesContent": ["/**\n * NotebookTabBar \u2014 tab strip showing open files, with dirty indicators,\n * close buttons, drag-and-drop reordering, double-click rename, and\n * right-click context menu.\n *\n * Uses the same `.tab` / `.tab-title` / `.tab-dirty` / `.tab-close` DOM structure\n * as the flow-page tab bar, so all editor tab bars share a unified appearance.\n *\n * Calls back into parent via:\n * onActivate(filePath)\n * onClose(filePath)\n * onCloseOthers(filePath)\n * onCloseToRight(filePath)\n * onCloseAll()\n * onCloseSaved()\n * onRename(oldPath, newPath)\n * onDuplicate(filePath)\n * onReorder(orderedPaths)\n * onRevealInExplorer(filePath)\n * onSplitRight(filePath)\n * onMoveToOtherPane(filePath)\n * onDropFromOtherPane(filePath) \u2014 cross-pane drag-and-drop\n */\n\nimport { DragReorder } from '../ui/components/drag_reorder.js';\n\n/** Custom MIME type so tab bars recognise cross-pane drags. */\nconst TAB_MIME = 'application/x-ecosim-tab';\n\nexport class NotebookTabBar {\n /** @type {HTMLElement} */\n #container = null;\n\n /** @type {object[]} [{filePath, label, fileType, isDirty}] */\n #tabs = [];\n\n /** @type {string|null} */\n #activeTab = null;\n\n /** @type {object} callbacks */\n #callbacks = {};\n\n // \u2500\u2500\u2500 Context menu \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /** @type {HTMLElement|null} */\n #contextMenuEl = null;\n\n /** @type {Function|null} */\n #boundHideContextMenu = null;\n\n // \u2500\u2500\u2500 Drag and drop \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n /** Same-pane reorder. Cross-pane drops stay in this class's own\n * container handlers (#onStripDrag*).\n * @type {DragReorder|null} */\n #dragReorder = null;\n\n mount(container, callbacks = {}) {\n this.#container = container;\n this.#callbacks = callbacks;\n this.#container.className = 'tabs notebook-tabs';\n this.#boundHideContextMenu = () => this.#hideContextMenu();\n this.#installStripDnDHandlers();\n // NOT PASSIVE: it has to be able to take the wheel from the page.\n this.#container.addEventListener('wheel', this.#onWheel, { passive: false });\n this.#dragReorder = new DragReorder({\n container: this.#container,\n itemSelector: '.tab',\n keyAttr: 'path',\n axis: 'x',\n draggableGuard: (e) => {\n if (e.target.closest('.twm-tab-close')) return false;\n const title = e.target.closest('.tab')?.querySelector('.twm-tab-title');\n return title?.getAttribute('contenteditable') !== 'true';\n },\n onDragStart: (e, key, item) => {\n // Carry the cross-pane MIME so other panes accept this tab.\n try { e.dataTransfer.setData(TAB_MIME, key); } catch (_) {}\n // C33. \u2026AND LET THE HOST ADD ITS OWN, which is the whole of\n // what a second consumer of this strip needs from it.\n //\n // `DragReorder` publishes this hook for exactly this purpose\n // (\"host hook to augment dataTransfer (e.g. add a cross-pane\n // MIME)\", `../ui/components/drag_reorder.js`), and this class\n // consumed it and stopped there. The tile renderer mounts one\n // of these per tile and needs to mark the drag as one of ITS\n // tabs \u2014 a different question from \"is this an editor tab\",\n // asked of the same gesture.\n //\n // ADDITIVE BY CONSTRUCTION: `TAB_MIME` is set first and\n // unchanged, so `EditorPane`'s existing cross-pane drop cannot\n // notice that anyone else is listening.\n try { this.#callbacks.onDragStart?.(e, key, item); }\n catch (err) { console.error('[tab-bar] onDragStart threw', err); }\n },\n onReorder: (order) => this.#callbacks.onReorder?.(order),\n });\n this.#render();\n }\n\n /**\n * Sync tabs from ProjectModel state.\n * @param {Array<{ filePath: string, fileType: string, isDirty: boolean }>} tabs\n * @param {string|null} activeFilePath\n */\n update(tabs, activeFilePath) {\n this.#tabs = tabs;\n this.#activeTab = activeFilePath;\n this.#render();\n }\n\n markDirty(filePath, isDirty) {\n const tab = this.#tabs.find(t => t.filePath === filePath);\n if (tab) {\n tab.isDirty = isDirty;\n this.#render();\n }\n }\n\n dispose() {\n this.#hideContextMenu();\n this.#removeStripDnDHandlers();\n this.#container?.removeEventListener('wheel', this.#onWheel);\n if (this.#revealFrame) cancelAnimationFrame(this.#revealFrame);\n this.#revealFrame = 0;\n this.#dragReorder?.destroy();\n this.#dragReorder = null;\n this.#container = null;\n }\n\n // Material symbol per file kind, merged over the generic set. The map used to\n // hardcode EcoAgent's kinds \u2014 `scenario`, `dashboard`, `model` \u2014 so a generic\n // tab bar knew one application's file taxonomy. Pass `fileTypeIcons` to add\n // your own.\n static #FILE_TYPE_ICONS = {\n module: 'extension',\n test: 'fact_check',\n diff: 'difference',\n unknown: 'description',\n };\n\n static #EXTRA_ICONS = {};\n\n /**\n * Add your application's file kinds to the tab-bar icon map.\n *\n * The map used to hardcode `scenario: 'science'`, `dashboard: 'dashboard'` and\n * `model: 'schema'` \u2014 EcoAgent's file taxonomy, baked into a generic tab bar.\n *\n * @param {Object.<string, string>} icons fileType -> material symbol name\n */\n static setFileTypeIcons(icons = {}) {\n NotebookTabBar.#EXTRA_ICONS = { ...icons };\n }\n\n static #iconFor(fileType) {\n return NotebookTabBar.#EXTRA_ICONS[fileType]\n ?? NotebookTabBar.#FILE_TYPE_ICONS[fileType]\n ?? NotebookTabBar.#FILE_TYPE_ICONS.unknown;\n }\n\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n // Rendering\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n #render() {\n if (!this.#container) return;\n\n // THE STRIP KEEPS ITS SCROLL ACROSS A REBUILD. Emptying it shrinks its\n // scroll width to nothing, which clamps `scrollLeft` to 0 \u2014 so every\n // render (a tab activated, a title changed) threw a strip scrolled to\n // its tenth tab back to its first.\n const keepLeft = this.#container.scrollLeft;\n this.#container.innerHTML = '';\n\n for (const tab of this.#tabs) {\n const isActive = tab.filePath === this.#activeTab;\n const rawPath = tab.filePath.replace(/^(diff|builtin):\\/\\//, '');\n const label = tab.label\n ?? (tab.fileType === 'diff'\n ? `\u2194 ${rawPath.split('/').pop()}`\n : rawPath.split('/').pop().replace(/\\.[^.]+$/, ''));\n const icon = NotebookTabBar.#iconFor(tab.fileType);\n\n const tabEl = document.createElement('div');\n tabEl.className = `tab${isActive ? ' active' : ''}${tab.isPreview ? ' tab--preview' : ''}`;\n tabEl.dataset.path = tab.filePath;\n tabEl.title = tab.filePath;\n\n tabEl.innerHTML = `\n <span class=\"material-symbols-outlined tab-icon\">${icon}</span>\n <span class=\"twm-tab-title\">${this.#escHtml(label)}</span>\n ${tab.isDirty ? '<span class=\"tab-dirty\">\\u25cf</span>' : ''}\n ${tab.readonly ? '' : '<button class=\"twm-tab-close\" title=\"Close tab\" aria-label=\"Close tab\">\\u00d7</button>'}\n `;\n\n // Click \u2192 activate\n tabEl.addEventListener('click', (e) => {\n if (e.target.closest('.twm-tab-close')) return;\n if (tabEl.querySelector('.twm-tab-title')?.getAttribute('contenteditable') === 'true') return;\n this.#callbacks.onActivate?.(tab.filePath);\n });\n\n // Close button\n tabEl.querySelector('.twm-tab-close')?.addEventListener('click', (e) => {\n e.stopPropagation();\n this.#callbacks.onClose?.(tab.filePath);\n });\n\n // Double-click \u2192 inline rename\n tabEl.addEventListener('dblclick', (e) => {\n if (e.target.closest('.twm-tab-close')) return;\n this.#beginInlineRename(tab.filePath);\n });\n\n // F2 \u2192 inline rename\n tabEl.addEventListener('keydown', (e) => {\n if (e.key === 'F2') {\n e.preventDefault();\n this.#beginInlineRename(tab.filePath);\n }\n });\n\n // Context menu\n tabEl.addEventListener('contextmenu', (e) => {\n e.preventDefault();\n e.stopPropagation();\n this.#showContextMenu(e.clientX, e.clientY, tab.filePath);\n });\n\n this.#container.appendChild(tabEl);\n }\n // Make the freshly-rendered tabs draggable-to-reorder.\n this.#dragReorder?.attach();\n\n this.#container.scrollLeft = keepLeft;\n // A tab that has just BECOME active \u2014 opened, switched to, moved here\n // from another pane \u2014 is scrolled into the strip. Only then: a render\n // for any other reason (a title, a dirty dot) leaves the strip where the\n // reader put it, rather than yanking it back to the active tab.\n const revealKey = `${this.#activeTab}\\u0000${this.#tabs.length}`;\n if (revealKey !== this.#revealedKey) {\n this.#revealedKey = revealKey;\n this.#revealActive(true);\n }\n }\n\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n // Scrolling (1.5) \u2014 the strip scrolls rather than wrapping or shrinking its\n // tabs, and hides its scrollbar (a bar in a 26px strip eats a third of every\n // label). So it owes a mouse user every other way in: a plain wheel, the\n // active tab brought into view, and \u2014 in `DragReorder._autoScrollEdge` \u2014\n // the edges scrolling while a tab is dragged toward them.\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n /** @type {string|null} the active tab + count last scrolled into view */\n #revealedKey = null;\n\n /** @type {number} a pending retry, for a strip not yet laid out */\n #revealFrame = 0;\n\n /** Bring the active tab fully into the strip \u2014 and ONLY the strip. Not\n * `scrollIntoView`, which scrolls every scrollable ancestor as well and\n * would drag the tile, the window or the page along with the tab. */\n #revealActive(retry = false) {\n const el = this.#container;\n if (!el) return;\n const active = el.querySelector(':scope > .tab.active');\n if (!active) return;\n if (!el.isConnected || el.clientWidth === 0) {\n // Mounted but not laid out yet (the renderer updates a strip before\n // it attaches it). Once, on the next frame \u2014 not a loop.\n if (retry && !this.#revealFrame && typeof requestAnimationFrame === 'function') {\n this.#revealFrame = requestAnimationFrame(() => {\n this.#revealFrame = 0;\n this.#revealActive(false);\n });\n }\n return;\n }\n const box = el.getBoundingClientRect();\n const tab = active.getBoundingClientRect();\n // Client pixels to the strip's own: 1 unless something above it zooms.\n const scale = (el.offsetWidth ? box.width / el.offsetWidth : 1) || 1;\n const left = (tab.left - box.left) / scale + el.scrollLeft;\n const right = left + tab.width / scale;\n if (left < el.scrollLeft) el.scrollLeft = left;\n else if (right > el.scrollLeft + el.clientWidth) el.scrollLeft = right - el.clientWidth;\n }\n\n /**\n * A VERTICAL WHEEL SCROLLS THE STRIP SIDEWAYS. Chromium and Firefox scroll\n * an element that overflows only horizontally with Shift+wheel or a\n * touchpad, and not with the plain wheel most people have \u2014 so with the\n * scrollbar hidden, every tab past the edge was unreachable with a mouse.\n *\n * The wheel is taken from the page only while the strip can actually move\n * that way: at either end it is left alone, so a wheel over a strip that\n * has nothing more to show still scrolls whatever is behind it. A gesture\n * that is already horizontal (a touchpad, Shift+wheel) and Ctrl+wheel (zoom)\n * are the browser's, untouched.\n */\n #onWheel = (e) => {\n const el = this.#container;\n if (!el || e.ctrlKey || e.shiftKey) return;\n if (Math.abs(e.deltaX) >= Math.abs(e.deltaY)) return;\n const room = el.scrollWidth - el.clientWidth;\n if (room <= 0) return;\n const unit = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? el.clientWidth : 1;\n const next = Math.max(0, Math.min(room, el.scrollLeft + e.deltaY * unit));\n if (Math.abs(next - el.scrollLeft) < 0.5) return;\n e.preventDefault();\n el.scrollLeft = next;\n };\n\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n // Drag and drop reordering (same-pane + cross-pane)\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n #installStripDnDHandlers() {\n if (!this.#container) return;\n this.#container.addEventListener('dragover', this.#onStripDragOver);\n this.#container.addEventListener('drop', this.#onStripDrop);\n this.#container.addEventListener('dragleave', this.#onStripDragLeave);\n }\n\n #removeStripDnDHandlers() {\n if (!this.#container) return;\n this.#container.removeEventListener('dragover', this.#onStripDragOver);\n this.#container.removeEventListener('drop', this.#onStripDrop);\n this.#container.removeEventListener('dragleave', this.#onStripDragLeave);\n }\n\n /** Check whether a drag event carries a cross-pane tab (not from this bar).\n *\n * C33. A HOST MAY NAME A SECOND MIME, at mount, as\n * `callbacks.externalTabMimes` \u2014 the tile renderer's tabs are not editor\n * tabs and do not carry `application/x-ecosim-tab`, so without this the\n * strip in tile B would refuse a tab dragged out of tile A's strip. Empty\n * by default, so every existing consumer keeps exactly today's admission\n * test.\n *\n * THE `isActive()` EARLY RETURN STAYS FIRST AND STAYS UNCHANGED. It is the\n * one line that keeps this bar's own reorder from being read as a foreign\n * drop, and no widening of the MIME list may be allowed to reach past it.\n *\n * Only `types` is consulted, never `getData` \u2014 under the HTML5 protected\n * mode `getData` returns the empty string for every event except `drop`,\n * so a payload read here would find nothing and arm nothing. */\n #isExternalTabDrag(e) {\n if (this.#dragReorder?.isActive()) return false; // same-pane drag is active\n const extra = Array.isArray(this.#callbacks.externalTabMimes)\n ? this.#callbacks.externalTabMimes : [];\n try {\n const types = e.dataTransfer.types;\n return types.includes(TAB_MIME) || extra.some((m) => types.includes(m));\n } catch { return false; }\n }\n\n // Same-pane reorder is handled by the shared DragReorder module (set\n // up in mount()). These container handlers cover ONLY cross-pane tab\n // drops \u2014 gated on `#isExternalTabDrag`, which defers while our own\n // reorder drag is active.\n\n #onStripDragOver = (e) => {\n if (this.#isExternalTabDrag(e)) {\n e.preventDefault();\n try { e.dataTransfer.dropEffect = 'move'; } catch (_) {}\n this.#container.classList.add('notebook-tabs--drop-target');\n }\n };\n\n #onStripDrop = (e) => {\n if (this.#isExternalTabDrag(e)) {\n e.preventDefault();\n this.#container.classList.remove('notebook-tabs--drop-target');\n const filePath = e.dataTransfer.getData(TAB_MIME) || e.dataTransfer.getData('text/plain');\n if (filePath) {\n this.#callbacks.onDropFromOtherPane?.(filePath);\n }\n }\n };\n\n #onStripDragLeave = (e) => {\n if (!this.#container.contains(e.relatedTarget)) {\n this.#container.classList.remove('notebook-tabs--drop-target');\n }\n };\n\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n // Context menu\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n #showContextMenu(x, y, filePath) {\n this.#hideContextMenu();\n\n const idx = this.#tabs.findIndex(t => t.filePath === filePath);\n const total = this.#tabs.length;\n\n const allItems = [\n { label: 'Rename', icon: 'edit', action: 'rename', shortcut: 'F2' },\n { label: 'Duplicate', icon: 'content_copy', action: 'duplicate' },\n { separator: true },\n { label: 'Split Right', icon: 'vertical_split', action: 'split-right', shortcut: 'Ctrl+\\\\' },\n { label: 'Move to Other Pane',icon: 'swap_horiz', action: 'move-to-other' },\n { separator: true },\n { label: 'Close', icon: 'close', action: 'close', danger: true },\n { label: 'Close Others', icon: 'close_fullscreen', action: 'close-others', danger: true, disabled: total <= 1 },\n { label: 'Close to the Right',icon: 'tab_close', action: 'close-right', danger: true, disabled: idx === -1 || idx >= total - 1 },\n { label: 'Close All', icon: 'tab_close_inactive', action: 'close-all', danger: true },\n { label: 'Close Saved', icon: 'check_circle', action: 'close-saved', danger: true },\n { separator: true },\n { label: 'Reveal in Explorer', icon: 'folder_open', action: 'reveal' },\n ];\n\n const menuFilter = this.#callbacks.menuFilter;\n const items = menuFilter\n ? allItems.filter(item => item.separator || menuFilter(item.action, filePath) !== false)\n : allItems;\n\n // Collapse consecutive / leading / trailing separators\n const cleaned = [];\n for (const item of items) {\n if (item.separator) {\n if (cleaned.length > 0 && !cleaned[cleaned.length - 1].separator) cleaned.push(item);\n } else {\n cleaned.push(item);\n }\n }\n while (cleaned.length > 0 && cleaned[cleaned.length - 1].separator) cleaned.pop();\n\n const menu = document.createElement('div');\n menu.className = 'nb-context-menu';\n\n for (const item of cleaned) {\n if (item.separator) {\n const sep = document.createElement('div');\n sep.className = 'nb-context-menu__separator';\n menu.appendChild(sep);\n continue;\n }\n\n const btn = document.createElement('button');\n btn.className = 'nb-context-menu__item';\n if (item.danger) btn.classList.add('nb-context-menu__item--danger');\n if (item.disabled) btn.classList.add('nb-context-menu__item--disabled');\n\n btn.innerHTML = `<span class=\"material-symbols-outlined\">${item.icon}</span><span>${this.#escHtml(item.label)}</span>${\n item.shortcut ? `<span class=\"nb-context-menu__shortcut\">${item.shortcut}</span>` : ''\n }`;\n\n if (!item.disabled) {\n btn.addEventListener('click', (e) => {\n e.stopPropagation();\n this.#hideContextMenu();\n this.#handleContextMenuAction(item.action, filePath);\n });\n }\n menu.appendChild(btn);\n }\n\n document.body.appendChild(menu);\n this.#contextMenuEl = menu;\n\n // Position with viewport clamping\n const rect = menu.getBoundingClientRect();\n const left = Math.min(x, window.innerWidth - rect.width - 8);\n const top = Math.min(y, window.innerHeight - rect.height - 8);\n menu.style.left = `${Math.max(0, left)}px`;\n menu.style.top = `${Math.max(0, top)}px`;\n\n // Animate in\n requestAnimationFrame(() => menu.classList.add('visible'));\n\n // Dismiss on outside click or Escape\n setTimeout(() => {\n document.addEventListener('click', this.#boundHideContextMenu, true);\n document.addEventListener('contextmenu', this.#boundHideContextMenu, true);\n }, 0);\n document.addEventListener('keydown', this.#onKeyDown);\n }\n\n #hideContextMenu() {\n if (!this.#contextMenuEl) return;\n this.#contextMenuEl.remove();\n this.#contextMenuEl = null;\n document.removeEventListener('click', this.#boundHideContextMenu, true);\n document.removeEventListener('contextmenu', this.#boundHideContextMenu, true);\n document.removeEventListener('keydown', this.#onKeyDown);\n }\n\n #onKeyDown = (e) => {\n if (e.key === 'Escape') this.#hideContextMenu();\n };\n\n #handleContextMenuAction(action, filePath) {\n switch (action) {\n case 'rename': this.#beginInlineRename(filePath); break;\n case 'duplicate': this.#callbacks.onDuplicate?.(filePath); break;\n case 'close': this.#callbacks.onClose?.(filePath); break;\n case 'close-others': this.#callbacks.onCloseOthers?.(filePath); break;\n case 'close-right': this.#callbacks.onCloseToRight?.(filePath); break;\n case 'close-all': this.#callbacks.onCloseAll?.(); break;\n case 'close-saved': this.#callbacks.onCloseSaved?.(); break;\n case 'split-right': this.#callbacks.onSplitRight?.(filePath); break;\n case 'move-to-other':this.#callbacks.onMoveToOtherPane?.(filePath); break;\n case 'reveal': this.#callbacks.onRevealInExplorer?.(filePath); break;\n }\n }\n\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n // Inline rename (contenteditable on tab title)\n // \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n\n #beginInlineRename(filePath) {\n const tabEl = this.#container?.querySelector(`.tab[data-path=\"${CSS.escape(filePath)}\"]`);\n if (!tabEl) return;\n\n const title = tabEl.querySelector('.twm-tab-title');\n if (!title || title.getAttribute('contenteditable') === 'true') return;\n\n const tab = this.#tabs.find(t => t.filePath === filePath);\n if (!tab) return;\n\n // model.notebook, builtin, and readonly files cannot be renamed\n if (tab.fileType === 'model') return;\n if (filePath.startsWith('builtin://')) return;\n if (tab.readonly) return;\n\n const original = title.textContent || '';\n title.setAttribute('contenteditable', 'true');\n title.focus();\n\n try {\n const range = document.createRange();\n range.selectNodeContents(title);\n const sel = window.getSelection();\n sel.removeAllRanges();\n sel.addRange(range);\n } catch (_) { /* ignore */ }\n\n let committed = false;\n\n const finish = (commit) => {\n if (committed) return;\n committed = true;\n\n const newName = (title.textContent || '').trim();\n title.removeAttribute('contenteditable');\n title.removeEventListener('keydown', onKeyDown);\n title.removeEventListener('blur', onBlur, true);\n\n try { window.getSelection()?.removeAllRanges?.(); } catch (_) { /* ignore */ }\n\n if (commit && newName && newName !== original && /^[\\w][\\w.-]*$/.test(newName)) {\n title.textContent = newName;\n const dir = filePath.split('/').slice(0, -1).join('/');\n const ext = filePath.split('.').pop();\n const newPath = `${dir}/${newName}.${ext}`;\n this.#callbacks.onRename?.(filePath, newPath);\n } else {\n title.textContent = original;\n }\n };\n\n const onKeyDown = (e) => {\n if (e.key === 'Enter') { e.preventDefault(); finish(true); }\n else if (e.key === 'Escape') { e.preventDefault(); finish(false); }\n e.stopPropagation();\n };\n\n const onBlur = () => finish(true);\n\n title.addEventListener('keydown', onKeyDown);\n title.addEventListener('blur', onBlur, true);\n }\n\n #escHtml(s) {\n return String(s).replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');\n }\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
package/dist/editor.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  NotebookTabBar
3
- } from "./chunk-QNQHQ24V.js";
3
+ } from "./chunk-NLTOY57M.js";
4
4
  import "./chunk-WVFGV5FT.js";
5
5
  import "./chunk-JYWURG5T.js";
6
6
 
package/dist/flexdesk.css CHANGED
@@ -553,6 +553,25 @@ body.twm-dt-col-resizing {
553
553
  .twm-preview-table.twm-dt-measuring .twm-data-table__filter-row {
554
554
  display: none !important;
555
555
  }
556
+ /* ── C10. The virtual window (`DataTable` with `virtualize`) ─────────────
557
+ * The column track is a `<colgroup>` (see `_createTable`), so it is the
558
+ * `<col>` widths the measuring pass has to lift — the same `!important` it
559
+ * already uses on the cells. The floors on body cells (100px on the first,
560
+ * 80px on the rest) are off: a full render lifts them with inline styles on
561
+ * its first row, and a virtual one has no fixed first row to write them on.
562
+ * The spacers carry their geometry inline; this only keeps a hover or a
563
+ * stripe from painting them. */
564
+ .twm-preview-table.twm-dt-measuring col {
565
+ width: auto !important;
566
+ }
567
+ .twm-preview-table.twm-dt--virtual tbody td {
568
+ min-width: 0;
569
+ max-width: none;
570
+ }
571
+ .twm-preview-table tbody tr.twm-dt-spacer,
572
+ .twm-preview-table tbody tr.twm-dt-spacer > td {
573
+ background: transparent !important;
574
+ }
556
575
  .twm-preview-table th {
557
576
  position: sticky;
558
577
  top: 0;
@@ -984,7 +1003,9 @@ body.twm-dt-col-resizing {
984
1003
  outline-offset: -2px;
985
1004
  }
986
1005
  /* Unified collapsible arrow system */
987
- .twm-collapsible-arrow {
1006
+ /* Compounded for the Material Symbols tie — see `.twm-leaf__icon`. */
1007
+ .twm-collapsible-arrow,
1008
+ .twm-collapsible-arrow.material-symbols-outlined {
988
1009
  transition: transform 0.2s ease;
989
1010
  transform: rotate(0deg); /* Default: pointing down (expanded) */
990
1011
  font-size: 10px;
@@ -1045,7 +1066,12 @@ body.twm-dt-col-resizing {
1045
1066
  margin: 0 1px;
1046
1067
  user-select: none;
1047
1068
  }
1048
- .twm-topbar-breadcrumb__icon {
1069
+ /* Written twice for `.twm-leaf__icon`'s reason: the icon also carries
1070
+ * `.material-symbols-outlined`, and Google's stylesheet — loaded after this
1071
+ * one — sets that class to 24px. A single-class rule ties and loses on source
1072
+ * order, so every crumb drew a 24px glyph beside 12px text (Tables BUG-0024). */
1073
+ .twm-topbar-breadcrumb__icon,
1074
+ .twm-topbar-breadcrumb__icon.material-symbols-outlined {
1049
1075
  font-size: var(--font-size-lg);
1050
1076
  }
1051
1077
  .twm-topbar-breadcrumb__item--current {
@@ -1343,7 +1369,9 @@ body.twm-dt-col-resizing {
1343
1369
  .twm-autocomplete-field__item:hover {
1344
1370
  background-color: #3a3a3a;
1345
1371
  }
1346
- .twm-autocomplete-field__icon {
1372
+ /* Compounded for the Material Symbols tie — see `.twm-leaf__icon`. */
1373
+ .twm-autocomplete-field__icon,
1374
+ .twm-autocomplete-field__icon.material-symbols-outlined {
1347
1375
  font-size: var(--font-size-2xl);
1348
1376
  color: var(--text-muted);
1349
1377
  flex-shrink: 0;
@@ -3356,7 +3384,9 @@ body.twm-window-maximized .twm-resize-handle {
3356
3384
  .twm-managed-window--no-drag .twm-managed-window__topbar {
3357
3385
  cursor: default;
3358
3386
  }
3359
- .twm-managed-window__icon {
3387
+ /* Compounded for the Material Symbols tie — see `.twm-leaf__icon`. */
3388
+ .twm-managed-window__icon,
3389
+ .twm-managed-window__icon.material-symbols-outlined {
3360
3390
  font-size: var(--font-size-lg);
3361
3391
  color: var(--text-muted);
3362
3392
  flex-shrink: 0;
@@ -3518,7 +3548,9 @@ button.twm-managed-window__btn svg rect {
3518
3548
  .twm-bar-windows__item:active {
3519
3549
  background: #1a6aa5;
3520
3550
  }
3521
- .twm-bar-windows__item-icon {
3551
+ /* Compounded for the Material Symbols tie — see `.twm-leaf__icon`. */
3552
+ .twm-bar-windows__item-icon,
3553
+ .twm-bar-windows__item-icon.material-symbols-outlined {
3522
3554
  font-size: var(--font-size-sm);
3523
3555
  flex-shrink: 0;
3524
3556
  }
@@ -3569,7 +3601,9 @@ button.twm-managed-window__btn svg rect {
3569
3601
  align-items: flex-start;
3570
3602
  gap: var(--space-lg);
3571
3603
  }
3572
- .twm-confirm-dialog__icon {
3604
+ /* Compounded for the Material Symbols tie — see `.twm-leaf__icon`. */
3605
+ .twm-confirm-dialog__icon,
3606
+ .twm-confirm-dialog__icon.material-symbols-outlined {
3573
3607
  font-size: 32px;
3574
3608
  color: var(--text-muted);
3575
3609
  flex-shrink: 0;
@@ -3638,7 +3672,9 @@ button.twm-confirm-dialog__btn:focus {
3638
3672
  border-bottom: 1px solid #333;
3639
3673
  flex-shrink: 0;
3640
3674
  }
3641
- .twm-about-dialog__logo {
3675
+ /* Compounded for the Material Symbols tie — see `.twm-leaf__icon`. */
3676
+ .twm-about-dialog__logo,
3677
+ .twm-about-dialog__logo.material-symbols-outlined {
3642
3678
  font-size: 40px;
3643
3679
  color: var(--accent-bright);
3644
3680
  }
@@ -3788,7 +3824,9 @@ button.twm-about-dialog__btn--close:focus {
3788
3824
  gap: 10px;
3789
3825
  min-width: 0;
3790
3826
  }
3791
- .twm-detail-header__icon {
3827
+ /* Compounded for the Material Symbols tie — see `.twm-leaf__icon`. */
3828
+ .twm-detail-header__icon,
3829
+ .twm-detail-header__icon.material-symbols-outlined {
3792
3830
  font-size: 20px;
3793
3831
  color: var(--text-muted, var(--text-muted));
3794
3832
  flex-shrink: 0;
@@ -4041,7 +4079,9 @@ button.twm-about-dialog__btn--close:focus {
4041
4079
  background-color: var(--ea-input-bg, #1e1e1e);
4042
4080
  border-color: rgba(96, 192, 96, 0.50);
4043
4081
  }
4044
- .twm-attr-editor__attention {
4082
+ /* Compounded for the Material Symbols tie — see `.twm-leaf__icon`. */
4083
+ .twm-attr-editor__attention,
4084
+ .twm-attr-editor__attention.material-symbols-outlined {
4045
4085
  /* Positioned in the row's left gutter so it doesn't push the
4046
4086
  * cells. The row already has `position: relative`. */
4047
4087
  position: absolute;
@@ -4052,7 +4092,9 @@ button.twm-about-dialog__btn--close:focus {
4052
4092
  font-size: 0.85em;
4053
4093
  pointer-events: none;
4054
4094
  }
4055
- .twm-attr-editor__revert {
4095
+ /* Compounded for the Material Symbols tie — see `.twm-leaf__icon`. */
4096
+ .twm-attr-editor__revert,
4097
+ .twm-attr-editor__revert.material-symbols-outlined {
4056
4098
  /* Floats over the right edge of the row, outside the column grid.
4057
4099
  * Sits ABOVE the trailing remove-button gutter that sortable_list
4058
4100
  * draws, so it stays visible. */
@@ -5609,8 +5651,11 @@ body.twm-dragging * { user-select: none; }
5609
5651
  * scrolls under them — a tab that shrinks to nothing as you open the tenth
5610
5652
  * one is a tab you cannot read or aim at. The scrollbar itself is hidden:
5611
5653
  * it is a 26px strip, and a horizontal bar in it would eat a third of the
5612
- * label. Wheel and drag still scroll it, and DragReorder auto-scrolls the
5613
- * edges while a tab is being dragged (`drag_reorder.js:_autoScrollEdge`). */
5654
+ * label. What replaces it is in `notebook_tab_bar.js` (1.5): a plain
5655
+ * vertical wheel scrolls the strip sideways (`#onWheel`), the tab that has
5656
+ * just become active is scrolled into view (`#revealActive`), and the edges
5657
+ * auto-scroll while a tab is dragged toward them
5658
+ * (`drag_reorder.js:_autoScrollEdge`). */
5614
5659
  overflow-x: auto;
5615
5660
  overflow-y: hidden;
5616
5661
  scrollbar-width: none;
@@ -5862,7 +5907,9 @@ body.twm-dragging * { user-select: none; }
5862
5907
  letter-spacing: 0.04em;
5863
5908
  color: var(--text-secondary);
5864
5909
  }
5865
- .twm-tile-tabmenu__head-icon { font-size: 14px; }
5910
+ /* Compounded for the Material Symbols tie — see `.twm-leaf__icon`. */
5911
+ .twm-tile-tabmenu__head-icon,
5912
+ .twm-tile-tabmenu__head-icon.material-symbols-outlined { font-size: 14px; }
5866
5913
  .twm-tile-tabmenu__head-label { flex: 1 1 auto; }
5867
5914
  .twm-tile-tabmenu__close {
5868
5915
  background: transparent;
@@ -6038,7 +6085,9 @@ body.twm-dragging * { user-select: none; }
6038
6085
  outline-offset: -1px;
6039
6086
  }
6040
6087
  .twm-tile-tabswitch__item--on { color: var(--text-bright); }
6041
- .twm-tile-tabswitch__check { font-size: 14px; color: var(--accent-bright, #1683c4); }
6088
+ /* Compounded for the Material Symbols tie — see `.twm-leaf__icon`. */
6089
+ .twm-tile-tabswitch__check,
6090
+ .twm-tile-tabswitch__check.material-symbols-outlined { font-size: 14px; color: var(--accent-bright, #1683c4); }
6042
6091
  .twm-tile-tabswitch__label {
6043
6092
  overflow: hidden;
6044
6093
  text-overflow: ellipsis;
@@ -6098,7 +6147,9 @@ body.twm-dragging * { user-select: none; }
6098
6147
  white-space: nowrap;
6099
6148
  user-select: none;
6100
6149
  }
6101
- .twm-cmdpal__type-chip-icon {
6150
+ /* Compounded for the Material Symbols tie — see `.twm-leaf__icon`. */
6151
+ .twm-cmdpal__type-chip-icon,
6152
+ .twm-cmdpal__type-chip-icon.material-symbols-outlined {
6102
6153
  font-size: 14px;
6103
6154
  }
6104
6155
  .twm-cmdpal__type-chip-label {
@@ -6181,7 +6232,9 @@ body.twm-dragging * { user-select: none; }
6181
6232
  background: var(--state-selected);
6182
6233
  color: var(--text-bright);
6183
6234
  }
6184
- .twm-cmdpal__icon { color: var(--text-muted); font-size: 16px; }
6235
+ /* Compounded for the Material Symbols tie — see `.twm-leaf__icon`. */
6236
+ .twm-cmdpal__icon,
6237
+ .twm-cmdpal__icon.material-symbols-outlined { color: var(--text-muted); font-size: 16px; }
6185
6238
  .twm-cmdpal__label { font-weight: var(--font-weight-medium); }
6186
6239
  .twm-cmdpal__kind,
6187
6240
  .twm-cmdpal__hint {
@@ -7002,7 +7055,9 @@ tr:hover .twm-row-action { opacity: 1; }
7002
7055
  color: var(--text-muted);
7003
7056
  text-align: center;
7004
7057
  }
7005
- .twm-window-placeholder__icon {
7058
+ /* Compounded for the Material Symbols tie — see `.twm-leaf__icon`. */
7059
+ .twm-window-placeholder__icon,
7060
+ .twm-window-placeholder__icon.material-symbols-outlined {
7006
7061
  font-size: 32px;
7007
7062
  opacity: 0.5;
7008
7063
  }
@@ -7081,7 +7136,9 @@ tr:hover .twm-row-action { opacity: 1; }
7081
7136
  background: #000;
7082
7137
  color: var(--text-bright);
7083
7138
  }
7084
- .twm-top-nav__icon {
7139
+ /* Compounded for the Material Symbols tie — see `.twm-leaf__icon`. */
7140
+ .twm-top-nav__icon,
7141
+ .twm-top-nav__icon.material-symbols-outlined {
7085
7142
  font-size: 16px;
7086
7143
  font-variation-settings: 'FILL' 0, 'wght' 400, 'GRAD' 0, 'opsz' 20;
7087
7144
  }
package/dist/tiles.js CHANGED
@@ -3,8 +3,8 @@ import {
3
3
  } from "./chunk-O5OHMWBB.js";
4
4
  import {
5
5
  openRawTracesWindow
6
- } from "./chunk-SWB5ARKN.js";
7
- import "./chunk-BIAOGX6K.js";
6
+ } from "./chunk-HWCX5I4R.js";
7
+ import "./chunk-CYTWMP2Y.js";
8
8
  import "./chunk-LH5TSOZW.js";
9
9
  import "./chunk-FL5KFNQH.js";
10
10
  import {
package/dist/widgets.js CHANGED
@@ -21,7 +21,7 @@ import {
21
21
  import {
22
22
  DataTable,
23
23
  createRafResizeObserver
24
- } from "./chunk-BIAOGX6K.js";
24
+ } from "./chunk-CYTWMP2Y.js";
25
25
  import {
26
26
  ManagedWindow
27
27
  } from "./chunk-LH5TSOZW.js";
package/dist/wm.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  import "./chunk-FL5KFNQH.js";
10
10
  import {
11
11
  NotebookTabBar
12
- } from "./chunk-QNQHQ24V.js";
12
+ } from "./chunk-NLTOY57M.js";
13
13
  import "./chunk-WVFGV5FT.js";
14
14
  import "./chunk-JYWURG5T.js";
15
15
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flexdesk",
3
- "version": "0.4.5",
3
+ "version": "0.4.6",
4
4
  "description": "A tiling window manager and widget set for desktop-class web applications. Zero runtime dependencies.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -61,6 +61,8 @@ export class NotebookTabBar {
61
61
  this.#container.className = 'tabs notebook-tabs';
62
62
  this.#boundHideContextMenu = () => this.#hideContextMenu();
63
63
  this.#installStripDnDHandlers();
64
+ // NOT PASSIVE: it has to be able to take the wheel from the page.
65
+ this.#container.addEventListener('wheel', this.#onWheel, { passive: false });
64
66
  this.#dragReorder = new DragReorder({
65
67
  container: this.#container,
66
68
  itemSelector: '.tab',
@@ -118,6 +120,9 @@ export class NotebookTabBar {
118
120
  dispose() {
119
121
  this.#hideContextMenu();
120
122
  this.#removeStripDnDHandlers();
123
+ this.#container?.removeEventListener('wheel', this.#onWheel);
124
+ if (this.#revealFrame) cancelAnimationFrame(this.#revealFrame);
125
+ this.#revealFrame = 0;
121
126
  this.#dragReorder?.destroy();
122
127
  this.#dragReorder = null;
123
128
  this.#container = null;
@@ -161,6 +166,11 @@ export class NotebookTabBar {
161
166
  #render() {
162
167
  if (!this.#container) return;
163
168
 
169
+ // THE STRIP KEEPS ITS SCROLL ACROSS A REBUILD. Emptying it shrinks its
170
+ // scroll width to nothing, which clamps `scrollLeft` to 0 — so every
171
+ // render (a tab activated, a title changed) threw a strip scrolled to
172
+ // its tenth tab back to its first.
173
+ const keepLeft = this.#container.scrollLeft;
164
174
  this.#container.innerHTML = '';
165
175
 
166
176
  for (const tab of this.#tabs) {
@@ -222,8 +232,87 @@ export class NotebookTabBar {
222
232
  }
223
233
  // Make the freshly-rendered tabs draggable-to-reorder.
224
234
  this.#dragReorder?.attach();
235
+
236
+ this.#container.scrollLeft = keepLeft;
237
+ // A tab that has just BECOME active — opened, switched to, moved here
238
+ // from another pane — is scrolled into the strip. Only then: a render
239
+ // for any other reason (a title, a dirty dot) leaves the strip where the
240
+ // reader put it, rather than yanking it back to the active tab.
241
+ const revealKey = `${this.#activeTab}\u0000${this.#tabs.length}`;
242
+ if (revealKey !== this.#revealedKey) {
243
+ this.#revealedKey = revealKey;
244
+ this.#revealActive(true);
245
+ }
225
246
  }
226
247
 
248
+ // ═════════════════════════════════════════════════════════════════════════
249
+ // Scrolling (1.5) — the strip scrolls rather than wrapping or shrinking its
250
+ // tabs, and hides its scrollbar (a bar in a 26px strip eats a third of every
251
+ // label). So it owes a mouse user every other way in: a plain wheel, the
252
+ // active tab brought into view, and — in `DragReorder._autoScrollEdge` —
253
+ // the edges scrolling while a tab is dragged toward them.
254
+ // ═════════════════════════════════════════════════════════════════════════
255
+
256
+ /** @type {string|null} the active tab + count last scrolled into view */
257
+ #revealedKey = null;
258
+
259
+ /** @type {number} a pending retry, for a strip not yet laid out */
260
+ #revealFrame = 0;
261
+
262
+ /** Bring the active tab fully into the strip — and ONLY the strip. Not
263
+ * `scrollIntoView`, which scrolls every scrollable ancestor as well and
264
+ * would drag the tile, the window or the page along with the tab. */
265
+ #revealActive(retry = false) {
266
+ const el = this.#container;
267
+ if (!el) return;
268
+ const active = el.querySelector(':scope > .tab.active');
269
+ if (!active) return;
270
+ if (!el.isConnected || el.clientWidth === 0) {
271
+ // Mounted but not laid out yet (the renderer updates a strip before
272
+ // it attaches it). Once, on the next frame — not a loop.
273
+ if (retry && !this.#revealFrame && typeof requestAnimationFrame === 'function') {
274
+ this.#revealFrame = requestAnimationFrame(() => {
275
+ this.#revealFrame = 0;
276
+ this.#revealActive(false);
277
+ });
278
+ }
279
+ return;
280
+ }
281
+ const box = el.getBoundingClientRect();
282
+ const tab = active.getBoundingClientRect();
283
+ // Client pixels to the strip's own: 1 unless something above it zooms.
284
+ const scale = (el.offsetWidth ? box.width / el.offsetWidth : 1) || 1;
285
+ const left = (tab.left - box.left) / scale + el.scrollLeft;
286
+ const right = left + tab.width / scale;
287
+ if (left < el.scrollLeft) el.scrollLeft = left;
288
+ else if (right > el.scrollLeft + el.clientWidth) el.scrollLeft = right - el.clientWidth;
289
+ }
290
+
291
+ /**
292
+ * A VERTICAL WHEEL SCROLLS THE STRIP SIDEWAYS. Chromium and Firefox scroll
293
+ * an element that overflows only horizontally with Shift+wheel or a
294
+ * touchpad, and not with the plain wheel most people have — so with the
295
+ * scrollbar hidden, every tab past the edge was unreachable with a mouse.
296
+ *
297
+ * The wheel is taken from the page only while the strip can actually move
298
+ * that way: at either end it is left alone, so a wheel over a strip that
299
+ * has nothing more to show still scrolls whatever is behind it. A gesture
300
+ * that is already horizontal (a touchpad, Shift+wheel) and Ctrl+wheel (zoom)
301
+ * are the browser's, untouched.
302
+ */
303
+ #onWheel = (e) => {
304
+ const el = this.#container;
305
+ if (!el || e.ctrlKey || e.shiftKey) return;
306
+ if (Math.abs(e.deltaX) >= Math.abs(e.deltaY)) return;
307
+ const room = el.scrollWidth - el.clientWidth;
308
+ if (room <= 0) return;
309
+ const unit = e.deltaMode === 1 ? 16 : e.deltaMode === 2 ? el.clientWidth : 1;
310
+ const next = Math.max(0, Math.min(room, el.scrollLeft + e.deltaY * unit));
311
+ if (Math.abs(next - el.scrollLeft) < 0.5) return;
312
+ e.preventDefault();
313
+ el.scrollLeft = next;
314
+ };
315
+
227
316
  // ═════════════════════════════════════════════════════════════════════════
228
317
  // Drag and drop reordering (same-pane + cross-pane)
229
318
  // ═════════════════════════════════════════════════════════════════════════