@omiron33/omi-neuron-web 0.2.1 → 0.2.13
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/README.md +103 -2
- package/dist/{NeuronWeb-DjG5-nih.d.ts → NeuronWeb-CZgKU1-X.d.ts} +18 -2
- package/dist/{NeuronWeb-Dy6c4vfH.d.cts → NeuronWeb-JVrxQksC.d.cts} +18 -2
- package/dist/{chunk-GGFSQOFW.cjs → chunk-AU557QID.cjs} +197 -13
- package/dist/chunk-AU557QID.cjs.map +1 -0
- package/dist/{chunk-R6HTCAOY.js → chunk-N44BAVIZ.js} +197 -13
- package/dist/chunk-N44BAVIZ.js.map +1 -0
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/visualization/index.cjs +6 -6
- package/dist/visualization/index.d.cts +2 -2
- package/dist/visualization/index.d.ts +2 -2
- package/dist/visualization/index.js +1 -1
- package/package.json +9 -1
- package/dist/chunk-GGFSQOFW.cjs.map +0 -1
- package/dist/chunk-R6HTCAOY.js.map +0 -1
package/README.md
CHANGED
|
@@ -656,6 +656,7 @@ export interface NeuronWebProps {
|
|
|
656
656
|
error?: string | null;
|
|
657
657
|
selectedNode?: NeuronNode | null;
|
|
658
658
|
focusNodeSlug?: string | null;
|
|
659
|
+
onFocusConsumed?: () => void;
|
|
659
660
|
visibleNodeSlugs?: string[] | null;
|
|
660
661
|
onNodeClick?: (node: NeuronNode) => void;
|
|
661
662
|
onNodeDoubleClick?: (node: NeuronNode) => void;
|
|
@@ -668,6 +669,9 @@ export interface NeuronWebProps {
|
|
|
668
669
|
onStudyPathComplete?: () => void;
|
|
669
670
|
layout?: NeuronLayoutOptions;
|
|
670
671
|
cameraFit?: CameraFitOptions;
|
|
672
|
+
cardsMode?: CardsMode;
|
|
673
|
+
clickCard?: ClickCardOptions;
|
|
674
|
+
clickZoom?: ClickZoomOptions;
|
|
671
675
|
theme?: NeuronWebThemeOverride;
|
|
672
676
|
domainColors?: Record<string, string>;
|
|
673
677
|
renderNodeHover?: (node: NeuronVisualNode) => React.ReactNode;
|
|
@@ -682,9 +686,9 @@ export interface NeuronWebProps {
|
|
|
682
686
|
|
|
683
687
|
Props currently used inside `NeuronWeb` (others are reserved for future use):
|
|
684
688
|
|
|
685
|
-
- Used: `graphData`, `className`, `style`, `fullHeight`, `isFullScreen`, `isLoading`, `error`, `renderEmptyState`, `renderLoadingState`, `ariaLabel`, `theme`, `layout`, `renderNodeHover`, `hoverCard`, `onNodeHover`, `onNodeClick`, `onNodeDoubleClick`, `onNodeFocused`, `onBackgroundClick`, `performanceMode`.
|
|
689
|
+
- Used: `graphData`, `className`, `style`, `fullHeight`, `isFullScreen`, `isLoading`, `error`, `renderEmptyState`, `renderLoadingState`, `ariaLabel`, `theme`, `layout`, `renderNodeHover`, `renderNodeDetail`, `hoverCard`, `clickCard`, `clickZoom`, `cardsMode`, `onNodeHover`, `onNodeClick`, `onNodeDoubleClick`, `onNodeFocused`, `onBackgroundClick`, `performanceMode`, `focusNodeSlug`, `onFocusConsumed`, `visibleNodeSlugs`.
|
|
686
690
|
- Used: `cameraFit` (auto-fit bounds to a viewport fraction).
|
|
687
|
-
- Reserved (declared but not used in the component yet): `selectedNode`, `
|
|
691
|
+
- Reserved (declared but not used in the component yet): `selectedNode`, `onEdgeClick`, `onCameraChange`, `studyPathRequest`, `onStudyPathComplete`, `domainColors`, `graphData.storyBeats`.
|
|
688
692
|
|
|
689
693
|
### Layout modes
|
|
690
694
|
|
|
@@ -730,6 +734,85 @@ When `isFullScreen` is true and `cameraFit.enabled` is not specified, auto-fit i
|
|
|
730
734
|
/>
|
|
731
735
|
```
|
|
732
736
|
|
|
737
|
+
### Programmatic focus (focusNodeSlug)
|
|
738
|
+
|
|
739
|
+
Use `focusNodeSlug` to drive selection + camera focus from outside the component
|
|
740
|
+
(mirrors Technochristian’s “focus a node when something else happens” behavior).
|
|
741
|
+
|
|
742
|
+
Behavior:
|
|
743
|
+
- Looks up by **slug**, with **id fallback**.
|
|
744
|
+
- Sets selection, pulses the node, and emphasizes connected edges.
|
|
745
|
+
- If `clickZoom.enabled` is true (default), the camera tween runs.
|
|
746
|
+
- Fires `onNodeFocused(node)` after the focus tween.
|
|
747
|
+
- Calls `onFocusConsumed()` so the parent can clear the request.
|
|
748
|
+
|
|
749
|
+
```tsx
|
|
750
|
+
<NeuronWeb
|
|
751
|
+
graphData={graphData}
|
|
752
|
+
focusNodeSlug={focusSlug}
|
|
753
|
+
onFocusConsumed={() => setFocusSlug(null)}
|
|
754
|
+
onNodeFocused={(node) => console.log('focused', node.slug)}
|
|
755
|
+
clickZoom={{ enabled: true }}
|
|
756
|
+
/>
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
### Filtered views (visibleNodeSlugs)
|
|
760
|
+
|
|
761
|
+
`visibleNodeSlugs` limits the graph to a subset of nodes (and their edges).
|
|
762
|
+
This is the mechanism Technochristian uses for filtered views and decluttering.
|
|
763
|
+
|
|
764
|
+
Semantics:
|
|
765
|
+
- `null` or `undefined` → show **all** nodes/edges.
|
|
766
|
+
- `[]` (empty array) → show **none**.
|
|
767
|
+
- Filters nodes by **slug or id**; edges are kept only if **both endpoints** are visible.
|
|
768
|
+
- `storyBeats` (if present) are filtered to beats with ≥ 2 visible nodeIds.
|
|
769
|
+
- A subtle fade/scale/blur transition is applied on each filter change.
|
|
770
|
+
- If the currently selected node disappears, selection is cleared.
|
|
771
|
+
|
|
772
|
+
```tsx
|
|
773
|
+
// Show only a curated slice
|
|
774
|
+
<NeuronWeb
|
|
775
|
+
graphData={graphData}
|
|
776
|
+
visibleNodeSlugs={['uap', 'neph', 'jude6']}
|
|
777
|
+
/>;
|
|
778
|
+
|
|
779
|
+
// Show everything (default)
|
|
780
|
+
<NeuronWeb graphData={graphData} visibleNodeSlugs={null} />;
|
|
781
|
+
|
|
782
|
+
// Hide everything
|
|
783
|
+
<NeuronWeb graphData={graphData} visibleNodeSlugs={[]} />;
|
|
784
|
+
```
|
|
785
|
+
|
|
786
|
+
### Click cards + click zoom
|
|
787
|
+
|
|
788
|
+
Enable a persistent card on click and optional zoom-to-node behavior:
|
|
789
|
+
|
|
790
|
+
```tsx
|
|
791
|
+
<NeuronWeb
|
|
792
|
+
graphData={graphData}
|
|
793
|
+
clickCard={{ enabled: true, width: 320, offset: [24, 24] }}
|
|
794
|
+
clickZoom={{ enabled: true }}
|
|
795
|
+
/>
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
### Card mode (global override)
|
|
799
|
+
|
|
800
|
+
`cardsMode` lets you force card behavior irrespective of `hoverCard.enabled` or `clickCard.enabled`
|
|
801
|
+
(when `cardsMode` is set, it wins).
|
|
802
|
+
|
|
803
|
+
```tsx
|
|
804
|
+
<NeuronWeb graphData={graphData} cardsMode="none" />
|
|
805
|
+
<NeuronWeb graphData={graphData} cardsMode="hover" />
|
|
806
|
+
<NeuronWeb graphData={graphData} cardsMode="click" />
|
|
807
|
+
<NeuronWeb graphData={graphData} cardsMode="both" />
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
Disable click cards or zoom:
|
|
811
|
+
|
|
812
|
+
```tsx
|
|
813
|
+
<NeuronWeb graphData={graphData} clickCard={{ enabled: false }} clickZoom={{ enabled: false }} />
|
|
814
|
+
```
|
|
815
|
+
|
|
733
816
|
To disable in fullscreen:
|
|
734
817
|
|
|
735
818
|
```tsx
|
|
@@ -814,6 +897,24 @@ pnpm lint
|
|
|
814
897
|
pnpm test
|
|
815
898
|
```
|
|
816
899
|
|
|
900
|
+
## Release via tags (automation)
|
|
901
|
+
|
|
902
|
+
This repo publishes on **tag push**. The tag version is authoritative and must be a **patch bump**.
|
|
903
|
+
|
|
904
|
+
Workflow:
|
|
905
|
+
- Create and push `vX.Y.Z` (same major/minor as `package.json`, patch +1).
|
|
906
|
+
- GitHub Actions updates `package.json` + `src/index.ts`, commits to `main`, builds, and publishes.
|
|
907
|
+
|
|
908
|
+
Helper skill (repo-local):
|
|
909
|
+
|
|
910
|
+
```bash
|
|
911
|
+
skills/tagged-npm-release/scripts/create_patch_tag.sh
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
Authentication:
|
|
915
|
+
- **Preferred**: npm **Trusted Publishing** (OIDC). Configure the GitHub repo/workflow in npm package settings. No secret required.
|
|
916
|
+
- **Legacy fallback**: set `NPM_TOKEN` in GitHub repo secrets and remove `--provenance` from the workflow.
|
|
917
|
+
|
|
817
918
|
## License
|
|
818
919
|
|
|
819
920
|
MIT
|
|
@@ -74,6 +74,15 @@ interface HoverCardOptions {
|
|
|
74
74
|
width?: number;
|
|
75
75
|
offset?: [number, number];
|
|
76
76
|
}
|
|
77
|
+
interface ClickCardOptions {
|
|
78
|
+
enabled?: boolean;
|
|
79
|
+
width?: number;
|
|
80
|
+
offset?: [number, number];
|
|
81
|
+
}
|
|
82
|
+
interface ClickZoomOptions {
|
|
83
|
+
enabled?: boolean;
|
|
84
|
+
}
|
|
85
|
+
type CardsMode = 'none' | 'hover' | 'click' | 'both';
|
|
77
86
|
interface CameraFitOptions {
|
|
78
87
|
/** Enable auto-fitting camera to node bounds */
|
|
79
88
|
enabled?: boolean;
|
|
@@ -97,7 +106,11 @@ interface NeuronWebProps {
|
|
|
97
106
|
isLoading?: boolean;
|
|
98
107
|
error?: string | null;
|
|
99
108
|
selectedNode?: NeuronNode | null;
|
|
109
|
+
/** Programmatically focus/select a node by slug (or id fallback). */
|
|
100
110
|
focusNodeSlug?: string | null;
|
|
111
|
+
/** Called after a focusNodeSlug request is processed. */
|
|
112
|
+
onFocusConsumed?: () => void;
|
|
113
|
+
/** Limit the rendered graph to these node slugs/ids; null shows all, empty array shows none. */
|
|
101
114
|
visibleNodeSlugs?: string[] | null;
|
|
102
115
|
onNodeClick?: (node: NeuronNode) => void;
|
|
103
116
|
onNodeDoubleClick?: (node: NeuronNode) => void;
|
|
@@ -110,6 +123,9 @@ interface NeuronWebProps {
|
|
|
110
123
|
onStudyPathComplete?: () => void;
|
|
111
124
|
layout?: NeuronLayoutOptions;
|
|
112
125
|
cameraFit?: CameraFitOptions;
|
|
126
|
+
cardsMode?: CardsMode;
|
|
127
|
+
clickCard?: ClickCardOptions;
|
|
128
|
+
clickZoom?: ClickZoomOptions;
|
|
113
129
|
theme?: NeuronWebThemeOverride;
|
|
114
130
|
domainColors?: Record<string, string>;
|
|
115
131
|
renderNodeHover?: (node: NeuronVisualNode) => React__default.ReactNode;
|
|
@@ -121,6 +137,6 @@ interface NeuronWebProps {
|
|
|
121
137
|
ariaLabel?: string;
|
|
122
138
|
}
|
|
123
139
|
|
|
124
|
-
declare function NeuronWeb({ graphData, className, style, fullHeight, isFullScreen, isLoading, error, renderEmptyState, renderLoadingState, ariaLabel, theme, layout, cameraFit, renderNodeHover, hoverCard, onNodeHover, onNodeClick, onNodeDoubleClick, onNodeFocused, onBackgroundClick, performanceMode, }: NeuronWebProps): React__default.ReactElement;
|
|
140
|
+
declare function NeuronWeb({ graphData, className, style, fullHeight, isFullScreen, isLoading, error, focusNodeSlug, onFocusConsumed, visibleNodeSlugs, renderEmptyState, renderLoadingState, ariaLabel, theme, layout, cameraFit, cardsMode, clickCard, clickZoom, renderNodeHover, renderNodeDetail, hoverCard, onNodeHover, onNodeClick, onNodeDoubleClick, onNodeFocused, onBackgroundClick, performanceMode, }: NeuronWebProps): React__default.ReactElement;
|
|
125
141
|
|
|
126
|
-
export { type CameraFitOptions as C, type HoverCardOptions as H, type NeuronWebTheme as N, type NeuronWebThemeOverride as a, type NeuronLayoutOptions as b, NeuronWeb as c, type NeuronWebProps as d, type NeuronLayoutMode as e };
|
|
142
|
+
export { type CameraFitOptions as C, type HoverCardOptions as H, type NeuronWebTheme as N, type NeuronWebThemeOverride as a, type NeuronLayoutOptions as b, NeuronWeb as c, type NeuronWebProps as d, type NeuronLayoutMode as e, type ClickCardOptions as f, type ClickZoomOptions as g, type CardsMode as h };
|
|
@@ -74,6 +74,15 @@ interface HoverCardOptions {
|
|
|
74
74
|
width?: number;
|
|
75
75
|
offset?: [number, number];
|
|
76
76
|
}
|
|
77
|
+
interface ClickCardOptions {
|
|
78
|
+
enabled?: boolean;
|
|
79
|
+
width?: number;
|
|
80
|
+
offset?: [number, number];
|
|
81
|
+
}
|
|
82
|
+
interface ClickZoomOptions {
|
|
83
|
+
enabled?: boolean;
|
|
84
|
+
}
|
|
85
|
+
type CardsMode = 'none' | 'hover' | 'click' | 'both';
|
|
77
86
|
interface CameraFitOptions {
|
|
78
87
|
/** Enable auto-fitting camera to node bounds */
|
|
79
88
|
enabled?: boolean;
|
|
@@ -97,7 +106,11 @@ interface NeuronWebProps {
|
|
|
97
106
|
isLoading?: boolean;
|
|
98
107
|
error?: string | null;
|
|
99
108
|
selectedNode?: NeuronNode | null;
|
|
109
|
+
/** Programmatically focus/select a node by slug (or id fallback). */
|
|
100
110
|
focusNodeSlug?: string | null;
|
|
111
|
+
/** Called after a focusNodeSlug request is processed. */
|
|
112
|
+
onFocusConsumed?: () => void;
|
|
113
|
+
/** Limit the rendered graph to these node slugs/ids; null shows all, empty array shows none. */
|
|
101
114
|
visibleNodeSlugs?: string[] | null;
|
|
102
115
|
onNodeClick?: (node: NeuronNode) => void;
|
|
103
116
|
onNodeDoubleClick?: (node: NeuronNode) => void;
|
|
@@ -110,6 +123,9 @@ interface NeuronWebProps {
|
|
|
110
123
|
onStudyPathComplete?: () => void;
|
|
111
124
|
layout?: NeuronLayoutOptions;
|
|
112
125
|
cameraFit?: CameraFitOptions;
|
|
126
|
+
cardsMode?: CardsMode;
|
|
127
|
+
clickCard?: ClickCardOptions;
|
|
128
|
+
clickZoom?: ClickZoomOptions;
|
|
113
129
|
theme?: NeuronWebThemeOverride;
|
|
114
130
|
domainColors?: Record<string, string>;
|
|
115
131
|
renderNodeHover?: (node: NeuronVisualNode) => React__default.ReactNode;
|
|
@@ -121,6 +137,6 @@ interface NeuronWebProps {
|
|
|
121
137
|
ariaLabel?: string;
|
|
122
138
|
}
|
|
123
139
|
|
|
124
|
-
declare function NeuronWeb({ graphData, className, style, fullHeight, isFullScreen, isLoading, error, renderEmptyState, renderLoadingState, ariaLabel, theme, layout, cameraFit, renderNodeHover, hoverCard, onNodeHover, onNodeClick, onNodeDoubleClick, onNodeFocused, onBackgroundClick, performanceMode, }: NeuronWebProps): React__default.ReactElement;
|
|
140
|
+
declare function NeuronWeb({ graphData, className, style, fullHeight, isFullScreen, isLoading, error, focusNodeSlug, onFocusConsumed, visibleNodeSlugs, renderEmptyState, renderLoadingState, ariaLabel, theme, layout, cameraFit, cardsMode, clickCard, clickZoom, renderNodeHover, renderNodeDetail, hoverCard, onNodeHover, onNodeClick, onNodeDoubleClick, onNodeFocused, onBackgroundClick, performanceMode, }: NeuronWebProps): React__default.ReactElement;
|
|
125
141
|
|
|
126
|
-
export { type CameraFitOptions as C, type HoverCardOptions as H, type NeuronWebTheme as N, type NeuronWebThemeOverride as a, type NeuronLayoutOptions as b, NeuronWeb as c, type NeuronWebProps as d, type NeuronLayoutMode as e };
|
|
142
|
+
export { type CameraFitOptions as C, type HoverCardOptions as H, type NeuronWebTheme as N, type NeuronWebThemeOverride as a, type NeuronLayoutOptions as b, NeuronWeb as c, type NeuronWebProps as d, type NeuronLayoutMode as e, type ClickCardOptions as f, type ClickZoomOptions as g, type CardsMode as h };
|
|
@@ -1049,13 +1049,20 @@ function NeuronWeb({
|
|
|
1049
1049
|
isFullScreen,
|
|
1050
1050
|
isLoading,
|
|
1051
1051
|
error,
|
|
1052
|
+
focusNodeSlug,
|
|
1053
|
+
onFocusConsumed,
|
|
1054
|
+
visibleNodeSlugs,
|
|
1052
1055
|
renderEmptyState,
|
|
1053
1056
|
renderLoadingState,
|
|
1054
1057
|
ariaLabel,
|
|
1055
1058
|
theme,
|
|
1056
1059
|
layout,
|
|
1057
1060
|
cameraFit,
|
|
1061
|
+
cardsMode,
|
|
1062
|
+
clickCard,
|
|
1063
|
+
clickZoom,
|
|
1058
1064
|
renderNodeHover,
|
|
1065
|
+
renderNodeDetail,
|
|
1059
1066
|
hoverCard,
|
|
1060
1067
|
onNodeHover,
|
|
1061
1068
|
onNodeClick,
|
|
@@ -1066,9 +1073,34 @@ function NeuronWeb({
|
|
|
1066
1073
|
}) {
|
|
1067
1074
|
const containerRef = react.useRef(null);
|
|
1068
1075
|
const hoverCardRef = react.useRef(null);
|
|
1076
|
+
const clickCardRef = react.useRef(null);
|
|
1069
1077
|
const [hoveredNodeId, setHoveredNodeId] = react.useState(null);
|
|
1070
1078
|
const [selectedNodeId, setSelectedNodeId] = react.useState(null);
|
|
1071
1079
|
const fitStateRef = react.useRef({ hasFit: false, signature: "" });
|
|
1080
|
+
const firstFilterChangeRef = react.useRef(true);
|
|
1081
|
+
const [filterTransitioning, setFilterTransitioning] = react.useState(false);
|
|
1082
|
+
const filteredGraphData = react.useMemo(() => {
|
|
1083
|
+
if (visibleNodeSlugs === null || visibleNodeSlugs === void 0) {
|
|
1084
|
+
return graphData;
|
|
1085
|
+
}
|
|
1086
|
+
const allowed = new Set(visibleNodeSlugs);
|
|
1087
|
+
const filteredNodes = graphData.nodes.filter(
|
|
1088
|
+
(node) => allowed.has(node.slug) || allowed.has(node.id)
|
|
1089
|
+
);
|
|
1090
|
+
const filteredEdges = graphData.edges.filter(
|
|
1091
|
+
(edge) => allowed.has(edge.from) && allowed.has(edge.to)
|
|
1092
|
+
);
|
|
1093
|
+
const filteredStoryBeats = graphData.storyBeats?.map((beat) => ({
|
|
1094
|
+
...beat,
|
|
1095
|
+
nodeIds: beat.nodeIds.filter((id) => allowed.has(id))
|
|
1096
|
+
})).filter((beat) => beat.nodeIds.length >= 2);
|
|
1097
|
+
return {
|
|
1098
|
+
...graphData,
|
|
1099
|
+
nodes: filteredNodes,
|
|
1100
|
+
edges: filteredEdges,
|
|
1101
|
+
storyBeats: filteredStoryBeats
|
|
1102
|
+
};
|
|
1103
|
+
}, [graphData, visibleNodeSlugs]);
|
|
1072
1104
|
const resolvedTheme = react.useMemo(
|
|
1073
1105
|
() => ({
|
|
1074
1106
|
...DEFAULT_THEME,
|
|
@@ -1079,13 +1111,27 @@ function NeuronWeb({
|
|
|
1079
1111
|
}),
|
|
1080
1112
|
[theme]
|
|
1081
1113
|
);
|
|
1114
|
+
const filterSignature = react.useMemo(() => {
|
|
1115
|
+
if (visibleNodeSlugs === null || visibleNodeSlugs === void 0) return "all";
|
|
1116
|
+
return visibleNodeSlugs.length ? visibleNodeSlugs.join("|") : "none";
|
|
1117
|
+
}, [visibleNodeSlugs]);
|
|
1118
|
+
react.useEffect(() => {
|
|
1119
|
+
if (firstFilterChangeRef.current) {
|
|
1120
|
+
firstFilterChangeRef.current = false;
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
setFilterTransitioning(true);
|
|
1124
|
+
const timer = setTimeout(() => setFilterTransitioning(false), resolvedTheme.animation.transitionDuration);
|
|
1125
|
+
return () => clearTimeout(timer);
|
|
1126
|
+
}, [filterSignature, resolvedTheme.animation.transitionDuration]);
|
|
1127
|
+
const workingGraph = filteredGraphData;
|
|
1082
1128
|
const resolvedPerformanceMode = react.useMemo(() => {
|
|
1083
1129
|
if (performanceMode && performanceMode !== "auto") return performanceMode;
|
|
1084
|
-
const count =
|
|
1130
|
+
const count = workingGraph.nodes.length;
|
|
1085
1131
|
if (count > 360) return "fallback";
|
|
1086
1132
|
if (count > 180) return "degraded";
|
|
1087
1133
|
return "normal";
|
|
1088
|
-
}, [performanceMode,
|
|
1134
|
+
}, [performanceMode, workingGraph.nodes.length]);
|
|
1089
1135
|
const sceneManager = useSceneManager(containerRef, {
|
|
1090
1136
|
backgroundColor: resolvedTheme.colors.background,
|
|
1091
1137
|
cameraFov: 52,
|
|
@@ -1166,8 +1212,8 @@ function NeuronWeb({
|
|
|
1166
1212
|
});
|
|
1167
1213
|
}, [sceneManager, resolvedTheme]);
|
|
1168
1214
|
const resolvedNodes = react.useMemo(
|
|
1169
|
-
() => applyFuzzyLayout(
|
|
1170
|
-
[
|
|
1215
|
+
() => applyFuzzyLayout(workingGraph.nodes, layout),
|
|
1216
|
+
[workingGraph.nodes, layout]
|
|
1171
1217
|
);
|
|
1172
1218
|
const resolvedCameraFit = react.useMemo(() => {
|
|
1173
1219
|
const enabled = cameraFit?.enabled ?? Boolean(isFullScreen);
|
|
@@ -1193,7 +1239,7 @@ function NeuronWeb({
|
|
|
1193
1239
|
}, [resolvedNodes]);
|
|
1194
1240
|
const edgesBySlug = react.useMemo(() => {
|
|
1195
1241
|
const map = /* @__PURE__ */ new Map();
|
|
1196
|
-
|
|
1242
|
+
workingGraph.edges.forEach((edge) => {
|
|
1197
1243
|
const add = (slug) => {
|
|
1198
1244
|
const list = map.get(slug);
|
|
1199
1245
|
if (list) list.push(edge.id);
|
|
@@ -1203,11 +1249,23 @@ function NeuronWeb({
|
|
|
1203
1249
|
add(edge.to);
|
|
1204
1250
|
});
|
|
1205
1251
|
return map;
|
|
1206
|
-
}, [
|
|
1252
|
+
}, [workingGraph.edges]);
|
|
1253
|
+
const nodeByIdentifier = react.useMemo(() => {
|
|
1254
|
+
const map = /* @__PURE__ */ new Map();
|
|
1255
|
+
resolvedNodes.forEach((node) => {
|
|
1256
|
+
map.set(node.slug, node);
|
|
1257
|
+
map.set(node.id, node);
|
|
1258
|
+
});
|
|
1259
|
+
return map;
|
|
1260
|
+
}, [resolvedNodes]);
|
|
1207
1261
|
const hoveredNode = hoveredNodeId ? nodeMap.get(hoveredNodeId) ?? null : null;
|
|
1208
|
-
const hoverCardEnabled = (hoverCard?.enabled ?? true) && resolvedPerformanceMode !== "fallback";
|
|
1262
|
+
const hoverCardEnabled = (cardsMode ? cardsMode === "hover" || cardsMode === "both" : hoverCard?.enabled ?? true) && resolvedPerformanceMode !== "fallback";
|
|
1209
1263
|
const hoverCardOffset = hoverCard?.offset ?? [18, 18];
|
|
1210
1264
|
const hoverCardWidth = hoverCard?.width ?? 240;
|
|
1265
|
+
const clickCardEnabled = (cardsMode ? cardsMode === "click" || cardsMode === "both" : clickCard?.enabled ?? false) && resolvedPerformanceMode !== "fallback";
|
|
1266
|
+
const clickCardOffset = clickCard?.offset ?? [24, 24];
|
|
1267
|
+
const clickCardWidth = clickCard?.width ?? 320;
|
|
1268
|
+
const clickZoomEnabled = clickZoom?.enabled ?? true;
|
|
1211
1269
|
react.useEffect(() => {
|
|
1212
1270
|
if (!sceneManager || !nodeRenderer || !edgeRenderer) return;
|
|
1213
1271
|
nodeRenderer.renderNodes(resolvedNodes);
|
|
@@ -1216,8 +1274,8 @@ function NeuronWeb({
|
|
|
1216
1274
|
if (!node.position) return;
|
|
1217
1275
|
positions.set(node.slug, new THREE__namespace.Vector3(...node.position));
|
|
1218
1276
|
});
|
|
1219
|
-
edgeRenderer.renderEdges(
|
|
1220
|
-
}, [resolvedNodes,
|
|
1277
|
+
edgeRenderer.renderEdges(workingGraph.edges, positions);
|
|
1278
|
+
}, [resolvedNodes, workingGraph.edges, sceneManager, nodeRenderer, edgeRenderer]);
|
|
1221
1279
|
react.useEffect(() => {
|
|
1222
1280
|
if (!sceneManager) return;
|
|
1223
1281
|
sceneManager.updateBackground(resolvedTheme.colors.background);
|
|
@@ -1283,6 +1341,22 @@ function NeuronWeb({
|
|
|
1283
1341
|
hoverCardRef.current.style.transform = `translate(${x}px, ${y}px)`;
|
|
1284
1342
|
}
|
|
1285
1343
|
}
|
|
1344
|
+
if (clickCardRef.current && selectedNodeId) {
|
|
1345
|
+
const position = nodeRenderer.getNodePosition(selectedNodeId);
|
|
1346
|
+
const rect = containerRef.current?.getBoundingClientRect();
|
|
1347
|
+
if (position && rect) {
|
|
1348
|
+
const screen = sceneManager.worldToScreen(position);
|
|
1349
|
+
const cardWidth = clickCardRef.current.offsetWidth;
|
|
1350
|
+
const cardHeight = clickCardRef.current.offsetHeight;
|
|
1351
|
+
const rawX = screen.x - rect.left + clickCardOffset[0];
|
|
1352
|
+
const rawY = screen.y - rect.top + clickCardOffset[1];
|
|
1353
|
+
const maxX = Math.max(8, rect.width - cardWidth - 8);
|
|
1354
|
+
const maxY = Math.max(8, rect.height - cardHeight - 8);
|
|
1355
|
+
const x = Math.min(Math.max(rawX, 8), maxX);
|
|
1356
|
+
const y = Math.min(Math.max(rawY, 8), maxY);
|
|
1357
|
+
clickCardRef.current.style.transform = `translate(${x}px, ${y}px)`;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1286
1360
|
});
|
|
1287
1361
|
}, [
|
|
1288
1362
|
sceneManager,
|
|
@@ -1290,12 +1364,64 @@ function NeuronWeb({
|
|
|
1290
1364
|
edgeRenderer,
|
|
1291
1365
|
animationController,
|
|
1292
1366
|
hoveredNodeId,
|
|
1293
|
-
hoverCardOffset
|
|
1367
|
+
hoverCardOffset,
|
|
1368
|
+
selectedNodeId,
|
|
1369
|
+
clickCardOffset
|
|
1294
1370
|
]);
|
|
1295
1371
|
react.useEffect(() => {
|
|
1296
1372
|
if (!interactionManager || !nodeRenderer) return;
|
|
1297
1373
|
interactionManager.setTargets(nodeRenderer.getNodeObjects(), nodeMap);
|
|
1298
1374
|
}, [interactionManager, nodeRenderer, nodeMap]);
|
|
1375
|
+
react.useEffect(() => {
|
|
1376
|
+
if (!nodeRenderer) return;
|
|
1377
|
+
if (hoveredNodeId && !nodeMap.has(hoveredNodeId)) {
|
|
1378
|
+
setHoveredNodeId(null);
|
|
1379
|
+
nodeRenderer.setHoveredNode(null);
|
|
1380
|
+
}
|
|
1381
|
+
}, [hoveredNodeId, nodeMap, nodeRenderer]);
|
|
1382
|
+
react.useEffect(() => {
|
|
1383
|
+
if (!nodeRenderer || !edgeRenderer) return;
|
|
1384
|
+
if (selectedNodeId && !nodeMap.has(selectedNodeId)) {
|
|
1385
|
+
setSelectedNodeId(null);
|
|
1386
|
+
nodeRenderer.setSelectedNode(null);
|
|
1387
|
+
edgeRenderer.setFocusEdges(null);
|
|
1388
|
+
}
|
|
1389
|
+
}, [selectedNodeId, nodeMap, nodeRenderer, edgeRenderer]);
|
|
1390
|
+
react.useEffect(() => {
|
|
1391
|
+
if (!focusNodeSlug || !nodeRenderer || !edgeRenderer) return;
|
|
1392
|
+
const node = nodeByIdentifier.get(focusNodeSlug);
|
|
1393
|
+
if (!node) {
|
|
1394
|
+
onFocusConsumed?.();
|
|
1395
|
+
return;
|
|
1396
|
+
}
|
|
1397
|
+
setSelectedNodeId(node.id);
|
|
1398
|
+
nodeRenderer.setSelectedNode(node.id);
|
|
1399
|
+
nodeRenderer.pulseNode(node.id);
|
|
1400
|
+
edgeRenderer.setFocusEdges(node.slug ? edgesBySlug.get(node.slug) ?? [] : []);
|
|
1401
|
+
if (clickZoomEnabled) {
|
|
1402
|
+
const nodePosition = nodeRenderer.getNodePosition(node.id);
|
|
1403
|
+
if (nodePosition) {
|
|
1404
|
+
animationController?.focusOnNode(nodePosition, () => {
|
|
1405
|
+
if (onNodeFocused) onNodeFocused(node);
|
|
1406
|
+
});
|
|
1407
|
+
} else if (onNodeFocused) {
|
|
1408
|
+
onNodeFocused(node);
|
|
1409
|
+
}
|
|
1410
|
+
} else if (onNodeFocused) {
|
|
1411
|
+
onNodeFocused(node);
|
|
1412
|
+
}
|
|
1413
|
+
onFocusConsumed?.();
|
|
1414
|
+
}, [
|
|
1415
|
+
focusNodeSlug,
|
|
1416
|
+
nodeByIdentifier,
|
|
1417
|
+
nodeRenderer,
|
|
1418
|
+
edgeRenderer,
|
|
1419
|
+
edgesBySlug,
|
|
1420
|
+
clickZoomEnabled,
|
|
1421
|
+
animationController,
|
|
1422
|
+
onNodeFocused,
|
|
1423
|
+
onFocusConsumed
|
|
1424
|
+
]);
|
|
1299
1425
|
react.useEffect(() => {
|
|
1300
1426
|
if (!interactionManager || !nodeRenderer || !edgeRenderer) return;
|
|
1301
1427
|
interactionManager.onNodeHover = (node) => {
|
|
@@ -1321,6 +1447,14 @@ function NeuronWeb({
|
|
|
1321
1447
|
setSelectedNodeId(node.id);
|
|
1322
1448
|
nodeRenderer.setSelectedNode(node.id);
|
|
1323
1449
|
nodeRenderer.pulseNode(node.id);
|
|
1450
|
+
if (clickZoomEnabled) {
|
|
1451
|
+
const nodePosition = nodeRenderer.getNodePosition(node.id);
|
|
1452
|
+
if (nodePosition) {
|
|
1453
|
+
animationController?.focusOnNode(nodePosition, () => {
|
|
1454
|
+
if (onNodeFocused) onNodeFocused(node);
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1324
1458
|
const slug = nodeSlugById.get(node.id);
|
|
1325
1459
|
edgeRenderer.setFocusEdges(slug ? edgesBySlug.get(slug) ?? [] : []);
|
|
1326
1460
|
if (onNodeClick) {
|
|
@@ -1355,6 +1489,7 @@ function NeuronWeb({
|
|
|
1355
1489
|
animationController,
|
|
1356
1490
|
hoveredNodeId,
|
|
1357
1491
|
selectedNodeId,
|
|
1492
|
+
clickZoomEnabled,
|
|
1358
1493
|
onNodeHover,
|
|
1359
1494
|
onNodeClick,
|
|
1360
1495
|
onNodeDoubleClick,
|
|
@@ -1385,6 +1520,27 @@ function NeuronWeb({
|
|
|
1385
1520
|
if (!resolvedNodes.length) {
|
|
1386
1521
|
return /* @__PURE__ */ jsxRuntime.jsx("div", { className, style, "aria-label": ariaLabel, children: renderEmptyState ? renderEmptyState() : /* @__PURE__ */ jsxRuntime.jsx("div", { children: "No data" }) });
|
|
1387
1522
|
}
|
|
1523
|
+
const sceneTransitionStyle = {
|
|
1524
|
+
position: "absolute",
|
|
1525
|
+
inset: 0,
|
|
1526
|
+
width: "100%",
|
|
1527
|
+
height: "100%",
|
|
1528
|
+
transition: `opacity ${resolvedTheme.animation.transitionDuration}ms ease, transform ${resolvedTheme.animation.transitionDuration}ms ease, filter ${resolvedTheme.animation.transitionDuration}ms ease`,
|
|
1529
|
+
opacity: filterTransitioning ? 0.85 : 1,
|
|
1530
|
+
transform: filterTransitioning ? "scale(0.985)" : "scale(1)",
|
|
1531
|
+
filter: filterTransitioning ? "blur(1px)" : "blur(0px)",
|
|
1532
|
+
transformOrigin: "center",
|
|
1533
|
+
willChange: "transform, filter, opacity"
|
|
1534
|
+
};
|
|
1535
|
+
const filterOverlayStyle = {
|
|
1536
|
+
position: "absolute",
|
|
1537
|
+
inset: 0,
|
|
1538
|
+
pointerEvents: "none",
|
|
1539
|
+
background: "radial-gradient(circle at 50% 35%, rgba(255,255,255,0.35), transparent 60%)",
|
|
1540
|
+
opacity: filterTransitioning ? 1 : 0,
|
|
1541
|
+
transition: `opacity ${resolvedTheme.animation.transitionDuration}ms ease`,
|
|
1542
|
+
zIndex: 2
|
|
1543
|
+
};
|
|
1388
1544
|
const resolvedStyle = {
|
|
1389
1545
|
position: isFullScreen ? "fixed" : "relative",
|
|
1390
1546
|
inset: isFullScreen ? 0 : void 0,
|
|
@@ -1406,9 +1562,10 @@ function NeuronWeb({
|
|
|
1406
1562
|
"div",
|
|
1407
1563
|
{
|
|
1408
1564
|
ref: containerRef,
|
|
1409
|
-
style:
|
|
1565
|
+
style: sceneTransitionStyle
|
|
1410
1566
|
}
|
|
1411
1567
|
),
|
|
1568
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: filterOverlayStyle }),
|
|
1412
1569
|
hoverCardEnabled && hoveredNode && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1413
1570
|
"div",
|
|
1414
1571
|
{
|
|
@@ -1435,6 +1592,33 @@ function NeuronWeb({
|
|
|
1435
1592
|
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { opacity: 0.75 }, children: typeof hoveredNode.metadata?.summary === "string" ? hoveredNode.metadata.summary : "Click to focus this node and explore connections." })
|
|
1436
1593
|
] })
|
|
1437
1594
|
}
|
|
1595
|
+
),
|
|
1596
|
+
clickCardEnabled && selectedNodeId && /* @__PURE__ */ jsxRuntime.jsx(
|
|
1597
|
+
"div",
|
|
1598
|
+
{
|
|
1599
|
+
ref: clickCardRef,
|
|
1600
|
+
style: {
|
|
1601
|
+
position: "absolute",
|
|
1602
|
+
width: clickCardWidth,
|
|
1603
|
+
pointerEvents: "auto",
|
|
1604
|
+
padding: "14px 16px",
|
|
1605
|
+
borderRadius: 14,
|
|
1606
|
+
background: "linear-gradient(140deg, rgba(10, 14, 32, 0.98) 0%, rgba(20, 26, 58, 0.94) 100%)",
|
|
1607
|
+
border: "1px solid rgba(140, 170, 255, 0.4)",
|
|
1608
|
+
boxShadow: "0 22px 60px rgba(5, 10, 30, 0.6)",
|
|
1609
|
+
color: resolvedTheme.colors.labelText,
|
|
1610
|
+
fontFamily: resolvedTheme.typography.labelFontFamily,
|
|
1611
|
+
fontSize: 13,
|
|
1612
|
+
zIndex: 5,
|
|
1613
|
+
opacity: 0.98,
|
|
1614
|
+
transition: `opacity ${resolvedTheme.animation.hoverCardFadeDuration}ms ease`,
|
|
1615
|
+
transform: `translate(${clickCardOffset[0]}px, ${clickCardOffset[1]}px)`
|
|
1616
|
+
},
|
|
1617
|
+
children: renderNodeDetail ? renderNodeDetail(nodeMap.get(selectedNodeId)) : /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
|
|
1618
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { fontSize: 14, fontWeight: 600, marginBottom: 8 }, children: nodeMap.get(selectedNodeId)?.label ?? "Selected node" }),
|
|
1619
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { style: { opacity: 0.75 }, children: typeof nodeMap.get(selectedNodeId)?.metadata?.summary === "string" ? nodeMap.get(selectedNodeId)?.metadata?.summary : "Click another node to explore more details." })
|
|
1620
|
+
] })
|
|
1621
|
+
}
|
|
1438
1622
|
)
|
|
1439
1623
|
]
|
|
1440
1624
|
}
|
|
@@ -1511,5 +1695,5 @@ exports.NeuronWeb = NeuronWeb;
|
|
|
1511
1695
|
exports.SceneManager = SceneManager;
|
|
1512
1696
|
exports.ThemeEngine = ThemeEngine;
|
|
1513
1697
|
exports.applyFuzzyLayout = applyFuzzyLayout;
|
|
1514
|
-
//# sourceMappingURL=chunk-
|
|
1515
|
-
//# sourceMappingURL=chunk-
|
|
1698
|
+
//# sourceMappingURL=chunk-AU557QID.cjs.map
|
|
1699
|
+
//# sourceMappingURL=chunk-AU557QID.cjs.map
|