@bundle-stats/ui 4.13.0-beta.0 → 4.13.0-beta.3

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 (54) hide show
  1. package/lib/components/bundle-assets/index.js +2 -2
  2. package/lib/components/bundle-assets/index.js.map +1 -1
  3. package/lib/components/bundle-modules/bundle-modules.js +4 -2
  4. package/lib/components/bundle-modules/bundle-modules.js.map +1 -1
  5. package/lib/components/bundle-modules/index.js +2 -2
  6. package/lib/components/bundle-modules/index.js.map +1 -1
  7. package/lib/components/bundle-packages/index.js +2 -2
  8. package/lib/components/bundle-packages/index.js.map +1 -1
  9. package/lib/components/metrics-display-selector/metrics-display-selector.js +2 -8
  10. package/lib/components/metrics-display-selector/metrics-display-selector.js.map +1 -1
  11. package/lib/components/metrics-treemap/metrics-treemap.js +68 -49
  12. package/lib/components/metrics-treemap/metrics-treemap.js.map +1 -1
  13. package/lib/components/metrics-treemap/metrics-treemap.module.css +58 -42
  14. package/lib/hooks/entry-info.js +12 -0
  15. package/lib/hooks/entry-info.js.map +1 -0
  16. package/lib/query-state.js +1 -1
  17. package/lib/query-state.js.map +1 -1
  18. package/lib/ui/button/button.module.css +1 -0
  19. package/lib-esm/components/bundle-assets/index.js +3 -3
  20. package/lib-esm/components/bundle-assets/index.js.map +1 -1
  21. package/lib-esm/components/bundle-modules/bundle-modules.js +4 -2
  22. package/lib-esm/components/bundle-modules/bundle-modules.js.map +1 -1
  23. package/lib-esm/components/bundle-modules/index.js +2 -2
  24. package/lib-esm/components/bundle-modules/index.js.map +1 -1
  25. package/lib-esm/components/bundle-packages/index.js +3 -3
  26. package/lib-esm/components/bundle-packages/index.js.map +1 -1
  27. package/lib-esm/components/metrics-display-selector/metrics-display-selector.js +2 -8
  28. package/lib-esm/components/metrics-display-selector/metrics-display-selector.js.map +1 -1
  29. package/lib-esm/components/metrics-treemap/metrics-treemap.js +71 -51
  30. package/lib-esm/components/metrics-treemap/metrics-treemap.js.map +1 -1
  31. package/lib-esm/components/metrics-treemap/metrics-treemap.module.css +58 -42
  32. package/lib-esm/hooks/entry-info.js +8 -0
  33. package/lib-esm/hooks/entry-info.js.map +1 -0
  34. package/lib-esm/query-state.js +1 -1
  35. package/lib-esm/query-state.js.map +1 -1
  36. package/lib-esm/ui/button/button.module.css +1 -0
  37. package/package.json +7 -9
  38. package/src/components/bundle-assets/index.jsx +8 -3
  39. package/src/components/bundle-modules/bundle-modules.tsx +4 -2
  40. package/src/components/bundle-modules/index.tsx +2 -2
  41. package/src/components/bundle-packages/index.jsx +8 -3
  42. package/src/components/metrics-display-selector/metrics-display-selector.tsx +4 -8
  43. package/src/components/metrics-treemap/metrics-treemap.module.css +58 -42
  44. package/src/components/metrics-treemap/metrics-treemap.tsx +141 -105
  45. package/src/hooks/entry-info.ts +19 -0
  46. package/src/query-state.tsx +1 -1
  47. package/src/ui/button/button.module.css +1 -0
  48. package/types/hooks/entry-info.d.ts +8 -0
  49. package/lib/hooks/show-entry.js +0 -13
  50. package/lib/hooks/show-entry.js.map +0 -1
  51. package/lib-esm/hooks/show-entry.js +0 -9
  52. package/lib-esm/hooks/show-entry.js.map +0 -1
  53. package/src/hooks/show-entry.ts +0 -17
  54. package/types/hooks/show-entry.d.ts +0 -10
