@wallarm-org/design-system 0.43.0 → 0.44.1

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 (95) hide show
  1. package/dist/components/AppShell/story-content/_storyConfigRenderer.d.ts +7 -0
  2. package/dist/components/AppShell/story-content/_storyConfigRenderer.js +46 -0
  3. package/dist/components/AppShell/story-content/_storyHomeContent.d.ts +2 -0
  4. package/dist/components/AppShell/story-content/_storyHomeContent.js +15 -0
  5. package/dist/components/AppShell/story-content/_storyNavConfigs.d.ts +6 -0
  6. package/dist/components/AppShell/story-content/_storyNavConfigs.js +693 -0
  7. package/dist/components/AppShell/story-content/index.d.ts +3 -0
  8. package/dist/components/AppShell/story-content/index.js +4 -0
  9. package/dist/components/Breadcrumbs/BreadcrumbsItem.js +2 -1
  10. package/dist/components/Breadcrumbs/BreadcrumbsScopeSwitcher.d.ts +18 -0
  11. package/dist/components/Breadcrumbs/BreadcrumbsScopeSwitcher.js +60 -0
  12. package/dist/components/Breadcrumbs/index.d.ts +1 -0
  13. package/dist/components/Breadcrumbs/index.js +2 -1
  14. package/dist/components/NavPanel/NavPanel.d.ts +8 -0
  15. package/dist/components/NavPanel/NavPanel.js +93 -0
  16. package/dist/components/NavPanel/NavPanelBack.d.ts +6 -0
  17. package/dist/components/NavPanel/NavPanelBack.js +31 -0
  18. package/dist/components/NavPanel/NavPanelContext.d.ts +12 -0
  19. package/dist/components/NavPanel/NavPanelContext.js +9 -0
  20. package/dist/components/NavPanel/NavPanelDivider.d.ts +3 -0
  21. package/dist/components/NavPanel/NavPanelDivider.js +10 -0
  22. package/dist/components/NavPanel/NavPanelGroup.d.ts +18 -0
  23. package/dist/components/NavPanel/NavPanelGroup.js +49 -0
  24. package/dist/components/NavPanel/NavPanelGroupContent.d.ts +6 -0
  25. package/dist/components/NavPanel/NavPanelGroupContent.js +36 -0
  26. package/dist/components/NavPanel/NavPanelGroupItem.d.ts +10 -0
  27. package/dist/components/NavPanel/NavPanelGroupItem.js +40 -0
  28. package/dist/components/NavPanel/NavPanelGroupLabel.d.ts +8 -0
  29. package/dist/components/NavPanel/NavPanelGroupLabel.js +48 -0
  30. package/dist/components/NavPanel/NavPanelHeader.d.ts +6 -0
  31. package/dist/components/NavPanel/NavPanelHeader.js +21 -0
  32. package/dist/components/NavPanel/NavPanelItem.d.ts +10 -0
  33. package/dist/components/NavPanel/NavPanelItem.js +35 -0
  34. package/dist/components/NavPanel/NavPanelResizeHandle.d.ts +2 -0
  35. package/dist/components/NavPanel/NavPanelResizeHandle.js +81 -0
  36. package/dist/components/NavPanel/NavPanelSectionHeader.d.ts +6 -0
  37. package/dist/components/NavPanel/NavPanelSectionHeader.js +22 -0
  38. package/dist/components/NavPanel/classes.d.ts +6 -0
  39. package/dist/components/NavPanel/classes.js +24 -0
  40. package/dist/components/NavPanel/index.d.ts +10 -0
  41. package/dist/components/NavPanel/index.js +11 -0
  42. package/dist/components/NavRail/NavRail.js +33 -2
  43. package/dist/components/NavRail/NavRailItem.js +10 -3
  44. package/dist/components/NavRail/classes.js +2 -2
  45. package/dist/components/NavRail/useShortcut.d.ts +2 -0
  46. package/dist/components/NavRail/useShortcut.js +44 -0
  47. package/dist/components/ProductNav/ProductNav.d.ts +13 -0
  48. package/dist/components/ProductNav/ProductNav.js +100 -0
  49. package/dist/components/ProductNav/ProductNavBreadcrumbs.d.ts +2 -0
  50. package/dist/components/ProductNav/ProductNavBreadcrumbs.js +38 -0
  51. package/dist/components/ProductNav/ProductNavContext.d.ts +22 -0
  52. package/dist/components/ProductNav/ProductNavContext.js +9 -0
  53. package/dist/components/ProductNav/ProductNavPanel.d.ts +6 -0
  54. package/dist/components/ProductNav/ProductNavPanel.js +82 -0
  55. package/dist/components/ProductNav/index.d.ts +10 -0
  56. package/dist/components/ProductNav/index.js +9 -0
  57. package/dist/components/ProductNav/matchNav.d.ts +16 -0
  58. package/dist/components/ProductNav/matchNav.js +108 -0
  59. package/dist/components/ProductNav/navUtils.d.ts +5 -0
  60. package/dist/components/ProductNav/navUtils.js +26 -0
  61. package/dist/components/ProductNav/types.d.ts +69 -0
  62. package/dist/components/ProductNav/types.js +0 -0
  63. package/dist/components/ProductNav/useLocationPathname.d.ts +4 -0
  64. package/dist/components/ProductNav/useLocationPathname.js +24 -0
  65. package/dist/components/ProductNav/useProductNav.d.ts +16 -0
  66. package/dist/components/ProductNav/useProductNav.js +19 -0
  67. package/dist/components/RemoteShell/RemoteShell.d.ts +7 -0
  68. package/dist/components/RemoteShell/RemoteShell.js +16 -0
  69. package/dist/components/RemoteShell/RemoteShellBreadcrumb.d.ts +6 -0
  70. package/dist/components/RemoteShell/RemoteShellBreadcrumb.js +16 -0
  71. package/dist/components/RemoteShell/RemoteShellContent.d.ts +6 -0
  72. package/dist/components/RemoteShell/RemoteShellContent.js +16 -0
  73. package/dist/components/RemoteShell/RemoteShellPanel.d.ts +6 -0
  74. package/dist/components/RemoteShell/RemoteShellPanel.js +16 -0
  75. package/dist/components/RemoteShell/index.d.ts +4 -0
  76. package/dist/components/RemoteShell/index.js +5 -0
  77. package/dist/components/SimpleCharts/LineChart/LineChart.js +5 -5
  78. package/dist/components/SimpleCharts/LineChart/LineChartZoomBrush.js +66 -12
  79. package/dist/components/SimpleCharts/LineChart/hooks/useLineChartActiveKey.js +4 -3
  80. package/dist/components/SimpleCharts/LineChart/hooks/useLineChartZoomState.d.ts +9 -2
  81. package/dist/components/SimpleCharts/LineChart/hooks/useLineChartZoomState.js +88 -34
  82. package/dist/hooks/index.d.ts +1 -0
  83. package/dist/hooks/index.js +2 -1
  84. package/dist/hooks/useArrowNav.d.ts +7 -0
  85. package/dist/hooks/useArrowNav.js +98 -0
  86. package/dist/icons/ChevronUpDown.d.ts +3 -0
  87. package/dist/icons/ChevronUpDown.js +12 -0
  88. package/dist/icons/MapPin.d.ts +3 -0
  89. package/dist/icons/MapPin.js +12 -0
  90. package/dist/icons/index.d.ts +2 -0
  91. package/dist/icons/index.js +3 -1
  92. package/dist/index.d.ts +3 -0
  93. package/dist/index.js +4 -1
  94. package/dist/metadata/components.json +4829 -682
  95. package/package.json +1 -1
