@sequent-org/moodboard 1.4.32 → 1.4.33

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 (136) hide show
  1. package/package.json +5 -1
  2. package/src/assets/fonts/inter/inter-cyrillic-400-normal.woff2 +0 -0
  3. package/src/assets/fonts/inter/inter-cyrillic-500-normal.woff2 +0 -0
  4. package/src/assets/fonts/inter/inter-latin-400-normal.woff2 +0 -0
  5. package/src/assets/fonts/inter/inter-latin-500-normal.woff2 +0 -0
  6. package/src/assets/icons/attachments.svg +3 -1
  7. package/src/assets/icons/comments.svg +2 -2
  8. package/src/assets/icons/connector.svg +6 -0
  9. package/src/assets/icons/emoji.svg +6 -1
  10. package/src/assets/icons/frame.svg +4 -1
  11. package/src/assets/icons/image.svg +5 -1
  12. package/src/assets/icons/laser.svg +1 -0
  13. package/src/assets/icons/lasso.svg +5 -0
  14. package/src/assets/icons/mindmap.svg +10 -2
  15. package/src/assets/icons/note.svg +4 -1
  16. package/src/assets/icons/pan.svg +5 -2
  17. package/src/assets/icons/pencil.svg +4 -1
  18. package/src/assets/icons/reactions.svg +5 -0
  19. package/src/assets/icons/redo.svg +3 -2
  20. package/src/assets/icons/select.svg +2 -8
  21. package/src/assets/icons/shapes.svg +5 -1
  22. package/src/assets/icons/text-add.svg +15 -1
  23. package/src/assets/icons/undo.svg +3 -2
  24. package/src/assets/reactions/1f44d.svg +20 -0
  25. package/src/assets/reactions/1f44e.svg +20 -0
  26. package/src/assets/reactions/2705.svg +20 -0
  27. package/src/assets/reactions/274c.svg +19 -0
  28. package/src/assets/reactions/2753.svg +20 -0
  29. package/src/assets/reactions/2764.svg +22 -0
  30. package/src/assets/reactions/2b50.svg +19 -0
  31. package/src/assets/reactions/plus-one.svg +25 -0
  32. package/src/core/PixiEngine.js +23 -0
  33. package/src/core/bootstrap/CoreInitializer.js +43 -0
  34. package/src/core/commands/GroupDeleteCommand.js +13 -1
  35. package/src/core/commands/UpdateShapeStyleCommand.js +121 -0
  36. package/src/core/commands/UpdateTextStyleCommand.js +17 -6
  37. package/src/core/commands/index.js +3 -0
  38. package/src/core/events/Events.js +22 -0
  39. package/src/core/flows/LayerAndViewportFlow.js +1 -0
  40. package/src/core/flows/ObjectLifecycleFlow.js +155 -7
  41. package/src/core/index.js +28 -1
  42. package/src/grid/CrossGridZoomPhases.js +3 -3
  43. package/src/initNoBundler.js +1 -1
  44. package/src/moodboard/DataManager.js +28 -0
  45. package/src/moodboard/MoodBoard.js +27 -0
  46. package/src/moodboard/bootstrap/MoodBoardInitializer.js +69 -1
  47. package/src/moodboard/bootstrap/MoodBoardUiFactory.js +22 -4
  48. package/src/moodboard/integration/MoodBoardEventBindings.js +5 -1
  49. package/src/moodboard/integration/MoodBoardLoadApi.js +10 -1
  50. package/src/moodboard/lifecycle/MoodBoardDestroyer.js +9 -0
  51. package/src/objects/ConnectorObject.js +2 -2
  52. package/src/objects/FrameObject.js +119 -59
  53. package/src/objects/ShapeObject.js +49 -74
  54. package/src/objects/shape/ShapeDrawer.js +210 -0
  55. package/src/services/ConnectorBindingResolver.js +112 -0
  56. package/src/services/ConnectorRouter.js +210 -0
  57. package/src/services/comments/CommentService.js +344 -0
  58. package/src/tools/object-tools/CommentTool.js +85 -0
  59. package/src/tools/object-tools/DrawingTool.js +110 -10
  60. package/src/tools/object-tools/LaserPointerTool.js +121 -0
  61. package/src/tools/object-tools/SelectTool.js +25 -1
  62. package/src/tools/object-tools/TextTool.js +6 -1
  63. package/src/tools/object-tools/connector/ConnectorDragController.js +50 -3
  64. package/src/tools/object-tools/connector/connectorGesture.js +33 -19
  65. package/src/tools/object-tools/placement/PlacementInputRouter.js +22 -1
  66. package/src/tools/object-tools/selection/BoxSelectController.js +24 -2
  67. package/src/tools/object-tools/selection/FrameTitleInlineEditorController.js +139 -0
  68. package/src/tools/object-tools/selection/InlineEditorController.js +12 -0
  69. package/src/tools/object-tools/selection/InlineEditorDomFactory.js +36 -0
  70. package/src/tools/object-tools/selection/LassoSelectController.js +125 -0
  71. package/src/tools/object-tools/selection/MindmapInlineEditorController.js +1 -0
  72. package/src/tools/object-tools/selection/SelectInputRouter.js +64 -5
  73. package/src/tools/object-tools/selection/SelectToolLifecycleController.js +11 -1
  74. package/src/tools/object-tools/selection/SelectToolSetup.js +13 -1
  75. package/src/tools/object-tools/selection/TextEditorInteractionController.js +46 -12
  76. package/src/tools/object-tools/selection/TextEditorSyncService.js +1 -0
  77. package/src/tools/object-tools/selection/TextInlineEditorController.js +65 -6
  78. package/src/ui/CommentPopover.js +6 -0
  79. package/src/ui/CommentsBar.js +91 -0
  80. package/src/ui/ConnectorPropertiesPanel.js +150 -0
  81. package/src/ui/ContextMenu.js +25 -0
  82. package/src/ui/DrawingPropertiesPanel.js +362 -0
  83. package/src/ui/FilePropertiesPanel.js +5 -0
  84. package/src/ui/FramePropertiesPanel.js +5 -0
  85. package/src/ui/HtmlTextLayer.js +246 -66
  86. package/src/ui/NotePropertiesPanel.js +6 -0
  87. package/src/ui/ShapePropertiesPanel.js +307 -0
  88. package/src/ui/TextPropertiesPanel.js +100 -1
  89. package/src/ui/Toolbar.js +25 -2
  90. package/src/ui/Topbar.js +2 -2
  91. package/src/ui/animation/HoverLiftController.js +6 -7
  92. package/src/ui/chat/ChatComposer.js +58 -7
  93. package/src/ui/chat/ChatWindow.js +60 -143
  94. package/src/ui/comments/CommentListPanel.js +213 -0
  95. package/src/ui/comments/CommentPinLayer.js +448 -0
  96. package/src/ui/comments/CommentThreadPopover.js +539 -0
  97. package/src/ui/comments/commentFormat.js +32 -0
  98. package/src/ui/connector-properties/ConnectorPropertiesPanelBindings.js +223 -0
  99. package/src/ui/connector-properties/ConnectorPropertiesPanelEventBridge.js +114 -0
  100. package/src/ui/connector-properties/ConnectorPropertiesPanelMapper.js +144 -0
  101. package/src/ui/connector-properties/ConnectorPropertiesPanelRenderer.js +447 -0
  102. package/src/ui/connector-properties/ConnectorPropertiesPanelState.js +61 -0
  103. package/src/ui/connectors/ConnectionAnchorsLayer.js +1 -0
  104. package/src/ui/connectors/ConnectorHandlesLayer.js +321 -0
  105. package/src/ui/connectors/ConnectorLabelLayer.js +334 -0
  106. package/src/ui/connectors/ConnectorLayer.js +264 -57
  107. package/src/ui/handles/HandlesDomRenderer.js +5 -13
  108. package/src/ui/handles/HandlesEventBridge.js +1 -0
  109. package/src/ui/handles/SingleSelectionHandlesController.js +4 -0
  110. package/src/ui/mindmap/MindmapCollapseLayer.js +1 -0
  111. package/src/ui/mindmap/MindmapConnectionLayer.js +1 -0
  112. package/src/ui/mindmap/MindmapHtmlTextLayer.js +6 -0
  113. package/src/ui/shape-properties/ShapePropertiesPanelDom.js +533 -0
  114. package/src/ui/shape-properties/ShapePropertiesPanelSync.js +132 -0
  115. package/src/ui/styles/chat.css +709 -19
  116. package/src/ui/styles/index.css +1 -0
  117. package/src/ui/styles/panels.css +112 -2
  118. package/src/ui/styles/shape-properties-panel.css +250 -0
  119. package/src/ui/styles/toolbar.css +7 -2
  120. package/src/ui/styles/topbar.css +1 -1
  121. package/src/ui/styles/workspace.css +257 -6
  122. package/src/ui/text-properties/TextFormatControls.js +88 -0
  123. package/src/ui/text-properties/TextListRenderer.js +137 -0
  124. package/src/ui/text-properties/TextPropertiesPanelBindings.js +27 -0
  125. package/src/ui/text-properties/TextPropertiesPanelEventBridge.js +3 -1
  126. package/src/ui/text-properties/TextPropertiesPanelMapper.js +56 -0
  127. package/src/ui/text-properties/TextPropertiesPanelRenderer.js +24 -0
  128. package/src/ui/text-properties/TextPropertiesPanelState.js +8 -0
  129. package/src/ui/toolbar/ReactionsPopupController.js +88 -0
  130. package/src/ui/toolbar/ToolbarActionRouter.js +71 -5
  131. package/src/ui/toolbar/ToolbarPopupsController.js +120 -118
  132. package/src/ui/toolbar/ToolbarRenderer.js +9 -1
  133. package/src/ui/toolbar/ToolbarStateController.js +4 -1
  134. package/src/utils/iconLoader.js +17 -16
  135. package/src/utils/markdown.js +14 -0
  136. package/src/utils/richText.js +125 -0
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Команда изменения стиля фигуры (цвет заливки, тип, обводка, радиус скругления) — одно действие в истории.
3
+ * Поддерживает частичные обновления: хранит только те поля, которые изменились.
4
+ *
5
+ * Снапшот: { color?, properties?: { kind?, cornerRadius?, borderColor?, borderWidth?, borderStyle?, borderOpacity? } }
6
+ */
7
+ import { BaseCommand } from './BaseCommand.js';
8
+ import { Events } from '../events/Events.js';
9
+
10
+ export class UpdateShapeStyleCommand extends BaseCommand {
11
+ /**
12
+ * @param {Object} coreMoodboard — ядро доски
13
+ * @param {string} objectId — id объекта-фигуры
14
+ * @param {Object} oldSnapshot — прежние значения (только изменяемые поля)
15
+ * @param {Object} newSnapshot — новые значения (только изменяемые поля)
16
+ */
17
+ constructor(coreMoodboard, objectId, oldSnapshot, newSnapshot) {
18
+ super('update_shape_style', 'Изменить стиль фигуры');
19
+ this.coreMoodboard = coreMoodboard;
20
+ this.objectId = objectId;
21
+ this.oldSnapshot = oldSnapshot;
22
+ this.newSnapshot = newSnapshot;
23
+ }
24
+
25
+ execute() {
26
+ this._apply(this.newSnapshot);
27
+ }
28
+
29
+ undo() {
30
+ // Локальный undo отключён: история состояния загружается с сервера по версиям.
31
+ }
32
+
33
+ canMergeWith(other) {
34
+ return other instanceof UpdateShapeStyleCommand &&
35
+ other.objectId === this.objectId &&
36
+ _snapshotKeysMatch(this.newSnapshot, other.newSnapshot);
37
+ }
38
+
39
+ mergeWith(other) {
40
+ if (!this.canMergeWith(other)) throw new Error('Cannot merge commands');
41
+ this.newSnapshot = other.newSnapshot;
42
+ this.timestamp = other.timestamp;
43
+ }
44
+
45
+ _apply(snapshot) {
46
+ const { coreMoodboard, objectId } = this;
47
+ const object = coreMoodboard.state.getObjects().find(o => o.id === objectId);
48
+ if (!object) return;
49
+
50
+ // Обновить состояние
51
+ if ('color' in snapshot) {
52
+ object.color = snapshot.color;
53
+ }
54
+ if (snapshot.properties) {
55
+ if (!object.properties) object.properties = {};
56
+ Object.assign(object.properties, snapshot.properties);
57
+ }
58
+ coreMoodboard.state.markDirty();
59
+
60
+ // Обновить PIXI-инстанс
61
+ const pixiObject = coreMoodboard.pixi?.objects?.get(objectId);
62
+ const instance = pixiObject?._mb?.instance;
63
+
64
+ if (instance) {
65
+ if ('color' in snapshot && instance.setColor) {
66
+ instance.setColor(snapshot.color);
67
+ }
68
+ if (snapshot.properties) {
69
+ const p = snapshot.properties;
70
+
71
+ // kind и cornerRadius — через setProperties (borderStyle идёт только в setStroke)
72
+ if ((p.kind !== undefined || p.cornerRadius !== undefined) && instance.setProperties) {
73
+ instance.setProperties({ kind: p.kind, cornerRadius: p.cornerRadius });
74
+ }
75
+
76
+ // Параметры обводки (borderStyle здесь тоже)
77
+ const hasStroke = p.borderColor !== undefined || p.borderWidth !== undefined ||
78
+ p.borderStyle !== undefined || p.borderOpacity !== undefined;
79
+ if (hasStroke && instance.setStroke) {
80
+ instance.setStroke({
81
+ borderColor: p.borderColor,
82
+ borderWidth: p.borderWidth,
83
+ borderStyle: p.borderStyle,
84
+ borderOpacity: p.borderOpacity,
85
+ });
86
+ }
87
+ }
88
+ }
89
+
90
+ // Синхронизировать _mb.properties с новым состоянием
91
+ if (pixiObject?._mb) {
92
+ if (!pixiObject._mb.properties) pixiObject._mb.properties = {};
93
+ if ('color' in snapshot) pixiObject._mb.color = snapshot.color;
94
+ if (snapshot.properties) Object.assign(pixiObject._mb.properties, snapshot.properties);
95
+ }
96
+
97
+ // Уведомить остальных подписчиков (с флагом чтобы избежать рекурсии в tryCreateShapeStyleCommand)
98
+ const updates = {};
99
+ if ('color' in snapshot) updates.color = snapshot.color;
100
+ if (snapshot.properties) updates.properties = { ...snapshot.properties };
101
+
102
+ coreMoodboard.eventBus.emit(Events.Object.StateChanged, {
103
+ objectId,
104
+ updates,
105
+ _fromCommand: true,
106
+ });
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Проверяет, что два снапшота содержат одинаковый набор ключей — для слияния команд.
112
+ */
113
+ function _snapshotKeysMatch(a, b) {
114
+ const topA = Object.keys(a).sort().join(',');
115
+ const topB = Object.keys(b).sort().join(',');
116
+ if (topA !== topB) return false;
117
+ if (a.properties && b.properties) {
118
+ return Object.keys(a.properties).sort().join(',') === Object.keys(b.properties).sort().join(',');
119
+ }
120
+ return !(a.properties || b.properties);
121
+ }
@@ -1,11 +1,14 @@
1
1
  /**
2
- * Команда изменения свойств текста (шрифт, размер, цвет, фон) для системы Undo/Redo.
3
- * Поддерживает: fontFamily, fontSize, color, backgroundColor.
2
+ * Команда изменения свойств текста для системы Undo/Redo.
3
+ * Поддерживает: fontFamily, fontSize, color, backgroundColor, markdown,
4
+ * bold, italic, underline, strikethrough, textAlign, lineHeight, listType.
4
5
  */
5
6
  import { BaseCommand } from './BaseCommand.js';
6
7
  import { Events } from '../events/Events.js';
7
8
  import { syncPixiTextProperties } from '../../ui/text-properties/TextPropertiesPanelMapper.js';
8
9
 
10
+ const PROPERTY_LEVEL = ['fontFamily', 'markdown', 'bold', 'italic', 'underline', 'strikethrough', 'textAlign', 'lineHeight', 'listType'];
11
+
9
12
  export class UpdateTextStyleCommand extends BaseCommand {
10
13
  /**
11
14
  * @param {Object} coreMoodboard — ядро доски
@@ -52,9 +55,9 @@ export class UpdateTextStyleCommand extends BaseCommand {
52
55
 
53
56
  const { property } = this;
54
57
 
55
- if (property === 'fontFamily') {
58
+ if (PROPERTY_LEVEL.includes(property)) {
56
59
  if (!object.properties) object.properties = {};
57
- object.properties.fontFamily = value;
60
+ object.properties[property] = value;
58
61
  } else {
59
62
  object[property] = value;
60
63
  }
@@ -69,8 +72,8 @@ export class UpdateTextStyleCommand extends BaseCommand {
69
72
 
70
73
  syncPixiTextProperties(this.coreMoodboard.eventBus, this.objectId, { [property]: value });
71
74
 
72
- const updates = property === 'fontFamily'
73
- ? { properties: { fontFamily: value } }
75
+ const updates = PROPERTY_LEVEL.includes(property)
76
+ ? { properties: { [property]: value } }
74
77
  : { [property]: value };
75
78
  this.coreMoodboard.eventBus.emit(Events.Object.StateChanged, {
76
79
  objectId: this.objectId,
@@ -85,6 +88,14 @@ function _propertyLabel(property) {
85
88
  fontSize: 'размер шрифта',
86
89
  color: 'цвет текста',
87
90
  backgroundColor: 'фон текста',
91
+ markdown: 'markdown-режим',
92
+ bold: 'жирный текст',
93
+ italic: 'курсив',
94
+ underline: 'подчёркивание',
95
+ strikethrough: 'зачёркивание',
96
+ textAlign: 'выравнивание текста',
97
+ lineHeight: 'межстрочный интервал',
98
+ listType: 'тип списка',
88
99
  };
89
100
  return labels[property] || property;
90
101
  }
@@ -18,3 +18,6 @@ export { UpdateTextStyleCommand } from './UpdateTextStyleCommand.js';
18
18
  export { UpdateNoteStyleCommand } from './UpdateNoteStyleCommand.js';
19
19
  export { UpdateFramePropertiesCommand } from './UpdateFramePropertiesCommand.js';
20
20
  export { UpdateFrameTypeCommand } from './UpdateFrameTypeCommand.js';
21
+ export { CreateConnectorCommand } from './CreateConnectorCommand.js';
22
+ export { UpdateConnectorCommand } from './UpdateConnectorCommand.js';
23
+ export { UpdateShapeStyleCommand } from './UpdateShapeStyleCommand.js';
@@ -9,6 +9,8 @@ export const Events = {
9
9
  SelectionRemove: 'tool:selection:remove',
10
10
  SelectionClear: 'tool:selection:clear',
11
11
  SelectionAll: 'tool:selection:all',
12
+ BoxSelectStart: 'tool:box:select:start',
13
+ BoxSelectCommit: 'tool:box:select:commit',
12
14
  HitTest: 'tool:hit:test',
13
15
  GetSelection: 'tool:get:selection',
14
16
  GetAllObjects: 'tool:get:all:objects',
@@ -153,6 +155,26 @@ export const Events = {
153
155
  Draw: {
154
156
  BrushSet: 'draw:brush:set',
155
157
  },
158
+
159
+ Lasso: {
160
+ ModeSet: 'lasso:mode:set',
161
+ },
162
+
163
+ Comment: {
164
+ PinCreated: 'comment:pin:created',
165
+ ThreadOpened: 'comment:thread:opened',
166
+ MessageAdded: 'comment:message:added',
167
+ Resolved: 'comment:resolved',
168
+ ColorChanged: 'comment:color:changed',
169
+ Deleted: 'comment:deleted',
170
+ RemoteUpdated: 'comment:remote:updated',
171
+ OpenDraftAt: 'comment:open:draft:at',
172
+ ThreadDeleted: 'comment:thread:deleted',
173
+ DraftOpened: 'comment:draft:opened',
174
+ DraftClosed: 'comment:draft:closed',
175
+ ResolvedFilterChanged: 'comment:resolved:filter:changed',
176
+ ListOpened: 'comment:list:opened',
177
+ },
156
178
  };
157
179
 
158
180
 
@@ -247,6 +247,7 @@ export function setupLayerAndViewportFlow(core) {
247
247
  const s = world?.scale?.x || 1;
248
248
  world.x = view.clientWidth / 2 - worldX * s;
249
249
  world.y = view.clientHeight / 2 - worldY * s;
250
+ core.eventBus.emit(Events.Viewport.Changed);
250
251
  });
251
252
 
252
253
  core.eventBus.on(Events.Tool.GroupDragStart, (data) => {
@@ -6,14 +6,24 @@ import {
6
6
  UpdateTextStyleCommand,
7
7
  UpdateNoteStyleCommand,
8
8
  UpdateFramePropertiesCommand,
9
+ UpdateConnectorCommand,
10
+ UpdateShapeStyleCommand,
9
11
  } from '../commands/index.js';
10
12
 
11
- const TEXT_STYLE_PROPS = ['fontFamily', 'fontSize', 'color', 'backgroundColor'];
13
+ const TEXT_STYLE_PROPS = ['fontFamily', 'fontSize', 'color', 'backgroundColor', 'markdown', 'bold', 'italic', 'underline', 'strikethrough', 'textAlign', 'lineHeight', 'listType'];
14
+ const TEXT_STYLE_PROPERTY_LEVEL = ['fontFamily', 'markdown', 'bold', 'italic', 'underline', 'strikethrough', 'textAlign', 'lineHeight', 'listType'];
12
15
  const TEXT_STYLE_DEFAULTS = {
13
16
  fontFamily: 'Roboto, Arial, sans-serif',
14
17
  fontSize: 18,
15
18
  color: '#000000',
16
19
  backgroundColor: 'transparent',
20
+ markdown: false,
21
+ bold: false,
22
+ italic: false,
23
+ underline: false,
24
+ strikethrough: false,
25
+ textAlign: 'left',
26
+ listType: 'none',
17
27
  };
18
28
 
19
29
  const NOTE_STYLE_PROPS = ['fontFamily', 'fontSize', 'textColor', 'backgroundColor'];
@@ -24,6 +34,64 @@ const NOTE_STYLE_DEFAULTS = {
24
34
  backgroundColor: 0xfff9c4,
25
35
  };
26
36
 
37
+ /**
38
+ * Если updates содержит color или shape-properties — создаёт UpdateShapeStyleCommand и выполняет её.
39
+ * Защищена от рекурсии: при emit из команды (_fromCommand=true) object уже обновлён,
40
+ * oldValue === newValue для всех полей → hasChanges = false → return false.
41
+ * @returns {boolean} true, если команда создана и применена
42
+ */
43
+ const SHAPE_PROP_KEYS = ['kind', 'cornerRadius', 'borderColor', 'borderWidth', 'borderStyle', 'borderOpacity'];
44
+
45
+ function tryCreateShapeStyleCommand(core, object, objectId, updates) {
46
+ if (object.type !== 'shape') return false;
47
+
48
+ const hasColor = 'color' in updates;
49
+ const hasProps = updates.properties &&
50
+ Object.keys(updates.properties).some(k => SHAPE_PROP_KEYS.includes(k));
51
+
52
+ if (!hasColor && !hasProps) return false;
53
+
54
+ const newSnapshot = {};
55
+ const oldSnapshot = {};
56
+ let hasChanges = false;
57
+
58
+ if (hasColor) {
59
+ const oldColor = object.color;
60
+ const newColor = updates.color;
61
+ if (oldColor !== newColor) {
62
+ oldSnapshot.color = oldColor;
63
+ newSnapshot.color = newColor;
64
+ hasChanges = true;
65
+ }
66
+ }
67
+
68
+ if (hasProps) {
69
+ oldSnapshot.properties = {};
70
+ newSnapshot.properties = {};
71
+ for (const key of SHAPE_PROP_KEYS) {
72
+ if (key in updates.properties) {
73
+ const oldVal = object.properties?.[key];
74
+ const newVal = updates.properties[key];
75
+ if (oldVal !== newVal) {
76
+ oldSnapshot.properties[key] = oldVal;
77
+ newSnapshot.properties[key] = newVal;
78
+ hasChanges = true;
79
+ }
80
+ }
81
+ }
82
+ if (Object.keys(newSnapshot.properties).length === 0) {
83
+ delete oldSnapshot.properties;
84
+ delete newSnapshot.properties;
85
+ }
86
+ }
87
+
88
+ if (!hasChanges) return false;
89
+
90
+ const command = new UpdateShapeStyleCommand(core, objectId, oldSnapshot, newSnapshot);
91
+ core.history.executeCommand(command);
92
+ return true;
93
+ }
94
+
27
95
  /**
28
96
  * Если updates.properties содержит ровно одно свойство стиля записки — создаёт UpdateNoteStyleCommand.
29
97
  * @returns {boolean} true, если команда создана и применена
@@ -56,9 +124,12 @@ function tryCreateTextStyleCommand(core, object, objectId, updates) {
56
124
  let property = null;
57
125
  let newValue = null;
58
126
 
59
- if (updates.properties?.fontFamily !== undefined && Object.keys(updates).length === 1) {
60
- property = 'fontFamily';
61
- newValue = updates.properties.fontFamily;
127
+ if (updates.properties && Object.keys(updates).length === 1) {
128
+ const propKeys = Object.keys(updates.properties);
129
+ if (propKeys.length === 1 && TEXT_STYLE_PROPERTY_LEVEL.includes(propKeys[0])) {
130
+ property = propKeys[0];
131
+ newValue = updates.properties[property];
132
+ }
62
133
  } else if (updates.fontSize !== undefined && !updates.properties && Object.keys(updates).length === 1) {
63
134
  property = 'fontSize';
64
135
  newValue = typeof updates.fontSize === 'string' ? parseInt(updates.fontSize, 10) : updates.fontSize;
@@ -72,8 +143,8 @@ function tryCreateTextStyleCommand(core, object, objectId, updates) {
72
143
 
73
144
  if (!property || !TEXT_STYLE_PROPS.includes(property)) return false;
74
145
 
75
- const oldValue = property === 'fontFamily'
76
- ? (object.properties?.fontFamily ?? TEXT_STYLE_DEFAULTS.fontFamily)
146
+ const oldValue = TEXT_STYLE_PROPERTY_LEVEL.includes(property)
147
+ ? (object.properties?.[property] ?? TEXT_STYLE_DEFAULTS[property])
77
148
  : (object[property] ?? object.properties?.[property] ?? TEXT_STYLE_DEFAULTS[property]);
78
149
 
79
150
  if (oldValue === newValue) return false;
@@ -83,6 +154,50 @@ function tryCreateTextStyleCommand(core, object, objectId, updates) {
83
154
  return true;
84
155
  }
85
156
 
157
+ const CONNECTOR_STYLE_KEYS = ['stroke', 'width', 'dash', 'route', 'head'];
158
+
159
+ /**
160
+ * Если updates.properties.style содержит поля стиля коннектора —
161
+ * создаёт UpdateConnectorCommand и выполняет её через историю.
162
+ * Также обрабатывает start/end (для swap-кнопки).
163
+ * @returns {boolean} true — команда применена, дальнейшая обработка не нужна
164
+ */
165
+ function tryCreateConnectorStyleCommand(core, object, objectId, updates) {
166
+ if (object.type !== 'connector') return false;
167
+ if (!updates.properties) return false;
168
+
169
+ const { style, start, end } = updates.properties;
170
+
171
+ // Проверяем, что в updates.properties только style/start/end (и нет посторонних ключей)
172
+ const allowedKeys = new Set(['style', 'start', 'end', 'locked']);
173
+ const hasOtherKeys = Object.keys(updates.properties).some(k => !allowedKeys.has(k));
174
+ if (hasOtherKeys) return false;
175
+ // Должно быть хотя бы одно из: style, start, end
176
+ if (!style && start === undefined && end === undefined
177
+ && updates.properties.locked === undefined) return false;
178
+
179
+ const commandUpdates = {};
180
+ if (style !== undefined) commandUpdates.style = style;
181
+ if (start !== undefined) commandUpdates.start = start;
182
+ if (end !== undefined) commandUpdates.end = end;
183
+
184
+ // locked хранится в properties напрямую, не через UpdateConnectorCommand
185
+ if (updates.properties.locked !== undefined) {
186
+ if (!object.properties) object.properties = {};
187
+ object.properties.locked = updates.properties.locked;
188
+ if (Object.keys(commandUpdates).length === 0) {
189
+ core.state.markDirty();
190
+ return true;
191
+ }
192
+ }
193
+
194
+ if (Object.keys(commandUpdates).length === 0) return false;
195
+
196
+ const command = new UpdateConnectorCommand(core, objectId, commandUpdates);
197
+ core.history.executeCommand(command);
198
+ return true;
199
+ }
200
+
86
201
  const FRAME_PROP_KEYS = ['title'];
87
202
 
88
203
  /**
@@ -131,7 +246,18 @@ export function setupObjectLifecycleFlow(core) {
131
246
 
132
247
  core.eventBus.on(Events.Tool.HitTest, (data) => {
133
248
  const result = core.pixi.hitTest(data.x, data.y);
134
- data.result = result;
249
+ if (result.type !== 'object' && core.connectorLayer) {
250
+ const worldLayer = core.pixi.worldLayer;
251
+ const worldPoint = worldLayer.toLocal({ x: data.x, y: data.y });
252
+ const id = core.connectorLayer.hitTest(worldPoint);
253
+ if (id) {
254
+ data.result = { type: 'object', object: id, pixiObject: core.pixi.objects.get(id) };
255
+ } else {
256
+ data.result = result;
257
+ }
258
+ } else {
259
+ data.result = result;
260
+ }
135
261
  });
136
262
 
137
263
  core.eventBus.on(Events.Tool.GetObjectPosition, (data) => {
@@ -228,9 +354,15 @@ export function setupObjectLifecycleFlow(core) {
228
354
  const noteStyleChange = tryCreateNoteStyleCommand(core, object, objectId, updates);
229
355
  if (noteStyleChange) return;
230
356
 
357
+ const shapeStyleChange = tryCreateShapeStyleCommand(core, object, objectId, updates);
358
+ if (shapeStyleChange) return;
359
+
231
360
  const textStyleChange = tryCreateTextStyleCommand(core, object, objectId, updates);
232
361
  if (textStyleChange) return;
233
362
 
363
+ const connectorStyleChange = tryCreateConnectorStyleCommand(core, object, objectId, updates);
364
+ if (connectorStyleChange) return;
365
+
234
366
  const framePropsChange = tryCreateFramePropertiesCommand(core, object, objectId, updates);
235
367
  if (framePropsChange) return;
236
368
 
@@ -282,6 +414,22 @@ export function setupObjectLifecycleFlow(core) {
282
414
  }
283
415
  }
284
416
  }
417
+
418
+ if (object.type === 'drawing' && updates.properties && instance.setStyle) {
419
+ const styleUpdates = {};
420
+ if (updates.properties.strokeColor !== undefined) {
421
+ styleUpdates.strokeColor = updates.properties.strokeColor;
422
+ }
423
+ if (updates.properties.strokeWidth !== undefined) {
424
+ styleUpdates.strokeWidth = updates.properties.strokeWidth;
425
+ }
426
+ if (updates.properties.mode !== undefined) {
427
+ styleUpdates.mode = updates.properties.mode;
428
+ }
429
+ if (Object.keys(styleUpdates).length > 0) {
430
+ instance.setStyle(styleUpdates);
431
+ }
432
+ }
285
433
  }
286
434
 
287
435
  core.state.markDirty();
package/src/core/index.js CHANGED
@@ -346,6 +346,17 @@ export class CoreMoodBoard {
346
346
  }
347
347
 
348
348
  createObject(type, position, properties = {}, extraData = {}) {
349
+ const validType = typeof type === 'string' && type.length > 0;
350
+ const validPos = position && typeof position === 'object'
351
+ && typeof position.x === 'number' && typeof position.y === 'number';
352
+ if (!validType || !validPos) {
353
+ console.error(
354
+ '[MoodBoard] createObject: неверные аргументы. Ожидается createObject(type, position, properties, extraData), ' +
355
+ 'где type — непустая строка, position — { x: number, y: number }.',
356
+ { type, position }
357
+ );
358
+ return null;
359
+ }
349
360
  const exists = (id) => {
350
361
  const inState = (this.state.state.objects || []).some(o => o.id === id);
351
362
  const inPixi = this.pixi?.objects?.has ? this.pixi.objects.has(id) : false;
@@ -394,7 +405,10 @@ export class CoreMoodBoard {
394
405
  properties = { ...(properties || {}), title: 'Фрейм 1' };
395
406
  }
396
407
  }
397
- const snappedCreatePos = this.gridSnapResolver
408
+ // Рисованные объекты (drawing) должны оставаться ровно там, где их нарисовали —
409
+ // примагничивание bbox к сетке сдвигало бы готовую линию относительно превью.
410
+ const shouldSnap = this.gridSnapResolver && type !== 'drawing';
411
+ const snappedCreatePos = shouldSnap
398
412
  ? this.gridSnapResolver.snapWorldTopLeft(position, {
399
413
  width: initialWidth,
400
414
  height: initialHeight,
@@ -514,6 +528,19 @@ export class CoreMoodBoard {
514
528
  }
515
529
  } catch (_) { /* no-op */ }
516
530
 
531
+ const loadId = objectData?.id ?? '(без id)';
532
+ const validType = typeof objectData?.type === 'string' && objectData.type.length > 0;
533
+ const pos = objectData?.position;
534
+ const validPos = pos && typeof pos === 'object'
535
+ && typeof pos.x === 'number' && typeof pos.y === 'number';
536
+ if (!validType || !validPos) {
537
+ console.warn(
538
+ `[MoodBoard] Пропуск объекта при загрузке (${loadId}): невалидные type или position`,
539
+ objectData
540
+ );
541
+ return null;
542
+ }
543
+
517
544
  // Инициализируем флаг компенсации пивота для загруженных объектов.
518
545
  // В state координаты хранятся как левый-верх. PIXI позиционирует по центру (anchor/pivot по центру),
519
546
  // поэтому при создании нужно ДОБАВИТЬ половину ширины/высоты (т.е. pivotCompensated должен быть false),
@@ -80,9 +80,9 @@ function sanitizeColor(value, fallback = 0xB0B0B0) {
80
80
  return Math.max(0, Math.min(0xFFFFFF, Math.round(n)));
81
81
  }
82
82
 
83
- const DEFAULT_CROSS_COLOR = 0xC2C2C2; // 194,194,194
84
- const DEFAULT_MID_CROSS_COLOR = 0xA7A7A7; // 167,167,167
85
- const DEFAULT_SMALL_CROSS_COLOR = 0xCDCDCD; // 205,205,205
83
+ const DEFAULT_CROSS_COLOR = 0xCFD3DB; // 207,211,219 — крупная сетка, чуть заметнее
84
+ const DEFAULT_MID_CROSS_COLOR = 0xD6DAE1; // 214,218,225 — средний участок
85
+ const DEFAULT_SMALL_CROSS_COLOR = 0xDDE0E6; // 221,224,230 — плотная сетка, самый тихий
86
86
  const COLOR_BAND_HIGH = 'high';
87
87
  const COLOR_BAND_MID = 'mid';
88
88
  const COLOR_BAND_SMALL = 'small';
@@ -59,7 +59,7 @@ export function injectCriticalStyles() {
59
59
  width: 100%;
60
60
  height: 100%;
61
61
  overflow: hidden;
62
- background: #f7fbff;
62
+ background: #f0f6fc;
63
63
  }
64
64
 
65
65
  .moodboard-workspace__toolbar {
@@ -131,12 +131,40 @@ export class DataManager {
131
131
  window.moodboardMindmapHtmlTextLayer.rebuildFromState();
132
132
  window.moodboardMindmapHtmlTextLayer.updateAll();
133
133
  }
134
+ // Нормализуем геометрию майндмапа после загрузки. Устаревшие доски могли
135
+ // сохранить растянутые координаты (раньше зазор между узлами зависел от
136
+ // зума). Перелейаут с фиксированным мировым зазором приводит их к норме.
137
+ // board:loaded для этого не годится: он эмитится при инициализации ядра
138
+ // на пустой доске, до загрузки объектов и подписки слоя ручек.
139
+ this._relayoutMindmapAfterLoad();
134
140
  if (window.moodboardMindmapConnectionLayer) {
135
141
  window.moodboardMindmapConnectionLayer.updateAll();
136
142
  }
137
143
  }, 100);
138
144
 
139
145
  }
