@sybilion/uilib 1.2.24 → 1.3.0

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/dist/esm/components/ui/Chart/Chart.js +1 -0
  2. package/dist/esm/components/ui/Chart/components/BaseChartWrapper.js +7 -32
  3. package/dist/esm/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.js +21 -0
  4. package/dist/esm/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.styl.js +7 -0
  5. package/dist/esm/components/ui/Chart/tools/chartPlotGeometry.js +65 -0
  6. package/dist/esm/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.js +37 -1
  7. package/dist/esm/components/ui/ChartAreaInteractive/ChartAreaInteractive.js +5 -2
  8. package/dist/esm/components/ui/ChartAreaInteractive/TimeRangeBrushLayer.js +205 -0
  9. package/dist/esm/components/ui/ChartAreaInteractive/TimeRangeBrushLayer.styl.js +7 -0
  10. package/dist/esm/components/ui/ChartAreaInteractive/TimeRangeBrushLayout.helpers.js +37 -0
  11. package/dist/esm/components/ui/ChartAreaInteractive/overlays/IntervalsOverlay/IntervalsOverlay.hooks.js +1 -0
  12. package/dist/esm/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.js +7 -60
  13. package/dist/esm/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.styl.js +2 -2
  14. package/dist/esm/components/ui/ChartAreaInteractive/overlays/ThresholdsOverlay/ThresholdsOverlay.hooks.js +1 -0
  15. package/dist/esm/components/ui/ChartAreaInteractive/overlays/useChartYRange.js +2 -4
  16. package/dist/esm/components/ui/Chat/ChatSheet/ChatSelector.js +1 -1
  17. package/dist/esm/components/ui/Chat/ChatSheet/ChatSelector.styl.js +1 -1
  18. package/dist/esm/components/ui/DropdownMenu/DropdownMenu.js +4 -4
  19. package/dist/esm/components/ui/TimeRangeControls/TimeRangeControls.js +7 -2
  20. package/dist/esm/components/ui/WorldMap/WorldMap.js +11 -0
  21. package/dist/esm/components/ui/WorldMap/WorldMap.styl.js +7 -0
  22. package/dist/esm/components/ui/WorldMap/map.svg.js +3 -0
  23. package/dist/esm/components/widgets/DriverCard/DriverCard.js +89 -0
  24. package/dist/esm/components/widgets/DriverCard/DriverCard.styl.js +7 -0
  25. package/dist/esm/components/widgets/DriverCard/DriverPerformanceChart.js +79 -0
  26. package/dist/esm/components/widgets/DriverCard/DriverPerformanceChart.styl.js +7 -0
  27. package/dist/esm/components/widgets/DriverCard/driverPerformanceChartData.js +50 -0
  28. package/dist/esm/components/widgets/DriverMap/DriverIcon/DriverIcon.constants.json.js +6 -0
  29. package/dist/esm/components/widgets/DriverMap/DriverIcon/DriverIcon.js +21 -0
  30. package/dist/esm/components/widgets/DriverMap/DriverIcon/DriverIcon.styl.js +7 -0
  31. package/dist/esm/components/widgets/DriverMap/DriverMap.helpers.js +107 -0
  32. package/dist/esm/components/widgets/DriverMap/DriverMap.js +129 -0
  33. package/dist/esm/components/widgets/DriverMap/DriverMap.styl.js +7 -0
  34. package/dist/esm/components/widgets/DriverMap/driverCategoryIcon.js +194 -0
  35. package/dist/esm/components/widgets/DriverMap/driverMapGeography.js +345 -0
  36. package/dist/esm/components/widgets/DriverMap/driverMapSelection.js +17 -0
  37. package/dist/esm/hooks/index.js +1 -0
  38. package/dist/esm/hooks/useEvent.js +0 -2
  39. package/dist/esm/index.js +7 -0
  40. package/dist/esm/types/src/components/ui/Chart/Chart.d.ts +1 -0
  41. package/dist/esm/types/src/components/ui/Chart/components/BaseChartWrapper.d.ts +2 -1
  42. package/dist/esm/types/src/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.d.ts +14 -0
  43. package/dist/esm/types/src/components/ui/Chart/tools/chartPlotGeometry.d.ts +30 -0
  44. package/dist/esm/types/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.d.ts +1 -1
  45. package/dist/esm/types/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.d.ts +11 -2
  46. package/dist/esm/types/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.types.d.ts +2 -2
  47. package/dist/esm/types/src/components/ui/ChartAreaInteractive/TimeRangeBrushLayer.d.ts +15 -0
  48. package/dist/esm/types/src/components/ui/ChartAreaInteractive/TimeRangeBrushLayout.helpers.d.ts +14 -0
  49. package/dist/esm/types/src/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.d.ts +1 -1
  50. package/dist/esm/types/src/components/ui/DropdownMenu/DropdownMenu.d.ts +2 -2
  51. package/dist/esm/types/src/components/ui/Page/PageColumns/PageColumns.d.ts +1 -1
  52. package/dist/esm/types/src/components/ui/TimeRangeControls/TimeRangeControls.d.ts +5 -7
  53. package/dist/esm/types/src/components/ui/WorldMap/WorldMap.d.ts +4 -0
  54. package/dist/esm/types/src/components/ui/WorldMap/index.d.ts +2 -0
  55. package/dist/esm/types/src/components/widgets/DriverCard/DriverCard.d.ts +9 -0
  56. package/dist/esm/types/src/components/widgets/DriverCard/DriverPerformanceChart.d.ts +5 -0
  57. package/dist/esm/types/src/components/widgets/DriverCard/driverPerformanceChartData.d.ts +7 -0
  58. package/dist/esm/types/src/components/widgets/DriverCard/index.d.ts +1 -0
  59. package/dist/esm/types/src/components/widgets/DriverMap/DriverIcon/DriverIcon.d.ts +17 -0
  60. package/dist/esm/types/src/components/widgets/DriverMap/DriverMap.d.ts +8 -0
  61. package/dist/esm/types/src/components/widgets/DriverMap/DriverMap.helpers.d.ts +21 -0
  62. package/dist/esm/types/src/components/widgets/DriverMap/driverCategoryIcon.d.ts +1 -0
  63. package/dist/esm/types/src/components/widgets/DriverMap/driverMapGeography.d.ts +80 -0
  64. package/dist/esm/types/src/components/widgets/DriverMap/driverMapSelection.d.ts +3 -0
  65. package/dist/esm/types/src/components/widgets/DriverMap/index.d.ts +6 -0
  66. package/dist/esm/types/src/docs/pages/DriverMapPage.d.ts +1 -0
  67. package/dist/esm/types/src/docs/pages/PageColumnsPage.d.ts +1 -0
  68. package/dist/esm/types/src/docs/pages/WorldMapPage.d.ts +1 -0
  69. package/dist/esm/types/src/docs/registry.d.ts +1 -1
  70. package/dist/esm/types/src/hooks/index.d.ts +1 -0
  71. package/dist/esm/types/src/index.d.ts +3 -0
  72. package/package.json +1 -1
  73. package/src/components/ui/Chart/Chart.tsx +5 -0
  74. package/src/components/ui/Chart/components/BaseChartWrapper.tsx +8 -41
  75. package/src/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.styl +60 -0
  76. package/src/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.styl.d.ts +15 -0
  77. package/src/components/ui/Chart/components/ChartEmptyState/ChartEmptyState.tsx +66 -0
  78. package/src/components/ui/Chart/tools/chartPlotGeometry.ts +89 -0
  79. package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.ts +44 -2
  80. package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.tsx +14 -1
  81. package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.types.ts +2 -3
  82. package/src/components/ui/ChartAreaInteractive/TimeRangeBrushLayer.styl +21 -0
  83. package/src/components/ui/ChartAreaInteractive/TimeRangeBrushLayer.styl.d.ts +9 -0
  84. package/src/components/ui/ChartAreaInteractive/TimeRangeBrushLayer.tsx +285 -0
  85. package/src/components/ui/ChartAreaInteractive/TimeRangeBrushLayout.helpers.ts +55 -0
  86. package/src/components/ui/ChartAreaInteractive/overlays/IntervalsOverlay/IntervalsOverlay.hooks.ts +1 -0
  87. package/src/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.styl +2 -7
  88. package/src/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.styl.d.ts +0 -1
  89. package/src/components/ui/ChartAreaInteractive/overlays/PinOverlay/PinOverlay.tsx +7 -71
  90. package/src/components/ui/ChartAreaInteractive/overlays/ThresholdsOverlay/ThresholdsOverlay.hooks.ts +1 -0
  91. package/src/components/ui/ChartAreaInteractive/overlays/useChartYRange.ts +2 -3
  92. package/src/components/ui/Chat/ChatSheet/ChatSelector.styl +3 -1
  93. package/src/components/ui/Chat/ChatSheet/ChatSelector.tsx +1 -1
  94. package/src/components/ui/DropdownMenu/DropdownMenu.tsx +4 -0
  95. package/src/components/ui/Page/PageColumns/PageColumns.tsx +1 -1
  96. package/src/components/ui/TimeRangeControls/TimeRangeControls.tsx +16 -17
  97. package/src/components/ui/WorldMap/WorldMap.styl +11 -0
  98. package/src/components/ui/WorldMap/WorldMap.styl.d.ts +7 -0
  99. package/src/components/ui/WorldMap/WorldMap.tsx +22 -0
  100. package/src/components/ui/WorldMap/index.ts +2 -0
  101. package/src/components/ui/WorldMap/map.svg +4337 -0
  102. package/src/components/ui/WorldMap/mapAspect.mixin.styl +3 -0
  103. package/src/components/ui/WorldMap/mapAspect.mixin.styl.d.ts +2 -0
  104. package/src/components/widgets/DriverCard/DriverCard.styl +169 -0
  105. package/src/components/widgets/DriverCard/DriverCard.styl.d.ts +40 -0
  106. package/src/components/widgets/DriverCard/DriverCard.tsx +219 -0
  107. package/src/components/widgets/DriverCard/DriverPerformanceChart.styl +43 -0
  108. package/src/components/widgets/DriverCard/DriverPerformanceChart.styl.d.ts +13 -0
  109. package/src/components/widgets/DriverCard/DriverPerformanceChart.tsx +150 -0
  110. package/src/components/widgets/DriverCard/driverPerformanceChartData.ts +64 -0
  111. package/src/components/widgets/DriverCard/index.ts +1 -0
  112. package/src/components/widgets/DriverMap/DriverIcon/DriverIcon.constants.json +3 -0
  113. package/src/components/widgets/DriverMap/DriverIcon/DriverIcon.styl +125 -0
  114. package/src/components/widgets/DriverMap/DriverIcon/DriverIcon.styl.d.ts +22 -0
  115. package/src/components/widgets/DriverMap/DriverIcon/DriverIcon.tsx +79 -0
  116. package/src/components/widgets/DriverMap/DriverMap.helpers.ts +164 -0
  117. package/src/components/widgets/DriverMap/DriverMap.styl +50 -0
  118. package/src/components/widgets/DriverMap/DriverMap.styl.d.ts +12 -0
  119. package/src/components/widgets/DriverMap/DriverMap.tsx +212 -0
  120. package/src/components/widgets/DriverMap/driverCategoryIcon.tsx +277 -0
  121. package/src/components/widgets/DriverMap/driverMapGeography.ts +478 -0
  122. package/src/components/widgets/DriverMap/driverMapSelection.ts +23 -0
  123. package/src/components/widgets/DriverMap/index.ts +16 -0
  124. package/src/docs/config/webpack.config.js +25 -9
  125. package/src/docs/pages/ChartAreaInteractivePage.tsx +2 -3
  126. package/src/docs/pages/DriverMapPage.tsx +268 -0
  127. package/src/docs/pages/PageColumnsPage.tsx +92 -0
  128. package/src/docs/pages/TimeRangeControlsPage.tsx +2 -3
  129. package/src/docs/pages/TooltipPage.tsx +14 -10
  130. package/src/docs/pages/WorldMapPage.styl +14 -0
  131. package/src/docs/pages/WorldMapPage.styl.d.ts +8 -0
  132. package/src/docs/pages/WorldMapPage.tsx +26 -0
  133. package/src/docs/registry.ts +18 -5
  134. package/src/hooks/index.ts +1 -0
  135. package/src/hooks/useEvent.ts +0 -2
  136. package/src/index.ts +3 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ "FADE_DURATION": 300
