@bundle-stats/ui 4.13.0-rc.0 → 4.13.0-rc.2

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 (86) hide show
  1. package/lib/components/bundle-assets/bundle-assets.js +24 -22
  2. package/lib/components/bundle-assets/bundle-assets.js.map +1 -1
  3. package/lib/components/bundle-assets/index.js +3 -1
  4. package/lib/components/bundle-assets/index.js.map +1 -1
  5. package/lib/components/bundle-modules/bundle-modules.js +11 -6
  6. package/lib/components/bundle-modules/bundle-modules.js.map +1 -1
  7. package/lib/components/bundle-modules/bundle-modules.utils.js +2 -13
  8. package/lib/components/bundle-modules/bundle-modules.utils.js.map +1 -1
  9. package/lib/components/bundle-modules/index.js +2 -1
  10. package/lib/components/bundle-modules/index.js.map +1 -1
  11. package/lib/components/bundle-packages/bundle-packages.js +12 -6
  12. package/lib/components/bundle-packages/bundle-packages.js.map +1 -1
  13. package/lib/components/metrics-treemap/metrics-treemap.constants.js +7 -0
  14. package/lib/components/metrics-treemap/metrics-treemap.constants.js.map +1 -1
  15. package/lib/components/metrics-treemap/metrics-treemap.js +69 -71
  16. package/lib/components/metrics-treemap/metrics-treemap.js.map +1 -1
  17. package/lib/components/metrics-treemap/metrics-treemap.module.css +58 -26
  18. package/lib/components/metrics-treemap/metrics-treemap.utils.js +60 -1
  19. package/lib/components/metrics-treemap/metrics-treemap.utils.js.map +1 -1
  20. package/lib/utils/colors.js +22 -0
  21. package/lib/utils/colors.js.map +1 -0
  22. package/lib/{utils.js → utils/components.js} +2 -17
  23. package/lib/utils/components.js.map +1 -0
  24. package/lib/utils/index.js +20 -0
  25. package/lib/utils/index.js.map +1 -0
  26. package/lib/utils/jobs.js +18 -0
  27. package/lib/utils/jobs.js.map +1 -0
  28. package/lib-esm/components/bundle-assets/bundle-assets.js +24 -23
  29. package/lib-esm/components/bundle-assets/bundle-assets.js.map +1 -1
  30. package/lib-esm/components/bundle-assets/index.js +3 -1
  31. package/lib-esm/components/bundle-assets/index.js.map +1 -1
  32. package/lib-esm/components/bundle-modules/bundle-modules.js +11 -6
  33. package/lib-esm/components/bundle-modules/bundle-modules.js.map +1 -1
  34. package/lib-esm/components/bundle-modules/bundle-modules.utils.js +1 -11
  35. package/lib-esm/components/bundle-modules/bundle-modules.utils.js.map +1 -1
  36. package/lib-esm/components/bundle-modules/index.js +3 -2
  37. package/lib-esm/components/bundle-modules/index.js.map +1 -1
  38. package/lib-esm/components/bundle-packages/bundle-packages.js +12 -6
  39. package/lib-esm/components/bundle-packages/bundle-packages.js.map +1 -1
  40. package/lib-esm/components/metrics-treemap/metrics-treemap.constants.js +6 -1
  41. package/lib-esm/components/metrics-treemap/metrics-treemap.constants.js.map +1 -1
  42. package/lib-esm/components/metrics-treemap/metrics-treemap.js +65 -68
  43. package/lib-esm/components/metrics-treemap/metrics-treemap.js.map +1 -1
  44. package/lib-esm/components/metrics-treemap/metrics-treemap.module.css +58 -26
  45. package/lib-esm/components/metrics-treemap/metrics-treemap.utils.js +57 -0
  46. package/lib-esm/components/metrics-treemap/metrics-treemap.utils.js.map +1 -1
  47. package/lib-esm/utils/colors.js +15 -0
  48. package/lib-esm/utils/colors.js.map +1 -0
  49. package/lib-esm/utils/components.js +16 -0
  50. package/lib-esm/utils/components.js.map +1 -0
  51. package/lib-esm/utils/index.js +4 -0
  52. package/lib-esm/utils/index.js.map +1 -0
  53. package/lib-esm/utils/jobs.js +11 -0
  54. package/lib-esm/utils/jobs.js.map +1 -0
  55. package/package.json +3 -3
  56. package/src/components/bundle-assets/bundle-assets.jsx +74 -48
  57. package/src/components/bundle-assets/index.jsx +4 -0
  58. package/src/components/bundle-modules/bundle-modules.tsx +33 -11
  59. package/src/components/bundle-modules/bundle-modules.types.d.ts +2 -17
  60. package/src/components/bundle-modules/bundle-modules.utils.ts +7 -18
  61. package/src/components/bundle-modules/index.tsx +4 -3
  62. package/src/components/bundle-packages/bundle-packages.jsx +21 -7
  63. package/src/components/metrics-treemap/metrics-treemap.constants.ts +11 -0
  64. package/src/components/metrics-treemap/metrics-treemap.module.css +58 -26
  65. package/src/components/metrics-treemap/metrics-treemap.tsx +173 -110
  66. package/src/components/metrics-treemap/metrics-treemap.utils.ts +107 -1
  67. package/src/utils/colors.ts +19 -0
  68. package/src/utils/components.tsx +19 -0
  69. package/src/utils/index.ts +3 -0
  70. package/src/utils/jobs.ts +22 -0
  71. package/types/components/bundle-assets/bundle-assets.d.ts +1 -5
  72. package/types/components/bundle-modules/bundle-modules.d.ts +4 -2
  73. package/types/components/bundle-modules/bundle-modules.utils.d.ts +3 -7
  74. package/types/components/bundle-modules/index.d.ts +2 -2
  75. package/types/components/metrics-treemap/metrics-treemap.constants.d.ts +8 -0
  76. package/types/components/metrics-treemap/metrics-treemap.d.ts +1 -1
  77. package/types/components/metrics-treemap/metrics-treemap.utils.d.ts +11 -1
  78. package/types/utils/colors.d.ts +1 -0
  79. package/types/utils/components.d.ts +2 -0
  80. package/types/utils/index.d.ts +3 -0
  81. package/types/utils/jobs.d.ts +7 -0
  82. package/lib/utils.js.map +0 -1
  83. package/lib-esm/utils.js +0 -30
  84. package/lib-esm/utils.js.map +0 -1
  85. package/src/utils.jsx +0 -38
  86. package/types/utils.d.ts +0 -3
