@sybilion/uilib 1.0.26 → 1.0.28

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 (25) hide show
  1. package/assets/mini-app-global.css +12 -12
  2. package/dist/esm/components/ui/Chart/Chart.styl.js +1 -1
  3. package/dist/esm/components/ui/Chart/components/BaseChartWrapper.js +117 -170
  4. package/dist/esm/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.js +101 -1
  5. package/dist/esm/components/ui/ChartAreaInteractive/ChartAreaInteractive.js +12 -4
  6. package/dist/esm/index.js +1 -0
  7. package/dist/esm/mini-app/MiniAppRoot.js +9 -5
  8. package/dist/esm/mini-app/miniAppThemeConfig.js +40 -0
  9. package/dist/esm/types/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.d.ts +6 -1
  10. package/dist/esm/types/src/docs/contexts/theme-context.d.ts +1 -0
  11. package/dist/esm/types/src/mini-app/MiniAppRoot.d.ts +4 -1
  12. package/dist/esm/types/src/mini-app/index.d.ts +4 -2
  13. package/dist/esm/types/src/mini-app/miniAppThemeConfig.d.ts +3 -0
  14. package/docs/workspace-mini-apps.md +3 -1
  15. package/package.json +1 -1
  16. package/src/components/ui/Chart/Chart.styl +7 -4
  17. package/src/components/ui/Chart/components/BaseChartWrapper.tsx +156 -193
  18. package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.ts +90 -40
  19. package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.tsx +15 -3
  20. package/src/docs/contexts/theme-context.tsx +9 -1
  21. package/src/docs/pages/ChartAreaInteractivePage.tsx +27 -1
  22. package/src/docs/pages/MiniAppRootPage.tsx +6 -1
  23. package/src/mini-app/MiniAppRoot.tsx +19 -1
  24. package/src/mini-app/index.ts +4 -8
  25. package/src/mini-app/miniAppThemeConfig.ts +45 -0
@@ -0,0 +1,40 @@
1
+ import { ThemeHelpers, ThemeDefaults } from '@homecode/ui';
2
+
3
+ const { colors, getColors, getConfig } = ThemeDefaults;
4
+ const defaultPalette = getColors();
5
+ const colorsConfig = {
6
+ light: {
7
+ ...ThemeHelpers.colorsConfigToVars({
8
+ ...getColors({
9
+ accent: colors.dark,
10
+ decent: colors.light,
11
+ }),
12
+ }),
13
+ },
14
+ dark: {
15
+ ...ThemeHelpers.colorsConfigToVars({
16
+ ...getColors({
17
+ accent: colors.light,
18
+ decent: colors.dark,
19
+ }),
20
+ }),
21
+ },
22
+ };
23
+ /** Homecode `<Theme config={...}>` shape for workspace mini-apps (generic palette). */
24
+ function getDefaultMiniAppThemeConfig(isDarkMode) {
25
+ return {
26
+ ...getConfig(),
27
+ ...colorsConfig[isDarkMode ? 'dark' : 'light'],
28
+ ...ThemeHelpers.colorsConfigToVars({
29
+ active: {
30
+ color: '#00a9c7',
31
+ mods: {
32
+ // @ts-ignore — extend defaults so --active-color-alpha-* variants match Homecode
33
+ alpha: [0, 50, 100, 200, ...defaultPalette.active.mods.alpha],
34
+ },
35
+ },
36
+ }),
37
+ };
38
+ }
39
+
40
+ export { getDefaultMiniAppThemeConfig };
@@ -9,7 +9,12 @@ declare const timeRangeToMonths: {
9
9
  readonly All: 12;
10
10
  };
11
11
  export type TimeRange = keyof typeof timeRangeToMonths;
12
- export declare const filterDataForTimeRange: (data: ChartDataPoint[], currentTimeRange: TimeRange, availableAnalyses: number[]) => ChartDataPoint[];
12
+ export type FilterDataForTimeRangeOptions = {
13
+ /** When set (e.g. selected forecast on Forecast tab), the window ends at the
14
+ * latest point that has shared historical or that analysis — not at another run. */
15
+ endDateAnchorAnalysisId?: number | null;
16
+ };
17
+ export declare const filterDataForTimeRange: (data: ChartDataPoint[], currentTimeRange: TimeRange, options?: FilterDataForTimeRangeOptions) => ChartDataPoint[];
13
18
  export declare const shortDateFormatter: (value: string) => string;
