@sequent-org/moodboard 1.4.58 → 1.4.60

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 (28) hide show
  1. package/package.json +1 -1
  2. package/src/core/ApiClient.js +41 -2
  3. package/src/core/PixiEngine.js +23 -2
  4. package/src/core/events/Events.js +1 -0
  5. package/src/core/flows/ClipboardFlow.js +48 -0
  6. package/src/objects/FrameObject.js +16 -3
  7. package/src/objects/ObjectFactory.js +2 -0
  8. package/src/objects/VideoObject.js +171 -0
  9. package/src/services/text/TextBoxMetrics.js +86 -0
  10. package/src/tools/manager/ToolManagerLifecycle.js +48 -2
  11. package/src/tools/object-tools/connector/ConnectorDragController.js +30 -1
  12. package/src/tools/object-tools/selection/NoteInlineEditorController.js +7 -1
  13. package/src/tools/object-tools/selection/TextEditorCaretService.js +4 -2
  14. package/src/tools/object-tools/selection/TextEditorInteractionController.js +14 -0
  15. package/src/tools/object-tools/selection/TextEditorLifecycleRegistry.js +7 -0
  16. package/src/tools/object-tools/selection/TextEditorPositioningService.js +4 -3
  17. package/src/tools/object-tools/selection/TextEditorSyncService.js +260 -6
  18. package/src/tools/object-tools/selection/TextInlineEditorController.js +44 -5
  19. package/src/ui/HtmlTextLayer.js +31 -13
  20. package/src/ui/animation/HoverLiftController.js +20 -0
  21. package/src/ui/chat/ChatWindow.js +39 -8
  22. package/src/ui/comments/CommentThreadPopover.js +3 -1
  23. package/src/ui/handles/HandlesDomRenderer.js +30 -3
  24. package/src/ui/handles/HandlesInteractionController.js +26 -28
  25. package/src/ui/styles/chat.css +43 -0
  26. package/src/ui/styles/topbar.css +20 -0
  27. package/src/ui/styles/workspace.css +35 -2
  28. package/src/ui/text-properties/TextPropertiesPanelRenderer.js +19 -19
@@ -9,6 +9,15 @@ import { MINDMAP_LAYOUT } from '../mindmap/MindmapLayoutConfig.js';
9
9
  import { MindmapStatePatchCommand } from '../../core/commands/MindmapStatePatchCommand.js';
10
10
 
11
11
  const HANDLES_ACCENT_COLOR = '#80D8FF';
12
+ const VERTICAL_RESIZE_CURSOR_COLOR = '#6B7280';
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']);
15
+ const VERTICAL_RESIZE_CORNER_CURSOR_ANGLES = {
16
+ nw: -45,
17
+ se: -45,
18
+ ne: 45,
19
+ sw: 45,
20
+ };
12
21
  const REVIT_SHOW_IN_MODEL_ICON_SVG = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 640" aria-hidden="true" focusable="false"><path d="M384 64C366.3 64 352 78.3 352 96C352 113.7 366.3 128 384 128L466.7 128L265.3 329.4C252.8 341.9 252.8 362.2 265.3 374.7C277.8 387.2 298.1 387.2 310.6 374.7L512 173.3L512 256C512 273.7 526.3 288 544 288C561.7 288 576 273.7 576 256L576 96C576 78.3 561.7 64 544 64L384 64zM144 160C99.8 160 64 195.8 64 240L64 496C64 540.2 99.8 576 144 576L400 576C444.2 576 480 540.2 480 496L480 416C480 398.3 465.7 384 448 384C430.3 384 416 398.3 416 416L416 496C416 504.8 408.8 512 400 512L144 512C135.2 512 128 504.8 128 496L128 240C128 231.2 135.2 224 144 224L224 224C241.7 224 256 209.7 256 192C256 174.3 241.7 160 224 160L144 160z"/></svg>';
13
22
  const MODEL3D_SHOW_IN_VIEWER_ICON_SVG = REVIT_SHOW_IN_MODEL_ICON_SVG;
14
23
  const MINDMAP_CHILD_WIDTH_FACTOR = 0.9;
@@ -39,6 +48,19 @@ function resolveBottomSiblingParentId(sourceObjectId, sourceMeta) {
39
48
  return sourceMeta?.parentId || null;
40
49
  }
41
50
 
