heartbeads 0.4.0
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 -0
- package/.next/app-build-manifest.json +49 -0
- package/.next/app-path-routes-manifest.json +1 -0
- package/.next/build-manifest.json +32 -0
- package/.next/export-marker.json +1 -0
- package/.next/images-manifest.json +1 -0
- package/.next/next-minimal-server.js.nft.json +1 -0
- package/.next/next-server.js.nft.json +1 -0
- package/.next/package.json +1 -0
- package/.next/prerender-manifest.json +1 -0
- package/.next/react-loadable-manifest.json +8 -0
- package/.next/required-server-files.json +1 -0
- package/.next/routes-manifest.json +1 -0
- package/.next/server/app/_not-found/page.js +1 -0
- package/.next/server/app/_not-found/page.js.nft.json +1 -0
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -0
- package/.next/server/app/_not-found.html +1 -0
- package/.next/server/app/_not-found.meta +6 -0
- package/.next/server/app/_not-found.rsc +9 -0
- package/.next/server/app/api/auth/route.js +1 -0
- package/.next/server/app/api/auth/route.js.nft.json +1 -0
- package/.next/server/app/api/beads/route.js +8 -0
- package/.next/server/app/api/beads/route.js.nft.json +1 -0
- package/.next/server/app/api/beads/stream/route.js +10 -0
- package/.next/server/app/api/beads/stream/route.js.nft.json +1 -0
- package/.next/server/app/api/beads.body +1 -0
- package/.next/server/app/api/beads.meta +1 -0
- package/.next/server/app/api/config/route.js +8 -0
- package/.next/server/app/api/config/route.js.nft.json +1 -0
- package/.next/server/app/api/docs/page.js +120 -0
- package/.next/server/app/api/docs/page.js.nft.json +1 -0
- package/.next/server/app/api/docs/page_client-reference-manifest.js +1 -0
- package/.next/server/app/api/docs.html +120 -0
- package/.next/server/app/api/docs.meta +5 -0
- package/.next/server/app/api/docs.rsc +70 -0
- package/.next/server/app/api/login/route.js +1 -0
- package/.next/server/app/api/login/route.js.nft.json +1 -0
- package/.next/server/app/api/logout/route.js +1 -0
- package/.next/server/app/api/logout/route.js.nft.json +1 -0
- package/.next/server/app/api/oauth/callback/route.js +1 -0
- package/.next/server/app/api/oauth/callback/route.js.nft.json +1 -0
- package/.next/server/app/api/oauth/client-metadata.json/route.js +1 -0
- package/.next/server/app/api/oauth/client-metadata.json/route.js.nft.json +1 -0
- package/.next/server/app/api/oauth/jwks.json/route.js +1 -0
- package/.next/server/app/api/oauth/jwks.json/route.js.nft.json +1 -0
- package/.next/server/app/api/records/route.js +1 -0
- package/.next/server/app/api/records/route.js.nft.json +1 -0
- package/.next/server/app/api/status/route.js +1 -0
- package/.next/server/app/api/status/route.js.nft.json +1 -0
- package/.next/server/app/api/v1/graph/route.js +1 -0
- package/.next/server/app/api/v1/graph/route.js.nft.json +1 -0
- package/.next/server/app/api/v1/issues/[id]/route.js +1 -0
- package/.next/server/app/api/v1/issues/[id]/route.js.nft.json +1 -0
- package/.next/server/app/api/v1/ready/route.js +1 -0
- package/.next/server/app/api/v1/ready/route.js.nft.json +1 -0
- package/.next/server/app/index.html +1 -0
- package/.next/server/app/index.meta +5 -0
- package/.next/server/app/index.rsc +9 -0
- package/.next/server/app/login/page.js +1 -0
- package/.next/server/app/login/page.js.nft.json +1 -0
- package/.next/server/app/login/page_client-reference-manifest.js +1 -0
- package/.next/server/app/login.html +1 -0
- package/.next/server/app/login.meta +5 -0
- package/.next/server/app/login.rsc +9 -0
- package/.next/server/app/opengraph-image.png/route.js +1 -0
- package/.next/server/app/opengraph-image.png/route.js.nft.json +1 -0
- package/.next/server/app/opengraph-image.png.body +0 -0
- package/.next/server/app/opengraph-image.png.meta +1 -0
- package/.next/server/app/page.js +24 -0
- package/.next/server/app/page.js.nft.json +1 -0
- package/.next/server/app/page_client-reference-manifest.js +1 -0
- package/.next/server/app/twitter-image.png/route.js +1 -0
- package/.next/server/app/twitter-image.png/route.js.nft.json +1 -0
- package/.next/server/app/twitter-image.png.body +0 -0
- package/.next/server/app/twitter-image.png.meta +1 -0
- package/.next/server/app-paths-manifest.json +22 -0
- package/.next/server/chunks/247.js +12 -0
- package/.next/server/chunks/29.js +1 -0
- package/.next/server/chunks/343.js +1 -0
- package/.next/server/chunks/460.js +12 -0
- package/.next/server/chunks/533.js +38 -0
- package/.next/server/chunks/542.js +27 -0
- package/.next/server/chunks/590.js +6 -0
- package/.next/server/chunks/615.js +15 -0
- package/.next/server/chunks/696.js +25 -0
- package/.next/server/chunks/719.js +2 -0
- package/.next/server/chunks/739.js +1 -0
- package/.next/server/chunks/950.js +2 -0
- package/.next/server/chunks/font-manifest.json +1 -0
- package/.next/server/edge-runtime-webpack.js +2 -0
- package/.next/server/edge-runtime-webpack.js.map +1 -0
- package/.next/server/font-manifest.json +1 -0
- package/.next/server/functions-config-manifest.json +1 -0
- package/.next/server/interception-route-rewrite-manifest.js +1 -0
- package/.next/server/middleware-build-manifest.js +1 -0
- package/.next/server/middleware-manifest.json +32 -0
- package/.next/server/middleware-react-loadable-manifest.js +1 -0
- package/.next/server/middleware.js +14 -0
- package/.next/server/middleware.js.map +1 -0
- package/.next/server/next-font-manifest.js +1 -0
- package/.next/server/next-font-manifest.json +1 -0
- package/.next/server/pages/404.html +1 -0
- package/.next/server/pages/500.html +1 -0
- package/.next/server/pages/_app.js +1 -0
- package/.next/server/pages/_app.js.nft.json +1 -0
- package/.next/server/pages/_document.js +1 -0
- package/.next/server/pages/_document.js.nft.json +1 -0
- package/.next/server/pages/_error.js +1 -0
- package/.next/server/pages/_error.js.nft.json +1 -0
- package/.next/server/pages-manifest.json +1 -0
- package/.next/server/server-reference-manifest.js +1 -0
- package/.next/server/server-reference-manifest.json +1 -0
- package/.next/server/webpack-runtime.js +1 -0
- package/.next/static/chunks/149.a3e3a5dc03e21086.js +1 -0
- package/.next/static/chunks/2200cc46-7c93a0e00b0bb825.js +1 -0
- package/.next/static/chunks/788-aa413085174e935a.js +1 -0
- package/.next/static/chunks/945-3ff1d381a0af1ecd.js +2 -0
- package/.next/static/chunks/971-bb44d52bcd9ee2a9.js +1 -0
- package/.next/static/chunks/app/_not-found/page-200b7a7a6cfc29df.js +1 -0
- package/.next/static/chunks/app/api/docs/page-1dc18f40154cdce6.js +1 -0
- package/.next/static/chunks/app/layout-13e3cdaaa416edb6.js +1 -0
- package/.next/static/chunks/app/login/page-60d930d64f021753.js +1 -0
- package/.next/static/chunks/app/not-found-ae1139bed2018dd8.js +1 -0
- package/.next/static/chunks/app/page-583300dd8af66e5a.js +1 -0
- package/.next/static/chunks/framework-6e06c675866dc992.js +1 -0
- package/.next/static/chunks/main-app-8b0c4a1007dbb7f4.js +1 -0
- package/.next/static/chunks/main-e680fb049d7426e1.js +1 -0
- package/.next/static/chunks/pages/_app-0c3037849002a4aa.js +1 -0
- package/.next/static/chunks/pages/_error-a647cd2c75dc4dc7.js +1 -0
- package/.next/static/chunks/polyfills-42372ed130431b0a.js +1 -0
- package/.next/static/chunks/webpack-117444a4bfe51057.js +1 -0
- package/.next/static/css/8c1b520a38ba4ccd.css +3 -0
- package/.next/static/vFM69sDrBUf_9ULwPmVAE/_buildManifest.js +1 -0
- package/.next/static/vFM69sDrBUf_9ULwPmVAE/_ssgManifest.js +1 -0
- package/README.md +389 -0
- package/app/api/auth/route.ts +103 -0
- package/app/api/beads/route.ts +27 -0
- package/app/api/beads/stream/route.ts +83 -0
- package/app/api/config/route.ts +48 -0
- package/app/api/docs/page.tsx +497 -0
- package/app/api/login/route.ts +42 -0
- package/app/api/logout/route.ts +14 -0
- package/app/api/oauth/callback/route.ts +97 -0
- package/app/api/oauth/client-metadata.json/route.ts +33 -0
- package/app/api/oauth/jwks.json/route.ts +32 -0
- package/app/api/records/route.ts +168 -0
- package/app/api/status/route.ts +25 -0
- package/app/api/v1/graph/route.ts +251 -0
- package/app/api/v1/issues/[id]/route.ts +158 -0
- package/app/api/v1/ready/route.ts +229 -0
- package/app/globals.css +230 -0
- package/app/layout.tsx +51 -0
- package/app/login/page.tsx +164 -0
- package/app/not-found.tsx +91 -0
- package/app/opengraph-image.png +0 -0
- package/app/page.tsx +2041 -0
- package/app/twitter-image.png +0 -0
- package/bin/heartbeads.mjs +225 -0
- package/components/ActivityItem.tsx +326 -0
- package/components/ActivityOverlay.tsx +125 -0
- package/components/ActivityPanel.tsx +345 -0
- package/components/AllCommentsPanel.tsx +270 -0
- package/components/AuthButton.tsx +202 -0
- package/components/BeadTooltip.tsx +246 -0
- package/components/BeadsGraph.tsx +2493 -0
- package/components/BeadsLogo.tsx +94 -0
- package/components/CommentTooltip.tsx +338 -0
- package/components/ContextMenu.tsx +272 -0
- package/components/DescriptionModal.tsx +595 -0
- package/components/GraphStats.tsx +121 -0
- package/components/HeartIcon.tsx +33 -0
- package/components/HelpPanel.tsx +339 -0
- package/components/MobileActionSheet.tsx +255 -0
- package/components/NodeDetail.tsx +793 -0
- package/components/SettingsModal.tsx +315 -0
- package/components/StatusLegend.tsx +99 -0
- package/components/TimelineBar.tsx +116 -0
- package/components/TutorialOverlay.tsx +235 -0
- package/hooks/useBeadsComments.ts +81 -0
- package/hooks/useIsMobile.ts +19 -0
- package/lib/activity.ts +377 -0
- package/lib/agent.ts +29 -0
- package/lib/api-helpers.ts +46 -0
- package/lib/auth/client.ts +221 -0
- package/lib/auth.tsx +159 -0
- package/lib/comments.ts +413 -0
- package/lib/diff-beads.ts +128 -0
- package/lib/discover.ts +228 -0
- package/lib/env.ts +33 -0
- package/lib/gate.ts +55 -0
- package/lib/parse-beads.ts +234 -0
- package/lib/session.ts +52 -0
- package/lib/settings.ts +42 -0
- package/lib/timeline.ts +138 -0
- package/lib/tts.ts +397 -0
- package/lib/types.ts +271 -0
- package/lib/utils.ts +48 -0
- package/lib/watch-beads.ts +97 -0
- package/next.config.mjs +4 -0
- package/package.json +81 -0
- package/postcss.config.mjs +9 -0
- package/public/image.png +0 -0
- package/scripts/generate-jwk.js +38 -0
- package/tailwind.config.ts +41 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Animated heartbeat (ECG) logo for heartbeads.
|
|
5
|
+
* A minimalistic, elegant heartbeat trace line that continuously animates
|
|
6
|
+
* with a drawing/pulse effect — representing the living pulse of a project.
|
|
7
|
+
*/
|
|
8
|
+
export function BeadsLogo({ className }: { className?: string }) {
|
|
9
|
+
return (
|
|
10
|
+
<svg
|
|
11
|
+
viewBox="0 0 32 32"
|
|
12
|
+
fill="none"
|
|
13
|
+
className={className}
|
|
14
|
+
aria-hidden="true"
|
|
15
|
+
>
|
|
16
|
+
{/* Heartbeat ECG trace — the signature PQRST waveform */}
|
|
17
|
+
<polyline
|
|
18
|
+
points="1,16 6,16 8,16 9.5,14 11,18 12.5,6 14,26 15.5,10 17,16 19,16 20.5,14.5 21.5,17.5 22.5,16 31,16"
|
|
19
|
+
stroke="currentColor"
|
|
20
|
+
strokeWidth="1.5"
|
|
21
|
+
strokeLinecap="round"
|
|
22
|
+
strokeLinejoin="round"
|
|
23
|
+
fill="none"
|
|
24
|
+
opacity="0.9"
|
|
25
|
+
>
|
|
26
|
+
{/* Dash animation: line draws itself, then fades and redraws */}
|
|
27
|
+
<animate
|
|
28
|
+
attributeName="stroke-dasharray"
|
|
29
|
+
values="0 60;60 0"
|
|
30
|
+
dur="2s"
|
|
31
|
+
repeatCount="indefinite"
|
|
32
|
+
/>
|
|
33
|
+
<animate
|
|
34
|
+
attributeName="opacity"
|
|
35
|
+
values="0.4;0.95;0.95;0.4"
|
|
36
|
+
dur="2s"
|
|
37
|
+
repeatCount="indefinite"
|
|
38
|
+
/>
|
|
39
|
+
</polyline>
|
|
40
|
+
|
|
41
|
+
{/* Trailing glow — a second faded copy offset in time */}
|
|
42
|
+
<polyline
|
|
43
|
+
points="1,16 6,16 8,16 9.5,14 11,18 12.5,6 14,26 15.5,10 17,16 19,16 20.5,14.5 21.5,17.5 22.5,16 31,16"
|
|
44
|
+
stroke="currentColor"
|
|
45
|
+
strokeWidth="2.5"
|
|
46
|
+
strokeLinecap="round"
|
|
47
|
+
strokeLinejoin="round"
|
|
48
|
+
fill="none"
|
|
49
|
+
opacity="0.15"
|
|
50
|
+
>
|
|
51
|
+
<animate
|
|
52
|
+
attributeName="stroke-dasharray"
|
|
53
|
+
values="0 60;60 0"
|
|
54
|
+
dur="2s"
|
|
55
|
+
repeatCount="indefinite"
|
|
56
|
+
/>
|
|
57
|
+
<animate
|
|
58
|
+
attributeName="opacity"
|
|
59
|
+
values="0;0.2;0.2;0"
|
|
60
|
+
dur="2s"
|
|
61
|
+
repeatCount="indefinite"
|
|
62
|
+
/>
|
|
63
|
+
</polyline>
|
|
64
|
+
|
|
65
|
+
{/* Pulse dot — travels along the trace */}
|
|
66
|
+
<circle cx="0" cy="16" r="1.5" fill="currentColor" opacity="0">
|
|
67
|
+
<animate
|
|
68
|
+
attributeName="cx"
|
|
69
|
+
values="1;6;8;9.5;11;12.5;14;15.5;17;19;20.5;21.5;22.5;31"
|
|
70
|
+
dur="2s"
|
|
71
|
+
repeatCount="indefinite"
|
|
72
|
+
/>
|
|
73
|
+
<animate
|
|
74
|
+
attributeName="cy"
|
|
75
|
+
values="16;16;16;14;18;6;26;10;16;16;14.5;17.5;16;16"
|
|
76
|
+
dur="2s"
|
|
77
|
+
repeatCount="indefinite"
|
|
78
|
+
/>
|
|
79
|
+
<animate
|
|
80
|
+
attributeName="opacity"
|
|
81
|
+
values="0;0.8;1;1;1;1;1;1;0.8;0.6;0.6;0.6;0.4;0"
|
|
82
|
+
dur="2s"
|
|
83
|
+
repeatCount="indefinite"
|
|
84
|
+
/>
|
|
85
|
+
<animate
|
|
86
|
+
attributeName="r"
|
|
87
|
+
values="1;1.5;2;2.5;2;2.5;2;1.5;1.5;1;1;1;1;0.5"
|
|
88
|
+
dur="2s"
|
|
89
|
+
repeatCount="indefinite"
|
|
90
|
+
/>
|
|
91
|
+
</circle>
|
|
92
|
+
</svg>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect, useCallback } from "react";
|
|
4
|
+
import type { GraphNode } from "@/lib/types";
|
|
5
|
+
import { PREFIX_COLORS } from "@/lib/types";
|
|
6
|
+
import type { BeadsComment } from "@/hooks/useBeadsComments";
|
|
7
|
+
import { useIsMobile } from "@/hooks/useIsMobile";
|
|
8
|
+
|
|
9
|
+
interface CommentTooltipProps {
|
|
10
|
+
node: GraphNode;
|
|
11
|
+
x: number;
|
|
12
|
+
y: number;
|
|
13
|
+
onClose: () => void;
|
|
14
|
+
onSubmit: (text: string) => Promise<void>;
|
|
15
|
+
isAuthenticated: boolean;
|
|
16
|
+
existingComments?: BeadsComment[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function CommentTooltip({
|
|
20
|
+
node,
|
|
21
|
+
x,
|
|
22
|
+
y,
|
|
23
|
+
onClose,
|
|
24
|
+
onSubmit,
|
|
25
|
+
isAuthenticated,
|
|
26
|
+
existingComments,
|
|
27
|
+
}: CommentTooltipProps) {
|
|
28
|
+
const [text, setText] = useState("");
|
|
29
|
+
const [sending, setSending] = useState(false);
|
|
30
|
+
const tooltipRef = useRef<HTMLDivElement>(null);
|
|
31
|
+
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
|
32
|
+
const [pos, setPos] = useState({ x: 0, y: 0 });
|
|
33
|
+
const [visible, setVisible] = useState(false);
|
|
34
|
+
const isMobile = useIsMobile();
|
|
35
|
+
|
|
36
|
+
// Position the tooltip after first render (measure dimensions)
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
if (!tooltipRef.current) return;
|
|
39
|
+
const tt = tooltipRef.current.getBoundingClientRect();
|
|
40
|
+
const vw = window.innerWidth;
|
|
41
|
+
const vh = window.innerHeight;
|
|
42
|
+
|
|
43
|
+
let nx = x + 14;
|
|
44
|
+
let ny = y - tt.height - 14;
|
|
45
|
+
|
|
46
|
+
// Clamp right
|
|
47
|
+
if (nx + tt.width > vw - 16) nx = vw - tt.width - 16;
|
|
48
|
+
// Clamp left
|
|
49
|
+
if (nx < 16) nx = 16;
|
|
50
|
+
// If overflows top, flip below cursor
|
|
51
|
+
if (ny < 16) ny = y + 28;
|
|
52
|
+
// Clamp bottom
|
|
53
|
+
if (ny + tt.height > vh - 16) ny = vh - tt.height - 16;
|
|
54
|
+
|
|
55
|
+
setPos({ x: nx, y: ny });
|
|
56
|
+
setVisible(true);
|
|
57
|
+
}, [x, y]);
|
|
58
|
+
|
|
59
|
+
// Auto-focus textarea on mount
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (isAuthenticated) {
|
|
62
|
+
// Small delay to let position settle
|
|
63
|
+
const timer = setTimeout(() => textareaRef.current?.focus(), 100);
|
|
64
|
+
return () => clearTimeout(timer);
|
|
65
|
+
}
|
|
66
|
+
}, [isAuthenticated]);
|
|
67
|
+
|
|
68
|
+
// Close on Escape
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
const handleKeyDown = (e: KeyboardEvent) => {
|
|
71
|
+
if (e.key === "Escape") onClose();
|
|
72
|
+
};
|
|
73
|
+
window.addEventListener("keydown", handleKeyDown);
|
|
74
|
+
return () => window.removeEventListener("keydown", handleKeyDown);
|
|
75
|
+
}, [onClose]);
|
|
76
|
+
|
|
77
|
+
// Close on click outside
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const handleMouseDown = (e: MouseEvent) => {
|
|
80
|
+
if (
|
|
81
|
+
tooltipRef.current &&
|
|
82
|
+
!tooltipRef.current.contains(e.target as Node)
|
|
83
|
+
) {
|
|
84
|
+
onClose();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
// Use a small delay so the right-click that opened the tooltip doesn't
|
|
88
|
+
// immediately close it
|
|
89
|
+
const timer = setTimeout(() => {
|
|
90
|
+
window.addEventListener("mousedown", handleMouseDown);
|
|
91
|
+
}, 50);
|
|
92
|
+
return () => {
|
|
93
|
+
clearTimeout(timer);
|
|
94
|
+
window.removeEventListener("mousedown", handleMouseDown);
|
|
95
|
+
};
|
|
96
|
+
}, [onClose]);
|
|
97
|
+
|
|
98
|
+
const handleSubmit = useCallback(async () => {
|
|
99
|
+
if (!text.trim() || sending) return;
|
|
100
|
+
setSending(true);
|
|
101
|
+
try {
|
|
102
|
+
await onSubmit(text.trim());
|
|
103
|
+
setText("");
|
|
104
|
+
} catch (err) {
|
|
105
|
+
console.error("Failed to post comment:", err);
|
|
106
|
+
} finally {
|
|
107
|
+
setSending(false);
|
|
108
|
+
}
|
|
109
|
+
}, [text, sending, onSubmit]);
|
|
110
|
+
|
|
111
|
+
// Handle Ctrl+Enter to submit
|
|
112
|
+
const handleKeyDown = useCallback(
|
|
113
|
+
(e: React.KeyboardEvent) => {
|
|
114
|
+
if ((e.metaKey || e.ctrlKey) && e.key === "Enter") {
|
|
115
|
+
e.preventDefault();
|
|
116
|
+
handleSubmit();
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
[handleSubmit]
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
const prefixColor = PREFIX_COLORS[node.prefix] || "#a1a1aa";
|
|
123
|
+
const commentCount = existingComments?.length || 0;
|
|
124
|
+
|
|
125
|
+
// Shared form content used in both mobile and desktop layouts
|
|
126
|
+
const formContent = (
|
|
127
|
+
<>
|
|
128
|
+
{/* Node info */}
|
|
129
|
+
<div className="mb-3">
|
|
130
|
+
<span className="text-xs font-mono text-emerald-600">
|
|
131
|
+
{node.id}
|
|
132
|
+
</span>
|
|
133
|
+
<h3 className="text-sm font-semibold text-zinc-800 leading-tight mt-0.5">
|
|
134
|
+
{node.title}
|
|
135
|
+
</h3>
|
|
136
|
+
</div>
|
|
137
|
+
|
|
138
|
+
{/* Existing comments preview */}
|
|
139
|
+
{commentCount > 0 && existingComments && (
|
|
140
|
+
<div className="mb-3 pb-3 border-b border-zinc-100">
|
|
141
|
+
<div className="flex items-center gap-1.5 mb-2">
|
|
142
|
+
<svg
|
|
143
|
+
className="w-3.5 h-3.5 text-blue-500"
|
|
144
|
+
fill="none"
|
|
145
|
+
viewBox="0 0 24 24"
|
|
146
|
+
strokeWidth={1.5}
|
|
147
|
+
stroke="currentColor"
|
|
148
|
+
>
|
|
149
|
+
<path
|
|
150
|
+
strokeLinecap="round"
|
|
151
|
+
strokeLinejoin="round"
|
|
152
|
+
d="M12 20.25c4.97 0 9-3.694 9-8.25s-4.03-8.25-9-8.25S3 7.444 3 12c0 2.104.859 4.023 2.273 5.48.432.447.74 1.04.586 1.641a4.483 4.483 0 01-.923 1.785A5.969 5.969 0 006 21c1.282 0 2.47-.402 3.445-1.087.81.22 1.668.337 2.555.337z"
|
|
153
|
+
/>
|
|
154
|
+
</svg>
|
|
155
|
+
<span className="text-[10px] font-medium text-zinc-500 uppercase tracking-wider">
|
|
156
|
+
{commentCount} comment{commentCount !== 1 ? "s" : ""}
|
|
157
|
+
</span>
|
|
158
|
+
</div>
|
|
159
|
+
{/* Show most recent 2 comments */}
|
|
160
|
+
<div className="space-y-2">
|
|
161
|
+
{existingComments.slice(0, 2).map((comment) => (
|
|
162
|
+
<div key={comment.uri} className="flex gap-1.5">
|
|
163
|
+
<div className="shrink-0 w-4 h-4 rounded-full bg-zinc-100 overflow-hidden mt-0.5">
|
|
164
|
+
{comment.avatar ? (
|
|
165
|
+
<img
|
|
166
|
+
src={comment.avatar}
|
|
167
|
+
alt=""
|
|
168
|
+
className="w-full h-full object-cover"
|
|
169
|
+
/>
|
|
170
|
+
) : (
|
|
171
|
+
<div className="w-full h-full flex items-center justify-center text-[7px] font-medium text-zinc-400">
|
|
172
|
+
{(comment.handle || comment.did)
|
|
173
|
+
.charAt(0)
|
|
174
|
+
.toUpperCase()}
|
|
175
|
+
</div>
|
|
176
|
+
)}
|
|
177
|
+
</div>
|
|
178
|
+
<div className="flex-1 min-w-0">
|
|
179
|
+
<a
|
|
180
|
+
href={`https://www.impactindexer.org/data?did=${comment.did}`}
|
|
181
|
+
target="_blank"
|
|
182
|
+
rel="noopener noreferrer"
|
|
183
|
+
className="text-[10px] font-medium text-zinc-500 hover:text-emerald-600 transition-colors"
|
|
184
|
+
>
|
|
185
|
+
{comment.displayName || comment.handle}
|
|
186
|
+
</a>
|
|
187
|
+
<p className="text-[11px] text-zinc-500 line-clamp-2 leading-tight">
|
|
188
|
+
{comment.text}
|
|
189
|
+
</p>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
))}
|
|
193
|
+
{commentCount > 2 && (
|
|
194
|
+
<p className="text-[10px] text-zinc-400 italic">
|
|
195
|
+
+{commentCount - 2} more — see detail panel
|
|
196
|
+
</p>
|
|
197
|
+
)}
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
|
|
202
|
+
{/* Compose area */}
|
|
203
|
+
{isAuthenticated ? (
|
|
204
|
+
<div>
|
|
205
|
+
<textarea
|
|
206
|
+
ref={textareaRef}
|
|
207
|
+
value={text}
|
|
208
|
+
onChange={(e) => setText(e.target.value)}
|
|
209
|
+
onKeyDown={handleKeyDown}
|
|
210
|
+
placeholder="Leave a comment..."
|
|
211
|
+
rows={3}
|
|
212
|
+
className="w-full px-2.5 py-2 text-xs border border-zinc-200 rounded-md bg-zinc-50 text-zinc-700 placeholder-zinc-400 resize-none focus:outline-none focus:ring-1 focus:ring-emerald-500 focus:border-emerald-500"
|
|
213
|
+
/>
|
|
214
|
+
<div className="flex items-center justify-between mt-2">
|
|
215
|
+
<span className="text-[10px] text-zinc-300">
|
|
216
|
+
{typeof navigator !== "undefined" &&
|
|
217
|
+
navigator.platform?.includes("Mac")
|
|
218
|
+
? "\u2318"
|
|
219
|
+
: "Ctrl"}
|
|
220
|
+
+Enter to send
|
|
221
|
+
</span>
|
|
222
|
+
<div className="flex items-center gap-2">
|
|
223
|
+
<button
|
|
224
|
+
onClick={onClose}
|
|
225
|
+
className="px-2.5 py-1 text-[11px] text-zinc-400 hover:text-zinc-600 transition-colors"
|
|
226
|
+
>
|
|
227
|
+
Cancel
|
|
228
|
+
</button>
|
|
229
|
+
<button
|
|
230
|
+
onClick={handleSubmit}
|
|
231
|
+
disabled={!text.trim() || sending}
|
|
232
|
+
className="px-3 py-1 text-[11px] font-medium text-white bg-emerald-500 rounded-md hover:bg-emerald-600 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
233
|
+
>
|
|
234
|
+
{sending ? (
|
|
235
|
+
<span className="flex items-center gap-1">
|
|
236
|
+
<svg
|
|
237
|
+
className="w-3 h-3 animate-spin"
|
|
238
|
+
fill="none"
|
|
239
|
+
viewBox="0 0 24 24"
|
|
240
|
+
>
|
|
241
|
+
<circle
|
|
242
|
+
className="opacity-25"
|
|
243
|
+
cx="12"
|
|
244
|
+
cy="12"
|
|
245
|
+
r="10"
|
|
246
|
+
stroke="currentColor"
|
|
247
|
+
strokeWidth="4"
|
|
248
|
+
/>
|
|
249
|
+
<path
|
|
250
|
+
className="opacity-75"
|
|
251
|
+
fill="currentColor"
|
|
252
|
+
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
|
|
253
|
+
/>
|
|
254
|
+
</svg>
|
|
255
|
+
Sending
|
|
256
|
+
</span>
|
|
257
|
+
) : (
|
|
258
|
+
"Send"
|
|
259
|
+
)}
|
|
260
|
+
</button>
|
|
261
|
+
</div>
|
|
262
|
+
</div>
|
|
263
|
+
</div>
|
|
264
|
+
) : (
|
|
265
|
+
<div className="text-center py-3">
|
|
266
|
+
<svg
|
|
267
|
+
className="w-5 h-5 text-zinc-300 mx-auto mb-1.5"
|
|
268
|
+
fill="none"
|
|
269
|
+
viewBox="0 0 24 24"
|
|
270
|
+
strokeWidth={1.5}
|
|
271
|
+
stroke="currentColor"
|
|
272
|
+
>
|
|
273
|
+
<path
|
|
274
|
+
strokeLinecap="round"
|
|
275
|
+
strokeLinejoin="round"
|
|
276
|
+
d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z"
|
|
277
|
+
/>
|
|
278
|
+
</svg>
|
|
279
|
+
<p className="text-xs text-zinc-400">Sign in to comment</p>
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
</>
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// Mobile: bottom sheet layout
|
|
286
|
+
if (isMobile) {
|
|
287
|
+
return (
|
|
288
|
+
<>
|
|
289
|
+
<div className="fixed inset-0 z-[99] bg-black/20" onClick={onClose} />
|
|
290
|
+
<div className="fixed inset-x-0 bottom-0 z-[100] bg-white rounded-t-2xl shadow-2xl animate-slide-up-sheet">
|
|
291
|
+
<div className="w-10 h-1 bg-zinc-300 rounded-full mx-auto mt-3 mb-1" />
|
|
292
|
+
<div className="px-4 pb-6 pt-2">
|
|
293
|
+
{formContent}
|
|
294
|
+
</div>
|
|
295
|
+
<div className="h-[env(safe-area-inset-bottom)]" />
|
|
296
|
+
</div>
|
|
297
|
+
</>
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Desktop: floating tooltip
|
|
302
|
+
return (
|
|
303
|
+
<div
|
|
304
|
+
ref={tooltipRef}
|
|
305
|
+
style={{
|
|
306
|
+
position: "fixed",
|
|
307
|
+
left: pos.x,
|
|
308
|
+
top: pos.y,
|
|
309
|
+
width: 320,
|
|
310
|
+
zIndex: 100,
|
|
311
|
+
opacity: visible ? 1 : 0,
|
|
312
|
+
transform: visible ? "translateY(0)" : "translateY(4px)",
|
|
313
|
+
transition: "opacity 0.2s ease, transform 0.2s ease",
|
|
314
|
+
}}
|
|
315
|
+
className="bg-white border border-zinc-200 rounded-lg overflow-hidden"
|
|
316
|
+
onContextMenu={(e) => e.preventDefault()}
|
|
317
|
+
>
|
|
318
|
+
<style>{`
|
|
319
|
+
.comment-tooltip-shadow {
|
|
320
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.08), 0 2px 8px rgba(0,0,0,0.08);
|
|
321
|
+
}
|
|
322
|
+
`}</style>
|
|
323
|
+
<div className="comment-tooltip-shadow p-[18px_20px]">
|
|
324
|
+
{/* Colored accent bar */}
|
|
325
|
+
<div
|
|
326
|
+
className="rounded-sm mb-2.5"
|
|
327
|
+
style={{
|
|
328
|
+
width: 24,
|
|
329
|
+
height: 2,
|
|
330
|
+
background: prefixColor,
|
|
331
|
+
opacity: 0.6,
|
|
332
|
+
}}
|
|
333
|
+
/>
|
|
334
|
+
{formContent}
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
);
|
|
338
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useRef, useEffect } from "react";
|
|
4
|
+
import type { GraphNode } from "@/lib/types";
|
|
5
|
+
|
|
6
|
+
interface ContextMenuProps {
|
|
7
|
+
node: GraphNode;
|
|
8
|
+
x: number;
|
|
9
|
+
y: number;
|
|
10
|
+
onShowDescription: () => void;
|
|
11
|
+
onAddComment: () => void;
|
|
12
|
+
onClaimTask?: () => void;
|
|
13
|
+
onUnclaimTask?: () => void;
|
|
14
|
+
onCollapseEpic?: () => void;
|
|
15
|
+
onUncollapseEpic?: () => void;
|
|
16
|
+
onFocusEpic?: () => void;
|
|
17
|
+
onExitFocusEpic?: () => void;
|
|
18
|
+
onClose: () => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function ContextMenu({
|
|
22
|
+
node,
|
|
23
|
+
x,
|
|
24
|
+
y,
|
|
25
|
+
onShowDescription,
|
|
26
|
+
onAddComment,
|
|
27
|
+
onClaimTask,
|
|
28
|
+
onUnclaimTask,
|
|
29
|
+
onCollapseEpic,
|
|
30
|
+
onUncollapseEpic,
|
|
31
|
+
onFocusEpic,
|
|
32
|
+
onExitFocusEpic,
|
|
33
|
+
onClose,
|
|
34
|
+
}: ContextMenuProps) {
|
|
35
|
+
const menuRef = useRef<HTMLDivElement>(null);
|
|
36
|
+
const [pos, setPos] = useState({ x: 0, y: 0 });
|
|
37
|
+
const [visible, setVisible] = useState(false);
|
|
38
|
+
|
|
39
|
+
// Position + clamp to viewport
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!menuRef.current) return;
|
|
42
|
+
const rect = menuRef.current.getBoundingClientRect();
|
|
43
|
+
const vw = window.innerWidth;
|
|
44
|
+
const vh = window.innerHeight;
|
|
45
|
+
let nx = x + 4;
|
|
46
|
+
let ny = y + 4;
|
|
47
|
+
if (nx + rect.width > vw - 16) nx = vw - rect.width - 16;
|
|
48
|
+
if (nx < 16) nx = 16;
|
|
49
|
+
if (ny + rect.height > vh - 16) ny = vh - rect.height - 16;
|
|
50
|
+
if (ny < 16) ny = 16;
|
|
51
|
+
setPos({ x: nx, y: ny });
|
|
52
|
+
setVisible(true);
|
|
53
|
+
}, [x, y]);
|
|
54
|
+
|
|
55
|
+
// Escape key
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
const handler = (e: KeyboardEvent) => {
|
|
58
|
+
if (e.key === "Escape") onClose();
|
|
59
|
+
};
|
|
60
|
+
window.addEventListener("keydown", handler);
|
|
61
|
+
return () => window.removeEventListener("keydown", handler);
|
|
62
|
+
}, [onClose]);
|
|
63
|
+
|
|
64
|
+
// Click outside (with delay so triggering right-click doesn't immediately close)
|
|
65
|
+
useEffect(() => {
|
|
66
|
+
const handler = (e: MouseEvent) => {
|
|
67
|
+
if (menuRef.current && !menuRef.current.contains(e.target as Node)) {
|
|
68
|
+
onClose();
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
const timer = setTimeout(
|
|
72
|
+
() => window.addEventListener("mousedown", handler),
|
|
73
|
+
50
|
|
74
|
+
);
|
|
75
|
+
return () => {
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
window.removeEventListener("mousedown", handler);
|
|
78
|
+
};
|
|
79
|
+
}, [onClose]);
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<div
|
|
83
|
+
ref={menuRef}
|
|
84
|
+
style={{
|
|
85
|
+
position: "fixed",
|
|
86
|
+
left: pos.x,
|
|
87
|
+
top: pos.y,
|
|
88
|
+
zIndex: 100,
|
|
89
|
+
opacity: visible ? 1 : 0,
|
|
90
|
+
transform: visible ? "translateY(0)" : "translateY(2px)",
|
|
91
|
+
transition: "opacity 0.15s ease, transform 0.15s ease",
|
|
92
|
+
}}
|
|
93
|
+
onContextMenu={(e) => e.preventDefault()}
|
|
94
|
+
>
|
|
95
|
+
<div
|
|
96
|
+
className="bg-white border border-zinc-200 rounded-lg overflow-hidden"
|
|
97
|
+
style={{
|
|
98
|
+
minWidth: 180,
|
|
99
|
+
boxShadow:
|
|
100
|
+
"0 4px 16px rgba(0,0,0,0.08), 0 1px 4px rgba(0,0,0,0.06)",
|
|
101
|
+
}}
|
|
102
|
+
>
|
|
103
|
+
{node.description && (
|
|
104
|
+
<button
|
|
105
|
+
onClick={onShowDescription}
|
|
106
|
+
className="w-full px-3 py-2.5 text-xs text-zinc-700 hover:bg-zinc-50 flex items-center gap-2 transition-colors border-b border-zinc-100"
|
|
107
|
+
>
|
|
108
|
+
<svg
|
|
109
|
+
className="w-3.5 h-3.5 text-zinc-400"
|
|
110
|
+
fill="none"
|
|
111
|
+
viewBox="0 0 24 24"
|
|
112
|
+
strokeWidth={1.5}
|
|
113
|
+
stroke="currentColor"
|
|
114
|
+
>
|
|
115
|
+
<path
|
|
116
|
+
strokeLinecap="round"
|
|
117
|
+
strokeLinejoin="round"
|
|
118
|
+
d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z"
|
|
119
|
+
/>
|
|
120
|
+
</svg>
|
|
121
|
+
Show description
|
|
122
|
+
</button>
|
|
123
|
+
)}
|
|
124
|
+
<button
|
|
125
|
+
onClick={onAddComment}
|
|
126
|
+
className={`w-full px-3 py-2.5 text-xs text-zinc-700 hover:bg-zinc-50 flex items-center gap-2 transition-colors${onClaimTask || onUnclaimTask || onCollapseEpic || onUncollapseEpic || onFocusEpic || onExitFocusEpic ? " border-b border-zinc-100" : ""}`}
|
|
127
|
+
>
|
|
128
|
+
<svg
|
|
129
|
+
className="w-3.5 h-3.5 text-zinc-400"
|
|
130
|
+
fill="none"
|
|
131
|
+
viewBox="0 0 24 24"
|
|
132
|
+
strokeWidth={1.5}
|
|
133
|
+
stroke="currentColor"
|
|
134
|
+
>
|
|
135
|
+
<path
|
|
136
|
+
strokeLinecap="round"
|
|
137
|
+
strokeLinejoin="round"
|
|
138
|
+
d="M12 20.25c4.97 0 9-3.694 9-8.25s-4.03-8.25-9-8.25S3 7.444 3 12c0 2.104.859 4.023 2.273 5.48.432.447.74 1.04.586 1.641a4.483 4.483 0 01-.923 1.785A5.969 5.969 0 006 21c1.282 0 2.47-.402 3.445-1.087.81.22 1.668.337 2.555.337z"
|
|
139
|
+
/>
|
|
140
|
+
</svg>
|
|
141
|
+
Add comment
|
|
142
|
+
</button>
|
|
143
|
+
{onClaimTask && (
|
|
144
|
+
<button
|
|
145
|
+
onClick={onClaimTask}
|
|
146
|
+
className={`w-full px-3 py-2.5 text-xs text-zinc-700 hover:bg-zinc-50 flex items-center gap-2 transition-colors${onCollapseEpic || onUncollapseEpic || onFocusEpic || onExitFocusEpic ? " border-b border-zinc-100" : ""}`}
|
|
147
|
+
>
|
|
148
|
+
<svg
|
|
149
|
+
className="w-3.5 h-3.5 text-zinc-400"
|
|
150
|
+
fill="none"
|
|
151
|
+
viewBox="0 0 24 24"
|
|
152
|
+
strokeWidth={1.5}
|
|
153
|
+
stroke="currentColor"
|
|
154
|
+
>
|
|
155
|
+
<path
|
|
156
|
+
strokeLinecap="round"
|
|
157
|
+
strokeLinejoin="round"
|
|
158
|
+
d="M15.75 6a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0zM4.501 20.118a7.5 7.5 0 0114.998 0A17.933 17.933 0 0112 21.75c-2.676 0-5.216-.584-7.499-1.632z"
|
|
159
|
+
/>
|
|
160
|
+
</svg>
|
|
161
|
+
Claim task
|
|
162
|
+
</button>
|
|
163
|
+
)}
|
|
164
|
+
{onUnclaimTask && (
|
|
165
|
+
<button
|
|
166
|
+
onClick={onUnclaimTask}
|
|
167
|
+
className={`w-full px-3 py-2.5 text-xs text-red-500 hover:bg-red-50 flex items-center gap-2 transition-colors${onCollapseEpic || onUncollapseEpic || onFocusEpic || onExitFocusEpic ? " border-b border-zinc-100" : ""}`}
|
|
168
|
+
>
|
|
169
|
+
<svg
|
|
170
|
+
className="w-3.5 h-3.5 text-red-400"
|
|
171
|
+
fill="none"
|
|
172
|
+
viewBox="0 0 24 24"
|
|
173
|
+
strokeWidth={1.5}
|
|
174
|
+
stroke="currentColor"
|
|
175
|
+
>
|
|
176
|
+
<path
|
|
177
|
+
strokeLinecap="round"
|
|
178
|
+
strokeLinejoin="round"
|
|
179
|
+
d="M22 10.5h-6m-2.25-4.125a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zM4 19.235v-.11a6.375 6.375 0 0112.75 0v.109A12.318 12.318 0 0110.374 21c-2.331 0-4.512-.645-6.374-1.766z"
|
|
180
|
+
/>
|
|
181
|
+
</svg>
|
|
182
|
+
Unclaim task
|
|
183
|
+
</button>
|
|
184
|
+
)}
|
|
185
|
+
{onCollapseEpic && (
|
|
186
|
+
<button
|
|
187
|
+
onClick={onCollapseEpic}
|
|
188
|
+
className={`w-full px-3 py-2.5 text-xs text-zinc-700 hover:bg-zinc-50 flex items-center gap-2 transition-colors${onFocusEpic || onExitFocusEpic ? " border-b border-zinc-100" : ""}`}
|
|
189
|
+
>
|
|
190
|
+
<svg
|
|
191
|
+
className="w-3.5 h-3.5 text-zinc-400"
|
|
192
|
+
fill="none"
|
|
193
|
+
viewBox="0 0 24 24"
|
|
194
|
+
strokeWidth={1.5}
|
|
195
|
+
stroke="currentColor"
|
|
196
|
+
>
|
|
197
|
+
<path
|
|
198
|
+
strokeLinecap="round"
|
|
199
|
+
strokeLinejoin="round"
|
|
200
|
+
d="M9 9V4.5M9 9H4.5M9 9 3.75 3.75M9 15v4.5M9 15H4.5M9 15l-5.25 5.25M15 9h4.5M15 9V4.5M15 9l5.25-5.25M15 15h4.5M15 15v4.5m0-4.5 5.25 5.25"
|
|
201
|
+
/>
|
|
202
|
+
</svg>
|
|
203
|
+
Collapse epic
|
|
204
|
+
</button>
|
|
205
|
+
)}
|
|
206
|
+
{onUncollapseEpic && (
|
|
207
|
+
<button
|
|
208
|
+
onClick={onUncollapseEpic}
|
|
209
|
+
className={`w-full px-3 py-2.5 text-xs text-zinc-700 hover:bg-zinc-50 flex items-center gap-2 transition-colors${onFocusEpic || onExitFocusEpic ? " border-b border-zinc-100" : ""}`}
|
|
210
|
+
>
|
|
211
|
+
<svg
|
|
212
|
+
className="w-3.5 h-3.5 text-zinc-400"
|
|
213
|
+
fill="none"
|
|
214
|
+
viewBox="0 0 24 24"
|
|
215
|
+
strokeWidth={1.5}
|
|
216
|
+
stroke="currentColor"
|
|
217
|
+
>
|
|
218
|
+
<path
|
|
219
|
+
strokeLinecap="round"
|
|
220
|
+
strokeLinejoin="round"
|
|
221
|
+
d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15"
|
|
222
|
+
/>
|
|
223
|
+
</svg>
|
|
224
|
+
Uncollapse epic
|
|
225
|
+
</button>
|
|
226
|
+
)}
|
|
227
|
+
{onFocusEpic && (
|
|
228
|
+
<button
|
|
229
|
+
onClick={onFocusEpic}
|
|
230
|
+
className="w-full px-3 py-2.5 text-xs text-zinc-700 hover:bg-zinc-50 flex items-center gap-2 transition-colors"
|
|
231
|
+
>
|
|
232
|
+
<svg
|
|
233
|
+
className="w-3.5 h-3.5 text-zinc-400"
|
|
234
|
+
fill="none"
|
|
235
|
+
viewBox="0 0 24 24"
|
|
236
|
+
strokeWidth={1.5}
|
|
237
|
+
stroke="currentColor"
|
|
238
|
+
>
|
|
239
|
+
<path
|
|
240
|
+
strokeLinecap="round"
|
|
241
|
+
strokeLinejoin="round"
|
|
242
|
+
d="M7.5 3.75H6A2.25 2.25 0 003.75 6v1.5M16.5 3.75H18A2.25 2.25 0 0120.25 6v1.5M16.5 20.25H18A2.25 2.25 0 0020.25 18v-1.5M7.5 20.25H6A2.25 2.25 0 013.75 18v-1.5"
|
|
243
|
+
/>
|
|
244
|
+
</svg>
|
|
245
|
+
Focus on epic
|
|
246
|
+
</button>
|
|
247
|
+
)}
|
|
248
|
+
{onExitFocusEpic && (
|
|
249
|
+
<button
|
|
250
|
+
onClick={onExitFocusEpic}
|
|
251
|
+
className="w-full px-3 py-2.5 text-xs text-emerald-600 hover:bg-emerald-50 flex items-center gap-2 transition-colors"
|
|
252
|
+
>
|
|
253
|
+
<svg
|
|
254
|
+
className="w-3.5 h-3.5 text-emerald-500"
|
|
255
|
+
fill="none"
|
|
256
|
+
viewBox="0 0 24 24"
|
|
257
|
+
strokeWidth={1.5}
|
|
258
|
+
stroke="currentColor"
|
|
259
|
+
>
|
|
260
|
+
<path
|
|
261
|
+
strokeLinecap="round"
|
|
262
|
+
strokeLinejoin="round"
|
|
263
|
+
d="M3.75 3.75v4.5m0-4.5h4.5m-4.5 0L9 9M3.75 20.25v-4.5m0 4.5h4.5m-4.5 0L9 15M20.25 3.75h-4.5m4.5 0v4.5m0-4.5L15 9m5.25 11.25h-4.5m4.5 0v-4.5m0 4.5L15 15"
|
|
264
|
+
/>
|
|
265
|
+
</svg>
|
|
266
|
+
Show full graph
|
|
267
|
+
</button>
|
|
268
|
+
)}
|
|
269
|
+
</div>
|
|
270
|
+
</div>
|
|
271
|
+
);
|
|
272
|
+
}
|