3
+ }
@@ -0,0 +1,125 @@
1
+ @import '../../../../lib/theme.styl';
2
+ json('./DriverIcon.constants.json')
3
+
4
+ $dur = unit(FADE_DURATION, 'ms')
5
+ $msOffset = 30ms
6
+
7
+ .root
8
+ position absolute
9
+ transform translateX(-50%) translateY(-50%)
10
+ cursor pointer
11
+ outline none
12
+
13
+ &:hover
14
+ transition transform 300ms ease-out
15
+
16
+ &:focus
17
+ outline none
18
+
19
+ .loadingHidden
20
+ opacity 0
21
+ pointer-events none
22
+
23
+ .badge
24
+ padding 0.25rem
25
+ transition all $dur ease-out
26
+ pointer-events none
27
+
28
+ opacity 0
29
+ transform translateY(10px)
30
+
31
+ svg
32
+ height 20px
33
+ width 20px
34
+
35
+ .size-s
36
+ .size-s .badge
37
+ width 2rem
38
+ height 2rem
39
+ border-radius 10px
40
+ .size-m
41
+ .size-m .badge
42
+ width 2.5rem
43
+ height 2.5rem
44
+ border-radius 12px
45
+ .size-l
46
+ .size-l .badge
47
+ width 3rem
48
+ height 3rem
49
+ border-radius 14px
50
+
51
+
52
+ // show/hide animations
53
+ .visible .badge
54
+ transition $dur ease-out
55
+ transition-property opacity, transform
56
+ opacity 1
57
+ transform translateY(0)
58
+
59
+ span
60
+ transition box-shadow 0s .1s ease-out
61
+
62
+ for $i in (1..6)
63
+ .root:nth-child({$i}) .badge
64
+ transition-delay ($i * $msOffset)
65
+ .visible:nth-child({$i}) .badge
66
+ transition-delay ($i * ($msOffset * 2))
67
+ .hidden:nth-child({$i}) .badge
68
+ transition-duration 'calc(%s - %s)' % ($dur ($i * $msOffset))
69
+
70
+
71
+ // Hover
72
+ .root:hover
73
+ z-index 10
74
+
75
+ .badge
76
+ box-shadow 0 0 0 1px var(--background)
77
+ transform scale(1.1)
78
+ transition-delay 0ms
79
+ transition-duration .2s
80
+
81
+ .selected .badge
82
+ .selected:hover .badge
83
+ transform scale(1.1)
84
+
85
+ // span
86
+ box-shadow inset 0 0 0 3px var(--sb-cyan-400)
87
+
88
+ :global(.dark) &
89
+ box-shadow inset 0 0 0 2px var(--sb-cyan-500)
90
+
91
+ .root:not(.selected):hover .icon
92
+ transform scale(1.05)
93
+
94
+
95
+ .tooltipContent
96
+ max-width 20rem
97
+ border 1px solid var(--border)
98
+ background-color var(--popover)
99
+ color var(--popover-foreground)
100
+ font-size 0.75rem
101
+ font-family var(--font-family-body)
102
+ box-shadow 0 10px 15px -3px rgba(0, 0, 0, 0.1)
103
+
104
+ .tooltipCategory
105
+ color var(--muted-foreground)
106
+ display flex
107
+ align-items center
108
+ gap 0.25rem
109
+
110
+ .tooltipCategoryIcon
111
+ height 0.75rem
112
+ width 0.75rem
113
+ color var(--muted-foreground)
114
+
115
+ .tooltipCategoryText
116
+ line-clamp(1)
117
+ overflow hidden
118
+
119
+ .tooltipName
120
+ font-weight normal
121
+
122
+ .tooltipSpacing
123
+ display flex
124
+ flex-direction column
125
+ gap 0.25rem
@@ -0,0 +1,22 @@
1
+ // This file is automatically generated.
2
+ // Please do not change this file!
3
+ interface CssExports {
4
+ 'badge': string;
5
+ 'hidden': string;
6
+ 'icon': string;
7
+ 'loadingHidden': string;
8
+ 'root': string;
9
+ 'selected': string;
10
+ 'size-l': string;
11
+ 'size-m': string;
12
+ 'size-s': string;
13
+ 'tooltipCategory': string;
14
+ 'tooltipCategoryIcon': string;
15
+ 'tooltipCategoryText': string;
16
+ 'tooltipContent': string;
17
+ 'tooltipName': string;
18
+ 'tooltipSpacing': string;
19
+ 'visible': string;
20
+ }
21
+ export const cssExports: CssExports;
22
+ export default cssExports;
@@ -0,0 +1,79 @@
1
+ import cn from 'classnames';
2
+
3
+ import { Badge } from '#uilib/components/ui/Badge';
4
+ import {
5
+ Tooltip,
6
+ TooltipContent,
7
+ TooltipTrigger,
8
+ } from '#uilib/components/ui/Tooltip';
9
+
10
+ import { getCategoryIcon } from '../driverCategoryIcon';
11
+ import type { DriverData } from '../driverMapGeography';
12
+ import S from './DriverIcon.styl';
13
+
14
+ export const BADGE_SIZES = {
15
+ s: 's',
16
+ m: 'm',
17
+ l: 'l',
18
+ } as const;
19
+
20
+ export type BadgeSize = (typeof BADGE_SIZES)[keyof typeof BADGE_SIZES];
21
+
22
+ interface DriverIconProps {
23
+ driver: DriverData;
24
+ size: BadgeSize;
25
+ isSelected: boolean;
26
+ onClick: () => void;
27
+ isVisible: boolean;
28
+ isLoading?: boolean;
29
+ }
30
+
31
+ export function DriverIcon({
32
+ driver,
33
+ size,
34
+ isSelected,
35
+ onClick,
36
+ isVisible,
37
+ isLoading = false,
38
+ }: DriverIconProps) {
39
+ return (
40
+ <Tooltip>
41
+ <TooltipTrigger
42
+ asChild
43
+ className={cn(
44
+ S.root,
45
+ S[`size-${size}`],
46
+ isSelected && S.selected,
47
+ isVisible ? S.visible : S.hidden,
48
+ isLoading && S.loadingHidden,
49
+ )}
50
+ >
51
+ <button
52
+ type="button"
53
+ onClick={onClick}
54
+ style={{
55
+ left: `${driver.coordinates.x}%`,
56
+ top: `${driver.coordinates.y}%`,
57
+ zIndex: isSelected ? 2 : '',
58
+ }}
59
+ >
60
+ <Badge className={S.badge}>
61
+ <div className={S.icon}>{getCategoryIcon(driver.category)}</div>
62
+ </Badge>
63
+ </button>
64
+ </TooltipTrigger>
65
+
66
+ <TooltipContent side="top" className={S.tooltipContent}>
67
+ <div className={S.tooltipSpacing}>
68
+ <div className={S.tooltipCategory}>
69
+ <span className={S.tooltipCategoryIcon}>
70
+ {getCategoryIcon(driver.category)}
71
+ </span>
72
+ <span className={S.tooltipCategoryText}>{driver.category}</span>
73
+ </div>
74
+ <div className={S.tooltipName}>{driver.name}</div>
75
+ </div>
76
+ </TooltipContent>
77
+ </Tooltip>
78
+ );
79
+ }
@@ -0,0 +1,164 @@
1
+ import { BADGE_SIZES, type BadgeSize } from './DriverIcon/DriverIcon';
2
+ import {
3
+ type DriverData,
4
+ geographicToSVG,
5
+ getResponsiveCoordinates,
6
+ svgToPercentage,
7
+ } from './driverMapGeography';
8
+ import { getDriverImportance } from './driverMapSelection';
9
+
10
+ // Calculate badge sizes for all drivers based on their importance
11
+ export const calculateBadgeSizes = (
12
+ drivers: DriverData[],
13
+ ): Record<string, BadgeSize> => {
14
+ if (drivers.length === 0) return {};
15
+
16
+ const importanceValues = drivers
17
+ .map(getDriverImportance)
18
+ .filter(
19
+ (val): val is number =>
20
+ typeof val === 'number' && !isNaN(val) && val >= 0,
21
+ );
22
+
23
+ if (importanceValues.length === 0) {
24
+ return drivers.reduce(
25
+ (acc, driver) => {
26
+ acc[driver.id] = BADGE_SIZES.s;
27
+ return acc;
28
+ },
29
+ {} as Record<string, BadgeSize>,
30
+ );
31
+ }
32
+
33
+ const min = Math.min(...importanceValues);
34
+ const max = Math.max(...importanceValues);
35
+ const range = max - min;
36
+
37
+ if (range === 0) {
38
+ return drivers.reduce(
39
+ (acc, driver) => {
40
+ acc[driver.id] = BADGE_SIZES.s;
41
+ return acc;
42
+ },
43
+ {} as Record<string, BadgeSize>,
44
+ );
45
+ }
46
+
47
+ return drivers.reduce(
48
+ (acc, driver) => {
49
+ const importance = getDriverImportance(driver);
50
+
51
+ if (
52
+ typeof importance !== 'number' ||
53
+ isNaN(importance) ||
54
+ importance < 0
55
+ ) {
56
+ acc[driver.id] = BADGE_SIZES.s;
57
+ return acc;
58
+ }
59
+
60
+ const normalized = (importance - min) / range;
61
+ acc[driver.id] =
62
+ normalized >= 0.66
63
+ ? BADGE_SIZES.l
64
+ : normalized >= 0.33
65
+ ? BADGE_SIZES.m
66
+ : BADGE_SIZES.s;
67
+ return acc;
68
+ },
69
+ {} as Record<string, BadgeSize>,
70
+ );
71
+ };
72
+
73
+ export const hasValidCoords = (
74
+ coords: { x: number; y: number } | null | undefined,
75
+ ): boolean => {
76
+ return (
77
+ coords?.x != null &&
78
+ coords?.y != null &&
79
+ typeof coords.x === 'number' &&
80
+ typeof coords.y === 'number' &&
81
+ !isNaN(coords.x) &&
82
+ !isNaN(coords.y) &&
83
+ Math.abs(coords.x) > 0 &&
84
+ Math.abs(coords.y) > 0
85
+ );
86
+ };
87
+
88
+ export const findMostSpecificRegion = (
89
+ driver: DriverData,
90
+ ): {
91
+ name: string | null;
92
+ coordinates: { x: number; y: number } | null;
93
+ } => {
94
+ if (driver.region?.length > 0) {
95
+ return {
96
+ name: driver.region[driver.region.length - 1],
97
+ coordinates: null,
98
+ };
99
+ }
100
+
101
+ const srcName = driver.src_region?.name;
102
+ const tgtName = driver.tgt_region?.name;
103
+ const srcCoords = driver.src_region?.coordinates;
104
+ const tgtCoords = driver.tgt_region?.coordinates;
105
+ const srcIsNonWorld =
106
+ srcName && srcName !== 'World' && hasValidCoords(srcCoords);
107
+ const tgtIsNonWorld =
108
+ tgtName && tgtName !== 'World' && hasValidCoords(tgtCoords);
109
+
110
+ if (tgtIsNonWorld) {
111
+ return {
112
+ name: tgtName,
113
+ coordinates: tgtCoords!,
114
+ };
115
+ }
116
+ if (srcIsNonWorld) {
117
+ return {
118
+ name: srcName,
119
+ coordinates: srcCoords!,
120
+ };
121
+ }
122
+
123
+ return { name: null, coordinates: null };
124
+ };
125
+
126
+ export const getDriverCoordinates = (
127
+ driver: DriverData,
128
+ specificRegion: {
129
+ name: string | null;
130
+ coordinates: { x: number; y: number } | null;
131
+ },
132
+ existingDrivers: DriverData[] = [],
133
+ ): DriverData['coordinates'] => {
134
+ if (!specificRegion.name || specificRegion.name === 'World') {
135
+ return driver.coordinates;
136
+ }
137
+
138
+ if (specificRegion.coordinates) {
139
+ const isLatLng =
140
+ Math.abs(specificRegion.coordinates.x) <= 180 &&
141
+ Math.abs(specificRegion.coordinates.y) <= 90;
142
+
143
+ if (isLatLng) {
144
+ const svgCoords = geographicToSVG(
145
+ specificRegion.coordinates.y,
146
+ specificRegion.coordinates.x,
147
+ );
148
+ const percentageCoords = svgToPercentage(svgCoords.x, svgCoords.y);
149
+ return {
150
+ x: Math.min(95, Math.max(5, percentageCoords.x)),
151
+ y: Math.min(95, Math.max(5, percentageCoords.y)),
152
+ continent: driver.coordinates?.continent || 'Global',
153
+ };
154
+ }
155
+
156
+ return {
157
+ x: specificRegion.coordinates.x,
158
+ y: specificRegion.coordinates.y,
159
+ continent: driver.coordinates?.continent || 'Global',
160
+ };
161
+ }
162
+
163
+ return getResponsiveCoordinates(specificRegion.name, existingDrivers);
164
+ };
@@ -0,0 +1,50 @@
1
+ @import '../../../lib/theme.styl'
2
+ @import '../../ui/WorldMap/mapAspect.mixin.styl'
3
+
4
+
5
+ .root
6
+ position relative
7
+ width 100%
8
+ mapAspect()
9
+ padding 0
10
+ overflow hidden
11
+
12
+ @media (min-width: 768px)
13
+ height 35rem
14
+ width fit-content
15
+ flex 1
16
+
17
+ .mapInner
18
+ position relative
19
+ max-height 100%
20
+ mapAspect()
21
+ margin 0 auto
22
+ border-radius var(--p-4)
23
+ overflow hidden
24
+
25
+ .mapWorld
26
+ position absolute
27
+ inset 0
28
+ box-sizing border-box
29
+
30
+ .shimmerOverlay
31
+ z-index 20
32
+
33
+ .inTransition
34
+ pointer-events none
35
+
36
+ .worldDrivers
37
+ position absolute
38
+ bottom 0
39
+ display flex
40
+ align-items center
41
+ gap 0.5rem
42
+ padding 0.5rem
43
+
44
+ button
45
+ position static !important
46
+ transform none !important
47
+
48
+ & > span
49
+ transform none !important
50
+ opacity 1 !important
@@ -0,0 +1,12 @@
1
+ // This file is automatically generated.
2
+ // Please do not change this file!
3
+ interface CssExports {
4
+ 'inTransition': string;
5
+ 'mapInner': string;
6
+ 'mapWorld': string;
7
+ 'root': string;
8
+ 'shimmerOverlay': string;
9
+ 'worldDrivers': string;
10
+ }
11
+ export const cssExports: CssExports;
12
+ export default cssExports;
@@ -0,0 +1,212 @@
1
+ import cn from 'classnames';
2
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
3
+
4
+ import { WorldMap } from '#uilib/components/ui/WorldMap';
5
+ import useEvent from '#uilib/hooks/useEvent';
6
+ import { Shimmer } from '@homecode/ui';
7
+
8
+ import { BadgeSize, DriverIcon } from './DriverIcon/DriverIcon';
9
+ import constants from './DriverIcon/DriverIcon.constants.json';
10
+ import {
11
+ calculateBadgeSizes,
12
+ findMostSpecificRegion,
13
+ getDriverCoordinates,
14
+ hasValidCoords,
15
+ } from './DriverMap.helpers';
16
+ import S from './DriverMap.styl';
17
+ import type { DriverData } from './driverMapGeography';
18
+ import { getHighestImportanceDriver } from './driverMapSelection';
19
+
20
+ export interface DriverMapProps {
21
+ drivers: DriverData[];
22
+ isLoading: boolean;
23
+ setSelectedDriver: (driver: DriverData) => void;
24
+ selectedDriver: DriverData | null;
25
+ }
26
+
27
+ const delay = (ms: number) => new Promise<void>(r => setTimeout(r, ms));
28
+
29
+ const transitionDelay = () => delay(constants.FADE_DURATION);
30
+
31
+ export function DriverMap({
32
+ drivers,
33
+ isLoading,
34
+ setSelectedDriver,
35
+ selectedDriver,
36
+ }: DriverMapProps) {
37
+ const visibleDriversRef = useRef<DriverData[]>([]);
38
+
39
+ const [showingDrivers, setShowingDrivers] = useState<DriverData[]>(drivers);
40
+ const [badgeSizes, setBadgeSizes] = useState<Record<string, BadgeSize>>({});
41
+ const [isVisible, setIsVisible] = useState(false);
42
+ const [isTransitioning, setIsTransitioning] = useState(false);
43
+
44
+ const makeTransition = async (nextDrivers: DriverData[]) => {
45
+ const hadDrivers = showingDrivers.length > 0;
46
+
47
+ setIsTransitioning(true);
48
+
49
+ if (hadDrivers) {
50
+ setIsVisible(false);
51
+ await transitionDelay();
52
+ }
53
+
54
+ if (nextDrivers.length > 0) {
55
+ setShowingDrivers(nextDrivers);
56
+ setIsVisible(false);
57
+
58
+ const sizes = calculateBadgeSizes(nextDrivers);
59
+ setBadgeSizes(sizes);
60
+
61
+ await delay(30);
62
+ setIsVisible(true);
63
+ await transitionDelay();
64
+ } else {
65
+ setShowingDrivers([]);
66
+ setBadgeSizes({});
67
+ setIsVisible(false);
68
+ }
69
+
70
+ setIsTransitioning(false);
71
+ };
72
+
73
+ const [worldDrivers, otherDrivers] = useMemo(() => {
74
+ const visibleDrivers: DriverData[] = [];
75
+ const worldDriversList: DriverData[] = [];
76
+
77
+ drivers.forEach(driver => {
78
+ let importance = driver.importance || 0;
79
+ if (importance === 0) {
80
+ const mean = driver?.rawImportance?.overall?.mean;
81
+ if (typeof mean === 'number') {
82
+ importance = Math.abs(mean) * 100;
83
+ }
84
+ }
85
+
86
+ const specificRegion = findMostSpecificRegion(driver);
87
+ const isEmptyRegion = driver.region?.length === 0;
88
+ const isWorldRegion =
89
+ specificRegion.name === 'World' ||
90
+ driver.region?.slice(-1)[0] === 'World';
91
+
92
+ const coordinates = getDriverCoordinates(
93
+ driver,
94
+ specificRegion,
95
+ visibleDrivers,
96
+ );
97
+ const driverToAdd = {
98
+ ...driver,
99
+ coordinates,
100
+ };
101
+
102
+ const shouldGoToWorldDrivers =
103
+ isWorldRegion ||
104
+ !hasValidCoords(driverToAdd.coordinates) ||
105
+ (isEmptyRegion && !specificRegion.name);
106
+
107
+ if (shouldGoToWorldDrivers && importance >= 0) {
108
+ worldDriversList.push(driver);
109
+ } else if (hasValidCoords(driverToAdd.coordinates) && importance >= 0) {
110
+ visibleDrivers.push(driverToAdd);
111
+ }
112
+ });
113
+
114
+ return [
115
+ worldDriversList.sort((a, b) => b.importance - a.importance),
116
+ visibleDrivers,
117
+ ];
118
+ }, [drivers]);
119
+
120
+ useEffect(() => {
121
+ void makeTransition(drivers);
122
+
123
+ if (drivers.length > 0) {
124
+ const idx = getHighestImportanceDriver(drivers);
125
+ if (idx !== null && idx >= 0) {
126
+ setSelectedDriver(drivers[idx]);
127
+ }
128
+ }
129
+ }, [drivers]);
130
+
131
+ useEffect(() => {
132
+ visibleDriversRef.current = otherDrivers;
133
+ }, [otherDrivers]);
134
+
135
+ const handleKeyDown = useCallback(
136
+ (e: KeyboardEvent) => {
137
+ const items = visibleDriversRef.current;
138
+
139
+ if (!selectedDriver || items.length === 0 || e.metaKey || e.ctrlKey)
140
+ return;
141
+
142
+ const stop = () => {
143
+ e.preventDefault();
144
+ e.stopPropagation();
145
+ };
146
+
147
+ const currIndex = items.findIndex(d => d.id === selectedDriver.id);
148
+
149
+ if (e.key === 'ArrowLeft') {
150
+ const prevIndex = currIndex > 0 ? currIndex - 1 : items.length - 1;
151
+
152
+ setSelectedDriver(items[prevIndex]);
153
+ stop();
154
+ } else if (e.key === 'ArrowRight') {
155
+ const nextIndex = currIndex < items.length - 1 ? currIndex + 1 : 0;
156
+
157
+ setSelectedDriver(items[nextIndex]);
158
+ stop();
159
+ }
160
+ },
161
+ [selectedDriver, setSelectedDriver],
162
+ );
163
+
164
+ useEvent({
165
+ event: 'keydown',
166
+ callback: handleKeyDown,
167
+ isCapture: true,
168
+ });
169
+
170
+ return (
171
+ <div className={cn(S.root, isTransitioning && S.inTransition)}>
172
+ <div className={S.mapInner}>
173
+ <WorldMap className={S.mapWorld} />
174
+ {isLoading && <Shimmer className={S.shimmerOverlay} size="l" />}
175
+ {otherDrivers.map(driver => (
176
+ <DriverIcon
177
+ key={driver.id}
178
+ isLoading={isLoading}
179
+ isVisible={isVisible}
180
+ driver={driver}
181
+ size={badgeSizes[driver.id] || 's'}
182
+ isSelected={selectedDriver?.id === driver.id}
183
+ onClick={() => setSelectedDriver(driver)}
184
+ />
185
+ ))}
186
+ </div>
187
+ <div className={S.worldDrivers}>
188
+ {worldDrivers.map(driver => {
189
+ const driverWithCoords = {
190
+ ...driver,
191
+ coordinates: driver.coordinates || {
192
+ x: 0,
193
+ y: 0,
194
+ continent: 'Global',
195
+ },
196
+ };
197
+ return (
198
+ <DriverIcon
199
+ key={driver.id}
200
+ isLoading={isLoading}
201
+ isVisible={isVisible}
202
+ driver={driverWithCoords}
203
+ size={badgeSizes[driver.id] || 's'}
204
+ isSelected={selectedDriver?.id === driver.id}
205
+ onClick={() => setSelectedDriver(driver)}
206
+ />
207
+ );
208
+ })}
209
+ </div>
210
+ </div>
211
+ );
212
+ }