@@ -0,0 +1,108 @@
1
+ const findMatchingNode = (items, segment, groupPath = [])=>{
2
+ for (const item of items)if ('section-header' !== item.type) {
3
+ if ('group' === item.type) {
4
+ const found = findMatchingNode(item.children, segment, [
5
+ ...groupPath,
6
+ item
7
+ ]);
8
+ if (found) return found;
9
+ } else if (item.path === segment) return {
10
+ node: item,
11
+ groupPath
12
+ };
13
+ }
14
+ return null;
15
+ };
16
+ const matchNav = (pathname, config)=>{
17
+ const segments = pathname.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
18
+ const navStack = [];
19
+ const breadcrumbSegments = [];
20
+ let activeItemId = null;
21
+ const rootEntry = {
22
+ title: config.productLabel,
23
+ parentLabel: null,
24
+ items: config.items,
25
+ activeItemId: null
26
+ };
27
+ navStack.push(rootEntry);
28
+ breadcrumbSegments.push({
29
+ type: 'link',
30
+ label: config.productLabel,
31
+ href: '/'
32
+ });
33
+ let segmentIndex = 0;
34
+ let currentItems = config.items;
35
+ let currentStackEntry = rootEntry;
36
+ while(segmentIndex < segments.length){
37
+ const seg = segments[segmentIndex];
38
+ const result = findMatchingNode(currentItems, seg);
39
+ if (!result) break;
40
+ const { node: match, groupPath } = result;
41
+ if ('link' === match.type) {
42
+ currentStackEntry.activeItemId = match.id;
43
+ activeItemId = match.id;
44
+ segmentIndex++;
45
+ for (const group of groupPath)breadcrumbSegments.push({
46
+ type: 'static',
47
+ label: group.label
48
+ });
49
+ breadcrumbSegments.push({
50
+ type: 'static',
51
+ label: match.label
52
+ });
53
+ break;
54
+ }
55
+ if ('drill' === match.type) {
56
+ currentStackEntry.activeItemId = match.id;
57
+ segmentIndex++;
58
+ if (segmentIndex >= segments.length) {
59
+ activeItemId = match.id;
60
+ breadcrumbSegments.push({
61
+ type: 'static',
62
+ label: match.label
63
+ });
64
+ break;
65
+ }
66
+ const paramValue = segments[segmentIndex];
67
+ segmentIndex++;
68
+ const matchedEntity = match.entities?.find((e)=>e.id === paramValue);
69
+ const entityLabel = matchedEntity?.label ?? paramValue;
70
+ const childPath = segments.slice(segmentIndex).join('/');
71
+ const scopeItems = match.entities?.map((e)=>({
72
+ id: e.id,
73
+ label: e.label,
74
+ description: e.description,
75
+ href: childPath ? `/${match.path}/${e.id}/${childPath}` : `/${match.path}/${e.id}`
76
+ }));
77
+ breadcrumbSegments.push({
78
+ type: 'scope-switcher',
79
+ label: entityLabel,
80
+ href: `/${segments.slice(0, segmentIndex).join('/')}`,
81
+ paramValue,
82
+ scopeItems
83
+ });
84
+ const drillEntry = {
85
+ title: entityLabel,
86
+ parentLabel: currentStackEntry.title,
87
+ items: match.children,
88
+ activeItemId: null
89
+ };
90
+ navStack.push(drillEntry);
91
+ currentItems = match.children;
92
+ currentStackEntry = drillEntry;
93
+ if (segmentIndex >= segments.length) {
94
+ activeItemId = null;
95
+ break;
96
+ }
97
+ continue;
98
+ }
99
+ }
100
+ const lastEntry = navStack[navStack.length - 1];
101
+ if (null === activeItemId && lastEntry) activeItemId = lastEntry.activeItemId;
102
+ return {
103
+ navStack,
104
+ breadcrumbSegments,
105
+ activeItemId
106
+ };
107
+ };
108
+ export { matchNav };
@@ -0,0 +1,5 @@
1
+ import type { NavConfigDrill, NavConfigNode } from './types';
2
+ /** Find the first link path in a config tree (used as default pathname) */
3
+ export declare function findFirstLinkPath(items: NavConfigNode[]): string | null;
4
+ /** Find a drill node by ID in a potentially nested item list */
5
+ export declare function findDrillNode(items: NavConfigNode[], id: string | null): NavConfigDrill | null;
@@ -0,0 +1,26 @@
1
+ function findFirstLinkPath(items) {
2
+ for (const item of items){
3
+ if ('link' === item.type) return item.path;
4
+ if ('group' === item.type) {
5
+ const found = findFirstLinkPath(item.children);
6
+ if (found) return found;
7
+ }
8
+ if ('drill' === item.type) {
9
+ const found = findFirstLinkPath(item.children);
10
+ if (found) return found;
11
+ }
12
+ }
13
+ return null;
14
+ }
15
+ function findDrillNode(items, id) {
16
+ if (!id) return null;
17
+ for (const item of items){
18
+ if ('drill' === item.type && item.id === id) return item;
19
+ if ('group' === item.type) {
20
+ const found = findDrillNode(item.children, id);
21
+ if (found) return found;
22
+ }
23
+ }
24
+ return null;
25
+ }
26
+ export { findDrillNode, findFirstLinkPath };
@@ -0,0 +1,69 @@
1
+ import type { ComponentType } from 'react';
2
+ import type { SvgIconProps } from '../../icons/SvgIcon';
3
+ export interface NavConfig {
4
+ productLabel: string;
5
+ items: NavConfigNode[];
6
+ }
7
+ export type NavConfigNode = NavConfigLink | NavConfigDrill | NavConfigGroup | NavConfigSectionHeader;
8
+ export interface NavConfigLink {
9
+ type: 'link';
10
+ id: string;
11
+ label: string;
12
+ path: string;
13
+ icon?: ComponentType<SvgIconProps>;
14
+ dividerAfter?: boolean;
15
+ }
16
+ export interface NavConfigDrill {
17
+ type: 'drill';
18
+ id: string;
19
+ label: string;
20
+ path: string;
21
+ /** URL parameter name for the dynamic segment */
22
+ param: string;
23
+ children: NavConfigNode[];
24
+ icon?: ComponentType<SvgIconProps>;
25
+ /** Available entities for the drill scope-switcher dropdown */
26
+ entities?: {
27
+ id: string;
28
+ label: string;
29
+ description?: string;
30
+ }[];
31
+ /** Render a visual divider after this item */
32
+ dividerAfter?: boolean;
33
+ }
34
+ export interface NavConfigGroup {
35
+ type: 'group';
36
+ id: string;
37
+ label: string;
38
+ children: NavConfigNode[];
39
+ icon?: ComponentType<SvgIconProps>;
40
+ defaultExpanded?: boolean;
41
+ /** Render a visual divider after this item */
42
+ dividerAfter?: boolean;
43
+ }
44
+ export interface NavConfigSectionHeader {
45
+ type: 'section-header';
46
+ id: string;
47
+ label: string;
48
+ dividerAfter?: boolean;
49
+ }
50
+ export interface NavStackEntry {
51
+ title: string;
52
+ parentLabel: string | null;
53
+ items: NavConfigNode[];
54
+ activeItemId: string | null;
55
+ }
56
+ export interface BreadcrumbSegment {
57
+ type: 'link' | 'scope-switcher' | 'static';
58
+ label: string;
59
+ href?: string;
60
+ /** Present on scope-switcher segments. Current entity parameter value. */
61
+ paramValue?: string;
62
+ /** Present on scope-switcher segments. Built scope items with hrefs for the dropdown. */
63
+ scopeItems?: {
64
+ id: string;
65
+ label: string;
66
+ description?: string;
67
+ href: string;
68
+ }[];
69
+ }
File without changes
@@ -0,0 +1,4 @@
1
+ /** Push a new pathname to the browser history and notify subscribers. */
2
+ export declare function pushPathname(path: string): void;
3
+ /** Subscribe to `window.location.pathname` via `useSyncExternalStore`. */
4
+ export declare function useLocationPathname(): string;
@@ -0,0 +1,24 @@
1
+ import { useSyncExternalStore } from "react";
2
+ let listeners = [];
3
+ function subscribe(listener) {
4
+ listeners = [
5
+ ...listeners,
6
+ listener
7
+ ];
8
+ window.addEventListener('popstate', listener);
9
+ return ()=>{
10
+ listeners = listeners.filter((l)=>l !== listener);
11
+ window.removeEventListener('popstate', listener);
12
+ };
13
+ }
14
+ function getSnapshot() {
15
+ return window.location.pathname;
16
+ }
17
+ function pushPathname(path) {
18
+ window.history.pushState(null, '', path);
19
+ for (const listener of listeners)listener();
20
+ }
21
+ function useLocationPathname() {
22
+ return useSyncExternalStore(subscribe, getSnapshot);
23
+ }
24
+ export { pushPathname, useLocationPathname };
@@ -0,0 +1,16 @@
1
+ import { type Dispatch, type SetStateAction } from 'react';
2
+ import type { BreadcrumbSegment, NavStackEntry } from './types';
3
+ export interface UseProductNavResult {
4
+ navStack: NavStackEntry[];
5
+ peekDepth: number;
6
+ setPeekDepth: Dispatch<SetStateAction<number>>;
7
+ breadcrumbSegments: BreadcrumbSegment[];
8
+ activeItemId: string | null;
9
+ }
10
+ /**
11
+ * Hook providing computed nav state + peekDepth management.
12
+ *
13
+ * Reads computed state from ProductNav context.
14
+ * `peekDepth` is the only local mutable state and resets on pathname changes.
15
+ */
16
+ export declare function useProductNav(): UseProductNavResult;
@@ -0,0 +1,19 @@
1
+ import { useEffect, useState } from "react";
2
+ import { useProductNavContext } from "./ProductNavContext.js";
3
+ function useProductNav() {
4
+ const { pathname, navStack, breadcrumbSegments, activeItemId } = useProductNavContext();
5
+ const [peekDepth, setPeekDepth] = useState(0);
6
+ useEffect(()=>{
7
+ setPeekDepth(0);
8
+ }, [
9
+ pathname
10
+ ]);
11
+ return {
12
+ navStack,
13
+ peekDepth,
14
+ setPeekDepth,
15
+ breadcrumbSegments,
16
+ activeItemId
17
+ };
18
+ }
19
+ export { useProductNav };
@@ -0,0 +1,7 @@
1
+ import type { FC, HTMLAttributes, ReactNode, Ref } from 'react';
2
+ import { type TestableProps } from '../../utils/testId';
3
+ export interface RemoteShellProps extends HTMLAttributes<HTMLDivElement>, TestableProps {
4
+ ref?: Ref<HTMLDivElement>;
5
+ children?: ReactNode;
6
+ }
7
+ export declare const RemoteShell: FC<RemoteShellProps>;
@@ -0,0 +1,16 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { cn } from "../../utils/cn.js";
3
+ import { TestIdProvider } from "../../utils/testId.js";
4
+ const RemoteShell = ({ ref, className, children, 'data-testid': testId, ...props })=>/*#__PURE__*/ jsx(TestIdProvider, {
5
+ value: testId,
6
+ children: /*#__PURE__*/ jsx("div", {
7
+ ...props,
8
+ ref: ref,
9
+ "data-slot": "remote-shell",
10
+ "data-testid": testId,
11
+ className: cn('grid h-full overflow-hidden [grid-template-areas:"panel_breadcrumb""panel_content"] [grid-template-columns:auto_1fr] [grid-template-rows:auto_1fr]', className),
12
+ children: children
13
+ })
14
+ });
15
+ RemoteShell.displayName = 'RemoteShell';
16
+ export { RemoteShell };
@@ -0,0 +1,6 @@
1
+ import type { FC, HTMLAttributes, ReactNode, Ref } from 'react';
2
+ export interface RemoteShellBreadcrumbProps extends HTMLAttributes<HTMLDivElement> {
3
+ ref?: Ref<HTMLDivElement>;
4
+ children?: ReactNode;
5
+ }
6
+ export declare const RemoteShellBreadcrumb: FC<RemoteShellBreadcrumbProps>;
@@ -0,0 +1,16 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { cn } from "../../utils/cn.js";
3
+ import { useTestId } from "../../utils/testId.js";
4
+ const RemoteShellBreadcrumb = ({ ref, className, children, ...props })=>{
5
+ const testId = useTestId('breadcrumb');
6
+ return /*#__PURE__*/ jsx("div", {
7
+ ...props,
8
+ ref: ref,
9
+ "data-slot": "remote-shell-breadcrumb",
10
+ "data-testid": testId,
11
+ className: cn('[grid-area:breadcrumb] flex items-center px-24 py-8', className),
12
+ children: children
13
+ });
14
+ };
15
+ RemoteShellBreadcrumb.displayName = 'RemoteShellBreadcrumb';
16
+ export { RemoteShellBreadcrumb };
@@ -0,0 +1,6 @@
1
+ import type { FC, HTMLAttributes, ReactNode, Ref } from 'react';
2
+ export interface RemoteShellContentProps extends HTMLAttributes<HTMLDivElement> {
3
+ ref?: Ref<HTMLDivElement>;
4
+ children?: ReactNode;
5
+ }
6
+ export declare const RemoteShellContent: FC<RemoteShellContentProps>;
@@ -0,0 +1,16 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { cn } from "../../utils/cn.js";
3
+ import { useTestId } from "../../utils/testId.js";
4
+ const RemoteShellContent = ({ ref, className, children, ...props })=>{
5
+ const testId = useTestId('content');
6
+ return /*#__PURE__*/ jsx("div", {
7
+ ...props,
8
+ ref: ref,
9
+ "data-slot": "remote-shell-content",
10
+ "data-testid": testId,
11
+ className: cn('[grid-area:content] min-h-0 overflow-auto px-24 py-16 [scrollbar-width:thin]', className),
12
+ children: children
13
+ });
14
+ };
15
+ RemoteShellContent.displayName = 'RemoteShellContent';
16
+ export { RemoteShellContent };
@@ -0,0 +1,6 @@
1
+ import type { FC, HTMLAttributes, ReactNode, Ref } from 'react';
2
+ export interface RemoteShellPanelProps extends HTMLAttributes<HTMLDivElement> {
3
+ ref?: Ref<HTMLDivElement>;
4
+ children?: ReactNode;
5
+ }
6
+ export declare const RemoteShellPanel: FC<RemoteShellPanelProps>;
@@ -0,0 +1,16 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { cn } from "../../utils/cn.js";
3
+ import { useTestId } from "../../utils/testId.js";
4
+ const RemoteShellPanel = ({ ref, className, children, ...props })=>{
5
+ const testId = useTestId('panel');
6
+ return /*#__PURE__*/ jsx("div", {
7
+ ...props,
8
+ ref: ref,
9
+ "data-slot": "remote-shell-panel",
10
+ "data-testid": testId,
11
+ className: cn('[grid-area:panel] min-h-0', className),
12
+ children: children
13
+ });
14
+ };
15
+ RemoteShellPanel.displayName = 'RemoteShellPanel';
16
+ export { RemoteShellPanel };
@@ -0,0 +1,4 @@
1
+ export { RemoteShell, type RemoteShellProps } from './RemoteShell';
2
+ export { RemoteShellBreadcrumb, type RemoteShellBreadcrumbProps } from './RemoteShellBreadcrumb';
3
+ export { RemoteShellContent, type RemoteShellContentProps } from './RemoteShellContent';
4
+ export { RemoteShellPanel, type RemoteShellPanelProps } from './RemoteShellPanel';
@@ -0,0 +1,5 @@
1
+ import { RemoteShell } from "./RemoteShell.js";
2
+ import { RemoteShellBreadcrumb } from "./RemoteShellBreadcrumb.js";
3
+ import { RemoteShellContent } from "./RemoteShellContent.js";
4
+ import { RemoteShellPanel } from "./RemoteShellPanel.js";
5
+ export { RemoteShell, RemoteShellBreadcrumb, RemoteShellContent, RemoteShellPanel };
@@ -25,15 +25,15 @@ function LineChart({ data, series, xKey, activeKey: controlledActiveKey, onActiv
25
25
  onActiveKeyChange,
26
26
  seriesByKey
27
27
  });