146
+
147
+ /**
148
+ * Приводит геометрию майндмапа к норме после загрузки доски.
149
+ * Запускается дважды: сразу (по сохранённым размерам) и с задержкой —
150
+ * чтобы перехватить узлы после авто-фита ширины/высоты текста.
151
+ */
152
+ _relayoutMindmapAfterLoad() {
153
+ const run = () => {
154
+ const renderer = window.moodboardHtmlHandlesLayer?.domRenderer;
155
+ if (renderer?.relayoutAllMindmapCompounds) {
156
+ renderer.relayoutAllMindmapCompounds();
157
+ }
158
+ };
159
+ run();
160
+ setTimeout(run, 80);
161
+ setTimeout(() => {
162
+ run();
163
+ if (window.moodboardMindmapConnectionLayer) {
164
+ window.moodboardMindmapConnectionLayer.updateAll();
165
+ }
166
+ }, 250);
167
+ }
140
168
 
141
169
  /**
142
170
  * Загружает настройки viewport (позиция и зум)
@@ -42,8 +42,18 @@ export class MoodBoard {
42
42
  onSave: null,
43
43
  onLoad: null,
44
44
  onDestroy: null,
45
+ enableComments: false,
46
+ currentUser: null,
47
+ comments: null,
45
48
  ...options
46
49
  };
50
+
51
+ /** @type {{ applyRemote: (event: object) => void } | null} */
52
+ this.comments = null;
53
+ this.commentService = null;
54
+ this.commentPinLayer = null;
55
+ this.commentThreadPopover = null;
56
+ this.commentListPanel = null;
47
57
 
48
58
  this.data = data;
49
59
 
@@ -183,6 +193,23 @@ export class MoodBoard {
183
193
  * Очистка ресурсов
184
194
  */
185
195
  destroy() {
196
+ if (this.commentPinLayer) {
197
+ this.commentPinLayer.destroy();
198
+ this.commentPinLayer = null;
199
+ }
200
+ if (this.commentThreadPopover) {
201
+ this.commentThreadPopover.destroy();
202
+ this.commentThreadPopover = null;
203
+ }
204
+ if (this.commentListPanel) {
205
+ this.commentListPanel.destroy();
206
+ this.commentListPanel = null;
207
+ }
208
+ if (this.commentService) {
209
+ this.commentService.destroy();
210
+ this.commentService = null;
211
+ }
212
+ this.comments = null;
186
213
  destroyMoodBoard(this);
187
214
  }
188
215