@cyber-harbour/ui 1.0.69 → 1.0.71

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.69",
3
+ "version": "1.0.71",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/index.mjs",
6
6
  "types": "dist/index.d.ts",
@@ -27,6 +27,7 @@ import {
27
27
  } from 'd3';
28
28
  import { styled, useTheme } from 'styled-components';
29
29
  import GraphLoader from './GraphLoader';
30
+ import { hexToRgba } from '../Theme';
30
31
 
31
32
  const RATIO = window.devicePixelRatio || 1;
32
33
  // Завантаження та підготовка зображень кнопок
@@ -94,6 +95,11 @@ export const Graph2D: any = forwardRef<Graph2DRef, Graph2DProps>(
94
95
  isDragging: false,
95
96
  width: width * RATIO,
96
97
  height: height * RATIO,
98
+ animation: {
99
+ id: null,
100
+ buttonIndex: null,
101
+ },
102
+ spinnerAngle: 0,
97
103
  });
98
104
 
99
105
  const { nodes, links } = useMemo(() => cloneDeep(graphData), [graphData]);
@@ -365,6 +371,44 @@ export const Graph2D: any = forwardRef<Graph2DRef, Graph2DProps>(
365
371
  [config, theme.graph2D.link]
366
372
  );
367
373
 
374
+ const drawIconWithOptionalCounter = (
375
+ ctx: CanvasRenderingContext2D,
376
+ icon: HTMLImageElement,
377
+ iconX: number,
378
+ iconY: number,
379
+ iconSize: number,
380
+ counter: number | null | undefined,
381
+ textColor?: string
382
+ ) => {
383
+ try {
384
+ if (counter !== null && counter !== undefined) {
385
+ const fontSize = iconSize * 0.8;
386
+ ctx.font = `${fontSize}px sans-serif`;
387
+ ctx.textBaseline = 'middle';
388
+ ctx.textAlign = 'left';
389
+
390
+ const counterText = new Intl.NumberFormat('en', {
391
+ notation: 'compact',
392
+ maximumFractionDigits: 1,
393
+ minimumFractionDigits: 0,
394
+ compactDisplay: 'short',
395
+ }).format(counter);
396
+
397
+ const textWidth = ctx.measureText(counterText).width;
398
+ const spacing = iconSize * 0.3;
399
+ const totalWidth = iconSize + spacing + textWidth;
400
+ const offsetX = totalWidth / 2;
401
+
402
+ ctx.drawImage(icon, iconX - offsetX, iconY - iconSize / 2, iconSize, iconSize);
403
+ ctx.fillStyle = textColor || '#99989C';
404
+ ctx.fillText(counterText, iconX - offsetX + iconSize + spacing, iconY);
405
+ } else {
406
+ ctx.drawImage(icon, iconX - iconSize / 2, iconY - iconSize / 2, iconSize, iconSize);
407
+ }
408
+ } catch (error) {
409
+ console.warn('Error rendering icon:', error);
410
+ }
411
+ };
368
412
  // Функція для рендерингу кнопок навколо вузла
