@sequent-org/moodboard 1.3.4 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/package.json +6 -1
  2. package/src/assets/icons/mindmap.svg +3 -0
  3. package/src/core/SaveManager.js +44 -15
  4. package/src/core/commands/MindmapStatePatchCommand.js +85 -0
  5. package/src/core/commands/UpdateContentCommand.js +47 -4
  6. package/src/core/flows/LayerAndViewportFlow.js +87 -14
  7. package/src/core/flows/ObjectLifecycleFlow.js +7 -2
  8. package/src/core/flows/SaveFlow.js +10 -7
  9. package/src/core/flows/TransformFlow.js +2 -2
  10. package/src/core/index.js +81 -11
  11. package/src/core/rendering/ObjectRenderer.js +7 -2
  12. package/src/grid/BaseGrid.js +65 -0
  13. package/src/grid/CrossGrid.js +89 -24
  14. package/src/grid/CrossGridZoomPhases.js +167 -0
  15. package/src/grid/DotGrid.js +117 -34
  16. package/src/grid/DotGridZoomPhases.js +214 -16
  17. package/src/grid/GridDiagnostics.js +80 -0
  18. package/src/grid/GridFactory.js +13 -11
  19. package/src/grid/LineGrid.js +176 -37
  20. package/src/grid/LineGridZoomPhases.js +163 -0
  21. package/src/grid/ScreenGridPhaseMachine.js +51 -0
  22. package/src/mindmap/MindmapCompoundContract.js +235 -0
  23. package/src/moodboard/ActionHandler.js +1 -0
  24. package/src/moodboard/DataManager.js +57 -0
  25. package/src/moodboard/bootstrap/MoodBoardUiFactory.js +21 -0
  26. package/src/moodboard/integration/MoodBoardEventBindings.js +26 -1
  27. package/src/moodboard/lifecycle/MoodBoardDestroyer.js +15 -0
  28. package/src/objects/MindmapObject.js +76 -0
  29. package/src/objects/ObjectFactory.js +3 -1
  30. package/src/services/BoardService.js +127 -31
  31. package/src/services/GridSnapResolver.js +60 -0
  32. package/src/services/MiroZoomLevels.js +39 -0
  33. package/src/services/SettingsApplier.js +0 -4
  34. package/src/services/ZoomPanController.js +51 -32
  35. package/src/tools/object-tools/PlacementTool.js +12 -3
  36. package/src/tools/object-tools/SelectTool.js +11 -1
  37. package/src/tools/object-tools/placement/GhostController.js +100 -1
  38. package/src/tools/object-tools/placement/PlacementEventsBridge.js +2 -0
  39. package/src/tools/object-tools/placement/PlacementInputRouter.js +2 -2
  40. package/src/tools/object-tools/selection/FileNameInlineEditorController.js +2 -2
  41. package/src/tools/object-tools/selection/InlineEditorController.js +15 -0
  42. package/src/tools/object-tools/selection/MindmapInlineEditorController.js +716 -0
  43. package/src/tools/object-tools/selection/SelectInputRouter.js +6 -0
  44. package/src/tools/object-tools/selection/SelectToolSetup.js +2 -0
  45. package/src/tools/object-tools/selection/TextEditorLifecycleRegistry.js +12 -16
  46. package/src/ui/ContextMenu.js +6 -6
  47. package/src/ui/DotGridDebugPanel.js +253 -0
  48. package/src/ui/HtmlTextLayer.js +1 -1
  49. package/src/ui/TextPropertiesPanel.js +2 -2
  50. package/src/ui/handles/GroupSelectionHandlesController.js +4 -1
  51. package/src/ui/handles/HandlesDomRenderer.js +1486 -15
  52. package/src/ui/handles/HandlesEventBridge.js +49 -5
  53. package/src/ui/handles/HandlesInteractionController.js +4 -4
  54. package/src/ui/mindmap/MindmapConnectionLayer.js +239 -0
  55. package/src/ui/mindmap/MindmapHtmlTextLayer.js +285 -0
  56. package/src/ui/mindmap/MindmapLayoutConfig.js +29 -0
  57. package/src/ui/mindmap/MindmapTextOverlayAdapter.js +144 -0
  58. package/src/ui/styles/toolbar.css +1 -0
  59. package/src/ui/styles/workspace.css +100 -0
  60. package/src/ui/toolbar/ToolbarActionRouter.js +35 -0
  61. package/src/ui/toolbar/ToolbarPopupsController.js +6 -6
  62. package/src/ui/toolbar/ToolbarRenderer.js +1 -0
  63. package/src/ui/toolbar/ToolbarStateController.js +1 -0
  64. package/src/utils/iconLoader.js +10 -4
