Github Inline Comments
Github Inline Comments is a React and Tailwind CSS component from Eldora UI, licensed MIT. Copy it and paste it into your project.
Eldora UI
MIT
other
What it is
Github Inline Comments is a React and Tailwind CSS component from Eldora UI, licensed MIT. Copy it and paste it into your project.
How to use it
Open it in the catalogue, press copy, and paste it into your project. The prompt you copy carries the code and the rules that tell your agent to reproduce it without changing anything.
Five free copies a month. No card.
Needs
npm i react lucide-react
Licence
This component comes from Eldora UI and is redistributed under the MIT licence, which permits commercial use. The copyright notice travels with the code.
Github Inline Comments
"use client"
import { useEffect, useRef, useState } from "react"
import { CheckCircle2, MessageSquarePlus, X } from "lucide-react"
import { cn } from "@/lib/utils"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Badge } from "@/components/ui/badge"
import { Button } from "@/components/ui/button"
import { Separator } from "@/components/ui/separator"
import { Textarea } from "@/components/ui/textarea"
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip"
type Line =
| { kind: "hunk"; content: string }
| { kind: "context"; old: number | null; new: number | null; content: string }
| { kind: "add"; old: number | null; new: number | null; content: string }
| { kind: "del"; old: number | null; new: number | null; content: string }
export default function GithubInlineComments({
diff,
fileName,
}: {
diff: readonly Line[]
fileName: string
}) {
return <DiffList diff={diff} fileName={fileName} />
}
function DiffList({
diff,
fileName,
}: {
diff: readonly Line[]
fileName: string
}) {
const rows = diff ?? []
// Tracks which line index currently has an open thread
const [openThreadAt, setOpenThreadAt] = useState<number | null>(null)
// Tracks thread status per line
const [resolvedMap, setResolvedMap] = useState<Record<number, boolean>>({})
function toggleResolve(idx: number) {
setResolvedMap((m) => ({ ...m, [idx]: !m[idx] }))
}
return (
<TooltipProvider delayDuration={150}>
<div
role="table"
aria-label={`Diff of ${fileName}`}
className="bg-card rounded-md border dark:border-white/10"
>
<div className="flex items-center justify-between border-b px-2 py-1 dark:border-white/10">
<div className="flex items-center gap-2">
<span className="text-[13px] font-medium">{fileName}</span>
<Badge
variant="secondary"
aria-label="File status"
className="h-5 px-1.5 text-[11px]"
>
modified
</Badge>
</div>
</div>
<ol role="rowgroup" className="divide-y dark:divide-white/10">
{rows.map((line, idx) => {
const isChange = line.kind === "add" || line.kind === "del"
const isOpen = openThreadAt === idx
const isResolved = !!resolvedMap[idx]
return (
<li
key={idx}
role="row"
className={cn(
"group relative flex items-stretch text-[13px]",
line.kind === "hunk" && "bg-muted/50 text-muted-foreground",
line.kind === "add" &&
"bg-emerald-50/60 dark:bg-emerald-950/20",
line.kind === "del" && "bg-rose-50/60 dark:bg-rose-950/20"
)}
>
<div className="absolute top-1/2 -left-4 -translate-y-1/2 opacity-0 transition-opacity group-hover:opacity-100 focus-within:opacity-100">
{line.kind !== "hunk" && (
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon"
variant="secondary"
aria-label="Add inline comment"
className="h-5 w-5 rounded-full shadow-sm"
onClick={() => setOpenThreadAt(isOpen ? null : idx)}
>
<MessageSquarePlus className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent side="left">Add comment</TooltipContent>
</Tooltip>
)}
</div>
<div
role="cell"
className={cn(
"text-muted-foreground grid w-16 shrink-0 grid-cols-2 border-r text-[11px] dark:border-white/10"
)}
>
<span className="px-2 py-1 text-right tabular-nums">
{line.kind === "add"
? ""
: line.kind === "hunk"
? ""
: (line.old ?? "")}
</span>
<span className="px-2 py-1 text-right tabular-nums">
{line.kind === "del"
? ""
: line.kind === "hunk"
? ""
: (line.new ?? "")}
</span>
</div>
<div role="cell" className="flex-1">
<pre
className={cn(
"px-2 py-1 font-mono text-[12px] leading-5 whitespace-pre-wrap",
isChange && "pl-5"
)}
aria-label={`${line.kind} line`}
>
<span
aria-hidden
className={cn(
"mr-1 inline-block w-2 text-center font-semibold",
line.kind === "add" && "text-emerald-600",
line.kind === "del" && "text-rose-600"
)}
>
{line.kind === "add"
? "+"
: line.kind === "del"
? "-"
: " "}
</span>
{line.content}
</pre>
{openThreadAt === idx && line.kind !== "hunk" && (
<div className="bg-background border-t px-2 py-1.5 dark:border-white/10">
<InlineThread
resolved={isResolved}
onToggleResolve={() => toggleResolve(idx)}
onClose={() => setOpenThreadAt(null)}
/>
</div>
)}
</div>
</li>
)
})}
</ol>
</div>
</TooltipProvider>
)
}
type Comment = {
id: string
author: string
initials: string
body: string
createdAt: string
}
function InlineThread({
resolved,
onToggleResolve,
onClose,
}: {
resolved: boolean
onToggleResolve: () => void
onClose: () => void
}) {
const [comments, setComments] = useState<Comment[]>([
{
id: "c1",
author: "Reviewer",
initials: "RV",
body: "Consider handling the undefined case explicitly.",
createdAt: "just now",
},
])
const [draft, setDraft] = useState("")
const textRef = useRef<HTMLTextAreaElement | null>(null)
function addComment() {
const text = draft.trim()
if (!text) return
setComments((c) => [
...c,
{
id: crypto.randomUUID(),
author: "You",
initials: "YO",
body: text,
createdAt: "now",
},
])
setDraft("")
// focus back for fast sequences
requestAnimationFrame(() => textRef.current?.focus())
}
useEffect(() => {
function onKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") {
onClose()
}
}
window.addEventListener("keydown", onKeyDown)
return () => window.removeEventListener("keydown", onKeyDown)
}, [onClose])
return (
<div className="bg-card rounded-md border dark:border-white/10">
{/* Header with status chip */}
<div className="flex items-center justify-between gap-2 px-2 py-1">
<div className="flex items-center gap-2">
<Badge
className={cn(
"h-5 gap-1 px-1.5 text-[11px]",
resolved
? "bg-emerald-600 text-white hover:bg-emerald-600/90 dark:bg-emerald-500 dark:hover:bg-emerald-500/90"
: "bg-secondary text-foreground dark:bg-neutral-800 dark:text-neutral-100"
)}
>
{resolved ? <CheckCircle2 className="h-3.5 w-3.5" /> : null}
{resolved ? "Resolved" : "Open"}
</Badge>
</div>
<div className="flex items-center gap-1.5">
<Button
variant={resolved ? "secondary" : "default"}
size="sm"
onClick={onToggleResolve}
aria-pressed={resolved}
className="h-7 px-2 text-[12px]"
>
{resolved ? "Reopen" : "Resolve"}
</Button>
<Button
variant="ghost"
size="icon"
aria-label="Close thread"
onClick={onClose}
className="h-7 w-7"
>
<X className="h-4 w-4" />
</Button>
</div>
</div>
{/* Comments list */}
<ul className="space-y-1 px-2 py-1.5">
{comments.map((c) => (
<li key={c.id} className="flex items-start gap-2">
<Avatar className="h-5 w-5">
<AvatarFallback className="text-[9px]">
{c.initials}
</AvatarFallback>
</Avatar>
<div className="min-w-0 flex-1">
<div className="flex items-center justify-between gap-2">
<p className="truncate text-[12px] font-medium">{c.author}</p>
<span className="text-muted-foreground ml-2 shrink-0 text-[10px]">
{c.createdAt}
</span>
</div>
<p className="mt-0.5 text-[13px] leading-5">{c.body}</p>
</div>
</li>
))}
</ul>
<Separator />
{/* Editor */}
<div className="flex flex-col gap-2 px-2 py-1.5">
<label htmlFor="inline-comment" className="sr-only">
Add a comment
</label>
<Textarea
id="inline-comment"
ref={textRef}
value={draft}
onChange={(e) => setDraft(e.target.value)}
placeholder="Comment"
rows={2}
className="min-h-[40px] py-1.5 text-[13px]"
/>
<div className="flex items-center justify-end gap-1.5">
<Button
variant="ghost"
size="sm"
onClick={onClose}
className="h-7 px-2 text-[12px]"
>
Cancel
</Button>
<Button
size="sm"
onClick={addComment}
disabled={!draft.trim()}
className="h-7 px-2 text-[12px]"
>
Comment
</Button>
</div>
</div>
</div>
)
}