@cyber-harbour/ui 1.0.34 → 1.0.36
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/dist/eye_light-3WS4REO5.png +0 -0
- package/dist/eye_light_hover-PVS4UAB4.png +0 -0
- package/dist/group_light-RVCSCGRJ.png +0 -0
- package/dist/group_light_hover-LVI5PRZM.png +0 -0
- package/dist/index.d.mts +43 -5
- package/dist/index.d.ts +43 -5
- package/dist/index.js +184 -170
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +268 -254
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -1
- package/src/Core/Header/Header.tsx +1 -1
- package/src/Core/Sidebar/Sidebar.tsx +3 -3
- package/src/Core/Sidebar/SidebarDelimeter.tsx +1 -1
- package/src/FullscreenCard/FullscreenCard.tsx +39 -8
- package/src/Graph2D/Graph2D.tsx +71 -70
- package/src/Graph2D/GraphLoader.tsx +84 -0
- package/src/Graph2D/eye_light.png +0 -0
- package/src/Graph2D/eye_light_hover.png +0 -0
- package/src/Graph2D/group_light.png +0 -0
- package/src/Graph2D/group_light_hover.png +0 -0
- package/src/Graph2D/types.ts +1 -0
- package/src/Layouts/PageLayout/PageLayout.tsx +6 -3
- package/src/Theme/ThemeProvider.tsx +9 -3
- package/src/Theme/index.ts +1 -1
- package/src/Theme/themes/config.ts +41 -0
- package/src/Theme/themes/dark.ts +703 -0
- package/src/Theme/themes/index.ts +2 -0
- package/src/Theme/{theme.ts → themes/light.ts} +30 -41
- package/src/Theme/types.ts +24 -0
- package/dist/cross_light-JTZWFLSV.png +0 -0
- package/dist/cross_light_hover-UQZ7E3CW.png +0 -0
- package/dist/eye_light-EQXRQBFN.png +0 -0
- package/dist/eye_light_hover-5XFRPJS4.png +0 -0
- package/src/Graph2D/cross_light.png +0 -0
- package/src/Graph2D/cross_light_hover.png +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cyber-harbour/ui",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.36",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"module": "dist/index.mjs",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@types/d3-force": "^3.0.10",
|
|
31
31
|
"d3-force": "^3.0.0",
|
|
32
|
+
"react-content-loader": "^7.0.2",
|
|
32
33
|
"react-force-graph-2d": "^1.27.1",
|
|
33
34
|
"react-tiny-popover": "^8.1.6",
|
|
34
35
|
"styled-components": "^6.1.18"
|
|
@@ -20,7 +20,7 @@ const StyledContainer = styled.header(
|
|
|
20
20
|
padding-right: 20px;
|
|
21
21
|
height: 56px;
|
|
22
22
|
background-color: ${theme.colors.background};
|
|
23
|
-
border-bottom: 1px solid ${theme.colors.stroke.
|
|
23
|
+
border-bottom: 1px solid ${theme.colors.stroke.light};
|
|
24
24
|
|
|
25
25
|
&:before {
|
|
26
26
|
content: '';
|
|
@@ -40,8 +40,8 @@ const StyledContainer = styled.aside<StyledProps>(
|
|
|
40
40
|
width: ${theme.sidebar.width};
|
|
41
41
|
padding: 12px;
|
|
42
42
|
height: 100%;
|
|
43
|
-
border-right: 1px solid ${theme.
|
|
44
|
-
background: ${theme.
|
|
43
|
+
border-right: 1px solid ${theme.sidebar.border};
|
|
44
|
+
background: ${theme.sidebar.background};
|
|
45
45
|
${
|
|
46
46
|
$collapsed
|
|
47
47
|
? `
|
|
@@ -60,7 +60,7 @@ const StyledContainer = styled.aside<StyledProps>(
|
|
|
60
60
|
height: 25dvh;
|
|
61
61
|
transform: translateY(-100%);
|
|
62
62
|
background: ${theme.colors.background};
|
|
63
|
-
border-right: 1px solid ${theme.
|
|
63
|
+
border-right: 1px solid ${theme.sidebar.border};
|
|
64
64
|
|
|
65
65
|
width: ${theme.sidebar.width};
|
|
66
66
|
${
|
|
@@ -1,26 +1,57 @@
|
|
|
1
1
|
import styled from 'styled-components';
|
|
2
|
+
import { pxToRem } from '../Theme';
|
|
2
3
|
|
|
3
4
|
interface FullscreenCardProps {
|
|
4
5
|
children: any;
|
|
6
|
+
className?: string;
|
|
5
7
|
position: 'absolute' | 'fixed';
|
|
6
8
|
isActive: boolean;
|
|
9
|
+
top?: number;
|
|
10
|
+
left?: number;
|
|
11
|
+
right?: number;
|
|
12
|
+
bottom?: number;
|
|
7
13
|
}
|
|
8
14
|
|
|
9
|
-
export const FullscreenCard = ({
|
|
10
|
-
|
|
15
|
+
export const FullscreenCard = ({
|
|
16
|
+
isActive,
|
|
17
|
+
position,
|
|
18
|
+
top = 0,
|
|
19
|
+
left = 0,
|
|
20
|
+
right,
|
|
21
|
+
bottom,
|
|
22
|
+
...props
|
|
23
|
+
}: FullscreenCardProps) => {
|
|
24
|
+
return (
|
|
25
|
+
<StyledContainer
|
|
26
|
+
$isActive={isActive}
|
|
27
|
+
$position={position}
|
|
28
|
+
$top={top}
|
|
29
|
+
$left={left}
|
|
30
|
+
$right={right}
|
|
31
|
+
$bottom={bottom}
|
|
32
|
+
{...props}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
11
35
|
};
|
|
12
36
|
|
|
13
|
-
const StyledContainer = styled.div<{
|
|
14
|
-
|
|
37
|
+
const StyledContainer = styled.div<{
|
|
38
|
+
$isActive: boolean;
|
|
39
|
+
$top?: number;
|
|
40
|
+
$left?: number;
|
|
41
|
+
$right?: number;
|
|
42
|
+
$bottom?: number;
|
|
43
|
+
$position: 'absolute' | 'fixed';
|
|
44
|
+
}>(
|
|
45
|
+
({ $isActive, $top, $left, $right, $bottom, $position, theme }) => `
|
|
15
46
|
${
|
|
16
47
|
$isActive
|
|
17
48
|
? `
|
|
18
49
|
position: ${$position};
|
|
19
|
-
top: 0;
|
|
20
|
-
left: 0;
|
|
21
|
-
height: 100%;
|
|
22
|
-
width: 100%;
|
|
23
50
|
z-index: 1000;
|
|
51
|
+
${$top ? `top: ${pxToRem($top, theme.baseSize)};` : ''}
|
|
52
|
+
${$left ? `left: ${pxToRem($left, theme.baseSize)};` : ''}
|
|
53
|
+
${$right ? `right: ${pxToRem($right, theme.baseSize)};` : ''}
|
|
54
|
+
${$bottom ? `bottom: ${pxToRem($bottom, theme.baseSize)};` : ''}
|
|
24
55
|
`
|
|
25
56
|
: ''
|
|
26
57
|
}
|
package/src/Graph2D/Graph2D.tsx
CHANGED
|
@@ -2,13 +2,12 @@ import ForceGraph2D, { ForceGraphMethods, LinkObject, NodeObject } from 'react-f
|
|
|
2
2
|
import { Graph2DProps } from './types';
|
|
3
3
|
import { useEffect, useRef, useState, useCallback, useLayoutEffect } from 'react';
|
|
4
4
|
import { forceCollide } from 'd3-force';
|
|
5
|
-
import { styled } from 'styled-components';
|
|
5
|
+
import { styled, useTheme } from 'styled-components';
|
|
6
6
|
import eyeLightIcon from './eye_light.png';
|
|
7
7
|
import eyeLightHoverIcon from './eye_light_hover.png';
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
const ALPHA_MIN = 0.5;
|
|
8
|
+
import groupLightIcon from './group_light.png';
|
|
9
|
+
import groupLightHoverIcon from './group_light_hover.png';
|
|
10
|
+
import GraphLoader from './GraphLoader';
|
|
12
11
|
|
|
13
12
|
// Створюємо та налаштовуємо об'єкти зображень
|
|
14
13
|
const imgEyeLightIcon = new Image();
|
|
@@ -17,11 +16,11 @@ imgEyeLightIcon.src = eyeLightIcon;
|
|
|
17
16
|
const imgEyeLightHoverIcon = new Image();
|
|
18
17
|
imgEyeLightHoverIcon.src = eyeLightHoverIcon;
|
|
19
18
|
|
|
20
|
-
const
|
|
21
|
-
|
|
19
|
+
const imgGroupLightIcon = new Image();
|
|
20
|
+
imgGroupLightIcon.src = groupLightIcon;
|
|
22
21
|
|
|
23
|
-
const
|
|
24
|
-
|
|
22
|
+
const imgGroupLightHoverIcon = new Image();
|
|
23
|
+
imgGroupLightHoverIcon.src = groupLightHoverIcon;
|
|
25
24
|
|
|
26
25
|
export const Graph2D = ({
|
|
27
26
|
graphData,
|
|
@@ -29,6 +28,7 @@ export const Graph2D = ({
|
|
|
29
28
|
height,
|
|
30
29
|
linkTarget,
|
|
31
30
|
linkSource,
|
|
31
|
+
loading = false,
|
|
32
32
|
config = {
|
|
33
33
|
fontSize: 3, // Максимальний розмір шрифту при максимальному зумі
|
|
34
34
|
nodeSizeBase: 30, // Базовий розмір вузла
|
|
@@ -44,6 +44,8 @@ export const Graph2D = ({
|
|
|
44
44
|
onLinkClick,
|
|
45
45
|
onBackgroundClick,
|
|
46
46
|
}: Graph2DProps) => {
|
|
47
|
+
const theme = useTheme();
|
|
48
|
+
|
|
47
49
|
// Стан для підсвічування вузлів і зв'язків
|
|
48
50
|
const [highlightNodes, setHighlightNodes] = useState(new Set());
|
|
49
51
|
const [highlightLinks, setHighlightLinks] = useState(new Set());
|
|
@@ -83,7 +85,7 @@ export const Graph2D = ({
|
|
|
83
85
|
};
|
|
84
86
|
|
|
85
87
|
// Обробка подій наведення на вузол
|
|
86
|
-
const handleNodeHover =
|
|
88
|
+
const handleNodeHover = (node: NodeObject | null, _: NodeObject | null) => {
|
|
87
89
|
const newHighlightNodes = new Set();
|
|
88
90
|
const newHighlightLinks = new Set();
|
|
89
91
|
|
|
@@ -106,10 +108,10 @@ export const Graph2D = ({
|
|
|
106
108
|
setHoverNode(node || null);
|
|
107
109
|
setHighlightNodes(newHighlightNodes);
|
|
108
110
|
setHighlightLinks(newHighlightLinks);
|
|
109
|
-
}
|
|
111
|
+
};
|
|
110
112
|
|
|
111
113
|
// Обробка подій наведення на зв'язок
|
|
112
|
-
const handleLinkHover =
|
|
114
|
+
const handleLinkHover = (link: any) => {
|
|
113
115
|
const newHighlightNodes = new Set();
|
|
114
116
|
const newHighlightLinks = new Set();
|
|
115
117
|
|
|
@@ -122,7 +124,7 @@ export const Graph2D = ({
|
|
|
122
124
|
|
|
123
125
|
setHighlightNodes(newHighlightNodes);
|
|
124
126
|
setHighlightLinks(newHighlightLinks);
|
|
125
|
-
}
|
|
127
|
+
};
|
|
126
128
|
|
|
127
129
|
const handleEngineTick = useCallback(() => {
|
|
128
130
|
if (isRendering)
|
|
@@ -131,7 +133,7 @@ export const Graph2D = ({
|
|
|
131
133
|
fgRef.current &&
|
|
132
134
|
fgRef.current.tick &&
|
|
133
135
|
graphData.nodes.length > 0 &&
|
|
134
|
-
graphData.nodes.length
|
|
136
|
+
graphData.nodes.length <= fgRef.current.tick
|
|
135
137
|
) {
|
|
136
138
|
if (tickTimerRef.current) {
|
|
137
139
|
clearTimeout(tickTimerRef.current);
|
|
@@ -152,7 +154,7 @@ export const Graph2D = ({
|
|
|
152
154
|
}, [graphData]);
|
|
153
155
|
|
|
154
156
|
// Створення взаємозв'язків між вузлами
|
|
155
|
-
|
|
157
|
+
useEffect(() => {
|
|
156
158
|
if (!graphData) return;
|
|
157
159
|
|
|
158
160
|
// Прив'язка вузлів до їхніх сусідів та зв'язків
|
|
@@ -176,33 +178,14 @@ export const Graph2D = ({
|
|
|
176
178
|
target.links.push(link);
|
|
177
179
|
});
|
|
178
180
|
|
|
179
|
-
// Налаштування відстані між вузлами
|
|
180
|
-
fgRef.current?.d3Force('link')?.distance((link: any) => {
|
|
181
|
-
// Отримуємо вузли на кінцях зв'язку
|
|
182
|
-
const source = link.source;
|
|
183
|
-
const target = link.target;
|
|
184
|
-
|
|
185
|
-
// Базова відстань
|
|
186
|
-
const baseDistance = config.nodeSizeBase * 2;
|
|
187
|
-
|
|
188
|
-
// Динамічна відстань на основі розміру вузлів
|
|
189
|
-
// Більші вузли повинні бути далі один від одного
|
|
190
|
-
const sourceSizeBase = source.size || config.nodeSizeBase;
|
|
191
|
-
const targetSizeBase = target.size || config.nodeSizeBase;
|
|
192
|
-
|
|
193
|
-
// Відстань залежить від суми розмірів вузлів
|
|
194
|
-
// Додаємо базову відстань 100
|
|
195
|
-
return baseDistance + (sourceSizeBase + targetSizeBase);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
181
|
// Додаємо різні сили для уникнення перекриття вузлів
|
|
199
182
|
if (fgRef.current) {
|
|
200
183
|
// 1. Додаємо силу відштовхування між всіма вузлами (charge force)
|
|
201
184
|
const chargeForce = fgRef.current.d3Force('charge');
|
|
202
185
|
if (chargeForce) {
|
|
203
186
|
chargeForce
|
|
204
|
-
.strength(
|
|
205
|
-
.distanceMax(
|
|
187
|
+
.strength(config.nodeSizeBase) // Збільшуємо силу відштовхування (negative for repulsion)
|
|
188
|
+
.distanceMax(50); // Максимальна дистанція, на якій діє ця сила
|
|
206
189
|
}
|
|
207
190
|
|
|
208
191
|
// 2. Додаємо силу центрування для кращої організації графа
|
|
@@ -222,13 +205,21 @@ export const Graph2D = ({
|
|
|
222
205
|
.iterations(3) // Більше ітерацій для точнішого розрахунку
|
|
223
206
|
.strength(1); // Максимальна сила (1 - тверде обмеження)
|
|
224
207
|
|
|
225
|
-
fgRef.current.
|
|
208
|
+
fgRef.current.d3Force('collide', collideForce);
|
|
226
209
|
} catch (err) {
|
|
227
210
|
console.error('Error setting up collision force:', err);
|
|
228
211
|
}
|
|
229
212
|
}
|
|
230
213
|
}, [graphData]);
|
|
231
214
|
|
|
215
|
+
useEffect(() => {
|
|
216
|
+
if (!isRendering && fgRef.current) {
|
|
217
|
+
setIsRendering(true);
|
|
218
|
+
fgRef.current.tick = 0;
|
|
219
|
+
fgRef.current.d3ReheatSimulation();
|
|
220
|
+
}
|
|
221
|
+
}, [graphData]);
|
|
222
|
+
|
|
232
223
|
// Функція для малювання кільця навколо підсвічених вузлів
|
|
233
224
|
const paintRing = useCallback(
|
|
234
225
|
(node: any, ctx: CanvasRenderingContext2D, globalScale: number) => {
|
|
@@ -238,7 +229,7 @@ export const Graph2D = ({
|
|
|
238
229
|
// Малюємо кільце навколо вузла
|
|
239
230
|
ctx.beginPath();
|
|
240
231
|
ctx.arc(node.x, node.y, radius, 0, 2 * Math.PI, false);
|
|
241
|
-
ctx.fillStyle =
|
|
232
|
+
ctx.fillStyle = theme.graph2D.ring.highlightFill;
|
|
242
233
|
ctx.fill();
|
|
243
234
|
},
|
|
244
235
|
[config]
|
|
@@ -257,9 +248,9 @@ export const Graph2D = ({
|
|
|
257
248
|
ctx.beginPath();
|
|
258
249
|
ctx.arc(x, y, buttonRadius, Math.PI, Math.PI * 2, false);
|
|
259
250
|
ctx.lineWidth = 1;
|
|
260
|
-
ctx.strokeStyle =
|
|
251
|
+
ctx.strokeStyle = theme.graph2D.button.stroke;
|
|
261
252
|
ctx.stroke();
|
|
262
|
-
ctx.fillStyle = hoverTopButton ?
|
|
253
|
+
ctx.fillStyle = hoverTopButton ? theme.graph2D.button.hoverFill : theme.graph2D.button.normalFill;
|
|
263
254
|
ctx.fill();
|
|
264
255
|
|
|
265
256
|
// Лінія розділення між кнопками
|
|
@@ -267,67 +258,68 @@ export const Graph2D = ({
|
|
|
267
258
|
ctx.moveTo(x - buttonRadius, y);
|
|
268
259
|
ctx.lineTo(x + buttonRadius, y);
|
|
269
260
|
ctx.lineWidth = 1;
|
|
270
|
-
ctx.strokeStyle =
|
|
261
|
+
ctx.strokeStyle = theme.graph2D.button.stroke;
|
|
271
262
|
ctx.stroke();
|
|
272
263
|
|
|
273
264
|
// Кнопка "згорнути" (нижня частина кільця)
|
|
274
265
|
ctx.beginPath();
|
|
275
266
|
ctx.arc(x, y, buttonRadius, Math.PI * 2, Math.PI, false);
|
|
276
267
|
ctx.lineWidth = 1;
|
|
277
|
-
ctx.strokeStyle =
|
|
268
|
+
ctx.strokeStyle = theme.graph2D.button.stroke;
|
|
278
269
|
ctx.stroke();
|
|
279
|
-
ctx.fillStyle = hoverBottomButton ?
|
|
270
|
+
ctx.fillStyle = hoverBottomButton ? theme.graph2D.button.hoverFill : theme.graph2D.button.normalFill;
|
|
280
271
|
ctx.fill();
|
|
281
272
|
|
|
282
273
|
// Додаємо іконку хрестика для кнопки "сховати вузол"
|
|
283
274
|
const iconSize = buttonRadius * 0.3; // Розмір іконки відносно радіуса кнопки (зменшено вдвічі)
|
|
284
275
|
|
|
285
276
|
// Вибір іконки в залежності від стану наведення для верхньої кнопки (сховати)
|
|
286
|
-
const
|
|
287
|
-
|
|
277
|
+
const groupIcon = hoverTopButton ? imgGroupLightHoverIcon : imgGroupLightIcon;
|
|
278
|
+
// Додаємо іконку ока для кнопки "згорнути дочірні вузли"
|
|
279
|
+
const eyeIcon = hoverBottomButton ? imgEyeLightHoverIcon : imgEyeLightIcon;
|
|
280
|
+
|
|
281
|
+
const renderEyeIcon = () => {
|
|
288
282
|
try {
|
|
289
|
-
ctx.drawImage(
|
|
283
|
+
ctx.drawImage(eyeIcon, x - iconSize / 2, y - (buttonRadius * 2) / 4 - iconSize - 1, iconSize, iconSize);
|
|
290
284
|
} catch (error) {
|
|
291
|
-
console.
|
|
285
|
+
console.warn('Error rendering group icon:', error);
|
|
292
286
|
}
|
|
293
287
|
};
|
|
294
288
|
// Використовуємо безпосередньо зображення, якщо воно вже завантажене
|
|
295
|
-
if (
|
|
289
|
+
if (eyeIcon.complete) {
|
|
296
290
|
// Розміщуємо іконку в центрі верхньої половини кнопки
|
|
297
|
-
|
|
291
|
+
renderEyeIcon();
|
|
298
292
|
} else {
|
|
299
293
|
// Якщо зображення ще не завантажене, додаємо обробник завершення завантаження
|
|
300
|
-
|
|
301
|
-
|
|
294
|
+
eyeIcon.onload = () => {
|
|
295
|
+
renderEyeIcon();
|
|
302
296
|
};
|
|
303
297
|
|
|
304
|
-
|
|
305
|
-
console.
|
|
298
|
+
eyeIcon.onerror = () => {
|
|
299
|
+
console.warn('Error loading group icon image');
|
|
306
300
|
};
|
|
307
301
|
}
|
|
308
302
|
|
|
309
|
-
|
|
310
|
-
const eyeIcon = hoverBottomButton ? imgEyeLightHoverIcon : imgEyeLightIcon;
|
|
311
|
-
const renderEyeIcon = () => {
|
|
303
|
+
const renderGroupIcon = () => {
|
|
312
304
|
try {
|
|
313
|
-
ctx.drawImage(
|
|
305
|
+
ctx.drawImage(groupIcon, x - iconSize / 2, y + (buttonRadius * 2) / 4 + 1, iconSize, iconSize);
|
|
314
306
|
} catch (error) {
|
|
315
|
-
console.
|
|
307
|
+
console.warn('Error rendering eye icon:', error);
|
|
316
308
|
}
|
|
317
309
|
};
|
|
318
310
|
// Використовуємо безпосередньо зображення, якщо воно вже завантажене
|
|
319
311
|
if (eyeIcon.complete) {
|
|
320
312
|
// Розміщуємо іконку в центрі нижньої половини кнопки
|
|
321
313
|
|
|
322
|
-
|
|
314
|
+
renderGroupIcon();
|
|
323
315
|
} else {
|
|
324
316
|
// Якщо зображення ще не завантажене, додаємо обробник завершення завантаження
|
|
325
317
|
eyeIcon.onload = () => {
|
|
326
|
-
|
|
318
|
+
renderGroupIcon();
|
|
327
319
|
};
|
|
328
320
|
|
|
329
321
|
eyeIcon.onerror = () => {
|
|
330
|
-
console.
|
|
322
|
+
console.warn('Error loading eye icon image');
|
|
331
323
|
};
|
|
332
324
|
}
|
|
333
325
|
|
|
@@ -581,7 +573,7 @@ export const Graph2D = ({
|
|
|
581
573
|
const gridSpacing = config.gridSpacing;
|
|
582
574
|
const dotSize = config.dotSize;
|
|
583
575
|
|
|
584
|
-
ctx.fillStyle =
|
|
576
|
+
ctx.fillStyle = theme.graph2D.grid.dotColor;
|
|
585
577
|
|
|
586
578
|
for (let x = 0; x < width; x += gridSpacing) {
|
|
587
579
|
for (let y = 0; y < height; y += gridSpacing) {
|
|
@@ -700,7 +692,7 @@ export const Graph2D = ({
|
|
|
700
692
|
};
|
|
701
693
|
|
|
702
694
|
// Малюємо лінію зв'язку з урахуванням місця для тексту, якщо він є
|
|
703
|
-
const lineColor = highlightLinks.has(link) ?
|
|
695
|
+
const lineColor = highlightLinks.has(link) ? theme.graph2D.link.highlighted : theme.graph2D.link.normal;
|
|
704
696
|
const lineWidth = highlightLinks.has(link) ? 1.5 : 0.5;
|
|
705
697
|
|
|
706
698
|
if (label) {
|
|
@@ -773,7 +765,7 @@ export const Graph2D = ({
|
|
|
773
765
|
ctx.lineTo(-arrowHeadLength, -arrowHeadWidth);
|
|
774
766
|
ctx.closePath();
|
|
775
767
|
|
|
776
|
-
ctx.fillStyle = highlightLinks.has(link) ?
|
|
768
|
+
ctx.fillStyle = highlightLinks.has(link) ? theme.graph2D.link.highlighted : theme.graph2D.link.normal;
|
|
777
769
|
ctx.fill();
|
|
778
770
|
ctx.restore();
|
|
779
771
|
|
|
@@ -787,7 +779,7 @@ export const Graph2D = ({
|
|
|
787
779
|
// Використовуємо реверсивне масштабування для тексту
|
|
788
780
|
const scaledFontSize = calculateFontSize(globalScale);
|
|
789
781
|
ctx.font = `${scaledFontSize}px Sans-Serif`;
|
|
790
|
-
ctx.fillStyle =
|
|
782
|
+
ctx.fillStyle = theme.graph2D.link.textColor; // Колір тексту
|
|
791
783
|
ctx.textAlign = 'center';
|
|
792
784
|
ctx.textBaseline = 'middle';
|
|
793
785
|
|
|
@@ -807,7 +799,9 @@ export const Graph2D = ({
|
|
|
807
799
|
// Рисуємо фон для тексту для кращої читаємості
|
|
808
800
|
const textWidth = ctx.measureText(label).width;
|
|
809
801
|
const padding = 2;
|
|
810
|
-
ctx.fillStyle = highlightLinks.has(link)
|
|
802
|
+
ctx.fillStyle = highlightLinks.has(link)
|
|
803
|
+
? theme.graph2D.link.highlightedTextBgColor
|
|
804
|
+
: theme.graph2D.link.textBgColor;
|
|
811
805
|
ctx.fillRect(
|
|
812
806
|
-textWidth / 2 - padding,
|
|
813
807
|
-scaledFontSize / 2 - padding,
|
|
@@ -816,7 +810,7 @@ export const Graph2D = ({
|
|
|
816
810
|
);
|
|
817
811
|
|
|
818
812
|
// Малюємо текст
|
|
819
|
-
ctx.fillStyle = highlightLinks.has(link) ?
|
|
813
|
+
ctx.fillStyle = highlightLinks.has(link) ? theme.graph2D.link.highlightedTextColor : theme.graph2D.link.textColor;
|
|
820
814
|
ctx.fillText(label, 0, 0);
|
|
821
815
|
|
|
822
816
|
// Відновлення стану контексту
|
|
@@ -921,10 +915,12 @@ export const Graph2D = ({
|
|
|
921
915
|
|
|
922
916
|
const handleBackgroundClick = (event: MouseEvent) => {
|
|
923
917
|
setSelectedNode(null);
|
|
918
|
+
onBackgroundClick?.();
|
|
924
919
|
};
|
|
925
920
|
|
|
926
921
|
return (
|
|
927
922
|
<Wrapper ref={wrapperRef}>
|
|
923
|
+
{(loading || isRendering) && <GraphLoader width={width} height={height} />}
|
|
928
924
|
<ForceGraph2D
|
|
929
925
|
ref={fgRef}
|
|
930
926
|
width={width}
|
|
@@ -948,12 +944,14 @@ export const Graph2D = ({
|
|
|
948
944
|
onNodeHover={handleNodeHover}
|
|
949
945
|
onLinkHover={handleLinkHover}
|
|
950
946
|
onEngineTick={handleEngineTick}
|
|
951
|
-
d3AlphaMin={
|
|
947
|
+
d3AlphaMin={0.001}
|
|
952
948
|
d3VelocityDecay={0.4}
|
|
953
949
|
d3AlphaDecay={0.038}
|
|
954
950
|
// Виділення зв'язків при наведенні
|
|
955
951
|
linkWidth={(link: any) => (highlightLinks.has(link) ? 3 : 1)}
|
|
956
|
-
linkColor={(link: any) =>
|
|
952
|
+
linkColor={(link: any) =>
|
|
953
|
+
highlightLinks.has(link) ? theme.graph2D.link.highlighted : theme.graph2D.link.normal
|
|
954
|
+
}
|
|
957
955
|
onRenderFramePre={renderGrid}
|
|
958
956
|
nodePointerAreaPaint={renderNodePointerAreaPaint}
|
|
959
957
|
nodeCanvasObject={renderNodeCanvasObject}
|
|
@@ -984,5 +982,8 @@ export const Graph2D = ({
|
|
|
984
982
|
};
|
|
985
983
|
|
|
986
984
|
const Wrapper = styled.div`
|
|
987
|
-
display:
|
|
985
|
+
display: block;
|
|
986
|
+
width: 100%;
|
|
987
|
+
min-width: 0;
|
|
988
|
+
position: relative;
|
|
988
989
|
`;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ContentLoader from 'react-content-loader';
|
|
3
|
+
import styled from 'styled-components';
|
|
4
|
+
|
|
5
|
+
const LoaderWrapper = styled.div`
|
|
6
|
+
position: absolute;
|
|
7
|
+
top: 0;
|
|
8
|
+
left: 0;
|
|
9
|
+
width: 100%;
|
|
10
|
+
height: 100%;
|
|
11
|
+
display: flex;
|
|
12
|
+
align-items: center;
|
|
13
|
+
justify-content: center;
|
|
14
|
+
z-index: 10;
|
|
15
|
+
`;
|
|
16
|
+
|
|
17
|
+
interface GraphLoaderProps {
|
|
18
|
+
width?: number;
|
|
19
|
+
height?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const GraphLoader: React.FC<GraphLoaderProps> = ({ width = 280, height = 280 }) => {
|
|
23
|
+
// Helper function to create a rect from line coordinates
|
|
24
|
+
const lineToRect = (x1: number, y1: number, x2: number, y2: number, thickness: number = 1) => {
|
|
25
|
+
// Calculate length and angle of the line
|
|
26
|
+
const length = Math.sqrt(Math.pow(x2 - x1, 2) + Math.pow(y2 - y1, 2));
|
|
27
|
+
const angle = (Math.atan2(y2 - y1, x2 - x1) * 180) / Math.PI;
|
|
28
|
+
|
|
29
|
+
// Calculate center point of the line
|
|
30
|
+
const centerX = (x1 + x2) / 2;
|
|
31
|
+
const centerY = (y1 + y2) / 2;
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<rect
|
|
35
|
+
x={centerX - length / 2}
|
|
36
|
+
y={centerY - thickness / 2}
|
|
37
|
+
width={length}
|
|
38
|
+
height={thickness}
|
|
39
|
+
transform={`rotate(${angle}, ${centerX}, ${centerY})`}
|
|
40
|
+
/>
|
|
41
|
+
);
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<LoaderWrapper>
|
|
46
|
+
<ContentLoader width={width} height={height} viewBox="0 0 280 280">
|
|
47
|
+
<path d="m55 38-0.97266 0.22852 7.0801 30.092-18.355-20.979-0.75195 0.6582 19.596 22.395 0.43164 1.834 0.97266-0.22852 0.75195-0.6582-0.37695-0.42969 9.625-27.912-0.94531-0.32617-9.4375 27.371-0.10547-0.12109zm8 34-0.78516 0.61914 0.0957 0.12305-12.311 13.258 0.73242 0.67969 12.205-13.145 14.277 18.084 0.78516-0.61914-14.373-18.207 0.10547-0.11328zm15 19-0.48438 0.875 46.992 25.996 8e-3 4e-3 20.506 11.592-28.182 4.5449 0.15998 0.98819 29.418-4.7441 0.25 0.14062-12.555 24.143 0.88672 0.46094 13-25 15 25 26 30v18l-11 18 0.85352 0.52148 9.8008-16.039-4.6543 33.518 0.99023 0.13867 4.7793-34.408 7.2305 16.27 0.91406-0.40625-7.9141-17.807v-17.104l18 12.316 0.56445-0.82617-18.896-12.928-25.855-29.836-14.633-24.387 0.01562-0.02344h23.805v-1h-23.152l13.848-21.234 55.201-28.791-0.45898-0.88476 0.77734 0.62305 11.402-14.25 16.668-11.842-0.58008-0.81641-16.785 11.928-11.486 14.355-55.434 28.912-14.277 21.893-7.7617-27.166-0.96094 0.27344 7.7227 27.031-1.1191 0.17969-21.604-12.211zm140.43-12.912-6.957-17.338-0.92773 0.37305 6.957 17.338zm-6.957-17.338 0.72266 0.69336 16.232-16.896-0.7207-0.69336zm-3.4766 137.25 5 15 0.94922-0.31641-5-15zm-91-63-0.48047-0.87695-31 17 0.48047 0.87695zm-31 17 5 18 0.96289-0.26758-5-18zm0 0-0.70703-0.70703-12.898 12.898-17.881 9.9336 0.48633 0.875 18-10zm5 18-0.64062-0.76758-18 15 0.64062 0.76758z" />
|
|
48
|
+
<circle cx="229.92" cy="63.7318" r="5" transform="rotate(173.661 229.92 63.7318)" />
|
|
49
|
+
<circle cx="227.711" cy="43.8541" r="5" transform="rotate(173.661 227.711 43.8541)" />
|
|
50
|
+
<circle cx="211.478" cy="60.7499" r="5" transform="rotate(173.661 211.478 60.7499)" />
|
|
51
|
+
<circle cx="218.434" cy="78.0877" r="5" transform="rotate(173.661 218.434 78.0877)" />
|
|
52
|
+
<circle cx="246.705" cy="51.8054" r="5" transform="rotate(173.661 246.705 51.8054)" />
|
|
53
|
+
<circle cx="42" cy="48" r="5" />
|
|
54
|
+
<circle cx="55" cy="38" r="5" />
|
|
55
|
+
<circle cx="73" cy="43" r="5" />
|
|
56
|
+
<circle cx="63" cy="72" r="5" />
|
|
57
|
+
<circle cx="50" cy="86" r="5" />
|
|
58
|
+
<circle cx="78" cy="91" r="5" />
|
|
59
|
+
<circle cx="73" cy="165" r="5" />
|
|
60
|
+
<circle cx="73" cy="185" r="5" />
|
|
61
|
+
<circle cx="91" cy="170" r="5" />
|
|
62
|
+
<circle cx="86" cy="152" r="5" />
|
|
63
|
+
<circle cx="148" cy="130" r="5" />
|
|
64
|
+
<circle cx="189" cy="185" r="5" />
|
|
65
|
+
<circle cx="163" cy="107" r="5" />
|
|
66
|
+
<circle cx="140" cy="102" r="5" />
|
|
67
|
+
<circle cx="117" cy="135" r="5" />
|
|
68
|
+
<circle cx="125" cy="117" r="5" />
|
|
69
|
+
<circle cx="208" cy="198" r="5" />
|
|
70
|
+
<circle cx="189" cy="203" r="5" />
|
|
71
|
+
<circle cx="198" cy="221" r="5" />
|
|
72
|
+
<circle cx="178" cy="221" r="5" />
|
|
73
|
+
<circle cx="184" cy="239" r="5" />
|
|
74
|
+
<circle cx="213" cy="213" r="5" />
|
|
75
|
+
<circle cx="173" cy="130" r="5" />
|
|
76
|
+
<circle cx="163" cy="155" r="5" />
|
|
77
|
+
<circle cx="135" cy="155" r="5" />
|
|
78
|
+
<circle cx="55" cy="175" r="5" />
|
|
79
|
+
</ContentLoader>
|
|
80
|
+
</LoaderWrapper>
|
|
81
|
+
);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
export default GraphLoader;
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/src/Graph2D/types.ts
CHANGED
|
@@ -54,6 +54,9 @@ export const StyledContainer = styled.div<StyledContainerProps>(
|
|
|
54
54
|
`
|
|
55
55
|
);
|
|
56
56
|
|
|
57
|
-
const StyledMain = styled.main
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
const StyledMain = styled.main(
|
|
58
|
+
({ theme }) => `
|
|
59
|
+
min-width: 0;
|
|
60
|
+
background: ${theme.colors.backgroundBase};
|
|
61
|
+
`
|
|
62
|
+
);
|
|
@@ -1,11 +1,17 @@
|
|
|
1
1
|
import { StyleSheetManager, ThemeProvider as ThemeProviderStyled, WebTarget } from 'styled-components';
|
|
2
|
-
import { lightTheme } from './
|
|
2
|
+
import { lightTheme, darkTheme } from './themes';
|
|
3
3
|
import { GlobalStyle } from './GlobalStyle';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
interface ThemeProviderProps {
|
|
6
|
+
children: any;
|
|
7
|
+
mode?: 'light' | 'LIGHT' | 'dark' | 'DARK';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type ThemeMode = 'light' | 'LIGHT' | 'dark' | 'DARK';
|
|
11
|
+
export const ThemeProvider = ({ children, mode }: ThemeProviderProps) => {
|
|
6
12
|
return (
|
|
7
13
|
<StyleSheetManager shouldForwardProp={shouldForwardProp}>
|
|
8
|
-
<ThemeProviderStyled theme={lightTheme}>
|
|
14
|
+
<ThemeProviderStyled theme={mode == 'light' || mode === 'LIGHT' ? lightTheme : darkTheme}>
|
|
9
15
|
<GlobalStyle />
|
|
10
16
|
{children}
|
|
11
17
|
</ThemeProviderStyled>
|
package/src/Theme/index.ts
CHANGED