369
413
  const renderNodeButtons = useCallback(
370
414
  (node: NodeObject, ctx: CanvasRenderingContext2D) => {
@@ -410,26 +454,70 @@ export const Graph2D: any = forwardRef<Graph2DRef, Graph2DProps>(
410
454
 
411
455
  // Вибираємо відповідну іконку залежно від стану наведення
412
456
  const buttonImage = buttonImages[i];
413
- const icon = isHovered ? buttonImage.hoverImg : buttonImage.normalImg;
414
-
415
- // Малюємо іконку
416
- if (icon.complete) {
417
- try {
418
- ctx.drawImage(icon, iconX - iconSize / 2, iconY - iconSize / 2, iconSize, iconSize);
419
- } catch (error) {
420
- console.warn('Error rendering button icon:', error);
457
+ // Якщо кнопка в лоадінгу малюємо спінер
458
+ if (buttonImage.loading) {
459
+ const spinnerRadius = iconSize / 2;
460
+ const dotRadius = iconSize / 12;
461
+
462
+ ctx.save();
463
+ ctx.translate(iconX, iconY);
464
+ ctx.rotate(stateRef.current.spinnerAngle);
465
+
466
+ // Малюємо кілька променів (ліній) по колу
467
+ for (let j = 0; j < 12; j++) {
468
+ const angle = (Math.PI * 2 * j) / 12;
469
+ const alpha = j / 12;
470
+
471
+ const dotX = Math.cos(angle) * spinnerRadius;
472
+ const dotY = Math.sin(angle) * spinnerRadius;
473
+
474
+ ctx.beginPath();
475
+ ctx.fillStyle = hexToRgba(theme.graph2D?.button?.spinnerColor, alpha);
476
+ ctx.arc(dotX, dotY, dotRadius, 0, Math.PI * 2);
477
+ ctx.fill();
478
+ }
479
+ ctx.restore();
480
+
481
+ stateRef.current.spinnerAngle += 0.1;
482
+ if (stateRef.current.spinnerAngle > Math.PI * 2) {
483
+ stateRef.current.spinnerAngle -= Math.PI * 2;
484
+ }
485
+ if (stateRef.current.animation.id !== null) {
486
+ cancelAnimationFrame(stateRef.current.animation.id);
487
+ stateRef.current.animation.id = null;
488
+ stateRef.current.animation.buttonIndex = null;
421
489
  }
490
+
491
+ stateRef.current.animation.id = requestAnimationFrame(renderCanvas2D);
492
+ stateRef.current.animation.buttonIndex = i;
422
493
  } else {
423
- // Встановлюємо обробник onload, якщо зображення ще не завантажено
424
- icon.onload = () => {
425
- if (ctx2dRef.current) {
426
- try {
427
- ctx.drawImage(icon, iconX - iconSize / 2, iconY - iconSize / 2, iconSize, iconSize);
428
- } catch (error) {
429
- console.warn('Error rendering button icon after load:', error);
494
+ if (stateRef.current.animation.id && stateRef.current.animation.buttonIndex === i) {
495
+ cancelAnimationFrame(stateRef.current.animation.id);
496
+ stateRef.current.animation.id = null;
497
+ stateRef.current.animation.buttonIndex = null;
498
+ }
499
+ const icon = isHovered ? buttonImage.hoverImg : buttonImage.normalImg;
500
+ //каунтер може не існувати
501
+ const counter = buttonImage.getCount ? buttonImage.getCount(node) : null;
502
+ // Малюємо іконку
503
+ if (icon.complete) {
504
+ drawIconWithOptionalCounter(ctx, icon, iconX, iconY, iconSize, counter, theme.graph2D?.button?.textColor);
505
+ } else {
506
+ // Встановлюємо обробник onload, якщо зображення ще не завантажено
507
+ icon.onload = () => {
508
+ if (ctx2dRef.current) {
509
+ drawIconWithOptionalCounter(
510
+ ctx,
511
+ icon,
512
+ iconX,
513
+ iconY,
514
+ iconSize,
515
+ counter,
516
+ theme.graph2D?.button?.textColor
517
+ );
430
518
  }
431
- }
432
- };
519
+ };
520
+ }
433
521
  }
434
522
  }
435
523
 
@@ -63,6 +63,13 @@ export interface GraphState {
63
63
  isDragging: boolean;
64
64
  width: number;
65
65
  height: number;
66
+ /** Поточна анімація */
67
+ animation: {
68
+ id: number | null;
69
+ buttonIndex: number | null;
70
+ };
71
+ /** Кут оберту спінера */
72
+ spinnerAngle: number;
66
73
  }
67
74
 
68
75
  export interface Graph2DProps {
@@ -87,6 +94,8 @@ export interface Graph2DProps {
87
94
  export interface NodeButton {
88
95
  img: string;
89
96
  hoverImg: string;
97
+ loading?: boolean;
98
+ getCount?: (node: NodeObject) => number;
90
99
  onClick: (node: NodeObject) => void;
91
100
  }
92
101
 
@@ -857,6 +857,8 @@ export const darkThemePx: Theme = {
857
857
  stroke: '#1A1A1A',
858
858
  normalFill: 'rgba(0, 0, 0, 0.8)',
859
859
  hoverFill: 'rgba(25, 25, 25, 0.9)',
860
+ textColor: '#99989C',
861
+ spinnerColor: '#80A0F5',
860
862
  },
861
863
  grid: {
862
864
  dotColor: 'rgba(255, 255, 255, 0.5)',
@@ -856,6 +856,8 @@ export const lightThemePx: Theme = {
856
856
  stroke: '#e5e5e5',
857
857
  normalFill: 'rgba(255, 255, 255, 0.8)',
858
858
  hoverFill: 'rgba(230, 230, 230, 0.9)',
859
+ textColor: '#99989C',
860
+ spinnerColor: '#80A0F5',
859
861
  },
860
862
  grid: {
861
863
  dotColor: 'rgba(0, 0, 0, 0.5)',
@@ -253,6 +253,8 @@ export type Theme = {
253
253
  stroke: string;
254
254
  normalFill: string;
255
255
  hoverFill: string;
256
+ textColor: string;
257
+ spinnerColor: string;
256
258
  };
257
259
  grid: {
258
260
  dotColor: string;