@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,533 @@
1
+ import {
2
+ FONT_OPTIONS,
3
+ FONT_SIZE_OPTIONS,
4
+ TEXT_COLOR_PRESETS,
5
+ } from '../text-properties/TextPropertiesPanelMapper.js';
6
+
7
+ // ── Константы ─────────────────────────────────────────────────────────────────
8
+
9
+ export const FILL_COLORS = [
10
+ { name: 'Белый', hex: '#FFFFFF', pixi: 0xFFFFFF },
11
+ { name: 'Светлый серый', hex: '#F5F5F5', pixi: 0xF5F5F5 },
12
+ { name: 'Голубой', hex: '#DBEAFE', pixi: 0xDBEAFE },
13
+ { name: 'Зеленый', hex: '#DCFCE7', pixi: 0xDCFCE7 },
14
+ { name: 'Желтый', hex: '#FEF9C3', pixi: 0xFEF9C3 },
15
+ { name: 'Розовый', hex: '#FCE7F3', pixi: 0xFCE7F3 },
16
+ { name: 'Синий', hex: '#BFDBFE', pixi: 0xBFDBFE },
17
+ { name: 'Оранжевый', hex: '#FED7AA', pixi: 0xFED7AA },
18
+ { name: 'Красный', hex: '#FECACA', pixi: 0xFECACA },
19
+ { name: 'Черный', hex: '#1A1A1A', pixi: 0x1A1A1A },
20
+ { name: 'Темно-серый', hex: '#404040', pixi: 0x404040 },
21
+ { name: 'Темно-синий', hex: '#1E3A5F', pixi: 0x1E3A5F },
22
+ ];
23
+
24
+ export const BORDER_COLORS = [
25
+ { name: 'Серый', hex: '#D4D4D4', pixi: 0xD4D4D4 },
26
+ { name: 'Темно-серый', hex: '#737373', pixi: 0x737373 },
27
+ { name: 'Черный', hex: '#1A1A1A', pixi: 0x1A1A1A },
28
+ { name: 'Синий', hex: '#2563EB', pixi: 0x2563EB },
29
+ { name: 'Красный', hex: '#EF4444', pixi: 0xEF4444 },
30
+ { name: 'Зеленый', hex: '#22C55E', pixi: 0x22C55E },
31
+ { name: 'Желтый', hex: '#EAB308', pixi: 0xEAB308 },
32
+ { name: 'Фиолетовый', hex: '#A855F7', pixi: 0xA855F7 },
33
+ { name: 'Белый', hex: '#FFFFFF', pixi: 0xFFFFFF },
34
+ { name: 'Прозрачный', hex: '#E5E7EB', pixi: 0xE5E7EB },
35
+ { name: 'Оранжевый', hex: '#F97316', pixi: 0xF97316 },
36
+ { name: 'Розовый', hex: '#EC4899', pixi: 0xEC4899 },
37
+ ];
38
+
39
+ export const SHAPE_KINDS = [
40
+ { kind: 'square', label: 'Прямоугольник' },
41
+ { kind: 'rounded', label: 'Скруглённый' },
42
+ { kind: 'circle', label: 'Эллипс' },
43
+ { kind: 'triangle', label: 'Треугольник' },
44
+ { kind: 'diamond', label: 'Ромб' },
45
+ { kind: 'parallelogram', label: 'Параллелограмм' },
46
+ { kind: 'arrow', label: 'Стрелка' },
47
+ ];
48
+
49
+ export const SHAPE_ICONS = {
50
+ square: '<svg width="22" height="22" viewBox="0 0 22 22" fill="none"><rect x="2" y="2" width="18" height="18" rx="0" stroke="currentColor" stroke-width="1.5"/></svg>',
51
+ rounded: '<svg width="22" height="22" viewBox="0 0 22 22" fill="none"><rect x="2" y="2" width="18" height="18" rx="5" stroke="currentColor" stroke-width="1.5"/></svg>',
52
+ circle: '<svg width="22" height="22" viewBox="0 0 22 22" fill="none"><ellipse cx="11" cy="11" rx="9" ry="9" stroke="currentColor" stroke-width="1.5"/></svg>',
53
+ triangle: '<svg width="22" height="22" viewBox="0 0 22 22" fill="none"><polygon points="11,2 21,20 1,20" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>',
54
+ diamond: '<svg width="22" height="22" viewBox="0 0 22 22" fill="none"><polygon points="11,2 21,11 11,20 1,11" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>',
55
+ parallelogram: '<svg width="22" height="22" viewBox="0 0 22 22" fill="none"><polygon points="5,18 19,18 17,4 3,4" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>',
56
+ arrow: '<svg width="22" height="22" viewBox="0 0 22 22" fill="none"><polygon points="2,8 2,14 13,14 13,18 21,11 13,4 13,8" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>',
57
+ };
58
+
59
+ const CARET_SVG = '<svg width="10" height="10" viewBox="0 0 10 10" fill="none"><path d="M2 3.5l3 3 3-3" stroke="#888" stroke-width="1.5" stroke-linecap="round"/></svg>';
60
+
61
+ // ── Строители секций ──────────────────────────────────────────────────────────
62
+
63
+ export function sep() {
64
+ const s = document.createElement('div');
65
+ s.className = 'spp-sep';
66
+ return s;
67
+ }
68
+
69
+ export function buildColorGrid(container, colors, onPick, swatchesOut) {
70
+ const grid = document.createElement('div');
71
+ grid.className = 'spp-color-grid';
72
+ colors.forEach((color) => {
73
+ const btn = document.createElement('button');
74
+ btn.type = 'button';
75
+ btn.className = 'spp-color-swatch' +
76
+ (color.hex.toUpperCase() === '#FFFFFF' ? ' spp-color-swatch--white' : '');
77
+ btn.style.backgroundColor = color.hex;
78
+ btn.title = color.name;
79
+ btn.dataset.colorHex = color.hex.toUpperCase();
80
+ const tick = document.createElement('span');
81
+ tick.className = 'spp-tick';
82
+ btn.appendChild(tick);
83
+ btn.addEventListener('click', () => {
84
+ swatchesOut.forEach(s => s.classList.remove('spp-color-swatch--active'));
85
+ btn.classList.add('spp-color-swatch--active');
86
+ onPick(color);
87
+ });
88
+ grid.appendChild(btn);
89
+ swatchesOut.push(btn);
90
+ });
91
+ container.appendChild(grid);
92
+ }
93
+
94
+ export function buildShapeGroup(inst) {
95
+ const trigger = document.createElement('button');
96
+ trigger.className = 'spp-trigger';
97
+ trigger.title = 'Форма';
98
+ trigger.innerHTML = SHAPE_ICONS['square'] + CARET_SVG;
99
+ inst._kindTrigger = trigger;
100
+
101
+ const popover = document.createElement('div');
102
+ popover.className = 'spp-popover';
103
+ Object.assign(popover.style, { top: '100%', left: '0', marginTop: '4px' });
104
+
105
+ const grid = document.createElement('div');
106
+ grid.className = 'spp-kind-grid';
107
+
108
+ inst._kindButtons = {};
109
+ SHAPE_KINDS.forEach(({ kind, label }) => {
110
+ const btn = document.createElement('button');
111
+ btn.className = 'spp-kind-btn';
112
+ btn.title = label;
113
+ btn.dataset.kind = kind;
114
+ btn.innerHTML = (SHAPE_ICONS[kind] || '') +
115
+ `<span>${label.length > 8 ? label.slice(0, 7) + '\u2026' : label}</span>`;
116
+ btn.addEventListener('click', () => {
117
+ inst._emit({ updates: { properties: { kind } } });
118
+ inst._closePopover();
119
+ });
120
+ grid.appendChild(btn);
121
+ inst._kindButtons[kind] = btn;
122
+ });
123
+
124
+ popover.appendChild(grid);
125
+ inst._kindPopover = popover;
126
+
127
+ const wrap = document.createElement('div');
128
+ wrap.style.position = 'relative';
129
+ wrap.appendChild(trigger);
130
+ wrap.appendChild(popover);
131
+
132
+ trigger.addEventListener('click', (e) => {
133
+ e.stopPropagation();
134
+ inst._togglePopover(popover);
135
+ });
136
+
137
+ return wrap;
138
+ }
139
+
140
+ export function buildFillGroup(inst) {
141
+ const label = document.createElement('span');
142
+ label.className = 'spp-label';
143
+ label.textContent = 'Заливка:';
144
+
145
+ const btn = document.createElement('button');
146
+ btn.className = 'spp-color-btn';
147
+ btn.style.backgroundColor = '#FFFFFF';
148
+ btn.title = 'Цвет заливки';
149
+ inst._fillColorBtn = btn;
150
+ inst._fillSwatches = [];
151
+
152
+ const popover = document.createElement('div');
153
+ popover.className = 'spp-popover';
154
+ Object.assign(popover.style, { top: '100%', left: '0', marginTop: '4px' });
155
+ buildColorGrid(popover, FILL_COLORS, (color) => {
156
+ inst._fillColorBtn.style.backgroundColor = color.hex;
157
+ inst._emit({ updates: { color: color.pixi } });
158
+ inst._closePopover();
159
+ }, inst._fillSwatches);
160
+ inst._fillPopover = popover;
161
+
162
+ const wrap = document.createElement('div');
163
+ wrap.style.position = 'relative';
164
+ wrap.appendChild(btn);
165
+ wrap.appendChild(popover);
166
+
167
+ btn.addEventListener('click', (e) => {
168
+ e.stopPropagation();
169
+ inst._togglePopover(popover);
170
+ });
171
+
172
+ return [label, wrap];
173
+ }
174
+
175
+ export function buildBorderGroup(inst) {
176
+ const trigger = document.createElement('button');
177
+ trigger.className = 'spp-trigger';
178
+ trigger.title = 'Рамка';
179
+ trigger.innerHTML =
180
+ '<svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="1.5" y="1.5" width="13" height="13" rx="1" stroke="currentColor" stroke-width="1.5"/></svg>' +
181
+ '<span style="font-size:11px;color:#555">Рамка</span>' + CARET_SVG;
182
+ inst._borderTrigger = trigger;
183
+
184
+ const popover = document.createElement('div');
185
+ popover.className = 'spp-popover';
186
+ Object.assign(popover.style, { top: '100%', left: '0', marginTop: '4px' });
187
+
188
+ const group = document.createElement('div');
189
+ group.className = 'spp-border-group';
190
+
191
+ // Стиль линии
192
+ const styleRow = _borderRow('Стиль:');
193
+ const styleBtns = document.createElement('div');
194
+ styleBtns.className = 'spp-style-btns';
195
+ inst._borderStyleBtns = {};
196
+ [
197
+ { value: 'solid', svg: '<svg width="22" height="14" viewBox="0 0 22 14"><line x1="1" y1="7" x2="21" y2="7" stroke="currentColor" stroke-width="2"/></svg>' },
198
+ { value: 'dashed', svg: '<svg width="22" height="14" viewBox="0 0 22 14"><line x1="1" y1="7" x2="21" y2="7" stroke="currentColor" stroke-width="2" stroke-dasharray="4 3"/></svg>' },
199
+ { value: 'dotted', svg: '<svg width="22" height="14" viewBox="0 0 22 14"><line x1="1" y1="7" x2="21" y2="7" stroke="currentColor" stroke-width="2" stroke-dasharray="2 3" stroke-linecap="round"/></svg>' },
200
+ ].forEach(({ value, svg }) => {
201
+ const b = document.createElement('button');
202
+ b.className = 'spp-btn';
203
+ b.title = value;
204
+ b.innerHTML = svg;
205
+ b.dataset.style = value;
206
+ b.addEventListener('click', () => {
207
+ inst._emit({ updates: { properties: { borderStyle: value } } });
208
+ inst._updateBorderStyleBtns(value);
209
+ });
210
+ styleBtns.appendChild(b);
211
+ inst._borderStyleBtns[value] = b;
212
+ });
213
+ styleRow.appendChild(styleBtns);
214
+ group.appendChild(styleRow);
215
+
216
+ // Толщина
217
+ const [widthRow, widthSlider, widthVal] = _sliderRow('Толщина:', 0, 10, 0.5, 1);
218
+ inst._borderWidthSlider = widthSlider;
219
+ inst._borderWidthVal = widthVal;
220
+ widthSlider.addEventListener('input', () => {
221
+ const v = parseFloat(widthSlider.value);
222
+ widthVal.textContent = String(v);
223
+ inst._emit({ updates: { properties: { borderWidth: v } } });
224
+ });
225
+ group.appendChild(widthRow);
226
+
227
+ // Прозрачность
228
+ const [opacityRow, opacitySlider, opacityVal] = _sliderRow('Прозрачность:', 0, 100, 1, 100, '%');
229
+ inst._borderOpacitySlider = opacitySlider;
230
+ inst._borderOpacityVal = opacityVal;
231
+ opacitySlider.addEventListener('input', () => {
232
+ const pct = parseInt(opacitySlider.value, 10);
233
+ opacityVal.textContent = `${pct}%`;
234
+ inst._emit({ updates: { properties: { borderOpacity: pct / 100 } } });
235
+ });
236
+ group.appendChild(opacityRow);
237
+
238
+ // Цвет рамки
239
+ const colorRow = _borderRow('Цвет рамки:');
240
+ const colorSwatch = document.createElement('button');
241
+ colorSwatch.className = 'spp-color-btn';
242
+ colorSwatch.style.backgroundColor = '#D4D4D4';
243
+ colorSwatch.title = 'Цвет рамки';
244
+ inst._borderColorBtn = colorSwatch;
245
+ inst._borderSwatches = [];
246
+
247
+ const bcPopover = document.createElement('div');
248
+ bcPopover.className = 'spp-popover';
249
+ Object.assign(bcPopover.style, { top: '100%', left: '0', marginTop: '4px', position: 'absolute' });
250
+ buildColorGrid(bcPopover, BORDER_COLORS, (color) => {
251
+ inst._borderColorBtn.style.backgroundColor = color.hex;
252
+ inst._emit({ updates: { properties: { borderColor: color.pixi } } });
253
+ inst._closePopover();
254
+ }, inst._borderSwatches);
255
+ inst._borderColorPopover = bcPopover;
256
+
257
+ const cWrap = document.createElement('div');
258
+ cWrap.style.position = 'relative';
259
+ cWrap.appendChild(colorSwatch);
260
+ cWrap.appendChild(bcPopover);
261
+ colorSwatch.addEventListener('click', (e) => {
262
+ e.stopPropagation();
263
+ inst._togglePopover(bcPopover);
264
+ });
265
+ colorRow.appendChild(cWrap);
266
+ group.appendChild(colorRow);
267
+
268
+ popover.appendChild(group);
269
+ inst._borderPopover = popover;
270
+
271
+ const wrap = document.createElement('div');
272
+ wrap.style.position = 'relative';
273
+ wrap.appendChild(trigger);
274
+ wrap.appendChild(popover);
275
+
276
+ trigger.addEventListener('click', (e) => {
277
+ e.stopPropagation();
278
+ inst._togglePopover(popover);
279
+ });
280
+
281
+ return wrap;
282
+ }
283
+
284
+ export function buildRadiusGroup(inst) {
285
+ const label = document.createElement('span');
286
+ label.className = 'spp-label';
287
+ label.textContent = 'Угол:';
288
+
289
+ const group = document.createElement('div');
290
+ group.className = 'spp-radius-group';
291
+
292
+ const slider = document.createElement('input');
293
+ slider.type = 'range';
294
+ slider.min = '0';
295
+ slider.max = '50';
296
+ slider.step = '1';
297
+ slider.value = '0';
298
+ slider.className = 'spp-radius-slider';
299
+ slider.title = 'Радиус скругления';
300
+ inst._radiusSlider = slider;
301
+
302
+ const valLabel = document.createElement('span');
303
+ valLabel.className = 'spp-label';
304
+ valLabel.textContent = '0';
305
+ valLabel.style.minWidth = '20px';
306
+ inst._radiusVal = valLabel;
307
+
308
+ slider.addEventListener('input', () => {
309
+ const v = parseInt(slider.value, 10);
310
+ valLabel.textContent = String(v);
311
+ inst._emit({ updates: { properties: { cornerRadius: v } } });
312
+ });
313
+
314
+ group.appendChild(slider);
315
+ group.appendChild(valLabel);
316
+ return [label, group];
317
+ }
318
+
319
+ export function buildTextGroup(inst) {
320
+ const nodes = [];
321
+
322
+ // Шрифт
323
+ const fontLabel = document.createElement('span');
324
+ fontLabel.className = 'spp-label';
325
+ fontLabel.textContent = 'Шрифт:';
326
+ nodes.push(fontLabel);
327
+
328
+ const fontSelect = document.createElement('select');
329
+ fontSelect.className = 'spp-select spp-select--font';
330
+ FONT_OPTIONS.forEach(({ value, name }) => {
331
+ const opt = document.createElement('option');
332
+ opt.value = value;
333
+ opt.textContent = name;
334
+ opt.style.fontFamily = value;
335
+ fontSelect.appendChild(opt);
336
+ });
337
+ fontSelect.addEventListener('change', () => {
338
+ inst._emit({ updates: { properties: { text: { fontFamily: fontSelect.value } } } });
339
+ });
340
+ inst._fontSelect = fontSelect;
341
+ nodes.push(fontSelect);
342
+
343
+ // Размер
344
+ const sizeLabel = document.createElement('span');
345
+ sizeLabel.className = 'spp-label';
346
+ sizeLabel.textContent = 'Р:';
347
+ nodes.push(sizeLabel);
348
+
349
+ const sizeSelect = document.createElement('select');
350
+ sizeSelect.className = 'spp-select spp-select--size';
351
+ FONT_SIZE_OPTIONS.forEach((sz) => {
352
+ const opt = document.createElement('option');
353
+ opt.value = sz;
354
+ opt.textContent = String(sz);
355
+ sizeSelect.appendChild(opt);
356
+ });
357
+ sizeSelect.value = '16';
358
+ sizeSelect.addEventListener('change', () => {
359
+ inst._emit({ updates: { properties: { text: { fontSize: parseInt(sizeSelect.value, 10) } } } });
360
+ });
361
+ inst._sizeSelect = sizeSelect;
362
+ nodes.push(sizeSelect);
363
+
364
+ nodes.push(sep());
365
+
366
+ // Цвет текста
367
+ const tcLabel = document.createElement('span');
368
+ tcLabel.className = 'spp-label';
369
+ tcLabel.textContent = 'Цвет:';
370
+ nodes.push(tcLabel);
371
+
372
+ const tcBtn = document.createElement('button');
373
+ tcBtn.className = 'spp-color-btn';
374
+ tcBtn.style.backgroundColor = '#111111';
375
+ tcBtn.title = 'Цвет текста';
376
+ inst._textColorBtn = tcBtn;
377
+ inst._textColorSwatches = [];
378
+
379
+ const tcPopover = document.createElement('div');
380
+ tcPopover.className = 'spp-popover';
381
+ Object.assign(tcPopover.style, { top: '100%', left: '0', marginTop: '4px' });
382
+ _buildTextColorGrid(tcPopover, inst);
383
+ inst._textColorPopover = tcPopover;
384
+
385
+ const tcWrap = document.createElement('div');
386
+ tcWrap.style.position = 'relative';
387
+ tcWrap.appendChild(tcBtn);
388
+ tcWrap.appendChild(tcPopover);
389
+ tcBtn.addEventListener('click', (e) => {
390
+ e.stopPropagation();
391
+ inst._togglePopover(tcPopover);
392
+ });
393
+ nodes.push(tcWrap);
394
+
395
+ nodes.push(sep());
396
+
397
+ // Bold
398
+ const boldBtn = document.createElement('button');
399
+ boldBtn.className = 'spp-btn';
400
+ boldBtn.title = 'Жирный';
401
+ boldBtn.innerHTML = '<svg width="14" height="14" viewBox="0 0 14 14"><text x="2" y="12" font-family="Arial" font-size="13" font-weight="bold" fill="currentColor">B</text></svg>';
402
+ boldBtn.addEventListener('click', () => {
403
+ const cur = boldBtn.classList.contains('spp-btn--active');
404
+ boldBtn.classList.toggle('spp-btn--active', !cur);
405
+ inst._emit({ updates: { properties: { text: { bold: !cur } } } });
406
+ });
407
+ inst._boldBtn = boldBtn;
408
+ nodes.push(boldBtn);
409
+
410
+ nodes.push(sep());
411
+
412
+ // Выравнивание
413
+ inst._alignBtns = {};
414
+ [
415
+ { value: 'left', svg: '<svg width="16" height="14" viewBox="0 0 16 14"><line x1="1" y1="3" x2="15" y2="3" stroke="currentColor" stroke-width="1.5"/><line x1="1" y1="7" x2="10" y2="7" stroke="currentColor" stroke-width="1.5"/><line x1="1" y1="11" x2="13" y2="11" stroke="currentColor" stroke-width="1.5"/></svg>', title: 'По левому краю' },
416
+ { value: 'center', svg: '<svg width="16" height="14" viewBox="0 0 16 14"><line x1="1" y1="3" x2="15" y2="3" stroke="currentColor" stroke-width="1.5"/><line x1="3" y1="7" x2="13" y2="7" stroke="currentColor" stroke-width="1.5"/><line x1="2" y1="11" x2="14" y2="11" stroke="currentColor" stroke-width="1.5"/></svg>', title: 'По центру' },
417
+ { value: 'right', svg: '<svg width="16" height="14" viewBox="0 0 16 14"><line x1="1" y1="3" x2="15" y2="3" stroke="currentColor" stroke-width="1.5"/><line x1="6" y1="7" x2="15" y2="7" stroke="currentColor" stroke-width="1.5"/><line x1="3" y1="11" x2="15" y2="11" stroke="currentColor" stroke-width="1.5"/></svg>', title: 'По правому краю' },
418
+ ].forEach(({ value, svg, title }) => {
419
+ const b = document.createElement('button');
420
+ b.className = 'spp-btn';
421
+ b.title = title;
422
+ b.innerHTML = svg;
423
+ b.dataset.align = value;
424
+ b.addEventListener('click', () => {
425
+ inst._setAlign(value);
426
+ inst._emit({ updates: { properties: { text: { textAlign: value } } } });
427
+ });
428
+ inst._alignBtns[value] = b;
429
+ nodes.push(b);
430
+ });
431
+
432
+ nodes.push(sep());
433
+
434
+ // Список
435
+ const listBtn = document.createElement('button');
436
+ listBtn.className = 'spp-btn';
437
+ listBtn.title = 'Маркированный список';
438
+ listBtn.innerHTML =
439
+ '<svg width="16" height="14" viewBox="0 0 16 14">' +
440
+ '<circle cx="2" cy="3" r="1.5" fill="currentColor"/><line x1="5" y1="3" x2="15" y2="3" stroke="currentColor" stroke-width="1.5"/>' +
441
+ '<circle cx="2" cy="7" r="1.5" fill="currentColor"/><line x1="5" y1="7" x2="15" y2="7" stroke="currentColor" stroke-width="1.5"/>' +
442
+ '<circle cx="2" cy="11" r="1.5" fill="currentColor"/><line x1="5" y1="11" x2="15" y2="11" stroke="currentColor" stroke-width="1.5"/>' +
443
+ '</svg>';
444
+ listBtn.addEventListener('click', () => {
445
+ const cur = listBtn.classList.contains('spp-btn--active');
446
+ listBtn.classList.toggle('spp-btn--active', !cur);
447
+ inst._emit({ updates: { properties: { text: { list: cur ? 'none' : 'bullet' } } } });
448
+ });
449
+ inst._listBtn = listBtn;
450
+ nodes.push(listBtn);
451
+
452
+ nodes.push(sep());
453
+
454
+ // Межстрочный интервал
455
+ const lhLabel = document.createElement('span');
456
+ lhLabel.className = 'spp-label';
457
+ lhLabel.textContent = 'Интервал:';
458
+ nodes.push(lhLabel);
459
+
460
+ const lhSelect = document.createElement('select');
461
+ lhSelect.className = 'spp-select';
462
+ lhSelect.style.minWidth = '60px';
463
+ [1.0, 1.2, 1.4, 1.6, 2.0].forEach((v) => {
464
+ const opt = document.createElement('option');
465
+ opt.value = String(v);
466
+ opt.textContent = String(v);
467
+ if (v === 1.4) opt.selected = true;
468
+ lhSelect.appendChild(opt);
469
+ });
470
+ lhSelect.addEventListener('change', () => {
471
+ inst._emit({ updates: { properties: { text: { lineHeight: parseFloat(lhSelect.value) } } } });
472
+ });
473
+ inst._lhSelect = lhSelect;
474
+ nodes.push(lhSelect);
475
+
476
+ return nodes;
477
+ }
478
+
479
+ // ── Внутренние утилиты ────────────────────────────────────────────────────────
480
+
481
+ function _borderRow(labelText) {
482
+ const row = document.createElement('div');
483
+ row.className = 'spp-border-row';
484
+ const lbl = document.createElement('span');
485
+ lbl.className = 'spp-border-label';
486
+ lbl.textContent = labelText;
487
+ row.appendChild(lbl);
488
+ return row;
489
+ }
490
+
491
+ function _sliderRow(labelText, min, max, step, defVal, suffix = '') {
492
+ const row = _borderRow(labelText);
493
+ const slider = document.createElement('input');
494
+ slider.type = 'range';
495
+ slider.min = String(min);
496
+ slider.max = String(max);
497
+ slider.step = String(step);
498
+ slider.value = String(defVal);
499
+ slider.className = 'spp-slider-full';
500
+ const valLabel = document.createElement('span');
501
+ valLabel.className = 'spp-slider-value';
502
+ valLabel.textContent = `${defVal}${suffix}`;
503
+ row.appendChild(slider);
504
+ row.appendChild(valLabel);
505
+ return [row, slider, valLabel];
506
+ }
507
+
508
+ function _buildTextColorGrid(container, inst) {
509
+ const grid = document.createElement('div');
510
+ grid.className = 'spp-color-grid';
511
+ TEXT_COLOR_PRESETS.forEach(({ color, name }) => {
512
+ const btn = document.createElement('button');
513
+ btn.type = 'button';
514
+ btn.className = 'spp-color-swatch' +
515
+ (color === '#ffffff' ? ' spp-color-swatch--white' : '');
516
+ btn.style.backgroundColor = color;
517
+ btn.title = name;
518
+ btn.dataset.colorHex = color.toUpperCase();
519
+ const tick = document.createElement('span');
520
+ tick.className = 'spp-tick';
521
+ btn.appendChild(tick);
522
+ btn.addEventListener('click', () => {
523
+ inst._textColorSwatches.forEach(s => s.classList.remove('spp-color-swatch--active'));
524
+ btn.classList.add('spp-color-swatch--active');
525
+ inst._textColorBtn.style.backgroundColor = color;
526
+ inst._emit({ updates: { properties: { text: { color } } } });
527
+ inst._closePopover();
528
+ });
529
+ grid.appendChild(btn);
530
+ inst._textColorSwatches.push(btn);
531
+ });
532
+ container.appendChild(grid);
533
+ }
@@ -0,0 +1,132 @@
1
+ import { FONT_OPTIONS, FONT_SIZE_OPTIONS } from '../text-properties/TextPropertiesPanelMapper.js';
2
+ import { SHAPE_ICONS } from './ShapePropertiesPanelDom.js';
3
+
4
+ const CARET_SVG = '<svg width="10" height="10" viewBox="0 0 10 10" fill="none"><path d="M2 3.5l3 3 3-3" stroke="#888" stroke-width="1.5" stroke-linecap="round"/></svg>';
5
+
6
+ /**
7
+ * Синхронизирует все контролы панели с текущим состоянием объекта.
8
+ * Читает данные через core.getObjectData(id).
9
+ */
10
+ export function updateControlsFromObject(inst) {
11
+ if (!inst.currentId) return;
12
+ const data = inst.core?.getObjectData?.(inst.currentId);
13
+ if (!data) return;
14
+
15
+ const props = data.properties || {};
16
+
17
+ _syncKind(inst, props.kind || 'square');
18
+ _syncRadius(inst, props.cornerRadius ?? 0);
19
+ _syncFillColor(inst, data.color ?? 0xFFFFFF);
20
+ _syncBorder(inst, props);
21
+ _syncText(inst, props.text || {});
22
+ }
23
+
24
+ export function updateBorderStyleBtns(inst, active) {
25
+ if (!inst._borderStyleBtns) return;
26
+ Object.entries(inst._borderStyleBtns).forEach(([val, btn]) => {
27
+ btn.classList.toggle('spp-btn--active', val === active);
28
+ });
29
+ }
30
+
31
+ export function setAlign(inst, value) {
32
+ if (!inst._alignBtns) return;
33
+ Object.entries(inst._alignBtns).forEach(([val, btn]) => {
34
+ btn.classList.toggle('spp-btn--active', val === value);
35
+ });
36
+ }
37
+
38
+ export function syncSwatches(swatches, hexUpper) {
39
+ if (!swatches) return;
40
+ swatches.forEach(s => {
41
+ const match = (s.dataset.colorHex || '').toUpperCase() === hexUpper.toUpperCase();
42
+ s.classList.toggle('spp-color-swatch--active', match);
43
+ });
44
+ }
45
+
46
+ export function pixiToHex(pixi) {
47
+ return `#${(pixi >>> 0).toString(16).padStart(6, '0').toUpperCase()}`;
48
+ }
49
+
50
+ // ── Приватные хелперы ─────────────────────────────────────────────────────────
51
+
52
+ function _syncKind(inst, kind) {
53
+ if (inst._kindButtons) {
54
+ Object.values(inst._kindButtons).forEach(b =>
55
+ b.classList.toggle('spp-kind-btn--active', b.dataset.kind === kind));
56
+ }
57
+ if (inst._kindTrigger && SHAPE_ICONS[kind]) {
58
+ inst._kindTrigger.innerHTML = SHAPE_ICONS[kind] + CARET_SVG;
59
+ }
60
+ }
61
+
62
+ function _syncRadius(inst, cr) {
63
+ if (inst._radiusSlider) {
64
+ inst._radiusSlider.value = String(cr);
65
+ if (inst._radiusVal) inst._radiusVal.textContent = String(cr);
66
+ }
67
+ }
68
+
69
+ function _syncFillColor(inst, fillPixi) {
70
+ if (inst._fillColorBtn) {
71
+ inst._fillColorBtn.style.backgroundColor = pixiToHex(fillPixi);
72
+ }
73
+ syncSwatches(inst._fillSwatches, pixiToHex(fillPixi));
74
+ }
75
+
76
+ function _syncBorder(inst, props) {
77
+ const bw = props.borderWidth ?? 1;
78
+ if (inst._borderWidthSlider) {
79
+ inst._borderWidthSlider.value = String(bw);
80
+ if (inst._borderWidthVal) inst._borderWidthVal.textContent = String(bw);
81
+ }
82
+
83
+ const bop = typeof props.borderOpacity === 'number' ? props.borderOpacity : 1;
84
+ if (inst._borderOpacitySlider) {
85
+ inst._borderOpacitySlider.value = String(Math.round(bop * 100));
86
+ if (inst._borderOpacityVal) inst._borderOpacityVal.textContent = `${Math.round(bop * 100)}%`;
87
+ }
88
+
89
+ updateBorderStyleBtns(inst, props.borderStyle || 'solid');
90
+
91
+ const bcPixi = props.borderColor ?? 0xD4D4D4;
92
+ if (inst._borderColorBtn) {
93
+ inst._borderColorBtn.style.backgroundColor = pixiToHex(bcPixi);
94
+ }
95
+ syncSwatches(inst._borderSwatches, pixiToHex(bcPixi));
96
+ }
97
+
98
+ function _syncText(inst, text) {
99
+ if (inst._fontSelect) {
100
+ const family = text.fontFamily || FONT_OPTIONS[0].value;
101
+ if ([...inst._fontSelect.options].some(o => o.value === family)) {
102
+ inst._fontSelect.value = family;
103
+ }
104
+ }
105
+
106
+ if (inst._sizeSelect) {
107
+ const fsStr = String(text.fontSize || 16);
108
+ if ([...inst._sizeSelect.options].some(o => o.value === fsStr)) {
109
+ inst._sizeSelect.value = fsStr;
110
+ }
111
+ }
112
+
113
+ if (inst._textColorBtn) {
114
+ inst._textColorBtn.style.backgroundColor = text.color || '#111111';
115
+ }
116
+
117
+ if (inst._boldBtn) {
118
+ inst._boldBtn.classList.toggle('spp-btn--active', !!text.bold);
119
+ }
120
+
121
+ setAlign(inst, text.textAlign || 'center');
122
+
123
+ if (inst._listBtn) {
124
+ inst._listBtn.classList.toggle('spp-btn--active', text.list === 'bullet');
125
+ }
126
+
127
+ if (inst._lhSelect) {
128
+ const lhStr = String(text.lineHeight || 1.4);
129
+ const found = [...inst._lhSelect.options].find(o => o.value === lhStr);
130
+ if (found) inst._lhSelect.value = lhStr;
131
+ }
132
+ }