@sequent-org/moodboard 1.0.24 → 1.1.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.
@@ -28,8 +28,12 @@ export class FramePropertiesPanel {
28
28
  });
29
29
 
30
30
  // Обновляем позицию при любых изменениях (как в TextPropertiesPanel)
31
+ this.eventBus.on(Events.Tool.DragStart, () => this.hide());
31
32
  this.eventBus.on(Events.Tool.DragUpdate, () => this.reposition());
33
+ this.eventBus.on(Events.Tool.DragEnd, () => this.updateFromSelection());
32
34
  this.eventBus.on(Events.Tool.GroupDragUpdate, () => this.reposition());
35
+ this.eventBus.on(Events.Tool.GroupDragStart, () => this.hide());
36
+ this.eventBus.on(Events.Tool.GroupDragEnd, () => this.updateFromSelection());
33
37
  this.eventBus.on(Events.Tool.ResizeUpdate, () => this.reposition());
34
38
  this.eventBus.on(Events.Tool.RotateUpdate, () => this.reposition());
35
39
 
@@ -88,6 +92,7 @@ export class FramePropertiesPanel {
88
92
 
89
93
  // Обновляем контролы в соответствии с текущими свойствами объекта
90
94
  this._updateControlsFromObject();
95
+ this._syncTypeFromObject();
91
96
  }
92
97
 
93
98
  hide() {
@@ -104,16 +109,16 @@ export class FramePropertiesPanel {
104
109
  position: 'absolute',
105
110
  display: 'none',
106
111
  alignItems: 'center',
107
- gap: '8px',
108
- padding: '8px 12px',
112
+ gap: '4px',
113
+ padding: '4px 6px',
109
114
  backgroundColor: 'white',
110
- border: '1px solid #e0e0e0',
111
- borderRadius: '8px',
112
- boxShadow: '0 2px 8px rgba(0, 0, 0, 0.15)',
113
- fontSize: '14px',
115
+ border: '1px solid #e5e7eb',
116
+ borderRadius: '6px',
117
+ boxShadow: '0 2px 6px rgba(0, 0, 0, 0.12)',
118
+ fontSize: '12px',
114
119
  fontFamily: 'Arial, sans-serif',
115
- minWidth: '280px',
116
- height: '60px',
120
+ minWidth: '160px',
121
+ height: 'auto',
117
122
  zIndex: '10000'
118
123
  });
119
124
 
