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,463 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Panel position JavaScript for the figure editor.
4
+
5
+ This module contains the JavaScript code for:
6
+ - Loading and displaying panel positions
7
+ - Updating panel positions via numerical inputs
8
+ """
9
+
10
+ SCRIPTS_PANEL_POSITION = r"""
11
+ // ===== PANEL POSITION (mm, upper-left origin) =====
12
+
13
+ let panelPositions = {};
14
+ let figSize = { width_mm: 0, height_mm: 0 };
15
+ let currentSelectedPanelIndex = null; // Track currently selected panel
16
+
17
+ // Load panel positions from server
18
+ async function loadPanelPositions() {
19
+ try {
20
+ const response = await fetch('/get_axes_positions');
21
+ const data = await response.json();
22
+
23
+ // Extract figure size
24
+ if (data._figsize) {
25
+ figSize = data._figsize;
26
+ delete data._figsize;
27
+ }
28
+
29
+ panelPositions = data;
30
+ // Update inputs if a panel is selected
31
+ if (currentSelectedPanelIndex !== null) {
32
+ updatePanelPositionInputs();
33
+ }
34
+ } catch (error) {
35
+ console.error('Failed to load panel positions:', error);
36
+ }
37
+ }
38
+
39
+ // Update position input fields based on currently selected panel
40
+ function updatePanelPositionInputs(showHighlight = true) {
41
+ const indicator = document.getElementById('current_panel_indicator');
42
+ const leftInput = document.getElementById('panel_left');
43
+ const topInput = document.getElementById('panel_top');
44
+ const widthInput = document.getElementById('panel_width');
45
+ const heightInput = document.getElementById('panel_height');
46
+ const applyBtn = document.getElementById('apply_panel_position');
47
+
48
+ // If no panel selected, show placeholder and disable inputs
49
+ if (currentSelectedPanelIndex === null) {
50
+ if (indicator) {
51
+ indicator.textContent = 'Select an element';
52
+ indicator.classList.remove('panel-selected');
53
+ }
54
+ [leftInput, topInput, widthInput, heightInput].forEach(input => {
55
+ if (input) {
56
+ input.value = '';
57
+ input.disabled = true;
58
+ }
59
+ });
60
+ if (applyBtn) applyBtn.disabled = true;
61
+ return;
62
+ }
63
+
64
+ const axKey = Object.keys(panelPositions).sort()[currentSelectedPanelIndex];
65
+ const pos = panelPositions[axKey];
66
+
67
+ if (!pos) return;
68
+
69
+ // Update indicator
70
+ if (indicator) {
71
+ const label = String.fromCharCode(65 + currentSelectedPanelIndex); // A, B, C...
72
+ indicator.textContent = `Panel ${label}`;
73
+ indicator.classList.add('panel-selected');
74
+ }
75
+
76
+ // Enable inputs and update values
77
+ [leftInput, topInput, widthInput, heightInput].forEach(input => {
78
+ if (input) input.disabled = false;
79
+ });
80
+ if (applyBtn) applyBtn.disabled = false;
81
+
82
+ // Values are already in mm from server
83
+ if (leftInput) leftInput.value = pos.left;
84
+ if (topInput) topInput.value = pos.top;
85
+ if (widthInput) widthInput.value = pos.width;
86
+ if (heightInput) heightInput.value = pos.height;
87
+
88
+ // Draw highlight
89
+ if (showHighlight) {
90
+ drawPanelSelectionHighlight(pos);
91
+ }
92
+ }
93
+
94
+ // Draw visual highlight around selected panel (axis bbox only)
95
+ function drawPanelSelectionHighlight(pos) {
96
+ const overlay = document.getElementById('selection-overlay');
97
+ if (!overlay) return;
98
+
99
+ // Clear previous panel highlight (but keep element selections)
100
+ const existingHighlight = document.getElementById('panel-selection-highlight');
101
+ if (existingHighlight) existingHighlight.remove();
102
+
103
+ const img = document.getElementById('preview-image');
104
+ if (!img || !img.naturalWidth || !img.naturalHeight) return;
105
+
106
+ // Ensure viewBox is set
107
+ overlay.setAttribute('viewBox', `0 0 ${img.naturalWidth} ${img.naturalHeight}`);
108
+ overlay.style.width = `${img.naturalWidth}px`;
109
+ overlay.style.height = `${img.naturalHeight}px`;
110
+
111
+ // Convert mm coords (upper-left origin) to image pixel coords
112
+ // pos.left, pos.top, pos.width, pos.height are in mm
113
+ const scaleX = img.naturalWidth / figSize.width_mm;
114
+ const scaleY = img.naturalHeight / figSize.height_mm;
115
+
116
+ const x = pos.left * scaleX;
117
+ const y = pos.top * scaleY; // Already in upper-left origin
118
+ const width = pos.width * scaleX;
119
+ const height = pos.height * scaleY;
120
+
121
+ // Create highlight rectangle
122
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
123
+ rect.id = 'panel-selection-highlight';
124
+ rect.setAttribute('x', x);
125
+ rect.setAttribute('y', y);
126
+ rect.setAttribute('width', width);
127
+ rect.setAttribute('height', height);
128
+ rect.setAttribute('fill', 'none');
129
+ rect.setAttribute('stroke', '#f59e0b');
130
+ rect.setAttribute('stroke-width', '3');
131
+ rect.setAttribute('stroke-dasharray', '8,4');
132
+ rect.style.pointerEvents = 'none';
133
+
134
+ overlay.appendChild(rect);
135
+ }
136
+
137
+ // Draw full panel bbox (union of all elements belonging to the panel)
138
+ function drawPanelBbox(axIndex) {
139
+ const overlay = document.getElementById('selection-overlay');
140
+ if (!overlay) return;
141
+
142
+ // Clear previous panel bbox
143
+ const existingBbox = document.getElementById('panel-bbox-highlight');
144
+ if (existingBbox) existingBbox.remove();
145
+
146
+ // Get panel_bboxes from currentBboxes metadata
147
+ const panelBboxes = currentBboxes?._meta?.panel_bboxes;
148
+ if (!panelBboxes || !panelBboxes[axIndex]) {
149
+ console.log('[PanelBbox] No panel bbox for ax', axIndex);
150
+ return;
151
+ }
152
+
153
+ const panelBbox = panelBboxes[axIndex];
154
+ const img = document.getElementById('preview-image');
155
+ if (!img || !img.naturalWidth || !img.naturalHeight) return;
156
+
157
+ // Ensure viewBox is set
158
+ overlay.setAttribute('viewBox', `0 0 ${img.naturalWidth} ${img.naturalHeight}`);
159
+ overlay.style.width = `${img.naturalWidth}px`;
160
+ overlay.style.height = `${img.naturalHeight}px`;
161
+
162
+ // panel bbox is already in pixel coordinates
163
+ const x = panelBbox.x;
164
+ const y = panelBbox.y;
165
+ const width = panelBbox.width;
166
+ const height = panelBbox.height;
167
+
168
+ // Create panel bbox rectangle (different style from axis highlight)
169
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
170
+ rect.id = 'panel-bbox-highlight';
171
+ rect.setAttribute('x', x - 2); // Small padding
172
+ rect.setAttribute('y', y - 2);
173
+ rect.setAttribute('width', width + 4);
174
+ rect.setAttribute('height', height + 4);
175
+ rect.setAttribute('fill', 'rgba(59, 130, 246, 0.05)'); // Very light blue fill
176
+ rect.setAttribute('stroke', '#3b82f6'); // Blue stroke
177
+ rect.setAttribute('stroke-width', '2');
178
+ rect.setAttribute('stroke-dasharray', '6,3');
179
+ rect.style.pointerEvents = 'none';
180
+
181
+ overlay.appendChild(rect);
182
+ console.log('[PanelBbox] Drew bbox for panel', axIndex, panelBbox);
183
+ }
184
+
185
+ // Clear panel bbox highlight
186
+ function clearPanelBbox() {
187
+ const existingBbox = document.getElementById('panel-bbox-highlight');
188
+ if (existingBbox) existingBbox.remove();
189
+ }
190
+
191
+ // Clear panel selection highlight
192
+ function clearPanelSelectionHighlight() {
193
+ const existingHighlight = document.getElementById('panel-selection-highlight');
194
+ if (existingHighlight) existingHighlight.remove();
195
+ }
196
+
197
+ // Select panel by index (called when clicking on axes in canvas)
198
+ function selectPanelByIndex(axIndex, switchToAxisTab = true) {
199
+ // Update the current selected panel index
200
+ currentSelectedPanelIndex = axIndex;
201
+
202
+ // Update inputs and highlight
203
+ updatePanelPositionInputs();
204
+
205
+ // Draw panel bbox (union of all elements)
206
+ drawPanelBbox(axIndex);
207
+
208
+ // Update panel caption input
209
+ if (typeof updatePanelCaptionInput === 'function') {
210
+ updatePanelCaptionInput(axIndex);
211
+ }
212
+
213
+ // Switch to Axis tab only if requested (default true for backwards compat)
214
+ if (switchToAxisTab) {
215
+ switchTab('axis');
216
+ }
217
+
218
+ console.log('Selected panel', axIndex);
219
+ }
220
+
221
+ // Clear panel selection (called when selection is cleared)
222
+ function clearPanelSelection() {
223
+ currentSelectedPanelIndex = null;
224
+ updatePanelPositionInputs(false);
225
+ clearPanelSelectionHighlight();
226
+ clearPanelBbox();
227
+ }
228
+
229
+ // Find panel index from axes key (e.g., "ax_0" -> 0)
230
+ function getPanelIndexFromKey(key) {
231
+ if (!key) return null;
232
+
233
+ // Handle "ax_N" format
234
+ const match = key.match(/ax_(\d+)/);
235
+ if (match) {
236
+ return parseInt(match[1], 10);
237
+ }
238
+
239
+ // Handle axes type elements - find by checking bboxes
240
+ const axKeys = Object.keys(panelPositions).sort();
241
+ for (let i = 0; i < axKeys.length; i++) {
242
+ if (axKeys[i] === key) {
243
+ return i;
244
+ }
245
+ }
246
+
247
+ return null;
248
+ }
249
+
250
+ // Apply panel position changes
251
+ async function applyPanelPosition() {
252
+ const leftInput = document.getElementById('panel_left');
253
+ const topInput = document.getElementById('panel_top');
254
+ const widthInput = document.getElementById('panel_width');
255
+ const heightInput = document.getElementById('panel_height');
256
+
257
+ if (currentSelectedPanelIndex === null) {
258
+ console.error('No panel selected');
259
+ return;
260
+ }
261
+
262
+ if (!leftInput || !topInput || !widthInput || !heightInput) {
263
+ console.error('Panel position inputs not found');
264
+ return;
265
+ }
266
+
267
+ const axIndex = currentSelectedPanelIndex;
268
+ const left = parseFloat(leftInput.value);
269
+ const top = parseFloat(topInput.value);
270
+ const width = parseFloat(widthInput.value);
271
+ const height = parseFloat(heightInput.value);
272
+
273
+ // Validate values (mm, must be positive and within figure bounds)
274
+ if ([left, top, width, height].some(v => isNaN(v) || v < 0)) {
275
+ alert('Position values must be positive numbers in mm');
276
+ return;
277
+ }
278
+
279
+ if (left + width > figSize.width_mm || top + height > figSize.height_mm) {
280
+ alert(`Panel extends beyond figure bounds (${figSize.width_mm.toFixed(1)} x ${figSize.height_mm.toFixed(1)} mm)`);
281
+ return;
282
+ }
283
+
284
+ document.body.classList.add('loading');
285
+
286
+ try {
287
+ const response = await fetch('/update_axes_position', {
288
+ method: 'POST',
289
+ headers: { 'Content-Type': 'application/json' },
290
+ body: JSON.stringify({
291
+ ax_index: axIndex,
292
+ left: left,
293
+ top: top,
294
+ width: width,
295
+ height: height
296
+ })
297
+ });
298
+
299
+ const data = await response.json();
300
+
301
+ if (data.success) {
302
+ // Update preview image and wait for it to load
303
+ const img = document.getElementById('preview-image');
304
+ if (img) {
305
+ await new Promise((resolve) => {
306
+ img.onload = resolve;
307
+ img.src = 'data:image/png;base64,' + data.image;
308
+ });
309
+ }
310
+
311
+ // Update image size
312
+ if (data.img_size) {
313
+ currentImgWidth = data.img_size.width;
314
+ currentImgHeight = data.img_size.height;
315
+ }
316
+
317
+ // Update bboxes
318
+ currentBboxes = data.bboxes;
319
+ loadHitmap();
320
+ updateHitRegions();
321
+
322
+ // Reload positions to reflect any adjustments (now with correct image dimensions)
323
+ await loadPanelPositions();
324
+
325
+ console.log('Panel position updated successfully');
326
+ } else {
327
+ alert('Failed to update position: ' + data.error);
328
+ }
329
+ } catch (error) {
330
+ alert('Failed to update position: ' + error.message);
331
+ }
332
+
333
+ document.body.classList.remove('loading');
334
+ }
335
+
336
+ // Initialize panel position controls
337
+ function initPanelPositionControls() {
338
+ const applyBtn = document.getElementById('apply_panel_position');
339
+ if (applyBtn) {
340
+ applyBtn.addEventListener('click', applyPanelPosition);
341
+ }
342
+
343
+ // Load initial positions
344
+ loadPanelPositions();
345
+
346
+ // Initialize debug toolbar if in debug mode
347
+ if (typeof DEBUG_MODE !== 'undefined' && DEBUG_MODE) {
348
+ initDebugToolbar();
349
+ }
350
+ }
351
+
352
+ // ===== DEBUG MODE: SHOW ALL BBOXES =====
353
+
354
+ let allBboxesVisible = false;
355
+
356
+ // Toggle showing all panel bboxes at once
357
+ function toggleAllBboxes() {
358
+ allBboxesVisible = !allBboxesVisible;
359
+
360
+ const overlay = document.getElementById('selection-overlay');
361
+ if (!overlay) return;
362
+
363
+ // Remove existing debug bboxes
364
+ document.querySelectorAll('.debug-panel-bbox').forEach(el => el.remove());
365
+
366
+ if (!allBboxesVisible) {
367
+ console.log('[Debug] All bboxes hidden');
368
+ updateDebugButton(false);
369
+ return;
370
+ }
371
+
372
+ const panelBboxes = currentBboxes?._meta?.panel_bboxes;
373
+ if (!panelBboxes) {
374
+ console.warn('[Debug] No panel bboxes available');
375
+ showToast('No panel bboxes available', 'warning');
376
+ return;
377
+ }
378
+
379
+ const img = document.getElementById('preview-image');
380
+ if (!img || !img.naturalWidth || !img.naturalHeight) return;
381
+
382
+ // Ensure viewBox is set
383
+ overlay.setAttribute('viewBox', `0 0 ${img.naturalWidth} ${img.naturalHeight}`);
384
+ overlay.style.width = `${img.naturalWidth}px`;
385
+ overlay.style.height = `${img.naturalHeight}px`;
386
+
387
+ // Colors for different panels
388
+ const colors = ['#ef4444', '#f59e0b', '#10b981', '#3b82f6', '#8b5cf6', '#ec4899', '#06b6d4', '#84cc16', '#f97316'];
389
+
390
+ // Draw bbox for each panel
391
+ Object.entries(panelBboxes).forEach(([axIdx, bbox], idx) => {
392
+ const color = colors[idx % colors.length];
393
+ const label = String.fromCharCode(65 + parseInt(axIdx)); // A, B, C...
394
+
395
+ // Create bbox rectangle
396
+ const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
397
+ rect.classList.add('debug-panel-bbox');
398
+ rect.setAttribute('x', bbox.x);
399
+ rect.setAttribute('y', bbox.y);
400
+ rect.setAttribute('width', bbox.width);
401
+ rect.setAttribute('height', bbox.height);
402
+ rect.setAttribute('fill', 'none');
403
+ rect.setAttribute('stroke', color);
404
+ rect.setAttribute('stroke-width', '2');
405
+ rect.setAttribute('stroke-dasharray', '5,3');
406
+ rect.style.pointerEvents = 'none';
407
+ overlay.appendChild(rect);
408
+
409
+ // Create label
410
+ const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
411
+ text.classList.add('debug-panel-bbox');
412
+ text.setAttribute('x', bbox.x + 5);
413
+ text.setAttribute('y', bbox.y + 15);
414
+ text.setAttribute('fill', color);
415
+ text.setAttribute('font-size', '14');
416
+ text.setAttribute('font-weight', 'bold');
417
+ text.style.pointerEvents = 'none';
418
+ text.textContent = `Panel ${label}`;
419
+ overlay.appendChild(text);
420
+ });
421
+
422
+ console.log('[Debug] Showing all panel bboxes:', Object.keys(panelBboxes).length);
423
+ updateDebugButton(true);
424
+ }
425
+
426
+ // Update debug button state
427
+ function updateDebugButton(active) {
428
+ const btn = document.getElementById('btn-debug-bboxes');
429
+ if (btn) {
430
+ btn.classList.toggle('active', active);
431
+ btn.title = active ? 'Hide All Bboxes (Alt+B)' : 'Show All Bboxes (Alt+B)';
432
+ }
433
+ }
434
+
435
+ // Initialize debug toolbar
436
+ function initDebugToolbar() {
437
+ // Find the preview controls area
438
+ const previewControls = document.querySelector('.preview-controls');
439
+ if (!previewControls) return;
440
+
441
+ // Create debug button
442
+ const debugBtn = document.createElement('button');
443
+ debugBtn.id = 'btn-debug-bboxes';
444
+ debugBtn.className = 'btn-icon btn-debug';
445
+ debugBtn.title = 'Show All Bboxes (Alt+B)';
446
+ debugBtn.innerHTML = '🔲';
447
+ debugBtn.style.cssText = 'margin-left: 8px; background: #374151; border: 1px dashed #f59e0b; color: #f59e0b;';
448
+ debugBtn.onclick = toggleAllBboxes;
449
+
450
+ // Add to controls
451
+ previewControls.appendChild(debugBtn);
452
+
453
+ console.log('[Debug] Debug toolbar initialized (FIGRECIPE_DEBUG_MODE=1)');
454
+ console.log('[Debug] Shortcuts: Alt+I = Element Inspector, Alt+B = Show All Bboxes');
455
+ }
456
+
457
+ // Call initialization on DOMContentLoaded
458
+ document.addEventListener('DOMContentLoaded', initPanelPositionControls);
459
+ """
460
+
461
+ __all__ = ["SCRIPTS_PANEL_POSITION"]
462
+
463
+ # EOF
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Panel resize JavaScript for the figure editor.
4
+
5
+ This module handles resizing of panels by dragging dividers:
6
+ - File browser panel (left)
7
+ - Data panel (left)
8
+ - Properties panel (right)
9
+
10
+ Features smart accordion:
11
+ - Auto-collapse when dragged below threshold
12
+ - Auto-expand when dragging from collapsed state
13
+ """
14
+
15
+ SCRIPTS_PANEL_RESIZE = """
16
+ // ==================== PANEL RESIZE ====================
17
+ // Enables dragging panel dividers to resize panels with smart accordion
18
+
19
+ // Store default widths for each panel
20
+ const panelDefaultWidths = {
21
+ 'file-browser-panel': 200,
22
+ 'datatable-panel': 280,
23
+ 'controls-panel': 350
24
+ };
25
+
26
+ // Collapse threshold - panels collapse when dragged below this width
27
+ const COLLAPSE_THRESHOLD = 60;
28
+
29
+ // Expand threshold - collapsed panels expand when dragged beyond this
30
+ const EXPAND_THRESHOLD = 50;
31
+
32
+ function initPanelResize() {
33
+ // File browser resize handle
34
+ const fileBrowserResize = document.getElementById('file-browser-resize');
35
+ const fileBrowserPanel = document.getElementById('file-browser-panel');
36
+
37
+ // Datatable resize handle
38
+ const datatableResize = document.getElementById('datatable-resize');
39
+ const datatablePanel = document.getElementById('datatable-panel');
40
+
41
+ // Properties panel resize handle (will be added to controls-panel)
42
+ const controlsPanel = document.querySelector('.controls-panel');
43
+
44
+ if (fileBrowserResize && fileBrowserPanel) {
45
+ initSmartResizer(fileBrowserResize, fileBrowserPanel, 'left', 'file-browser-panel');
46
+ }
47
+
48
+ if (datatableResize && datatablePanel) {
49
+ initSmartResizer(datatableResize, datatablePanel, 'left', 'datatable-panel');
50
+ }
51
+
52
+ // Add resize handle for properties panel
53
+ if (controlsPanel) {
54
+ let propertiesResize = document.getElementById('properties-resize');
55
+ if (!propertiesResize) {
56
+ propertiesResize = document.createElement('div');
57
+ propertiesResize.id = 'properties-resize';
58
+ propertiesResize.className = 'properties-resize';
59
+ controlsPanel.insertBefore(propertiesResize, controlsPanel.firstChild);
60
+ }
61
+ initSmartResizer(propertiesResize, controlsPanel, 'right', 'controls-panel');
62
+ }
63
+
64
+ console.log('[PanelResize] Initialized with smart accordion');
65
+ }
66
+
67
+ function initSmartResizer(resizeHandle, panel, side, panelId) {
68
+ let isResizing = false;
69
+ let startX = 0;
70
+ let startWidth = 0;
71
+ let wasCollapsed = false;
72
+
73
+ // Handle click on resize handle when panel is collapsed - expand it
74
+ resizeHandle.addEventListener('click', (e) => {
75
+ if (panel.classList.contains('collapsed')) {
76
+ e.stopPropagation();
77
+ expandPanel(panel, panelId);
78
+ }
79
+ });
80
+
81
+ resizeHandle.addEventListener('mousedown', (e) => {
82
+ isResizing = true;
83
+ startX = e.clientX;
84
+ wasCollapsed = panel.classList.contains('collapsed');
85
+
86
+ // If collapsed, start with minimal width
87
+ if (wasCollapsed) {
88
+ startWidth = panel.offsetWidth;
89
+ } else {
90
+ startWidth = panel.offsetWidth;
91
+ // Save current width as default if it's reasonable
92
+ if (startWidth > COLLAPSE_THRESHOLD) {
93
+ panelDefaultWidths[panelId] = startWidth;
94
+ }
95
+ }
96
+
97
+ resizeHandle.classList.add('resizing');
98
+ document.body.style.cursor = 'col-resize';
99
+ document.body.style.userSelect = 'none';
100
+ e.preventDefault();
101
+ });
102
+
103
+ document.addEventListener('mousemove', (e) => {
104
+ if (!isResizing) return;
105
+
106
+ const deltaX = e.clientX - startX;
107
+ let newWidth;
108
+
109
+ if (side === 'left') {
110
+ // For left panel, positive delta increases width
111
+ newWidth = startWidth + deltaX;
112
+ } else {
113
+ // For right panel, negative delta increases width
114
+ newWidth = startWidth - deltaX;
115
+ }
116
+
117
+ // Smart accordion behavior
118
+ if (wasCollapsed) {
119
+ // Expanding from collapsed state
120
+ if (Math.abs(deltaX) > EXPAND_THRESHOLD) {
121
+ // Expand to default width
122
+ expandPanel(panel, panelId);
123
+ // Continue resizing from expanded state
124
+ startX = e.clientX;
125
+ startWidth = panelDefaultWidths[panelId];
126
+ wasCollapsed = false;
127
+ }
128
+ } else {
129
+ // Normal resize - check for collapse threshold
130
+ if (newWidth < COLLAPSE_THRESHOLD) {
131
+ // Collapse the panel
132
+ collapsePanel(panel, panelId);
133
+ isResizing = false;
134
+ resizeHandle.classList.remove('resizing');
135
+ document.body.style.cursor = '';
136
+ document.body.style.userSelect = '';
137
+ return;
138
+ }
139
+
140
+ // Clamp to min/max (but above collapse threshold)
141
+ const minWidth = Math.max(COLLAPSE_THRESHOLD, parseInt(getComputedStyle(panel).minWidth) || 160);
142
+ const maxWidth = parseInt(getComputedStyle(panel).maxWidth) || 500;
143
+ newWidth = Math.max(minWidth, Math.min(maxWidth, newWidth));
144
+
145
+ panel.style.width = newWidth + 'px';
146
+ }
147
+ });
148
+
149
+ document.addEventListener('mouseup', () => {
150
+ if (isResizing) {
151
+ isResizing = false;
152
+ resizeHandle.classList.remove('resizing');
153
+ document.body.style.cursor = '';
154
+ document.body.style.userSelect = '';
155
+
156
+ // Save width if not collapsed
157
+ if (!panel.classList.contains('collapsed')) {
158
+ const currentWidth = panel.offsetWidth;
159
+ if (currentWidth > COLLAPSE_THRESHOLD) {
160
+ panelDefaultWidths[panelId] = currentWidth;
161
+ localStorage.setItem(`figrecipe_${panelId}_width`, currentWidth);
162
+ }
163
+ }
164
+ }
165
+ });
166
+
167
+ // Also handle mouse leaving window
168
+ document.addEventListener('mouseleave', () => {
169
+ if (isResizing) {
170
+ isResizing = false;
171
+ resizeHandle.classList.remove('resizing');
172
+ document.body.style.cursor = '';
173
+ document.body.style.userSelect = '';
174
+ }
175
+ });
176
+ }
177
+
178
+ function collapsePanel(panel, panelId) {
179
+ panel.classList.add('collapsed');
180
+
181
+ // Update localStorage
182
+ const storageKey = getStorageKey(panelId);
183
+ if (storageKey) {
184
+ localStorage.setItem(storageKey, 'true');
185
+ }
186
+
187
+ // Dispatch custom event for accordion sync
188
+ panel.dispatchEvent(new CustomEvent('panelCollapsed', { bubbles: true }));
189
+ console.log(`[PanelResize] ${panelId} collapsed via drag`);
190
+ }
191
+
192
+ function expandPanel(panel, panelId) {
193
+ panel.classList.remove('collapsed');
194
+
195
+ // Restore saved width or default
196
+ const savedWidth = localStorage.getItem(`figrecipe_${panelId}_width`);
197
+ const width = savedWidth ? parseInt(savedWidth) : panelDefaultWidths[panelId];
198
+ panel.style.width = width + 'px';
199
+
200
+ // Update localStorage
201
+ const storageKey = getStorageKey(panelId);
202
+ if (storageKey) {
203
+ localStorage.setItem(storageKey, 'false');
204
+ }
205
+
206
+ // Dispatch custom event for accordion sync
207
+ panel.dispatchEvent(new CustomEvent('panelExpanded', { bubbles: true }));
208
+ console.log(`[PanelResize] ${panelId} expanded via drag to ${width}px`);
209
+ }
210
+
211
+ function getStorageKey(panelId) {
212
+ const keyMap = {
213
+ 'file-browser-panel': 'figrecipe_filebrowser_collapsed',
214
+ 'datatable-panel': 'figrecipe_data_collapsed',
215
+ 'controls-panel': 'figrecipe_properties_collapsed'
216
+ };
217
+ return keyMap[panelId];
218
+ }
219
+
220
+ // Initialize on DOM ready
221
+ if (document.readyState === 'loading') {
222
+ document.addEventListener('DOMContentLoaded', initPanelResize);
223
+ } else {
224
+ initPanelResize();
225
+ }
226
+ """
227
+
228
+ __all__ = ["SCRIPTS_PANEL_RESIZE"]
229
+
230
+ # EOF