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,449 @@
1
+ /**
2
+ * Monaco Editor Factory
3
+ *
4
+ * Creates and manages Monaco editor instances for cells.
5
+ * Handles:
6
+ * - Creating full editors (multi-line, for CodeCells)
7
+ * - Creating single-line inline editors (for ParameterCell values, expression fields)
8
+ * - Theme registration (ecosim-dark, ecosim-light)
9
+ * - Shared editor options
10
+ * - Instance lifecycle (dispose on cell removal)
11
+ */
12
+
13
+ import { monacoReady, initMonaco } from './monaco_loader.js';
14
+ // The default language is the CONSUMER's. This imported ECOLANG_ID — EcoAgent's
15
+ // expression language — so a generic Monaco factory shipped knowing the name of one
16
+ // application's DSL. Register your language with Monaco and name it here.
17
+ let _defaultLanguage = 'plaintext';
18
+
19
+ /** @param {string} id A language id already registered with Monaco. */
20
+ export function setDefaultLanguage(id) {
21
+ if (typeof id === 'string' && id) _defaultLanguage = id;
22
+ }
23
+
24
+ /** Dispose a Monaco editor, swallowing the benign "Canceled" error from pending async ops. */
25
+ function safeDispose(editor) {
26
+ try { editor.dispose(); } catch (e) { if (e?.message !== 'Canceled') throw e; }
27
+ }
28
+
29
+ // ─── Theme definitions ─────────────────────────────────────────────────────────
30
+
31
+ const ECOSIM_DARK_THEME = {
32
+ base: 'vs-dark',
33
+ inherit: true,
34
+ rules: [
35
+ { token: 'comment', foreground: '6a9955', fontStyle: 'italic' },
36
+ { token: 'keyword', foreground: '569cd6', fontStyle: 'bold' },
37
+ { token: 'keyword.directive', foreground: 'c586c0', fontStyle: 'bold' },
38
+ { token: 'keyword.operator', foreground: '569cd6' },
39
+ { token: 'constant', foreground: 'c586c0' },
40
+ { token: 'number', foreground: 'b5cea8' },
41
+ { token: 'string', foreground: 'ce9178' },
42
+ { token: 'type.identifier', foreground: '4ec9b0', fontStyle: 'bold' },
43
+ { token: 'support.function', foreground: 'a6e22e' },
44
+ { token: 'identifier', foreground: '4fc1ff' },
45
+ { token: 'operator', foreground: 'd4d4d4' },
46
+ { token: 'delimiter', foreground: '808080' },
47
+ ],
48
+ colors: {
49
+ // Layer-2 input well — almost black, matches the EcoAgent
50
+ // three-layer contrast pattern (chrome #1a1a1a, content
51
+ // #212121, input/code #0a0a0a). Makes editable surfaces
52
+ // visually recess from the surrounding cards.
53
+ 'editor.background': '#0a0a0a',
54
+ 'editor.foreground': '#e0e0e0',
55
+ 'editor.lineHighlightBackground': '#161616',
56
+ 'editor.selectionBackground': '#264f78',
57
+ 'editor.inactiveSelectionBackground': '#1f3350',
58
+ 'editorLineNumber.foreground': '#5a5a5a',
59
+ 'editorLineNumber.activeForeground': '#c6c6c6',
60
+ 'editorCursor.foreground': '#39ff14',
61
+ 'editor.findMatchBackground': '#9e6a03',
62
+ 'editor.findMatchHighlightBackground': '#f2cc6030',
63
+ 'editorIndentGuide.background1': '#2e2e2e',
64
+ 'editorIndentGuide.activeBackground1': '#5a5a5a',
65
+ 'editorGutter.background': '#0a0a0a',
66
+ // Pop-ups stay slightly lighter so they read as floating
67
+ // surfaces above the editor body, not part of it.
68
+ 'editorWidget.background': '#1a1a1a',
69
+ 'editorSuggestWidget.background': '#1a1a1a',
70
+ 'editorSuggestWidget.border': '#383838',
71
+ 'editorSuggestWidget.selectedBackground': '#094771',
72
+ 'scrollbarSlider.background': '#4a4a4d40',
73
+ 'scrollbarSlider.hoverBackground': '#4a4a4d70',
74
+ },
75
+ };
76
+
77
+ const ECOSIM_LIGHT_THEME = {
78
+ base: 'vs',
79
+ inherit: true,
80
+ rules: [
81
+ { token: 'comment', foreground: '6a737d', fontStyle: 'italic' },
82
+ { token: 'keyword', foreground: 'd73a49', fontStyle: 'bold' },
83
+ { token: 'keyword.directive', foreground: '6f42c1', fontStyle: 'bold' },
84
+ { token: 'keyword.operator', foreground: 'd73a49' },
85
+ { token: 'constant', foreground: 'e36209' },
86
+ { token: 'number', foreground: '005cc5' },
87
+ { token: 'string', foreground: '22863a' },
88
+ { token: 'type.identifier', foreground: '005cc5', fontStyle: 'bold' },
89
+ { token: 'support.function', foreground: '6f42c1' },
90
+ { token: 'identifier', foreground: '24292e' },
91
+ { token: 'operator', foreground: 'd73a49' },
92
+ { token: 'delimiter', foreground: '586069' },
93
+ ],
94
+ colors: {
95
+ 'editor.background': '#ffffff',
96
+ 'editor.foreground': '#24292e',
97
+ 'editor.lineHighlightBackground': '#f6f8fa',
98
+ 'editor.selectionBackground': '#c8d3e6',
99
+ 'editorLineNumber.foreground': '#babbbd',
100
+ 'editorLineNumber.activeForeground': '#586069',
101
+ 'editorCursor.foreground': '#044289',
102
+ 'editorGutter.background': '#ffffff',
103
+ 'editorWidget.background': '#f6f8fa',
104
+ 'editorSuggestWidget.background': '#f6f8fa',
105
+ 'editorSuggestWidget.border': '#e1e4e8',
106
+ 'scrollbarSlider.background': '#babbbd40',
107
+ 'scrollbarSlider.hoverBackground': '#babbbd70',
108
+ },
109
+ };
110
+
111
+ let _themesRegistered = false;
112
+
113
+ function ensureThemes(monaco) {
114
+ if (_themesRegistered) return;
115
+ _themesRegistered = true;
116
+ monaco.editor.defineTheme('ecosim-dark', ECOSIM_DARK_THEME);
117
+ monaco.editor.defineTheme('ecosim-light', ECOSIM_LIGHT_THEME);
118
+ }
119
+
120
+ // ─── Shared editor options ─────────────────────────────────────────────────────
121
+
122
+ function baseOptions(overrides = {}) {
123
+ return {
124
+ language: _defaultLanguage,
125
+ theme: 'ecosim-dark',
126
+ fontFamily: 'Consolas, "Courier New", monospace',
127
+ fontSize: 13,
128
+ lineHeight: 19.5,
129
+ tabSize: 4,
130
+ insertSpaces: true,
131
+ automaticLayout: true,
132
+ minimap: { enabled: false },
133
+ scrollBeyondLastLine: false,
134
+ renderLineHighlight: 'gutter',
135
+ matchBrackets: 'always',
136
+ padding: { top: 8, bottom: 8 },
137
+ overviewRulerLanes: 0,
138
+ hideCursorInOverviewRuler: true,
139
+ overviewRulerBorder: false,
140
+ scrollbar: { handleMouseWheel: false },
141
+ folding: true,
142
+ lineDecorationsWidth: 0,
143
+ // Reserve a couple extra gutter columns so the right-aligned line
144
+ // numbers get some breathing room on the left instead of sitting flush
145
+ // against the editor edge (digits stay right-aligned; the slack lands
146
+ // on the left). Covers up to 5-digit files too.
147
+ lineNumbersMinChars: 5,
148
+ suggest: {
149
+ showKeywords: true,
150
+ showFunctions: true,
151
+ showVariables: true,
152
+ showConstants: true,
153
+ showModules: true,
154
+ snippetsPreventQuickSuggestions: false,
155
+ },
156
+ quickSuggestions: { other: true, comments: false, strings: false },
157
+ parameterHints: { enabled: true },
158
+ wordWrap: 'off',
159
+ fixedOverflowWidgets: true,
160
+ ...overrides,
161
+ };
162
+ }
163
+
164
+ function singleLineOptions(overrides = {}) {
165
+ return baseOptions({
166
+ lineNumbers: 'off',
167
+ lineDecorationsWidth: 0,
168
+ lineNumbersMinChars: 0,
169
+ glyphMargin: false,
170
+ folding: false,
171
+ renderLineHighlight: 'none',
172
+ scrollbar: { vertical: 'hidden', horizontal: 'hidden', handleMouseWheel: false },
173
+ wordWrap: 'off',
174
+ overviewRulerLanes: 0,
175
+ padding: { top: 3, bottom: 3 },
176
+ ...overrides,
177
+ });
178
+ }
179
+
180
+ // ─── Factory ───────────────────────────────────────────────────────────────────
181
+
182
+ /**
183
+ * @typedef {{ editor: object, dispose: () => void, getValue: () => string, setValue: (v: string) => void, onDidChange: (cb: () => void) => object }} EditorHandle
184
+ */
185
+
186
+ export class MonacoEditorFactory {
187
+ #monaco = null;
188
+ #currentTheme = 'ecosim-dark';
189
+
190
+ constructor() {}
191
+
192
+ async init() {
193
+ this.#monaco = await monacoReady;
194
+ ensureThemes(this.#monaco);
195
+ return this;
196
+ }
197
+
198
+ get monaco() { return this.#monaco; }
199
+
200
+ /**
201
+ * Set global theme for all editors.
202
+ * @param {'ecosim-dark'|'ecosim-light'} theme
203
+ */
204
+ setTheme(theme) {
205
+ this.#currentTheme = theme;
206
+ this.#monaco?.editor.setTheme(theme);
207
+ }
208
+
209
+ /**
210
+ * Create a full multi-line editor (for CodeCell).
211
+ *
212
+ * @param {HTMLElement} container
213
+ * @param {string} initialValue
214
+ * @param {{ language?: string, readOnly?: boolean, lineNumbers?: string } & object} [overrides]
215
+ * @returns {EditorHandle}
216
+ */
217
+ createEditor(container, initialValue = '', overrides = {}) {
218
+ if (!this.#monaco) throw new Error('MonacoEditorFactory not initialized');
219
+
220
+ const { noAutoHeight, ...editorOverrides } = overrides;
221
+
222
+ const editor = this.#monaco.editor.create(container, {
223
+ ...baseOptions({ theme: this.#currentTheme }),
224
+ value: initialValue,
225
+ ...editorOverrides,
226
+ });
227
+
228
+ // Auto-resize to content height.
229
+ // When the "limit code cell height" setting is active (data-code-max-height="true"
230
+ // on the editor container), cap at 400px so Monaco handles internal scrolling.
231
+ // Otherwise, grow to fit all content with no upper bound.
232
+ //
233
+ // Monaco's handleMouseWheel is toggled dynamically: disabled when the editor
234
+ // auto-sizes to fit content (so wheel events propagate to the notebook scroller),
235
+ // enabled only when content is capped and there is actual internal overflow.
236
+ //
237
+ // When noAutoHeight is true, skip auto-sizing entirely and let the container
238
+ // control dimensions (e.g. flex layout). Mouse wheel scrolling stays enabled.
239
+ let minHeight = 80;
240
+ let heightDisposable = null;
241
+ let updateHeight = null;
242
+ if (noAutoHeight) {
243
+ editor.updateOptions({ scrollbar: { handleMouseWheel: true } });
244
+ } else {
245
+ updateHeight = () => {
246
+ const maxHeightEnabled = container.closest('.notebook-editor-container')?.dataset.codeMaxHeight === 'true';
247
+ const maxH = maxHeightEnabled ? 400 : Infinity;
248
+ const rawContent = editor.getContentHeight();
249
+ const contentHeight = Math.min(maxH, Math.max(minHeight, rawContent));
250
+ container.style.height = `${contentHeight}px`;
251
+ editor.layout();
252
+
253
+ // Enable Monaco wheel handling only when content overflows the capped height
254
+ const needsInternalScroll = maxHeightEnabled && rawContent > maxH;
255
+ editor.updateOptions({ scrollbar: { handleMouseWheel: needsInternalScroll } });
256
+ };
257
+ heightDisposable = editor.onDidContentSizeChange(updateHeight);
258
+ updateHeight();
259
+ }
260
+
261
+ // Re-evaluate height when the code-max-height setting changes on the ancestor
262
+ const editorContainer = container.closest('.notebook-editor-container');
263
+ let maxHeightObserver = null;
264
+ if (updateHeight && editorContainer) {
265
+ maxHeightObserver = new MutationObserver(() => updateHeight());
266
+ maxHeightObserver.observe(editorContainer, { attributes: true, attributeFilter: ['data-code-max-height'] });
267
+ }
268
+
269
+ // Scroll the find widget into view when it becomes visible.
270
+ // The editor auto-sizes to content (no internal scroll), so the notebook
271
+ // container scrolls instead. The find widget sits at top:0 of the editor,
272
+ // which may be above the viewport for tall cells.
273
+ let findWidgetObserver = null;
274
+ const observeFindWidget = () => {
275
+ const overflowGuard = container.querySelector('.overflow-guard');
276
+ if (!overflowGuard) return;
277
+
278
+ findWidgetObserver = new MutationObserver(() => {
279
+ const fw = container.querySelector('.find-widget.visible');
280
+ if (fw) fw.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
281
+ });
282
+ findWidgetObserver.observe(overflowGuard, {
283
+ subtree: true,
284
+ attributes: true,
285
+ attributeFilter: ['class'],
286
+ });
287
+ };
288
+ // Defer slightly — Monaco builds DOM asynchronously after create()
289
+ requestAnimationFrame(observeFindWidget);
290
+
291
+ return {
292
+ editor,
293
+ getValue: () => editor.getValue(),
294
+ setValue: (v) => { if (editor.getValue() !== v) editor.setValue(v); },
295
+ onDidChange: (cb) => editor.onDidChangeModelContent(cb),
296
+ focus: () => editor.focus(),
297
+ layout: () => editor.layout(),
298
+ setMinHeight: (h) => { minHeight = h; },
299
+ updateHeight,
300
+ dispose: () => {
301
+ findWidgetObserver?.disconnect();
302
+ maxHeightObserver?.disconnect();
303
+ heightDisposable?.dispose();
304
+ safeDispose(editor);
305
+ },
306
+ };
307
+ }
308
+
309
+ /**
310
+ * Create a single-line inline editor (for ParameterCell value, expression fields).
311
+ *
312
+ * @param {HTMLElement} container
313
+ * @param {string} initialValue
314
+ * @param {{ language?: string, placeholder?: string } & object} [overrides]
315
+ * @returns {EditorHandle}
316
+ */
317
+ createInlineEditor(container, initialValue = '', overrides = {}) {
318
+ if (!this.#monaco) throw new Error('MonacoEditorFactory not initialized');
319
+
320
+ container.style.height = '26px';
321
+
322
+ const editor = this.#monaco.editor.create(container, {
323
+ ...singleLineOptions({ theme: this.#currentTheme }),
324
+ value: initialValue,
325
+ ...overrides,
326
+ });
327
+
328
+ // Prevent newlines in single-line mode
329
+ editor.onKeyDown((e) => {
330
+ if (e.keyCode === this.#monaco.KeyCode.Enter) {
331
+ e.preventDefault();
332
+ e.stopPropagation();
333
+ }
334
+ });
335
+
336
+ return {
337
+ editor,
338
+ getValue: () => editor.getValue(),
339
+ setValue: (v) => { if (editor.getValue() !== v) editor.setValue(v); },
340
+ onDidChange: (cb) => editor.onDidChangeModelContent(cb),
341
+ focus: () => editor.focus(),
342
+ layout: () => editor.layout(),
343
+ dispose: () => safeDispose(editor),
344
+ };
345
+ }
346
+
347
+ /**
348
+ * Create a plain text editor (for documentation cells with markdown).
349
+ * Uses VS Code's built-in markdown language, no EcoLang features.
350
+ *
351
+ * @param {HTMLElement} container
352
+ * @param {string} initialValue
353
+ * @returns {EditorHandle}
354
+ */
355
+ createMarkdownEditor(container, initialValue = '') {
356
+ return this.createEditor(container, initialValue, {
357
+ language: 'markdown',
358
+ wordWrap: 'on',
359
+ lineNumbers: 'off',
360
+ folding: false,
361
+ quickSuggestions: false,
362
+ });
363
+ }
364
+
365
+ /**
366
+ * Create a read-only DSL viewer (shown in the "DSL" tab of a Godley/parameter cell).
367
+ *
368
+ * @param {HTMLElement} container
369
+ * @param {string} dslContent
370
+ * @returns {EditorHandle}
371
+ */
372
+ createDslViewer(container, dslContent = '') {
373
+ return this.createEditor(container, dslContent, {
374
+ readOnly: true,
375
+ lineNumbers: 'off',
376
+ glyphMargin: false,
377
+ folding: false,
378
+ });
379
+ }
380
+
381
+ /**
382
+ * Create a side-by-side diff editor for comparing two versions of a file.
383
+ *
384
+ * @param {HTMLElement} container
385
+ * @param {string} originalContent - Left side (e.g. HEAD version)
386
+ * @param {string} modifiedContent - Right side (e.g. working copy)
387
+ * @param {{ language?: string, readOnly?: boolean }} [options]
388
+ * @returns {{ editor: object, dispose: Function, setModels: Function }}
389
+ */
390
+ createDiffEditor(container, originalContent = '', modifiedContent = '', options = {}) {
391
+ const monaco = this.#monaco;
392
+ const language = options.language || 'plaintext';
393
+
394
+ const originalModel = monaco.editor.createModel(originalContent, language);
395
+ const modifiedModel = monaco.editor.createModel(modifiedContent, language);
396
+
397
+ const diffEditor = monaco.editor.createDiffEditor(container, {
398
+ theme: this.#currentTheme,
399
+ readOnly: options.readOnly !== false,
400
+ renderSideBySide: true,
401
+ automaticLayout: true,
402
+ minimap: { enabled: false },
403
+ scrollBeyondLastLine: false,
404
+ lineNumbers: 'on',
405
+ glyphMargin: false,
406
+ folding: true,
407
+ renderOverviewRuler: false,
408
+ enableSplitViewResizing: true,
409
+ });
410
+
411
+ diffEditor.setModel({ original: originalModel, modified: modifiedModel });
412
+
413
+ return {
414
+ editor: diffEditor,
415
+ setModels(original, modified, lang) {
416
+ originalModel.dispose();
417
+ modifiedModel.dispose();
418
+ const newOrig = monaco.editor.createModel(original, lang || language);
419
+ const newMod = monaco.editor.createModel(modified, lang || language);
420
+ diffEditor.setModel({ original: newOrig, modified: newMod });
421
+ },
422
+ dispose() {
423
+ safeDispose(diffEditor);
424
+ safeDispose(originalModel);
425
+ safeDispose(modifiedModel);
426
+ },
427
+ };
428
+ }
429
+ }
430
+
431
+ /** Singleton instance, initialized lazily. */
432
+ let _factoryInstance = null;
433
+
434
+ // Where the app keeps Monaco. Set once, at boot.
435
+ let _monacoBasePath = null;
436
+
437
+ /** @param {string} basePath */
438
+ export function setMonacoBasePath(basePath) {
439
+ if (typeof basePath === 'string' && basePath) _monacoBasePath = basePath;
440
+ }
441
+
442
+ export async function getEditorFactory() {
443
+ if (!_factoryInstance) {
444
+ // Ensure Monaco is initialized — initMonaco is idempotent, safe to call from any entry point
445
+ initMonaco(_monacoBasePath);
446
+ _factoryInstance = await new MonacoEditorFactory().init();
447
+ }
448
+ return _factoryInstance;
449
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Monaco Loader — wraps the AMD require() bootstrap into an ES module Promise.
3
+ *
4
+ * Monaco ships as AMD modules. vs/loader.js is injected dynamically on demand
5
+ * (NOT via a global <script> tag) so it does not pollute window.define before
6
+ * Plotly loads. Plotly auto-detects AMD define and would wrap itself as a module
7
+ * instead of setting window.Plotly if loader.js ran at page load.
8
+ */
9
+
10
+ let _resolveMonaco;
11
+ let _monacoReady = false;
12
+
13
+ /** @type {Promise<typeof monaco>} */
14
+ export const monacoReady = new Promise(resolve => { _resolveMonaco = resolve; });
15
+
16
+ function injectLoaderScript(src) {
17
+ return new Promise((resolve, reject) => {
18
+ const s = document.createElement('script');
19
+ s.src = src;
20
+ s.onload = resolve;
21
+ s.onerror = () => reject(new Error(`Failed to load Monaco loader: ${src}`));
22
+ document.head.appendChild(s);
23
+ });
24
+ }
25
+
26
+ function injectMonacoCss(basePath) {
27
+ const id = 'monaco-editor-css';
28
+ if (document.getElementById(id)) return;
29
+ const link = document.createElement('link');
30
+ link.id = id;
31
+ link.rel = 'stylesheet';
32
+ link.href = `${basePath}/vs/editor/editor.main.css`;
33
+ document.head.appendChild(link);
34
+ }
35
+
36
+ /**
37
+ * Call once when the notebook page initialises.
38
+ * Where Monaco lives is the CONSUMER's business.
39
+ *
40
+ * `basePath` defaulted to 'vendor/monaco' — a path into ONE application's tree,
41
+ * which would 404 for anyone else. Exactly the bug plotly_wrapper.js had, and it
42
+ * takes exactly the same fix: no default, and say so if nobody supplies one.
43
+ *
44
+ * Monaco is a PEER dependency — 14 MB this bundle deliberately does not contain.
45
+ *
46
+ * @param {string} basePath Path prefix for the vs/ directory. REQUIRED.
47
+ */
48
+ export async function initMonaco(basePath) {
49
+ if (!basePath) {
50
+ throw new Error(
51
+ '[twm/editor] initMonaco() needs the path to your copy of Monaco, e.g. '
52
+ + "initMonaco('vendor/monaco'). Monaco is a peer dependency; the library "
53
+ + 'does not guess where you put it.');
54
+ }
55
+ if (_monacoReady) return monacoReady;
56
+ _monacoReady = true;
57
+
58
+ // Inject CSS and loader.js dynamically — kept out of the HTML to avoid global bleed
59
+ injectMonacoCss(basePath);
60
+ if (!window.require) {
61
+ await injectLoaderScript(`${basePath}/vs/loader.js`);
62
+ }
63
+
64
+ window.require.config({
65
+ paths: { vs: `${basePath}/vs` },
66
+ 'vs/nls': { availableLanguages: {} },
67
+ });
68
+
69
+ window.require(['vs/editor/editor.main'], () => {
70
+ _resolveMonaco(window.monaco);
71
+ });
72
+
73
+ return monacoReady;
74
+ }