flexdesk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +195 -0
  3. package/THIRD_PARTY_LICENSES.md +53 -0
  4. package/css/base.css +3439 -0
  5. package/css/flexdesk.css +5797 -0
  6. package/css/overrides.css +2349 -0
  7. package/css/reset.css +46 -0
  8. package/css/tokens.css +137 -0
  9. package/dist/charts.js +780 -0
  10. package/dist/charts.js.map +7 -0
  11. package/dist/chunk-3PHCPZHT.js +222 -0
  12. package/dist/chunk-3PHCPZHT.js.map +7 -0
  13. package/dist/chunk-CT4YXXLP.js +1770 -0
  14. package/dist/chunk-CT4YXXLP.js.map +7 -0
  15. package/dist/chunk-DRYCDMEG.js +2217 -0
  16. package/dist/chunk-DRYCDMEG.js.map +7 -0
  17. package/dist/chunk-DVU44T77.js +855 -0
  18. package/dist/chunk-DVU44T77.js.map +7 -0
  19. package/dist/chunk-FL5KFNQH.js +640 -0
  20. package/dist/chunk-FL5KFNQH.js.map +7 -0
  21. package/dist/chunk-FOOS3T5L.js +117 -0
  22. package/dist/chunk-FOOS3T5L.js.map +7 -0
  23. package/dist/chunk-JYWURG5T.js +32 -0
  24. package/dist/chunk-JYWURG5T.js.map +7 -0
  25. package/dist/chunk-TLZUUFOE.js +276 -0
  26. package/dist/chunk-TLZUUFOE.js.map +7 -0
  27. package/dist/chunk-UCJ2WD4D.js +625 -0
  28. package/dist/chunk-UCJ2WD4D.js.map +7 -0
  29. package/dist/chunk-WVFGV5FT.js +205 -0
  30. package/dist/chunk-WVFGV5FT.js.map +7 -0
  31. package/dist/core.js +1098 -0
  32. package/dist/core.js.map +7 -0
  33. package/dist/editor.js +1661 -0
  34. package/dist/editor.js.map +7 -0
  35. package/dist/flexdesk.css +5797 -0
  36. package/dist/host.js +132 -0
  37. package/dist/host.js.map +7 -0
  38. package/dist/reset.css +46 -0
  39. package/dist/tile_registry-6WZPTUZV.js +28 -0
  40. package/dist/tile_registry-6WZPTUZV.js.map +7 -0
  41. package/dist/tiles.js +2068 -0
  42. package/dist/tiles.js.map +7 -0
  43. package/dist/tokens.css +137 -0
  44. package/dist/widgets.js +5438 -0
  45. package/dist/widgets.js.map +7 -0
  46. package/dist/wm.js +4830 -0
  47. package/dist/wm.js.map +7 -0
  48. package/package.json +80 -0
  49. package/src/charts/chart_types.js +699 -0
  50. package/src/charts/data_series_plot_window.js +761 -0
  51. package/src/charts/downsample.js +119 -0
  52. package/src/charts/plot_config.js +176 -0
  53. package/src/charts/plot_popout_window.js +1560 -0
  54. package/src/charts/plotly_wrapper.js +432 -0
  55. package/src/core/event_bus.js +230 -0
  56. package/src/core/logging.js +300 -0
  57. package/src/core/settings.js +813 -0
  58. package/src/core/state_guard.js +382 -0
  59. package/src/core/state_guard_config.js +309 -0
  60. package/src/core/state_machine.js +258 -0
  61. package/src/editor/editor_pane.js +264 -0
  62. package/src/editor/monaco_editor_factory.js +449 -0
  63. package/src/editor/monaco_loader.js +74 -0
  64. package/src/editor/notebook_search.js +460 -0
  65. package/src/editor/notebook_tab_bar.js +456 -0
  66. package/src/editor/notebook_undo_manager.js +108 -0
  67. package/src/editor/split_pane_container.js +197 -0
  68. package/src/help/help_modal.js +409 -0
  69. package/src/help/help_registry.js +65 -0
  70. package/src/host/host.js +69 -0
  71. package/src/host/pywebview_host.js +133 -0
  72. package/src/tiles/config_schema.js +256 -0
  73. package/src/tiles/layout_persistence.js +662 -0
  74. package/src/tiles/tile_base.js +785 -0
  75. package/src/tiles/tile_grid.js +905 -0
  76. package/src/tiles/tile_registry.js +136 -0
  77. package/src/tiling/command_palette.js +328 -0
  78. package/src/tiling/content_registry.js +110 -0
  79. package/src/tiling/desktops.js +133 -0
  80. package/src/tiling/entity_sources.js +179 -0
  81. package/src/tiling/keymap.js +156 -0
  82. package/src/tiling/kind_taxonomy.js +172 -0
  83. package/src/tiling/landing_table.js +717 -0
  84. package/src/tiling/loading_overlay.js +33 -0
  85. package/src/tiling/nav_panel.js +142 -0
  86. package/src/tiling/page_factory.js +215 -0
  87. package/src/tiling/panel_keys.js +145 -0
  88. package/src/tiling/shell.js +437 -0
  89. package/src/tiling/tile_breadcrumb.js +222 -0
  90. package/src/tiling/tile_renderer.js +459 -0
  91. package/src/tiling/tile_tab_menu.js +340 -0
  92. package/src/tiling/tile_tree.js +880 -0
  93. package/src/tiling/wm.js +1352 -0
  94. package/src/ui/base/component_base.js +30 -0
  95. package/src/ui/base/controller_base.js +48 -0
  96. package/src/ui/base/page_base.js +102 -0
  97. package/src/ui/components/about_dialog.js +224 -0
  98. package/src/ui/components/action_dropdown.js +389 -0
  99. package/src/ui/components/attribute_list_editor.js +652 -0
  100. package/src/ui/components/autocomplete_field.js +732 -0
  101. package/src/ui/components/computing_status_window.js +81 -0
  102. package/src/ui/components/confirm_dialog.js +307 -0
  103. package/src/ui/components/context_menu.js +87 -0
  104. package/src/ui/components/data_table.js +2228 -0
  105. package/src/ui/components/detail_header.js +177 -0
  106. package/src/ui/components/drag_reorder.js +0 -0
  107. package/src/ui/components/gallery_picker.js +147 -0
  108. package/src/ui/components/inline_renamer.js +129 -0
  109. package/src/ui/components/managed_window.js +804 -0
  110. package/src/ui/components/modal.js +512 -0
  111. package/src/ui/components/notification_history.js +264 -0
  112. package/src/ui/components/slide_out_panel.js +185 -0
  113. package/src/ui/components/slider_field.js +122 -0
  114. package/src/ui/components/sortable_list.js +309 -0
  115. package/src/ui/components/table_state_store.js +81 -0
  116. package/src/ui/components/toast.js +59 -0
  117. package/src/ui/components/tree_view.js +460 -0
  118. package/src/ui/components/window_taskbar.js +143 -0
  119. package/src/ui/controllers/panel_state_machine.js +634 -0
  120. package/src/ui/controllers/window_chrome_controller.js +659 -0
  121. package/src/ui/notification_center.js +576 -0
  122. package/src/ui/utils/overlay_scrollbar.js +492 -0
  123. package/src/ui/utils/raf_resize_observer.js +31 -0
  124. package/src/ui/utils/tooltip_service.js +406 -0
