@sequent-org/moodboard 1.4.73 → 1.4.75

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sequent-org/moodboard",
3
- "version": "1.4.73",
3
+ "version": "1.4.75",
4
4
  "type": "module",
5
5
  "description": "Interactive moodboard",
6
6
  "main": "./src/index.js",
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-square-icon lucide-message-square"><path d="M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"/></svg>
@@ -1,5 +1,4 @@
1
1
  import { Toolbar } from '../../ui/Toolbar.js';
2
- import { SaveStatus } from '../../ui/SaveStatus.js';
3
2
  import { Topbar } from '../../ui/Topbar.js';
4
3
  import { ZoomPanel } from '../../ui/ZoomPanel.js';
5
4
  import { MapPanel } from '../../ui/MapPanel.js';
@@ -40,11 +39,6 @@ function initToolbar(board) {
40
39
  window.reloadIcon = (iconName) => board.toolbar.reloadToolbarIcon(iconName);
41
40
  }
42
41
 
43
- board.saveStatus = new SaveStatus(
44
- board.workspaceElement,
45
- board.coreMoodboard.eventBus
46
- );
47
-
48
42
  bindToolbarEvents(board);
49
43
  }
50
44
 
@@ -191,6 +191,27 @@ function _drawFillShape(g, w, h, kind, cornerRadius) {
191
191
  g.lineTo(skew, 0);
192
192
  break;
193
193
  }
194
+ case 'dialog': {
195
+ const r = Math.max(2, Math.min(w, h) * 0.16);
196
+ const bodyBottom = h - Math.max(4, h * 0.22);
197
+ const tailRightBase = w * 0.42;
198
+ const tailLeftBase = w * 0.24;
199
+ const tailTipX = w * 0.18;
200
+ g.moveTo(r, 0);
201
+ g.lineTo(w - r, 0);
202
+ g.arcTo(w, 0, w, r, r);
203
+ g.lineTo(w, bodyBottom - r);
204
+ g.arcTo(w, bodyBottom, w - r, bodyBottom, r);
205
+ g.lineTo(tailRightBase, bodyBottom);
206
+ g.lineTo(tailTipX, h);
207
+ g.lineTo(tailLeftBase, bodyBottom);
208
+ g.lineTo(r, bodyBottom);
209
+ g.arcTo(0, bodyBottom, 0, bodyBottom - r, r);
210
+ g.lineTo(0, r);
211
+ g.arcTo(0, 0, r, 0, r);
212
+ g.closePath();
213
+ break;
214
+ }
194
215
  case 'arrow': {
195
216
  const shaftH = Math.max(6, h * 0.3);
196
217
  const shaftY = (h - shaftH) / 2;
@@ -63,6 +63,12 @@ export function getShapeTextSafeArea(kind, w, h) {
63
63
  const shaftY = (H - shaftH) / 2;
64
64
  return { left: 0, top: shaftY, width: W * 0.6, height: shaftH };
65
65
  }
66
+ case 'dialog': {
67
+ // Тело облачка (без хвостика снизу) — bodyBottom совпадает с ShapeDrawer.
68
+ // Текст центрируется по вертикали внутри тела, а не всего бокса с хвостом.
69
+ const bodyBottom = H - Math.max(4, H * 0.22);
70
+ return { left: 0, top: 0, width: W, height: bodyBottom };
71
+ }
66
72
  case 'square':
67
73
  case 'rounded':
68
74
  default:
@@ -385,6 +385,13 @@ export function openMindmapEditor(object, create = false) {
385
385
  if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') {
386
386
  window.requestAnimationFrame(() => {
387
387
  alignTextareaLineTop();
388
+ // Каретка позиционируется по textarea.getBoundingClientRect(). После
389
+ // выравнивания раскладка textarea окончательна — пересчитываем каретку,
390
+ // иначе в медленном окружении она остаётся в позиции раннего кадра
391
+ // (когда textarea ещё не сжат/не отцентрован) и «прилипает» к верху капсулы.
392
+ if (this.textEditor && this.textEditor.caret && this.textEditor.textarea === textarea) {
393
+ updateCustomCaret(textarea, this.textEditor.caret);
394
+ }
388
395
  });
389
396
  }
390
397
  };
@@ -846,6 +853,20 @@ export function openMindmapEditor(object, create = false) {
846
853
  object,
847
854
  textarea,
848
855
  });