@@ -25,40 +25,72 @@
25
25
 
26
26
  .tileGroupTitle {
27
27
  width: 100%;
28
- padding: 4px 2px 4px var(--space-xxsmall);
28
+ }
29
+
30
+ .tileGroupTitleContent {
31
+ width: 100%;
32
+ padding: var(--space-xxxsmall) 2px var(--space-xxxsmall) var(--space-xxsmall);
33
+ display: flex;
34
+ align-items: center;
29
35
  color: var(--color-text);
30
36
  font-family: var(--font-family-fixed);
31
37
  font-size: 10px;
32
- line-height: 16px;
38
+ line-height: var(--space-small);
33
39
  overflow: hidden;
34
40
  white-space: nowrap;
35
41
  text-overflow: ellipsis;
36
42
  }
37
43
 
44
+ .tileGroupTitleText,
38
45
  .tileGroupTitleTotal {
39
- margin-left: var(--space-xxxsmall);
46
+ flex: 0 0 auto;
47
+ min-width: 0;
48
+ }
49
+
50
+ .tileGroupTitleTotal {
51
+ margin-left: var(--space-xxsmall);
52
+ color: var(--color-text-light);
53
+ }
54
+
55
+ .tileGroupTitle {
56
+ transition: var(--ui-transition-out);
57
+ }
58
+
59
+ .tileGroupTitle:hover .tileGroupTitleText {
60
+ text-decoration: underline;
61
+ transition: var(--ui-transition-in);
40
62
  }
41
63
 
42
64
  /** Tile group size variation */
