@cyber-harbour/ui 1.0.32 → 1.0.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cyber-harbour/ui",
3
- "version": "1.0.32",
3
+ "version": "1.0.33",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -1,11 +1,17 @@
1
1
  import { createComponent, FabricComponent } from '../../Theme';
2
2
  import { styled } from 'styled-components';
3
3
 
4
- type BoxProps = FabricComponent<{
5
- children: any;
6
- }>;
4
+ type BoxProps = FabricComponent<
5
+ {
6
+ children: any;
7
+ } & React.DetailedHTMLProps<React.HTMLAttributes<HTMLDivElement>, HTMLDivElement>
8
+ >;
7
9
 
8
- export const Box = styled(createComponent<BoxProps>('div'))(
10
+ export const Box = ({ children, ...props }: BoxProps) => {
11
+ return <StyledBox {...props}>{children}</StyledBox>;
12
+ };
13
+
14
+ const StyledBox = styled(createComponent('div'))(
9
15
  ({ theme }) => `
10
16
  padding: ${theme.box.padding};
11
17
  border-radius: ${theme.box.borderRadius};
@@ -1,4 +1,4 @@
1
- import { createComponent, FabricComponent, pxToRem } from '../../Theme';
1
+ import { createComponent, FabricComponent } from '../../Theme';
2
2
  import { styled } from 'styled-components';
3
3
 
4
4
  type LineProps = FabricComponent<
@@ -12,6 +12,7 @@ type TypographyProps = FabricComponent<{
12
12
  fontStyle?: CSSProperties['fontStyle'];
13
13
  color?: ColorVariant | string;
14
14
  className?: string;
15
+ ellipsis?: boolean;
15
16
  }>;
16
17
 
17
18
  // Create a styled component that can be dynamically rendered as different HTML elements
@@ -20,7 +21,8 @@ const StyledTypography = styled(createComponent('div'))<{
20
21
  $weight?: CSSProperties['fontWeight'];
21
22
  $style?: CSSProperties['fontStyle'];
22
23
  $color?: ColorVariant | string;
23
- }>(({ theme, $variant, $color, $weight = '400', $style = 'initial' }) => {
24
+ $ellipsis?: boolean;
25
+ }>(({ theme, $variant, $color, $weight = '400', $style = 'initial', $ellipsis }) => {
24
26
  // Resolve color from theme if it's a theme color path, or use the direct color value
25
27
 
26
28
  return `
@@ -28,6 +30,7 @@ const StyledTypography = styled(createComponent('div'))<{
28
30
  font-weight: ${$weight};
29
31
  font-style: ${$style};
30
32
  color: ${resolveThemeColor(theme, $color) || theme.colors.text.main};
33
+ ${$ellipsis ? 'overflow: hidden; text-overflow: ellipsis; white-space: nowrap;' : ''}
31
34
  `;
32
35
  });
33
36
 
@@ -40,6 +43,7 @@ export const Typography = ({
40
43
  color,
41
44
  className,
42
45
  style,
46
+ ellipsis = false,
43
47
  ...props
44
48
  }: TypographyProps) => {
45
49
  // Determine which HTML element to render based on the variant if not explicitly specified
@@ -52,6 +56,7 @@ export const Typography = ({
52
56
  $weight={weight}
53
57
  $style={fontStyle}
54
58
  $color={color}
59
+ $ellipsis={ellipsis}
55
60
  className={className}
56
61
  style={style}
57
62
  {...props}
@@ -1,6 +1,6 @@
1
1
  import ForceGraph2D, { ForceGraphMethods, LinkObject, NodeObject } from 'react-force-graph-2d';
2
2
  import { Graph2DProps } from './types';
3
- import { useEffect, useRef, useState, useCallback } from 'react';
3
+ import { useEffect, useRef, useState, useCallback, useLayoutEffect } from 'react';
4
4
  import { forceCollide } from 'd3-force';
5
5
  import { styled } from 'styled-components';
6
6
  import eyeLightIcon from './eye_light.png';
@@ -8,6 +8,8 @@ import eyeLightHoverIcon from './eye_light_hover.png';
8
8
  import crossLightIcon from './cross_light.png';
9
9
  import crossLightHoverIcon from './cross_light_hover.png';
10
10
 
11
+ const ALPHA_MIN = 0.5;
12
+
11
13
  // Створюємо та налаштовуємо об'єкти зображень
12
14
  const imgEyeLightIcon = new Image();
13
15
  imgEyeLightIcon.src = eyeLightIcon;
@@ -37,20 +39,35 @@ export const Graph2D = ({
37
39
  maxZoom: 4, // Максимальний зум
38
40
  },
39
41
  onNodeClick,
42
+ onNodeHover,
43
+ onLinkHover,
40
44
  onLinkClick,
45
+ onBackgroundClick,
41
46
  }: Graph2DProps) => {
42
47
  // Стан для підсвічування вузлів і зв'язків
43
48
  const [highlightNodes, setHighlightNodes] = useState(new Set());
44
49
  const [highlightLinks, setHighlightLinks] = useState(new Set());
45
50
  const [hoverNode, setHoverNode] = useState<any>(null);
51
+ const [selectedNode, setSelectedNode] = useState<any>(null);
52
+ const [unVisibleNodes, setUnVisibleNodes] = useState(new Set<string>());
46
53
  const [hiddenNodes, setHiddenNodes] = useState(new Set<string>());
47
54
  const [collapsedNodes, setCollapsedNodes] = useState(new Set<string>());
48
55
  // Стани для відстеження наведення на кнопки
49
56
  const [hoverTopButton, setHoverTopButton] = useState(false);
50
57
  const [hoverBottomButton, setHoverBottomButton] = useState(false);
58
+ const [isRendering, setIsRendering] = useState(true);
51
59
 
52
- const fgRef = useRef<ForceGraphMethods>(null) as React.MutableRefObject<ForceGraphMethods<NodeObject, LinkObject>>;
60
+ const fgRef = useRef<
61
+ ForceGraphMethods & {
62
+ tick?: number;
63
+ }
64
+ >(null) as React.MutableRefObject<
65
+ ForceGraphMethods<NodeObject, LinkObject> & {
66
+ tick?: number;
67
+ }
68
+ >;
53
69
  const wrapperRef = useRef<HTMLDivElement>(null);
70
+ const tickTimerRef = useRef<NodeJS.Timeout | null>(null);
54
71
 
55
72
  // Функція для реверсивного масштабування тексту
56
73
  // При максимальному зумі текст має розмір config.fontSize
@@ -84,6 +101,8 @@ export const Graph2D = ({
84
101
  }
85
102
  }
86
103
 
104
+ onNodeHover?.(node);
105
+
87
106
  setHoverNode(node || null);
88
107
  setHighlightNodes(newHighlightNodes);
89
108
  setHighlightLinks(newHighlightLinks);
@@ -98,14 +117,42 @@ export const Graph2D = ({
98
117
  newHighlightLinks.add(link);
99
118
  newHighlightNodes.add(link.source);
100
119
  newHighlightNodes.add(link.target);
120
+ onLinkHover?.(link);
101
121
  }
102
122
 
103
123
  setHighlightNodes(newHighlightNodes);
104
124
  setHighlightLinks(newHighlightLinks);
105
125
  }, []);
106
126
 
127
+ const handleEngineTick = useCallback(() => {
128
+ if (isRendering)
129
+ if (
130
+ graphData &&
131
+ fgRef.current &&
132
+ fgRef.current.tick &&
133
+ graphData.nodes.length > 0 &&
134
+ graphData.nodes.length * ALPHA_MIN <= fgRef.current.tick
135
+ ) {
136
+ if (tickTimerRef.current) {
137
+ clearTimeout(tickTimerRef.current);
138
+ }
139
+ fgRef.current.zoomToFit(0, 20);
140
+ setIsRendering(false);
141
+ } else {
142
+ fgRef.current.tick = fgRef.current.tick ? (fgRef.current.tick = fgRef.current.tick + 1) : 1;
143
+ if (tickTimerRef.current) {
144
+ clearTimeout(tickTimerRef.current);
145
+ }
146
+ tickTimerRef.current = setTimeout(() => {
147
+ //force tick check
148
+ fgRef.current.zoomToFit(0, 20);
149
+ setIsRendering(false);
150
+ }, 1500);
151
+ }
152
+ }, [graphData]);
153
+
107
154
  // Створення взаємозв'язків між вузлами
108
- useEffect(() => {
155
+ useLayoutEffect(() => {
109
156
  if (!graphData) return;
110
157
 
111
158
  // Прив'язка вузлів до їхніх сусідів та зв'язків
@@ -175,13 +222,10 @@ export const Graph2D = ({
175
222
  .iterations(3) // Більше ітерацій для точнішого розрахунку
176
223
  .strength(1); // Максимальна сила (1 - тверде обмеження)
177
224
 
178
- fgRef.current.d3Force('collide', collideForce);
225
+ fgRef.current.pauseAnimation().d3Force('collide', collideForce).resumeAnimation();
179
226
  } catch (err) {
180
227
  console.error('Error setting up collision force:', err);
181
228
  }
182
-
183
- // Перезапустити симуляцію для застосування змін
184
- fgRef.current.resumeAnimation();
185
229
  }
186
230
  }, [graphData]);
187
231
 
@@ -292,15 +336,35 @@ export const Graph2D = ({
292
336
  [config, hoverTopButton, hoverBottomButton]
293
337
  );
294
338
 
295
- const collapseNode = (collapsed: Set<string>, node: NodeObject) => {
296
- if (node && node.id && !collapsed.has(`${node.id}`) && graphData) {
339
+ const hideNode = (unvisibles: Set<string>, node: NodeObject) => {
340
+ if (node && node.id && !unvisibles.has(`${node.id}`) && graphData) {
297
341
  // Прив'язка вузлів до їхніх сусідів та зв'язків
298
342
  const targets = graphData.links.filter((link: any) => {
299
343
  return link.source.id === node.id && link.label !== 'MATCH';
300
344
  });
301
345
  targets.forEach((link: any) => {
302
- collapsed.add(`${link.target.id}`);
303
- collapseNode(collapsed, link.target);
346
+ unvisibles.add(`${link.target.id}`);
347
+ hideNode(unvisibles, link.target);
348
+ });
349
+ }
350
+ };
351
+
352
+ const showNode = (unvisibles: Set<string>, node: NodeObject) => {
353
+ if (node && node.id && graphData) {
354
+ // Прив'язка вузлів до їхніх сусідів та зв'язків
355
+ const targets = graphData.links.filter((link: any) => {
356
+ return link.source.id === node.id && link.label !== 'MATCH';
357
+ });
358
+
359
+ targets.forEach((link: any) => {
360
+ if (unvisibles.has(`${link.target.id}`)) {
361
+ if (!hiddenNodes.has(`${link.target.id}`)) {
362
+ unvisibles.delete(`${link.target.id}`);
363
+ if (!collapsedNodes.has(`${link.target.id}`)) {
364
+ showNode(unvisibles, link.target);
365
+ }
366
+ }
367
+ }
304
368
  });
305
369
  }
306
370
  };
@@ -308,18 +372,31 @@ export const Graph2D = ({
308
372
  // Функція для обробки кліку на кнопку "сховати вузол"
309
373
  const handleHideNode = (node: any) => {
310
374
  const newHiddenNodes = new Set(hiddenNodes);
311
- collapseNode(newHiddenNodes, node);
312
- newHiddenNodes.add(node.id);
313
-
375
+ const newUnVisibleNodes = new Set(unVisibleNodes);
376
+ if (newHiddenNodes.has(node.id)) {
377
+ newHiddenNodes.delete(node.id);
378
+ showNode(newUnVisibleNodes, node);
379
+ } else {
380
+ newHiddenNodes.add(node.id);
381
+ hideNode(newUnVisibleNodes, node);
382
+ }
314
383
  setHiddenNodes(newHiddenNodes);
384
+ setUnVisibleNodes(newUnVisibleNodes);
315
385
  };
316
386
 
317
387
  // Функція для обробки кліку на кнопку "згорнути дочірні вузли"
318
388
  const handleCollapseChildren = (node: any) => {
319
389
  const newCollapsedNodes = new Set(collapsedNodes);
320
-
321
- collapseNode(newCollapsedNodes, node);
390
+ const newUnVisibleNodes = new Set(unVisibleNodes);
391
+ if (newCollapsedNodes.has(node.id)) {
392
+ newCollapsedNodes.delete(node.id);
393
+ showNode(newUnVisibleNodes, node);
394
+ } else {
395
+ newCollapsedNodes.add(node.id);
396
+ hideNode(newUnVisibleNodes, node);
397
+ }
322
398
  setCollapsedNodes(newCollapsedNodes);
399
+ setUnVisibleNodes(newUnVisibleNodes);
323
400
  };
324
401
 
325
402
  // Функція для визначення, чи знаходиться точка в межах сектора кола (кнопки)
@@ -381,7 +458,6 @@ export const Graph2D = ({
381
458
  return;
382
459
  }
383
460
 
384
- const nodeSize = config.nodeSizeBase;
385
461
  const buttonRadius = (config.nodeSizeBase * config.nodeAreaFactor) / 2;
386
462
  const zoom = fgRef.current.zoom() || 1; // Отримуємо поточний зум
387
463
 
@@ -466,6 +542,10 @@ export const Graph2D = ({
466
542
  };
467
543
  }, [hoverNode, config, isPointInButtonArea, hoverTopButton, hoverBottomButton]);
468
544
 
545
+ useEffect(() => {
546
+ if (fgRef.current) fgRef.current.zoomToFit(0, 20); // Автоматичне масштабування графа при першому рендері
547
+ }, [width, height]);
548
+
469
549
  const truncateText = (text: string, maxWidth: number, ctx: CanvasRenderingContext2D): string => {
470
550
  if (!text) return '';
471
551
 
@@ -488,6 +568,7 @@ export const Graph2D = ({
488
568
  };
489
569
 
490
570
  const renderGrid = (ctx: CanvasRenderingContext2D, globalScale: number) => {
571
+ if (isRendering) return; // Не малюємо сітку під час рендерингу
491
572
  // This will be called before each rendering frame
492
573
  ctx.getTransform();
493
574
  ctx.save();
@@ -521,7 +602,7 @@ export const Graph2D = ({
521
602
  globalScale: number
522
603
  ) => {
523
604
  const { x, y } = node;
524
- const radius = config.nodeSizeBase;
605
+ const radius = selectedNode === node ? (config.nodeSizeBase * config.nodeAreaFactor) / 2 : config.nodeSizeBase / 2;
525
606
 
526
607
  ctx.beginPath();
527
608
  ctx.arc(x as number, y as number, radius, 0, 2 * Math.PI);
@@ -535,14 +616,14 @@ export const Graph2D = ({
535
616
  // Якщо вузол підсвічений, малюємо кільце
536
617
  if (highlightNodes.has(node)) {
537
618
  // Якщо це наведений вузол, малюємо кнопки
538
- if (node === hoverNode) {
539
- paintNodeButtons(node, ctx, globalScale);
540
- } else {
541
- paintRing(node, ctx, globalScale);
542
- }
619
+ if (node !== selectedNode) paintRing(node, ctx, globalScale);
620
+ }
621
+
622
+ if (node === selectedNode) {
623
+ paintNodeButtons(node, ctx, globalScale);
543
624
  }
544
625
 
545
- const { x, y, color, label } = node;
626
+ const { x, y, color, fontColor, label } = node;
546
627
 
547
628
  const size = config.nodeSizeBase;
548
629
  const radius = config.nodeSizeBase / 2;
@@ -550,7 +631,8 @@ export const Graph2D = ({
550
631
  // Малюємо коло
551
632
  ctx.beginPath();
552
633
  ctx.arc(x as number, y as number, radius, 0, 2 * Math.PI);
553
- ctx.fillStyle = color; // Колір контуру TODO: додати прив'язку до значення label
634
+ ctx.fillStyle = collapsedNodes.has(node.id as string) ? `${color}50` : color; // Колір контуру з opacity для згорнутих вузлів
635
+
554
636
  ctx.fill();
555
637
 
556
638
  // пігтовока до малювання тексту
@@ -565,7 +647,7 @@ export const Graph2D = ({
565
647
  ctx.font = `${scaledFontSize}px Sans-Serif`;
566
648
  ctx.textAlign = 'center';
567
649
  ctx.textBaseline = 'middle';
568
- ctx.fillStyle = 'black';
650
+ ctx.fillStyle = fontColor;
569
651
 
570
652
  const truncatedLabel = truncateText(label, maxWidth, ctx);
571
653
  ctx.fillText(truncatedLabel, 0, 0);
@@ -577,11 +659,127 @@ export const Graph2D = ({
577
659
  // Отримуємо позиції початку і кінця зв'язку
578
660
  const { source, target, label } = link;
579
661
 
580
- if (!label) return; // Пропускаємо, якщо немає мітки
581
-
582
662
  // Координати початку і кінця зв'язку
583
663
  const start = { x: source.x, y: source.y };
584
664
  const end = { x: target.x, y: target.y };
665
+
666
+ // Відстань між вузлами
667
+ const dx = end.x - start.x;
668
+ const dy = end.y - start.y;
669
+ const distance = Math.sqrt(dx * dx + dy * dy);
670
+
671
+ // Розмір вузла
672
+ const sourceSize = config.nodeSizeBase;
673
+ const targetSize = config.nodeSizeBase;
674
+
675
+ // Нормалізовані вектори для напрямку
676
+ const unitDx = dx / distance;
677
+ const unitDy = dy / distance;
678
+
679
+ // Скоригований початок і кінець (щоб стрілка не починалася з центру вузла і не закінчувалася в центрі вузла)
680
+ const startRadius = sourceSize / 2;
681
+ const endRadius = targetSize / 2;
682
+
683
+ // Зміщені позиції початку і кінця
684
+ const adjustedStart = {
685
+ x: start.x + unitDx * startRadius,
686
+ y: start.y + unitDy * startRadius,
687
+ };
688
+
689
+ // Для кінцевої точки віднімаємо невелику відстань, щоб стрілка не заходила в вузол
690
+ const arrowHeadLength = 4;
691
+ const adjustedEnd = {
692
+ x: end.x - unitDx * (endRadius + arrowHeadLength),
693
+ y: end.y - unitDy * (endRadius + arrowHeadLength),
694
+ };
695
+
696
+ // Позиція для стрілки (трохи ближче до кінцевого вузла)
697
+ const adjusteArrowdEnd = {
698
+ x: end.x - unitDx * (endRadius + 1),
699
+ y: end.y - unitDy * (endRadius + 1),
700
+ };
701
+
702
+ // Малюємо лінію зв'язку з урахуванням місця для тексту, якщо він є
703
+ const lineColor = highlightLinks.has(link) ? '#ff9900' : '#999';
704
+ const lineWidth = highlightLinks.has(link) ? 1.5 : 0.5;
705
+
706
+ if (label) {
707
+ // Розраховуємо ширину тексту для визначення розміру проміжку
708
+ const scaledFontSize = calculateFontSize(globalScale);
709
+ ctx.font = `${scaledFontSize}px Sans-Serif`;
710
+ const textWidth = ctx.measureText(label).width;
711
+
712
+ // Розраховуємо довжину проміжку вздовж лінії
713
+ const gapLength = Math.sqrt(textWidth * textWidth + scaledFontSize * scaledFontSize);
714
+
715
+ // Загальна довжина лінії між вузлами
716
+ const lineLength = distance - startRadius - endRadius - arrowHeadLength;
717
+
718
+ // Розрахунок відстані від початку до середини і від середини до кінця
719
+ const halfLineLength = lineLength / 2;
720
+ const gapHalf = gapLength / 2;
721
+
722
+ // Малюємо першу частину лінії (від початку до проміжку)
723
+ if (halfLineLength > gapHalf) {
724
+ // Розрахунок точок перед проміжком
725
+ const gapStart = {
726
+ x: adjustedStart.x + unitDx * (halfLineLength - gapHalf),
727
+ y: adjustedStart.y + unitDy * (halfLineLength - gapHalf),
728
+ };
729
+
730
+ ctx.beginPath();
731
+ ctx.moveTo(adjustedStart.x, adjustedStart.y);
732
+ ctx.lineTo(gapStart.x, gapStart.y);
733
+ ctx.strokeStyle = lineColor;
734
+ ctx.lineWidth = lineWidth;
735
+ ctx.stroke();
736
+
737
+ // Розрахунок точок після проміжку
738
+ const gapEnd = {
739
+ x: adjustedStart.x + unitDx * (halfLineLength + gapHalf),
740
+ y: adjustedStart.y + unitDy * (halfLineLength + gapHalf),
741
+ };
742
+
743
+ ctx.beginPath();
744
+ ctx.moveTo(gapEnd.x, gapEnd.y);
745
+ ctx.lineTo(adjustedEnd.x, adjustedEnd.y);
746
+ ctx.strokeStyle = lineColor;
747
+ ctx.lineWidth = lineWidth;
748
+ ctx.stroke();
749
+ }
750
+ } else {
751
+ // Якщо немає тексту, малюємо повну лінію
752
+ ctx.beginPath();
753
+ ctx.moveTo(adjustedStart.x, adjustedStart.y);
754
+ ctx.lineTo(adjustedEnd.x, adjustedEnd.y);
755
+ ctx.strokeStyle = lineColor;
756
+ ctx.lineWidth = lineWidth;
757
+ ctx.stroke();
758
+ }
759
+
760
+ // Малюємо стрілку
761
+ const arrowHeadWidth = 2;
762
+ const angle = Math.atan2(dy, dx);
763
+
764
+ ctx.save();
765
+ ctx.translate(adjusteArrowdEnd.x, adjusteArrowdEnd.y);
766
+ ctx.rotate(angle);
767
+
768
+ // Малюємо наконечник стрілки
769
+ ctx.beginPath();
770
+ ctx.moveTo(0, 0);
771
+ ctx.lineTo(-arrowHeadLength, arrowHeadWidth);
772
+ ctx.lineTo(-arrowHeadLength, 0); // Стрілка трохи вдавлена всередину
773
+ ctx.lineTo(-arrowHeadLength, -arrowHeadWidth);
774
+ ctx.closePath();
775
+
776
+ ctx.fillStyle = highlightLinks.has(link) ? '#ff9900' : '#999';
777
+ ctx.fill();
778
+ ctx.restore();
779
+
780
+ // Якщо немає мітки, не малюємо текст
781
+ if (!label) return;
782
+
585
783
  // Знаходимо середину лінії для розміщення тексту
586
784
  const middleX = start.x + (end.x - start.x) / 2;
587
785
  const middleY = start.y + (end.y - start.y) / 2;
@@ -594,7 +792,6 @@ export const Graph2D = ({
594
792
  ctx.textBaseline = 'middle';
595
793
 
596
794
  // Визначення кута нахилу лінії для повороту тексту
597
- const angle = Math.atan2(end.y - start.y, end.x - start.x);
598
795
  ctx.save();
599
796
  // Переміщення до центру лінії та поворот тексту
600
797
  ctx.translate(middleX, middleY);
@@ -717,9 +914,13 @@ export const Graph2D = ({
717
914
  event.stopPropagation();
718
915
  return;
719
916
  }
720
-
721
917
  // Якщо клік не на кнопках, обробляємо клік на вузлі
722
- if (onNodeClick) onNodeClick(node);
918
+ setSelectedNode(node);
919
+ onNodeClick?.(node);
920
+ };
921
+
922
+ const handleBackgroundClick = (event: MouseEvent) => {
923
+ setSelectedNode(null);
723
924
  };
724
925
 
725
926
  return (
@@ -733,39 +934,48 @@ export const Graph2D = ({
733
934
  linkSource={linkSource}
734
935
  onLinkClick={onLinkClick}
735
936
  onNodeClick={handleNodeClick}
937
+ onBackgroundClick={handleBackgroundClick}
736
938
  nodeLabel={(node: any) => `${node.label || ''}`} // Показуємо повний текст у тултіпі
737
939
  linkLabel={(link: any) => link.label}
738
940
  nodeAutoColorBy="label"
739
- linkDirectionalArrowLength={3.5}
740
- linkDirectionalArrowRelPos={1}
741
941
  linkCurvature={0}
942
+ // Вимикаємо вбудовані стрілки, оскільки використовуємо свою реалізацію
943
+ linkDirectionalArrowLength={0}
742
944
  // Обмеження максимального зуму
743
- maxZoom={config.maxZoom}
744
- minZoom={1}
945
+ //maxZoom={config.maxZoom}
946
+ minZoom={0.01}
745
947
  // Додавання обробників наведення
746
948
  onNodeHover={handleNodeHover}
747
949
  onLinkHover={handleLinkHover}
950
+ onEngineTick={handleEngineTick}
951
+ d3AlphaMin={ALPHA_MIN}
952
+ d3VelocityDecay={0.4}
953
+ d3AlphaDecay={0.038}
748
954
  // Виділення зв'язків при наведенні
749
955
  linkWidth={(link: any) => (highlightLinks.has(link) ? 3 : 1)}
750
956
  linkColor={(link: any) => (highlightLinks.has(link) ? '#ff9900' : '#999')}
751
957
  onRenderFramePre={renderGrid}
752
958
  nodePointerAreaPaint={renderNodePointerAreaPaint}
753
959
  nodeCanvasObject={renderNodeCanvasObject}
754
- linkCanvasObjectMode={() => 'after'}
960
+ linkCanvasObjectMode={() => 'replace'} // 'replace' замість 'after' для повної заміни стандартного рендерингу зв'язків
755
961
  linkCanvasObject={renderLinkCanvasObject}
756
962
  linkVisibility={(link: any) => {
963
+ if (isRendering) return false; // Не показуємо вузол, якщо граф ще рендериться
964
+
757
965
  // Перевіряємо, чи вузол прихований
758
966
  if (hiddenNodes.has(link.source.id) || hiddenNodes.has(link.target.id)) return false;
759
967
  // Перевіряємо, чи вузол згорнутий
760
- if (collapsedNodes.has(link.source.id) || collapsedNodes.has(link.target.id)) return false;
968
+ if (collapsedNodes.has(link.source.id)) return false;
969
+ if (unVisibleNodes.has(link.source.id) || unVisibleNodes.has(link.target.id)) return false;
761
970
 
762
971
  return true; // Показуємо вузол, якщо не прихований і не згорнутий
763
972
  }}
764
973
  nodeVisibility={(node: NodeObject) => {
974
+ if (isRendering) return false; // Не показуємо вузол, якщо граф ще рендериться
765
975
  // Перевіряємо, чи вузол прихований
766
976
  if (hiddenNodes.has(node.id as string)) return false;
767
977
  // Перевіряємо, чи вузол згорнутий
768
- if (collapsedNodes.has(node.id as string)) return false;
978
+ if (unVisibleNodes.has(node.id as string)) return false;
769
979
  return true; // Показуємо вузол, якщо не прихований і не згорнутий
770
980
  }}
771
981
  />
@@ -1,4 +1,4 @@
1
- import { GraphData } from 'react-force-graph-2d';
1
+ import { GraphData, NodeObject } from 'react-force-graph-2d';
2
2
 
3
3
  export interface Graph2DProps {
4
4
  graphData?: GraphData;
@@ -19,7 +19,9 @@ export interface Graph2DProps {
19
19
  gridSpacing: number;
20
20
  dotSize: number;
21
21
  };
22
-
23
- onNodeClick?: (node: any) => void;
24
- onLinkClick?: (link: any) => void;
22
+ onNodeClick?: (node: NodeObject) => void;
23
+ onLinkClick?: (link: NodeObject) => void;
24
+ onNodeHover?: (node: NodeObject | null) => void;
25
+ onLinkHover?: (link: NodeObject | null) => void;
26
+ onBackgroundClick?: () => void;
25
27
  }
@@ -6,13 +6,19 @@ type ContainerProps = FabricComponent<{
6
6
  maxWidth?: string | number;
7
7
  }>;
8
8
 
9
- export const Container = ({ ...props }: ContainerProps) => {
10
- return <StyledContainer {...props} />;
9
+ type StyledContainerProps = {
10
+ $maxWidth?: string | number;
11
11
  };
12
12
 
13
- const StyledContainer = styled(createComponent<ContainerProps>('div'))`
14
- padding-inline: ${pxToRem(20)};
15
- width: 100%;
16
- min-width: 0;
17
- max-width: ${({ maxWidth }) => (typeof maxWidth === 'number' ? pxToRem(maxWidth) : maxWidth || '100%')};
18
- `;
13
+ export const Container = ({ maxWidth, ...props }: ContainerProps) => {
14
+ return <StyledContainer {...props} $maxWidth={maxWidth} />;
15
+ };
16
+
17
+ const StyledContainer = styled(createComponent<StyledContainerProps>('div'))(({ theme, $maxWidth }) => {
18
+ return `
19
+ padding-inline: ${pxToRem(20, theme.baseSize)};
20
+ width: 100%;
21
+ min-width: 0;
22
+ max-width: ${typeof $maxWidth === 'number' ? pxToRem($maxWidth, theme.baseSize) : $maxWidth || '100%'};
23
+ `;
24
+ });
@@ -10,6 +10,7 @@ import { Theme } from './types';
10
10
  */
11
11
  export const lightThemePx: Theme = {
12
12
  mode: 'light',
13
+ baseSize: 14, // Базовий розмір шрифту для конвертації px в rem
13
14
  // Секція кольорів з теми
14
15
  colors: {
15
16
  background: '#ffffff',
@@ -708,7 +709,5 @@ export const lightThemePx: Theme = {
708
709
  };
709
710
 
710
711
  // Конвертуємо всі розміри з px в rem
711
- export const lightTheme = convertPaletteToRem(lightThemePx) as DefaultTheme;
712
- export const darkTheme = convertPaletteToRem(lightThemePx) as DefaultTheme;
713
-
714
- console.log('Light theme:', lightTheme.contextMenu);
712
+ export const lightTheme = convertPaletteToRem(lightThemePx, lightThemePx.baseSize) as DefaultTheme;
713
+ export const darkTheme = convertPaletteToRem(lightThemePx, lightThemePx.baseSize) as DefaultTheme;
@@ -59,6 +59,7 @@ export type InputSizeStyle = {
59
59
  // Тип для палітри
60
60
  export type Theme = {
61
61
  mode: 'light' | 'dark';
62
+ baseSize: number;
62
63
  colors: {
63
64
  background: string;
64
65
  primary: {
@@ -73,8 +73,9 @@ export const pxToRem = (pxValue: number | string, baseSize: number = 16): string
73
73
  return `${remValue}rem`;
74
74
  };
75
75
 
76
- const IGNORE_CONVERT_KEYS: Record<string, string[]> = {
76
+ const IGNORE_CONVERT_KEYS: Record<string, string[] | boolean> = {
77
77
  contextMenu: ['padding'],
78
+ baseSize: true,
78
79
  };
79
80
 
80
81
  /**
@@ -107,7 +108,10 @@ export const convertPaletteToRem = (
107
108
  key.toLowerCase().includes(prop.toLowerCase())
108
109
  )
109
110
  ) {
110
- if (!(parentKey && IGNORE_CONVERT_KEYS[parentKey]?.includes(key))) {
111
+ if (
112
+ !(parentKey && Array.isArray(IGNORE_CONVERT_KEYS[parentKey]) && IGNORE_CONVERT_KEYS[parentKey].includes(key)) &&
113
+ !IGNORE_CONVERT_KEYS[key]
114
+ ) {
111
115
  result[key] = pxToRem(value, baseSize);
112
116
  } else {
113
117
  result[key] = value; // Keep original value if it's in the ignore list
package/tsup.config.ts CHANGED
@@ -19,8 +19,10 @@ export default defineConfig({
19
19
  platform: 'browser',
20
20
  skipNodeModulesBundle: true,
21
21
  esbuildOptions(options) {
22
- options.drop = ['console', 'debugger'];
23
- options.pure = ['console.log', 'console.info', 'console.debug', 'console.warn'];
22
+ if (process.env.NODE_ENV === 'development') {
23
+ options.drop = ['console', 'debugger'];
24
+ options.pure = ['console.log', 'console.info', 'console.debug', 'console.warn'];
25
+ }
24
26
  },
25
27
  shims: true,
26
28
  keepNames: true,