@@ -158,25 +163,25 @@ export class FramePropertiesPanel {
158
163
  const { x, y } = posData.position;
159
164
  const { width, height } = sizeData.size;
160
165
 
161
- // Позиционируем панель НАД фреймом, но ниже ручек ресайза
162
- const panelWidth = this.panel.offsetWidth || 200;
163
- const panelHeight = this.panel.offsetHeight || 44;
164
-
165
- const panelX = x + panelWidth/2.5; // по центру фрейма
166
-
167
- // Пытаемся разместить панель над фреймом
168
- let panelY = y ;
169
-
170
- // Если панель уходит за верхнюю границу экрана, размещаем её ниже фрейма
171
- // 10px ниже фрейма
166
+ // Позиционируем панель над фреймом, по центру
167
+ const panelRect = this.panel.getBoundingClientRect();
168
+ const panelW = Math.max(1, panelRect.width || 280);
169
+ const panelH = Math.max(1, panelRect.height || 60);
170
+ let panelX = x + (width / 2) - (panelW / 2);
171
+ let panelY = y - panelH - 40; // отступ 40px над фреймом
172
+
173
+ // Если панель уходит за верх, переносим ниже фрейма
174
+ if (panelY < 0) {
175
+ panelY = y + height + 40;
176
+ }
172
177
 
173
- console.log('🖼️ FramePropertiesPanel: Positioning above frame:', {
178
+ console.log('🖼️ FramePropertiesPanel: Positioning above frame:', {
174
179
  frameX: x, frameY: y, frameWidth: width, frameHeight: height,
175
180
  panelX, panelY
176
181
  });
177
182
 
178
- this.panel.style.left = `${panelX}px`;
179
- this.panel.style.top = `${panelY}px`;
183
+ this.panel.style.left = `${Math.round(panelX)}px`;
184
+ this.panel.style.top = `${Math.round(panelY)}px`;
180
185
 
181
186
  console.log('🖼️ FramePropertiesPanel: Panel CSS applied:', {
182
187
  left: this.panel.style.left,
@@ -190,16 +195,17 @@ export class FramePropertiesPanel {
190
195
  Object.assign(titleContainer.style, {
191
196
  display: 'flex',
192
197
  alignItems: 'center',
193
- gap: '8px',
194
- padding: '8px 12px'
198
+ gap: '4px',
199
+ padding: '4px 6px'
195
200
  });
196
201
 
197
202
  // Лейбл
198
203
  const titleLabel = document.createElement('span');
199
204
  titleLabel.textContent = 'Название:';
200
- titleLabel.style.fontSize = '12px';
205
+ titleLabel.style.fontSize = '11px';
201
206
  titleLabel.style.color = '#666';
202
- titleLabel.style.minWidth = '60px';
207
+ titleLabel.style.width = '56px';
208
+ titleLabel.style.textAlign = 'right';
203
209
 
204
210
  // Поле ввода для названия
205
211
  const titleInput = document.createElement('input');
@@ -207,11 +213,12 @@ export class FramePropertiesPanel {
207
213
  titleInput.placeholder = 'Название фрейма';
208
214
  Object.assign(titleInput.style, {
209
215
  flex: '1',
210
- padding: '4px 8px',
216
+ padding: '2px 4px',
211
217
  border: '1px solid #ddd',
212
218
  borderRadius: '4px',
213
219
  fontSize: '12px',
214
- outline: 'none'
220
+ outline: 'none',
221
+ height: '22px'
215
222
  });
216
223
 
217
224
  // Обработчик изменения названия
@@ -221,6 +228,14 @@ export class FramePropertiesPanel {
221
228
  }
222
229
  });
223
230
 
231
+ // Блокируем Delete/Backspace от всплытия, чтобы не удалялся весь фрейм при фокусе в поле
232
+ titleInput.addEventListener('keydown', (e) => {
233
+ const key = e.key;
234
+ if (key === 'Backspace' || key === 'Delete') {
235
+ e.stopPropagation();
236
+ }
237
+ });
238
+
224
239
  // Обработчик Enter для подтверждения
225
240
  titleInput.addEventListener('keypress', (e) => {
226
241
  if (e.key === 'Enter') {
@@ -239,22 +254,23 @@ export class FramePropertiesPanel {
239
254
  Object.assign(colorContainer.style, {
240
255
  display: 'flex',
241
256
  alignItems: 'center',
242
- gap: '8px',
243
- padding: '8px 12px'
257
+ gap: '4px',
258
+ padding: '4px 6px'
244
259
  });
245
260
 
246
261
  // Лейбл для цвета
247
262
  const colorLabel = document.createElement('span');
248
263
  colorLabel.textContent = 'Фон:';
249
- colorLabel.style.fontSize = '12px';
264
+ colorLabel.style.fontSize = '11px';
250
265
  colorLabel.style.color = '#666';
251
- colorLabel.style.minWidth = '60px';
266
+ colorLabel.style.width = '56px';
267
+ colorLabel.style.textAlign = 'right';
252
268
 
253
269
  // Кнопка выбора цвета
254
270
  const colorButton = document.createElement('button');
255
271
  Object.assign(colorButton.style, {
256
- width: '32px',
257
- height: '24px',
272
+ width: '20px',
273
+ height: '20px',
258
274
  border: '1px solid #ccc',
259
275
  borderRadius: '4px',
260
276
  cursor: 'pointer',
@@ -274,8 +290,60 @@ export class FramePropertiesPanel {
274
290
  colorContainer.appendChild(colorLabel);
275
291
  colorContainer.appendChild(colorButton);
276
292
 
293
+ // Контейнер для типа фрейма
294
+ const typeContainer = document.createElement('div');
295
+ Object.assign(typeContainer.style, {
296
+ display: 'flex',
297
+ alignItems: 'center',
298
+ gap: '4px',
299
+ padding: '4px 6px'
300
+ });
301
+
302
+ const typeLabel = document.createElement('span');
303
+ typeLabel.textContent = 'Тип:';
304
+ typeLabel.style.fontSize = '11px';
305
+ typeLabel.style.color = '#666';
306
+ typeLabel.style.width = '56px';
307
+ typeLabel.style.textAlign = 'right';
308
+
309
+ const typeSelect = document.createElement('select');
310
+ Object.assign(typeSelect.style, {
311
+ flex: '1',
312
+ padding: '2px 4px',
313
+ border: '1px solid #ddd',
314
+ borderRadius: '4px',
315
+ fontSize: '12px',
316
+ outline: 'none',
317
+ height: '22px',
318
+ maxWidth: '100%'
319
+ });
320
+ const options = [
321
+ { value: 'custom', label: 'Произвольный' },
322
+ { value: 'a4', label: 'A4' },
323
+ { value: '1x1', label: '1:1' },
324
+ { value: '4x3', label: '4:3' },
325
+ { value: '16x9', label: '16:9' }
326
+ ];
327
+ options.forEach(opt => {
328
+ const o = document.createElement('option');
329
+ o.value = opt.value;
330
+ o.textContent = opt.label;
331
+ typeSelect.appendChild(o);
332
+ });
333
+ this.frameTypeSelect = typeSelect;
334
+
335
+ typeSelect.addEventListener('change', () => {
336
+ if (!this.currentId) return;
337
+ const v = typeSelect.value;
338
+ this._applyFrameType(v);
339
+ });
340
+
341
+ typeContainer.appendChild(typeLabel);
342
+ typeContainer.appendChild(typeSelect);
343
+
277
344
  panel.appendChild(titleContainer);
278
345
  panel.appendChild(colorContainer);
346
+ panel.appendChild(typeContainer);
279
347
 
280
348
  // Создаем палитру цветов (скрытую)
281
349
  this._createColorPalette(panel);
@@ -446,6 +514,81 @@ export class FramePropertiesPanel {
446
514
  this.colorButton.title = `Цвет фона: ${hexColor}`;
447
515
  }
448
516
 
517
+ _syncTypeFromObject() {
518
+ if (!this.frameTypeSelect || !this.currentId) return;
519
+ const objectData = this.core.getObjectData(this.currentId);
520
+ const t = (objectData && objectData.properties && objectData.properties.type) || 'custom';
521
+ this.frameTypeSelect.value = t;
522
+ }
523
+
524
+ _applyFrameType(typeValue) {
525
+ if (!this.currentId) return;
526
+
527
+ // 1) Обновляем тип и временно отключаем фиксацию пропорций на время программного ресайза
528
+ const willLockAfter = typeValue !== 'custom';
529
+ this.eventBus.emit(Events.Object.StateChanged, {
530
+ objectId: this.currentId,
531
+ updates: { properties: { type: typeValue, lockedAspect: false } }
532
+ });
533
+
534
+ // 2) Для пресетов меняем размеры под аспект, сохраняя центр
535
+ if (!willLockAfter) return; // Произвольный: без изменения размеров
536
+
537
+ // Аспект по типу
538
+ const aspectMap = {
539
+ 'a4': 210 / 297,
540
+ '1x1': 1,
541
+ '4x3': 4 / 3,
542
+ '16x9': 16 / 9
543
+ };
544
+ const aspect = aspectMap[typeValue] || 1;
545
+
546
+ // Текущие позиция и размер
547
+ const posData = { objectId: this.currentId, position: null };
548
+ const sizeData = { objectId: this.currentId, size: null };
549
+ this.eventBus.emit(Events.Tool.GetObjectPosition, posData);
550
+ this.eventBus.emit(Events.Tool.GetObjectSize, sizeData);
551
+ if (!posData.position || !sizeData.size) return;
552
+
553
+ const oldX = posData.position.x;
554
+ const oldY = posData.position.y;
555
+ const oldW = Math.max(1, sizeData.size.width);
556
+ const oldH = Math.max(1, sizeData.size.height);
557
+ const cx = oldX + oldW / 2;
558
+ const cy = oldY + oldH / 2;
559
+
560
+ // Сохраняем визуальный масштаб: подбираем размеры с тем же приблизительным "площадью"
561
+ const area = oldW * oldH;
562
+ let newW = Math.max(1, Math.round(Math.sqrt(area * aspect)));
563
+ let newH = Math.max(1, Math.round(newW / aspect));
564
+
565
+ const newX = Math.round(cx - newW / 2);
566
+ const newY = Math.round(cy - newH / 2);
567
+
568
+ // Применяем через события resize для согласованности с историей/ядром
569
+ this.eventBus.emit(Events.Tool.ResizeUpdate, {
570
+ object: this.currentId,
571
+ size: { width: newW, height: newH },
572
+ position: { x: newX, y: newY }
573
+ });
574
+ this.eventBus.emit(Events.Tool.ResizeEnd, {
575
+ object: this.currentId,
576
+ oldSize: { width: oldW, height: oldH },
577
+ newSize: { width: newW, height: newH },
578
+ oldPosition: { x: oldX, y: oldY },
579
+ newPosition: { x: newX, y: newY }
580
+ });
581
+
582
+ // Обновим UI сразу
583
+ this._syncTypeFromObject();
584
+
585
+ // 3) Включаем обратно фиксацию пропорций (для пресетов)
586
+ this.eventBus.emit(Events.Object.StateChanged, {
587
+ objectId: this.currentId,
588
+ updates: { properties: { lockedAspect: true } }
589
+ });
590
+ }
591
+
449
592
  destroy() {
450
593
  // Удаляем обработчик клика по документу
451
594
  document.removeEventListener('click', this._documentClickHandler.bind(this));