43
- .tileGroupSizeDefault {
65
+ .tileGroup {
44
66
  transition: var(--ui-transition-in);
45
67
  transition-property: left, top, width, height;
46
68
  }
47
69
 
70
+ .tileGroup--minimal .tileGroupTitle,
71
+ .tileGroup--small .tileGroupTitle {
72
+ height: 100%; /* full height when title is missing or we do not render any children */
73
+ }
74
+
75
+ .tileGroup--minimal .tileGroupTitleContent,
76
+ .tileGroup--small .tileGroupTitleContent {
77
+ min-height: 100%;
78
+ }
79
+
48
80
  /** Tile group nested variation */
49
81
  .nested .tileGroup {
50
- background-color: rgba(27, 26, 33, 0.05);
51
- outline: 1px solid rgba(255, 255, 255, 0.75);
82
+ background-color: rgba(27, 26, 33, 0.03);
83
+ outline: 1px solid rgba(255, 255, 255, 0.6);
52
84
  }
53
85
 
54
86
  .nested .tileGroup:has(> .tileGroupTitle:hover),
55
87
  .nested .tileGroup:has(> .tile:hover) {
56
- background-color: rgba(27, 26, 33, 0.1);
88
+ background-color: rgba(27, 26, 33, 0.06);
57
89
  outline: 1px solid rgba(255, 255, 255, 1);
58
90
  }
59
91
 
60
92
  .nested .tileGroup--NEGATIVE {
61
- background-color: rgba(194, 31, 37, 0.05);
93
+ background-color: rgba(194, 31, 37, 0.04);
62
94
  }
63
95
 
64
96
  .nested .tileGroup--NEGATIVE:has(> .tileGroupTitle:hover),
@@ -67,7 +99,7 @@
67
99
  }
68
100
 
69
101
  .nested .tileGroup--POSITIVE {
70
- background-color: rgba(42, 147, 39, 0.05);
102
+ background-color: rgba(42, 147, 39, 0.04);
71
103
  }
72
104
 
73
105
  .nested .tileGroup--POSITIVE:has(> .tileGroupTitle:hover),
@@ -141,7 +173,13 @@
141
173
  flex: 0 1 auto;
142
174
  width: 100%;
143
175
  min-width: 0;
144
- max-height: calc(2em * var(--line-height-heading));
176
+ max-height: calc(2em * var(--line-height-heading)); /* render max 2 rows */
177
+ overflow-wrap: break-word;
178
+ word-wrap: break-word;
179
+ word-break: break-all;
180
+ word-break: break-word;
181
+ hyphens: auto;
182
+ white-space: normal;
145
183
  overflow: hidden;
146
184
  }
147
185
 
@@ -162,9 +200,8 @@
162
200
 
163
201
  /** Tile size variation */
164
202
  .tileSizeSmall .tileContent {
165
- padding: var(--space-xxxsmall) var(--space-xxsmall);
166
- gap: 0;
167
- font-size: var(--size-xsmall);
203
+ padding: var(--space-xxxsmall);
204
+ font-size: 10px;
168
205
  }
169
206
 
170
207
  .tileSizeSmall .tileContentLabel {
@@ -173,7 +210,7 @@
173
210
 
174
211
  .tileSizeDefault {
175
212
  transition: var(--ui-transition-in);
176
- transition-property: left top width height;
213
+ transition-property: left, top, width, height;
177
214
  }
178
215
 
179
216
  /** Tile diff variation */
@@ -187,7 +224,7 @@
187
224
  color: var(--color-highlight);
188
225
  transition: var(--ui-transition-out);
189
226
  transition-property: opacity;
190
- opacity: 0.5;
227
+ opacity: 0.6;
191
228
  }
192
229
 
193
230
  .tile:hover::before {
@@ -196,11 +233,16 @@
196
233
  }
197
234
 
198
235
  .tile-NO_CHANGE {
199
- color: var(--color-text-ultra-light);
236
+ color: var(--color-text-light);
200
237
  }
201
238
 
202
239
  .tile-NO_CHANGE::before {
203
240
  color: var(--color-gray-50);
241
+ opacity: 0.4;
242
+ }
243
+
244
+ .tile-NO_CHANGE:hover::before {
245
+ opacity: 0.2;
204
246
  }
205
247
 
