figrecipe 0.6.0__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 (269) hide show
  1. figrecipe/__init__.py +161 -1030
  2. figrecipe/__main__.py +12 -0
  3. figrecipe/_api/__init__.py +48 -0
  4. figrecipe/_api/_extract.py +108 -0
  5. figrecipe/_api/_notebook.py +61 -0
  6. figrecipe/_api/_panel.py +113 -0
  7. figrecipe/_api/_save.py +287 -0
  8. figrecipe/_api/_seaborn_proxy.py +34 -0
  9. figrecipe/_api/_style_manager.py +153 -0
  10. figrecipe/_api/_subplots.py +333 -0
  11. figrecipe/_api/_validate.py +82 -0
  12. figrecipe/_cli/__init__.py +7 -0
  13. figrecipe/_cli/_compose.py +87 -0
  14. figrecipe/_cli/_convert.py +117 -0
  15. figrecipe/_cli/_crop.py +82 -0
  16. figrecipe/_cli/_edit.py +70 -0
  17. figrecipe/_cli/_extract.py +128 -0
  18. figrecipe/_cli/_fonts.py +47 -0
  19. figrecipe/_cli/_info.py +67 -0
  20. figrecipe/_cli/_main.py +58 -0
  21. figrecipe/_cli/_reproduce.py +79 -0
  22. figrecipe/_cli/_style.py +77 -0
  23. figrecipe/_cli/_validate.py +66 -0
  24. figrecipe/_cli/_version.py +50 -0
  25. figrecipe/_composition/__init__.py +32 -0
  26. figrecipe/_composition/_alignment.py +452 -0
  27. figrecipe/_composition/_compose.py +179 -0
  28. figrecipe/_composition/_import_axes.py +127 -0
  29. figrecipe/_composition/_visibility.py +125 -0
  30. figrecipe/_dev/__init__.py +4 -93
  31. figrecipe/_dev/_plotters.py +76 -0
  32. figrecipe/_dev/_run_demos.py +56 -0
  33. figrecipe/_dev/browser/__init__.py +69 -0
  34. figrecipe/_dev/browser/_audio.py +240 -0
  35. figrecipe/_dev/browser/_caption.py +356 -0
  36. figrecipe/_dev/browser/_click_effect.py +146 -0
  37. figrecipe/_dev/browser/_cursor.py +196 -0
  38. figrecipe/_dev/browser/_highlight.py +105 -0
  39. figrecipe/_dev/browser/_narration.py +237 -0
  40. figrecipe/_dev/browser/_recorder.py +446 -0
  41. figrecipe/_dev/browser/_utils.py +178 -0
  42. figrecipe/_dev/browser/_video_trim/__init__.py +152 -0
  43. figrecipe/_dev/browser/_video_trim/_detection.py +223 -0
  44. figrecipe/_dev/browser/_video_trim/_markers.py +140 -0
  45. figrecipe/_dev/demo_plotters/__init__.py +35 -166
  46. figrecipe/_dev/demo_plotters/_categories.py +81 -0
  47. figrecipe/_dev/demo_plotters/_figure_creators.py +119 -0
  48. figrecipe/_dev/demo_plotters/_helpers.py +31 -0
  49. figrecipe/_dev/demo_plotters/_registry.py +50 -0
  50. figrecipe/_dev/demo_plotters/bar_categorical/__init__.py +4 -0
  51. figrecipe/_dev/demo_plotters/contour_surface/__init__.py +4 -0
  52. figrecipe/_dev/demo_plotters/distribution/__init__.py +4 -0
  53. figrecipe/_dev/demo_plotters/image_matrix/__init__.py +4 -0
  54. figrecipe/_dev/demo_plotters/line_curve/__init__.py +4 -0
  55. figrecipe/_dev/demo_plotters/{plot_plot.py → line_curve/plot_plot.py} +3 -2
  56. figrecipe/_dev/demo_plotters/scatter_points/__init__.py +4 -0
  57. figrecipe/_dev/demo_plotters/special/__init__.py +4 -0
  58. figrecipe/_dev/demo_plotters/{plot_pie.py → special/plot_pie.py} +5 -1
  59. figrecipe/_dev/demo_plotters/spectral_signal/__init__.py +4 -0
  60. figrecipe/_dev/demo_plotters/vector_flow/__init__.py +4 -0
  61. figrecipe/_editor/__init__.py +61 -13
  62. figrecipe/_editor/_bbox/__init__.py +43 -0
  63. figrecipe/_editor/_bbox/_collections.py +177 -0
  64. figrecipe/_editor/_bbox/_elements.py +159 -0
  65. figrecipe/_editor/_bbox/_extract.py +402 -0
  66. figrecipe/_editor/_bbox/_extract_axes.py +370 -0
  67. figrecipe/_editor/_bbox/_extract_text.py +466 -0
  68. figrecipe/_editor/_bbox/_lines.py +173 -0
  69. figrecipe/_editor/_bbox/_transforms.py +146 -0
  70. figrecipe/_editor/_call_overrides.py +183 -0
  71. figrecipe/_editor/_datatable_plot_handlers.py +249 -0
  72. figrecipe/_editor/_figure_layout.py +211 -0
  73. figrecipe/_editor/_flask_app.py +200 -1030
  74. figrecipe/_editor/_helpers.py +251 -0
  75. figrecipe/_editor/_hitmap/__init__.py +76 -0
  76. figrecipe/_editor/_hitmap/_artists/__init__.py +21 -0
  77. figrecipe/_editor/_hitmap/_artists/_collections.py +345 -0
  78. figrecipe/_editor/_hitmap/_artists/_images.py +68 -0
  79. figrecipe/_editor/_hitmap/_artists/_lines.py +107 -0
  80. figrecipe/_editor/_hitmap/_artists/_patches.py +163 -0
  81. figrecipe/_editor/_hitmap/_artists/_text.py +190 -0
  82. figrecipe/_editor/_hitmap/_colors.py +181 -0
  83. figrecipe/_editor/_hitmap/_detect.py +194 -0
  84. figrecipe/_editor/_hitmap/_restore.py +154 -0
  85. figrecipe/_editor/_hitmap_main.py +182 -0
  86. figrecipe/_editor/_overrides.py +4 -1
  87. figrecipe/_editor/_plot_types_registry.py +190 -0
  88. figrecipe/_editor/_preferences.py +135 -0
  89. figrecipe/_editor/_render_overrides.py +507 -0
  90. figrecipe/_editor/_renderer.py +81 -186
  91. figrecipe/_editor/_routes_annotation.py +114 -0
  92. figrecipe/_editor/_routes_axis.py +482 -0
  93. figrecipe/_editor/_routes_captions.py +130 -0
  94. figrecipe/_editor/_routes_composition.py +270 -0
  95. figrecipe/_editor/_routes_core.py +126 -0
  96. figrecipe/_editor/_routes_datatable.py +364 -0
  97. figrecipe/_editor/_routes_element.py +335 -0
  98. figrecipe/_editor/_routes_files.py +443 -0
  99. figrecipe/_editor/_routes_image.py +200 -0
  100. figrecipe/_editor/_routes_snapshot.py +94 -0
  101. figrecipe/_editor/_routes_style.py +243 -0
  102. figrecipe/_editor/_templates/__init__.py +116 -1
  103. figrecipe/_editor/_templates/_html.py +154 -64
  104. figrecipe/_editor/_templates/_html_components/__init__.py +13 -0
  105. figrecipe/_editor/_templates/_html_components/_composition_toolbar.py +79 -0
  106. figrecipe/_editor/_templates/_html_components/_file_browser.py +41 -0
  107. figrecipe/_editor/_templates/_html_datatable.py +92 -0
  108. figrecipe/_editor/_templates/_scripts/__init__.py +178 -0
  109. figrecipe/_editor/_templates/_scripts/_accordion.py +328 -0
  110. figrecipe/_editor/_templates/_scripts/_annotation_drag.py +504 -0
  111. figrecipe/_editor/_templates/_scripts/_api.py +228 -0
  112. figrecipe/_editor/_templates/_scripts/_canvas_context_menu.py +182 -0
  113. figrecipe/_editor/_templates/_scripts/_captions.py +231 -0
  114. figrecipe/_editor/_templates/_scripts/_colors.py +485 -0
  115. figrecipe/_editor/_templates/_scripts/_composition.py +283 -0
  116. figrecipe/_editor/_templates/_scripts/_core.py +493 -0
  117. figrecipe/_editor/_templates/_scripts/_datatable/__init__.py +59 -0
  118. figrecipe/_editor/_templates/_scripts/_datatable/_cell_edit.py +97 -0
  119. figrecipe/_editor/_templates/_scripts/_datatable/_clipboard.py +164 -0
  120. figrecipe/_editor/_templates/_scripts/_datatable/_context_menu.py +221 -0
  121. figrecipe/_editor/_templates/_scripts/_datatable/_core.py +150 -0
  122. figrecipe/_editor/_templates/_scripts/_datatable/_editable.py +511 -0
  123. figrecipe/_editor/_templates/_scripts/_datatable/_import.py +161 -0
  124. figrecipe/_editor/_templates/_scripts/_datatable/_plot.py +261 -0
  125. figrecipe/_editor/_templates/_scripts/_datatable/_selection.py +438 -0
  126. figrecipe/_editor/_templates/_scripts/_datatable/_table.py +256 -0
  127. figrecipe/_editor/_templates/_scripts/_datatable/_tabs.py +354 -0
  128. figrecipe/_editor/_templates/_scripts/_debug_snapshot.py +186 -0
  129. figrecipe/_editor/_templates/_scripts/_element_editor.py +325 -0
  130. figrecipe/_editor/_templates/_scripts/_files.py +429 -0
  131. figrecipe/_editor/_templates/_scripts/_files_context_menu.py +240 -0
  132. figrecipe/_editor/_templates/_scripts/_hitmap.py +512 -0
  133. figrecipe/_editor/_templates/_scripts/_image_drop.py +428 -0
  134. figrecipe/_editor/_templates/_scripts/_inspector.py +315 -0
  135. figrecipe/_editor/_templates/_scripts/_labels.py +464 -0
  136. figrecipe/_editor/_templates/_scripts/_legend_drag.py +270 -0
  137. figrecipe/_editor/_templates/_scripts/_modals.py +226 -0
  138. figrecipe/_editor/_templates/_scripts/_multi_select.py +198 -0
  139. figrecipe/_editor/_templates/_scripts/_overlays.py +292 -0
  140. figrecipe/_editor/_templates/_scripts/_panel_drag.py +505 -0
  141. figrecipe/_editor/_templates/_scripts/_panel_drag_snapshot.py +33 -0
  142. figrecipe/_editor/_templates/_scripts/_panel_position.py +463 -0
  143. figrecipe/_editor/_templates/_scripts/_panel_resize.py +230 -0
  144. figrecipe/_editor/_templates/_scripts/_panel_snap.py +307 -0
  145. figrecipe/_editor/_templates/_scripts/_region_select.py +255 -0
  146. figrecipe/_editor/_templates/_scripts/_selection.py +244 -0
  147. figrecipe/_editor/_templates/_scripts/_sync.py +242 -0
  148. figrecipe/_editor/_templates/_scripts/_tabs.py +89 -0
  149. figrecipe/_editor/_templates/_scripts/_undo_redo.py +348 -0
  150. figrecipe/_editor/_templates/_scripts/_view_mode.py +107 -0
  151. figrecipe/_editor/_templates/_scripts/_zoom.py +212 -0
  152. figrecipe/_editor/_templates/_styles/__init__.py +78 -0
  153. figrecipe/_editor/_templates/_styles/_base.py +111 -0
  154. figrecipe/_editor/_templates/_styles/_buttons.py +327 -0
  155. figrecipe/_editor/_templates/_styles/_color_input.py +123 -0
  156. figrecipe/_editor/_templates/_styles/_composition.py +87 -0
  157. figrecipe/_editor/_templates/_styles/_controls.py +430 -0
  158. figrecipe/_editor/_templates/_styles/_datatable/__init__.py +40 -0
  159. figrecipe/_editor/_templates/_styles/_datatable/_editable.py +203 -0
  160. figrecipe/_editor/_templates/_styles/_datatable/_panel.py +268 -0
  161. figrecipe/_editor/_templates/_styles/_datatable/_table.py +479 -0
  162. figrecipe/_editor/_templates/_styles/_datatable/_toolbar.py +384 -0
  163. figrecipe/_editor/_templates/_styles/_datatable/_vars.py +123 -0
  164. figrecipe/_editor/_templates/_styles/_dynamic_props.py +144 -0
  165. figrecipe/_editor/_templates/_styles/_file_browser.py +466 -0
  166. figrecipe/_editor/_templates/_styles/_forms.py +224 -0
  167. figrecipe/_editor/_templates/_styles/_hitmap.py +191 -0
  168. figrecipe/_editor/_templates/_styles/_inspector.py +90 -0
  169. figrecipe/_editor/_templates/_styles/_labels.py +118 -0
  170. figrecipe/_editor/_templates/_styles/_modals.py +127 -0
  171. figrecipe/_editor/_templates/_styles/_overlays.py +130 -0
  172. figrecipe/_editor/_templates/_styles/_preview.py +430 -0
  173. figrecipe/_editor/_templates/_styles/_selection.py +73 -0
  174. figrecipe/_editor/_templates/_styles/_spinner.py +117 -0
  175. figrecipe/_editor/static/audio/click.mp3 +0 -0
  176. figrecipe/_editor/static/click.mp3 +0 -0
  177. figrecipe/_editor/static/icons/favicon.ico +0 -0
  178. figrecipe/_integrations/__init__.py +17 -0
  179. figrecipe/_integrations/_scitex_stats.py +298 -0
  180. figrecipe/_params/_DECORATION_METHODS.py +8 -0
  181. figrecipe/_recorder.py +63 -109
  182. figrecipe/_recorder_utils.py +124 -0
  183. figrecipe/_reproducer/__init__.py +18 -0
  184. figrecipe/_reproducer/_core.py +509 -0
  185. figrecipe/_reproducer/_custom_plots.py +279 -0
  186. figrecipe/_reproducer/_seaborn.py +100 -0
  187. figrecipe/_reproducer/_violin.py +186 -0
  188. figrecipe/_signatures/_kwargs.py +273 -0
  189. figrecipe/_signatures/_loader.py +21 -423
  190. figrecipe/_signatures/_parsing.py +147 -0
  191. figrecipe/_utils/__init__.py +3 -0
  192. figrecipe/_utils/_bundle.py +205 -0
  193. figrecipe/_wrappers/_axes.py +252 -895
  194. figrecipe/_wrappers/_axes_helpers.py +136 -0
  195. figrecipe/_wrappers/_axes_plots.py +418 -0
  196. figrecipe/_wrappers/_axes_seaborn.py +157 -0
  197. figrecipe/_wrappers/_caption_generator.py +218 -0
  198. figrecipe/_wrappers/_figure.py +188 -1
  199. figrecipe/_wrappers/_panel_labels.py +127 -0
  200. figrecipe/_wrappers/_plot_helpers.py +143 -0
  201. figrecipe/_wrappers/_stat_annotation.py +274 -0
  202. figrecipe/_wrappers/_violin_helpers.py +180 -0
  203. figrecipe/styles/__init__.py +8 -6
  204. figrecipe/styles/_dotdict.py +72 -0
  205. figrecipe/styles/_finalize.py +134 -0
  206. figrecipe/styles/_fonts.py +77 -0
  207. figrecipe/styles/_kwargs_converter.py +178 -0
  208. figrecipe/styles/_plot_styles.py +209 -0
  209. figrecipe/styles/_style_applier.py +42 -480
  210. figrecipe/styles/_style_loader.py +16 -192
  211. figrecipe/styles/_themes.py +151 -0
  212. figrecipe/styles/presets/MATPLOTLIB.yaml +2 -1
  213. figrecipe/styles/presets/SCITEX.yaml +40 -28
  214. figrecipe-0.9.0.dist-info/METADATA +427 -0
  215. figrecipe-0.9.0.dist-info/RECORD +277 -0
  216. figrecipe-0.9.0.dist-info/entry_points.txt +2 -0
  217. figrecipe/_editor/_bbox.py +0 -978
  218. figrecipe/_editor/_hitmap.py +0 -937
  219. figrecipe/_editor/_templates/_scripts.py +0 -2778
  220. figrecipe/_editor/_templates/_styles.py +0 -1326
  221. figrecipe/_reproducer.py +0 -975
  222. figrecipe-0.6.0.dist-info/METADATA +0 -394
  223. figrecipe-0.6.0.dist-info/RECORD +0 -90
  224. /figrecipe/_dev/demo_plotters/{plot_bar.py → bar_categorical/plot_bar.py} +0 -0
  225. /figrecipe/_dev/demo_plotters/{plot_barh.py → bar_categorical/plot_barh.py} +0 -0
  226. /figrecipe/_dev/demo_plotters/{plot_contour.py → contour_surface/plot_contour.py} +0 -0
  227. /figrecipe/_dev/demo_plotters/{plot_contourf.py → contour_surface/plot_contourf.py} +0 -0
  228. /figrecipe/_dev/demo_plotters/{plot_tricontour.py → contour_surface/plot_tricontour.py} +0 -0
  229. /figrecipe/_dev/demo_plotters/{plot_tricontourf.py → contour_surface/plot_tricontourf.py} +0 -0
  230. /figrecipe/_dev/demo_plotters/{plot_tripcolor.py → contour_surface/plot_tripcolor.py} +0 -0
  231. /figrecipe/_dev/demo_plotters/{plot_triplot.py → contour_surface/plot_triplot.py} +0 -0
  232. /figrecipe/_dev/demo_plotters/{plot_boxplot.py → distribution/plot_boxplot.py} +0 -0
  233. /figrecipe/_dev/demo_plotters/{plot_ecdf.py → distribution/plot_ecdf.py} +0 -0
  234. /figrecipe/_dev/demo_plotters/{plot_hist.py → distribution/plot_hist.py} +0 -0
  235. /figrecipe/_dev/demo_plotters/{plot_hist2d.py → distribution/plot_hist2d.py} +0 -0
  236. /figrecipe/_dev/demo_plotters/{plot_violinplot.py → distribution/plot_violinplot.py} +0 -0
  237. /figrecipe/_dev/demo_plotters/{plot_hexbin.py → image_matrix/plot_hexbin.py} +0 -0
  238. /figrecipe/_dev/demo_plotters/{plot_imshow.py → image_matrix/plot_imshow.py} +0 -0
  239. /figrecipe/_dev/demo_plotters/{plot_matshow.py → image_matrix/plot_matshow.py} +0 -0
  240. /figrecipe/_dev/demo_plotters/{plot_pcolor.py → image_matrix/plot_pcolor.py} +0 -0
  241. /figrecipe/_dev/demo_plotters/{plot_pcolormesh.py → image_matrix/plot_pcolormesh.py} +0 -0
  242. /figrecipe/_dev/demo_plotters/{plot_spy.py → image_matrix/plot_spy.py} +0 -0
  243. /figrecipe/_dev/demo_plotters/{plot_errorbar.py → line_curve/plot_errorbar.py} +0 -0
  244. /figrecipe/_dev/demo_plotters/{plot_fill.py → line_curve/plot_fill.py} +0 -0
  245. /figrecipe/_dev/demo_plotters/{plot_fill_between.py → line_curve/plot_fill_between.py} +0 -0
  246. /figrecipe/_dev/demo_plotters/{plot_fill_betweenx.py → line_curve/plot_fill_betweenx.py} +0 -0
  247. /figrecipe/_dev/demo_plotters/{plot_stackplot.py → line_curve/plot_stackplot.py} +0 -0
  248. /figrecipe/_dev/demo_plotters/{plot_stairs.py → line_curve/plot_stairs.py} +0 -0
  249. /figrecipe/_dev/demo_plotters/{plot_step.py → line_curve/plot_step.py} +0 -0
  250. /figrecipe/_dev/demo_plotters/{plot_scatter.py → scatter_points/plot_scatter.py} +0 -0
  251. /figrecipe/_dev/demo_plotters/{plot_eventplot.py → special/plot_eventplot.py} +0 -0
  252. /figrecipe/_dev/demo_plotters/{plot_loglog.py → special/plot_loglog.py} +0 -0
  253. /figrecipe/_dev/demo_plotters/{plot_semilogx.py → special/plot_semilogx.py} +0 -0
  254. /figrecipe/_dev/demo_plotters/{plot_semilogy.py → special/plot_semilogy.py} +0 -0
  255. /figrecipe/_dev/demo_plotters/{plot_stem.py → special/plot_stem.py} +0 -0
  256. /figrecipe/_dev/demo_plotters/{plot_acorr.py → spectral_signal/plot_acorr.py} +0 -0
  257. /figrecipe/_dev/demo_plotters/{plot_angle_spectrum.py → spectral_signal/plot_angle_spectrum.py} +0 -0
  258. /figrecipe/_dev/demo_plotters/{plot_cohere.py → spectral_signal/plot_cohere.py} +0 -0
  259. /figrecipe/_dev/demo_plotters/{plot_csd.py → spectral_signal/plot_csd.py} +0 -0
  260. /figrecipe/_dev/demo_plotters/{plot_magnitude_spectrum.py → spectral_signal/plot_magnitude_spectrum.py} +0 -0
  261. /figrecipe/_dev/demo_plotters/{plot_phase_spectrum.py → spectral_signal/plot_phase_spectrum.py} +0 -0
  262. /figrecipe/_dev/demo_plotters/{plot_psd.py → spectral_signal/plot_psd.py} +0 -0
  263. /figrecipe/_dev/demo_plotters/{plot_specgram.py → spectral_signal/plot_specgram.py} +0 -0
  264. /figrecipe/_dev/demo_plotters/{plot_xcorr.py → spectral_signal/plot_xcorr.py} +0 -0
  265. /figrecipe/_dev/demo_plotters/{plot_barbs.py → vector_flow/plot_barbs.py} +0 -0
  266. /figrecipe/_dev/demo_plotters/{plot_quiver.py → vector_flow/plot_quiver.py} +0 -0
  267. /figrecipe/_dev/demo_plotters/{plot_streamplot.py → vector_flow/plot_streamplot.py} +0 -0
  268. {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/WHEEL +0 -0
  269. {figrecipe-0.6.0.dist-info → figrecipe-0.9.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,429 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """File browser JavaScript for the file tree panel."""
4
+
5
+ SCRIPTS_FILES = """
6
+ // ==================== FILE BROWSER ====================
7
+ // File tree panel for browsing and switching between recipe files
8
+
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
+ }
94
+
95
+ async function loadFileList() {
96
+ const fileTree = document.getElementById('file-tree');
97
+ if (!fileTree) return;
98
+
99
+ // Load saved expanded state
100
+ loadExpandedState();
101
+
102
+ try {
103
+ const response = await fetch('/api/files');
104
+ if (!response.ok) {
105
+ fileTree.innerHTML = '<li class="file-tree-empty"><p>No files found</p></li>';
106
+ return;
107
+ }
108
+
109
+ const data = await response.json();
110
+ const tree = data.tree || [];
111
+ const files = data.files || [];
112
+ currentFilePath = data.current_file;
113
+
114
+ // Build file tree HTML
115
+ let treeHtml = '';
116
+
117
+ // Show unsaved figure entry when no current file path (new/unsaved figure)
118
+ if (!currentFilePath) {
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;
131
+ }
132
+
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
+ });
168
+ });
169
+
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
+ });
177
+
178
+ console.log('[FileBrowser] Loaded', files.length, 'files in', tree.length, 'root items');
179
+
180
+ } catch (error) {
181
+ console.error('[FileBrowser] Error loading files:', error);
182
+ fileTree.innerHTML = '<li class="file-tree-empty"><p>Error loading files</p></li>';
183
+ }
184
+ }
185
+
186
+ async function switchToFile(filePath) {
187
+ if (!filePath || filePath === currentFilePath) return;
188
+
189
+ showToast('Loading figure...', 'info');
190
+
191
+ try {
192
+ const response = await fetch('/api/switch', {
193
+ method: 'POST',
194
+ headers: { 'Content-Type': 'application/json' },
195
+ body: JSON.stringify({ path: filePath })
196
+ });
197
+
198
+ if (!response.ok) {
199
+ const error = await response.json();
200
+ throw new Error(error.error || 'Failed to switch file');
201
+ }
202
+
203
+ const data = await response.json();
204
+
205
+ // Update preview image
206
+ const img = document.getElementById('preview-image');
207
+ if (img && data.image) {
208
+ img.src = 'data:image/png;base64,' + data.image;
209
+ }
210
+
211
+ // Update bboxes
212
+ if (data.bboxes) {
213
+ window.currentBboxes = data.bboxes;
214
+ }
215
+
216
+ // Update color map for hitmap
217
+ if (data.color_map) {
218
+ window.currentColorMap = data.color_map;
219
+ }
220
+
221
+ // Update current file path
222
+ currentFilePath = filePath;
223
+
224
+ // Clear selection
225
+ clearSelection();
226
+ document.getElementById('selected-element-panel')?.style.setProperty('display', 'none');
227
+
228
+ showToast('Loaded: ' + filePath, 'success');
229
+ console.log('[FileSwitcher] Switched to:', filePath);
230
+
231
+ // Reload file list to update selection state
232
+ loadFileList();
233
+
234
+ } catch (error) {
235
+ console.error('[FileSwitcher] Error switching file:', error);
236
+ showToast('Error: ' + error.message, 'error');
237
+ // Revert selector
238
+ loadFileList();
239
+ }
240
+ }
241
+
242
+ async function createNewFigure() {
243
+ showToast('Creating new figure...', 'info');
244
+
245
+ try {
246
+ const response = await fetch('/api/new', {
247
+ method: 'POST',
248
+ headers: { 'Content-Type': 'application/json' }
249
+ });
250
+
251
+ if (!response.ok) {
252
+ const error = await response.json();
253
+ throw new Error(error.error || 'Failed to create new figure');
254
+ }
255
+
256
+ const data = await response.json();
257
+
258
+ // Update preview image
259
+ const img = document.getElementById('preview-image');
260
+ if (img && data.image) {
261
+ img.src = 'data:image/png;base64,' + data.image;
262
+ }
263
+
264
+ // Update bboxes
265
+ if (data.bboxes) {
266
+ window.currentBboxes = data.bboxes;
267
+ }
268
+
269
+ // Update color map for hitmap
270
+ if (data.color_map) {
271
+ window.currentColorMap = data.color_map;
272
+ }
273
+
274
+ // Update current file path to the new file
275
+ currentFilePath = data.file || null;
276
+
277
+ // Clear selection
278
+ if (typeof clearSelection === 'function') {
279
+ clearSelection();
280
+ }
281
+ const selectedPanel = document.getElementById('selected-element-panel');
282
+ if (selectedPanel) selectedPanel.style.display = 'none';
283
+
284
+ const fileName = data.file_name || 'new_figure';
285
+ showToast(`Created: ${fileName}.yaml`, 'success');
286
+ console.log('[FileSwitcher] Created new figure:', data.file);
287
+
288
+ // Reload file list to show (Unsaved figure)
289
+ loadFileList();
290
+
291
+ } catch (error) {
292
+ console.error('[FileSwitcher] Error creating new figure:', error);
293
+ showToast('Error: ' + error.message, 'error');
294
+ }
295
+ }
296
+
297
+ function toggleFileBrowser() {
298
+ const panel = document.getElementById('file-browser-panel');
299
+ const collapseBtn = document.getElementById('btn-collapse-browser');
300
+ if (!panel) return;
301
+
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';
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');
314
+
315
+ if (newBtn) {
316
+ newBtn.addEventListener('click', createNewFigure);
317
+ }
318
+
319
+ if (refreshBtn) {
320
+ refreshBtn.addEventListener('click', loadFileList);
321
+ }
322
+
323
+ if (collapseBtn) {
324
+ collapseBtn.addEventListener('click', toggleFileBrowser);
325
+ }
326
+
327
+ // Load file list on init
328
+ loadFileList();
329
+ }
330
+
331
+ // Initialize file browser after DOM is ready
332
+ if (document.readyState === 'loading') {
333
+ document.addEventListener('DOMContentLoaded', initFileBrowser);
334
+ } else {
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
+ }
424
+ }
425
+
426
+ console.log('[FileBrowser] Loaded - Use file tree to switch figures');
427
+ """
428
+
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