14
19
  export declare const longDateFormatter: (value: string) => string;
15
20
  /**
@@ -4,5 +4,6 @@ export declare function ThemeProvider({ children }: {
4
4
  }): import("react/jsx-runtime").JSX.Element;
5
5
  export declare const useTheme: () => {
6
6
  theme: ThemeMode;
7
+ isDarkMode: boolean;
7
8
  setTheme: (theme: ThemeMode) => void;
8
9
  };
@@ -1,6 +1,7 @@
1
1
  import type { ReactNode } from 'react';
2
2
  import React from 'react';
3
3
  import { type ThemeSyncPayload } from './miniAppProtocol';
4
+ import { type MiniAppThemeConfig } from './miniAppThemeConfig';
4
5
  export type MiniAppShellContextValue = {
5
6
  theme: ThemeSyncPayload;
6
7
  };
@@ -11,5 +12,7 @@ export type MiniAppRootProps = {
11
12
  /** Included in READY payload when set. */
12
13
  appId?: string;
13
14
  onThemeChange?: (theme: ThemeSyncPayload) => void;
15
+ /** Overrides `@homecode/ui` `<Theme config>` builder (defaults to generic mini-app palette). */
16
+ getThemeConfig?: (isDarkMode: boolean) => MiniAppThemeConfig;
14
17
  };
15
- export declare function MiniAppRoot({ children, className, appId, onThemeChange, }: MiniAppRootProps): React.ReactElement;
18
+ export declare function MiniAppRoot({ children, className, appId, onThemeChange, getThemeConfig, }: MiniAppRootProps): React.ReactElement;
@@ -1,4 +1,6 @@
1
1
  export { applyThemeToDocument, buildReadyMessage, MINIAPP_CHANNEL, MINIAPP_VERSION, parseThemeSyncMessage, resolveParentOriginFromReferrer, } from './miniAppProtocol';
2
2
  export type { MiniAppMessageReady, MiniAppMessageThemeSync, ThemeSyncPayload, } from './miniAppProtocol';
3
- export { MiniAppRoot, useMiniAppShellTheme, } from './MiniAppRoot';
4
- export type { MiniAppRootProps, MiniAppShellContextValue, } from './MiniAppRoot';
3
+ export { getDefaultMiniAppThemeConfig } from './miniAppThemeConfig';
4
+ export type { MiniAppThemeConfig } from './miniAppThemeConfig';
5
+ export { MiniAppRoot, useMiniAppShellTheme } from './MiniAppRoot';
6
+ export type { MiniAppRootProps, MiniAppShellContextValue } from './MiniAppRoot';
@@ -0,0 +1,3 @@
1
+ /** Homecode `<Theme config={...}>` shape for workspace mini-apps (generic palette). */
2
+ export declare function getDefaultMiniAppThemeConfig(isDarkMode: boolean): any;
3
+ export type MiniAppThemeConfig = ReturnType<typeof getDefaultMiniAppThemeConfig>;
@@ -1,6 +1,6 @@
1
1
  # Workspace mini-apps (Sybilion iframe)
2
2
 
3
- The Sybilion client embeds mini-apps in an **iframe** and syncs theme with `postMessage`. Use **`MiniAppRoot`** so this document’s `<html>` gets **`light` / `dark`** (uilib uses `.dark { … }` for tokens).
3
+ The Sybilion client embeds mini-apps in an **iframe** and syncs theme with `postMessage`. Use **`MiniAppRoot`** so this document’s `<html>` gets **`light` / `dark`** (uilib uses `.dark { … }` for tokens), and so **`@homecode/ui`’s `<Theme />`** runs with vars matching that mode (`getDefaultMiniAppThemeConfig`; override via props if needed).
4
4
 
5
5
  1. Import **`@sybilion/uilib/mini-app-global.css`** (slim tokens + font imports; ships in this package).
6
6
  2. Wrap the React root:
@@ -18,4 +18,6 @@ createRoot(document.getElementById('root')!).render(
18
18
 
19
19
  3. Optional: **`useMiniAppShellTheme()`** for **`{ theme: { mode, isDarkMode } }`** under the provider (object leaves room for more shell fields later).
20
20
 
21
+ 4. Optional: **`getThemeConfig`** on **`MiniAppRoot`** — passed **`(isDarkMode) => config`** like the Sybilion app’s **`getThemeConfig`** from **`src/lib/theme.ts`**, so iframe UI matches host accent/danger tokens.
22
+
21
23
  **Bridge:** `MiniAppRoot` handles `THEME_SYNC`, sends `READY` on mount/load, and checks `event.source === window.parent` plus `document.referrer` origin when present. If the referrer is missing (strict `Referrer-Policy`), `READY` may use `targetOrigin` `*` — document that for your host.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sybilion/uilib",
3
- "version": "1.0.26",
3
+ "version": "1.0.28",
4
4
  "description": "Sybilion Design System — React UI components (Webpack + Stylus)",
5
5
  "publishConfig": {
6
6
  "access": "public",
@@ -57,10 +57,9 @@
57
57
  position relative
58
58
  z-index 1
59
59
 
60
- // Tooltip wrapper is positioned absolutely, can have higher z-index
60
+ // Tooltip portal positions with absolute + transform; do not override with relative.
61
61
  .recharts-tooltip-wrapper
62
62
  z-index 3 !important
63
- position relative
64
63
 
65
64
  // Active dots and cursor
66
65
  .recharts-active-dot
@@ -75,8 +74,12 @@
75
74
  // Tooltip styles
76
75
  .tooltipContainer
77
76
  display grid
78
- width minmax(300px, 100%)
79
- min-width 8rem
77
+ box-sizing border-box
78
+ width max-content
79
+ max-width 100%
80
+ min-width 0
81
+ overflow-wrap break-word
82
+ word-break break-word
80
83
  align-items start
81
84
  gap 0.375rem /* gap-1.5 */
82
85
  padding 0.375rem 0.625rem /* py-1.5 px-2.5 */
@@ -35,6 +35,78 @@ import { ChartAxes } from './ChartAxes';
35
35
  import { ChartGrid } from './ChartGrid';
36
36
  import { LegendSvg } from './LegendSvg/LegendSvg';
37
37
 
38
+ type ChartMargin = { top: number; right: number; bottom: number; left: number };
39
+
40
+ const DEFAULT_CHART_MARGIN: ChartMargin = {
41
+ top: 5,
42
+ right: 5,
43
+ bottom: 5,
44
+ left: 5,
45
+ };
46
+
47
+ function resolveChartMargin(
48
+ margin: Partial<ChartMargin> | undefined,
49
+ ): ChartMargin {
50
+ return {
51
+ top: margin?.top ?? DEFAULT_CHART_MARGIN.top,
52
+ right: margin?.right ?? DEFAULT_CHART_MARGIN.right,
53
+ bottom: margin?.bottom ?? DEFAULT_CHART_MARGIN.bottom,
54
+ left: margin?.left ?? DEFAULT_CHART_MARGIN.left,
55
+ };
56
+ }
57
+
58
+ /** Plot box inside `.recharts-wrapper`, same convention as Recharts cartesian viewBox. */
59
+ function getPlotViewBox(wrapper: HTMLElement, m: ChartMargin) {
60
+ const w = wrapper.clientWidth;
61
+ const h = wrapper.clientHeight;
62
+ return {
63
+ x: m.left,
64
+ y: m.top,
65
+ width: Math.max(0, w - m.left - m.right),
66
+ height: Math.max(0, h - m.top - m.bottom),
67
+ };
68
+ }
69
+
70
+ function clampTooltipTranslate(args: {
71
+ coordinate: { x: number; y: number };
72
+ viewBox: { x: number; y: number; width: number; height: number };
73
+ tooltipWidth: number;
74
+ tooltipHeight: number;
75
+ offset: number;
76
+ edgeMargin: number;
77
+ }): { x: number; y: number } {
78
+ const {
79
+ coordinate,
80
+ viewBox,
81
+ tooltipWidth: tw,
82
+ tooltipHeight: th,
83
+ offset,
84
+ edgeMargin,
85
+ } = args;
86
+
87
+ const minX = viewBox.x + edgeMargin;
88
+ const maxX = viewBox.x + viewBox.width - tw - edgeMargin;
89
+ const minY = viewBox.y + edgeMargin;
90
+ const maxY = viewBox.y + viewBox.height - th - edgeMargin;
91
+
92
+ const clamp = (v: number, lo: number, hi: number) =>
93
+ Math.min(Math.max(v, lo), Math.max(lo, hi));
94
+
95
+ let tx = coordinate.x + offset;
96
+ if (tx + tw > viewBox.x + viewBox.width - edgeMargin) {
97
+ tx = coordinate.x - tw - offset;
98
+ }
99
+ tx = clamp(tx, minX, maxX);
100
+
101
+ let ty = coordinate.y + offset;
102
+ if (ty + th > viewBox.y + viewBox.height - edgeMargin) {
103
+ ty = coordinate.y - th - offset;
104
+ }
105
+ ty = clamp(ty, minY, maxY);
106
+
107
+ return { x: tx, y: ty };
108
+ }
109
+
38
110
  export interface BaseChartWrapperProps {
39
111
  renderId?: string; // unique id for the render, used to identify the render in the render queue
40
112
  chartConfig?: ChartConfig;
@@ -200,21 +272,10 @@ const BaseChartWrapperContent = forwardRef<
200
272
  legendMarginLeft = 0,
201
273
  } = props;
202
274
 
203
- const activeDataRef = useRef<any>(null);
204
- /** Raw cursor position from Recharts (tooltip anchor). */
205
- const rawTooltipCoordinateRef = useRef<{ x: number; y: number } | null>(null);
206
- /** Last transform actually applied after viewport/boundary adjustment. */
207
- const lastAppliedTooltipPositionRef = useRef<{ x: number; y: number } | null>(
208
- null,
209
- );
210
- const isTooltipActiveRef = useRef<boolean>(false);
211
-
212
- // const [activeDotsData, setActiveDotsData] = useState<ActiveDot[]>([]);
213
275
  const [shouldAnimate, setShouldAnimate] = useState(false);
214
276
 
215
277
  const rootRef = useRef<HTMLDivElement>(null);
216
278
 
217
- // Merge forwarded ref with internal rootRef using callback ref
218
279
  const setRefs = (node: HTMLDivElement | null) => {
219
280
  rootRef.current = node;
220
281
  if (typeof ref === 'function') {
@@ -223,137 +284,105 @@ const BaseChartWrapperContent = forwardRef<
223
284
  ref.current = node;
224
285
  }
225
286
  };
226
- // const prevGridHeightRef = useRef<number>(0);
227
- const tooltipWrapperRef = useRef<HTMLElement | null>(null);
287
+
288
+ const resolvedChartMargin = useMemo(
289
+ () => resolveChartMargin(margin),
290
+ [margin?.top, margin?.right, margin?.bottom, margin?.left],
291
+ );
228
292
 
229
293
  const TOOLTIP_EDGE_MARGIN = 8;
294
+ const TOOLTIP_OFFSET = 10;
230
295
 
231
- const applyTooltipPosition = (opts?: { skipBoundaryAdjust?: boolean }) => {
296
+ const tooltipWrapperRef = useRef<HTMLElement | null>(null);
297
+ const rawTooltipCoordinateRef = useRef<{ x: number; y: number } | null>(null);
298
+ const tooltipSizeRef = useRef<{ width: number; height: number }>({
299
+ width: 0,
300
+ height: 0,
301
+ });
302
+ const tooltipResizeObserverRef = useRef<ResizeObserver | null>(null);
303
+ const tooltipResizeObservedTargetRef = useRef<Element | null>(null);
304
+
305
+ const applyTooltipPosition = () => {
232
306
  const wrapper = tooltipWrapperRef.current;
233
- const root = rootRef.current;
234
- const raw = rawTooltipCoordinateRef.current;
235
- if (!wrapper || !raw) return;
307
+ const coord = rawTooltipCoordinateRef.current;
308
+ const chartWrapper = rootRef.current?.querySelector('.recharts-wrapper');
236
309
 
237
- const setTransform = (el: HTMLElement, x: number, y: number) => {
238
- el.style.transform = `translate(${x}px, ${y}px)`;
239
- lastAppliedTooltipPositionRef.current = { x, y };
240
- };
310
+ if (!wrapper || !coord || !(chartWrapper instanceof HTMLElement)) return;
241
311
 
242
- setTransform(wrapper, raw.x, raw.y);
312
+ const measured = wrapper.getBoundingClientRect();
313
+ const width = tooltipSizeRef.current.width || measured.width;
314
+ const height = tooltipSizeRef.current.height || measured.height;
243
315
 
244
- if (opts?.skipBoundaryAdjust || !root) return;
316
+ if (width <= 0 || height <= 0) return;
245
317
 
246
- const runAdjust = () => {
247
- const w = tooltipWrapperRef.current;
248
- const rEl = rootRef.current;
249
- const rCoord = rawTooltipCoordinateRef.current;
250
- if (!w || !rEl || !rCoord) return;
251
-
252
- const rootRect = rEl.getBoundingClientRect();
253
- const rightBound = Math.min(rootRect.right, window.innerWidth);
254
- let x = rCoord.x;
255
- const y = rCoord.y;
256
-
257
- w.style.transform = `translate(${x}px, ${y}px)`;
258
- let tooltipRect = w.getBoundingClientRect();
259
-
260
- if (tooltipRect.right > rightBound - TOOLTIP_EDGE_MARGIN) {
261
- x -= tooltipRect.right - rightBound + TOOLTIP_EDGE_MARGIN;
262
- }
318
+ tooltipSizeRef.current = { width, height };
263
319
 
264
- const leftBound = Math.max(rootRect.left, 0);
265
- w.style.transform = `translate(${x}px, ${y}px)`;
266
- tooltipRect = w.getBoundingClientRect();
267
-
268
- if (tooltipRect.left < leftBound + TOOLTIP_EDGE_MARGIN) {
269
- x += leftBound + TOOLTIP_EDGE_MARGIN - tooltipRect.left;
270
- }
271
-
272
- setTransform(w, x, y);
273
- };
274
-
275
- requestAnimationFrame(() => {
276
- requestAnimationFrame(runAdjust);
320
+ const next = clampTooltipTranslate({
321
+ coordinate: coord,
322
+ viewBox: getPlotViewBox(chartWrapper, resolvedChartMargin),
323
+ tooltipWidth: width,
324
+ tooltipHeight: height,
325
+ offset: TOOLTIP_OFFSET,
326
+ edgeMargin: TOOLTIP_EDGE_MARGIN,
277
327
  });
328
+
329
+ wrapper.style.transform = `translate(${next.x}px, ${next.y}px)`;
278
330
  };
279
331
 
280
332
  const applyTooltipPositionRef = useRef(applyTooltipPosition);
281
333
  applyTooltipPositionRef.current = applyTooltipPosition;
282
334
 
283
- // Effect to watch for tooltip wrapper and apply transforms/opacity
335
+ // Own only the final transform: smooth follow cursor, but clamp in local chart coords first.
284
336
  useEffect(() => {
285
- const restorePosition = () => {
286
- const pos =
287
- lastAppliedTooltipPositionRef.current ??
288
- rawTooltipCoordinateRef.current;
289
- if (tooltipWrapperRef.current && pos) {
290
- const currentTransform = tooltipWrapperRef.current.style.transform;
291
- const expectedTransform = `translate(${pos.x}px, ${pos.y}px)`;
292
-
293
- // Always restore position if transform is missing, reset, or doesn't match expected position
294
- // This prevents Recharts from resetting the tooltip position
295
- if (
296
- !currentTransform ||
297
- currentTransform === 'none' ||
298
- currentTransform === 'translate(0px, 0px)' ||
299
- currentTransform !== expectedTransform
300
- ) {
301
- tooltipWrapperRef.current.style.transform = expectedTransform;
302
- }
337
+ const connectResizeObserver = (wrapperEl: Element) => {
338
+ if (tooltipResizeObservedTargetRef.current === wrapperEl) {
339
+ return;
303
340
  }
304
- };
305
-
306
- const findAndSetupTooltipWrapper = () => {
307
- const wrapper = rootRef.current?.querySelector(
308
- '.recharts-tooltip-wrapper',
309
- ) as HTMLElement | null;
341
+ const resizeRo =
342
+ tooltipResizeObserverRef.current ??
343
+ new ResizeObserver(entries => {
344
+ const entry = entries[0];
345
+ if (!entry) return;
346
+ const { width, height } = entry.contentRect;
347
+ tooltipSizeRef.current = { width, height };
348
+ requestAnimationFrame(() => applyTooltipPositionRef.current());
349
+ });
350
+ tooltipResizeObserverRef.current = resizeRo;
310
351
 
311
- if (wrapper && wrapper !== tooltipWrapperRef.current) {
312
- tooltipWrapperRef.current = wrapper;
352
+ const prevObserved = tooltipResizeObservedTargetRef.current;
353
+ if (prevObserved && prevObserved !== wrapperEl) {
354
+ resizeRo.unobserve(prevObserved);
355
+ }
356
+ tooltipResizeObservedTargetRef.current = wrapperEl;
357
+ resizeRo.observe(wrapperEl);
313
358
 
314
- // Add transition for smooth movement and opacity
315
- wrapper.style.transition =
359
+ if (wrapperEl instanceof HTMLElement) {
360
+ tooltipWrapperRef.current = wrapperEl;
361
+ wrapperEl.style.transition =
316
362
  'transform 0.2s ease-out, opacity 0.2s ease-out';
317
-
318
- // Override Recharts' visibility: hidden with visibility: visible
319
- // We'll control visibility through opacity instead
320
- wrapper.style.visibility = 'visible';
321
-
322
- // Set initial opacity based on active state
323
- const isActive = activeDataRef.current?.active === true;
324
- wrapper.style.opacity = isActive ? '1' : '0';
325
- wrapper.style.pointerEvents = isActive ? 'auto' : 'none';
326
-
327
- // Always restore position if we have a last position
328
- restorePosition();
329
- if (rawTooltipCoordinateRef.current) {
330
- applyTooltipPositionRef.current();
331
- }
363
+ requestAnimationFrame(() => applyTooltipPositionRef.current());
332
364
  }
365
+ };
333
366
 
334
- // Always ensure visibility is visible (Recharts sets it to hidden)
335
- if (tooltipWrapperRef.current) {
336
- tooltipWrapperRef.current.style.visibility = 'visible';
337
- // Continuously restore position to prevent resets
338
- restorePosition();
339
- }
367
+ const tryObserveTooltipWrapper = () => {
368
+ const wrapper = rootRef.current?.querySelector(
369
+ '.recharts-tooltip-wrapper',
370
+ );
371
+ if (wrapper) connectResizeObserver(wrapper);
340
372
  };
341
373
 
342
- // Initial check
343
- findAndSetupTooltipWrapper();
374
+ tryObserveTooltipWrapper();
344
375
 
345
- // Watch for tooltip wrapper changes and style mutations
346
376
  const observer = new MutationObserver(mutations => {
347
- findAndSetupTooltipWrapper();
377
+ tryObserveTooltipWrapper();
348
378
 
349
- // If transform attribute changed, restore position if needed
350
379
  mutations.forEach(mutation => {
351
380
  if (
352
381
  mutation.type === 'attributes' &&
353
382
  mutation.attributeName === 'style' &&
354
383
  mutation.target === tooltipWrapperRef.current
355
384
  ) {
356
- restorePosition();
385
+ requestAnimationFrame(() => applyTooltipPositionRef.current());
357
386
  }
358
387
  });
359
388
  });
@@ -367,28 +396,18 @@ const BaseChartWrapperContent = forwardRef<
367
396
  });
368
397
  }
369
398
 
370
- // Use requestAnimationFrame to continuously monitor and restore position
371
- // Only run when we have a last position to maintain
372
- let rafId: number | null = null;
373
- const monitorPosition = () => {
374
- if (rawTooltipCoordinateRef.current && tooltipWrapperRef.current) {
375
- restorePosition();
376
- rafId = requestAnimationFrame(monitorPosition);
377
- } else {
378
- rafId = null;
379
- }
399
+ const onWinResize = () => {
400
+ requestAnimationFrame(() => applyTooltipPositionRef.current());
380
401
  };
381
-
382
- // Start monitoring if we already have a position
383
- if (rawTooltipCoordinateRef.current) {
384
- rafId = requestAnimationFrame(monitorPosition);
385
- }
402
+ window.addEventListener('resize', onWinResize);
386
403
 
387
404
  return () => {
388
405
  observer.disconnect();
389
- if (rafId !== null) {
390
- cancelAnimationFrame(rafId);
391
- }
406
+ tooltipResizeObserverRef.current?.disconnect();
407
+ tooltipResizeObserverRef.current = null;
408
+ tooltipResizeObservedTargetRef.current = null;
409
+ tooltipWrapperRef.current = null;
410
+ window.removeEventListener('resize', onWinResize);
392
411
  };
393
412
  }, []);
394
413
 
@@ -403,23 +422,6 @@ const BaseChartWrapperContent = forwardRef<
403
422
  // If no valid payload items, render ChartTooltipContent with active=false and empty payload
404
423
  // This allows ChartTooltipContent to clear its lastTooltipData state
405
424
  if (!filteredPayload || filteredPayload.length === 0) {
406
- // Update refs to reflect inactive state
407
- if (isTooltipActiveRef.current) {
408
- isTooltipActiveRef.current = false;
409
- // Always maintain last position when tooltip becomes inactive
410
- const pos =
411
- lastAppliedTooltipPositionRef.current ??
412
- rawTooltipCoordinateRef.current;
413
- if (pos && tooltipWrapperRef.current) {
414
- const wrapper = tooltipWrapperRef.current;
415
- wrapper.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
416
- wrapper.style.visibility = 'visible';
417
- wrapper.style.opacity = '0';
418
- wrapper.style.pointerEvents = 'none';
419
- }
420
- }
421
- // Render ChartTooltipContent with active=false and empty payload to trigger cleanup
422
- // This ensures lastTooltipData is cleared when there's no valid data
423
425
  return (
424
426
  <ChartTooltipContent
425
427
  active={false}
@@ -431,60 +433,16 @@ const BaseChartWrapperContent = forwardRef<
431
433
  );
432
434
  }
433
435
 
434
- // Store tooltip data in ref (not state) to avoid re-render
435
- // Use filtered payload instead of original props.payload
436
- activeDataRef.current = {
437
- ...props,
438
- payload: filteredPayload,
439
- };
440
-
441
- const wasActive = isTooltipActiveRef.current;
442
436
  const isActive = props.active === true;
443
437
 
444
- // When tooltip becomes active and has coordinate, update last position
445
438
  if (isActive && props.coordinate) {
446
- const newCoordinate = {
439
+ rawTooltipCoordinateRef.current = {
447
440
  x: props.coordinate.x,
448
441
  y: props.coordinate.y,
449
442
  };
450
-
451
- rawTooltipCoordinateRef.current = newCoordinate;
452
- isTooltipActiveRef.current = true;
453
-
454
- if (tooltipWrapperRef.current) {
455
- tooltipWrapperRef.current.style.visibility = 'visible';
456
- tooltipWrapperRef.current.style.opacity = '1';
457
- tooltipWrapperRef.current.style.pointerEvents = 'auto';
458
- applyTooltipPosition();
459
- }
460
- } else if (!isActive && wasActive) {
461
- isTooltipActiveRef.current = false;
462
-
463
- // Always maintain last position when tooltip becomes inactive
464
- const pos =
465
- lastAppliedTooltipPositionRef.current ??
466
- rawTooltipCoordinateRef.current;
467
- if (pos && tooltipWrapperRef.current) {
468
- const wrapper = tooltipWrapperRef.current;
469
- wrapper.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
470
- // Keep visibility visible but hide with opacity transition
471
- wrapper.style.visibility = 'visible';
472
- wrapper.style.opacity = '0';
473
- wrapper.style.pointerEvents = 'none';
474
- }
475
- } else if (!isActive && !wasActive) {
476
- // Ensure opacity is 0 when inactive, but keep visibility visible and maintain position
477
- if (tooltipWrapperRef.current) {
478
- tooltipWrapperRef.current.style.visibility = 'visible';
479
- tooltipWrapperRef.current.style.opacity = '0';
480
- tooltipWrapperRef.current.style.pointerEvents = 'none';
481
- const pos =
482
- lastAppliedTooltipPositionRef.current ??
483
- rawTooltipCoordinateRef.current;
484
- if (pos) {
485
- tooltipWrapperRef.current.style.transform = `translate(${pos.x}px, ${pos.y}px)`;
486
- }
487
- }
443
+ requestAnimationFrame(() => applyTooltipPositionRef.current());
444
+ } else {
445
+ rawTooltipCoordinateRef.current = null;
488
446
  }
489
447
 
490
448
  return (
@@ -753,7 +711,12 @@ const BaseChartWrapperContent = forwardRef<
753
711
 
754
712
  {showTooltip && (
755
713
  <div>
756
- <ChartTooltip cursor={false} content={renderTooltipContent} />
714
+ <ChartTooltip
715
+ cursor={false}
716
+ offset={TOOLTIP_OFFSET}
717
+ allowEscapeViewBox={{ x: false, y: false }}
718
+ content={renderTooltipContent}
719
+ />
757
720
  </div>
758
721
  )}
759
722
  </ChartComponent>