@@ -0,0 +1,460 @@
1
+ /**
2
+ * NotebookSearchBar — VS Code-style find/replace bar for notebook-level search.
3
+ *
4
+ * Searches across all cells using getSearchableText(), highlights matches
5
+ * via cell-specific applySearchHighlights(), and supports replace for
6
+ * Monaco-based cells (code, documentation).
7
+ *
8
+ * Mounted at the top of the notebook-editor-container (sticky positioned).
9
+ */
10
+
11
+ // ─── Match descriptor ────────────────────────────────────────────────────────
12
+
13
+ /**
14
+ * @typedef {{ cellId: string, indexInCell: number, charOffset: number }} SearchMatch
15
+ */
16
+
17
+ // ─── Component ───────────────────────────────────────────────────────────────
18
+
19
+ export class NotebookSearchBar {
20
+ /** @type {HTMLElement} parent container (.notebook-editor-container) */
21
+ #parent = null;
22
+ /** @type {HTMLElement} the search bar root element */
23
+ #el = null;
24
+ /** @type {HTMLInputElement} */
25
+ #searchInput = null;
26
+ /** @type {HTMLInputElement} */
27
+ #replaceInput = null;
28
+ /** @type {HTMLElement} match info label */
29
+ #matchInfo = null;
30
+
31
+ // State
32
+ #isOpen = false;
33
+ #showReplace = false;
34
+ #caseSensitive = false;
35
+ #wholeWord = false;
36
+ #useRegex = false;
37
+
38
+ // Match tracking
39
+ /** @type {SearchMatch[]} */
40
+ #matches = [];
41
+ #currentIndex = -1;
42
+
43
+ // Callbacks into NotebookEditor
44
+ #getCells = null; // () => [{ id, type }]
45
+ #getCellRenderer = null; // (id) => CellBase|null
46
+ #getCellElement = null; // (id) => HTMLElement|null
47
+ #scrollToCell = null; // (id) => void
48
+
49
+ /**
50
+ * @param {HTMLElement} parent — .notebook-editor-container
51
+ * @param {{ getCells, getCellRenderer, getCellElement, scrollToCell }} callbacks
52
+ */
53
+ constructor(parent, { getCells, getCellRenderer, getCellElement, scrollToCell }) {
54
+ this.#parent = parent;
55
+ this.#getCells = getCells;
56
+ this.#getCellRenderer = getCellRenderer;
57
+ this.#getCellElement = getCellElement;
58
+ this.#scrollToCell = scrollToCell;
59
+
60
+ this.#render();
61
+ }
62
+
63
+ get isOpen() { return this.#isOpen; }
64
+
65
+ // ─── Public API ──────────────────────────────────────────────────────────
66
+
67
+ open(showReplace = false) {
68
+ this.#isOpen = true;
69
+ this.#showReplace = showReplace;
70
+ this.#el.classList.add('nb-search-bar--open');
71
+ this.#el.querySelector('.nb-search-bar__replace-row').style.display =
72
+ showReplace ? '' : 'none';
73
+ this.#updateToggleIcon();
74
+
75
+ // Pre-fill with current selection text (if any)
76
+ const sel = window.getSelection()?.toString()?.trim();
77
+ if (sel && sel.length < 200 && !sel.includes('\n')) {
78
+ this.#searchInput.value = sel;
79
+ }
80
+
81
+ this.#searchInput.focus();
82
+ this.#searchInput.select();
83
+
84
+ if (this.#searchInput.value) this.#runSearch();
85
+ }
86
+
87
+ close() {
88
+ this.#isOpen = false;
89
+ this.#el.classList.remove('nb-search-bar--open');
90
+ this.#clearAllHighlights();
91
+ this.#matches = [];
92
+ this.#currentIndex = -1;
93
+ this.#updateMatchInfo();
94
+ }
95
+
96
+ dispose() {
97
+ this.#clearAllHighlights();
98
+ this.#el?.remove();
99
+ this.#el = null;
100
+ }
101
+
102
+ // ─── Rendering ───────────────────────────────────────────────────────────
103
+
104
+ #render() {
105
+ const el = document.createElement('div');
106
+ el.className = 'nb-search-bar';
107
+ el.innerHTML = `
108
+ <div class="nb-search-bar__find-row">
109
+ <button class="nb-search-bar__btn nb-search-bar__toggle-replace" title="Toggle Replace">
110
+ <span class="material-symbols-outlined">chevron_right</span>
111
+ </button>
112
+ <div class="nb-search-bar__input-wrap">
113
+ <input class="nb-search-bar__input nb-search-bar__find-input"
114
+ type="text" placeholder="Find" spellcheck="false" />
115
+ </div>
116
+ <span class="nb-search-bar__match-info">No results</span>
117
+ <button class="nb-search-bar__btn" data-action="prev" title="Previous Match (Shift+Enter)">
118
+ <span class="material-symbols-outlined">arrow_upward</span>
119
+ </button>
120
+ <button class="nb-search-bar__btn" data-action="next" title="Next Match (Enter)">
121
+ <span class="material-symbols-outlined">arrow_downward</span>
122
+ </button>
123
+ <div class="nb-search-bar__separator"></div>
124
+ <button class="nb-search-bar__btn nb-search-bar__btn--toggle" data-action="case"
125
+ title="Match Case (Alt+C)">Aa</button>
126
+ <button class="nb-search-bar__btn nb-search-bar__btn--toggle" data-action="word"
127
+ title="Match Whole Word (Alt+W)">
128
+ <span class="nb-search-bar__icon-word">ab</span>
129
+ </button>
130
+ <button class="nb-search-bar__btn nb-search-bar__btn--toggle" data-action="regex"
131
+ title="Use Regular Expression (Alt+R)">.*</button>
132
+ <div class="nb-search-bar__separator"></div>
133
+ <button class="nb-search-bar__btn" data-action="close" title="Close (Escape)">
134
+ <span class="material-symbols-outlined">close</span>
135
+ </button>
136
+ </div>
137
+ <div class="nb-search-bar__replace-row" style="display:none">
138
+ <div class="nb-search-bar__spacer"></div>
139
+ <div class="nb-search-bar__input-wrap">
140
+ <input class="nb-search-bar__input nb-search-bar__replace-input"
141
+ type="text" placeholder="Replace" spellcheck="false" />
142
+ </div>
143
+ <button class="nb-search-bar__btn" data-action="replace" title="Replace (Ctrl+Shift+1)">
144
+ <span class="material-symbols-outlined">find_replace</span>
145
+ </button>
146
+ <button class="nb-search-bar__btn" data-action="replace-all" title="Replace All (Ctrl+Alt+Enter)">
147
+ <span class="material-symbols-outlined">swap_horiz</span>
148
+ </button>
149
+ </div>
150
+ `;
151
+
152
+ this.#el = el;
153
+ this.#searchInput = el.querySelector('.nb-search-bar__find-input');
154
+ this.#replaceInput = el.querySelector('.nb-search-bar__replace-input');
155
+ this.#matchInfo = el.querySelector('.nb-search-bar__match-info');
156
+
157
+ this.#bindEvents();
158
+ this.#parent.prepend(el);
159
+ }
160
+
161
+ #bindEvents() {
162
+ // Search input
163
+ this.#searchInput.addEventListener('input', () => this.#runSearch());
164
+
165
+ this.#searchInput.addEventListener('keydown', (e) => {
166
+ if (e.key === 'Enter' && !e.shiftKey) {
167
+ e.preventDefault();
168
+ this.#navigateNext();
169
+ } else if (e.key === 'Enter' && e.shiftKey) {
170
+ e.preventDefault();
171
+ this.#navigatePrev();
172
+ } else if (e.key === 'Escape') {
173
+ e.preventDefault();
174
+ this.close();
175
+ }
176
+ });
177
+
178
+ // Replace input
179
+ this.#replaceInput.addEventListener('keydown', (e) => {
180
+ if (e.key === 'Enter' && !e.shiftKey) {
181
+ e.preventDefault();
182
+ this.#doReplace();
183
+ } else if (e.key === 'Escape') {
184
+ e.preventDefault();
185
+ this.close();
186
+ }
187
+ });
188
+
189
+ // Buttons
190
+ this.#el.addEventListener('click', (e) => {
191
+ const btn = e.target.closest('[data-action]');
192
+ if (!btn) return;
193
+ switch (btn.dataset.action) {
194
+ case 'prev': this.#navigatePrev(); break;
195
+ case 'next': this.#navigateNext(); break;
196
+ case 'close': this.close(); break;
197
+ case 'replace': this.#doReplace(); break;
198
+ case 'replace-all': this.#doReplaceAll(); break;
199
+ case 'case':
200
+ this.#caseSensitive = !this.#caseSensitive;
201
+ btn.classList.toggle('nb-search-bar__btn--active', this.#caseSensitive);
202
+ this.#runSearch();
203
+ break;
204
+ case 'word':
205
+ this.#wholeWord = !this.#wholeWord;
206
+ btn.classList.toggle('nb-search-bar__btn--active', this.#wholeWord);
207
+ this.#runSearch();
208
+ break;
209
+ case 'regex':
210
+ this.#useRegex = !this.#useRegex;
211
+ btn.classList.toggle('nb-search-bar__btn--active', this.#useRegex);
212
+ this.#runSearch();
213
+ break;
214
+ }
215
+ });
216
+
217
+ // Toggle replace row
218
+ this.#el.querySelector('.nb-search-bar__toggle-replace').addEventListener('click', () => {
219
+ this.#showReplace = !this.#showReplace;
220
+ this.#el.querySelector('.nb-search-bar__replace-row').style.display =
221
+ this.#showReplace ? '' : 'none';
222
+ this.#updateToggleIcon();
223
+ });
224
+
225
+ // Alt shortcuts for toggles
226
+ this.#el.addEventListener('keydown', (e) => {
227
+ if (e.altKey && e.key === 'c') {
228
+ e.preventDefault();
229
+ this.#caseSensitive = !this.#caseSensitive;
230
+ this.#el.querySelector('[data-action="case"]').classList.toggle('nb-search-bar__btn--active', this.#caseSensitive);
231
+ this.#runSearch();
232
+ } else if (e.altKey && e.key === 'w') {
233
+ e.preventDefault();
234
+ this.#wholeWord = !this.#wholeWord;
235
+ this.#el.querySelector('[data-action="word"]').classList.toggle('nb-search-bar__btn--active', this.#wholeWord);
236
+ this.#runSearch();
237
+ } else if (e.altKey && e.key === 'r') {
238
+ e.preventDefault();
239
+ this.#useRegex = !this.#useRegex;
240
+ this.#el.querySelector('[data-action="regex"]').classList.toggle('nb-search-bar__btn--active', this.#useRegex);
241
+ this.#runSearch();
242
+ }
243
+ });
244
+
245
+ // Prevent keydown from bubbling to NotebookEditor (avoids undo/redo interference)
246
+ // Also suppress browser Ctrl+F/Ctrl+H when the search bar already has focus
247
+ this.#el.addEventListener('keydown', (e) => {
248
+ e.stopPropagation();
249
+ if (e.ctrlKey && (e.key === 'f' || e.key === 'h')) {
250
+ e.preventDefault();
251
+ if (e.key === 'h' && !this.#showReplace) {
252
+ this.#showReplace = true;
253
+ this.#el.querySelector('.nb-search-bar__replace-row').style.display = '';
254
+ this.#updateToggleIcon();
255
+ }
256
+ }
257
+ });
258
+ }
259
+
260
+ #updateToggleIcon() {
261
+ const icon = this.#el.querySelector('.nb-search-bar__toggle-replace .material-symbols-outlined');
262
+ if (icon) icon.textContent = this.#showReplace ? 'expand_more' : 'chevron_right';
263
+ }
264
+
265
+ #updateMatchInfo() {
266
+ if (!this.#matchInfo) return;
267
+ if (this.#matches.length === 0) {
268
+ const hasQuery = this.#searchInput?.value?.length > 0;
269
+ this.#matchInfo.textContent = hasQuery ? 'No results' : '';
270
+ this.#matchInfo.classList.toggle('nb-search-bar__match-info--no-results', hasQuery);
271
+ } else {
272
+ this.#matchInfo.textContent = `${this.#currentIndex + 1} of ${this.#matches.length}`;
273
+ this.#matchInfo.classList.remove('nb-search-bar__match-info--no-results');
274
+ }
275
+ }
276
+
277
+ // ─── Search logic ────────────────────────────────────────────────────────
278
+
279
+ #buildQuery() {
280
+ const raw = this.#searchInput.value;
281
+ if (!raw) return null;
282
+
283
+ let pattern;
284
+ if (this.#useRegex) {
285
+ try { pattern = raw; new RegExp(pattern); }
286
+ catch { return null; } // invalid regex
287
+ } else {
288
+ pattern = raw.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
289
+ }
290
+
291
+ if (this.#wholeWord) pattern = `\\b${pattern}\\b`;
292
+
293
+ const flags = 'g' + (this.#caseSensitive ? '' : 'i');
294
+ try { return new RegExp(pattern, flags); }
295
+ catch { return null; }
296
+ }
297
+
298
+ #runSearch() {
299
+ this.#clearAllHighlights();
300
+ this.#matches = [];
301
+ this.#currentIndex = -1;
302
+
303
+ const query = this.#buildQuery();
304
+ if (!query) {
305
+ this.#updateMatchInfo();
306
+ return;
307
+ }
308
+
309
+ const cells = this.#getCells();
310
+
311
+ for (const { id } of cells) {
312
+ const renderer = this.#getCellRenderer(id);
313
+ if (!renderer) continue;
314
+
315
+ const text = renderer.getSearchableText();
316
+ if (!text) continue;
317
+
318
+ query.lastIndex = 0;
319
+ let m;
320
+ let indexInCell = 0;
321
+ while ((m = query.exec(text)) !== null) {
322
+ this.#matches.push({ cellId: id, indexInCell, charOffset: m.index });
323
+ indexInCell++;
324
+ if (!query.global) break;
325
+ }
326
+ }
327
+
328
+ if (this.#matches.length > 0) {
329
+ this.#currentIndex = 0;
330
+ this.#applyHighlights();
331
+ }
332
+
333
+ this.#updateMatchInfo();
334
+ }
335
+
336
+ // ─── Navigation ──────────────────────────────────────────────────────────
337
+
338
+ #navigateNext() {
339
+ if (this.#matches.length === 0) return;
340
+ this.#currentIndex = (this.#currentIndex + 1) % this.#matches.length;
341
+ this.#applyHighlights();
342
+ this.#scrollToCurrentMatch();
343
+ this.#updateMatchInfo();
344
+ }
345
+
346
+ #navigatePrev() {
347
+ if (this.#matches.length === 0) return;
348
+ this.#currentIndex = (this.#currentIndex - 1 + this.#matches.length) % this.#matches.length;
349
+ this.#applyHighlights();
350
+ this.#scrollToCurrentMatch();
351
+ this.#updateMatchInfo();
352
+ }
353
+
354
+ #scrollToCurrentMatch() {
355
+ const match = this.#matches[this.#currentIndex];
356
+ if (!match) return;
357
+ this.#scrollToCell(match.cellId);
358
+
359
+ // Also scroll the specific highlight into view if possible
360
+ requestAnimationFrame(() => {
361
+ const cellEl = this.#getCellElement(match.cellId);
362
+ if (!cellEl) return;
363
+ const active = cellEl.querySelector('.nb-search-match--active');
364
+ if (active) active.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
365
+ });
366
+ }
367
+
368
+ // ─── Highlighting ────────────────────────────────────────────────────────
369
+
370
+ #applyHighlights() {
371
+ const query = this.#buildQuery();
372
+ if (!query) return;
373
+
374
+ // Group matches by cell
375
+ const matchesByCell = new Map();
376
+ for (const m of this.#matches) {
377
+ if (!matchesByCell.has(m.cellId)) matchesByCell.set(m.cellId, []);
378
+ matchesByCell.get(m.cellId).push(m);
379
+ }
380
+
381
+ const currentMatch = this.#matches[this.#currentIndex];
382
+
383
+ // Apply highlights to each cell that has matches
384
+ const cells = this.#getCells();
385
+ for (const { id } of cells) {
386
+ const renderer = this.#getCellRenderer(id);
387
+ if (!renderer) continue;
388
+
389
+ const cellEl = this.#getCellElement(id);
390
+
391
+ if (matchesByCell.has(id)) {
392
+ const activeOffset = (currentMatch?.cellId === id)
393
+ ? currentMatch.charOffset
394
+ : -1;
395
+ renderer.applySearchHighlights(query, activeOffset);
396
+ cellEl?.classList.add('notebook-cell--search-match');
397
+ cellEl?.classList.toggle('notebook-cell--search-active', currentMatch?.cellId === id);
398
+ } else {
399
+ renderer.clearSearchHighlights();
400
+ cellEl?.classList.remove('notebook-cell--search-match', 'notebook-cell--search-active');
401
+ }
402
+ }
403
+ }
404
+
405
+ #clearAllHighlights() {
406
+ const cells = this.#getCells();
407
+ for (const { id } of cells) {
408
+ const renderer = this.#getCellRenderer(id);
409
+ renderer?.clearSearchHighlights();
410
+ const cellEl = this.#getCellElement(id);
411
+ cellEl?.classList.remove('notebook-cell--search-match', 'notebook-cell--search-active');
412
+ }
413
+ }
414
+
415
+ // ─── Replace ─────────────────────────────────────────────────────────────
416
+
417
+ #doReplace() {
418
+ if (this.#matches.length === 0 || this.#currentIndex < 0) return;
419
+
420
+ const match = this.#matches[this.#currentIndex];
421
+ const renderer = this.#getCellRenderer(match.cellId);
422
+ if (!renderer) return;
423
+
424
+ const query = this.#buildQuery();
425
+ if (!query) return;
426
+
427
+ const replacement = this.#replaceInput.value;
428
+ const success = renderer.replaceMatch(query, match.indexInCell, replacement);
429
+
430
+ if (success) {
431
+ // Re-run search (match count changed), try to stay near same position
432
+ const wasIndex = this.#currentIndex;
433
+ this.#runSearch();
434
+ if (this.#matches.length > 0) {
435
+ this.#currentIndex = Math.min(wasIndex, this.#matches.length - 1);
436
+ this.#applyHighlights();
437
+ this.#scrollToCurrentMatch();
438
+ this.#updateMatchInfo();
439
+ }
440
+ }
441
+ }
442
+
443
+ #doReplaceAll() {
444
+ const query = this.#buildQuery();
445
+ if (!query || this.#matches.length === 0) return;
446
+
447
+ const replacement = this.#replaceInput.value;
448
+
449
+ // Collect unique cell IDs that have matches
450
+ const cellIds = [...new Set(this.#matches.map(m => m.cellId))];
451
+ let totalReplaced = 0;
452
+
453
+ for (const cellId of cellIds) {
454
+ const renderer = this.#getCellRenderer(cellId);
455
+ if (renderer) totalReplaced += renderer.replaceAll(query, replacement);
456
+ }
457
+
458
+ if (totalReplaced > 0) this.#runSearch();
459
+ }
460
+ }