@sequent-org/moodboard 1.4.48 → 1.4.49

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.
@@ -125,6 +125,7 @@ export class CommentThreadPopover {
125
125
  this._onThreadOpened = this._onThreadOpened.bind(this);
126
126
  this._onRemote = () => this._refreshIfOpen();
127
127
  this._onOpenDraftAt = this._onOpenDraftAt.bind(this);
128
+ this._onOpenImageDraft = this._onOpenImageDraft.bind(this);
128
129
  this._onThreadDeleted = this._onThreadDeleted.bind(this);
129
130
  }
130
131
 
@@ -143,6 +144,7 @@ export class CommentThreadPopover {
143
144
  this.eventBus.on(Events.Comment.RemoteUpdated, this._onRemote);
144
145
  this.eventBus.on(Events.Comment.MessageAdded, this._onRemote);
145
146
  this.eventBus.on(Events.Comment.OpenDraftAt, this._onOpenDraftAt);
147
+ this.eventBus.on(Events.Comment.OpenImageDraft, this._onOpenImageDraft);
146
148
  this.eventBus.on(Events.Comment.ThreadDeleted, this._onThreadDeleted);
147
149
  this.eventBus.on(Events.Viewport.Changed, () => this.reposition());
148
150
  this.eventBus.on(Events.Tool.PanUpdate, () => this.reposition());
@@ -155,6 +157,7 @@ export class CommentThreadPopover {
155
157
  this.eventBus.off(Events.Comment.RemoteUpdated, this._onRemote);
156
158
  this.eventBus.off(Events.Comment.MessageAdded, this._onRemote);
157
159
  this.eventBus.off(Events.Comment.OpenDraftAt, this._onOpenDraftAt);
160
+ this.eventBus.off(Events.Comment.OpenImageDraft, this._onOpenImageDraft);
158
161
  this.eventBus.off(Events.Comment.ThreadDeleted, this._onThreadDeleted);
159
162
  if (this.layer) this.layer.remove();
160
163
  this.layer = null;
@@ -178,6 +181,70 @@ export class CommentThreadPopover {
178
181
  this.openDraftAt({ x: local.x, y: local.y });
179
182
  }
180
183
 
184
+ /**
185
+ * Открывает черновик комментария, привязанный к центру объекта.
186
+ * Если на объекте уже есть комментарии, смещает точку от занятых позиций.
187
+ */
188
+ _onOpenImageDraft({ objectId }) {
189
+ if (!objectId) return;
190
+
191
+ const posData = { objectId, position: null };
192
+ const sizeData = { objectId, size: null };
193
+ this.eventBus.emit(Events.Tool.GetObjectPosition, posData);
194
+ this.eventBus.emit(Events.Tool.GetObjectSize, sizeData);
195
+
196
+ if (!posData.position || !sizeData.size) return;
197
+
198
+ const { x: left, y: top } = posData.position;
199
+ const { width, height } = sizeData.size;
200
+
201
+ const anchored = this.commentService.getAllThreads().filter(
202
+ (t) => t.anchor_object_id === objectId
203
+ );
204
+
205
+ // anchor_dx/anchor_dy хранятся как смещение от top-left объекта,
206
+ // поэтому центр = (width/2, height/2) в этом пространстве
207
+ const { dx, dy } = this._findFreeCommentOffset(anchored, width / 2, height / 2);
208
+
209
+ this.openDraftAt(
210
+ { x: left + dx, y: top + dy },
211
+ { anchor_object_id: objectId, anchor_dx: dx, anchor_dy: dy }
212
+ );
213
+ }
214
+
215
+ /**
216
+ * Ищет свободный anchor-offset от top-left объекта.
217
+ * Начинает с центра (centerDx, centerDy), затем пробует 8 направлений шагами по 10px.
218
+ * «Свободным» считается offset, удалённый от всех занятых не менее чем на 12px.
219
+ */
220
+ _findFreeCommentOffset(anchoredThreads, centerDx, centerDy) {
221
+ const STEP = 10;
222
+ const MIN_DIST = 12;
223
+
224
+ const isFree = (dx, dy) => anchoredThreads.every((t) => {
225
+ const ex = t.anchor_dx || 0;
226
+ const ey = t.anchor_dy || 0;
227
+ return Math.sqrt((dx - ex) ** 2 + (dy - ey) ** 2) >= MIN_DIST;
228
+ });
229
+
230
+ if (isFree(centerDx, centerDy)) return { dx: centerDx, dy: centerDy };
231
+
232
+ const directions = [
233
+ [1, 0], [0, 1], [-1, 0], [0, -1],
234
+ [1, 1], [-1, 1], [1, -1], [-1, -1],
235
+ ];
236
+
237
+ for (let radius = 1; radius <= 20; radius++) {
238
+ for (const [dirX, dirY] of directions) {
239
+ const dx = centerDx + dirX * STEP * radius;
240
+ const dy = centerDy + dirY * STEP * radius;
241
+ if (isFree(dx, dy)) return { dx, dy };
242
+ }
243
+ }
244
+
245
+ return { dx: centerDx + STEP, dy: centerDy };
246
+ }
247
+
181
248
  _onThreadOpened({ threadId, pinEl }) {
182
249
  if (threadId == null && this._draftWorld) return;
183
250
  this._draftWorld = null;
@@ -203,6 +270,7 @@ export class CommentThreadPopover {
203
270
  this._closePalette();
204
271
  this._disarmOutsideClose();
205
272
  if (wasDraft) this.eventBus.emit(Events.Comment.DraftClosed, {});
273
+ this.eventBus.emit(Events.Comment.PopoverClosed, {});
206
274
  }
207
275
 
208
276
  reposition() {
@@ -18,6 +18,7 @@ export class ConnectionAnchorsLayer {
18
18
  this.hoveredObjectId = null;
19
19
  this._dragController = null;
20
20
  this._onAnchorPointerDown = null;
21
+ this._commentPopoverOpen = false;
21
22
  }
22
23
 
23
24
  attach() {
@@ -94,7 +95,12 @@ export class ConnectionAnchorsLayer {
94
95
  [Events.Viewport.Changed, () => this.update()],
95
96
  [Events.UI.ZoomPercent, () => this.update()],
96
97
  [Events.History.Changed, () => this.update()],
97
- [Events.Board.Loaded, () => this.update()]
98
+ [Events.Board.Loaded, () => this.update()],
99
+ [Events.Comment.ThreadOpened, () => { this._commentPopoverOpen = true; this.layer.innerHTML = ''; }],
100
+ [Events.Comment.DraftOpened, () => { this._commentPopoverOpen = true; this.layer.innerHTML = ''; }],
101
+ [Events.Comment.DraftClosed, () => { this._commentPopoverOpen = false; this.update(); }],
102
+ [Events.Comment.ThreadDeleted, () => { this._commentPopoverOpen = false; this.update(); }],
103
+ [Events.Comment.PopoverClosed, () => { this._commentPopoverOpen = false; this.update(); }],
98
104
  ];
99
105
 
100
106
  bindings.forEach(([event, handler]) => {
@@ -137,6 +143,7 @@ export class ConnectionAnchorsLayer {
137
143
 
138
144
  update() {
139
145
  if (!this.layer) return;
146
+ if (this._commentPopoverOpen) return;
140
147
  this.layer.innerHTML = '';
141
148
 
142
149
  const selection = Array.from(this.core?.selectTool?.selectedObjects || []);
@@ -355,7 +355,7 @@
355
355
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1), 0 1px 3px rgba(0, 0, 0, 0.06);
356
356
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
357
357
  font-size: 13px;
358
- z-index: 1000;
358
+ z-index: 3000;
359
359
  user-select: none;
360
360
  white-space: nowrap;
361
361
  }
@@ -604,3 +604,390 @@
604
604
  color: #6B7280;
605
605
  }
606
606
 
607
+ /* Border Radius Popover */
608
+ .ipp-border-radius-popover {
609
+ position: absolute;
610
+ top: calc(100% + 6px);
611
+ left: 50%;
612
+ transform: translateX(-50%);
613
+ background: #ffffff;
614
+ border: 1px solid #E5E7EB;
615
+ border-radius: 8px;
616
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1), 0 4px 10px rgba(0, 0, 0, 0.05);
617
+ padding: 10px 12px;
618
+ display: none;
619
+ flex-direction: column;
620
+ gap: 8px;
621
+ min-width: 220px;
622
+ z-index: 1001;
623
+ }
624
+
625
+ .ipp-border-radius-popover.is-open {
626
+ display: flex;
627
+ }
628
+
629
+ .ipp-border-radius-label {
630
+ font-size: 12px;
631
+ font-weight: 500;
632
+ color: #374151;
633
+ font-family: inherit;
634
+ user-select: none;
635
+ }
636
+
637
+ .ipp-border-radius-controls {
638
+ display: flex;
639
+ align-items: center;
640
+ gap: 8px;
641
+ }
642
+
643
+ .ipp-border-radius-slider {
644
+ flex: 1;
645
+ height: 4px;
646
+ -webkit-appearance: none;
647
+ appearance: none;
648
+ background: #E5E7EB;
649
+ border-radius: 2px;
650
+ outline: none;
651
+ cursor: pointer;
652
+ }
653
+
654
+ .ipp-border-radius-slider::-webkit-slider-runnable-track {
655
+ height: 4px;
656
+ border-radius: 2px;
657
+ }
658
+
659
+ .ipp-border-radius-slider::-moz-range-track {
660
+ height: 4px;
661
+ border-radius: 2px;
662
+ background: #E5E7EB;
663
+ }
664
+
665
+ .ipp-border-radius-slider::-moz-range-progress {
666
+ height: 4px;
667
+ border-radius: 2px;
668
+ background: #444ce7;
669
+ }
670
+
671
+ .ipp-border-radius-slider::-webkit-slider-thumb {
672
+ -webkit-appearance: none;
673
+ appearance: none;
674
+ width: 14px;
675
+ height: 14px;
676
+ border-radius: 50%;
677
+ background: #444ce7;
678
+ cursor: pointer;
679
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
680
+ margin-top: -5px;
681
+ }
682
+
683
+ .ipp-border-radius-slider::-moz-range-thumb {
684
+ width: 14px;
685
+ height: 14px;
686
+ border-radius: 50%;
687
+ background: #444ce7;
688
+ cursor: pointer;
689
+ border: none;
690
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
691
+ }
692
+
693
+ .ipp-border-radius-number {
694
+ width: 48px;
695
+ height: 28px;
696
+ border: 1px solid #E5E7EB;
697
+ border-radius: 6px;
698
+ font-size: 13px;
699
+ font-family: inherit;
700
+ color: #374151;
701
+ text-align: center;
702
+ padding: 0 4px;
703
+ line-height: 28px;
704
+ outline: none;
705
+ background: #fff;
706
+ -moz-appearance: textfield;
707
+ flex-shrink: 0;
708
+ }
709
+
710
+ .ipp-border-radius-number::-webkit-outer-spin-button,
711
+ .ipp-border-radius-number::-webkit-inner-spin-button {
712
+ -webkit-appearance: none;
713
+ margin: 0;
714
+ }
715
+
716
+ .ipp-border-radius-number:focus {
717
+ border-color: #6B7280;
718
+ }
719
+
720
+ /* Info Popover */
721
+ .ipp-info-popover {
722
+ position: absolute;
723
+ top: 0;
724
+ right: calc(100% + 4px);
725
+ background: #ffffff;
726
+ border: 1px solid #E5E7EB;
727
+ border-radius: 8px;
728
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1), 0 4px 10px rgba(0, 0, 0, 0.05);
729
+ padding: 6px;
730
+ display: none;
731
+ flex-direction: column;
732
+ min-width: 200px;
733
+ z-index: 1002;
734
+ font-family: inherit;
735
+ }
736
+
737
+ .ipp-info-popover.is-open {
738
+ display: flex;
739
+ }
740
+
741
+ .ipp-info-section {
742
+ padding: 6px 8px;
743
+ display: flex;
744
+ flex-direction: column;
745
+ gap: 2px;
746
+ }
747
+
748
+ .ipp-info-section + .ipp-info-section {
749
+ border-top: 1px solid #F3F4F6;
750
+ margin-top: 2px;
751
+ padding-top: 8px;
752
+ }
753
+
754
+ .ipp-info-section__label {
755
+ font-size: 11px;
756
+ color: #9CA3AF;
757
+ font-weight: 400;
758
+ line-height: 1.3;
759
+ }
760
+
761
+ .ipp-info-section__name {
762
+ font-size: 13px;
763
+ color: #374151;
764
+ font-weight: 400;
765
+ line-height: 1.4;
766
+ font-family: inherit;
767
+ }
768
+
769
+ .ipp-info-section__date {
770
+ font-size: 12px;
771
+ color: #9CA3AF;
772
+ font-family: inherit;
773
+ }
774
+
775
+ /* Border Style Popover */
776
+ .ipp-border-style-wrapper {
777
+ position: relative;
778
+ display: inline-flex;
779
+ }
780
+
781
+ .ipp-border-style-popover {
782
+ position: absolute;
783
+ top: calc(100% + 6px);
784
+ left: 50%;
785
+ transform: translateX(-50%);
786
+ background: #ffffff;
787
+ border: 1px solid #E5E7EB;
788
+ border-radius: 10px;
789
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1), 0 4px 10px rgba(0, 0, 0, 0.05);
790
+ padding: 12px;
791
+ display: none;
792
+ flex-direction: column;
793
+ gap: 10px;
794
+ min-width: 220px;
795
+ z-index: 1001;
796
+ font-family: inherit;
797
+ }
798
+
799
+ .ipp-border-style-popover.is-open {
800
+ display: flex;
801
+ }
802
+
803
+ .ipp-bs-style-row {
804
+ display: flex;
805
+ gap: 4px;
806
+ }
807
+
808
+ .ipp-bs-style-btn {
809
+ flex: 1;
810
+ display: flex;
811
+ align-items: center;
812
+ justify-content: center;
813
+ height: 30px;
814
+ border: 1.5px solid #E5E7EB;
815
+ border-radius: 6px;
816
+ background: #fff;
817
+ color: #6B7280;
818
+ cursor: pointer;
819
+ transition: border-color 0.15s, color 0.15s;
820
+ padding: 0;
821
+ }
822
+
823
+ .ipp-bs-style-btn:hover {
824
+ border-color: #9CA3AF;
825
+ color: #374151;
826
+ }
827
+
828
+ .ipp-bs-style-btn.is-active {
829
+ border-color: #444ce7;
830
+ color: #444ce7;
831
+ background: #EEF2FF;
832
+ }
833
+
834
+ .ipp-bs-section {
835
+ display: flex;
836
+ flex-direction: column;
837
+ gap: 6px;
838
+ }
839
+
840
+ .ipp-bs-row-header {
841
+ display: flex;
842
+ align-items: center;
843
+ justify-content: space-between;
844
+ }
845
+
846
+ .ipp-bs-label {
847
+ font-size: 12px;
848
+ font-weight: 500;
849
+ color: #374151;
850
+ font-family: inherit;
851
+ user-select: none;
852
+ }
853
+
854
+ .ipp-bs-value {
855
+ font-size: 12px;
856
+ color: #6B7280;
857
+ font-family: inherit;
858
+ user-select: none;
859
+ }
860
+
861
+ .ipp-bs-slider {
862
+ width: 100%;
863
+ height: 4px;
864
+ -webkit-appearance: none;
865
+ appearance: none;
866
+ background: #E5E7EB;
867
+ border-radius: 2px;
868
+ outline: none;
869
+ cursor: pointer;
870
+ }
871
+
872
+ .ipp-bs-slider::-webkit-slider-runnable-track {
873
+ height: 4px;
874
+ border-radius: 2px;
875
+ }
876
+
877
+ .ipp-bs-slider::-moz-range-track {
878
+ height: 4px;
879
+ border-radius: 2px;
880
+ background: #E5E7EB;
881
+ }
882
+
883
+ .ipp-bs-slider::-moz-range-progress {
884
+ height: 4px;
885
+ border-radius: 2px;
886
+ background: #444ce7;
887
+ }
888
+
889
+ .ipp-bs-slider::-webkit-slider-thumb {
890
+ -webkit-appearance: none;
891
+ appearance: none;
892
+ width: 14px;
893
+ height: 14px;
894
+ border-radius: 50%;
895
+ background: #444ce7;
896
+ cursor: pointer;
897
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
898
+ margin-top: -5px;
899
+ }
900
+
901
+ .ipp-bs-slider::-moz-range-thumb {
902
+ width: 14px;
903
+ height: 14px;
904
+ border-radius: 50%;
905
+ background: #444ce7;
906
+ cursor: pointer;
907
+ border: none;
908
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
909
+ }
910
+
911
+ .ipp-bs-palette {
912
+ display: flex;
913
+ flex-direction: column;
914
+ gap: 4px;
915
+ }
916
+
917
+ .ipp-bs-palette-row {
918
+ display: flex;
919
+ gap: 4px;
920
+ justify-content: space-between;
921
+ }
922
+
923
+ .ipp-bs-swatch {
924
+ width: 22px;
925
+ height: 22px;
926
+ border-radius: 50%;
927
+ border: none;
928
+ cursor: pointer;
929
+ padding: 0;
930
+ flex-shrink: 0;
931
+ transition: transform 0.1s, box-shadow 0.15s;
932
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
933
+ }
934
+
935
+ .ipp-bs-swatch:hover {
936
+ transform: scale(1.18);
937
+ }
938
+
939
+ .ipp-bs-swatch.is-active {
940
+ box-shadow: 0 0 0 2px #fff, 0 0 0 3.5px #444ce7;
941
+ }
942
+
943
+ .ipp-bs-swatch--none {
944
+ background: #fff;
945
+ position: relative;
946
+ overflow: hidden;
947
+ box-shadow: 0 0 0 1px #E5E7EB;
948
+ }
949
+
950
+ .ipp-bs-swatch--none::after {
951
+ content: '';
952
+ position: absolute;
953
+ top: 50%;
954
+ left: -2px;
955
+ right: -2px;
956
+ height: 1.5px;
957
+ background: #9CA3AF;
958
+ transform: translateY(-50%) rotate(-45deg);
959
+ pointer-events: none;
960
+ }
961
+
962
+ .ipp-bs-swatch--none.is-active {
963
+ box-shadow: 0 0 0 2px #fff, 0 0 0 3.5px #444ce7;
964
+ }
965
+
966
+ .ipp-bs-custom-row {
967
+ display: flex;
968
+ flex-wrap: wrap;
969
+ gap: 4px;
970
+ align-items: center;
971
+ }
972
+
973
+ .ipp-bs-add-btn {
974
+ width: 22px;
975
+ height: 22px;
976
+ border-radius: 50%;
977
+ border: 1.5px dashed #D1D5DB;
978
+ background: #fff;
979
+ color: #9CA3AF;
980
+ cursor: pointer;
981
+ display: flex;
982
+ align-items: center;
983
+ justify-content: center;
984
+ padding: 0;
985
+ flex-shrink: 0;
986
+ transition: border-color 0.15s, color 0.15s;
987
+ }
988
+
989
+ .ipp-bs-add-btn:hover {
990
+ border-color: #9CA3AF;
991
+ color: #6B7280;
992
+ }
993
+
@@ -0,0 +1,39 @@
1
+ import * as PIXI from 'pixi.js';
2
+
3
+ /**
4
+ * Применяет PIXI-маску с закруглёнными углами к спрайту изображения.
5
+ * Sprite имеет anchor(0.5, 0.5), поэтому маска рисуется от (-w/2, -h/2) в локальных координатах.
6
+ * @param {PIXI.Sprite} pixiObject
7
+ * @param {number} radius — радиус в мировых пикселях (0 = убрать маску)
8
+ */
9
+ export function applyRoundedMask(pixiObject, radius) {
10
+ const w = pixiObject.width;
11
+ const h = pixiObject.height;
12
+ if (!w || !h) return;
13
+
14
+ const maxR = Math.floor(Math.min(w, h) / 2);
15
+ const r = Math.max(0, Math.min(Math.round(radius), maxR));
16
+
17
+ if (pixiObject._borderRadiusMask) {
18
+ pixiObject._borderRadiusMask.destroy({ children: true });
19
+ pixiObject._borderRadiusMask = null;
20
+ }
21
+ pixiObject.mask = null;
22
+
23
+ if (r <= 0) return;
24
+
25
+ const sx = Math.abs(pixiObject.scale?.x || 1);
26
+ const sy = Math.abs(pixiObject.scale?.y || 1);
27
+ const localW = sx > 0 ? w / sx : w;
28
+ const localH = sy > 0 ? h / sy : h;
29
+ const localR = Math.min(r / Math.min(sx || 1, sy || 1), Math.floor(Math.min(localW, localH) / 2));
30
+
31
+ const g = new PIXI.Graphics();
32
+ g.beginFill(0xffffff);
33
+ g.drawRoundedRect(-localW / 2, -localH / 2, localW, localH, localR);
34
+ g.endFill();
35
+
36
+ pixiObject.addChild(g);
37
+ pixiObject._borderRadiusMask = g;
38
+ pixiObject.mask = g;
39
+ }