@johndimm/constellations 1.0.0 → 1.0.2
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/App.tsx +352 -70
- package/FullPageConstellations.tsx +7 -5
- package/components/AppConfirmDialog.tsx +1 -0
- package/components/AppHeader.tsx +69 -29
- package/components/AppNotifications.tsx +1 -0
- package/components/BrowsePeople.tsx +3 -0
- package/components/ControlPanel.tsx +46 -371
- package/components/Graph.tsx +251 -87
- package/components/HelpOverlay.tsx +1 -0
- package/components/NodeContextMenu.tsx +123 -3
- package/components/PeopleBrowserSidebar.tsx +15 -6
- package/components/Sidebar.tsx +46 -19
- package/components/TimelineView.tsx +1 -0
- package/embedded.css +38 -0
- package/hooks/useExpansion.ts +61 -229
- package/hooks/useGraphActions.ts +1 -0
- package/hooks/useGraphState.ts +75 -40
- package/hooks/useKioskMode.ts +1 -0
- package/hooks/useNodeClickHandler.ts +23 -15
- package/hooks/useSearchHandlers.ts +57 -19
- package/host.ts +1 -1
- package/index.css +17 -3
- package/package.json +4 -1
- package/services/aiService.ts +23 -0
- package/services/aiUtils.ts +216 -207
- package/services/cacheService.ts +1 -0
- package/services/crossrefService.ts +1 -0
- package/services/deepseekService.ts +467 -0
- package/services/geminiService.ts +532 -733
- package/services/graphUtils.ts +128 -18
- package/services/imageService.ts +18 -0
- package/services/openAlexService.ts +1 -0
- package/services/resolveImageForTitle.ts +458 -0
- package/services/wikipediaImage.ts +1 -0
- package/services/wikipediaService.ts +56 -46
- package/types.ts +3 -0
- package/utils/evidenceUtils.ts +1 -0
- package/utils/graphLogicUtils.ts +1 -0
- package/utils/wikiUtils.ts +14 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
3
|
import { Search, X, Filter, ChevronRight } from 'lucide-react';
|
|
3
4
|
|
|
@@ -36,6 +37,10 @@ interface PeopleBrowserSidebarProps {
|
|
|
36
37
|
isOpen: boolean;
|
|
37
38
|
onClose: () => void;
|
|
38
39
|
onSelectPerson: (personName: string) => void;
|
|
40
|
+
/** With `useAbsoluteLayout`, use `top-14` in the constellations `main`; with `fixed`, use viewport top. */
|
|
41
|
+
offsetTopClass?: string;
|
|
42
|
+
/** When true, position inside the graph `main` (embedded hosts); avoids broken `fixed` in iframes/clipped roots. */
|
|
43
|
+
useAbsoluteLayout?: boolean;
|
|
39
44
|
}
|
|
40
45
|
|
|
41
46
|
const sumPageViews = (pageviews: Record<string, number> | undefined) => {
|
|
@@ -49,7 +54,7 @@ const sumPageViews = (pageviews: Record<string, number> | undefined) => {
|
|
|
49
54
|
const cleanCategoryLabel = (category: string) =>
|
|
50
55
|
category.replace(/^Category:/i, '').replace(/_/g, ' ');
|
|
51
56
|
|
|
52
|
-
const PeopleBrowserSidebar: React.FC<PeopleBrowserSidebarProps> = ({ isOpen, onClose, onSelectPerson }) => {
|
|
57
|
+
const PeopleBrowserSidebar: React.FC<PeopleBrowserSidebarProps> = ({ isOpen, onClose, onSelectPerson, offsetTopClass = "top-16", useAbsoluteLayout = false }) => {
|
|
53
58
|
const [people, setPeople] = useState<Person[]>([]);
|
|
54
59
|
const [loading, setLoading] = useState(false);
|
|
55
60
|
const [error, setError] = useState<string | null>(null);
|
|
@@ -456,15 +461,19 @@ const PeopleBrowserSidebar: React.FC<PeopleBrowserSidebarProps> = ({ isOpen, onC
|
|
|
456
461
|
|
|
457
462
|
if (!isOpen) return null;
|
|
458
463
|
|
|
459
|
-
|
|
460
|
-
const panelClasses =
|
|
464
|
+
const pos = useAbsoluteLayout ? "absolute bottom-0" : "fixed";
|
|
465
|
+
const panelClasses = `${pos} right-3 sm:right-4 z-[60] transition-transform duration-300 ease-in-out ${isCollapsed ? "translate-x-[calc(100%+2rem)]" : "translate-x-0"} ${offsetTopClass}`;
|
|
461
466
|
const panelStyle = isMobile
|
|
462
|
-
? { width:
|
|
463
|
-
: { width:
|
|
467
|
+
? { width: "calc(100% - 1.5rem)", maxWidth: "28rem" }
|
|
468
|
+
: { width: "28rem" };
|
|
464
469
|
|
|
465
470
|
return (
|
|
466
471
|
<div className={panelClasses} style={panelStyle}>
|
|
467
|
-
<div
|
|
472
|
+
<div
|
|
473
|
+
className={`bg-slate-900/95 backdrop-blur-xl rounded-xl border border-slate-700 shadow-2xl relative pointer-events-auto flex flex-col ${
|
|
474
|
+
useAbsoluteLayout ? "max-h-[calc(100%-1.5rem)]" : "max-h-[calc(100vh-2rem)]"
|
|
475
|
+
}`}
|
|
476
|
+
>
|
|
468
477
|
{/* Header */}
|
|
469
478
|
<div className="p-4 border-b border-slate-700 flex-shrink-0">
|
|
470
479
|
<div className="flex items-center justify-between mb-3">
|
package/components/Sidebar.tsx
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
"use client";
|
|
1
2
|
import React, { useState, useEffect, useRef } from 'react';
|
|
2
3
|
import { GraphNode, GraphLink } from '../types';
|
|
3
4
|
import { X, ExternalLink, ChevronLeft, ChevronRight } from 'lucide-react';
|
|
@@ -11,11 +12,23 @@ interface SidebarProps {
|
|
|
11
12
|
externalToggleSignal?: number;
|
|
12
13
|
isAdminMode?: boolean;
|
|
13
14
|
forceExpanded?: boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Top offset: with `useAbsoluteLayout`, this is from the constellations `main` (use `top-14`).
|
|
17
|
+
* With `position: fixed`, use viewport space (e.g. `top-14` standalone or `top-[6.25rem]` over a host).
|
|
18
|
+
*/
|
|
19
|
+
offsetTopClass?: string;
|
|
20
|
+
/**
|
|
21
|
+
* When true (e.g. embedded in Trailer), use `position: absolute` in the constellations root so
|
|
22
|
+
* the panel is not `fixed` to the wrong viewport/clip. Must match the control bar (`top-14` in `main`).
|
|
23
|
+
*/
|
|
24
|
+
useAbsoluteLayout?: boolean;
|
|
14
25
|
}
|
|
15
26
|
|
|
16
|
-
const Sidebar: React.FC<SidebarProps> = ({ selectedNode, selectedLink, onClose, onCollapseChange, externalToggleSignal, isAdminMode, forceExpanded }) => {
|
|
27
|
+
const Sidebar: React.FC<SidebarProps> = ({ selectedNode, selectedLink, onClose, onCollapseChange, externalToggleSignal, isAdminMode, forceExpanded, offsetTopClass = "top-14", useAbsoluteLayout = false }) => {
|
|
17
28
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
18
|
-
const [isMobile, setIsMobile] = useState(
|
|
29
|
+
const [isMobile, setIsMobile] = useState(
|
|
30
|
+
() => typeof window !== "undefined" && window.innerWidth < 768
|
|
31
|
+
);
|
|
19
32
|
const [showFullSummary, setShowFullSummary] = useState(false);
|
|
20
33
|
const userManuallyCollapsedRef = useRef(false);
|
|
21
34
|
const lastToggleSignalRef = useRef<number | undefined>(undefined);
|
|
@@ -60,7 +73,7 @@ const Sidebar: React.FC<SidebarProps> = ({ selectedNode, selectedLink, onClose,
|
|
|
60
73
|
setShowFullSummary(false);
|
|
61
74
|
}, [selectedNode, selectedLink, isMobile, forceExpanded]);
|
|
62
75
|
|
|
63
|
-
// External toggle (from header
|
|
76
|
+
// External toggle (from header) — use functional setState (effect must not call a stale handler)
|
|
64
77
|
useEffect(() => {
|
|
65
78
|
if (externalToggleSignal === undefined) return;
|
|
66
79
|
if (lastToggleSignalRef.current === undefined) {
|
|
@@ -69,15 +82,20 @@ const Sidebar: React.FC<SidebarProps> = ({ selectedNode, selectedLink, onClose,
|
|
|
69
82
|
}
|
|
70
83
|
if (externalToggleSignal !== lastToggleSignalRef.current) {
|
|
71
84
|
lastToggleSignalRef.current = externalToggleSignal;
|
|
72
|
-
|
|
85
|
+
setIsCollapsed((c) => {
|
|
86
|
+
const next = !c;
|
|
87
|
+
userManuallyCollapsedRef.current = next;
|
|
88
|
+
return next;
|
|
89
|
+
});
|
|
73
90
|
}
|
|
74
91
|
}, [externalToggleSignal]);
|
|
75
92
|
|
|
76
93
|
const handleToggleCollapse = () => {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
94
|
+
setIsCollapsed((c) => {
|
|
95
|
+
const next = !c;
|
|
96
|
+
userManuallyCollapsedRef.current = next;
|
|
97
|
+
return next;
|
|
98
|
+
});
|
|
81
99
|
};
|
|
82
100
|
|
|
83
101
|
if (!selectedNode && !selectedLink) return null;
|
|
@@ -86,35 +104,44 @@ const Sidebar: React.FC<SidebarProps> = ({ selectedNode, selectedLink, onClose,
|
|
|
86
104
|
const isPerson = selectedNode ? (selectedNode.is_atomic === true || selectedNode.is_person === true || (selectedNode.type.toLowerCase() === 'person' || selectedNode.type.toLowerCase() === 'actor')) : false;
|
|
87
105
|
|
|
88
106
|
// Unified side panel styling - slides right on both mobile and desktop
|
|
89
|
-
//
|
|
90
|
-
// When collapsed, we translate most of it away but leave 24px (1.5rem-ish) for the handle.
|
|
107
|
+
// When embedded, `absolute` + same `top` as control bar avoids `fixed` viewport/clip bugs in hosts.
|
|
91
108
|
const effectiveMobile = forceExpanded ? false : isMobile;
|
|
92
|
-
const panelWidth = effectiveMobile
|
|
93
|
-
|
|
94
|
-
|
|
109
|
+
const panelWidth = effectiveMobile
|
|
110
|
+
? useAbsoluteLayout
|
|
111
|
+
? "calc(100% - 1.5rem)"
|
|
112
|
+
: "calc(100vw - 1.5rem)"
|
|
113
|
+
: "26rem";
|
|
114
|
+
const pos = useAbsoluteLayout ? "absolute" : "fixed";
|
|
115
|
+
const panelClasses = `${pos} bottom-0 right-0 z-[55] transition-transform duration-300 ease-in-out ${isCollapsed ? "translate-x-[calc(100%-24px)]" : "translate-x-0"} ${offsetTopClass}`;
|
|
116
|
+
const panelStyle: React.CSSProperties = {
|
|
117
|
+
width: panelWidth,
|
|
118
|
+
maxWidth: "28rem",
|
|
119
|
+
paddingRight: effectiveMobile ? "0.75rem" : "1rem",
|
|
120
|
+
};
|
|
95
121
|
|
|
96
122
|
return (
|
|
97
123
|
<>
|
|
98
124
|
<div className={panelClasses} style={panelStyle}>
|
|
99
|
-
<div className="bg-slate-900/95 backdrop-blur-xl rounded-xl border border-slate-700 shadow-2xl relative pointer-events-auto flex
|
|
125
|
+
<div className="bg-slate-900/95 backdrop-blur-xl rounded-xl border border-slate-700 shadow-2xl relative pointer-events-auto flex h-full min-h-0 flex-col overflow-hidden p-4 sm:p-6">
|
|
100
126
|
{/* Persistent Toggle Handle */}
|
|
101
127
|
<button
|
|
128
|
+
type="button"
|
|
102
129
|
onClick={handleToggleCollapse}
|
|
103
130
|
className={`absolute top-1/2 -translate-y-1/2 -left-8 w-8 h-24 bg-slate-800 border border-slate-700 border-r-0 rounded-l-xl flex flex-col items-center justify-center text-slate-400 hover:text-white transition-all group shadow-xl ${isCollapsed ? 'opacity-100' : 'opacity-0 group-hover:opacity-100'}`}
|
|
104
|
-
title={isCollapsed ? "Expand
|
|
131
|
+
title={isCollapsed ? "Expand details panel" : "Collapse details panel"}
|
|
105
132
|
>
|
|
106
133
|
{isCollapsed ? <ChevronLeft size={16} /> : <ChevronRight size={16} />}
|
|
107
134
|
<div className="[writing-mode:vertical-lr] text-[9px] uppercase tracking-tighter mt-1 font-bold">Details</div>
|
|
108
135
|
</button>
|
|
109
136
|
|
|
110
|
-
<div className="flex-1 overflow-
|
|
111
|
-
<div className="
|
|
112
|
-
<h2 className="text-xl font-bold text-white
|
|
137
|
+
<div className="flex min-h-0 flex-1 flex-col overflow-y-auto pr-1 custom-scrollbar">
|
|
138
|
+
<div className="mb-3 shrink-0">
|
|
139
|
+
<h2 className="text-xl font-bold leading-tight text-white">
|
|
113
140
|
{selectedNode ? selectedNode.title : "Connection Details"}
|
|
114
141
|
</h2>
|
|
115
142
|
</div>
|
|
116
143
|
|
|
117
|
-
<div className="
|
|
144
|
+
<div className="min-h-0 space-y-4 pb-1">
|
|
118
145
|
{/* Selected Edge Evidence (when user clicks an edge) */}
|
|
119
146
|
{selectedLink && (
|
|
120
147
|
<div className="p-3 bg-slate-800/40 rounded-lg border border-slate-600/40">
|
package/embedded.css
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* Component-specific styles for @johndimm/constellations when embedded in a host app.
|
|
2
|
+
Import this in your root layout: import "@johndimm/constellations/embedded.css" */
|
|
3
|
+
|
|
4
|
+
.custom-scrollbar::-webkit-scrollbar {
|
|
5
|
+
width: 4px;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
.custom-scrollbar::-webkit-scrollbar-track {
|
|
9
|
+
background: transparent;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
.custom-scrollbar::-webkit-scrollbar-thumb {
|
|
13
|
+
background: #475569;
|
|
14
|
+
border-radius: 2px;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.custom-scrollbar:hover::-webkit-scrollbar-thumb {
|
|
18
|
+
background: #64748b;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@keyframes cc-fade-in {
|
|
22
|
+
from { opacity: 0; }
|
|
23
|
+
to { opacity: 1; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@keyframes cc-scale-in {
|
|
27
|
+
from { transform: scale(0.95); opacity: 0; }
|
|
28
|
+
to { transform: scale(1); opacity: 1; }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@keyframes cc-fade-in-up {
|
|
32
|
+
from { transform: translate(-50%, 20px); opacity: 0; }
|
|
33
|
+
to { transform: translate(-50%, 0); opacity: 1; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.animate-fade-in { animation: cc-fade-in 0.2s ease-out forwards; }
|
|
37
|
+
.animate-scale-in { animation: cc-scale-in 0.2s cubic-bezier(0.16, 1, 0.3, 1) forwards; }
|
|
38
|
+
.animate-fade-in-up { animation: cc-fade-in-up 0.3s ease-out forwards; }
|