856
+
857
+ // При открытии редактора textarea сжимается до одной строки и центрируется flex-ом
858
+ // асинхронно; первый расчёт каретки может произойти до устаканивания раскладки
859
+ // (offsetY ≈ 0 → каретка у верха капсулы). Пересчитываем после двух кадров, когда
860
+ // геометрия textarea окончательна — независимо от скорости окружения.
861
+ if (typeof window !== 'undefined' && typeof window.requestAnimationFrame === 'function') {
862
+ window.requestAnimationFrame(() => {
863
+ window.requestAnimationFrame(() => {
864
+ if (this.textEditor && this.textEditor.caret && this.textEditor.textarea === textarea) {
865
+ updateCustomCaret(textarea, this.textEditor.caret);
866
+ }
867
+ });
868
+ });
869
+ }
849
870
  }
850
871
 
851
872
  export function closeMindmapEditor(commit) {
@@ -89,6 +89,7 @@ export class HtmlHandlesLayer {
89
89
  this._onDprChange = null;
90
90
  }
91
91
  this.eventBridge.detach();
92
+ this.interactionController.destroy();
92
93
 
93
94
  if (this.layer) {
94
95
  this.layer.remove();
@@ -227,6 +227,10 @@ export class NotePropertiesPanel {
227
227
  backgroundColor: '#fff',
228
228
  cursor: 'pointer',
229
229
  minWidth: '140px',
230
+ // Хост может форсить color-scheme: dark (как futurello), из-за чего
231
+ // нативный select и его опции рендерятся белым текстом на белом фоне.
232
+ colorScheme: 'light',
233
+ color: '#111827',
230
234
  });
231
235
  this.fontSelect = fontSelect;
232
236
 
@@ -249,6 +253,8 @@ export class NotePropertiesPanel {
249
253
  option.value = font.value;
250
254
  option.textContent = font.name;
251
255
  option.style.fontFamily = font.value;
256
+ option.style.color = '#111827';
257
+ option.style.backgroundColor = '#fff';
252
258
  fontSelect.appendChild(option);
253
259
  });
254
260
 
@@ -322,7 +328,12 @@ export class NotePropertiesPanel {
322
328
  border: '1px solid #ccc',
323
329
  borderRadius: '4px',
324
330
  fontSize: '11px',
325
- textAlign: 'center'
331
+ textAlign: 'center',
332
+ // Хост может форсить color-scheme: dark (как futurello) — фиксируем
333
+ // светлую схему и явные цвета, иначе число становится белым на чёрном.
334
+ colorScheme: 'light',
335
+ color: '#111827',
336
+ backgroundColor: '#fff'
326
337
  });
327
338
 
