@johndimm/constellations 1.0.1 → 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.
Files changed (38) hide show
  1. package/App.tsx +352 -70
  2. package/FullPageConstellations.tsx +7 -4
  3. package/components/AppConfirmDialog.tsx +1 -0
  4. package/components/AppHeader.tsx +69 -29
  5. package/components/AppNotifications.tsx +1 -0
  6. package/components/BrowsePeople.tsx +3 -0
  7. package/components/ControlPanel.tsx +46 -371
  8. package/components/Graph.tsx +251 -87
  9. package/components/HelpOverlay.tsx +1 -0
  10. package/components/NodeContextMenu.tsx +123 -3
  11. package/components/PeopleBrowserSidebar.tsx +15 -6
  12. package/components/Sidebar.tsx +46 -19
  13. package/components/TimelineView.tsx +1 -0
  14. package/hooks/useExpansion.ts +61 -229
  15. package/hooks/useGraphActions.ts +1 -0
  16. package/hooks/useGraphState.ts +75 -40
  17. package/hooks/useKioskMode.ts +1 -0
  18. package/hooks/useNodeClickHandler.ts +23 -15
  19. package/hooks/useSearchHandlers.ts +57 -19
  20. package/host.ts +1 -1
  21. package/index.css +17 -3
  22. package/package.json +2 -1
  23. package/services/aiService.ts +23 -0
  24. package/services/aiUtils.ts +216 -207
  25. package/services/cacheService.ts +1 -0
  26. package/services/crossrefService.ts +1 -0
  27. package/services/deepseekService.ts +467 -0
  28. package/services/geminiService.ts +532 -733
  29. package/services/graphUtils.ts +128 -18
  30. package/services/imageService.ts +18 -0
  31. package/services/openAlexService.ts +1 -0
  32. package/services/resolveImageForTitle.ts +458 -0
  33. package/services/wikipediaImage.ts +1 -0
  34. package/services/wikipediaService.ts +56 -46
  35. package/types.ts +3 -0
  36. package/utils/evidenceUtils.ts +1 -0
  37. package/utils/graphLogicUtils.ts +1 -0
  38. 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
- // Position at the same location as regular sidebar, but with higher z-index when open
460
- const panelClasses = `fixed top-16 right-3 sm:right-4 z-[60] transition-transform duration-300 ease-in-out ${isCollapsed ? 'translate-x-[calc(100%+2rem)]' : 'translate-x-0'}`;
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: 'calc(100% - 1.5rem)', maxWidth: '28rem' }
463
- : { width: '28rem' };
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 className="bg-slate-900/95 backdrop-blur-xl rounded-xl border border-slate-700 shadow-2xl relative pointer-events-auto flex flex-col max-h-[calc(100vh-2rem)]">
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">
@@ -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(window.innerWidth < 768);
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 button)
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
- handleToggleCollapse();
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
- const newCollapsed = !isCollapsed;
78
- setIsCollapsed(newCollapsed);
79
- // Track that user manually collapsed it
80
- userManuallyCollapsedRef.current = newCollapsed;
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
- // Side panel styling - always slides right.
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 ? 'calc(100vw - 1.5rem)' : '26rem';
93
- const panelClasses = `fixed top-16 right-0 z-50 transition-transform duration-300 ease-in-out ${isCollapsed ? 'translate-x-[calc(100%-24px)]' : 'translate-x-0'}`;
94
- const panelStyle = { width: panelWidth, maxWidth: '28rem', paddingRight: effectiveMobile ? '0.75rem' : '1rem' };
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 flex-col p-6 h-[calc(100vh-6rem)] overflow-visible">
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 sidebar" : "Collapse sidebar"}
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-visible">
111
- <div className="flex justify-between items-start mb-4">
112
- <h2 className="text-xl font-bold text-white leading-tight">
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="space-y-4 overflow-y-auto pr-1">
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">
@@ -1,3 +1,4 @@
1
+ "use client";
1
2
  import React from 'react';
2
3
  // This component has been deprecated and its functionality integrated into Graph.tsx
3
4
  const TimelineView: React.FC = () => null;