51
+ function shouldUseVerticalResizeCursor(mbType, handleName) {
52
+ return VERTICAL_RESIZE_CURSOR_TYPES.has(mbType)
53
+ && (handleName === 'handle' || handleName === 'top' || handleName === 'bottom');
54
+ }
55
+
56
+ function createVerticalResizeCornerCursor(dir, rotation) {
57
+ const baseAngle = VERTICAL_RESIZE_CORNER_CURSOR_ANGLES[dir] || 0;
58
+ const totalAngle = baseAngle + (rotation || 0);
59
+ 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
+ const dataUrl = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
61
+ return `url("${dataUrl}") 12 12, nwse-resize`;
62
+ }
63
+
42
64
  function relayoutMindmapBranchLevel({ core, eventBus, parentId, side }) {
43
65
  if (!core || !eventBus || !parentId || (side !== 'left' && side !== 'right')) return;
44
66
  const objects = core?.state?.state?.objects || [];
@@ -1166,10 +1188,11 @@ export class HandlesDomRenderer {
1166
1188
  let sourceMindmapProperties = null;
1167
1189
  const occupiedOutgoingSides = new Set();
1168
1190
  const hiddenIncomingSide = { value: null };
1191
+ let mbType = null;
1169
1192
  if (id !== '__group__') {
1170
1193
  const req = { objectId: id, pixiObject: null };
1171
1194
  this.host.eventBus.emit('tool:get:object:pixi', req);
1172
- const mbType = req.pixiObject && req.pixiObject._mb && req.pixiObject._mb.type;
1195
+ mbType = req.pixiObject && req.pixiObject._mb && req.pixiObject._mb.type;
1173
1196
  isFileTarget = mbType === 'file';
1174
1197
  isFrameTarget = mbType === 'frame';
1175
1198
  isMindmapTarget = mbType === 'mindmap';
@@ -1244,7 +1267,9 @@ export class HandlesDomRenderer {
1244
1267
  }
1245
1268
 
1246
1269
  const mkCorner = (dir, x, y) => {
1247
- const cursor = createRotatedResizeCursor(dir, rotation);
1270
+ const cursor = shouldUseVerticalResizeCursor(mbType, 'handle')
1271
+ ? createVerticalResizeCornerCursor(dir, rotation)
1272
+ : createRotatedResizeCursor(dir, rotation);
1248
1273
  const h = document.createElement('div');
1249
1274
  h.dataset.dir = dir;
1250
1275
  h.dataset.id = id;
@@ -1288,7 +1313,9 @@ export class HandlesDomRenderer {
1288
1313
 
1289
1314
  const edgeSize = 10;
1290
1315
  const makeEdge = (name, style, cursorHandleType) => {
1291
- const cursor = createRotatedResizeCursor(cursorHandleType, rotation);
1316
+ const cursor = shouldUseVerticalResizeCursor(mbType, name)
1317
+ ? VERTICAL_RESIZE_CURSOR
1318
+ : createRotatedResizeCursor(cursorHandleType, rotation);
1292
1319
  const e = document.createElement('div');
1293
1320
  e.dataset.edge = name;
1294
1321
  e.dataset.id = id;
@@ -216,12 +216,14 @@ export class HandlesInteractionController {
216
216
  let aspectLockDominantAxis = null;
217
217
  let isTextTarget = false;
218
218
  let isNoteTarget = false;
219
+ let isVerticalResizeTarget = false;
219
220
  {
220
221
  const req = { objectId: id, pixiObject: null };
221
222
  this.host.eventBus.emit(Events.Tool.GetObjectPixi, req);
222
223
  const mbType = req.pixiObject && req.pixiObject._mb && req.pixiObject._mb.type;
223
224
  isTextTarget = (mbType === 'text' || mbType === 'simple-text');
224
225
  isNoteTarget = (mbType === 'note');
226
+ isVerticalResizeTarget = ['frame', 'text', 'simple-text', 'image'].includes(mbType);
225
227
  }
226
228
 
227
229
  const onMove = (ev) => {
@@ -263,9 +265,9 @@ export class HandlesInteractionController {
263
265
  newW = rotatedBox.width;
264
266
  newH = rotatedBox.height;
265
267
  } else {
266
- if (dir.includes('e')) newW = Math.max(1, startCSS.width + dx);
268
+ if (dir.includes('e') && !isVerticalResizeTarget) newW = Math.max(1, startCSS.width + dx);
267
269
  if (dir.includes('s')) newH = Math.max(1, startCSS.height + dy);
268
- if (dir.includes('w')) {
270
+ if (dir.includes('w') && !isVerticalResizeTarget) {
269
271
  newW = Math.max(1, startCSS.width - dx);
270
272
  newLeft = startCSS.left + dx;
271
273
  }
@@ -344,7 +346,7 @@ export class HandlesInteractionController {
344
346
  el.style.width = `${Math.max(1, Math.round(newW))}px`;
345
347
  el.style.height = 'auto';
346
348
  const measured = Math.max(1, Math.round(el.scrollHeight));
347
- newH = measured;
349
+ newH = Math.max(newH, measured);
348
350
  }
349
351
  } catch (_) {}
350
352
  }
@@ -418,35 +420,30 @@ export class HandlesInteractionController {
418
420
  const mbType = req.pixiObject && req.pixiObject._mb && req.pixiObject._mb.type;
419
421
  isFrameTarget = mbType === 'frame';
420
422
  }
421
- const resizeEndData = {
422
- object: id,
423
- oldSize: { width: startWorld.width, height: startWorld.height },
424
- newSize: { width: worldW, height: worldH },
425
- oldPosition: { x: startWorld.x, y: startWorld.y },
426
- newPosition: isFrameTarget ? null : (isEdgeLeftOrTop ? { x: worldX, y: worldY } : { x: startWorld.x, y: startWorld.y }),
427
- };
428
- this.host.eventBus.emit(Events.Tool.ResizeEnd, resizeEndData);
429
- try {
430
- const req2 = { objectId: id, pixiObject: null };
431
- this.host.eventBus.emit(Events.Tool.GetObjectPixi, req2);
432
- const mbType2 = req2.pixiObject && req2.pixiObject._mb && req2.pixiObject._mb.type;
433
- if (mbType2 === 'text' || mbType2 === 'simple-text') {
423
+
424
+ let finalWorldH = worldH;
425
+ if (isTextTarget) {
426
+ try {
434
427
  const textLayer = (typeof window !== 'undefined') ? window.moodboardHtmlTextLayer : null;
435
428
  const el = textLayer && textLayer.idToEl ? textLayer.idToEl.get && textLayer.idToEl.get(id) : null;
436
429
  if (el) {
437
430
  el.style.width = `${Math.max(1, Math.round(endCSS.width))}px`;
438
431
  el.style.height = 'auto';
439
432
  const measured = Math.max(1, Math.round(el.scrollHeight));
440
- const worldH2 = measured / s;
441
- const fixData = {
442
- object: id,
443
- size: { width: worldW, height: worldH2 },
444
- position: isFrameTarget ? null : (isEdgeLeftOrTop ? { x: worldX, y: worldY } : { x: startWorld.x, y: startWorld.y }),
445
- };
446
- this.host.eventBus.emit(Events.Tool.ResizeUpdate, fixData);
433
+ const finalCssH = Math.max(measured, endCSS.height);
434
+ finalWorldH = finalCssH / s;
447
435
  }
448
- }
449
- } catch (_) {}
436
+ } catch (_) {}
437
+ }
438
+
439
+ const resizeEndData = {
440
+ object: id,
441
+ oldSize: { width: startWorld.width, height: startWorld.height },
442
+ newSize: { width: worldW, height: finalWorldH },
443
+ oldPosition: { x: startWorld.x, y: startWorld.y },
444
+ newPosition: edgeFinalPositionChanged ? { x: worldX, y: worldY } : { x: startWorld.x, y: startWorld.y },
445
+ };
446
+ this.host.eventBus.emit(Events.Tool.ResizeEnd, resizeEndData);
450
447
  }
451
448
  };
452
449
 
@@ -626,7 +623,7 @@ export class HandlesInteractionController {
626
623
  el.style.width = `${Math.max(1, Math.round(newW))}px`;
627
624
  el.style.height = 'auto';
628
625
  const measured = Math.max(1, Math.round(el.scrollHeight));
629
- newH = measured;
626
+ newH = Math.max(newH, measured);
630
627
  }
631
628
  } catch (_) {}
632
629
  }
@@ -717,7 +714,7 @@ export class HandlesInteractionController {
717
714
  } else {
718
715
  const edgeFinalPositionChanged = (endCSS.left !== startCSS.left) || (endCSS.top !== startCSS.top);
719
716
  let finalWorldH = worldH;
720
- if (isTextTarget && (edge === 'left' || edge === 'right')) {
717
+ if (isTextTarget) {
721
718
  try {
722
719
  const textLayer = (typeof window !== 'undefined') ? window.moodboardHtmlTextLayer : null;
723
720
  const el = textLayer && textLayer.idToEl ? textLayer.idToEl.get && textLayer.idToEl.get(id) : null;
@@ -725,7 +722,8 @@ export class HandlesInteractionController {
725
722
  el.style.width = `${Math.max(1, Math.round(endCSS.width))}px`;
726
723
  el.style.height = 'auto';
727
724
  const measured = Math.max(1, Math.round(el.scrollHeight));
728
- finalWorldH = measured / s;
725
+ const finalCssH = Math.max(measured, endCSS.height);
726
+ finalWorldH = finalCssH / s;
729
727
  }
730
728
  } catch (_) {}
731
729
  }
@@ -32,6 +32,49 @@
32
32
  }
33
33
  }
34
34
 
35
+ /* Mobile (≤768px): панель генерации — компактный bottom-sheet во всю ширину,
36
+ приподнят над зум/карта-баром, не наезжает на тулбар и холст. */
37
+ @media (max-width: 768px) {
38
+ .moodboard-chat {
39
+ left: 8px;
40
+ right: 8px;
41
+ bottom: 84px; /* выше зум/карта-бара (16px + 60px touch-кнопки + зазор) */
42
+ transform: none;
43
+ width: auto;
44
+ max-width: none;
45
+ }
46
+
47
+ .moodboard-chat__composer {
48
+ min-height: 0;
49
+ padding: 0 6px 6px 8px;
50
+ gap: 6px;
51
+ }
52
+
53
+ /* iOS не зумит инпут при font-size ≥ 16px */
54
+ .moodboard-chat__textarea {
55
+ font-size: 16px;
56
+ min-height: 40px;
57
+ height: 40px;
58
+ padding: 12px 0 4px;
59
+ }
60
+
61
+ /* Пилюли и кнопка отправки переносятся, чтобы не выпадать за край */
62
+ .moodboard-chat__actions-row {
63
+ flex-wrap: wrap;
64
+ gap: 6px;
65
+ }
66
+
67
+ .moodboard-chat__pills {
68
+ gap: 8px 10px;
69
+ flex-wrap: wrap;
70
+ }
71
+
72
+ /* История диалога не должна занимать весь экран на телефоне */
73
+ .moodboard-chat__history {
74
+ max-height: 40vh;
75
+ }
76
+ }
77
+
35
78
  .moodboard-chat__history {
36
79
  max-height: 360px;
37
80
  overflow-y: auto;
@@ -111,3 +111,23 @@
111
111
  .moodboard-topbar__button--zoom { width: 32px; height: 32px; font-size: 16px; }
112
112
  .moodboard-topbar__zoom-label { min-width: 48px; text-align: center; font-size: 14px; }
113
113
 
114
+ /* Mobile (≤768px): топбар не должен выходить за край экрана. Сжимаем зазоры,
115
+ ограничиваем ширину вьюпортом и даём прокрутку внутри пилюли, если кнопок много. */
116
+ @media (max-width: 768px) {
117
+ .moodboard-topbar {
118
+ top: 8px;
119
+ left: 8px;
120
+ gap: 2px;
121
+ padding: 6px;
122
+ max-width: calc(100vw - 16px);
123
+ overflow-x: auto;
124
+ scrollbar-width: none; /* Firefox */
125
+ }
126
+
127
+ .moodboard-topbar::-webkit-scrollbar { display: none; }
128
+
129
+ .moodboard-topbar__button { flex: 0 0 auto; }
130
+
131
+ .moodboard-topbar__divider { margin: 0 3px; }
132
+ }
133
+
@@ -1575,9 +1575,42 @@
1575
1575
 
1576
1576
  /* Responsive */
1577
1577
  @media (max-width: 768px) {
1578
+ /* Тулбар прижимаем ближе к краю, чтобы оставить максимум холста.
1579
+ Высоту регулирует ToolbarResponsiveController (схлопывание в «Ещё»). */
1580
+ .moodboard-workspace__toolbar {
1581
+ left: 8px;
1582
+ }
1583
+
1578
1584
  .moodboard-toolbar {
1579
- overflow-x: auto;
1580
- padding: 10px;
1585
+ padding: 6px;
1586
+ gap: 6px;
1587
+ }
1588
+
1589
+ /* Зум- и карта-бары компактнее и ближе к краю на телефоне */
1590
+ .moodboard-zoombar {
1591
+ right: 72px;
1592
+ bottom: 16px;
1593
+ padding: 6px;
1594
+ gap: 6px;
1595
+ }
1596
+
1597
+ .moodboard-mapbar {
1598
+ right: 8px;
1599
+ bottom: 16px;
1600
+ padding: 6px;
1601
+ }
1602
+
1603
+ .moodboard-zoombar__menu,
1604
+ .moodboard-mapbar__popup {
1605
+ right: 0;
1606
+ }
1607
+
1608
+ /* Мини-карта не должна перекрывать половину экрана */
1609
+ .moodboard-mapbar__popup {
1610
+ width: 60vw;
1611
+ height: 28vh;
1612
+ min-width: 180px;
1613
+ min-height: 120px;
1581
1614
  }
1582
1615
  }
1583
1616
 
@@ -7,6 +7,16 @@ import {
7
7
  import { createTextFormatControls } from './TextFormatControls.js';
8
8
  import { createTextLockMoreControls } from './TextLockMoreControls.js';
9
9
 
10
+ const NO_COLOR_ICON_SVG = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 23.5C18.3513 23.5 23.5 18.3513 23.5 12C23.5 5.64873 18.3513 0.5 12 0.5C5.64873 0.5 0.5 5.64873 0.5 12C0.5 18.3513 5.64873 23.5 12 23.5Z" stroke="#B2B2B2" stroke-width="1.2"></path><path d="M4.27344 19.7266L19.7148 4.28516" stroke="#B2B2B2" stroke-width="1.2"></path></svg>`;
11
+
12
+ function createNoColorIcon(extraStyle = '') {
13
+ const wrapper = document.createElement('span');
14
+ wrapper.className = 'no-color-icon';
15
+ wrapper.style.cssText = `display:inline-flex;pointer-events:none;width:20px;height:20px;${extraStyle}`;
16
+ wrapper.innerHTML = NO_COLOR_ICON_SVG;
17
+ return wrapper;
18
+ }
19
+
10
20
  export function createTextPropertiesPanelRenderer(panelInstance) {
11
21
  const panel = document.createElement('div');
12
22
  panel.className = 'text-properties-panel';
@@ -106,20 +116,18 @@ export function updateCurrentBgColorButton(panelInstance, color) {
106
116
  line.remove();
107
117
  }
108
118
 
109
- if (!panelInstance.currentBgColorButton.querySelector('img.no-color-icon')) {
110
- const img = document.createElement('img');
111
- img.src = '/icons/no-color.svg';
112
- img.className = 'no-color-icon';
113
- img.style.cssText = 'width: 20px; height: 20px; pointer-events: none; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);';
114
- panelInstance.currentBgColorButton.appendChild(img);
119
+ if (!panelInstance.currentBgColorButton.querySelector('.no-color-icon')) {
120
+ panelInstance.currentBgColorButton.appendChild(
121
+ createNoColorIcon('position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);')
122
+ );
115
123
  }
116
124
  } else {
117
125
  panelInstance.currentBgColorButton.style.backgroundColor = color;
118
126
  panelInstance.currentBgColorButton.title = `Цвет выделения: ${color}`;
119
127
 
120
- const img = panelInstance.currentBgColorButton.querySelector('img.no-color-icon');
121
- if (img) {
122
- img.remove();
128
+ const icon = panelInstance.currentBgColorButton.querySelector('.no-color-icon');
129
+ if (icon) {
130
+ icon.remove();
123
131
  }
124
132
 
125
133
  const line = panelInstance.currentBgColorButton.querySelector('div');
@@ -514,11 +522,7 @@ function createHighlightColorGrid(panelInstance, container) {
514
522
  position: relative;
515
523
  `;
516
524
 
517
- const img = document.createElement('img');
518
- img.src = '/icons/no-color.svg';
519
- img.className = 'no-color-icon';
520
- img.style.cssText = 'width: 20px; height: 20px; pointer-events: none;';
521
- colorButton.appendChild(img);
525
+ colorButton.appendChild(createNoColorIcon());
522
526
  } else {
523
527
  colorButton.style.cssText = `
524
528
  width: 28px;
@@ -668,11 +672,7 @@ function createBackgroundColorGrid(panelInstance, container) {
668
672
  position: relative;
669
673
  `;
670
674
 
671
- const img = document.createElement('img');
672
- img.src = '/icons/no-color.svg';
673
- img.className = 'no-color-icon';
674
- img.style.cssText = 'width: 20px; height: 20px; pointer-events: none;';
675
- colorButton.appendChild(img);
675
+ colorButton.appendChild(createNoColorIcon());
676
676
  } else {
677
677
  colorButton.style.cssText = `
678
678
  width: 28px;