beads-map 0.2.0 → 0.2.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/.next/BUILD_ID +1 -1
- package/.next/app-build-manifest.json +2 -2
- package/.next/app-path-routes-manifest.json +1 -1
- package/.next/build-manifest.json +2 -2
- package/.next/next-minimal-server.js.nft.json +1 -1
- package/.next/next-server.js.nft.json +1 -1
- package/.next/prerender-manifest.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +1 -1
- package/.next/server/app/api/beads.body +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +2 -2
- package/.next/server/app/page.js +3 -3
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/app-paths-manifest.json +6 -6
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/pages-manifest.json +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/app/page-13ee27a84e4a0c70.js +1 -0
- package/.next/static/css/dbf588b653aa4019.css +3 -0
- package/app/page.tsx +118 -6
- package/bin/beads-map.mjs +32 -0
- package/components/ActivityItem.tsx +326 -0
- package/components/ActivityOverlay.tsx +123 -0
- package/components/ActivityPanel.tsx +345 -0
- package/components/AllCommentsPanel.tsx +9 -4
- package/components/AuthButton.tsx +7 -2
- package/components/BeadsGraph.tsx +11 -5
- package/components/CommentTooltip.tsx +7 -2
- package/components/NodeDetail.tsx +14 -4
- package/lib/activity.ts +377 -0
- package/lib/diff-beads.ts +3 -0
- package/package.json +1 -1
- package/.next/static/chunks/app/page-f36cdcae49f1d2af.js +0 -1
- package/.next/static/css/a4e34aaaa51183d9.css +0 -3
- /package/.next/static/{YVdbDxCehgqcYmLncYRFB → dxp53pVl-eTmydUx_hpyJ}/_buildManifest.js +0 -0
- /package/.next/static/{YVdbDxCehgqcYmLncYRFB → dxp53pVl-eTmydUx_hpyJ}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import type { ActivityEvent } from "@/lib/activity";
|
|
4
|
+
import { ActivityItem } from "./ActivityItem";
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
events: ActivityEvent[];
|
|
8
|
+
collapsed: boolean;
|
|
9
|
+
onToggleCollapse: () => void;
|
|
10
|
+
onExpandPanel: () => void;
|
|
11
|
+
onNodeClick: (nodeId: string) => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Compact always-visible activity card in the top-right of the graph canvas.
|
|
16
|
+
* Shows latest 5 events. Click "See all" to open the full ActivityPanel.
|
|
17
|
+
*/
|
|
18
|
+
export function ActivityOverlay({
|
|
19
|
+
events,
|
|
20
|
+
collapsed,
|
|
21
|
+
onToggleCollapse,
|
|
22
|
+
onExpandPanel,
|
|
23
|
+
onNodeClick,
|
|
24
|
+
}: Props) {
|
|
25
|
+
// Count events in last 5 minutes for the collapsed badge
|
|
26
|
+
const recentCount = events.filter(
|
|
27
|
+
(e) => Date.now() - e.time < 5 * 60 * 1000
|
|
28
|
+
).length;
|
|
29
|
+
|
|
30
|
+
if (collapsed) {
|
|
31
|
+
return (
|
|
32
|
+
<button
|
|
33
|
+
onClick={onToggleCollapse}
|
|
34
|
+
className="group flex items-center gap-2 px-3 py-2 bg-white/90 backdrop-blur-sm rounded-full border border-zinc-200/80 shadow-sm hover:bg-white hover:shadow-md transition-all"
|
|
35
|
+
title="Show activity feed"
|
|
36
|
+
>
|
|
37
|
+
<svg
|
|
38
|
+
className="w-3.5 h-3.5 text-zinc-400 group-hover:text-emerald-500 transition-colors"
|
|
39
|
+
viewBox="0 0 16 16"
|
|
40
|
+
fill="none"
|
|
41
|
+
stroke="currentColor"
|
|
42
|
+
strokeWidth="1.5"
|
|
43
|
+
>
|
|
44
|
+
<circle cx="8" cy="8" r="6" />
|
|
45
|
+
<polyline points="8,4 8,8 11,10" />
|
|
46
|
+
</svg>
|
|
47
|
+
<span className="text-xs text-zinc-500 group-hover:text-zinc-700 transition-colors">Activity</span>
|
|
48
|
+
{recentCount > 0 && (
|
|
49
|
+
<span className="text-[10px] font-medium text-white bg-emerald-500 rounded-full px-1.5 py-0.5 min-w-[18px] text-center leading-tight">
|
|
50
|
+
{recentCount}
|
|
51
|
+
</span>
|
|
52
|
+
)}
|
|
53
|
+
</button>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="w-72 bg-white/95 backdrop-blur-sm rounded-xl border border-zinc-200/80 shadow-lg overflow-hidden">
|
|
59
|
+
{/* Header */}
|
|
60
|
+
<div className="flex items-center justify-between px-3.5 py-2.5 border-b border-zinc-100/80">
|
|
61
|
+
<div className="flex items-center gap-2">
|
|
62
|
+
<div className="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse" />
|
|
63
|
+
<span className="text-[11px] font-semibold text-zinc-600">
|
|
64
|
+
Live Activity
|
|
65
|
+
</span>
|
|
66
|
+
</div>
|
|
67
|
+
<button
|
|
68
|
+
onClick={onToggleCollapse}
|
|
69
|
+
className="text-zinc-400 hover:text-zinc-600 transition-colors p-1 -mr-0.5 rounded-full hover:bg-zinc-100"
|
|
70
|
+
title="Collapse"
|
|
71
|
+
>
|
|
72
|
+
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
|
|
73
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M5 15l7-7 7 7" />
|
|
74
|
+
</svg>
|
|
75
|
+
</button>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
{/* Event list */}
|
|
79
|
+
<div className="py-0.5 max-h-[260px] overflow-y-auto custom-scrollbar">
|
|
80
|
+
{events.length === 0 ? (
|
|
81
|
+
<div className="py-6 text-center">
|
|
82
|
+
<svg className="w-5 h-5 text-zinc-200 mx-auto mb-1.5" viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5">
|
|
83
|
+
<circle cx="8" cy="8" r="6" />
|
|
84
|
+
<polyline points="8,4 8,8 11,10" />
|
|
85
|
+
</svg>
|
|
86
|
+
<p className="text-[11px] text-zinc-400">No activity yet</p>
|
|
87
|
+
</div>
|
|
88
|
+
) : (
|
|
89
|
+
events.slice(0, 6).map((event, i) => (
|
|
90
|
+
<div
|
|
91
|
+
key={event.id}
|
|
92
|
+
className={i === 0 ? "bg-emerald-50/30" : ""}
|
|
93
|
+
>
|
|
94
|
+
<ActivityItem
|
|
95
|
+
event={event}
|
|
96
|
+
variant="compact"
|
|
97
|
+
onNodeClick={onNodeClick}
|
|
98
|
+
/>
|
|
99
|
+
</div>
|
|
100
|
+
))
|
|
101
|
+
)}
|
|
102
|
+
</div>
|
|
103
|
+
|
|
104
|
+
{/* Footer */}
|
|
105
|
+
{events.length > 0 && (
|
|
106
|
+
<div className="flex items-center justify-between px-3.5 py-2 border-t border-zinc-100/80 bg-zinc-50/30">
|
|
107
|
+
<span className="text-[10px] text-zinc-400">
|
|
108
|
+
{events.length} event{events.length !== 1 ? "s" : ""}
|
|
109
|
+
</span>
|
|
110
|
+
<button
|
|
111
|
+
onClick={onExpandPanel}
|
|
112
|
+
className="text-[11px] text-emerald-600 hover:text-emerald-700 transition-colors font-medium flex items-center gap-1"
|
|
113
|
+
>
|
|
114
|
+
See all
|
|
115
|
+
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
|
|
116
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M9 5l7 7-7 7" />
|
|
117
|
+
</svg>
|
|
118
|
+
</button>
|
|
119
|
+
</div>
|
|
120
|
+
)}
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo } from "react";
|
|
4
|
+
import type { ActivityEvent, ActivityFilterCategory } from "@/lib/activity";
|
|
5
|
+
import { getEventCategory } from "@/lib/activity";
|
|
6
|
+
import { ActivityItem } from "./ActivityItem";
|
|
7
|
+
|
|
8
|
+
interface Props {
|
|
9
|
+
events: ActivityEvent[];
|
|
10
|
+
isOpen: boolean;
|
|
11
|
+
onClose: () => void;
|
|
12
|
+
onNodeClick: (nodeId: string) => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const FILTER_CATEGORIES: {
|
|
16
|
+
key: ActivityFilterCategory;
|
|
17
|
+
label: string;
|
|
18
|
+
icon: string;
|
|
19
|
+
}[] = [
|
|
20
|
+
{ key: "issues", label: "Issues", icon: "circle" },
|
|
21
|
+
{ key: "deps", label: "Deps", icon: "link" },
|
|
22
|
+
{ key: "comments", label: "Comments", icon: "chat" },
|
|
23
|
+
{ key: "claims", label: "Claims", icon: "user" },
|
|
24
|
+
{ key: "likes", label: "Likes", icon: "heart" },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// ============================================================================
|
|
28
|
+
// Time grouping helpers
|
|
29
|
+
// ============================================================================
|
|
30
|
+
|
|
31
|
+
function getTimeGroup(time: number): string {
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
const diff = now - time;
|
|
34
|
+
const hours = diff / (1000 * 60 * 60);
|
|
35
|
+
const days = diff / (1000 * 60 * 60 * 24);
|
|
36
|
+
|
|
37
|
+
if (hours < 1) return "Just now";
|
|
38
|
+
if (hours < 24) return "Today";
|
|
39
|
+
if (days < 2) return "Yesterday";
|
|
40
|
+
if (days < 7) return "This week";
|
|
41
|
+
if (days < 30) return "This month";
|
|
42
|
+
return "Older";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function groupEventsByTime(events: ActivityEvent[]): { label: string; events: ActivityEvent[] }[] {
|
|
46
|
+
const groups: Map<string, ActivityEvent[]> = new Map();
|
|
47
|
+
const order: string[] = [];
|
|
48
|
+
|
|
49
|
+
for (const event of events) {
|
|
50
|
+
const label = getTimeGroup(event.time);
|
|
51
|
+
if (!groups.has(label)) {
|
|
52
|
+
groups.set(label, []);
|
|
53
|
+
order.push(label);
|
|
54
|
+
}
|
|
55
|
+
groups.get(label)!.push(event);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return order.map((label) => ({ label, events: groups.get(label)! }));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Full slide-in sidebar panel for the activity feed.
|
|
63
|
+
* Includes search bar, filter chips, and time-grouped events.
|
|
64
|
+
* Same pattern as AllCommentsPanel / NodeDetail.
|
|
65
|
+
*/
|
|
66
|
+
export function ActivityPanel({
|
|
67
|
+
events,
|
|
68
|
+
isOpen,
|
|
69
|
+
onClose,
|
|
70
|
+
onNodeClick,
|
|
71
|
+
}: Props) {
|
|
72
|
+
const [search, setSearch] = useState("");
|
|
73
|
+
const [activeFilters, setActiveFilters] = useState<Set<ActivityFilterCategory>>(
|
|
74
|
+
new Set(FILTER_CATEGORIES.map((c) => c.key))
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
const toggleFilter = (category: ActivityFilterCategory) => {
|
|
78
|
+
setActiveFilters((prev) => {
|
|
79
|
+
const next = new Set(prev);
|
|
80
|
+
if (next.has(category)) {
|
|
81
|
+
// Don't allow deactivating all filters
|
|
82
|
+
if (next.size > 1) next.delete(category);
|
|
83
|
+
} else {
|
|
84
|
+
next.add(category);
|
|
85
|
+
}
|
|
86
|
+
return next;
|
|
87
|
+
});
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const filteredEvents = useMemo(() => {
|
|
91
|
+
let filtered = events;
|
|
92
|
+
|
|
93
|
+
// Apply category filters
|
|
94
|
+
filtered = filtered.filter((e) =>
|
|
95
|
+
activeFilters.has(getEventCategory(e.type))
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Apply text search
|
|
99
|
+
if (search.trim()) {
|
|
100
|
+
const q = search.trim().toLowerCase();
|
|
101
|
+
filtered = filtered.filter(
|
|
102
|
+
(e) =>
|
|
103
|
+
e.nodeId.toLowerCase().includes(q) ||
|
|
104
|
+
(e.nodeTitle && e.nodeTitle.toLowerCase().includes(q)) ||
|
|
105
|
+
(e.actor?.handle && e.actor.handle.toLowerCase().includes(q)) ||
|
|
106
|
+
(e.detail && e.detail.toLowerCase().includes(q))
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return filtered;
|
|
111
|
+
}, [events, activeFilters, search]);
|
|
112
|
+
|
|
113
|
+
const groupedEvents = useMemo(
|
|
114
|
+
() => groupEventsByTime(filteredEvents),
|
|
115
|
+
[filteredEvents]
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
const allActive = activeFilters.size === FILTER_CATEGORIES.length;
|
|
119
|
+
|
|
120
|
+
return (
|
|
121
|
+
<>
|
|
122
|
+
{/* Desktop sidebar */}
|
|
123
|
+
<aside
|
|
124
|
+
className={`hidden md:flex absolute top-0 right-0 h-full w-[360px] bg-white border-l border-zinc-200 flex-col shadow-xl z-30 transform transition-transform duration-300 ease-out ${
|
|
125
|
+
isOpen ? "translate-x-0" : "translate-x-full"
|
|
126
|
+
}`}
|
|
127
|
+
>
|
|
128
|
+
{/* Header */}
|
|
129
|
+
<div className="shrink-0 px-5 py-3 border-b border-zinc-100 flex items-center justify-between">
|
|
130
|
+
<div className="flex items-center gap-2">
|
|
131
|
+
<h2 className="text-xs font-semibold text-zinc-400 uppercase tracking-wider">
|
|
132
|
+
Activity
|
|
133
|
+
</h2>
|
|
134
|
+
{events.length > 0 && (
|
|
135
|
+
<span className="px-1.5 py-0.5 bg-emerald-50 text-emerald-600 rounded-full text-[10px] font-medium">
|
|
136
|
+
{events.length}
|
|
137
|
+
</span>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
<button
|
|
141
|
+
onClick={onClose}
|
|
142
|
+
className="p-1 text-zinc-400 hover:text-zinc-600 transition-colors rounded-full hover:bg-zinc-100"
|
|
143
|
+
>
|
|
144
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}>
|
|
145
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
146
|
+
</svg>
|
|
147
|
+
</button>
|
|
148
|
+
</div>
|
|
149
|
+
|
|
150
|
+
{/* Search */}
|
|
151
|
+
<div className="px-4 pt-3 pb-2 shrink-0">
|
|
152
|
+
<div className="flex items-center bg-zinc-50/80 rounded-full border border-zinc-200/60 overflow-hidden">
|
|
153
|
+
<div className="pl-3 pr-1 text-zinc-400">
|
|
154
|
+
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
|
|
155
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
|
156
|
+
</svg>
|
|
157
|
+
</div>
|
|
158
|
+
<input
|
|
159
|
+
type="text"
|
|
160
|
+
value={search}
|
|
161
|
+
onChange={(e) => setSearch(e.target.value)}
|
|
162
|
+
placeholder="Search activity..."
|
|
163
|
+
className="flex-1 px-2 py-2 text-xs text-zinc-800 bg-transparent outline-none placeholder:text-zinc-400"
|
|
164
|
+
/>
|
|
165
|
+
{search && (
|
|
166
|
+
<button
|
|
167
|
+
onClick={() => setSearch("")}
|
|
168
|
+
className="pr-3 text-zinc-400 hover:text-zinc-600"
|
|
169
|
+
>
|
|
170
|
+
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
|
|
171
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
172
|
+
</svg>
|
|
173
|
+
</button>
|
|
174
|
+
)}
|
|
175
|
+
</div>
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
{/* Filter chips */}
|
|
179
|
+
<div className="flex flex-wrap gap-1.5 px-4 pb-3 shrink-0">
|
|
180
|
+
{FILTER_CATEGORIES.map((cat) => {
|
|
181
|
+
const isActive = activeFilters.has(cat.key);
|
|
182
|
+
return (
|
|
183
|
+
<button
|
|
184
|
+
key={cat.key}
|
|
185
|
+
onClick={() => toggleFilter(cat.key)}
|
|
186
|
+
className={`px-2.5 py-1 text-[11px] font-medium rounded-full border transition-all ${
|
|
187
|
+
isActive
|
|
188
|
+
? "bg-emerald-50 text-emerald-700 border-emerald-200 shadow-sm"
|
|
189
|
+
: "bg-white text-zinc-400 border-zinc-200 hover:text-zinc-600 hover:border-zinc-300"
|
|
190
|
+
}`}
|
|
191
|
+
>
|
|
192
|
+
{cat.label}
|
|
193
|
+
</button>
|
|
194
|
+
);
|
|
195
|
+
})}
|
|
196
|
+
</div>
|
|
197
|
+
|
|
198
|
+
{/* Event list with time groups */}
|
|
199
|
+
<div className="flex-1 overflow-y-auto custom-scrollbar border-t border-zinc-100">
|
|
200
|
+
{filteredEvents.length === 0 ? (
|
|
201
|
+
<div className="flex flex-col items-center justify-center py-12 text-center">
|
|
202
|
+
<svg
|
|
203
|
+
className="w-8 h-8 text-zinc-200 mb-3"
|
|
204
|
+
fill="none"
|
|
205
|
+
viewBox="0 0 24 24"
|
|
206
|
+
strokeWidth={1.5}
|
|
207
|
+
stroke="currentColor"
|
|
208
|
+
>
|
|
209
|
+
<path
|
|
210
|
+
strokeLinecap="round"
|
|
211
|
+
strokeLinejoin="round"
|
|
212
|
+
d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z"
|
|
213
|
+
/>
|
|
214
|
+
</svg>
|
|
215
|
+
<p className="text-xs text-zinc-400">
|
|
216
|
+
{search.trim()
|
|
217
|
+
? "No activity matching your search"
|
|
218
|
+
: "No activity yet"}
|
|
219
|
+
</p>
|
|
220
|
+
{search.trim() && (
|
|
221
|
+
<button
|
|
222
|
+
onClick={() => setSearch("")}
|
|
223
|
+
className="text-[11px] text-emerald-600 hover:text-emerald-700 mt-1.5"
|
|
224
|
+
>
|
|
225
|
+
Clear search
|
|
226
|
+
</button>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
) : (
|
|
230
|
+
groupedEvents.map((group) => (
|
|
231
|
+
<div key={group.label}>
|
|
232
|
+
{/* Time group header */}
|
|
233
|
+
<div className="sticky top-0 z-[1] px-4 py-1.5 bg-zinc-50/95 backdrop-blur-sm border-b border-zinc-100/60">
|
|
234
|
+
<span className="text-[10px] font-semibold text-zinc-400 uppercase tracking-wider">
|
|
235
|
+
{group.label}
|
|
236
|
+
</span>
|
|
237
|
+
</div>
|
|
238
|
+
{/* Events in this group */}
|
|
239
|
+
<div className="divide-y divide-zinc-50">
|
|
240
|
+
{group.events.map((event) => (
|
|
241
|
+
<ActivityItem
|
|
242
|
+
key={event.id}
|
|
243
|
+
event={event}
|
|
244
|
+
variant="full"
|
|
245
|
+
onNodeClick={onNodeClick}
|
|
246
|
+
/>
|
|
247
|
+
))}
|
|
248
|
+
</div>
|
|
249
|
+
</div>
|
|
250
|
+
))
|
|
251
|
+
)}
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
{/* Footer stats */}
|
|
255
|
+
<div className="shrink-0 px-5 py-2.5 border-t border-zinc-100 bg-zinc-50/50">
|
|
256
|
+
<div className="text-[10px] text-zinc-400">
|
|
257
|
+
{filteredEvents.length} event{filteredEvents.length !== 1 ? "s" : ""}
|
|
258
|
+
{!allActive && ` (filtered)`}
|
|
259
|
+
{search.trim() && ` matching "${search.trim()}"`}
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
</aside>
|
|
263
|
+
|
|
264
|
+
{/* Mobile drawer */}
|
|
265
|
+
<div
|
|
266
|
+
className={`md:hidden fixed inset-x-0 bottom-0 z-20 transition-transform duration-300 ease-out ${
|
|
267
|
+
isOpen ? "translate-y-0" : "translate-y-full"
|
|
268
|
+
}`}
|
|
269
|
+
>
|
|
270
|
+
<div className="bg-white rounded-t-2xl shadow-xl border-t border-zinc-200 max-h-[60vh] flex flex-col">
|
|
271
|
+
{/* Drag handle */}
|
|
272
|
+
<div className="flex justify-center pt-3 pb-1">
|
|
273
|
+
<div className="w-8 h-1 bg-zinc-300 rounded-full" />
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
{/* Header */}
|
|
277
|
+
<div className="flex items-center justify-between px-5 py-2">
|
|
278
|
+
<div className="flex items-center gap-2">
|
|
279
|
+
<h2 className="text-sm font-semibold text-zinc-900">Activity</h2>
|
|
280
|
+
{events.length > 0 && (
|
|
281
|
+
<span className="px-1.5 py-0.5 bg-emerald-50 text-emerald-600 rounded-full text-[10px] font-medium">
|
|
282
|
+
{events.length}
|
|
283
|
+
</span>
|
|
284
|
+
)}
|
|
285
|
+
</div>
|
|
286
|
+
<button
|
|
287
|
+
onClick={onClose}
|
|
288
|
+
className="p-1 text-zinc-400 hover:text-zinc-600"
|
|
289
|
+
>
|
|
290
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={1.5}>
|
|
291
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
292
|
+
</svg>
|
|
293
|
+
</button>
|
|
294
|
+
</div>
|
|
295
|
+
|
|
296
|
+
{/* Filter chips */}
|
|
297
|
+
<div className="flex flex-wrap gap-1.5 px-5 pb-3">
|
|
298
|
+
{FILTER_CATEGORIES.map((cat) => (
|
|
299
|
+
<button
|
|
300
|
+
key={cat.key}
|
|
301
|
+
onClick={() => toggleFilter(cat.key)}
|
|
302
|
+
className={`px-2.5 py-1 text-[11px] font-medium rounded-full border transition-colors ${
|
|
303
|
+
activeFilters.has(cat.key)
|
|
304
|
+
? "bg-emerald-50 text-emerald-700 border-emerald-200"
|
|
305
|
+
: "bg-white text-zinc-400 border-zinc-200"
|
|
306
|
+
}`}
|
|
307
|
+
>
|
|
308
|
+
{cat.label}
|
|
309
|
+
</button>
|
|
310
|
+
))}
|
|
311
|
+
</div>
|
|
312
|
+
|
|
313
|
+
{/* Event list */}
|
|
314
|
+
<div className="flex-1 overflow-y-auto custom-scrollbar">
|
|
315
|
+
{filteredEvents.length === 0 ? (
|
|
316
|
+
<div className="py-8 text-center text-xs text-zinc-400">
|
|
317
|
+
No activity matching filters
|
|
318
|
+
</div>
|
|
319
|
+
) : (
|
|
320
|
+
groupedEvents.map((group) => (
|
|
321
|
+
<div key={group.label}>
|
|
322
|
+
<div className="sticky top-0 z-[1] px-5 py-1.5 bg-zinc-50/95 backdrop-blur-sm border-b border-zinc-100/60">
|
|
323
|
+
<span className="text-[10px] font-semibold text-zinc-400 uppercase tracking-wider">
|
|
324
|
+
{group.label}
|
|
325
|
+
</span>
|
|
326
|
+
</div>
|
|
327
|
+
<div className="divide-y divide-zinc-50">
|
|
328
|
+
{group.events.map((event) => (
|
|
329
|
+
<ActivityItem
|
|
330
|
+
key={event.id}
|
|
331
|
+
event={event}
|
|
332
|
+
variant="full"
|
|
333
|
+
onNodeClick={onNodeClick}
|
|
334
|
+
/>
|
|
335
|
+
))}
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
))
|
|
339
|
+
)}
|
|
340
|
+
</div>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
</>
|
|
344
|
+
);
|
|
345
|
+
}
|
|
@@ -46,14 +46,14 @@ export default function AllCommentsPanel({
|
|
|
46
46
|
</div>
|
|
47
47
|
<button
|
|
48
48
|
onClick={onClose}
|
|
49
|
-
className="p-1 text-zinc-400 hover:text-zinc-600 transition-colors"
|
|
49
|
+
className="p-1 text-zinc-400 hover:text-zinc-600 transition-colors rounded-full hover:bg-zinc-100"
|
|
50
50
|
>
|
|
51
51
|
<svg
|
|
52
52
|
className="w-4 h-4"
|
|
53
53
|
fill="none"
|
|
54
54
|
stroke="currentColor"
|
|
55
55
|
viewBox="0 0 24 24"
|
|
56
|
-
strokeWidth={
|
|
56
|
+
strokeWidth={1.5}
|
|
57
57
|
>
|
|
58
58
|
<path
|
|
59
59
|
strokeLinecap="round"
|
|
@@ -197,11 +197,16 @@ function AllCommentCard({
|
|
|
197
197
|
</div>
|
|
198
198
|
)}
|
|
199
199
|
</div>
|
|
200
|
-
<
|
|
200
|
+
<a
|
|
201
|
+
href={`https://www.impactindexer.org/data?did=${comment.did}`}
|
|
202
|
+
target="_blank"
|
|
203
|
+
rel="noopener noreferrer"
|
|
204
|
+
className="text-xs font-medium text-zinc-600 truncate hover:text-emerald-600 transition-colors"
|
|
205
|
+
>
|
|
201
206
|
{comment.displayName ||
|
|
202
207
|
comment.handle ||
|
|
203
208
|
comment.did.slice(0, 16) + "..."}
|
|
204
|
-
</
|
|
209
|
+
</a>
|
|
205
210
|
<span className="text-[10px] text-zinc-300 shrink-0">
|
|
206
211
|
{formatRelativeTime(comment.createdAt)}
|
|
207
212
|
</span>
|
|
@@ -83,9 +83,14 @@ export function AuthButton() {
|
|
|
83
83
|
|
|
84
84
|
{showDropdown && (
|
|
85
85
|
<div className="absolute right-0 top-full mt-2 w-44 bg-white rounded-xl shadow-xl border border-zinc-100 py-2 z-50">
|
|
86
|
-
<
|
|
86
|
+
<a
|
|
87
|
+
href={session.did ? `https://www.impactindexer.org/data?did=${session.did}` : "#"}
|
|
88
|
+
target="_blank"
|
|
89
|
+
rel="noopener noreferrer"
|
|
90
|
+
className="block px-3 py-1.5 text-xs text-zinc-400 truncate hover:text-emerald-600 transition-colors"
|
|
91
|
+
>
|
|
87
92
|
@{session.handle}
|
|
88
|
-
</
|
|
93
|
+
</a>
|
|
89
94
|
<div className="h-px bg-zinc-100 my-1" />
|
|
90
95
|
<button
|
|
91
96
|
onClick={handleLogout}
|
|
@@ -35,10 +35,12 @@ interface BeadsGraphProps {
|
|
|
35
35
|
onBackgroundClick: () => void;
|
|
36
36
|
onNodeRightClick?: (node: GraphNode, event: MouseEvent) => void;
|
|
37
37
|
commentedNodeIds?: Map<string, number>;
|
|
38
|
-
claimedNodeAvatars?: Map<string, { avatar?: string; handle: string; claimedAt: string }>;
|
|
39
|
-
onAvatarHover?: (info: { handle: string; avatar?: string; claimedAt: string; x: number; y: number } | null) => void;
|
|
38
|
+
claimedNodeAvatars?: Map<string, { avatar?: string; handle: string; claimedAt: string; did?: string }>;
|
|
39
|
+
onAvatarHover?: (info: { handle: string; avatar?: string; claimedAt: string; did?: string; x: number; y: number } | null) => void;
|
|
40
40
|
timelineActive?: boolean;
|
|
41
41
|
stats?: { total: number; edges: number; prefixes: string[] };
|
|
42
|
+
/** When a right sidebar (NodeDetail, Comments, Activity) is open, shift bottom-right legend inward */
|
|
43
|
+
sidebarOpen?: boolean;
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
// Node size calculation
|
|
@@ -205,6 +207,7 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
205
207
|
onAvatarHover,
|
|
206
208
|
timelineActive,
|
|
207
209
|
stats,
|
|
210
|
+
sidebarOpen,
|
|
208
211
|
}, ref) {
|
|
209
212
|
const graphRef = useRef<any>(null);
|
|
210
213
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
@@ -255,7 +258,7 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
255
258
|
const hoveredNodeRef = useRef<GraphNode | null>(hoveredNode);
|
|
256
259
|
const connectedNodesRef = useRef<Set<string>>(new Set());
|
|
257
260
|
const commentedNodeIdsRef = useRef<Map<string, number>>(commentedNodeIds || new Map());
|
|
258
|
-
const claimedNodeAvatarsRef = useRef<Map<string, { avatar?: string; handle: string; claimedAt: string }>>(
|
|
261
|
+
const claimedNodeAvatarsRef = useRef<Map<string, { avatar?: string; handle: string; claimedAt: string; did?: string }>>(
|
|
259
262
|
claimedNodeAvatars || new Map()
|
|
260
263
|
);
|
|
261
264
|
// Callback ref for refreshing graph when avatar images finish loading
|
|
@@ -503,7 +506,7 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
503
506
|
if (dx * dx + dy * dy <= avatarRadius * avatarRadius) {
|
|
504
507
|
if (hoveredAvatarNodeRef.current !== node.id) {
|
|
505
508
|
hoveredAvatarNodeRef.current = node.id;
|
|
506
|
-
cb({ handle: claim.handle, avatar: claim.avatar, claimedAt: claim.claimedAt, x: e.clientX, y: e.clientY });
|
|
509
|
+
cb({ handle: claim.handle, avatar: claim.avatar, claimedAt: claim.claimedAt, did: claim.did, x: e.clientX, y: e.clientY });
|
|
507
510
|
}
|
|
508
511
|
return;
|
|
509
512
|
}
|
|
@@ -1699,7 +1702,10 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
1699
1702
|
|
|
1700
1703
|
{/* Bottom-right info panel: stats + status colors + legend (hidden when timeline active) */}
|
|
1701
1704
|
{!timelineActive && (
|
|
1702
|
-
<div
|
|
1705
|
+
<div
|
|
1706
|
+
className="absolute bottom-4 z-10 bg-white/90 backdrop-blur-sm rounded-lg border border-zinc-200 shadow-sm px-3 py-2 text-xs text-zinc-400 transition-[right] duration-300 ease-out"
|
|
1707
|
+
style={{ right: sidebarOpen ? "calc(360px + 1rem)" : "1rem" }}
|
|
1708
|
+
>
|
|
1703
1709
|
{stats && (
|
|
1704
1710
|
<div className="text-zinc-500 mb-1.5">
|
|
1705
1711
|
<strong className="text-zinc-700">{stats.total}</strong> issues
|
|
@@ -206,9 +206,14 @@ export function CommentTooltip({
|
|
|
206
206
|
)}
|
|
207
207
|
</div>
|
|
208
208
|
<div className="flex-1 min-w-0">
|
|
209
|
-
<
|
|
209
|
+
<a
|
|
210
|
+
href={`https://www.impactindexer.org/data?did=${comment.did}`}
|
|
211
|
+
target="_blank"
|
|
212
|
+
rel="noopener noreferrer"
|
|
213
|
+
className="text-[10px] font-medium text-zinc-500 hover:text-emerald-600 transition-colors"
|
|
214
|
+
>
|
|
210
215
|
{comment.displayName || comment.handle}
|
|
211
|
-
</
|
|
216
|
+
</a>
|
|
212
217
|
<p className="text-[11px] text-zinc-500 line-clamp-2 leading-tight">
|
|
213
218
|
{comment.text}
|
|
214
219
|
</p>
|
|
@@ -445,9 +445,14 @@ function InlineReplyForm({
|
|
|
445
445
|
<div className="mt-2 ml-4 pl-3 border-l border-emerald-200 space-y-1.5">
|
|
446
446
|
<div className="flex items-center gap-1.5 text-[10px] text-zinc-400">
|
|
447
447
|
<span>Replying to</span>
|
|
448
|
-
<
|
|
448
|
+
<a
|
|
449
|
+
href={`https://www.impactindexer.org/data?did=${replyingTo.did}`}
|
|
450
|
+
target="_blank"
|
|
451
|
+
rel="noopener noreferrer"
|
|
452
|
+
className="font-medium text-zinc-600 hover:text-emerald-600 transition-colors"
|
|
453
|
+
>
|
|
449
454
|
{replyingTo.displayName || replyingTo.handle}
|
|
450
|
-
</
|
|
455
|
+
</a>
|
|
451
456
|
</div>
|
|
452
457
|
<div className="flex gap-2">
|
|
453
458
|
<input
|
|
@@ -614,11 +619,16 @@ function CommentItem({
|
|
|
614
619
|
</div>
|
|
615
620
|
)}
|
|
616
621
|
</div>
|
|
617
|
-
<
|
|
622
|
+
<a
|
|
623
|
+
href={`https://www.impactindexer.org/data?did=${comment.did}`}
|
|
624
|
+
target="_blank"
|
|
625
|
+
rel="noopener noreferrer"
|
|
626
|
+
className="text-xs font-medium text-zinc-600 truncate hover:text-emerald-600 transition-colors"
|
|
627
|
+
>
|
|
618
628
|
{comment.displayName ||
|
|
619
629
|
comment.handle ||
|
|
620
630
|
comment.did.slice(0, 16) + "..."}
|
|
621
|
-
</
|
|
631
|
+
</a>
|
|
622
632
|
<span className="text-[10px] text-zinc-300 shrink-0">
|
|
623
633
|
{formatRelativeTime(comment.createdAt)}
|
|
624
634
|
</span>
|