figrecipe 0.7.4__py3-none-any.whl → 0.9.0__py3-none-any.whl

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 (143) hide show
  1. figrecipe/__init__.py +74 -76
  2. figrecipe/__main__.py +12 -0
  3. figrecipe/_api/_panel.py +67 -0
  4. figrecipe/_api/_save.py +100 -4
  5. figrecipe/_cli/__init__.py +7 -0
  6. figrecipe/_cli/_compose.py +87 -0
  7. figrecipe/_cli/_convert.py +117 -0
  8. figrecipe/_cli/_crop.py +82 -0
  9. figrecipe/_cli/_edit.py +70 -0
  10. figrecipe/_cli/_extract.py +128 -0
  11. figrecipe/_cli/_fonts.py +47 -0
  12. figrecipe/_cli/_info.py +67 -0
  13. figrecipe/_cli/_main.py +58 -0
  14. figrecipe/_cli/_reproduce.py +79 -0
  15. figrecipe/_cli/_style.py +77 -0
  16. figrecipe/_cli/_validate.py +66 -0
  17. figrecipe/_cli/_version.py +50 -0
  18. figrecipe/_composition/__init__.py +32 -0
  19. figrecipe/_composition/_alignment.py +452 -0
  20. figrecipe/_composition/_compose.py +179 -0
  21. figrecipe/_composition/_import_axes.py +127 -0
  22. figrecipe/_composition/_visibility.py +125 -0
  23. figrecipe/_dev/__init__.py +2 -0
  24. figrecipe/_dev/browser/__init__.py +69 -0
  25. figrecipe/_dev/browser/_audio.py +240 -0
  26. figrecipe/_dev/browser/_caption.py +356 -0
  27. figrecipe/_dev/browser/_click_effect.py +146 -0
  28. figrecipe/_dev/browser/_cursor.py +196 -0
  29. figrecipe/_dev/browser/_highlight.py +105 -0
  30. figrecipe/_dev/browser/_narration.py +237 -0
  31. figrecipe/_dev/browser/_recorder.py +446 -0
  32. figrecipe/_dev/browser/_utils.py +178 -0
  33. figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
  34. figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
  35. figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
  36. figrecipe/_editor/__init__.py +36 -36
  37. figrecipe/_editor/_bbox/_extract.py +155 -9
  38. figrecipe/_editor/_bbox/_extract_text.py +124 -0
  39. figrecipe/_editor/_call_overrides.py +183 -0
  40. figrecipe/_editor/_datatable_plot_handlers.py +249 -0
  41. figrecipe/_editor/_figure_layout.py +211 -0
  42. figrecipe/_editor/_flask_app.py +157 -16
  43. figrecipe/_editor/_helpers.py +17 -8
  44. figrecipe/_editor/_hitmap/_detect.py +89 -32
  45. figrecipe/_editor/_hitmap_main.py +4 -4
  46. figrecipe/_editor/_overrides.py +4 -1
  47. figrecipe/_editor/_plot_types_registry.py +190 -0
  48. figrecipe/_editor/_render_overrides.py +38 -11
  49. figrecipe/_editor/_renderer.py +46 -1
  50. figrecipe/_editor/_routes_annotation.py +114 -0
  51. figrecipe/_editor/_routes_axis.py +35 -6
  52. figrecipe/_editor/_routes_captions.py +130 -0
  53. figrecipe/_editor/_routes_composition.py +270 -0
  54. figrecipe/_editor/_routes_core.py +15 -173
  55. figrecipe/_editor/_routes_datatable.py +364 -0
  56. figrecipe/_editor/_routes_element.py +37 -19
  57. figrecipe/_editor/_routes_files.py +443 -0
  58. figrecipe/_editor/_routes_image.py +200 -0
  59. figrecipe/_editor/_routes_snapshot.py +94 -0
  60. figrecipe/_editor/_routes_style.py +28 -8
  61. figrecipe/_editor/_templates/__init__.py +40 -2
  62. figrecipe/_editor/_templates/_html.py +97 -103
  63. figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
  64. figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
  65. figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
  66. figrecipe/_editor/_templates/_html_datatable.py +92 -0
  67. figrecipe/_editor/_templates/_scripts/__init__.py +58 -0
  68. figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
  69. figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
  70. figrecipe/_editor/_templates/_scripts/_api.py +1 -1
  71. figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
  72. figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
  73. figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
  74. figrecipe/_editor/_templates/_scripts/_core.py +94 -37
  75. figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
  76. figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
  77. figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
  78. figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
  79. figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
  80. figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
  81. figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
  82. figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
  83. figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
  84. figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
  85. figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
  86. figrecipe/_editor/_templates/_scripts/_element_editor.py +17 -2
  87. figrecipe/_editor/_templates/_scripts/_files.py +274 -40
  88. figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
  89. figrecipe/_editor/_templates/_scripts/_hitmap.py +87 -84
  90. figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
  91. figrecipe/_editor/_templates/_scripts/_legend_drag.py +5 -0
  92. figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
  93. figrecipe/_editor/_templates/_scripts/_panel_drag.py +219 -48
  94. figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
  95. figrecipe/_editor/_templates/_scripts/_panel_position.py +238 -54
  96. figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
  97. figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
  98. figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
  99. figrecipe/_editor/_templates/_scripts/_selection.py +8 -1
  100. figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
  101. figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
  102. figrecipe/_editor/_templates/_scripts/_zoom.py +52 -19
  103. figrecipe/_editor/_templates/_styles/__init__.py +9 -0
  104. figrecipe/_editor/_templates/_styles/_base.py +47 -0
  105. figrecipe/_editor/_templates/_styles/_buttons.py +127 -6
  106. figrecipe/_editor/_templates/_styles/_composition.py +87 -0
  107. figrecipe/_editor/_templates/_styles/_controls.py +168 -3
  108. figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
  109. figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
  110. figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
  111. figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
  112. figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
  113. figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
  114. figrecipe/_editor/_templates/_styles/_dynamic_props.py +5 -5
  115. figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
  116. figrecipe/_editor/_templates/_styles/_forms.py +98 -0
  117. figrecipe/_editor/_templates/_styles/_hitmap.py +7 -0
  118. figrecipe/_editor/_templates/_styles/_modals.py +29 -0
  119. figrecipe/_editor/_templates/_styles/_overlays.py +5 -5
  120. figrecipe/_editor/_templates/_styles/_preview.py +213 -8
  121. figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
  122. figrecipe/_editor/static/audio/click.mp3 +0 -0
  123. figrecipe/_editor/static/click.mp3 +0 -0
  124. figrecipe/_editor/static/icons/favicon.ico +0 -0
  125. figrecipe/_integrations/__init__.py +17 -0
  126. figrecipe/_integrations/_scitex_stats.py +298 -0
  127. figrecipe/_params/_DECORATION_METHODS.py +2 -0
  128. figrecipe/_recorder.py +28 -3
  129. figrecipe/_reproducer/_core.py +60 -49
  130. figrecipe/_utils/__init__.py +3 -0
  131. figrecipe/_utils/_bundle.py +205 -0
  132. figrecipe/_wrappers/_axes.py +150 -2
  133. figrecipe/_wrappers/_caption_generator.py +218 -0
  134. figrecipe/_wrappers/_figure.py +26 -1
  135. figrecipe/_wrappers/_stat_annotation.py +274 -0
  136. figrecipe/styles/_style_applier.py +10 -2
  137. figrecipe/styles/presets/SCITEX.yaml +11 -4
  138. {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/METADATA +144 -146
  139. figrecipe-0.9.0.dist-info/RECORD +277 -0
  140. figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
  141. figrecipe-0.7.4.dist-info/RECORD +0 -188
  142. {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
  143. {figrecipe-0.7.4.dist-info → figrecipe-0.9.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,53 +1,185 @@
1
1
  #!/usr/bin/env python3
2
2
  # -*- coding: utf-8 -*-
3
- """File switcher JavaScript for switching between recipe files."""
3
+ """File browser JavaScript for the file tree panel."""
4
4
 
5
5
  SCRIPTS_FILES = """
6
- // ==================== FILE SWITCHER ====================
7
- // Allows switching between recipe files without restarting the server
6
+ // ==================== FILE BROWSER ====================
7
+ // File tree panel for browsing and switching between recipe files
8
8
 
9
9
  let currentFilePath = null;
10
+ let fileBrowserCollapsed = false;
11
+ let expandedFolders = new Set();
12
+
13
+ // Load expanded state from localStorage
14
+ function loadExpandedState() {
15
+ try {
16
+ const saved = localStorage.getItem('figrecipe-expanded-folders');
17
+ if (saved) {
18
+ expandedFolders = new Set(JSON.parse(saved));
19
+ }
20
+ } catch (e) {
21
+ console.warn('[FileBrowser] Failed to load expanded state:', e);
22
+ }
23
+ }
24
+
25
+ // Save expanded state to localStorage
26
+ function saveExpandedState() {
27
+ try {
28
+ localStorage.setItem('figrecipe-expanded-folders', JSON.stringify([...expandedFolders]));
29
+ } catch (e) {
30
+ console.warn('[FileBrowser] Failed to save expanded state:', e);
31
+ }
32
+ }
33
+
34
+ // Toggle folder expand/collapse
35
+ function toggleFolder(folderPath) {
36
+ if (expandedFolders.has(folderPath)) {
37
+ expandedFolders.delete(folderPath);
38
+ } else {
39
+ expandedFolders.add(folderPath);
40
+ }
41
+ saveExpandedState();
42
+
43
+ // Update DOM
44
+ const folderEl = document.querySelector(`.file-tree-folder[data-path="${folderPath}"]`);
45
+ if (folderEl) {
46
+ folderEl.classList.toggle('expanded', expandedFolders.has(folderPath));
47
+ }
48
+ }
49
+
50
+ // Render a tree item (file or folder)
51
+ function renderTreeItem(item, level = 0) {
52
+ const indent = level * 16; // 16px per level
53
+
54
+ if (item.type === 'directory') {
55
+ const isExpanded = expandedFolders.has(item.path);
56
+ const expandedClass = isExpanded ? ' expanded' : '';
57
+
58
+ let childrenHtml = '';
59
+ if (item.children && item.children.length > 0) {
60
+ childrenHtml = item.children.map(child => renderTreeItem(child, level + 1)).join('');
61
+ }
62
+
63
+ return `<li class="file-tree-folder${expandedClass}" data-path="${item.path}">
64
+ <div class="file-tree-entry" data-path="${item.path}" data-type="folder" style="padding-left: ${12 + indent}px;">
65
+ <span class="file-tree-icon">📁</span>
66
+ <span class="file-tree-name">${item.name}</span>
67
+ <span class="file-tree-badge folder-badge">${item.children ? item.children.length : 0}</span>
68
+ </div>
69
+ <ul class="file-tree-children">
70
+ ${childrenHtml}
71
+ </ul>
72
+ </li>`;
73
+ } else {
74
+ // File item
75
+ const isCurrent = item.is_current;
76
+ const currentClass = isCurrent ? ' current' : '';
77
+ const hasImageClass = item.has_image ? ' has-image' : '';
78
+ const icon = item.has_image ? '📊' : '📄';
79
+ const badge = item.has_image ? '<span class="file-tree-badge">PNG</span>' : '';
80
+
81
+ return `<li class="file-tree-item">
82
+ <div class="file-tree-entry${currentClass}${hasImageClass}" data-path="${item.path}" data-type="file" style="padding-left: ${12 + indent}px;">
83
+ <span class="file-tree-icon">${icon}</span>
84
+ <span class="file-tree-name">${item.name}</span>
85
+ ${badge}
86
+ <span class="file-tree-actions">
87
+ <button class="file-action-btn btn-rename" data-path="${item.path}" title="Rename">✏️</button>
88
+ <button class="file-action-btn btn-delete" data-path="${item.path}" title="Delete">🗑️</button>
89
+ </span>
90
+ </div>
91
+ </li>`;
92
+ }
93
+ }
10
94
 
11
95
  async function loadFileList() {
12
- const selector = document.getElementById('file-selector');
13
- if (!selector) return;
96
+ const fileTree = document.getElementById('file-tree');
97
+ if (!fileTree) return;
98
+
99
+ // Load saved expanded state
100
+ loadExpandedState();
14
101
 
15
102
  try {
16
103
  const response = await fetch('/api/files');
17
104
  if (!response.ok) {
18
- selector.innerHTML = '<option value="">No files found</option>';
105
+ fileTree.innerHTML = '<li class="file-tree-empty"><p>No files found</p></li>';
19
106
  return;
20
107
  }
21
108
 
22
109
  const data = await response.json();
110
+ const tree = data.tree || [];
23
111
  const files = data.files || [];
24
112
  currentFilePath = data.current_file;
25
113
 
26
- if (files.length === 0) {
27
- selector.innerHTML = '<option value="">(No recipe files in directory)</option>';
28
- return;
29
- }
114
+ // Build file tree HTML
115
+ let treeHtml = '';
30
116
 
31
- // Build options
32
- let optionsHtml = '';
117
+ // Show unsaved figure entry when no current file path (new/unsaved figure)
33
118
  if (!currentFilePath) {
34
- optionsHtml += '<option value="" selected>(Unsaved figure)</option>';
119
+ treeHtml += `<li class="file-tree-item">
120
+ <div class="file-tree-entry current" data-path="" data-type="file">
121
+ <span class="file-tree-icon">✨</span>
122
+ <span class="file-tree-name">(Unsaved figure)</span>
123
+ </div>
124
+ </li>`;
125
+ }
126
+
127
+ // Show empty state only if no unsaved figure AND no files
128
+ if (tree.length === 0 && files.length === 0 && currentFilePath !== null) {
129
+ fileTree.innerHTML = '<li class="file-tree-empty"><p>No recipe files</p><p>Create one with figrecipe.subplots()</p></li>';
130
+ return;
35
131
  }
36
132
 
37
- files.forEach(file => {
38
- const isCurrent = file.is_current;
39
- const icon = file.has_image ? '📊 ' : '📄 ';
40
- const selected = isCurrent ? ' selected' : '';
41
- optionsHtml += `<option value="${file.path}"${selected}>${icon}${file.name}</option>`;
133
+ // Render tree structure
134
+ tree.forEach(item => {
135
+ treeHtml += renderTreeItem(item, 0);
136
+ });
137
+
138
+ fileTree.innerHTML = treeHtml;
139
+
140
+ // Add click handlers for folder entries (expand/collapse)
141
+ fileTree.querySelectorAll('.file-tree-folder > .file-tree-entry').forEach(entry => {
142
+ entry.addEventListener('click', (e) => {
143
+ const folderPath = entry.dataset.path;
144
+ if (folderPath !== undefined) {
145
+ toggleFolder(folderPath);
146
+ }
147
+ });
148
+ });
149
+
150
+ // Add click handlers for file entries
151
+ fileTree.querySelectorAll('.file-tree-entry[data-type="file"]').forEach(entry => {
152
+ entry.addEventListener('click', (e) => {
153
+ // Don't switch if clicking action buttons
154
+ if (e.target.closest('.file-action-btn')) return;
155
+ const path = entry.dataset.path;
156
+ if (path) {
157
+ switchToFile(path);
158
+ }
159
+ });
160
+ });
161
+
162
+ // Add click handlers for rename buttons
163
+ fileTree.querySelectorAll('.btn-rename').forEach(btn => {
164
+ btn.addEventListener('click', (e) => {
165
+ e.stopPropagation();
166
+ renameFile(btn.dataset.path);
167
+ });
42
168
  });
43
169
 
44
- selector.innerHTML = optionsHtml;
170
+ // Add click handlers for delete buttons
171
+ fileTree.querySelectorAll('.btn-delete').forEach(btn => {
172
+ btn.addEventListener('click', (e) => {
173
+ e.stopPropagation();
174
+ deleteFile(btn.dataset.path);
175
+ });
176
+ });
45
177
 
46
- console.log('[FileSwitcher] Loaded', files.length, 'files');
178
+ console.log('[FileBrowser] Loaded', files.length, 'files in', tree.length, 'root items');
47
179
 
48
180
  } catch (error) {
49
- console.error('[FileSwitcher] Error loading files:', error);
50
- selector.innerHTML = '<option value="">Error loading files</option>';
181
+ console.error('[FileBrowser] Error loading files:', error);
182
+ fileTree.innerHTML = '<li class="file-tree-empty"><p>Error loading files</p></li>';
51
183
  }
52
184
  }
53
185
 
@@ -139,8 +271,8 @@ async function createNewFigure() {
139
271
  window.currentColorMap = data.color_map;
140
272
  }
141
273
 
142
- // Clear current file path (unsaved figure)
143
- currentFilePath = null;
274
+ // Update current file path to the new file
275
+ currentFilePath = data.file || null;
144
276
 
145
277
  // Clear selection
146
278
  if (typeof clearSelection === 'function') {
@@ -149,8 +281,9 @@ async function createNewFigure() {
149
281
  const selectedPanel = document.getElementById('selected-element-panel');
150
282
  if (selectedPanel) selectedPanel.style.display = 'none';
151
283
 
152
- showToast('New blank figure created', 'success');
153
- console.log('[FileSwitcher] Created new blank figure');
284
+ const fileName = data.file_name || 'new_figure';
285
+ showToast(`Created: ${fileName}.yaml`, 'success');
286
+ console.log('[FileSwitcher] Created new figure:', data.file);
154
287
 
155
288
  // Reload file list to show (Unsaved figure)
156
289
  loadFileList();
@@ -161,35 +294,136 @@ async function createNewFigure() {
161
294
  }
162
295
  }
163
296
 
164
- function initFileSwitcher() {
165
- const selector = document.getElementById('file-selector');
166
- const newBtn = document.getElementById('btn-new-figure');
297
+ function toggleFileBrowser() {
298
+ const panel = document.getElementById('file-browser-panel');
299
+ const collapseBtn = document.getElementById('btn-collapse-browser');
300
+ if (!panel) return;
167
301
 
168
- if (selector) {
169
- selector.addEventListener('change', (e) => {
170
- const filePath = e.target.value;
171
- if (filePath) {
172
- switchToFile(filePath);
173
- }
174
- });
302
+ fileBrowserCollapsed = !fileBrowserCollapsed;
303
+ panel.classList.toggle('collapsed', fileBrowserCollapsed);
304
+ if (collapseBtn) {
305
+ collapseBtn.innerHTML = fileBrowserCollapsed ? '&#x276F;' : '&#x276E;';
306
+ collapseBtn.title = fileBrowserCollapsed ? 'Expand panel' : 'Collapse panel';
175
307
  }
308
+ }
309
+
310
+ function initFileBrowser() {
311
+ const newBtn = document.getElementById('btn-new-file');
312
+ const refreshBtn = document.getElementById('btn-refresh-files');
313
+ const collapseBtn = document.getElementById('btn-collapse-browser');
176
314
 
177
315
  if (newBtn) {
178
316
  newBtn.addEventListener('click', createNewFigure);
179
317
  }
180
318
 
319
+ if (refreshBtn) {
320
+ refreshBtn.addEventListener('click', loadFileList);
321
+ }
322
+
323
+ if (collapseBtn) {
324
+ collapseBtn.addEventListener('click', toggleFileBrowser);
325
+ }
326
+
181
327
  // Load file list on init
182
328
  loadFileList();
183
329
  }
184
330
 
185
- // Initialize file switcher after DOM is ready
331
+ // Initialize file browser after DOM is ready
186
332
  if (document.readyState === 'loading') {
187
- document.addEventListener('DOMContentLoaded', initFileSwitcher);
333
+ document.addEventListener('DOMContentLoaded', initFileBrowser);
188
334
  } else {
189
- initFileSwitcher();
335
+ initFileBrowser();
336
+ }
337
+
338
+ async function deleteFile(filePath) {
339
+ if (!filePath) return;
340
+
341
+ const fileName = filePath.split('/').pop().replace('.yaml', '');
342
+ if (!confirm(`Delete "${fileName}" and its associated files (.yaml, .png)?`)) {
343
+ return;
344
+ }
345
+
346
+ showToast('Deleting...', 'info');
347
+
348
+ try {
349
+ const response = await fetch('/api/delete', {
350
+ method: 'POST',
351
+ headers: { 'Content-Type': 'application/json' },
352
+ body: JSON.stringify({ path: filePath })
353
+ });
354
+
355
+ const data = await response.json();
356
+
357
+ if (!response.ok) {
358
+ throw new Error(data.error || 'Failed to delete');
359
+ }
360
+
361
+ showToast(`Deleted: ${data.deleted.join(', ')}`, 'success');
362
+ console.log('[FileBrowser] Deleted:', data.deleted);
363
+
364
+ // If we deleted the current file, switch to another or create new
365
+ if (data.was_current) {
366
+ if (data.switch_to) {
367
+ // Switch to another existing file
368
+ console.log('[FileBrowser] Switching to:', data.switch_to);
369
+ await switchToFile(data.switch_to);
370
+ } else {
371
+ // No other files, create a new one
372
+ console.log('[FileBrowser] No files left, creating new figure');
373
+ await createNewFigure();
374
+ }
375
+ } else {
376
+ // Just reload file list
377
+ loadFileList();
378
+ }
379
+
380
+ } catch (error) {
381
+ console.error('[FileBrowser] Delete error:', error);
382
+ showToast('Error: ' + error.message, 'error');
383
+ }
384
+ }
385
+
386
+ async function renameFile(filePath) {
387
+ if (!filePath) return;
388
+
389
+ const oldName = filePath.split('/').pop().replace('.yaml', '');
390
+ const newName = prompt(`Rename "${oldName}" to:`, oldName);
391
+
392
+ if (!newName || newName === oldName) return;
393
+
394
+ showToast('Renaming...', 'info');
395
+
396
+ try {
397
+ const response = await fetch('/api/rename', {
398
+ method: 'POST',
399
+ headers: { 'Content-Type': 'application/json' },
400
+ body: JSON.stringify({ path: filePath, new_name: newName })
401
+ });
402
+
403
+ const data = await response.json();
404
+
405
+ if (!response.ok) {
406
+ throw new Error(data.error || 'Failed to rename');
407
+ }
408
+
409
+ showToast(`Renamed to: ${data.new_name}`, 'success');
410
+ console.log('[FileBrowser] Renamed:', data.renamed);
411
+
412
+ // Update current file path if it was the renamed file
413
+ if (currentFilePath === filePath) {
414
+ currentFilePath = data.new_name + '.yaml';
415
+ }
416
+
417
+ // Reload file list
418
+ loadFileList();
419
+
420
+ } catch (error) {
421
+ console.error('[FileBrowser] Rename error:', error);
422
+ showToast('Error: ' + error.message, 'error');
423
+ }
190
424
  }
191
425
 
192
- console.log('[FileSwitcher] Loaded - Use dropdown to switch figures');
426
+ console.log('[FileBrowser] Loaded - Use file tree to switch figures');
193
427
  """
194
428
 
195
429
  __all__ = ["SCRIPTS_FILES"]
@@ -0,0 +1,240 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Files tree right-click context menu JavaScript."""
4
+
5
+ JS_FILES_CONTEXT_MENU = """
6
+ // ============================================================================
7
+ // Files Tree Context Menu (Right-Click Menu)
8
+ // ============================================================================
9
+ let filesContextMenu = null;
10
+ let filesContextTarget = null; // Track which file was right-clicked
11
+
12
+ function createFilesContextMenu() {
13
+ if (filesContextMenu) return;
14
+
15
+ filesContextMenu = document.createElement('div');
16
+ filesContextMenu.className = 'files-context-menu';
17
+ filesContextMenu.style.display = 'none';
18
+ filesContextMenu.innerHTML = `
19
+ <div class="context-menu-item" data-action="open">
20
+ Open
21
+ </div>
22
+ <div class="context-menu-item" data-action="rename">
23
+ Rename
24
+ </div>
25
+ <div class="context-menu-divider"></div>
26
+ <div class="context-menu-item" data-action="duplicate">
27
+ Duplicate
28
+ </div>
29
+ <div class="context-menu-item" data-action="download">
30
+ Download
31
+ </div>
32
+ <div class="context-menu-divider"></div>
33
+ <div class="context-menu-item context-menu-danger" data-action="delete">
34
+ Delete
35
+ </div>
36
+ <div class="context-menu-divider"></div>
37
+ <div class="context-menu-item" data-action="new-file">
38
+ New figure
39
+ </div>
40
+ <div class="context-menu-item" data-action="refresh">
41
+ Refresh list
42
+ </div>
43
+ `;
44
+ document.body.appendChild(filesContextMenu);
45
+ setupFilesContextMenuListeners();
46
+ }
47
+
48
+ function setupFilesContextMenuListeners() {
49
+ if (!filesContextMenu) return;
50
+
51
+ // Click on menu items
52
+ filesContextMenu.querySelectorAll('.context-menu-item').forEach(item => {
53
+ item.addEventListener('click', (e) => {
54
+ e.stopPropagation();
55
+ const action = item.dataset.action;
56
+ handleFilesContextMenuAction(action);
57
+ hideFilesContextMenu();
58
+ });
59
+ });
60
+
61
+ // Hide on click outside
62
+ document.addEventListener('click', hideFilesContextMenu);
63
+ document.addEventListener('scroll', hideFilesContextMenu, true);
64
+ document.addEventListener('keydown', (e) => {
65
+ if (e.key === 'Escape') hideFilesContextMenu();
66
+ });
67
+ }
68
+
69
+ function handleFilesContextMenuAction(action) {
70
+ switch (action) {
71
+ case 'open':
72
+ if (filesContextTarget) {
73
+ loadFile(filesContextTarget);
74
+ }
75
+ break;
76
+ case 'rename':
77
+ if (filesContextTarget) {
78
+ const newName = prompt('Enter new name:', filesContextTarget.split('/').pop());
79
+ if (newName && newName !== filesContextTarget.split('/').pop()) {
80
+ renameFile(filesContextTarget, newName);
81
+ }
82
+ }
83
+ break;
84
+ case 'duplicate':
85
+ if (filesContextTarget) {
86
+ duplicateFile(filesContextTarget);
87
+ }
88
+ break;
89
+ case 'download':
90
+ if (filesContextTarget) {
91
+ downloadFile(filesContextTarget);
92
+ }
93
+ break;
94
+ case 'delete':
95
+ if (filesContextTarget) {
96
+ if (confirm(`Delete "${filesContextTarget.split('/').pop()}"?`)) {
97
+ deleteFile(filesContextTarget);
98
+ }
99
+ }
100
+ break;
101
+ case 'new-file':
102
+ document.getElementById('btn-new-file')?.click();
103
+ break;
104
+ case 'refresh':
105
+ document.getElementById('btn-refresh-files')?.click();
106
+ break;
107
+ }
108
+ filesContextTarget = null;
109
+ }
110
+
111
+ async function renameFile(filePath, newName) {
112
+ try {
113
+ const response = await fetch('/api/rename', {
114
+ method: 'POST',
115
+ headers: { 'Content-Type': 'application/json' },
116
+ body: JSON.stringify({ path: filePath, new_name: newName })
117
+ });
118
+ const result = await response.json();
119
+ if (result.success) {
120
+ refreshFileList();
121
+ showToast('File renamed');
122
+ } else {
123
+ showToast(result.error || 'Failed to rename', 'error');
124
+ }
125
+ } catch (err) {
126
+ console.error('Rename error:', err);
127
+ showToast('Failed to rename', 'error');
128
+ }
129
+ }
130
+
131
+ async function duplicateFile(filePath) {
132
+ try {
133
+ const response = await fetch('/api/duplicate', {
134
+ method: 'POST',
135
+ headers: { 'Content-Type': 'application/json' },
136
+ body: JSON.stringify({ path: filePath })
137
+ });
138
+ const result = await response.json();
139
+ if (result.success) {
140
+ refreshFileList();
141
+ showToast('File duplicated');
142
+ } else {
143
+ showToast(result.error || 'Failed to duplicate', 'error');
144
+ }
145
+ } catch (err) {
146
+ console.error('Duplicate error:', err);
147
+ showToast('Failed to duplicate', 'error');
148
+ }
149
+ }
150
+
151
+ function downloadFile(filePath) {
152
+ window.location.href = `/api/download?path=${encodeURIComponent(filePath)}`;
153
+ }
154
+
155
+ async function deleteFile(filePath) {
156
+ try {
157
+ const response = await fetch('/api/delete', {
158
+ method: 'POST',
159
+ headers: { 'Content-Type': 'application/json' },
160
+ body: JSON.stringify({ path: filePath })
161
+ });
162
+ const result = await response.json();
163
+ if (result.success) {
164
+ refreshFileList();
165
+ showToast('File deleted');
166
+ } else {
167
+ showToast(result.error || 'Failed to delete', 'error');
168
+ }
169
+ } catch (err) {
170
+ console.error('Delete error:', err);
171
+ showToast('Failed to delete', 'error');
172
+ }
173
+ }
174
+
175
+ function showFilesContextMenu(e, filePath) {
176
+ if (!filesContextMenu) createFilesContextMenu();
177
+
178
+ e.preventDefault();
179
+ e.stopPropagation();
180
+
181
+ filesContextTarget = filePath;
182
+
183
+ const x = e.clientX;
184
+ const y = e.clientY;
185
+
186
+ // Position off-screen to measure
187
+ filesContextMenu.style.left = '-9999px';
188
+ filesContextMenu.style.top = '-9999px';
189
+ filesContextMenu.style.display = 'block';
190
+
191
+ const menuWidth = filesContextMenu.offsetWidth;
192
+ const menuHeight = filesContextMenu.offsetHeight;
193
+
194
+ // Adjust position to fit in viewport
195
+ let left = x;
196
+ let top = y;
197
+ if (x + menuWidth > window.innerWidth - 10) {
198
+ left = x - menuWidth;
199
+ }
200
+ if (y + menuHeight > window.innerHeight - 10) {
201
+ top = y - menuHeight;
202
+ }
203
+
204
+ filesContextMenu.style.left = `${Math.max(10, left)}px`;
205
+ filesContextMenu.style.top = `${Math.max(10, top)}px`;
206
+
207
+ // Update menu based on context
208
+ const isFile = filePath && !filePath.endsWith('/');
209
+ filesContextMenu.querySelectorAll('[data-action="open"], [data-action="rename"], [data-action="duplicate"], [data-action="download"], [data-action="delete"]').forEach(item => {
210
+ item.style.display = isFile ? 'flex' : 'none';
211
+ });
212
+ }
213
+
214
+ function hideFilesContextMenu() {
215
+ if (filesContextMenu) {
216
+ filesContextMenu.style.display = 'none';
217
+ }
218
+ }
219
+
220
+ // Initialize files context menu
221
+ function initializeFilesContextMenu() {
222
+ const fileTree = document.getElementById('file-tree');
223
+ if (fileTree) {
224
+ fileTree.addEventListener('contextmenu', (e) => {
225
+ const fileEntry = e.target.closest('.file-tree-entry');
226
+ if (fileEntry) {
227
+ const filePath = fileEntry.dataset.path;
228
+ showFilesContextMenu(e, filePath);
229
+ } else {
230
+ // Right-click on empty area
231
+ showFilesContextMenu(e, null);
232
+ }
233
+ });
234
+ }
235
+ }
236
+ """
237
+
238
+ __all__ = ["JS_FILES_CONTEXT_MENU"]
239
+
240
+ # EOF