328
339
  fontSizeInput.addEventListener('change', () => {
@@ -11,7 +11,7 @@ import { MindmapStatePatchCommand } from '../../core/commands/MindmapStatePatchC
11
11
  const HANDLES_ACCENT_COLOR = '#80D8FF';
12
12
  const VERTICAL_RESIZE_CURSOR_COLOR = '#6B7280';
13
13
  const VERTICAL_RESIZE_CURSOR = 'url("/icons/move-vertical.svg") 12 12, ns-resize';
14
- const VERTICAL_RESIZE_CURSOR_TYPES = new Set(['frame', 'text', 'simple-text', 'image']);
14
+ const VERTICAL_RESIZE_CURSOR_TYPES = new Set(['frame', 'text', 'simple-text', 'image', 'shape']);
15
15
  const VERTICAL_RESIZE_CORNER_CURSOR_ANGLES = {
16
16
  nw: -45,
17
17
  se: -45,
@@ -50,15 +50,26 @@ function resolveBottomSiblingParentId(sourceObjectId, sourceMeta) {
50
50
 
51
51
  function shouldUseVerticalResizeCursor(mbType, handleName) {
52
52
  return VERTICAL_RESIZE_CURSOR_TYPES.has(mbType)
53
- && (handleName === 'handle' || handleName === 'top' || handleName === 'bottom');
53
+ && (handleName === 'handle'
54
+ || handleName === 'top'
55
+ || handleName === 'bottom'
56
+ || handleName === 'left'
57
+ || handleName === 'right');
54
58
  }
55
59
 
56
- function createVerticalResizeCornerCursor(dir, rotation) {
57
- const baseAngle = VERTICAL_RESIZE_CORNER_CURSOR_ANGLES[dir] || 0;
58
- const totalAngle = baseAngle + (rotation || 0);
60
+ function buildVerticalResizeArrowCursor(totalAngle, fallback) {
59
61
  const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><g transform="rotate(${totalAngle} 12 12)"><path d="M12 2v20" stroke="${VERTICAL_RESIZE_CURSOR_COLOR}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="m8 18 4 4 4-4" stroke="${VERTICAL_RESIZE_CURSOR_COLOR}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/><path d="m8 6 4-4 4 4" stroke="${VERTICAL_RESIZE_CURSOR_COLOR}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></g></svg>`;
60
62
  const dataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
61
- return `url("${dataUrl}") 12 12, nwse-resize`;
63
+ return `url("${dataUrl}") 12 12, ${fallback}`;
64
+ }
65
+
66
+ function createVerticalResizeCornerCursor(dir, rotation) {
67
+ const baseAngle = VERTICAL_RESIZE_CORNER_CURSOR_ANGLES[dir] || 0;
68
+ return buildVerticalResizeArrowCursor(baseAngle + (rotation || 0), 'nwse-resize');
69
+ }
70
+
71
+ function createHorizontalResizeCursor(rotation) {
72
+ return buildVerticalResizeArrowCursor(90 + (rotation || 0), 'ew-resize');
62
73
  }
63
74
 
64
75
  function relayoutMindmapBranchLevel({ core, eventBus, parentId, side }) {
@@ -1313,9 +1324,14 @@ export class HandlesDomRenderer {
1313
1324
 
1314
1325
  const edgeSize = 10;
1315
1326
  const makeEdge = (name, style, cursorHandleType) => {
1316
- const cursor = shouldUseVerticalResizeCursor(mbType, name)
1317
- ? VERTICAL_RESIZE_CURSOR
1318
- : createRotatedResizeCursor(cursorHandleType, rotation);
1327
+ let cursor;
1328
+ if (shouldUseVerticalResizeCursor(mbType, name)) {
1329
+ cursor = (name === 'left' || name === 'right')
1330
+ ? createHorizontalResizeCursor(rotation)
1331
+ : VERTICAL_RESIZE_CURSOR;
1332
+ } else {
1333
+ cursor = createRotatedResizeCursor(cursorHandleType, rotation);
1334
+ }
1319
1335
  const e = document.createElement('div');
1320
1336
  e.dataset.edge = name;
1321
1337
  e.dataset.id = id;
@@ -8,6 +8,30 @@ const TEXT_CORNER_MAX_FONT = 512;
8
8
  export class HandlesInteractionController {
9
9
  constructor(host) {
10
10
  this.host = host;
11
+ this._dragCursorStyleEl = null;
12
+ }
13
+
14
+ // Во время drag указатель уходит с маленького div-а ручки на canvas,
15
+ // и браузер подменяет курсор на дефолтный canvas-курсор — иконка ресайза пропадает.
16
+ // Глобальный <style> с !important держит нужную иконку видимой на всём документе.
17
+ _lockDragCursor(cursorValue) {
18
+ if (!cursorValue || typeof document === 'undefined') return;
19
+ this._unlockDragCursor();
20
+ const styleEl = document.createElement('style');
21
+ styleEl.textContent = `* { cursor: ${cursorValue} !important; }`;
22
+ document.head.appendChild(styleEl);
23
+ this._dragCursorStyleEl = styleEl;
24
+ }
25
+
26
+ _unlockDragCursor() {
27
+ if (this._dragCursorStyleEl) {
28
+ this._dragCursorStyleEl.remove();
29
+ this._dragCursorStyleEl = null;
30
+ }
31
+ }
32
+
33
+ destroy() {
34
+ this._unlockDragCursor();
11
35
  }
12
36
 
13
37
  _parseBoxRotation(box) {
@@ -191,6 +215,8 @@ export class HandlesInteractionController {
191
215
  }
192
216
  }
193
217
 
218
+ this._lockDragCursor(e.currentTarget.style.cursor);
219
+
194
220
  const world = this.host.core.pixi.worldLayer || this.host.core.pixi.app.stage;
195
221
  const s = world?.scale?.x || 1;
196
222
  const tx = world?.x || 0;
@@ -469,6 +495,7 @@ export class HandlesInteractionController {
469
495
  const onUp = () => {
470
496
  document.removeEventListener('pointermove', onMove);
471
497
  document.removeEventListener('pointerup', onUp);
498
+ this._unlockDragCursor();
472
499
 
473
500
  if (isTextCornerScale) {
474
501
  // Кегль уже сохранён слитой UpdateTextStyleCommand из onMove.
@@ -546,6 +573,7 @@ export class HandlesInteractionController {
546
573
  onEdgeResizeDown(e) {
547
574
  e.preventDefault();
548
575
  e.stopPropagation();
576
+ this._lockDragCursor(e.currentTarget.style.cursor);
549
577
  const id = e.currentTarget.dataset.id;
550
578
  const isGroup = id === '__group__';
551
579
  const edge = e.currentTarget.dataset.edge;
@@ -756,6 +784,7 @@ export class HandlesInteractionController {
756
784
  const onUp = () => {
757
785
  document.removeEventListener('pointermove', onMove);
758
786
  document.removeEventListener('pointerup', onUp);
787
+ this._unlockDragCursor();
759
788
 
760
789
  // Клик по рамке без перетаскивания на текстовом объекте → редактирование
761
790
  if (!_edgeHasMoved && isTextTarget && !isGroup) {
@@ -11,7 +11,7 @@
11
11
  .moodboard-chat {
12
12
  position: absolute;
13
13
  left: 50%;
14
- bottom: 32px;
14
+ bottom: 16px;
15
15
  transform: translateX(-50%);
16
16
  width: min(720px, 96%);
17
17
  z-index: 2500;
@@ -1386,7 +1386,11 @@
1386
1386
  .shape-parallelogram {
1387
1387
  width: 17px; height: 11px; background: #ffffff; border: 1.5px solid #94a3b8; transform: skewX(-20deg);
1388
1388
  }
1389
- .shape-arrow { font-size: 16px; color: #94a3b8; line-height: 1; }
1389
+ .shape-dialog {
1390
+ width: 15px; height: 15px; color: #94a3b8;
1391
+ display: inline-flex; align-items: center; justify-content: center;
1392
+ }
1393
+ .shape-dialog svg { width: 15px; height: 15px; display: block; }
1390
1394
 
1391
1395
 
1392
1396
 
@@ -118,6 +118,8 @@ export class ToolbarPopupsController {
118
118
  const grid = document.createElement('div');
119
119
  grid.className = 'moodboard-shapes__grid';
120
120
 
121
+ const DIALOG_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z"/></svg>';
122
+
121
123
  const shapes = [
122
124
  { id: 'shape', title: 'Добавить фигуру', isToolbarAction: true },
123
125
  { id: 'rounded-square', title: 'Скругленный квадрат' },
@@ -125,7 +127,7 @@ export class ToolbarPopupsController {
125
127
  { id: 'triangle', title: 'Треугольник' },
126
128
  { id: 'diamond', title: 'Ромб' },
127
129
  { id: 'parallelogram', title: 'Параллелограмм' },
128
- { id: 'arrow', title: 'Стрелка' }
130
+ { id: 'dialog', title: 'Диалог', svg: DIALOG_SVG }
129
131
  ];
130
132
 
131
133
  shapes.forEach((s) => {
@@ -137,9 +139,9 @@ export class ToolbarPopupsController {
137
139
  icon.className = 'moodboard-shapes__icon shape-square';
138
140
  } else {
139
141
  icon.className = `moodboard-shapes__icon shape-${s.id}`;
140
- if (s.id === 'arrow') {
141
- icon.innerHTML = '<svg width="18" height="12" viewBox="0 0 18 12" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><rect x="0" y="5" width="12" height="2" rx="1" fill="#94a3b8"/><path d="M12 0 L18 6 L12 12 Z" fill="#94a3b8"/></svg>';
142
- }
142
+ }
143
+ if (s.svg) {
144
+ icon.innerHTML = s.svg;
143
145
  }
144
146
  btn.appendChild(icon);
145
147
  btn.addEventListener('click', () => {
@@ -155,7 +157,7 @@ export class ToolbarPopupsController {
155
157
  triangle: { kind: 'triangle' },
156
158
  diamond: { kind: 'diamond' },
157
159
  parallelogram: { kind: 'parallelogram' },
158
- arrow: { kind: 'arrow' }
160
+ dialog: { kind: 'dialog' }
159
161
  };
160
162
  const props = propsMap[s.id] || { kind: 'square' };
161
163
  this.toolbar.eventBus.emit(Events.Place.Set, { type: 'shape', properties: props });