@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.
- package/assets/mini-app-global.css +12 -12
- package/dist/esm/components/ui/Chart/Chart.styl.js +1 -1
- package/dist/esm/components/ui/Chart/components/BaseChartWrapper.js +117 -170
- package/dist/esm/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.js +101 -1
- package/dist/esm/components/ui/ChartAreaInteractive/ChartAreaInteractive.js +12 -4
- package/dist/esm/index.js +1 -0
- package/dist/esm/mini-app/MiniAppRoot.js +9 -5
- package/dist/esm/mini-app/miniAppThemeConfig.js +40 -0
- package/dist/esm/types/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.d.ts +6 -1
- package/dist/esm/types/src/docs/contexts/theme-context.d.ts +1 -0
- package/dist/esm/types/src/mini-app/MiniAppRoot.d.ts +4 -1
- package/dist/esm/types/src/mini-app/index.d.ts +4 -2
- package/dist/esm/types/src/mini-app/miniAppThemeConfig.d.ts +3 -0
- package/docs/workspace-mini-apps.md +3 -1
- package/package.json +1 -1
- package/src/components/ui/Chart/Chart.styl +7 -4
- package/src/components/ui/Chart/components/BaseChartWrapper.tsx +156 -193
- package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.ts +90 -40
- package/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.tsx +15 -3
- package/src/docs/contexts/theme-context.tsx +9 -1
- package/src/docs/pages/ChartAreaInteractivePage.tsx +27 -1
- package/src/docs/pages/MiniAppRootPage.tsx +6 -1
- package/src/mini-app/MiniAppRoot.tsx +19 -1
- package/src/mini-app/index.ts +4 -8
- 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 };
|
package/dist/esm/types/src/components/ui/ChartAreaInteractive/ChartAreaInteractive.helpers.d.ts
CHANGED
|
@@ -9,7 +9,12 @@ declare const timeRangeToMonths: {
|
|
|
9
9
|
readonly All: 12;
|
|
10
10
|
};
|
|
11
11
|
export type TimeRange = keyof typeof timeRangeToMonths;
|
|
12
|
-
export
|
|
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
|
/**
|
|
@@ -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 {
|
|
4
|
-
export type {
|
|
3
|
+
export { getDefaultMiniAppThemeConfig } from './miniAppThemeConfig';
|
|
4
|
+
export type { MiniAppThemeConfig } from './miniAppThemeConfig';
|
|
5
|
+
export { MiniAppRoot, useMiniAppShellTheme } from './MiniAppRoot';
|
|
6
|
+
export type { MiniAppRootProps, MiniAppShellContextValue } from './MiniAppRoot';
|
|
@@ -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
|
@@ -57,10 +57,9 @@
|
|
|
57
57
|
position relative
|
|
58
58
|
z-index 1
|
|
59
59
|
|
|
60
|
-
// Tooltip
|
|
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
|
-
|
|
79
|
-
|
|
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
|
-
|
|
227
|
-
const
|
|
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
|
|
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
|
|
234
|
-
const
|
|
235
|
-
if (!wrapper || !raw) return;
|
|
307
|
+
const coord = rawTooltipCoordinateRef.current;
|
|
308
|
+
const chartWrapper = rootRef.current?.querySelector('.recharts-wrapper');
|
|
236
309
|
|
|
237
|
-
|
|
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
|
-
|
|
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 (
|
|
316
|
+
if (width <= 0 || height <= 0) return;
|
|
245
317
|
|
|
246
|
-
|
|
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
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
//
|
|
335
|
+
// Own only the final transform: smooth follow cursor, but clamp in local chart coords first.
|
|
284
336
|
useEffect(() => {
|
|
285
|
-
const
|
|
286
|
-
|
|
287
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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
|
-
|
|
312
|
-
|
|
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
|
-
|
|
315
|
-
|
|
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
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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
|
-
|
|
343
|
-
findAndSetupTooltipWrapper();
|
|
374
|
+
tryObserveTooltipWrapper();
|
|
344
375
|
|
|
345
|
-
// Watch for tooltip wrapper changes and style mutations
|
|
346
376
|
const observer = new MutationObserver(mutations => {
|
|
347
|
-
|
|
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
|
-
|
|
385
|
+
requestAnimationFrame(() => applyTooltipPositionRef.current());
|
|
357
386
|
}
|
|
358
387
|
});
|
|
359
388
|
});
|
|
@@ -367,28 +396,18 @@ const BaseChartWrapperContent = forwardRef<
|
|
|
367
396
|
});
|
|
368
397
|
}
|
|
369
398
|
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
390
|
-
|
|
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
|
-
|
|
439
|
+
rawTooltipCoordinateRef.current = {
|
|
447
440
|
x: props.coordinate.x,
|
|
448
441
|
y: props.coordinate.y,
|
|
449
442
|
};
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
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
|
|
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>
|