@@ -1,16 +1,9 @@
1
- import React, { forwardRef, useCallback, useMemo, useRef, useState } from 'react';
1
+ import React, { RefObject, forwardRef, useCallback, useMemo, useRef } from 'react';
2
+ import { useDebounce, useHoverDirty, useMeasure, useMouseHovered } from 'react-use';
2
3
  import cx from 'classnames';
3
- import { useMeasure } from 'react-use';
4
4
  import type { ReportMetricRow, MetricRunInfo, MetricRunInfoBaseline } from '@bundle-stats/utils';
5
5
  import { hierarchy, treemap, treemapSquarify } from 'd3';
6
- import {
7
- FloatingPortal,
8
- useClientPoint,
9
- useFloating,
10
- useHover,
11
- useInteractions,
12
- } from '@floating-ui/react';
13
- import { arrow, offset } from '@floating-ui/react-dom';
6
+ import { Tooltip, TooltipArrow, TooltipAnchor, useTooltipState } from 'ariakit/tooltip';
14
7
 
15
8
  import { Stack } from '../../layout/stack';
16
9
  import { FileName } from '../../ui/file-name';
@@ -19,7 +12,7 @@ import { RunInfo } from '../run-info';
19
12
  import css from './metrics-treemap.module.css';
20
13
 
21
14
  const SQUARIFY_RATIO = 1.78;
22
- const PADDING_OUTER = 0;
15
+ const PADDING_OUTER = 1;
23
16
  const PADDING_INNER = 1;
24
17
 
25
18
  type TileSizeDisplay = 'minimal' | 'small' | 'default';
@@ -36,10 +29,10 @@ const VERTICAL_SPACING = 4;
36
29
  const LINE_HEIGHT = 16;
37
30
  const LINE_HEIGHT_SMALL = 13.3;
38
31
 
