@automattic/charts 1.10.3 → 1.11.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 (30) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/TOKENS.md +72 -0
  3. package/dist/index.cjs +143 -42
  4. package/dist/index.cjs.map +1 -1
  5. package/dist/index.css +54 -41
  6. package/dist/index.d.cts +17 -7
  7. package/dist/index.d.ts +17 -7
  8. package/dist/index.js +143 -42
  9. package/dist/index.js.map +1 -1
  10. package/package.json +1 -1
  11. package/src/charts/conversion-funnel-chart/conversion-funnel-chart.module.scss +7 -7
  12. package/src/charts/heatmap-chart/heatmap-chart.module.scss +18 -13
  13. package/src/charts/heatmap-chart/heatmap-chart.tsx +11 -7
  14. package/src/charts/heatmap-chart/private/heatmap-legend.tsx +3 -3
  15. package/src/charts/heatmap-chart/test/heatmap-chart.test.tsx +13 -7
  16. package/src/charts/leaderboard-chart/hooks/index.ts +1 -0
  17. package/src/charts/leaderboard-chart/hooks/use-fitted-row-count.ts +115 -0
  18. package/src/charts/leaderboard-chart/leaderboard-chart.module.scss +39 -12
  19. package/src/charts/leaderboard-chart/leaderboard-chart.tsx +67 -14
  20. package/src/charts/leaderboard-chart/test/leaderboard-chart-fit-rows.test.tsx +365 -0
  21. package/src/charts/leaderboard-chart/test/leaderboard-chart.test.tsx +73 -2
  22. package/src/charts/leaderboard-chart/types.ts +12 -0
  23. package/src/charts/line-chart/line-chart.module.scss +2 -2
  24. package/src/charts/private/grid-control/grid-control.module.scss +1 -1
  25. package/src/charts/private/svg-empty-state/svg-empty-state.module.scss +1 -1
  26. package/src/charts/private/x-zoom.module.scss +12 -10
  27. package/src/components/tooltip/base-tooltip.module.scss +3 -2
  28. package/src/components/trend-indicator/trend-indicator.module.scss +6 -5
  29. package/src/providers/chart-context/themes.ts +33 -19
  30. package/src/types.ts +2 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automattic/charts",
3
- "version": "1.10.3",
3
+ "version": "1.11.0",
4
4
  "description": "Display charts within Automattic products.",
5
5
  "homepage": "https://github.com/Automattic/jetpack/tree/HEAD/projects/js-packages/charts/#readme",