@@ -3,11 +3,17 @@ import { Events } from '../../../core/events/Events.js';
3
3
  export function onMouseDown(event) {
4
4
  // Если активен текстовый редактор, закрываем его при клике вне
5
5
  if (this.textEditor.active) {
6
+ const activeEditorType = this.textEditor.objectType;
6
7
  if (this.textEditor.objectType === 'file') {
7
8
  this._closeFileNameEditor(true);
8
9
  } else {
9
10
  this._closeTextEditor(true);
10
11
  }
12
+ // Mindmap UX: outside click should fully reset on first click
13
+ // (editor close + selection clear + handles/buttons hidden).
14
+ if (activeEditorType === 'mindmap') {
15
+ this.clearSelection();
16
+ }
11
17
  return; // Прерываем выполнение, чтобы не обрабатывать клик дальше
12
18
  }
13
19
 
@@ -93,6 +93,8 @@ export function registerSelectToolCoreSubscriptions(instance) {
93
93
  const objectType = object.type || (object.object && object.object.type) || 'text';
94
94
  if (objectType === 'file') {
95
95
  instance._openFileNameEditor(object, object.create || false);
96
+ } else if (objectType === 'mindmap') {
97
+ instance._openMindmapEditor(object, object.create || false);
96
98
  } else {
97
99
  if (object.create) {
98
100
  instance._openTextEditor(object, true);
@@ -20,31 +20,27 @@ export function updateGlobalTextEditorHandlesLayer() {
20
20
  export function hideStaticTextDuringEditing(controller, objectId) {
21
21
  if (!objectId) return;
22
22
 
23
- if (typeof window !== 'undefined' && window.moodboardHtmlTextLayer) {
24
- const el = window.moodboardHtmlTextLayer.idToEl.get(objectId);
25
- if (el) {
26
- controller.eventBus.emit(Events.Tool.HideObjectText, { objectId });
27
- } else {
28
- console.warn(`❌ SelectTool: HTML-элемент для объекта ${objectId} не найден, пропускаем HideObjectText`);
23
+ if (typeof window !== 'undefined') {
24
+ const textEl = window.moodboardHtmlTextLayer?.idToEl?.get?.(objectId);
25
+ const mindmapEl = window.moodboardMindmapHtmlTextLayer?.idToEl?.get?.(objectId);
26
+ if (!textEl && !mindmapEl) {
27
+ console.warn(`❌ SelectTool: HTML-элемент для объекта ${objectId} не найден, fallback через событие`);
29
28
  }
30
- } else {
31
- controller.eventBus.emit(Events.Tool.HideObjectText, { objectId });
32
29
  }
30
+ controller.eventBus.emit(Events.Tool.HideObjectText, { objectId });
33
31
  }
34
32
 
35
33
  export function showStaticTextAfterEditing(controller, objectId) {
36
34
  if (!objectId) return;
37
35
 
38
- if (typeof window !== 'undefined' && window.moodboardHtmlTextLayer) {
39
- const el = window.moodboardHtmlTextLayer.idToEl.get(objectId);
40
- if (el) {
41
- controller.eventBus.emit(Events.Tool.ShowObjectText, { objectId });
42
- } else {
43
- console.warn(`❌ SelectTool: HTML-элемент для объекта ${objectId} не найден, пропускаем ShowObjectText`);
36
+ if (typeof window !== 'undefined') {
37
+ const textEl = window.moodboardHtmlTextLayer?.idToEl?.get?.(objectId);
38
+ const mindmapEl = window.moodboardMindmapHtmlTextLayer?.idToEl?.get?.(objectId);
39
+ if (!textEl && !mindmapEl) {
40
+ console.warn(`❌ SelectTool: HTML-элемент для объекта ${objectId} не найден, fallback через событие`);
44
41
  }
45
- } else {
46
- controller.eventBus.emit(Events.Tool.ShowObjectText, { objectId });
47
42
  }
43
+ controller.eventBus.emit(Events.Tool.ShowObjectText, { objectId });
48
44
  }
49
45
 
50
46
  export function hideNotePixiText(controller, objectId) {
@@ -65,11 +65,11 @@ export class ContextMenu {
65
65
  }
66
66
 
67
67
  show(x, y, context = 'canvas', targetId = null) {
68
- this.lastX = x;
69
- this.lastY = y;
68
+ this.lastX = Math.round(x);
69
+ this.lastY = Math.round(y);
70
70
  this.renderItems(context, targetId);
71
- this.element.style.left = `${x}px`;
72
- this.element.style.top = `${y}px`;
71
+ this.element.style.left = `${this.lastX}px`;
72
+ this.element.style.top = `${this.lastY}px`;
73
73
  this.element.style.display = 'block';
74
74
  this.isVisible = true;
75
75
  this.ensureInViewport();
@@ -90,8 +90,8 @@ export class ContextMenu {
90
90
  if (dx !== 0 || dy !== 0) {
91
91
  const left = parseInt(this.element.style.left || '0', 10) + dx;
92
92
  const top = parseInt(this.element.style.top || '0', 10) + dy;
93
- this.element.style.left = `${left}px`;
94
- this.element.style.top = `${top}px`;
93
+ this.element.style.left = `${Math.round(left)}px`;
94
+ this.element.style.top = `${Math.round(top)}px`;
95
95
  }
96
96
  }
97
97
 
@@ -0,0 +1,253 @@
1
+ import { getCrossCheckpointForZoom, updateCrossCheckpoint } from '../grid/CrossGridZoomPhases.js';
2
+ import { Events } from '../core/events/Events.js';
3
+
4
+ function intToHex(color) {
5
+ const n = Math.max(0, Math.min(0xFFFFFF, Math.round(Number(color) || 0)));
6
+ return `#${n.toString(16).padStart(6, '0')}`;
7
+ }
8
+
9
+ function hexToInt(hex) {
10
+ const h = String(hex || '').trim().replace('#', '');
11
+ if (!/^[0-9a-fA-F]{6}$/.test(h)) return null;
12
+ return Number.parseInt(h, 16);
13
+ }
14
+
15
+ export class DotGridDebugPanel {
16
+ constructor(container, coreMoodboard) {
17
+ this.container = container;
18
+ this.core = coreMoodboard;
19
+ this.element = null;
20
+ this._pollTimer = null;
21
+ this._activeCheckpoint = null;
22
+ this._isApplying = false;
23
+ this._init();
24
+ }
25
+
26
+ _init() {
27
+ this._create();
28
+ this._attach();
29
+ this._startPolling();
30
+ }
31
+
32
+ _create() {
33
+ const panel = document.createElement('div');
34
+ panel.style.position = 'absolute';
35
+ panel.style.right = '16px';
36
+ panel.style.top = '16px';
37
+ panel.style.zIndex = '9999';
38
+ panel.style.width = '260px';
39
+ panel.style.padding = '10px';
40
+ panel.style.border = '1px solid #d9dee8';
41
+ panel.style.borderRadius = '10px';
42
+ panel.style.background = 'rgba(255,255,255,0.97)';
43
+ panel.style.boxShadow = '0 6px 20px rgba(0,0,0,0.12)';
44
+ panel.style.fontFamily = 'Segoe UI, Arial, sans-serif';
45
+ panel.style.fontSize = '12px';
46
+ panel.style.color = '#1f2937';
47
+
48
+ const title = document.createElement('div');
49
+ title.textContent = 'Cross Grid Debug';
50
+ title.style.fontWeight = '700';
51
+ title.style.marginBottom = '8px';
52
+ panel.appendChild(title);
53
+
54
+ this.infoEl = document.createElement('div');
55
+ this.infoEl.style.marginBottom = '8px';
56
+ this.infoEl.style.color = '#475569';
57
+ this.infoEl.textContent = 'zoom: -, checkpoint: -, grid: -';
58
+ panel.appendChild(this.infoEl);
59
+
60
+ const makeRow = (labelText, inputEl) => {
61
+ const row = document.createElement('div');
62
+ row.style.marginBottom = '8px';
63
+ const label = document.createElement('label');
64
+ label.textContent = labelText;
65
+ label.style.display = 'block';
66
+ label.style.marginBottom = '4px';
67
+ row.appendChild(label);
68
+ row.appendChild(inputEl);
69
+ panel.appendChild(row);
70
+ };
71
+
72
+ this.crossSizeRange = document.createElement('input');
73
+ this.crossSizeRange.type = 'range';
74
+ this.crossSizeRange.min = '1';
75
+ this.crossSizeRange.max = '12';
76
+ this.crossSizeRange.step = '1';
77
+ this.crossSizeRange.style.width = '100%';
78
+ makeRow('cross half size', this.crossSizeRange);
79
+
80
+ this.crossSizeValue = document.createElement('div');
81
+ this.crossSizeValue.style.marginTop = '-6px';
82
+ this.crossSizeValue.style.marginBottom = '8px';
83
+ this.crossSizeValue.style.color = '#64748b';
84
+ panel.appendChild(this.crossSizeValue);
85
+
86
+ this.spacingRange = document.createElement('input');
87
+ this.spacingRange.type = 'range';
88
+ this.spacingRange.min = '8';
89
+ this.spacingRange.max = '140';
90
+ this.spacingRange.step = '1';
91
+ this.spacingRange.style.width = '100%';
92
+ makeRow('spacing', this.spacingRange);
93
+
94
+ this.spacingValue = document.createElement('div');
95
+ this.spacingValue.style.marginTop = '-6px';
96
+ this.spacingValue.style.marginBottom = '8px';
97
+ this.spacingValue.style.color = '#64748b';
98
+ panel.appendChild(this.spacingValue);
99
+
100
+ this.colorInput = document.createElement('input');
101
+ this.colorInput.type = 'color';
102
+ this.colorInput.style.width = '100%';
103
+ this.colorInput.style.height = '30px';
104
+ this.colorInput.style.border = '1px solid #d1d5db';
105
+ this.colorInput.style.borderRadius = '6px';
106
+ makeRow('color', this.colorInput);
107
+
108
+ const controls = document.createElement('div');
109
+ controls.style.display = 'flex';
110
+ controls.style.gap = '6px';
111
+ controls.style.marginTop = '6px';
112
+
113
+ this.copyBtn = document.createElement('button');
114
+ this.copyBtn.textContent = 'Copy checkpoint';
115
+ this.copyBtn.style.flex = '1';
116
+ this.copyBtn.style.padding = '6px 8px';
117
+ this.copyBtn.style.border = '1px solid #cbd5e1';
118
+ this.copyBtn.style.borderRadius = '6px';
119
+ this.copyBtn.style.cursor = 'pointer';
120
+ this.copyBtn.style.background = '#f8fafc';
121
+ controls.appendChild(this.copyBtn);
122
+
123
+ this.enableCrossBtn = document.createElement('button');
124
+ this.enableCrossBtn.textContent = 'Set Cross';
125
+ this.enableCrossBtn.style.flex = '0 0 80px';
126
+ this.enableCrossBtn.style.padding = '6px 8px';
127
+ this.enableCrossBtn.style.border = '1px solid #93c5fd';
128
+ this.enableCrossBtn.style.borderRadius = '6px';
129
+ this.enableCrossBtn.style.cursor = 'pointer';
130
+ this.enableCrossBtn.style.background = '#dbeafe';
131
+ controls.appendChild(this.enableCrossBtn);
132
+
133
+ panel.appendChild(controls);
134
+ this.element = panel;
135
+ this.container.appendChild(panel);
136
+ }
137
+
138
+ _attach() {
139
+ this._onCrossSizeInput = () => this._applyPatch({ crossHalfSize: Number(this.crossSizeRange.value) });
140
+ this._onSpacingInput = () => this._applyPatch({ spacing: Number(this.spacingRange.value) });
141
+ this._onColorInput = () => {
142
+ const color = hexToInt(this.colorInput.value);
143
+ if (color != null) this._applyPatch({ color });
144
+ };
145
+ this._onCopyClick = async () => {
146
+ if (!this._activeCheckpoint) return;
147
+ const worldScale = this._getWorldScale();
148
+ const payload = {
149
+ zoomPercent: this._activeCheckpoint.zoomPercent,
150
+ zoomLabel: `${Math.round((worldScale || 1) * 100)}%`,
151
+ gridType: 'cross',
152
+ spacing: this._activeCheckpoint.spacing,
153
+ crossHalfSize: this._activeCheckpoint.crossHalfSize,
154
+ color: `0x${Number(this._activeCheckpoint.color || 0).toString(16).toUpperCase().padStart(6, '0')}`,
155
+ };
156
+ const text = JSON.stringify(payload);
157
+ try {
158
+ await navigator.clipboard.writeText(text);
159
+ this.copyBtn.textContent = 'Copied';
160
+ setTimeout(() => { this.copyBtn.textContent = 'Copy checkpoint'; }, 1000);
161
+ } catch (_) {
162
+ this.copyBtn.textContent = 'Copy failed';
163
+ setTimeout(() => { this.copyBtn.textContent = 'Copy checkpoint'; }, 1000);
164
+ }
165
+ };
166
+ this._onEnableCrossClick = () => {
167
+ const eventBus = this.core?.eventBus;
168
+ if (eventBus) {
169
+ eventBus.emit(Events.UI.GridChange, { type: 'cross' });
170
+ return;
171
+ }
172
+ };
173
+
174
+ this.crossSizeRange.addEventListener('input', this._onCrossSizeInput);
175
+ this.spacingRange.addEventListener('input', this._onSpacingInput);
176
+ this.colorInput.addEventListener('input', this._onColorInput);
177
+ this.copyBtn.addEventListener('click', this._onCopyClick);
178
+ this.enableCrossBtn.addEventListener('click', this._onEnableCrossClick);
179
+ }
180
+
181
+ _startPolling() {
182
+ this._pollTimer = setInterval(() => {
183
+ const scale = this._getWorldScale();
184
+ if (!Number.isFinite(scale)) return;
185
+ const checkpoint = getCrossCheckpointForZoom(scale);
186
+ if (!checkpoint) return;
187
+ const grid = this._getGrid();
188
+ this.infoEl.textContent = `zoom: ${Math.round(scale * 100)}%, checkpoint: ${checkpoint.zoomPercent}%, grid: ${grid?.type || 'none'}`;
189
+
190
+ const changed = !this._activeCheckpoint || this._activeCheckpoint.zoomPercent !== checkpoint.zoomPercent;
191
+ this._activeCheckpoint = checkpoint;
192
+ if (changed && !this._isApplying) {
193
+ this.crossSizeRange.value = String(checkpoint.crossHalfSize);
194
+ this.spacingRange.value = String(checkpoint.spacing);
195
+ this.colorInput.value = intToHex(checkpoint.color);
196
+ }
197
+ this.crossSizeValue.textContent = `cross half size: ${this.crossSizeRange.value}px`;
198
+ this.spacingValue.textContent = `spacing: ${this.spacingRange.value}px`;
199
+ }, 150);
200
+ }
201
+
202
+ _getWorldScale() {
203
+ const world = this.core?.pixi?.worldLayer || this.core?.pixi?.app?.stage;
204
+ return world?.scale?.x ?? null;
205
+ }
206
+
207
+ _getGrid() {
208
+ return this.core?.boardService?.grid || null;
209
+ }
210
+
211
+ _refreshGridVisual() {
212
+ const boardService = this.core?.boardService;
213
+ if (boardService && typeof boardService.refreshGridViewport === 'function') {
214
+ boardService.refreshGridViewport();
215
+ return;
216
+ }
217
+ const grid = this._getGrid();
218
+ if (grid && typeof grid.updateVisual === 'function') {
219
+ grid.updateVisual();
220
+ }
221
+ }
222
+
223
+ _applyPatch(patch) {
224
+ if (!this._activeCheckpoint) return;
225
+ this._isApplying = true;
226
+ const updated = updateCrossCheckpoint(this._activeCheckpoint.zoomPercent, patch);
227
+ if (updated) {
228
+ this._activeCheckpoint = updated;
229
+ this.crossSizeRange.value = String(updated.crossHalfSize);
230
+ this.spacingRange.value = String(updated.spacing);
231
+ this.colorInput.value = intToHex(updated.color);
232
+ this.crossSizeValue.textContent = `cross half size: ${this.crossSizeRange.value}px`;
233
+ this.spacingValue.textContent = `spacing: ${this.spacingRange.value}px`;
234
+ this._refreshGridVisual();
235
+ }
236
+ this._isApplying = false;
237
+ }
238
+
239
+ destroy() {
240
+ if (this._pollTimer) {
241
+ clearInterval(this._pollTimer);
242
+ this._pollTimer = null;
243
+ }
244
+ this.crossSizeRange?.removeEventListener('input', this._onCrossSizeInput);
245
+ this.spacingRange?.removeEventListener('input', this._onSpacingInput);
246
+ this.colorInput?.removeEventListener('input', this._onColorInput);
247
+ this.copyBtn?.removeEventListener('click', this._onCopyClick);
248
+ this.enableCrossBtn?.removeEventListener('click', this._onEnableCrossClick);
249
+ if (this.element) this.element.remove();
250
+ this.element = null;
251
+ }
252
+ }
253
+
@@ -199,7 +199,7 @@ export class HtmlTextLayer {
199
199
  el.dataset.id = objectId;
200
200
  // Получаем свойства из properties объекта
201
201
  const fontFamily = objectData.properties?.fontFamily || objectData.fontFamily || 'Caveat, Arial, cursive';
202
- const color = objectData.color || objectData.properties?.color || '#000000';
202
+ const color = objectData.color || objectData.properties?.color || objectData.properties?.textColor || '#000000';
203
203
  const backgroundColor = objectData.backgroundColor || objectData.properties?.backgroundColor || 'transparent';
204
204
 
205
205
  // Базовый line-height исходя из стартового размера шрифта
@@ -258,8 +258,8 @@ export class TextPropertiesPanel {
258
258
  const finalX = Math.max(10, Math.min(panelX, containerRect.width - this.panel.offsetWidth - 10));
259
259
  const finalY = Math.max(10, panelY);
260
260
 
261
- this.panel.style.left = `${finalX}px`;
262
- this.panel.style.top = `${finalY}px`;
261
+ this.panel.style.left = `${Math.round(finalX)}px`;
262
+ this.panel.style.top = `${Math.round(finalY)}px`;
263
263
  }
264
264
 
265
265
  _onDocMouseDown(event) {
@@ -15,6 +15,7 @@ export class GroupSelectionHandlesController {
15
15
  height: preview.startBounds.height,
16
16
  }, '__group__', {
17
17
  rotation: preview.angle || 0,
18
+ selectionIds: ids,
18
19
  });
19
20
  return;
20
21
  }
@@ -24,6 +25,8 @@ export class GroupSelectionHandlesController {
24
25
  this.host.hide();
25
26
  return;
26
27
  }
27
- this.host._showBounds(worldBounds, '__group__');
28
+ this.host._showBounds(worldBounds, '__group__', {
29
+ selectionIds: ids,
30
+ });
28
31
  }
29
32
  }