39
- function resolveTileSizeDisplay(width: number, height: number): TileSizeDisplay {
32
+ function resolveLeafSizeDisplay(width: number, height: number): TileSizeDisplay {
40
33
  if (
41
34
  height > PADDING_TOP + PADDING_BOTTOM + VERTICAL_SPACING + LINE_HEIGHT * 2 &&
42
- width > PADDING_LEFT + PADDING_RIGHT + 96
35
+ width > PADDING_LEFT + PADDING_RIGHT + 128
43
36
  ) {
44
37
  return 'default';
45
38
  }
@@ -54,19 +47,20 @@ function resolveTileSizeDisplay(width: number, height: number): TileSizeDisplay
54
47
  return 'minimal';
55
48
  }
56
49
 
57
- interface TooltipContentProps {
58
- data: ReportMetricRow;
50
+ interface LeafTooltipContentProps {
51
+ item: ReportMetricRow;
59
52
  }
60
53
 
61
- const TooltipContent = (props: TooltipContentProps) => {
62
- const { data } = props;
63
- const currentRun = data.runs[0] as MetricRunInfo;
64
- const baselineRun = data.runs[data.runs.length - 1] as MetricRunInfoBaseline;
54
+ const LeafTooltipContent = (props: LeafTooltipContentProps) => {
55
+ const { item } = props;
56
+
57
+ const currentRun = item.runs[0] as MetricRunInfo;
58
+ const baselineRun = item.runs[item.runs.length - 1] as MetricRunInfoBaseline;
65
59
 
66
60
  return (
67
- <Stack space="small" className={css.tooltipContent}>
68
- <h3 className={css.tooltipContentTitle}>
69
- <FileName as="code" name={data.label} />
61
+ <Stack space="small" className={css.leafTooltipContent}>
62
+ <h3 className={css.leafTooltipContentTitle}>
63
+ <FileName as="code" name={item.label} />
70
64
  </h3>
71
65
  <RunInfo
72
66
  current={currentRun.displayValue}
@@ -78,7 +72,77 @@ const TooltipContent = (props: TooltipContentProps) => {
78
72
  );
79
73
  };
80
74
 
81
- const TileButton = forwardRef((props: React.ComponentProps<'button'>, ref) => (
75
+ interface LeafContentProps {
76
+ sizeDisplay: 'minimal' | 'small' | 'default';
77
+ item: ReportMetricRow;
78
+ runInfo: MetricRunInfo;
79
+ }
80
+
81
+ const LeafContent = forwardRef((props: LeafContentProps, ref: React.Ref<HTMLDivElement>) => {
82
+ const { sizeDisplay, item, runInfo } = props;
83
+
84
+ if (sizeDisplay === 'minimal') {
85
+ return <div className={css.leafContent} ref={ref} />;
86
+ }
87
+
88
+ return (
89
+ <div className={css.leafContent} ref={ref}>
90
+ <FileName as="p" className={css.leadContentLabel} name={item.label} />
91
+ {sizeDisplay !== 'small' && (
92
+ <p className={css.leadContentValue}>
93
+ <span className={css.leadContentMetric}>{runInfo.displayValue}</span>
94
+ <Delta
95
+ className={css.leadContentDelta}
96
+ displayValue={runInfo.displayDeltaPercentage}
97
+ deltaType={runInfo.deltaType}
98
+ />
99
+ </p>
100
+ )}
101
+ </div>
102
+ );
103
+ });
104
+
105
+ const LeafContentWithTooltip = (props: LeafContentProps & { parentRef: RefObject<Element> }) => {
106
+ const { sizeDisplay, item, runInfo, parentRef } = props;
107
+
108
+ const pointer = useMouseHovered(parentRef, { whenHovered: true });
109
+
110
+ // Return custom rect based on the mouse position
111
+ const getAnchorRect = useCallback(() => {
112
+ // Skip custom component rect area if the pointer position is empty
113
+ if (pointer.docX === 0 && pointer.docY === 0 && pointer.elW === 0 && pointer.elH === 0) {
114
+ return null;
115
+ }
116
+
117
+ const newRect = {
118
+ x: pointer.docX - (document?.defaultView?.scrollX ?? 0),
119
+ y: pointer.docY - (document?.defaultView?.scrollY ?? 0),
120
+ w: 1,
121
+ h: 1,
122
+ };
123
+
124
+ return newRect;
125
+ }, [pointer.docX, pointer.docY]);
126
+
127
+ const tooltipState = useTooltipState({ gutter: 8, getAnchorRect, timeout: 120 });
128
+
129
+ // Update tooltip position when poiner values are changing
130
+ useDebounce(tooltipState.render, 10, [pointer.docX, pointer.docY]);
131
+
132
+ return (
133
+ <>
134
+ <TooltipAnchor state={tooltipState} className={css.leafContentTooltipAnchor}>
135
+ <LeafContent sizeDisplay={sizeDisplay} item={item} runInfo={runInfo} />
136
+ </TooltipAnchor>
137
+ <Tooltip state={tooltipState} className={css.tooltip}>
138
+ <TooltipArrow state={tooltipState} size={16} className={css.leafTooltipArrow} />
139
+ <LeafTooltipContent item={item} />
140
+ </Tooltip>
141
+ </>
142
+ );
143
+ };
144
+
145
+ const LeafAction = forwardRef((props: React.ComponentProps<'button'>, ref) => (
82
146
  // @ts-expect-error
83
147
  <button type="button" xmlns="http://www.w3.org/1999/xhtml" ref={ref} {...props} />
84
148
  ));
@@ -105,98 +169,60 @@ interface LeafProps {
105
169
  onClick?: (id: string) => void;
106
170
  }
107
171
 
108
- const Tile = (props: LeafProps) => {
172
+ const Leaf = (props: LeafProps) => {
109
173
  const { x, y, width, height, data, onClick } = props;
174
+
110
175
  const { item } = data;
111
176
  const runInfo = item.runs?.[0] as MetricRunInfo;
112
177
 
113
- const [isOpen, setIsOpen] = useState(false);
114
- const arrowRef = useRef(null);
115
- const { refs, floatingStyles, context, middlewareData } = useFloating({
116
- placement: 'top',
117
- open: isOpen,
118
- onOpenChange: setIsOpen,
119
- middleware: [arrow({ element: arrowRef }), offset(16)],
120
- });
121
- const clientPoint = useClientPoint(context);
122
- const hover = useHover(context);
123
- const { getReferenceProps, getFloatingProps } = useInteractions([hover, clientPoint]);
124
-
125
- const tileSizeDisplay = useMemo(() => resolveTileSizeDisplay(width, height), [width, height]);
126
-
127
- const handleOnClick = useCallback(() => {
128
- if (onClick) {
129
- onClick(item.key);
130
- }
131
- }, [item.key]);
178
+ const sizeDisplay = useMemo(() => resolveLeafSizeDisplay(width, height), [width, height]);
179
+ const handleOnClick = useCallback(() => onClick?.(item.key), [item.key, onClick]);
180
+
181
+ const leafContentRef = useRef<HTMLElement>(null);
182
+ const hover = useHoverDirty(leafContentRef);
183
+
184
+ const leafClassName = cx(
185
+ css.leaf,
186
+ css[`leaf-${runInfo.deltaType}`],
187
+ sizeDisplay === 'small' && css.leafSmall,
188
+ );
132
189
 
133
190
  return (
134
- <g
135
- className={cx(
136
- css.tile,
137
- !item.changed && css['tile-NO_CHANGE'],
138
- css[`tile-${runInfo.deltaType}`],
139
- tileSizeDisplay === 'small' && css.tileSmall,
140
- )}
141
- >
142
- <rect x={x} y={y} width={width} height={height} className={css.tileOverlay} />
143
- <foreignObject height={height} width={width} x={x} y={y}>
144
- <TileButton
145
- onClick={handleOnClick}
146
- className={css.tileContent}
147
- ref={refs.setReference}
148
- {...getReferenceProps()}
149
- >
150
- {tileSizeDisplay !== 'minimal' && (
151
- <div className={css.tileContentWrapper}>
152
- <p className={css.tileContentLabel}>{item.label}</p>
153
- <p className={css.tileContentValue}>
154
- <span className={css.tileContentMetric}>{runInfo.displayValue}</span>
155
- <Delta
156
- className={css.tileContentDelta}
157
- displayValue={runInfo.displayDeltaPercentage}
158
- deltaType={runInfo.deltaType}
159
- />
160
- </p>
161
- </div>
191
+ <g className={leafClassName}>
192
+ <rect x={x} y={y} width={width} height={height} className={css.leafBackdrop} />
193
+ <foreignObject height={height} width={width} x={x} y={y} className={css.leafWrapper}>
194
+ <LeafAction onClick={handleOnClick} ref={leafContentRef} className={css.leafAction}>
195
+ {hover ? (
196
+ <LeafContentWithTooltip
197
+ sizeDisplay={sizeDisplay}
198
+ item={item}
199
+ runInfo={runInfo}
200
+ parentRef={leafContentRef}
201
+ />
202
+ ) : (
203
+ <LeafContent sizeDisplay={sizeDisplay} item={item} runInfo={runInfo} />
162
204
  )}
163
- </TileButton>
164
- {isOpen && (
165
- <FloatingPortal>
166
- <div
167
- ref={refs.setFloating}
168
- style={floatingStyles}
169
- {...getFloatingProps()}
170
- className={css.tooltip}
171
- >
172
- <div
173
- ref={arrowRef}
174
- style={{ left: middlewareData.arrow?.x }}
175
- className={css.tooltipArrow}
176
- />
177
- <TooltipContent data={item} />
178
- </div>
179
- </FloatingPortal>
180
- )}
205
+ </LeafAction>
181
206
  </foreignObject>
182
207
  </g>
183
208
  );
184
209
  };
185
210
 
186
- interface MetricsTreemapProps {
211
+ interface UseMetricsTreemapHierarchyParams {
187
212
  items: Array<ReportMetricRow>;
188
- emptyMessage?: React.ReactNode;
189
- onItemClick?: (entryId: string) => void;
213
+ width: number;
214
+ height: number;
190
215
  }
191
216
 
192
- export const MetricsTreemap = (props: MetricsTreemapProps & React.ComponentProps<'div'>) => {
193
- const { className = '', items, onItemClick, emptyMessage = 'No data', ...restProps } = props;
194
-
195
- const [containerRef, { width, height }] = useMeasure<HTMLDivElement>();
217
+ function useMetricsTreemapHierarchy(params: UseMetricsTreemapHierarchyParams) {
218
+ const { items, width, height } = params;
196
219
 
197
220
  const treemapHierarchy = useMemo(() => {
198
221
  const children = items.map((item) => {
199
- // set tile value to the largest run value to display the impact of deleted items
222
+ /**
223
+ * Set leaf value to the largest run value to allow to
224
+ * display the impact of deleted / items
225
+ */
200
226
  const values = item.runs.map((run) => run?.value || 0);
201
227
  const value = Math.max(...values);
202
228
 
@@ -234,29 +260,39 @@ export const MetricsTreemap = (props: MetricsTreemapProps & React.ComponentProps
234
260
  return tremapLayout.leaves().filter((leaf) => Boolean(leaf.parent));
235
261
  }, [height, width, treemapHierarchy]);
236
262
 
263
+ return cells;
264
+ }
265
+
266
+ interface MetricsTreemapProps {
267
+ items: Array<ReportMetricRow>;
268
+ emptyMessage?: React.ReactNode;
269
+ onItemClick?: (entryId: string) => void;
270
+ }
271
+
272
+ export const MetricsTreemap = (props: MetricsTreemapProps & React.ComponentProps<'div'>) => {
273
+ const { className = '', items, onItemClick, emptyMessage = 'No data', ...restProps } = props;
274
+
275
+ const [containerRef, { width, height }] = useMeasure<HTMLDivElement>();
276
+ const cells = useMetricsTreemapHierarchy({ items, width, height });
277
+
237
278
  return (
238
279
  <div className={cx(css.root, className)} {...restProps} ref={containerRef}>
239
280
  {cells.length > 0 ? (
240
- <svg
241
- width={width}
242
- height={height}
243
- viewBox={`0 0 ${width} ${height}`}
244
- className={css.canvas}
245
- >
281
+ <svg viewBox={`0 0 ${width} ${height}`} className={css.canvas}>
246
282
  {cells.map((leaf) => {
247
283
  if (!leaf.parent) {
248
284
  return null;
249
285
  }
250
286
 
251
287
  return (
252
- <Tile
288
+ <Leaf
253
289
  x={leaf.x0}
254
290
  y={leaf.y0}
255
291
  width={leaf.x1 - leaf.x0}
256
292
  height={leaf.y1 - leaf.y0}
257
293
  data={leaf.data as TreeNode}
258
- key={leaf.id}
259
294
  onClick={onItemClick}
295
+ key={leaf.id}
260
296
  />
261
297
  );
262
298
  })}
@@ -0,0 +1,19 @@
1
+ import { useCallback } from 'react';
2
+
3
+ interface UseEntryInfoParams {
4
+ setState: (data: { entryId: string }) => void;
5
+ }
6
+
7
+ type UseEntryInfo = [() => void, (entryId: string) => void];
8
+
9
+ /**
10
+ * Show/hide side panel entry
11
+ */
12
+ export function useEntryInfo(params: UseEntryInfoParams): UseEntryInfo {
13
+ const { setState } = params;
14
+
15
+ const hideEntryInfo = useCallback(() => setState({ entryId: '' }), [setState]);
16
+ const showEntryInfo = useCallback((entryId: string) => setState({ entryId }), [setState]);
17
+
18
+ return [hideEntryInfo, showEntryInfo];
19
+ }
@@ -30,7 +30,7 @@ export const useComponentQueryState = (componentName: string) => {
30
30
 
31
31
  setQueryState({ [componentName]: newState });
32
32
  },
33
- [componentState],
33
+ [componentState, setQueryState],
34
34
  );
35
35
 
36
36
  return [componentState, setState];
@@ -256,6 +256,7 @@ a.root {
256
256
  .small .glyph {
257
257
  width: calc(var(--space-xsmall) + 2px);
258
258
  height: calc(var(--space-xsmall) + 2px);
259
+ margin: 1px; /* compensate to keep the same height when the text is not provided */
259
260
  }
260
261
 
261
262
  .medium {
@@ -0,0 +1,8 @@
1
+ interface UseEntryInfoParams {
2
+ setState: (data: {
3
+ entryId: string;
4
+ }) => void;
5
+ }
6
+ type UseEntryInfo = [() => void, (entryId: string) => void];
7
+ export declare function useEntryInfo(params: UseEntryInfoParams): UseEntryInfo;
8
+ export {};
@@ -1,13 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.useShowEntry = void 0;
4
- const react_1 = require("react");
5
- const useShowEntry = (params) => {
6
- const { setState } = params;
7
- return (0, react_1.useMemo)(() => ({
8
- hideEntryInfo: () => setState({ entryId: '' }),
9
- showEntryInfo: (entryId) => setState({ entryId }),
10
- }), [setState]);
11
- };
12
- exports.useShowEntry = useShowEntry;
13
- //# sourceMappingURL=show-entry.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"show-entry.js","sourceRoot":"","sources":["../../src/hooks/show-entry.ts"],"names":[],"mappings":";;;AAAA,iCAAgC;AAMzB,MAAM,YAAY,GAAG,CAAC,MAAwB,EAAE,EAAE;IACvD,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC;IAE5B,OAAO,IAAA,eAAO,EACZ,GAAG,EAAE,CAAC,CAAC;QACL,aAAa,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;QAC9C,aAAa,EAAE,CAAC,OAAe,EAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;KAC1D,CAAC,EACF,CAAC,QAAQ,CAAC,CACX,CAAC;AACJ,CAAC,CAAC;AAVW,QAAA,YAAY,gBAUvB"}
@@ -1,9 +0,0 @@
1
- import { useMemo } from 'react';
2
- export const useShowEntry = (params) => {
3
- const { setState } = params;
4
- return useMemo(() => ({
5
- hideEntryInfo: () => setState({ entryId: '' }),
6
- showEntryInfo: (entryId) => setState({ entryId }),
7
- }), [setState]);
8
- };
9
- //# sourceMappingURL=show-entry.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"show-entry.js","sourceRoot":"","sources":["../../src/hooks/show-entry.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,CAAC;AAMhC,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,MAAwB,EAAE,EAAE;IACvD,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,CAAC;IAE5B,OAAO,OAAO,CACZ,GAAG,EAAE,CAAC,CAAC;QACL,aAAa,EAAE,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,EAAE,EAAE,CAAC;QAC9C,aAAa,EAAE,CAAC,OAAe,EAAE,EAAE,CAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,CAAC;KAC1D,CAAC,EACF,CAAC,QAAQ,CAAC,CACX,CAAC;AACJ,CAAC,CAAC"}
@@ -1,17 +0,0 @@
1
- import { useMemo } from 'react';
2
-
3
- interface UseEntryIdParams {
4
- setState: (data: { entryId: string }) => void;
5
- }
6
-
7
- export const useShowEntry = (params: UseEntryIdParams) => {
8
- const { setState } = params;
9
-
10
- return useMemo(
11
- () => ({
12
- hideEntryInfo: () => setState({ entryId: '' }),
13
- showEntryInfo: (entryId: string) => setState({ entryId }),
14
- }),
15
- [setState],
16
- );
17
- };
@@ -1,10 +0,0 @@
1
- interface UseEntryIdParams {
2
- setState: (data: {
3
- entryId: string;
4
- }) => void;
5
- }
6
- export declare const useShowEntry: (params: UseEntryIdParams) => {
7
- hideEntryInfo: () => void;
8
- showEntryInfo: (entryId: string) => void;
9
- };
10
- export {};