6
6
  "bugs": {
@@ -12,7 +12,7 @@
12
12
 
13
13
  .main-rate {
14
14
  overflow: hidden;
15
- color: var(--wpds-color-foreground-content-neutral, #1e1e1e);
15
+ color: var(--a8c-charts-color-label, var(--wpds-color-foreground-content-neutral, #1e1e1e));
16
16
  text-overflow: ellipsis;
17
17
  font-size: var(--wpds-typography-font-size-xl, 18px);
18
18
  font-style: normal;
@@ -54,7 +54,7 @@
54
54
  }
55
55
 
56
56
  .step-label {
57
- color: var(--wpds-color-foreground-content-neutral-weak, #707070);
57
+ color: var(--a8c-charts-color-label-secondary, var(--wpds-color-foreground-content-neutral-weak, #707070));
58
58
  font-size: var(--wpds-typography-font-size-sm, 12px);
59
59
  font-weight: var(--wpds-typography-font-weight-regular, 400);
60
60
  line-height: var(--wpds-typography-line-height-xs, 16px);
@@ -64,7 +64,7 @@
64
64
  }
65
65
 
66
66
  .step-rate {
67
- color: var(--wpds-color-foreground-content-neutral, #1e1e1e);
67
+ color: var(--a8c-charts-color-label, var(--wpds-color-foreground-content-neutral, #1e1e1e));
68
68
  font-size: var(--wpds-typography-font-size-md, 13px);
69
69
  font-weight: var(--wpds-typography-font-weight-medium, 499);
70
70
  line-height: var(--wpds-typography-line-height-xs, 16px);
@@ -100,7 +100,7 @@
100
100
  }
101
101
 
102
102
  .tooltip-wrapper {
103
- background: var(--wpds-color-background-surface-neutral-strong, #fff);
103
+ background: var(--a8c-charts-color-background, var(--wpds-color-background-surface-neutral-strong, #fff));
104
104
 
105
105
  // Override .visx-tooltip inline styles.
106
106
  border-radius: var(--wpds-border-radius-md, 4px) !important;
@@ -110,7 +110,7 @@
110
110
  }
111
111
 
112
112
  .tooltip-title {
113
- color: var(--wpds-color-foreground-content-neutral, #1e1e1e);
113
+ color: var(--a8c-charts-color-label, var(--wpds-color-foreground-content-neutral, #1e1e1e));
114
114
  font-size: var(--wpds-typography-font-size-sm, 12px);
115
115
  font-style: normal;
116
116
  font-weight: var(--wpds-typography-font-weight-regular, 400);
@@ -118,7 +118,7 @@
118
118
  }
119
119
 
120
120
  .tooltip-content {
121
- color: var(--wpds-color-foreground-content-neutral, #1e1e1e);
121
+ color: var(--a8c-charts-color-label, var(--wpds-color-foreground-content-neutral, #1e1e1e));
122
122
  font-size: var(--wpds-typography-font-size-md, 13px);
123
123
  font-style: normal;
124
124
  font-weight: var(--wpds-typography-font-weight-medium, 499);
@@ -127,6 +127,6 @@
127
127
 
128
128
  .empty-state {
129
129
  text-align: center;
130
- color: var(--wpds-color-foreground-content-neutral-weak, #707070);
130
+ color: var(--a8c-charts-color-label-secondary, var(--wpds-color-foreground-content-neutral-weak, #707070));
131
131
  font-size: var(--wpds-typography-font-size-lg, 16px);
132
132
  }
@@ -1,3 +1,8 @@
1
+ // Focus-ring width shared across the grid and cell-selected focus outlines. A
2
+ // Sass var, not a custom property: a compile-time constant scoped to this
3
+ // module, so it needs no runtime cascade and never lands on the DOM.
4
+ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-focus, 2px));
5
+
1
6
  .heatmap-chart {
2
7
  width: 100%;
3
8
  height: 100%;
@@ -14,14 +19,14 @@
14
19
 
15
20
  .heatmap-chart__empty {
16
21
  padding: var(--wpds-dimension-padding-lg, 16px);
17
- color: var(--wpds-color-foreground-content-neutral-weak, #707070);
22
+ color: var(--a8c-charts-color-label-secondary, var(--wpds-color-foreground-content-neutral-weak, #707070));
18
23
  }
19
24
 
20
25
  // Track template is set inline because CSS `repeat()` won't take a `var()`
21
26
  // count. ARIA rows are `display: contents` so their cells join this grid.
22
27
  .heatmap-chart__grid {
23
28
  display: grid;
24
- gap: var(--heatmap-cell-gap, var(--wpds-dimension-gap-xs, 4px));
29
+ gap: var(--a8c-charts-dimension-heatmap-cell-gap, var(--wpds-dimension-gap-xs, 4px));
25
30
  flex: 1 1 0;
26
31
  width: 100%;
27
32
  // Override flex's auto minimum so short widgets do not scroll.
@@ -29,8 +34,8 @@
29
34
 
30
35
  &:focus-visible {
31
36
  outline:
32
- var(--wpds-border-width-focus, var(--wp-admin-border-width-focus, 2px)) solid
33
- var(--wpds-color-stroke-focus, var(--wp-admin-theme-color, #3858e9));
37
+ $focus-ring-width solid
38
+ var(--a8c-charts-color-focus, var(--wpds-color-stroke-focus, var(--wp-admin-theme-color, #3858e9)));
34
39
  outline-offset: 2px;
35
40
  border-radius: var(--wpds-border-radius-sm, 2px);
36
41
  }
@@ -58,7 +63,7 @@
58
63
 
59
64
  .heatmap-chart__col-label,
60
65
  .heatmap-chart__row-label {
61
- color: var(--wpds-color-foreground-content-neutral-weak, #707070);
66
+ color: var(--a8c-charts-color-label-secondary, var(--wpds-color-foreground-content-neutral-weak, #707070));
62
67
  font-size: var(--wpds-typography-font-size-xs, 11px);
63
68
  white-space: nowrap;
64
69
  overflow: visible;
@@ -84,7 +89,7 @@
84
89
  min-height: 0;
85
90
  border-radius: var(--wpds-border-radius-sm, 2px);
86
91
  // Empty (no-data) default; cells with a value override it below.
87
- background: var(--wpds-color-background-track-neutral-weak, #f0f0f0);
92
+ background: var(--a8c-charts-color-track, var(--wpds-color-background-track-neutral-weak, #f0f0f0));
88
93
  }
89
94
 
90
95
  // An empty grid slot (calendar ragged edges): keeps its track but paints and
@@ -97,30 +102,30 @@
97
102
  // Floor the mix at 15% so the lowest value stays visibly tinted, distinct from
98
103
  // an empty cell.
99
104
  .heatmap-chart__cell--filled {
100
- background: color-mix(in sRGB, var(--heatmap-primary) calc((0.15 + 0.85 * var(--intensity)) * 100%), var(--heatmap-bg, #fff));
105
+ background: color-mix(in sRGB, var(--a8c-charts-color-heatmap-primary) calc((0.15 + 0.85 * var(--a8c-charts-heatmap-cell-intensity)) * 100%), var(--a8c-charts-color-heatmap-background, #fff));
101
106
  }
102
107
 
103
108
  .heatmap-chart__cell--selected {
104
109
  outline:
105
- var(--wpds-border-width-focus, var(--wp-admin-border-width-focus, 2px)) solid
106
- var(--wpds-color-stroke-focus, var(--wp-admin-theme-color, #3858e9));
107
- outline-offset: calc(-1 * var(--wpds-border-width-focus, var(--wp-admin-border-width-focus, 2px)));
110
+ $focus-ring-width solid
111
+ var(--a8c-charts-color-focus, var(--wpds-color-stroke-focus, var(--wp-admin-theme-color, #3858e9)));
112
+ outline-offset: calc(-1 * #{$focus-ring-width});
108
113
  }
109
114
 
110
115
  .heatmap-chart__cell-value {
111
- color: var(--wpds-color-foreground-content-neutral, #1e1e1e);
116
+ color: var(--a8c-charts-color-label, var(--wpds-color-foreground-content-neutral, #1e1e1e));
112
117
  font-size: var(--wpds-typography-font-size-md, 13px);
113
118
  }
114
119
 
115
120
  // Light text on fills dark enough to out-contrast dark text (decided in JS from
116
121
  // the blended fill luminance).
117
122
  .heatmap-chart__cell--strong .heatmap-chart__cell-value {
118
- color: var(--wpds-color-foreground-interactive-neutral-strong, #f0f0f0);
123
+ color: var(--a8c-charts-color-label-inverse, var(--wpds-color-foreground-interactive-neutral-strong, #f0f0f0));
119
124
  }
120
125
 
121
126
  .heatmap-chart__legend-swatch {
122
127
  width: var(--wpds-dimension-size-3xs, 12px);
123
128
  height: var(--wpds-dimension-size-3xs, 12px);
124
129
  border-radius: var(--wpds-border-radius-sm, 2px);
125
- background: color-mix(in sRGB, var(--heatmap-primary) calc((0.15 + 0.85 * var(--intensity)) * 100%), var(--heatmap-bg, #fff));
130
+ background: color-mix(in sRGB, var(--a8c-charts-color-heatmap-primary) calc((0.15 + 0.85 * var(--a8c-charts-heatmap-cell-intensity)) * 100%), var(--a8c-charts-color-heatmap-background, #fff));
126
131
  }
@@ -277,20 +277,20 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
277
277
  // and a min floor makes the grid overflow (for a scrollable wrapper)
278
278
  // rather than crushing cells on long ranges.
279
279
  const columnTrack = compact
280
- ? 'var(--heatmap-cell-size)'
280
+ ? 'var(--a8c-charts-dimension-heatmap-cell-size)'
281
281
  : `minmax(${ minCellWidth ?? 0 }px, ${ maxCellWidth ? `${ maxCellWidth }px` : '1fr' })`;
282
282
  const rowTrack = compact
283
- ? 'var(--heatmap-cell-size)'
283
+ ? 'var(--a8c-charts-dimension-heatmap-cell-size)'
284
284
  : `minmax(${ minCellHeight ?? 0 }px, ${ maxCellHeight ? `${ maxCellHeight }px` : '1fr' })`;
285
285
  const gridStyle: Record< string, string | number > = {
286
- '--heatmap-primary': primaryColorHex,
287
- '--heatmap-bg': theme.backgroundColor,
286
+ '--a8c-charts-color-heatmap-primary': primaryColorHex,
287
+ '--a8c-charts-color-heatmap-background': theme.backgroundColor,
288
288
  gridTemplateColumns: `auto repeat(${ columns }, ${ columnTrack })`,
289
289
  gridTemplateRows: `auto repeat(${ rows }, ${ rowTrack })`,
290
290
  };
291
291
  if ( compact ) {
292
- gridStyle[ '--heatmap-cell-gap' ] = `${ compactCellGap }px`;
293
- gridStyle[ '--heatmap-cell-size' ] = `${ compactCellSize }px`;
292
+ gridStyle[ '--a8c-charts-dimension-heatmap-cell-gap' ] = `${ compactCellGap }px`;
293
+ gridStyle[ '--a8c-charts-dimension-heatmap-cell-size' ] = `${ compactCellSize }px`;
294
294
  }
295
295
 
296
296
  const activeDescendant =
@@ -419,7 +419,11 @@ const HeatmapChartInternal: FC< HeatmapChartProps > = ( {
419
419
  selectedIndex === flatIndex,
420
420
  } ) }
421
421
  style={
422
- present ? ( { '--intensity': normalized } as CSSProperties ) : undefined
422
+ present
423
+ ? ( {
424
+ '--a8c-charts-heatmap-cell-intensity': normalized,
425
+ } as CSSProperties )
426
+ : undefined
423
427
  }
424
428
  onMouseMove={ handleCellMouseMove }
425
429
  onMouseLeave={ handleCellMouseLeave }
@@ -37,9 +37,9 @@ export const HeatmapLegend: FC< HeatmapLegendProps > = ( { steps = 5, lessLabel,
37
37
  className={ styles[ 'heatmap-chart__legend-swatch' ] }
38
38
  style={
39
39
  {
40
- '--heatmap-primary': primaryColorHex,
41
- '--heatmap-bg': backgroundColor,
42
- '--intensity': intensity,
40
+ '--a8c-charts-color-heatmap-primary': primaryColorHex,
41
+ '--a8c-charts-color-heatmap-background': backgroundColor,
42
+ '--a8c-charts-heatmap-cell-intensity': intensity,
43
43
  } as CSSProperties
44
44
  }
45
45
  />
@@ -285,7 +285,7 @@ describe( 'HeatmapChart', () => {
285
285
  renderChart();
286
286
  const grid = screen.getByRole( 'grid', { name: /heatmap/i } );
287
287
  // Non-compact sets no inline gap — the SCSS falls back to the WPDS token.
288
- expect( grid.style.getPropertyValue( '--heatmap-cell-gap' ) ).toBe( '' );
288
+ expect( grid.style.getPropertyValue( '--a8c-charts-dimension-heatmap-cell-gap' ) ).toBe( '' );
289
289
  } );
290
290
 
291
291
  test( 'applies the compact gap inline from the theme compactCellGap', () => {
@@ -295,7 +295,9 @@ describe( 'HeatmapChart', () => {
295
295
  </GlobalChartsProvider>
296
296
  );
297
297
  const grid = screen.getByRole( 'grid', { name: /heatmap/i } );
298
- expect( grid.style.getPropertyValue( '--heatmap-cell-gap' ) ).toBe( '3px' );
298
+ expect( grid.style.getPropertyValue( '--a8c-charts-dimension-heatmap-cell-gap' ) ).toBe(
299
+ '3px'
300
+ );
299
301
  } );
300
302
 
301
303
  test( 'sizes compact cells to the theme compactCellSize', () => {
@@ -305,9 +307,13 @@ describe( 'HeatmapChart', () => {
305
307
  </GlobalChartsProvider>
306
308
  );
307
309
  const grid = screen.getByRole( 'grid', { name: /heatmap/i } );
308
- expect( grid.style.getPropertyValue( '--heatmap-cell-size' ) ).toBe( '20px' );
310
+ expect( grid.style.getPropertyValue( '--a8c-charts-dimension-heatmap-cell-size' ) ).toBe(
311
+ '20px'
312
+ );
309
313
  // Compact track template is built from the fixed cell size.
310
- expect( grid.style.gridTemplateColumns ).toContain( 'var(--heatmap-cell-size)' );
314
+ expect( grid.style.gridTemplateColumns ).toContain(
315
+ 'var(--a8c-charts-dimension-heatmap-cell-size)'
316
+ );
311
317
  } );
312
318
 
313
319
  test( 'caps cell width without changing the normal vertical layout', () => {
@@ -343,7 +349,7 @@ describe( 'HeatmapChart', () => {
343
349
  test( 'applies the primaryColor prop as the cell-scale color', () => {
344
350
  renderChart( { primaryColor: '#abcdef' } );
345
351
  const grid = screen.getByRole( 'grid', { name: /heatmap/i } );
346
- expect( grid.style.getPropertyValue( '--heatmap-primary' ) ).toBe( '#abcdef' );
352
+ expect( grid.style.getPropertyValue( '--a8c-charts-color-heatmap-primary' ) ).toBe( '#abcdef' );
347
353
  } );
348
354
 
349
355
  test( 'resolves primaryColor from the chart theme', () => {
@@ -353,7 +359,7 @@ describe( 'HeatmapChart', () => {
353
359
  </GlobalChartsProvider>
354
360
  );
355
361
  const grid = screen.getByRole( 'grid', { name: /heatmap/i } );
356
- expect( grid.style.getPropertyValue( '--heatmap-primary' ) ).toBe( '#0a0b0c' );
362
+ expect( grid.style.getPropertyValue( '--a8c-charts-color-heatmap-primary' ) ).toBe( '#0a0b0c' );
357
363
  } );
358
364
 
359
365
  test( 'falls back to the palette colors[0] when no prop or theme primaryColor is set', () => {
@@ -363,7 +369,7 @@ describe( 'HeatmapChart', () => {
363
369
  </GlobalChartsProvider>
364
370
  );
365
371
  const grid = screen.getByRole( 'grid', { name: /heatmap/i } );
366
- expect( grid.style.getPropertyValue( '--heatmap-primary' ) ).toBe( '#0a0b0c' );
372
+ expect( grid.style.getPropertyValue( '--a8c-charts-color-heatmap-primary' ) ).toBe( '#0a0b0c' );
367
373
  } );
368
374
 
369
375
  test( 'the unresponsive export pins explicit width and height', () => {
@@ -1 +1,2 @@
1
+ export { useFittedRowCount, SUBPIXEL_TOLERANCE } from './use-fitted-row-count';
1
2
  export { useLeaderboardLegendItems } from './use-leaderboard-legend-items';
@@ -0,0 +1,115 @@
1
+ import { useCallback, useLayoutEffect, useRef, useState } from 'react';
2
+
3
+ /**
4
+ * Fractional layout rounding can put a row's bottom a hair past the container's
5
+ * without any visible clipping. Allow that much slack before hiding a row.
6
+ *
7
+ * Exported so tests and stories assert against the real tolerance rather than
8
+ * restating it.
9
+ */
10
+ export const SUBPIXEL_TOLERANCE = 0.5;
11
+
12
+ /**
13
+ * Counts how many leading rows fit inside the content container.
14
+ *
15
+ * Rows are read from the DOM rather than derived from constants: row height
16
+ * depends on the theme's row gap, label wrapping, and whatever a caller renders
17
+ * as a label, so any restated number would drift.
18
+ *
19
+ * Every row is a direct grid child marked with `data-row-index`; interactive rows
20
+ * use a button wrapper and non-interactive rows use a div wrapper.
21
+ *
22
+ * @param enabled - Whether to measure at all. When false every row fits.
23
+ * @param rowCount - Total number of rows rendered.
24
+ * @param data - Rendered row data, whose geometry may change without changing the count.
25
+ * @return Ref for the content container and the number of leading rows that fit.
26
+ */
27
+ export function useFittedRowCount( enabled: boolean, rowCount: number, data: unknown ) {
28
+ const contentRef = useRef< HTMLDivElement | null >( null );
29
+ const [ fittedCount, setFittedCount ] = useState( rowCount );
30
+ const [ isMeasurable, setIsMeasurable ] = useState( true );
31
+
32
+ const measure = useCallback( () => {
33
+ const content = contentRef.current;
34
+ if ( ! content ) {
35
+ return;
36
+ }
37
+
38
+ // Switching from the default scrollable mode can leave the content at a
39
+ // non-zero scroll offset, which would shift every measurement below.
40
+ content.scrollTop = 0;
41
+
42
+ const rows = content.querySelectorAll< HTMLElement >(
43
+ ':scope > [data-leaderboard-grid] > [data-row-index]'
44
+ );
45
+
46
+ const rowBottoms: number[] = [];
47
+ rows.forEach( row => {
48
+ const index = Number( row.getAttribute( 'data-row-index' ) );
49
+ // An unparseable index would write a non-numeric key, leaving a hole
50
+ // that silently truncates the scan below at the preceding row.
51
+ if ( ! Number.isInteger( index ) || index < 0 || index >= rowCount ) {
52
+ return;
53
+ }
54
+ const { bottom } = row.getBoundingClientRect();
55
+ rowBottoms[ index ] = Math.max( rowBottoms[ index ] ?? -Infinity, bottom );
56
+ } );
57
+
58
+ // Fail open. If the rows cannot be measured at all — a changed DOM shape,
59
+ // a detached container — falling back to the scrollable default keeps the
60
+ // data reachable, where hiding every row reads as a broken tile.
61
+ if ( rowBottoms.length === 0 ) {
62
+ setIsMeasurable( false );
63
+ setFittedCount( rowCount );
64
+ return;
65
+ }
66
+ setIsMeasurable( true );
67
+
68
+ const contentBottom = content.getBoundingClientRect().bottom + SUBPIXEL_TOLERANCE;
69
+
70
+ let fits = 0;
71
+ while ( fits < rowBottoms.length && rowBottoms[ fits ] <= contentBottom ) {
72
+ fits++;
73
+ }
74
+
75
+ // Bail before setState: setState inside a ResizeObserver callback is the
76
+ // usual way this grows a render loop.
77
+ setFittedCount( current => ( current === fits ? current : fits ) );
78
+ }, [ rowCount ] );
79
+
80
+ // Measure after row data changes so geometry updates are caught even when the
81
+ // row count and the grid's overall size stay the same (for example, one label
82
+ // wraps while another unwraps after a same-length data update).
83
+ useLayoutEffect( () => {
84
+ if ( ! enabled ) {
85
+ setFittedCount( rowCount );
86
+ return;
87
+ }
88
+
89
+ measure();
90
+ }, [ enabled, rowCount, data, measure ] );
91
+
92
+ useLayoutEffect( () => {
93
+ if ( ! enabled ) {
94
+ return;
95
+ }
96
+
97
+ const content = contentRef.current;
98
+ if ( ! content ) {
99
+ return;
100
+ }
101
+
102
+ const observer = new ResizeObserver( measure );
103
+ // The container catches tile resizing; the grid catches font, image,
104
+ // theme, or label changes that alter row geometry on an unchanged tile.
105
+ observer.observe( content );
106
+ const grid = content.querySelector< HTMLElement >( ':scope > [data-leaderboard-grid]' );
107
+ if ( grid ) {
108
+ observer.observe( grid );
109
+ }
110
+
111
+ return () => observer.disconnect();
112
+ }, [ enabled, rowCount, measure ] );
113
+
114
+ return { contentRef, fittedCount: enabled ? fittedCount : rowCount, isMeasurable };
115
+ }
@@ -16,6 +16,15 @@
16
16
  flex: 1;
17
17
  min-height: 0;
18
18
  overflow: auto;
19
+
20
+ // With fitRows the chart hides the rows that would not fit, so there is
21
+ // nothing left to scroll to — a scrollbar here would only ever reveal the
22
+ // hidden rows' reserved space.
23
+ &--fit {
24
+ overflow: hidden;
25
+ // Anchors .fitEmptyState, which is laid over the hidden rows.
26
+ position: relative;
27
+ }
19
28
  }
20
29
  }
21
30
 
@@ -51,7 +60,7 @@
51
60
  // row's fixed-pixel hover inset from collapsing a narrow bar to nothing.
52
61
  min-width: var(--wpds-dimension-size-5xs, 4px);
53
62
 
54
- border-radius: var(--a8c--charts--leaderboard--bar--border-radius, 9999px);
63
+ border-radius: var(--a8c-charts-border-radius-leaderboard-bar, var(--a8c--charts--leaderboard--bar--border-radius, 9999px));
55
64
  z-index: -1; /* places it behind the label */
56
65
 
57
66
  &--animated {
@@ -95,14 +104,32 @@
95
104
  .emptyState {
96
105
  padding: var(--wpds-dimension-padding-3xl, 32px) var(--wpds-dimension-padding-lg, 16px);
97
106
  text-align: center;
98
- color: var(--wpds-color-foreground-content-neutral-weak, #707070);
107
+ color: var(--a8c-charts-color-label-secondary, var(--wpds-color-foreground-content-neutral-weak, #707070));
99
108
  font-size: var(--wpds-typography-font-size-md, 13px);
100
109
  font-style: italic;
101
110
  }
102
111
 
103
- .row {
104
- --focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-focus, 2px));
112
+ // Shown when not even one row fits. The rows stay in the layout underneath so
113
+ // they remain measurable, so this is taken out of flow to sit over them.
114
+ .fitEmptyState {
115
+ position: absolute;
116
+ inset: 0;
117
+ display: flex;
118
+ align-items: center;
119
+ justify-content: center;
120
+ // The shared empty state reserves generous vertical whitespace. This overlay
121
+ // is specifically for compact containers, where the text may fit even though
122
+ // a complete data row does not.
123
+ padding-block: 0;
124
+ }
105
125
 
126
+ // Focus-ring width shared so a standard row's reserved padding and an
127
+ // interactive row's outline (and its inset offset) stay equal. A Sass var,
128
+ // not a custom property: a compile-time constant scoped to this module, so it
129
+ // needs no runtime cascade and never lands on the DOM as an unprefixed var.
130
+ $focus-ring-width: var(--wpds-border-width-focus, var(--wp-admin-border-width-focus, 2px));
131
+
132
+ .row {
106
133
  display: grid;
107
134
  grid-column: 1 / -1;
108
135
  grid-template-columns: subgrid;
@@ -110,7 +137,7 @@
110
137
  // Every row reserves the same space for the interactive focus ring. Keeping
111
138
  // this on the shared wrapper ensures that adding or removing an action never
112
139
  // changes the row height or subgrid column edges.
113
- padding: var(--focus-ring-width);
140
+ padding: $focus-ring-width;
114
141
  }
115
142
 
116
143
  .interactiveRow {
@@ -153,14 +180,14 @@
153
180
 
154
181
  // Fixed reserve for the chevron — the value slides by it in full, and the
155
182
  // bar width inset (see leaderboard-chart.tsx) scales it by the bar's share.
156
- --a8c--charts--leaderboard--bar--hover-inset: var(--wpds-dimension-gap-2xl, 32px);
183
+ --a8c-charts-dimension-leaderboard-bar-hover-inset: var(--wpds-dimension-gap-2xl, 32px);
157
184
 
158
185
  .bar {
159
186
  opacity: 0.5;
160
187
  }
161
188
 
162
189
  .valueContainer {
163
- transform: translateX(calc(var(--a8c--charts--leaderboard--bar--hover-inset) * -1));
190
+ transform: translateX(calc(var(--a8c-charts-dimension-leaderboard-bar-hover-inset) * -1));
164
191
  }
165
192
 
166
193
  .chevron {
@@ -171,18 +198,18 @@
171
198
  // In RTL the value slides toward the inline start the other way (rightward).
172
199
  [dir="rtl"] &:hover .valueContainer,
173
200
  [dir="rtl"] &:focus-visible .valueContainer {
174
- transform: translateX(var(--a8c--charts--leaderboard--bar--hover-inset));
201
+ transform: translateX(var(--a8c-charts-dimension-leaderboard-bar-hover-inset));
175
202
  }
176
203
 
177
204
  &:focus-visible {
178
205
  border-radius: var(--wpds-border-radius-sm, 2px);
179
206
  outline:
180
- var(--focus-ring-width) solid
181
- var(--wpds-color-stroke-focus, var(--wp-admin-theme-color, #3858e9));
207
+ $focus-ring-width solid
208
+ var(--a8c-charts-color-focus, var(--wpds-color-stroke-focus, var(--wp-admin-theme-color, #3858e9)));
182
209
  // Inset by its own width so the outer edge hugs the row boundary: the
183
210
  // outline then sits in the shared row padding, clear of the bar, and stays
184
211
  // inside the overflow:auto container that would clip an outset ring.
185
- outline-offset: calc(-1 * var(--focus-ring-width));
212
+ outline-offset: calc(-1 * #{$focus-ring-width});
186
213
  }
187
214
  }
188
215
 
@@ -193,7 +220,7 @@
193
220
  bottom: 0;
194
221
  margin-block: auto;
195
222
  opacity: 0;
196
- color: var(--wpds-color-foreground-content-neutral-weak, #707070);
223
+ color: var(--a8c-charts-color-label-secondary, var(--wpds-color-foreground-content-neutral-weak, #707070));
197
224
  transition:
198
225
  opacity var(--wpds-motion-duration-sm, 100ms)
199
226
  var(--wpds-motion-easing-subtle, cubic-bezier(0.15, 0, 0.15, 1));