28
+ const onZoomChangeRef = useRef(onZoomChange);
29
+ onZoomChangeRef.current = onZoomChange;
28
30
  const emitZoom = useCallback((range)=>{
29
- onZoomChange?.(range);
30
- }, [
31
- onZoomChange
32
- ]);
31
+ onZoomChangeRef.current?.(range);
32
+ }, []);
33
33
  const zoom = useLineChartZoomState({
34
34
  data,
35
35
  xKey,
36
- onZoomChange
36
+ onZoomChangeRef
37
37
  });
38
38
  const hiddenSet = useMemo(()=>{
39
39
  if (!filteredKeys?.length) return EMPTY_HIDDEN_SET;
@@ -1,5 +1,5 @@
1
1
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
- import { useContext, useEffect, useMemo, useRef } from "react";
2
+ import { useContext, useEffect, useMemo, useRef, useState } from "react";
3
3
  import { createPortal } from "react-dom";
4
4
  import { ReferenceArea, usePlotArea } from "recharts";
5
5
  import { formatChartDateTime } from "../lib/timeFormatters.js";
@@ -12,22 +12,81 @@ import { LineChartZoomPopoverRange } from "./LineChartZoomPopoverRange.js";
12
12
  import { formatRange as formatRange_js_formatRange } from "./lib/formatRange.js";
13
13
  const defaultFormatRange = formatRange_js_formatRange((value)=>formatChartDateTime(value) || String(value));
14
14
  const POPOVER_OFFSET_X = 12;
15
+ const RECHARTS_SURFACE = '.recharts-surface';
15
16
  const LineChartZoomBrush = ({ disabled = false, formatRange = defaultFormatRange, confirmLabel = 'Zoom in', container })=>{
16
17
  const dataCtx = useContext(LineChartDataContext);
17
18
  const zoomCtx = useContext(LineChartZoomContext);
18
19
  const popoverRef = useRef(null);
19
20
  const plotArea = usePlotArea();
20
21
  const rootRef = zoomCtx?.rootRef;
21
- const centerY = useMemo(()=>{
22
- if (!plotArea || !rootRef?.current) return null;
23
- const surface = rootRef.current.querySelector('.recharts-surface');
24
- if (!surface) return null;
22
+ const drag = zoomCtx?.drag ?? null;
23
+ const pending = zoomCtx?.pending ?? null;
24
+ const cancelPending = zoomCtx?.cancelPending;
25
+ const confirmZoom = zoomCtx?.confirmZoom;
26
+ const isPopoverOpen = null !== drag || null !== pending;
27
+ const isPending = null !== pending;
28
+ const [scrollTick, setScrollTick] = useState(0);
29
+ useEffect(()=>{
30
+ if (!isPopoverOpen) return;
31
+ let lastTop = null;
32
+ let lastLeft = null;
33
+ const onScroll = ()=>{
34
+ const surface = rootRef?.current?.querySelector(RECHARTS_SURFACE);
35
+ if (!surface) return;
36
+ const { top, left } = surface.getBoundingClientRect();
37
+ if (top === lastTop && left === lastLeft) return;
38
+ lastTop = top;
39
+ lastLeft = left;
40
+ setScrollTick((n)=>n + 1);
41
+ };
42
+ window.addEventListener('scroll', onScroll, {
43
+ passive: true,
44
+ capture: true
45
+ });
46
+ window.addEventListener('resize', onScroll);
47
+ return ()=>{
48
+ window.removeEventListener('scroll', onScroll, {
49
+ capture: true
50
+ });
51
+ window.removeEventListener('resize', onScroll);
52
+ };
53
+ }, [
54
+ isPopoverOpen,
55
+ rootRef
56
+ ]);
57
+ const cachedGeometryRef = useRef(null);
58
+ const popoverGeometry = useMemo(()=>{
59
+ if (!isPopoverOpen) {
60
+ cachedGeometryRef.current = null;
61
+ return null;
62
+ }
63
+ if (!plotArea || !rootRef?.current) return cachedGeometryRef.current;
64
+ const surface = rootRef.current.querySelector(RECHARTS_SURFACE);
65
+ if (!surface) return cachedGeometryRef.current;
25
66
  const rect = surface.getBoundingClientRect();
26
- return rect.top + plotArea.y + plotArea.height / 2;
67
+ const next = {
68
+ centerY: rect.top + plotArea.y + plotArea.height / 2,
69
+ left: rect.left
70
+ };
71
+ cachedGeometryRef.current = next;
72
+ return next;
27
73
  }, [
74
+ isPopoverOpen,
28
75
  plotArea,
76
+ rootRef,
77
+ scrollTick
78
+ ]);
79
+ const pendingAnchorLeft = useMemo(()=>{
80
+ if (!isPending || !rootRef?.current) return null;
81
+ const surface = rootRef.current.querySelector(RECHARTS_SURFACE);
82
+ if (!surface) return null;
83
+ return surface.getBoundingClientRect().left;
84
+ }, [
85
+ isPending,
29
86
  rootRef
30
87
  ]);
88
+ const centerY = popoverGeometry?.centerY ?? null;
89
+ const pendingScrollDeltaX = isPending && null !== pendingAnchorLeft && popoverGeometry ? popoverGeometry.left - pendingAnchorLeft : 0;
31
90
  const registerEnabled = zoomCtx?.registerEnabled;
32
91
  useEffect(()=>{
33
92
  if (disabled || !registerEnabled) return;
@@ -36,10 +95,6 @@ const LineChartZoomBrush = ({ disabled = false, formatRange = defaultFormatRange
36
95
  disabled,
37
96
  registerEnabled
38
97
  ]);
39
- const drag = zoomCtx?.drag ?? null;
40
- const pending = zoomCtx?.pending ?? null;
41
- const cancelPending = zoomCtx?.cancelPending;
42
- const confirmZoom = zoomCtx?.confirmZoom;
43
98
  useZoomPendingListeners({
44
99
  enabled: null !== pending,
45
100
  rootRef,
@@ -72,7 +127,6 @@ const LineChartZoomBrush = ({ disabled = false, formatRange = defaultFormatRange
72
127
  ]);
73
128
  if (disabled || !dataCtx || !zoomCtx) return null;
74
129
  const popoverPosition = drag ?? pending ?? null;
75
- const isPending = null !== pending;
76
130
  const popoverContent = range && popoverPosition ? /*#__PURE__*/ jsx("div", {
77
131
  ref: popoverRef,
78
132
  "data-slot": "line-chart-zoom-cursor-popover",
@@ -80,7 +134,7 @@ const LineChartZoomBrush = ({ disabled = false, formatRange = defaultFormatRange
80
134
  className: lineChartZoomCursorPopoverClasses,
81
135
  style: {
82
136
  top: centerY ?? popoverPosition.clientY,
83
- left: popoverPosition.clientX + POPOVER_OFFSET_X,
137
+ left: popoverPosition.clientX + pendingScrollDeltaX + POPOVER_OFFSET_X,
84
138
  transform: 'translateY(-50%)',
85
139
  pointerEvents: isPending ? 'auto' : 'none'
86
140
  },
@@ -16,14 +16,15 @@ const useLineChartActiveKey = ({ controlledActiveKey, onActiveKeyChange, seriesB
16
16
  ]);
17
17
  const lastActiveKeyRef = useRef(void 0);
18
18
  lastActiveKeyRef.current = activeKey;
19
+ const onActiveKeyChangeRef = useRef(onActiveKeyChange);
20
+ onActiveKeyChangeRef.current = onActiveKeyChange;
19
21
  const setActiveKey = useCallback((key)=>{
20
22
  if (lastActiveKeyRef.current === key) return;
21
23
  lastActiveKeyRef.current = key;
22
24
  setInternalActiveKey(key);
23
- onActiveKeyChange?.(key);
25
+ onActiveKeyChangeRef.current?.(key);
24
26
  }, [
25
- setInternalActiveKey,
26
- onActiveKeyChange
27
+ setInternalActiveKey
27
28
  ]);
28
29
  return {
29
30
  activeKey,
@@ -1,3 +1,4 @@
1
+ import { type RefObject } from 'react';
1
2
  import type { LineChartDatum, LineChartZoomDragState, LineChartZoomPendingState, LineChartZoomRange } from '../LineChartContext';
2
3
  interface UseLineChartZoomStateResult {
3
4
  enabled: boolean;
@@ -25,13 +26,19 @@ interface UseLineChartZoomStateResult {
25
26
  * `useZoomDragListeners` so the popover keeps tracking when the cursor leaves
26
27
  * the SVG and a mouseup outside the chart still releases into pending.
27
28
  *
29
+ * Drag updates from recharts (`updateDrag` — carries index+coords) and from
30
+ * the window listener (`handleDragMove` — coords only) both write into a
31
+ * single pending-frame buffer and flush together on the next animation frame.
32
+ * That collapses the two motion paths into one React commit per frame and
33
+ * keeps state writes off the synchronous mousemove path.
34
+ *
28
35
  * Dataset changes invalidate cached indices on both `drag` and `pending`, so
29
36
  * both reset whenever `data` or `xKey` flips — otherwise a stale range could
30
37
  * be committed against a refreshed dataset.
31
38
  */
32
- export declare const useLineChartZoomState: ({ data, xKey, onZoomChange, }: {
39
+ export declare const useLineChartZoomState: ({ data, xKey, onZoomChangeRef, }: {
33
40
  data: LineChartDatum[];
34
41
  xKey: string;
35
- onZoomChange: ((range: LineChartZoomRange | null) => void) | undefined;
42
+ onZoomChangeRef: RefObject<((range: LineChartZoomRange | null) => void) | undefined>;
36
43
  }) => UseLineChartZoomStateResult;
37
44
  export {};