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
@@ -12,6 +12,7 @@ SCRIPTS_PANEL_POSITION = r"""
12
12
 
13
13
  let panelPositions = {};
14
14
  let figSize = { width_mm: 0, height_mm: 0 };
15
+ let currentSelectedPanelIndex = null; // Track currently selected panel
15
16
 
16
17
  // Load panel positions from server
17
18
  async function loadPanelPositions() {
@@ -26,54 +27,57 @@ async function loadPanelPositions() {
26
27
  }
27
28
 
28
29
  panelPositions = data;
29
- updatePanelSelector();
30
- updatePanelPositionInputs();
30
+ // Update inputs if a panel is selected
31
+ if (currentSelectedPanelIndex !== null) {
32
+ updatePanelPositionInputs();
33
+ }
31
34
  } catch (error) {
32
35
  console.error('Failed to load panel positions:', error);
33
36
  }
34
37
  }
35
38
 
36
- // Update panel selector dropdown
37
- function updatePanelSelector() {
38
- const selector = document.getElementById('panel_selector');
39
- if (!selector) return;
40
-
41
- selector.innerHTML = '';
42
- const axKeys = Object.keys(panelPositions).sort();
43
-
44
- axKeys.forEach((key, index) => {
45
- const opt = document.createElement('option');
46
- opt.value = index;
47
- // Show panel label (A, B, C...) if available
48
- const label = String.fromCharCode(65 + index); // A, B, C...
49
- opt.textContent = `Panel ${label} (${key})`;
50
- selector.appendChild(opt);
51
- });
52
-
53
- // Add change event listener - mark as explicitly selected on dropdown change
54
- selector.addEventListener('change', () => {
55
- panelExplicitlySelected = true;
56
- updatePanelPositionInputs();
57
- });
58
- }
59
-
60
- let panelExplicitlySelected = false;
61
-
62
- // Update position input fields based on selected panel
39
+ // Update position input fields based on currently selected panel
63
40
  function updatePanelPositionInputs(showHighlight = true) {
64
- const selector = document.getElementById('panel_selector');
65
- if (!selector) return;
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
+ }
66
63
 
67
- const axIndex = parseInt(selector.value, 10);
68
- const axKey = Object.keys(panelPositions).sort()[axIndex];
64
+ const axKey = Object.keys(panelPositions).sort()[currentSelectedPanelIndex];
69
65
  const pos = panelPositions[axKey];
70
66
 
71
67
  if (!pos) return;
72
68
 
73
- const leftInput = document.getElementById('panel_left');
74
- const topInput = document.getElementById('panel_top');
75
- const widthInput = document.getElementById('panel_width');
76
- const heightInput = document.getElementById('panel_height');
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;
77
81
 
78
82
  // Values are already in mm from server
79
83
  if (leftInput) leftInput.value = pos.left;
@@ -81,13 +85,13 @@ function updatePanelPositionInputs(showHighlight = true) {
81
85
  if (widthInput) widthInput.value = pos.width;
82
86
  if (heightInput) heightInput.value = pos.height;
83
87
 
84
- // Only draw highlight if explicitly selected (not on initial load)
85
- if (showHighlight && panelExplicitlySelected) {
88
+ // Draw highlight
89
+ if (showHighlight) {
86
90
  drawPanelSelectionHighlight(pos);
87
91
  }
88
92
  }
89
93
 
90
- // Draw visual highlight around selected panel
94
+ // Draw visual highlight around selected panel (axis bbox only)
91
95
  function drawPanelSelectionHighlight(pos) {
92
96
  const overlay = document.getElementById('selection-overlay');
93
97
  if (!overlay) return;
@@ -130,6 +134,60 @@ function drawPanelSelectionHighlight(pos) {
130
134
  overlay.appendChild(rect);
131
135
  }
132
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
+
133
191
  // Clear panel selection highlight
134
192
  function clearPanelSelectionHighlight() {
135
193
  const existingHighlight = document.getElementById('panel-selection-highlight');
@@ -137,25 +195,37 @@ function clearPanelSelectionHighlight() {
137
195
  }
138
196
 
139
197
  // Select panel by index (called when clicking on axes in canvas)
140
- function selectPanelByIndex(axIndex) {
141
- const selector = document.getElementById('panel_selector');
142
- if (!selector) return;
143
-
144
- // Mark as explicitly selected
145
- panelExplicitlySelected = true;
146
-
147
- // Update dropdown selection
148
- selector.value = axIndex;
198
+ function selectPanelByIndex(axIndex, switchToAxisTab = true) {
199
+ // Update the current selected panel index
200
+ currentSelectedPanelIndex = axIndex;
149
201
 
150
202
  // Update inputs and highlight
151
203
  updatePanelPositionInputs();
152
204
 
153
- // Switch to Axis tab
154
- switchTab('axis');
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
+ }
155
217
 
156
218
  console.log('Selected panel', axIndex);
157
219
  }
158
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
+
159
229
  // Find panel index from axes key (e.g., "ax_0" -> 0)
160
230
  function getPanelIndexFromKey(key) {
161
231
  if (!key) return null;
@@ -179,18 +249,22 @@ function getPanelIndexFromKey(key) {
179
249
 
180
250
  // Apply panel position changes
181
251
  async function applyPanelPosition() {
182
- const selector = document.getElementById('panel_selector');
183
252
  const leftInput = document.getElementById('panel_left');
184
253
  const topInput = document.getElementById('panel_top');
185
254
  const widthInput = document.getElementById('panel_width');
186
255
  const heightInput = document.getElementById('panel_height');
187
256
 
188
- if (!selector || !leftInput || !topInput || !widthInput || !heightInput) {
257
+ if (currentSelectedPanelIndex === null) {
258
+ console.error('No panel selected');
259
+ return;
260
+ }
261
+
262
+ if (!leftInput || !topInput || !widthInput || !heightInput) {
189
263
  console.error('Panel position inputs not found');
190
264
  return;
191
265
  }
192
266
 
193
- const axIndex = parseInt(selector.value, 10);
267
+ const axIndex = currentSelectedPanelIndex;
194
268
  const left = parseFloat(leftInput.value);
195
269
  const top = parseFloat(topInput.value);
196
270
  const width = parseFloat(widthInput.value);
@@ -268,6 +342,116 @@ function initPanelPositionControls() {
268
342
 
269
343
  // Load initial positions
270
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');
271
455
  }
272
456
 
273
457
  // Call initialization on DOMContentLoaded
@@ -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