206
248
  .tile-HIGH_NEGATIVE::before {
@@ -213,20 +255,10 @@
213
255
 
214
256
  .tile-LOW_NEGATIVE::before {
215
257
  color: var(--color-red-50);
216
- opacity: 0.6;
217
- }
218
-
219
- .tile-LOW_NEGATIVE:hover::before {
220
- opacity: 0.5;
221
258
  }
222
259
 
223
260
  .tile-LOW_POSITIVE::before {
224
261
  color: var(--color-green-50);
225
- opacity: 0.6;
226
- }
227
-
228
- .tile-LOW_POSITIVE:hover::before {
229
- opacity: 0.5;
230
262
  }
231
263
 
232
264
  .tile-POSITIVE::before {
@@ -9,77 +9,43 @@ import React, {
9
9
  useMemo,
10
10
  useRef,
11
11
  } from 'react';
12
- import { useDebounce, useHoverDirty, useMeasure, useMouseHovered } from 'react-use';
12
+ import { useHoverDirty, useMeasure } from 'react-use';
13
13
  import cx from 'classnames';
14
14
  import { HierarchyRectangularNode, hierarchy, treemap, treemapSquarify } from 'd3';
15
- import { Tooltip, TooltipArrow, TooltipAnchor, useTooltipState } from 'ariakit/tooltip';
15
+ import { Tooltip, TooltipArrow, TooltipAnchor } from 'ariakit/tooltip';
16
16
  import {
17
17
  type ReportMetricRow,
18
18
  type MetricRunInfo,
19
19
  type MetricRunInfoBaseline,
20
- getMetricRunInfo,
21
20
  METRIC_TYPE_CONFIGS,
21
+ formatFileSize,
22
+ getMetricRunInfo,
22
23
  } from '@bundle-stats/utils';
23
24
 
24
25
  import { Stack } from '../../layout/stack';
25
26
  import { FileName } from '../../ui/file-name';
26
27
  import { Delta } from '../delta';
27
28
  import { RunInfo } from '../run-info';
28
- import type { TreeLeaf, TreeNode, Tree } from './metrics-treemap.constants';
29
+ import {
30
+ type TreeLeaf,
31
+ type TreeNode,
32
+ type Tree,
33
+ SQUARIFY_RATIO,
34
+ PADDING_OUTER,
35
+ PADDING_INNER,
36
+ NESTED_PADDING,
37
+ NESTED_PADDING_TOP,
38
+ NESTED_PADDING_LEFT,
39
+ TileSizeDisplay,
40
+ } from './metrics-treemap.constants';
29
41
  import * as I18N from './metrics-treemap.i18n';
30
42
  import css from './metrics-treemap.module.css';
31
- import { resolveGroupDeltaType } from './metrics-treemap.utils';
32
-
33
- const SQUARIFY_RATIO = 1.66;
34
- const PADDING_OUTER = 1;
35
- const PADDING_INNER = 2;
36
- const NESTED_PADDING = 4;
37
- const NESTED_PADDING_TOP = 16 + NESTED_PADDING * 2;
38
- const NESTED_PADDING_LEFT = 8;
39
-
40
- /**
41
- * Resolve the tile's size using predefined values to avoid
42
- * computing the size of the content for every tile
43
- */
44
- const PADDING_TOP = 4;
45
- const PADDING_BOTTOM = 2; // substracted to allow longer texts to appear
46
- const PADDING_LEFT = 2;
47
- const PADDING_RIGHT = 4; // substracted to allow longer texts to not break
48
- const VERTICAL_SPACING = 4;
49
- const LINE_HEIGHT = 16;
50
- const LINE_HEIGHT_SMALL = 13.3;
51
-
52
- function resolveTileGroupSizeDisplay(width: number, height: number): TileSizeDisplay {
53
- if (height < NESTED_PADDING_TOP || width < 24) {
54
- return 'minimal';
55
- }
56
-
57
- if (height < NESTED_PADDING_TOP + 8) {
58
- return 'small';
59
- }
60
-
61
- return 'default';
62
- }
63
-
64
- type TileSizeDisplay = 'minimal' | 'small' | 'default';
65
-
66
- function resolveTileSizeDisplay(width: number, height: number): TileSizeDisplay {
67
- if (
68
- height > PADDING_TOP + PADDING_BOTTOM + VERTICAL_SPACING + LINE_HEIGHT * 2 &&
69
- width > PADDING_LEFT + PADDING_RIGHT + 128
70
- ) {
71
- return 'default';
72
- }
73
-
74
- if (
75
- height > PADDING_TOP + PADDING_BOTTOM + LINE_HEIGHT_SMALL * 2 &&
76
- width > PADDING_LEFT + PADDING_BOTTOM + 48
77
- ) {
78
- return 'small';
79
- }
80
-
81
- return 'minimal';
82
- }
43
+ import {
44
+ resolveGroupDeltaType,
45
+ resolveTileSizeDisplay,
46
+ resolveTileGroupSizeDisplay,
47
+ useTooltipStateWithMouseFollow,
48
+ } from './metrics-treemap.utils';
83
49
 
84
50
  interface TileTooltipContentProps {
85
51
  item: ReportMetricRow;
@@ -118,7 +84,7 @@ interface TileContentProps {
118
84
  /**
119
85
  * The estimated size of the tile
120
86
  */
121
- sizeDisplay: 'minimal' | 'small' | 'default';
87
+ sizeDisplay: TileSizeDisplay;
122
88
  /**
123
89
  * Metric run info
124
90
  */
@@ -128,23 +94,33 @@ interface TileContentProps {
128
94
  const TileContent = forwardRef((props: TileContentProps, ref: Ref<HTMLDivElement>) => {
129
95
  const { label, sizeDisplay, item, runInfo } = props;
130
96
 
97
+ // Render only the container
131
98
  if (sizeDisplay === 'minimal') {
132
99
  return <div className={css.tileContent} ref={ref} />;
133
100
  }
134
101
 
102
+ const resolvedLabel = label || item.label;
103
+
104
+ // Render only the label
105
+ if (sizeDisplay === 'small') {
106
+ return (
107
+ <div className={css.tileContent} ref={ref}>
108
+ <p className={css.tileContentLabel}>{resolvedLabel}</p>
109
+ </div>
110
+ );
111
+ }
112
+
135
113
  return (
136
114
  <div className={css.tileContent} ref={ref}>
137
- <FileName as="p" className={css.tileContentLabel} name={label || item.label} />
138
- {sizeDisplay !== 'small' && (
139
- <p className={css.tileContentValue}>
140
- <span className={css.tileContentMetric}>{runInfo.displayValue}</span>
141
- <Delta
142
- className={css.tileContentDelta}
143
- displayValue={runInfo.displayDeltaPercentage}
144
- deltaType={runInfo.deltaType}
145
- />
146
- </p>
147
- )}
115
+ <p className={css.tileContentLabel}>{label || item.label}</p>
116
+ <p className={css.tileContentValue}>
117
+ <span className={css.tileContentMetric}>{runInfo.displayValue}</span>
118
+ <Delta
119
+ className={css.tileContentDelta}
120
+ displayValue={runInfo.displayDeltaPercentage}
121
+ deltaType={runInfo.deltaType}
122
+ />
123
+ </p>
148
124
  </div>
149
125
  );
150
126
  });
@@ -152,29 +128,7 @@ const TileContent = forwardRef((props: TileContentProps, ref: Ref<HTMLDivElement
152
128
  const TileContentWithTooltip = (props: TileContentProps & { parentRef: RefObject<Element> }) => {
153
129
  const { label, sizeDisplay, item, runInfo, parentRef } = props;
154
130
 
155
- const pointer = useMouseHovered(parentRef, { whenHovered: true });
156
-
157
- // Return custom rect based on the mouse position
158
- const getAnchorRect = useCallback(() => {
159
- // Skip custom component rect area if the pointer position is empty
160
- if (pointer.docX === 0 && pointer.docY === 0 && pointer.elW === 0 && pointer.elH === 0) {
161
- return null;
162
- }
163
-
164
- const newRect = {
165
- x: pointer.docX - (document?.defaultView?.scrollX ?? 0),
166
- y: pointer.docY - (document?.defaultView?.scrollY ?? 0),
167
- w: 1,
168
- h: 1,
169
- };
170
-
171
- return newRect;
172
- }, [pointer.docX, pointer.docY]);
173
-
174
- const tooltipState = useTooltipState({ gutter: 12, getAnchorRect, timeout: 120 });
175
-
176
- // Update tooltip position when poiner values are changing
177
- useDebounce(tooltipState.render, 5, [pointer.docX, pointer.docY]);
131
+ const tooltipState = useTooltipStateWithMouseFollow({ parentRef });
178
132
 
179
133
  return (
180
134
  <>
@@ -221,7 +175,7 @@ const Tile = (props: TileProps) => {
221
175
  css[`tile-${runInfo.deltaType}`],
222
176
  sizeDisplay === 'small' && css.tileSizeSmall,
223
177
  sizeDisplay === 'default' && css.tileSizeDefault,
224
- left === 0 && css.tileFirstCol,
178
+ left === PADDING_INNER && css.tileFirstCol,
225
179
  );
226
180
 
227
181
  /* eslint-disable jsx-a11y/click-events-have-key-events, jsx-a11y/interactive-supports-focus */
@@ -249,6 +203,98 @@ const Tile = (props: TileProps) => {
249
203
  );
250
204
  };
251
205
 
206
+ interface TileGroupTitleTooltipContentProps {
207
+ title: string;
208
+ runInfo?: MetricRunInfo;
209
+ baselineDisplayValue?: string;
210
+ }
211
+
212
+ const TileGroupTitleTooltipContent = (props: TileGroupTitleTooltipContentProps) => {
213
+ const { title, runInfo, baselineDisplayValue } = props;
214
+
215
+ return (
216
+ <Stack space="small" className={css.tileTooltip}>
217
+ <h3 className={css.tileTooltipContentTitle}>
218
+ <FileName as="code" name={title} />
219
+ </h3>
220
+ {runInfo && (
221
+ <RunInfo
222
+ current={runInfo.displayValue}
223
+ baseline={baselineDisplayValue}
224
+ delta={runInfo.displayDeltaPercentage}
225
+ deltaType={runInfo.deltaType}
226
+ />
227
+ )}
228
+ </Stack>
229
+ );
230
+ };
231
+
232
+ interface TileGroupTitleContentProps {
233
+ title?: string;
234
+ runInfo?: MetricRunInfo;
235
+ }
236
+
237
+ const TileGroupTitleContent = (props: TileGroupTitleContentProps) => {
238
+ const { title, runInfo } = props;
239
+
240
+ return (
241
+ <div className={css.tileGroupTitleContent}>
242
+ {title && <span className={css.tileGroupTitleText}>{title}</span>}
243
+ {runInfo && (
244
+ <span className={css.tileGroupTitleTotal}>
245
+ {runInfo.displayValue}
246
+ {'displayDelta' in runInfo && `(${runInfo.displayDeltaPercentage})`}
247
+ </span>
248
+ )}
249
+ </div>
250
+ );
251
+ };
252
+
253
+ type TileGroupTitleContentWithTooltipProps = {
254
+ parentRef: RefObject<Element>;
255
+ tooltipContent: TileGroupTitleTooltipContentProps;
256
+ } & TileGroupTitleContentProps;
257
+
258
+ const TileGroupTitleContentWithTooltip = (props: TileGroupTitleContentWithTooltipProps) => {
259
+ const { parentRef, tooltipContent, ...restProps } = props;
260
+
261
+ const tooltipState = useTooltipStateWithMouseFollow({ parentRef });
262
+
263
+ return (
264
+ <>
265
+ <TooltipAnchor state={tooltipState} className={css.tileContentTooltipAnchor}>
266
+ <TileGroupTitleContent {...restProps} />
267
+ </TooltipAnchor>
268
+ <Tooltip state={tooltipState} className={css.tooltip}>
269
+ <TooltipArrow state={tooltipState} size={16} className={css.tileTooltipArrow} />
270
+ <TileGroupTitleTooltipContent {...tooltipContent} />
271
+ </Tooltip>
272
+ </>
273
+ );
274
+ };
275
+
276
+ type TileGroupTitleProps = Omit<TileGroupTitleContentWithTooltipProps, 'parentRef'>;
277
+
278
+ const TileGroupTitle = (props: TileGroupTitleProps) => {
279
+ const { tooltipContent, ...restProps } = props;
280
+ const contentRef = useRef<HTMLDivElement>(null);
281
+ const hover = useHoverDirty(contentRef);
282
+
283
+ return (
284
+ <div className={css.tileGroupTitle} ref={contentRef}>
285
+ {hover ? (
286
+ <TileGroupTitleContentWithTooltip
287
+ parentRef={contentRef}
288
+ tooltipContent={tooltipContent}
289
+ {...restProps}
290
+ />
291
+ ) : (
292
+ <TileGroupTitleContent {...restProps} />
293
+ )}
294
+ </div>
295
+ );
296
+ };
297
+
252
298
  interface TileGroupProps extends ComponentProps<'div'> {
253
299
  /**
254
300
  * Node title
@@ -323,11 +369,36 @@ const TileGroup = (props: TileGroupProps) => {
323
369
  [width, height],
324
370
  );
325
371
 
326
- const metricRunInfo =
327
- total &&
328
- getMetricRunInfo(METRIC_TYPE_CONFIGS.METRIC_TYPE_FILE_SIZE, total.current, total.baseline);
329
- const groupDeltaType = resolveGroupDeltaType(metricRunInfo);
330
- const rootClassName = cx(css.tileGroup, css[`tileGroup--${groupDeltaType}`]);
372
+ // Prepare data
373
+ const [runInfo, baselineDisplayValue, deltaType] = useMemo(() => {
374
+ if (!total) {
375
+ return [];
376
+ }
377
+
378
+ const resolvedRunInfo = getMetricRunInfo(
379
+ METRIC_TYPE_CONFIGS.METRIC_TYPE_FILE_SIZE,
380
+ total.current,
381
+ total.baseline,
382
+ ) as MetricRunInfo;
383
+ const resolvedBaselineDisplayValue = formatFileSize(total.baseline);
384
+ const resolvedDeltaType = resolveGroupDeltaType(resolvedRunInfo);
385
+
386
+ return [resolvedRunInfo, resolvedBaselineDisplayValue, resolvedDeltaType];
387
+ }, [total]);
388
+
389
+ const rootClassName = cx(
390
+ css.tileGroup,
391
+ css[`tileGroup--${deltaType}`],
392
+ css[`tileGroup--${displaySize}`],
393
+ );
394
+
395
+ // Tooltip data
396
+ // - by default show the node id (full path) as title
397
+ const tooltipContent = {
398
+ title: id || title,
399
+ runInfo,
400
+ baselineDisplayValue,
401
+ } as TileGroupTitleTooltipContentProps;
331
402
 
332
403
  if (title && displaySize === 'minimal') {
333
404
  return (
@@ -337,7 +408,9 @@ const TileGroup = (props: TileGroupProps) => {
337
408
  aria-label={I18N.TILE_GROUP_LABEL}
338
409
  className={rootClassName}
339
410
  style={{ left, top, width, height }}
340
- />
411
+ >
412
+ <TileGroupTitle tooltipContent={tooltipContent} />
413
+ </div>
341
414
  );
342
415
  }
343
416
 
@@ -350,7 +423,7 @@ const TileGroup = (props: TileGroupProps) => {
350
423
  style={{ left, top, width, height }}
351
424
  className={rootClassName}
352
425
  >
353
- <div className={css.tileGroupTitle}>{title}</div>
426
+ <TileGroupTitle title={title} tooltipContent={tooltipContent} />
354
427
  </div>
355
428
  );
356
429
  }
@@ -363,17 +436,7 @@ const TileGroup = (props: TileGroupProps) => {
363
436
  className={cx(rootClassName, css.tileGroupSizeDefault)}
364
437
  style={{ left, top, width, height }}
365
438
  >
366
- {title && (
367
- <div className={css.tileGroupTitle}>
368
- {title}
369
- {metricRunInfo && (
370
- <span className={css.tileGroupTitleTotal}>
371
- {`${metricRunInfo.displayValue}`}
372
- {'displayDelta' in metricRunInfo && ` (${metricRunInfo.displayDeltaPercentage})`}
373
- </span>
374
- )}
375
- </div>
376
- )}
439
+ <TileGroupTitle title={title} runInfo={runInfo} tooltipContent={tooltipContent} />
377
440
  {childNodes?.map((childNode) => {
378
441
  if ('children' in childNode) {
379
442
  const groupData = childNode.data as Tree;
@@ -1,3 +1,6 @@
1
+ import { type RefObject, useCallback } from 'react';
2
+ import { useDebounce, useMouseHovered } from 'react-use';
3
+ import { useTooltipState } from 'ariakit/tooltip';
1
4
  import {
2
5
  DeltaType,
3
6
  type MetricRunInfoBaseline,
@@ -5,7 +8,15 @@ import {
5
8
  type ReportMetricRow,
6
9
  } from '@bundle-stats/utils';
7
10
 
8
- import type { TreeLeaf, TreeNodeChildren, Tree, TreeTotal } from './metrics-treemap.constants';
11
+ import {
12
+ type TreeLeaf,
13
+ type TreeNodeChildren,
14
+ type Tree,
15
+ type TreeTotal,
16
+ type TileSizeDisplay,
17
+ type TileGroupSizeDisplay,
18
+ NESTED_PADDING_TOP,
19
+ } from './metrics-treemap.constants';
9
20
 
10
21
  const ROOT_LABEL = '(root)';
11
22
 
@@ -160,3 +171,98 @@ export function resolveGroupDeltaType(
160
171
 
161
172
  return DeltaType.NO_CHANGE;
162
173
  }
174
+
175
+ /**
176
+ * Resolve the tile's size using predefined values to avoid
177
+ * computing the size of the content for every tile
178
+ */
179
+ const PADDING_TOP = 8;
180
+ const PADDING_BOTTOM = 8;
181
+ const PADDING_LEFT = 8;
182
+ const PADDING_RIGHT = 8;
183
+ const LINE_HEIGHT = 16;
184
+ const LINE_HEIGHT_SMALL = 13.3;
185
+
186
+ export function resolveTileGroupSizeDisplay(width: number, height: number): TileGroupSizeDisplay {
187
+ if (height < NESTED_PADDING_TOP) {
188
+ return 'minimal';
189
+ }
190
+
191
+ if (width < 24) {
192
+ return 'minimal';
193
+ }
194
+
195
+ if (height < NESTED_PADDING_TOP + 8) {
196
+ return 'small';
197
+ }
198
+
199
+ return 'default';
200
+ }
201
+
202
+ export function resolveTileSizeDisplay(width: number, height: number): TileSizeDisplay {
203
+ if (height < PADDING_TOP + PADDING_BOTTOM + LINE_HEIGHT_SMALL) {
204
+ return 'minimal';
205
+ }
206
+
207
+ if (width < PADDING_LEFT + PADDING_RIGHT + 42) {
208
+ return 'minimal';
209
+ }
210
+
211
+ if (height < PADDING_TOP + PADDING_TOP + LINE_HEIGHT * 2) {
212
+ return 'small';
213
+ }
214
+
215
+ if (width < PADDING_LEFT + PADDING_RIGHT + 96) {
216
+ return 'small';
217
+ }
218
+
219
+ return 'default';
220
+ }
221
+
222
+ interface UseTooltipStateWithMouseFollowOptions {
223
+ /**
224
+ * React ref to parent div
225
+ */
226
+ parentRef: RefObject<Element>;
227
+ /**
228
+ * Tooltip gutter
229
+ */
230
+ gutter?: number;
231
+ /**
232
+ * Tooltip timeout
233
+ */
234
+ timeout?: number;
235
+ }
236
+
237
+ /**
238
+ * Ariakit tooltip state hook with mouse follow functionality
239
+ */
240
+ export function useTooltipStateWithMouseFollow(options: UseTooltipStateWithMouseFollowOptions) {
241
+ const { parentRef, gutter = 16, timeout = 180 } = options;
242
+
243
+ const pointer = useMouseHovered(parentRef, { whenHovered: true });
244
+
245
+ // Return custom rect based on the mouse position
246
+ const getAnchorRect = useCallback(() => {
247
+ // Skip custom component rect area if the pointer position is empty
248
+ if (pointer.docX === 0 && pointer.docY === 0 && pointer.elW === 0 && pointer.elH === 0) {
249
+ return null;
250
+ }
251
+
252
+ const newRect = {
253
+ x: pointer.docX - (document?.defaultView?.scrollX ?? 0),
254
+ y: pointer.docY - (document?.defaultView?.scrollY ?? 0),
255
+ w: 1,
256
+ h: 1,
257
+ };
258
+
259
+ return newRect;
260
+ }, [pointer.docX, pointer.docY]);
261
+
262
+ const tooltipState = useTooltipState({ gutter, getAnchorRect, timeout });
263
+
264
+ // Update tooltip position when pointer values change
265
+ useDebounce(tooltipState.render, 10, [pointer.docX, pointer.docY]);
266
+
267
+ return tooltipState;
268
+ }
@@ -0,0 +1,19 @@
1
+ import chunk from 'lodash/chunk';
2
+
3
+ import COLORS from '../chart-colors.json';
4
+
5
+ export function getColors(count = 2) {
6
+ const chunks = chunk(COLORS, Math.round(COLORS.length / count));
7
+
8
+ return chunks.map((chunkColors, index) => {
9
+ if (index === 0) {
10
+ return chunkColors[0];
11
+ }
12
+
13
+ if (index === chunks.length - 1) {
14
+ return chunkColors[chunkColors.length - 1];
15
+ }
16
+
17
+ return chunkColors[Math.floor(chunkColors.length / 2)];
18
+ });
19
+ }
@@ -0,0 +1,19 @@
1
+ import React from 'react';
2
+ import cx from 'classnames';
3
+
4
+ export const getRenderChildWithClassName = (className: string) => (child: any) => {
5
+ if (child === null || child === '') {
6
+ return null;
7
+ }
8
+
9
+ const { props } = child;
10
+
11
+ if (!props) {
12
+ return <div className={className}>{child}</div>;
13
+ }
14
+
15
+ return React.cloneElement(child, {
16
+ ...props,
17
+ className: cx(className, props.className), // eslint-disable-line react/prop-types
18
+ });
19
+ };