@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
@@ -60,7 +60,6 @@ const BOARD_IMAGE_REARRANGE_MS = 520;
60
60
  const BOARD_IMAGE_PENDING_ENTER_FACTOR = 1.6;
61
61
  // Каскад между блоками одного батча (мс): пользователь видит, что они приезжают друг за другом.
62
62
  const BOARD_IMAGE_PENDING_STAGGER_MS = 90;
63
- const REFERENCE_DRAG_PREVIEW_SIZE = 96;
64
63
 
65
64
  const MODEL_OPTIONS = [
66
65
  {
@@ -145,12 +144,12 @@ export class ChatWindow {
145
144
  this._pendingOverlays = new Map();
146
145
  this._pendingOverlayTimers = new Map();
147
146
  this._boardImageShiftAnimations = new Map();
148
- this._boardCursor = null;
149
- this._draggedReferenceObject = null;
150
- this._draggedReferenceStartPosition = null;
151
- this._referenceDragPreview = null;
152
- this._referenceDragHandlers = null;
153
147
  this._clearSelectionOnSendClick = null;
148
+ this._selectionHandlers = null;
149
+ // Окно от BoxSelectStart до BoxSelectCommit: в это время SelectionAdd
150
+ // приходит на каждый mousemove и не должен пушить превью в чат —
151
+ // финальный набор картинок мы получим из BoxSelectCommit по strict-contains.
152
+ this._boxSelectActive = false;
154
153
  }
155
154
 
156
155
  attach() {
@@ -180,7 +179,7 @@ export class ChatWindow {
180
179
  this._clearSelectionOnSendClick = () => this._clearBoardSelection();
181
180
  this._refs.send.addEventListener('click', this._clearSelectionOnSendClick);
182
181
  this._composer.attach();
183
- this._attachReferenceDragEvents();
182
+ this._attachSelectionEvents();
184
183
 
185
184
  this._extendedPromptModal = new ChatExtendedPromptModal(
186
185
  this._container,
@@ -263,13 +262,12 @@ export class ChatWindow {
263
262
  if (!this._attached) return;
264
263
  this._clearPendingOverlays();
265
264
  this._cancelBoardImageShiftAnimations();
266
- this._clearReferenceDragState();
267
265
  if (this._unsubscribe) { this._unsubscribe(); this._unsubscribe = null; }
268
266
  if (this._clearSelectionOnSendClick && this._refs?.send) {
269
267
  this._refs.send.removeEventListener('click', this._clearSelectionOnSendClick);
270
268
  this._clearSelectionOnSendClick = null;
271
269
  }
272
- this._detachReferenceDragEvents();
270
+ this._detachSelectionEvents();
273
271
  this._shiftedForImageBatchKeys.clear();
274
272
  this._boardImageShiftHistory.clear();
275
273
  this._composer?.destroy();
@@ -465,175 +463,94 @@ export class ChatWindow {
465
463
  };
466
464
  }
467
465
 
468
- _attachReferenceDragEvents() {
466
+ _attachSelectionEvents() {
469
467
  const eventBus = this._boardCore?.eventBus;
470
- if (!eventBus || typeof eventBus.on !== 'function' || this._referenceDragHandlers) return;
468
+ if (!eventBus || typeof eventBus.on !== 'function' || this._selectionHandlers) return;
471
469
 
472
- const onCursorMove = ({ x, y } = {}) => {
473
- if (Number.isFinite(x) && Number.isFinite(y)) {
474
- this._boardCursor = { x, y };
475
- this._updateReferenceDragPreview();
476
- }
470
+ const onSelectionAdd = (data) => {
471
+ void this._handleSelectionAdd(data);
477
472
  };
478
- const onDragStart = (data) => {
479
- this._handleReferenceDragStart(data);
473
+ const onSelectionRemove = (data) => {
474
+ const objectId = data?.object;
475
+ if (objectId) this._composer?.removeAttachmentForObject?.(objectId);
480
476
  };
481
- const onDragEnd = (data) => {
482
- void this._handleReferenceDragEnd(data);
477
+ const onSelectionClear = () => {
478
+ this._composer?.removeAllBoardAttachments?.();
483
479
  };
484
- const onSelectionAdd = (data) => {
485
- void this._handleSelectionAdd(data);
480
+ const onBoxSelectStart = () => {
481
+ this._boxSelectActive = true;
482
+ };
483
+ const onBoxSelectCommit = (data) => {
484
+ void this._handleBoxSelectCommit(data);
486
485
  };
487
486
 
488
- this._referenceDragHandlers = { onCursorMove, onDragStart, onDragEnd, onSelectionAdd };
489
- eventBus.on(Events.UI.CursorMove, onCursorMove);
490
- eventBus.on(Events.Tool.DragStart, onDragStart);
491
- eventBus.on(Events.Tool.DragEnd, onDragEnd);
487
+ this._selectionHandlers = {
488
+ onSelectionAdd,
489
+ onSelectionRemove,
490
+ onSelectionClear,
491
+ onBoxSelectStart,
492
+ onBoxSelectCommit
493
+ };
492
494
  eventBus.on(Events.Tool.SelectionAdd, onSelectionAdd);
495
+ eventBus.on(Events.Tool.SelectionRemove, onSelectionRemove);
496
+ eventBus.on(Events.Tool.SelectionClear, onSelectionClear);
497
+ eventBus.on(Events.Tool.BoxSelectStart, onBoxSelectStart);
498
+ eventBus.on(Events.Tool.BoxSelectCommit, onBoxSelectCommit);
493
499
  }
494
500
 
495
- _detachReferenceDragEvents() {
501
+ _detachSelectionEvents() {
496
502
  const eventBus = this._boardCore?.eventBus;
497
- const handlers = this._referenceDragHandlers;
503
+ const handlers = this._selectionHandlers;
498
504
  if (!eventBus || typeof eventBus.off !== 'function' || !handlers) return;
499
505
 
500
- eventBus.off(Events.UI.CursorMove, handlers.onCursorMove);
501
- eventBus.off(Events.Tool.DragStart, handlers.onDragStart);
502
- eventBus.off(Events.Tool.DragEnd, handlers.onDragEnd);
503
506
  eventBus.off(Events.Tool.SelectionAdd, handlers.onSelectionAdd);
504
- this._referenceDragHandlers = null;
507
+ eventBus.off(Events.Tool.SelectionRemove, handlers.onSelectionRemove);
508
+ eventBus.off(Events.Tool.SelectionClear, handlers.onSelectionClear);
509
+ eventBus.off(Events.Tool.BoxSelectStart, handlers.onBoxSelectStart);
510
+ eventBus.off(Events.Tool.BoxSelectCommit, handlers.onBoxSelectCommit);
511
+ this._selectionHandlers = null;
512
+ this._boxSelectActive = false;
505
513
  }
506
514
 
507
515
  async _handleSelectionAdd(data = {}) {
508
- const objectId = data?.object;
509
- const object = this._boardCore?.state?.state?.objects?.find((item) => item?.id === objectId);
510
-
511
- if (!isReferenceImageObject(object)) return;
512
-
513
- await this._addImageObjectAsReference(object);
514
- }
516
+ // Во время box-select каждый mousemove перевыставляет selection и снова
517
+ // эмитит SelectionAdd для тех же id. Финальный набор reference-картинок
518
+ // мы получим из BoxSelectCommit по strict-contains, поэтому здесь молчим.
519
+ if (this._boxSelectActive) return;
515
520
 
516
- _handleReferenceDragStart(data = {}) {
517
521
  const objectId = data?.object;
518
522
  const object = this._boardCore?.state?.state?.objects?.find((item) => item?.id === objectId);
519
- this._draggedReferenceObject = isReferenceImageObject(object) ? object : null;
520
- this._draggedReferenceStartPosition = this._draggedReferenceObject?.position
521
- ? { ...this._draggedReferenceObject.position }
522
- : null;
523
- this._updateReferenceDragPreview();
524
- }
525
523
 
526
- async _handleReferenceDragEnd(data = {}) {
527
- const isDropTarget = this._isBoardCursorOverInput();
528
- const objectId = data?.object;
529
- const object = this._boardCore?.state?.state?.objects?.find((item) => item?.id === objectId);
530
- const startPosition = this._draggedReferenceStartPosition;
531
- this._clearReferenceDragState();
532
- if (!isDropTarget || !isReferenceImageObject(object)) return null;
524
+ if (!isReferenceImageObject(object)) return;
533
525
 
534
- this._restoreReferenceObjectPosition(objectId, startPosition);
535
526
  await this._addImageObjectAsReference(object);
536
527
  }
537
528
 
538
- _restoreReferenceObjectPosition(objectId, position) {
539
- if (!objectId || !position) return;
540
-
541
- const updatePosition = this._boardCore?.updateObjectPositionDirect;
542
- if (typeof updatePosition === 'function') {
543
- updatePosition.call(this._boardCore, objectId, position, { snap: false });
544
- return;
545
- }
546
-
547
- const object = this._boardCore?.state?.state?.objects?.find((item) => item?.id === objectId);
548
- if (object?.position) {
549
- object.position = { ...position };
550
- }
551
- }
529
+ async _handleBoxSelectCommit(data = {}) {
530
+ this._boxSelectActive = false;
531
+ const ids = Array.isArray(data?.selected) ? data.selected : (Array.isArray(data?.objects) ? data.objects : []);
532
+ if (!ids.length || !this._composer) return;
552
533
 
553
- _updateReferenceDragPreview() {
554
- const object = this._draggedReferenceObject;
555
- if (!object || !this._isBoardCursorOverInput()) {
556
- this._hideReferenceDragPreview();
557
- return;
558
- }
559
-
560
- const src = getImageObjectSource(object);
561
- if (!src) {
562
- this._hideReferenceDragPreview();
563
- return;
564
- }
565
-
566
- const preview = this._ensureReferenceDragPreview(object, src);
567
- const { clientX, clientY } = this._getBoardCursorClientPosition();
568
- preview.style.left = `${Math.round(clientX)}px`;
569
- preview.style.top = `${Math.round(clientY)}px`;
570
- this._refs?.textarea?.closest?.('.moodboard-chat__input-row')?.classList.add('is-reference-drop-target');
571
- }
572
-
573
- _ensureReferenceDragPreview(object, src) {
574
- if (!this._referenceDragPreview) {
575
- const preview = document.createElement('img');
576
- preview.className = 'moodboard-chat__reference-drag-preview';
577
- preview.alt = getImageObjectFileName(object, src);
578
- preview.width = REFERENCE_DRAG_PREVIEW_SIZE;
579
- preview.height = REFERENCE_DRAG_PREVIEW_SIZE;
580
- document.body.appendChild(preview);
581
- this._referenceDragPreview = preview;
582
- }
583
-
584
- if (this._referenceDragPreview.src !== src) {
585
- this._referenceDragPreview.src = src;
534
+ const all = this._boardCore?.state?.state?.objects ?? [];
535
+ const byId = new Map(all.map((item) => [item?.id, item]));
536
+ const seen = new Set();
537
+ for (const id of ids) {
538
+ if (!id || seen.has(id)) continue;
539
+ seen.add(id);
540
+ const obj = byId.get(id);
541
+ if (!obj || !isReferenceImageObject(obj)) continue;
542
+ await this._addImageObjectAsReference(obj);
586
543
  }
587
- this._referenceDragPreview.alt = getImageObjectFileName(object, src);
588
-
589
- return this._referenceDragPreview;
590
- }
591
-
592
- _hideReferenceDragPreview() {
593
- this._referenceDragPreview?.remove();
594
- this._referenceDragPreview = null;
595
- this._refs?.textarea?.closest?.('.moodboard-chat__input-row')?.classList.remove('is-reference-drop-target');
596
- }
597
-
598
- _clearReferenceDragState() {
599
- this._draggedReferenceObject = null;
600
- this._draggedReferenceStartPosition = null;
601
- this._boardCursor = null;
602
- this._hideReferenceDragPreview();
603
- }
604
-
605
- _isBoardCursorOverInput() {
606
- const cursor = this._boardCursor;
607
- const inputRow = this._refs?.textarea?.closest?.('.moodboard-chat__input-row');
608
- if (!cursor || !inputRow) return false;
609
-
610
- const containerRect = this._container.getBoundingClientRect?.();
611
- const rect = inputRow.getBoundingClientRect();
612
- const { clientX, clientY } = this._getBoardCursorClientPosition(containerRect);
613
-
614
- return clientX >= rect.left
615
- && clientX <= rect.right
616
- && clientY >= rect.top
617
- && clientY <= rect.bottom;
618
- }
619
-
620
- _getBoardCursorClientPosition(containerRect = null) {
621
- const rect = containerRect || this._container.getBoundingClientRect?.();
622
- const cursor = this._boardCursor || { x: 0, y: 0 };
623
-
624
- return {
625
- clientX: (rect?.left || 0) + cursor.x,
626
- clientY: (rect?.top || 0) + cursor.y
627
- };
628
544
  }
629
545
 
630
546
  async _addImageObjectAsReference(object) {
631
547
  if (!object || !this._composer) return;
548
+ if (this._composer.hasAttachmentForObject?.(object.id)) return;
632
549
 
633
550
  try {
634
551
  const file = await createFileFromImageObject(object);
635
552
  if (!file) return;
636
- this._composer.addAttachment(file);
553
+ this._composer.addAttachment(file, { sourceObjectId: object.id });
637
554
  this._composer.focus();
638
555
  } catch (err) {
639
556
  console.warn('[ChatWindow] cannot add selected image reference:', err);
@@ -0,0 +1,213 @@
1
+ import { Events } from '../../core/events/Events.js';
2
+ import { formatTime, pluralize, stripHtml } from './commentFormat.js';
3
+
4
+ const CLOSE_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
5
+ const CHECKMARK_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`;
6
+ const TRASH_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M3 6h18M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2M6 6l1 14a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-14"/></svg>`;
7
+
8
+ /**
9
+ * Боковая панель со списком всех тредов доски.
10
+ * Открывается/закрывается по Events.Comment.ListOpened (toggle).
11
+ */
12
+ export class CommentListPanel {
13
+ constructor(container, eventBus, core, commentService) {
14
+ this.container = container;
15
+ this.eventBus = eventBus;
16
+ this.core = core;
17
+ this.commentService = commentService;
18
+
19
+ this._panel = null;
20
+ this._body = null;
21
+ this._isOpen = false;
22
+
23
+ this._onListOpened = () => this.toggle();
24
+ this._onUpdate = () => { if (this._isOpen) this._renderList(); };
25
+ }
26
+
27
+ attach() {
28
+ this._panel = document.createElement('div');
29
+ this._panel.className = 'moodboard-comments-list';
30
+ this._panel.style.display = 'none';
31
+
32
+ const header = document.createElement('div');
33
+ header.className = 'moodboard-comments-list__header';
34
+
35
+ const closeBtn = document.createElement('button');
36
+ closeBtn.type = 'button';
37
+ closeBtn.className = 'moodboard-comments-list__close';
38
+ closeBtn.setAttribute('aria-label', 'Закрыть');
39
+ closeBtn.innerHTML = CLOSE_SVG;
40
+ closeBtn.addEventListener('click', () => this.hide());
41
+
42
+ const title = document.createElement('span');
43
+ title.className = 'moodboard-comments-list__title';
44
+ title.textContent = 'Комментарии';
45
+
46
+ header.appendChild(title);
47
+ header.appendChild(closeBtn);
48
+
49
+ this._body = document.createElement('div');
50
+ this._body.className = 'moodboard-comments-list__body';
51
+
52
+ this._panel.appendChild(header);
53
+ this._panel.appendChild(this._body);
54
+ this.container.appendChild(this._panel);
55
+
56
+ this.eventBus.on(Events.Comment.ListOpened, this._onListOpened);
57
+ this.eventBus.on(Events.Comment.RemoteUpdated, this._onUpdate);
58
+ this.eventBus.on(Events.Comment.MessageAdded, this._onUpdate);
59
+ this.eventBus.on(Events.Comment.Resolved, this._onUpdate);
60
+ this.eventBus.on(Events.Comment.ThreadDeleted, this._onUpdate);
61
+ this.eventBus.on(Events.Comment.PinCreated, this._onUpdate);
62
+ this.eventBus.on(Events.Comment.ColorChanged, this._onUpdate);
63
+ }
64
+
65
+ destroy() {
66
+ this.eventBus.off(Events.Comment.ListOpened, this._onListOpened);
67
+ this.eventBus.off(Events.Comment.RemoteUpdated, this._onUpdate);
68
+ this.eventBus.off(Events.Comment.MessageAdded, this._onUpdate);
69
+ this.eventBus.off(Events.Comment.Resolved, this._onUpdate);
70
+ this.eventBus.off(Events.Comment.ThreadDeleted, this._onUpdate);
71
+ this.eventBus.off(Events.Comment.PinCreated, this._onUpdate);
72
+ this.eventBus.off(Events.Comment.ColorChanged, this._onUpdate);
73
+ if (this._panel) {
74
+ this._panel.remove();
75
+ this._panel = null;
76
+ }
77
+ this._body = null;
78
+ }
79
+
80
+ toggle() {
81
+ if (this._isOpen) {
82
+ this.hide();
83
+ } else {
84
+ this.show();
85
+ }
86
+ }
87
+
88
+ show() {
89
+ if (!this._panel) return;
90
+ this._isOpen = true;
91
+ this._panel.style.display = 'flex';
92
+ this._renderList();
93
+ }
94
+
95
+ hide() {
96
+ if (!this._panel) return;
97
+ this._isOpen = false;
98
+ this._panel.style.display = 'none';
99
+ }
100
+
101
+ _renderList() {
102
+ if (!this._body) return;
103
+ const threads = this.commentService.getAllThreads();
104
+ this._body.replaceChildren();
105
+
106
+ if (!threads || threads.length === 0) {
107
+ const empty = document.createElement('div');
108
+ empty.className = 'moodboard-comments-list__empty';
109
+ empty.textContent = 'Пока нет комментариев';
110
+ this._body.appendChild(empty);
111
+ return;
112
+ }
113
+
114
+ const fragment = document.createDocumentFragment();
115
+ for (const thread of threads) {
116
+ fragment.appendChild(this._buildCard(thread));
117
+ }
118
+ this._body.appendChild(fragment);
119
+ }
120
+
121
+ _buildCard(thread) {
122
+ const root = document.createElement('div');
123
+ root.className = 'moodboard-comments-list__card' +
124
+ (thread.resolved ? ' moodboard-comments-list__card--resolved' : '');
125
+ root.addEventListener('click', (e) => this._onCardClick(e, thread));
126
+
127
+ const firstMsg = thread.messages?.items?.[0];
128
+ const currentUser = this.commentService.currentUser;
129
+ const isSelf = currentUser?.id != null && thread.created_by != null &&
130
+ String(thread.created_by) === String(currentUser.id);
131
+ const authorName = firstMsg?.author_name || (isSelf ? 'Вы' : 'Участник');
132
+ const timeStr = firstMsg?.created_at ? formatTime(firstMsg.created_at) : '';
133
+ const content = stripHtml(firstMsg?.content || '');
134
+
135
+ const colorDot = document.createElement('span');
136
+ colorDot.className = 'moodboard-comments-list__card-dot';
137
+ if (thread.color) colorDot.style.background = thread.color;
138
+
139
+ const meta = document.createElement('div');
140
+ meta.className = 'moodboard-comments-list__card-meta';
141
+
142
+ const author = document.createElement('span');
143
+ author.className = 'moodboard-comments-list__card-author';
144
+ author.textContent = authorName;
145
+
146
+ const time = document.createElement('span');
147
+ time.className = 'moodboard-comments-list__card-time';
148
+ time.textContent = timeStr;
149
+
150
+ meta.appendChild(author);
151
+ meta.appendChild(time);
152
+
153
+ const top = document.createElement('div');
154
+ top.className = 'moodboard-comments-list__card-top';
155
+ top.appendChild(colorDot);
156
+ top.appendChild(meta);
157
+
158
+ const body = document.createElement('div');
159
+ body.className = 'moodboard-comments-list__card-body';
160
+ body.textContent = content;
161
+
162
+ const replyCount = (thread.messages?.items?.length || 0) - 1;
163
+ let repliesEl = null;
164
+ if (replyCount > 0) {
165
+ repliesEl = document.createElement('div');
166
+ repliesEl.className = 'moodboard-comments-list__card-replies';
167
+ repliesEl.textContent = `${replyCount} ${pluralize(replyCount, 'ответ', 'ответа', 'ответов')}`;
168
+ }
169
+
170
+ const actions = document.createElement('div');
171
+ actions.className = 'moodboard-comments-list__card-actions';
172
+
173
+ const resolveBtn = document.createElement('button');
174
+ resolveBtn.type = 'button';
175
+ resolveBtn.className = 'moodboard-comments-list__resolve-btn' +
176
+ (thread.resolved ? ' moodboard-comments-list__resolve-btn--resolved' : '');
177
+ resolveBtn.setAttribute('aria-label', thread.resolved ? 'Вернуть' : 'Решить');
178
+ resolveBtn.innerHTML = CHECKMARK_SVG + `<span>${thread.resolved ? 'Вернуть' : 'Решить'}</span>`;
179
+ resolveBtn.addEventListener('click', (e) => {
180
+ e.stopPropagation();
181
+ this.commentService.resolveThread(thread.id, !thread.resolved).catch(console.error);
182
+ });
183
+
184
+ const deleteBtn = document.createElement('button');
185
+ deleteBtn.type = 'button';
186
+ deleteBtn.className = 'moodboard-comments-list__delete-btn';
187
+ deleteBtn.setAttribute('aria-label', 'Удалить');
188
+ deleteBtn.innerHTML = TRASH_SVG;
189
+ deleteBtn.addEventListener('click', (e) => {
190
+ e.stopPropagation();
191
+ this.commentService.deleteThread(thread.id).catch(console.error);
192
+ });
193
+
194
+ actions.appendChild(resolveBtn);
195
+ actions.appendChild(deleteBtn);
196
+
197
+ root.appendChild(top);
198
+ root.appendChild(body);
199
+ if (repliesEl) root.appendChild(repliesEl);
200
+ root.appendChild(actions);
201
+
202
+ return root;
203
+ }
204
+
205
+ _onCardClick(e, thread) {
206
+ const pos = this.commentService.getThreadWorldPosition(thread, this.core);
207
+ if (pos) {
208
+ this.eventBus.emit(Events.UI.MinimapCenterOn, { worldX: pos.x, worldY: pos.y });
209
+ this.eventBus.emit(Events.Viewport.Changed);
210
+ }
211
+ this.commentService.openThread(thread.id);
212
+ }
213
+ }