beads-map 0.3.1 → 0.3.4
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 +1 -1
- package/.next/server/functions-config-manifest.json +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.json +1 -1
- package/.next/static/chunks/app/page-cf8e14cb4afc8112.js +1 -0
- package/.next/static/css/ade5301262971664.css +3 -0
- package/README.md +21 -11
- package/app/page.tsx +150 -7
- package/components/BeadTooltip.tsx +26 -2
- package/components/BeadsGraph.tsx +433 -243
- package/components/ContextMenu.tsx +51 -5
- package/components/DescriptionModal.tsx +48 -18
- package/components/HelpPanel.tsx +336 -0
- package/components/NodeDetail.tsx +33 -8
- package/components/TutorialOverlay.tsx +187 -0
- package/lib/types.ts +2 -1
- package/lib/utils.ts +23 -0
- package/package.json +1 -1
- package/.next/static/chunks/app/page-a0493d6741516b53.js +0 -1
- package/.next/static/css/4fded26534cb91e3.css +0 -3
- /package/.next/static/{_OvcD8YYgVPHv6Tomg-pB → JmL0suxsggbSwPxWcmUFV}/_buildManifest.js +0 -0
- /package/.next/static/{_OvcD8YYgVPHv6Tomg-pB → JmL0suxsggbSwPxWcmUFV}/_ssgManifest.js +0 -0
|
@@ -13,6 +13,8 @@ interface ContextMenuProps {
|
|
|
13
13
|
onUnclaimTask?: () => void;
|
|
14
14
|
onCollapseEpic?: () => void;
|
|
15
15
|
onUncollapseEpic?: () => void;
|
|
16
|
+
onFocusEpic?: () => void;
|
|
17
|
+
onExitFocusEpic?: () => void;
|
|
16
18
|
onClose: () => void;
|
|
17
19
|
}
|
|
18
20
|
|
|
@@ -26,6 +28,8 @@ export function ContextMenu({
|
|
|
26
28
|
onUnclaimTask,
|
|
27
29
|
onCollapseEpic,
|
|
28
30
|
onUncollapseEpic,
|
|
31
|
+
onFocusEpic,
|
|
32
|
+
onExitFocusEpic,
|
|
29
33
|
onClose,
|
|
30
34
|
}: ContextMenuProps) {
|
|
31
35
|
const menuRef = useRef<HTMLDivElement>(null);
|
|
@@ -119,7 +123,7 @@ export function ContextMenu({
|
|
|
119
123
|
)}
|
|
120
124
|
<button
|
|
121
125
|
onClick={onAddComment}
|
|
122
|
-
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 ? " border-b border-zinc-100" : ""}`}
|
|
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" : ""}`}
|
|
123
127
|
>
|
|
124
128
|
<svg
|
|
125
129
|
className="w-3.5 h-3.5 text-zinc-400"
|
|
@@ -139,7 +143,7 @@ export function ContextMenu({
|
|
|
139
143
|
{onClaimTask && (
|
|
140
144
|
<button
|
|
141
145
|
onClick={onClaimTask}
|
|
142
|
-
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 ? " border-b border-zinc-100" : ""}`}
|
|
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" : ""}`}
|
|
143
147
|
>
|
|
144
148
|
<svg
|
|
145
149
|
className="w-3.5 h-3.5 text-zinc-400"
|
|
@@ -160,7 +164,7 @@ export function ContextMenu({
|
|
|
160
164
|
{onUnclaimTask && (
|
|
161
165
|
<button
|
|
162
166
|
onClick={onUnclaimTask}
|
|
163
|
-
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 ? " border-b border-zinc-100" : ""}`}
|
|
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" : ""}`}
|
|
164
168
|
>
|
|
165
169
|
<svg
|
|
166
170
|
className="w-3.5 h-3.5 text-red-400"
|
|
@@ -181,7 +185,7 @@ export function ContextMenu({
|
|
|
181
185
|
{onCollapseEpic && (
|
|
182
186
|
<button
|
|
183
187
|
onClick={onCollapseEpic}
|
|
184
|
-
className=
|
|
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" : ""}`}
|
|
185
189
|
>
|
|
186
190
|
<svg
|
|
187
191
|
className="w-3.5 h-3.5 text-zinc-400"
|
|
@@ -202,7 +206,7 @@ export function ContextMenu({
|
|
|
202
206
|
{onUncollapseEpic && (
|
|
203
207
|
<button
|
|
204
208
|
onClick={onUncollapseEpic}
|
|
205
|
-
className=
|
|
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" : ""}`}
|
|
206
210
|
>
|
|
207
211
|
<svg
|
|
208
212
|
className="w-3.5 h-3.5 text-zinc-400"
|
|
@@ -220,6 +224,48 @@ export function ContextMenu({
|
|
|
220
224
|
Uncollapse epic
|
|
221
225
|
</button>
|
|
222
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
|
+
)}
|
|
223
269
|
</div>
|
|
224
270
|
</div>
|
|
225
271
|
);
|
|
@@ -1,16 +1,29 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import { useState } from "react";
|
|
3
4
|
import { createPortal } from "react-dom";
|
|
4
5
|
import ReactMarkdown from "react-markdown";
|
|
5
6
|
import remarkGfm from "remark-gfm";
|
|
6
7
|
import type { GraphNode } from "@/lib/types";
|
|
8
|
+
import { buildDescriptionCopyText } from "@/lib/utils";
|
|
7
9
|
|
|
8
10
|
interface DescriptionModalProps {
|
|
9
11
|
node: GraphNode;
|
|
10
12
|
onClose: () => void;
|
|
13
|
+
repoUrl?: string;
|
|
11
14
|
}
|
|
12
15
|
|
|
13
|
-
export function DescriptionModal({ node, onClose }: DescriptionModalProps) {
|
|
16
|
+
export function DescriptionModal({ node, onClose, repoUrl }: DescriptionModalProps) {
|
|
17
|
+
const [copied, setCopied] = useState(false);
|
|
18
|
+
|
|
19
|
+
const handleCopy = () => {
|
|
20
|
+
if (!node.description) return;
|
|
21
|
+
navigator.clipboard.writeText(buildDescriptionCopyText(node, repoUrl)).then(() => {
|
|
22
|
+
setCopied(true);
|
|
23
|
+
setTimeout(() => setCopied(false), 1500);
|
|
24
|
+
});
|
|
25
|
+
};
|
|
26
|
+
|
|
14
27
|
if (!node.description) return null;
|
|
15
28
|
|
|
16
29
|
return createPortal(
|
|
@@ -32,24 +45,41 @@ export function DescriptionModal({ node, onClose }: DescriptionModalProps) {
|
|
|
32
45
|
{node.title}
|
|
33
46
|
</span>
|
|
34
47
|
</div>
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
className="w-4 h-4"
|
|
41
|
-
fill="none"
|
|
42
|
-
stroke="currentColor"
|
|
43
|
-
viewBox="0 0 24 24"
|
|
44
|
-
strokeWidth={2}
|
|
48
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
49
|
+
<button
|
|
50
|
+
onClick={handleCopy}
|
|
51
|
+
className="p-1 text-zinc-400 hover:text-zinc-600 transition-colors"
|
|
52
|
+
title="Copy description"
|
|
45
53
|
>
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
54
|
+
{copied ? (
|
|
55
|
+
<svg className="w-4 h-4 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
|
|
56
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
|
57
|
+
</svg>
|
|
58
|
+
) : (
|
|
59
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
|
|
60
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9.75a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184" />
|
|
61
|
+
</svg>
|
|
62
|
+
)}
|
|
63
|
+
</button>
|
|
64
|
+
<button
|
|
65
|
+
onClick={onClose}
|
|
66
|
+
className="p-1 text-zinc-400 hover:text-zinc-600 transition-colors"
|
|
67
|
+
>
|
|
68
|
+
<svg
|
|
69
|
+
className="w-4 h-4"
|
|
70
|
+
fill="none"
|
|
71
|
+
stroke="currentColor"
|
|
72
|
+
viewBox="0 0 24 24"
|
|
73
|
+
strokeWidth={2}
|
|
74
|
+
>
|
|
75
|
+
<path
|
|
76
|
+
strokeLinecap="round"
|
|
77
|
+
strokeLinejoin="round"
|
|
78
|
+
d="M6 18L18 6M6 6l12 12"
|
|
79
|
+
/>
|
|
80
|
+
</svg>
|
|
81
|
+
</button>
|
|
82
|
+
</div>
|
|
53
83
|
</div>
|
|
54
84
|
{/* Modal body */}
|
|
55
85
|
<div className="flex-1 overflow-y-auto px-5 py-4 custom-scrollbar description-markdown text-sm text-zinc-700 leading-relaxed">
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { TUTORIAL_STEPS } from "./TutorialOverlay";
|
|
4
|
+
|
|
5
|
+
// Catppuccin Latte accents
|
|
6
|
+
const CAT = {
|
|
7
|
+
red: "#d20f39",
|
|
8
|
+
teal: "#179299",
|
|
9
|
+
peach: "#fe640b",
|
|
10
|
+
blue: "#1e66f5",
|
|
11
|
+
green: "#40a02b",
|
|
12
|
+
mauve: "#8839ef",
|
|
13
|
+
sapphire: "#209fb5",
|
|
14
|
+
pink: "#ea76cb",
|
|
15
|
+
surface: "#dce0e8",
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
interface HelpPanelProps {
|
|
19
|
+
isOpen: boolean;
|
|
20
|
+
onClose: () => void;
|
|
21
|
+
tutorialStep?: number | null;
|
|
22
|
+
onStartTutorial?: () => void;
|
|
23
|
+
onNextStep?: () => void;
|
|
24
|
+
onPrevStep?: () => void;
|
|
25
|
+
onEndTutorial?: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function HelpPanel({
|
|
29
|
+
isOpen,
|
|
30
|
+
onClose,
|
|
31
|
+
tutorialStep = null,
|
|
32
|
+
onStartTutorial,
|
|
33
|
+
onNextStep,
|
|
34
|
+
onPrevStep,
|
|
35
|
+
onEndTutorial,
|
|
36
|
+
}: HelpPanelProps) {
|
|
37
|
+
const isTutorialActive = tutorialStep !== null;
|
|
38
|
+
const headerText = isTutorialActive ? "Tutorial" : "Welcome to Heartbeads";
|
|
39
|
+
|
|
40
|
+
const content = isTutorialActive ? (
|
|
41
|
+
<TutorialContent
|
|
42
|
+
step={tutorialStep}
|
|
43
|
+
onNext={onNextStep!}
|
|
44
|
+
onPrev={onPrevStep!}
|
|
45
|
+
onEnd={onEndTutorial!}
|
|
46
|
+
/>
|
|
47
|
+
) : (
|
|
48
|
+
<HelpContent onStartTutorial={onStartTutorial} />
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<>
|
|
53
|
+
{/* Desktop: right sidebar — z-[60] during tutorial so it sits above the overlay */}
|
|
54
|
+
<aside
|
|
55
|
+
className={`hidden md:flex absolute top-0 right-0 h-full w-[360px] bg-white border-l border-zinc-200 flex-col shadow-xl transform transition-transform duration-300 ease-out ${
|
|
56
|
+
isOpen ? "translate-x-0" : "translate-x-full"
|
|
57
|
+
} ${isTutorialActive ? "z-[60]" : "z-30"}`}
|
|
58
|
+
>
|
|
59
|
+
<div className="flex items-center justify-between px-5 py-3 border-b border-zinc-100 shrink-0">
|
|
60
|
+
<h2 className="text-sm font-semibold text-zinc-900">
|
|
61
|
+
{headerText}
|
|
62
|
+
</h2>
|
|
63
|
+
<button
|
|
64
|
+
onClick={onClose}
|
|
65
|
+
className="p-1 text-zinc-400 hover:text-zinc-600 transition-colors"
|
|
66
|
+
>
|
|
67
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
|
|
68
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
69
|
+
</svg>
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
<div className="flex-1 overflow-y-auto custom-scrollbar">
|
|
73
|
+
{content}
|
|
74
|
+
</div>
|
|
75
|
+
</aside>
|
|
76
|
+
|
|
77
|
+
{/* Mobile: bottom drawer */}
|
|
78
|
+
<div
|
|
79
|
+
className={`md:hidden fixed inset-x-0 bottom-0 transform transition-transform duration-300 ease-out ${
|
|
80
|
+
isOpen ? "translate-y-0" : "translate-y-full"
|
|
81
|
+
} ${isTutorialActive ? "z-[60]" : "z-20"}`}
|
|
82
|
+
>
|
|
83
|
+
<div className="bg-white rounded-t-2xl shadow-2xl border-t border-zinc-200 max-h-[70vh] flex flex-col">
|
|
84
|
+
<div className="flex items-center justify-between px-5 py-3 border-b border-zinc-100 shrink-0">
|
|
85
|
+
<h2 className="text-sm font-semibold text-zinc-900">
|
|
86
|
+
{headerText}
|
|
87
|
+
</h2>
|
|
88
|
+
<button
|
|
89
|
+
onClick={onClose}
|
|
90
|
+
className="p-1 text-zinc-400 hover:text-zinc-600 transition-colors"
|
|
91
|
+
>
|
|
92
|
+
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
|
|
93
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M6 18L18 6M6 6l12 12" />
|
|
94
|
+
</svg>
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
<div className="flex-1 overflow-y-auto custom-scrollbar">
|
|
98
|
+
{content}
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* ------------------------------------------------------------------ */
|
|
107
|
+
/* Tutorial step-by-step content */
|
|
108
|
+
/* ------------------------------------------------------------------ */
|
|
109
|
+
|
|
110
|
+
function TutorialContent({
|
|
111
|
+
step,
|
|
112
|
+
onNext,
|
|
113
|
+
onPrev,
|
|
114
|
+
onEnd,
|
|
115
|
+
}: {
|
|
116
|
+
step: number;
|
|
117
|
+
onNext: () => void;
|
|
118
|
+
onPrev: () => void;
|
|
119
|
+
onEnd: () => void;
|
|
120
|
+
}) {
|
|
121
|
+
const currentStep = TUTORIAL_STEPS[step];
|
|
122
|
+
const totalSteps = TUTORIAL_STEPS.length;
|
|
123
|
+
const isFirst = step === 0;
|
|
124
|
+
const isLast = step === totalSteps - 1;
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<div className="px-5 py-4 flex flex-col min-h-[300px]">
|
|
128
|
+
{/* Step indicator */}
|
|
129
|
+
<div className="flex items-center justify-between mb-4">
|
|
130
|
+
<span className="text-xs font-medium" style={{ color: CAT.teal }}>
|
|
131
|
+
{step + 1} / {totalSteps}
|
|
132
|
+
</span>
|
|
133
|
+
<div className="flex gap-1">
|
|
134
|
+
{Array.from({ length: totalSteps }).map((_, i) => (
|
|
135
|
+
<div
|
|
136
|
+
key={i}
|
|
137
|
+
className="w-1.5 h-1.5 rounded-full transition-colors"
|
|
138
|
+
style={{
|
|
139
|
+
backgroundColor:
|
|
140
|
+
i === step ? CAT.green : i < step ? CAT.teal : CAT.surface,
|
|
141
|
+
}}
|
|
142
|
+
/>
|
|
143
|
+
))}
|
|
144
|
+
</div>
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
{/* Step title */}
|
|
148
|
+
<h3 className="text-base font-semibold text-zinc-900 mb-2">
|
|
149
|
+
{currentStep.title}
|
|
150
|
+
</h3>
|
|
151
|
+
|
|
152
|
+
{/* Step description */}
|
|
153
|
+
<p className="text-[13px] text-zinc-600 leading-relaxed mb-6">
|
|
154
|
+
{currentStep.description}
|
|
155
|
+
</p>
|
|
156
|
+
|
|
157
|
+
{/* Spacer */}
|
|
158
|
+
<div className="flex-1" />
|
|
159
|
+
|
|
160
|
+
{/* Navigation */}
|
|
161
|
+
<div className="flex items-center gap-2 pt-4 border-t border-zinc-100">
|
|
162
|
+
{isFirst ? (
|
|
163
|
+
<button
|
|
164
|
+
onClick={onEnd}
|
|
165
|
+
className="px-4 py-2 text-sm font-medium text-zinc-400 hover:text-zinc-600 rounded-lg hover:bg-zinc-50 transition-colors"
|
|
166
|
+
>
|
|
167
|
+
Skip
|
|
168
|
+
</button>
|
|
169
|
+
) : (
|
|
170
|
+
<button
|
|
171
|
+
onClick={onPrev}
|
|
172
|
+
className="px-4 py-2 text-sm font-medium rounded-lg hover:bg-zinc-50 transition-colors"
|
|
173
|
+
style={{ color: CAT.blue }}
|
|
174
|
+
>
|
|
175
|
+
{"Back"}
|
|
176
|
+
</button>
|
|
177
|
+
)}
|
|
178
|
+
<div className="flex-1" />
|
|
179
|
+
{isLast ? (
|
|
180
|
+
<button
|
|
181
|
+
onClick={onEnd}
|
|
182
|
+
className="px-4 py-2 text-sm font-medium text-white rounded-lg bg-emerald-500 hover:bg-emerald-600 transition-colors"
|
|
183
|
+
>
|
|
184
|
+
{"Done"}
|
|
185
|
+
</button>
|
|
186
|
+
) : (
|
|
187
|
+
<button
|
|
188
|
+
onClick={onNext}
|
|
189
|
+
className="px-4 py-2 text-sm font-medium text-white rounded-lg bg-emerald-500 hover:bg-emerald-600 transition-colors"
|
|
190
|
+
>
|
|
191
|
+
{"Next"}
|
|
192
|
+
</button>
|
|
193
|
+
)}
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/* ------------------------------------------------------------------ */
|
|
200
|
+
/* Bullet with Catppuccin colored dot */
|
|
201
|
+
/* ------------------------------------------------------------------ */
|
|
202
|
+
|
|
203
|
+
function Bullet({ color, children }: { color: string; children: React.ReactNode }) {
|
|
204
|
+
return (
|
|
205
|
+
<li className="flex gap-2.5 items-start">
|
|
206
|
+
<span
|
|
207
|
+
className="w-1.5 h-1.5 rounded-full mt-[6px] shrink-0"
|
|
208
|
+
style={{ backgroundColor: color }}
|
|
209
|
+
/>
|
|
210
|
+
<span>{children}</span>
|
|
211
|
+
</li>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function SectionTitle({ children, color }: { children: React.ReactNode; color: string }) {
|
|
216
|
+
return (
|
|
217
|
+
<h3
|
|
218
|
+
className="text-[11px] font-semibold uppercase tracking-widest mb-2 mt-5 first:mt-0"
|
|
219
|
+
style={{ color }}
|
|
220
|
+
>
|
|
221
|
+
{children}
|
|
222
|
+
</h3>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* ------------------------------------------------------------------ */
|
|
227
|
+
/* Static help content */
|
|
228
|
+
/* ------------------------------------------------------------------ */
|
|
229
|
+
|
|
230
|
+
function HelpContent({ onStartTutorial }: { onStartTutorial?: () => void }) {
|
|
231
|
+
return (
|
|
232
|
+
<div className="px-5 py-4 text-[13px] text-zinc-600 leading-relaxed">
|
|
233
|
+
{/* Start Tutorial */}
|
|
234
|
+
{onStartTutorial && (
|
|
235
|
+
<button
|
|
236
|
+
onClick={onStartTutorial}
|
|
237
|
+
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 mb-5 text-white text-sm font-medium rounded-lg bg-emerald-500 hover:bg-emerald-600 transition-colors"
|
|
238
|
+
>
|
|
239
|
+
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" strokeWidth={2} stroke="currentColor">
|
|
240
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M12 18v-5.25m0 0a6.01 6.01 0 001.5-.189m-1.5.189a6.01 6.01 0 01-1.5-.189m3.75 7.478a12.06 12.06 0 01-4.5 0m3.75 2.383a14.406 14.406 0 01-3 0M14.25 18v-.192c0-.983.658-1.823 1.508-2.316a7.5 7.5 0 10-7.517 0c.85.493 1.509 1.333 1.509 2.316V18" />
|
|
241
|
+
</svg>
|
|
242
|
+
Take the guided tour
|
|
243
|
+
</button>
|
|
244
|
+
)}
|
|
245
|
+
|
|
246
|
+
<p className="text-zinc-900 font-medium mb-1">
|
|
247
|
+
Your command center for AI coding tasks.
|
|
248
|
+
</p>
|
|
249
|
+
<p className="mb-4 text-zinc-500">
|
|
250
|
+
Heartbeads shows everything your AI agents are working on as a
|
|
251
|
+
live, interactive graph — tasks, dependencies, who's doing what, and
|
|
252
|
+
what's blocking progress.
|
|
253
|
+
</p>
|
|
254
|
+
|
|
255
|
+
<SectionTitle color={CAT.red}>The graph</SectionTitle>
|
|
256
|
+
<p className="mb-2">
|
|
257
|
+
Each <strong>circle</strong> is a task. <strong>Flowing particles</strong>{" "}
|
|
258
|
+
stream between them to show dependency direction.
|
|
259
|
+
</p>
|
|
260
|
+
<ul className="space-y-1.5 mb-3">
|
|
261
|
+
<Bullet color={CAT.red}><strong>Bigger circles</strong> — more connected, more important</Bullet>
|
|
262
|
+
<Bullet color={CAT.red}><strong>Solid lines + particles</strong> — “blocks” (A must finish before B)</Bullet>
|
|
263
|
+
<Bullet color={CAT.red}><strong>Dashed lines</strong> — parent-child grouping</Bullet>
|
|
264
|
+
<Bullet color={CAT.red}><strong>Colored ring</strong> — which project it belongs to</Bullet>
|
|
265
|
+
</ul>
|
|
266
|
+
|
|
267
|
+
<SectionTitle color={CAT.blue}>Navigation</SectionTitle>
|
|
268
|
+
<ul className="space-y-1.5 mb-3">
|
|
269
|
+
<Bullet color={CAT.blue}><strong>Click</strong> a node to open its details</Bullet>
|
|
270
|
+
<Bullet color={CAT.blue}><strong>Hover</strong> for a quick summary</Bullet>
|
|
271
|
+
<Bullet color={CAT.blue}><strong>Right-click</strong> for actions — descriptions, comments, claims, collapse</Bullet>
|
|
272
|
+
<Bullet color={CAT.blue}><strong>Scroll</strong> to zoom, <strong>drag</strong> to pan</Bullet>
|
|
273
|
+
<Bullet color={CAT.blue}><strong>Cmd/Ctrl+F</strong> to search by name, ID, owner, or commenter</Bullet>
|
|
274
|
+
</ul>
|
|
275
|
+
|
|
276
|
+
<SectionTitle color={CAT.teal}>Layouts</SectionTitle>
|
|
277
|
+
<p className="mb-2">
|
|
278
|
+
Top-left buttons rearrange the graph:
|
|
279
|
+
</p>
|
|
280
|
+
<ul className="space-y-1.5 mb-3">
|
|
281
|
+
<Bullet color={CAT.teal}><strong>Force</strong> — organic, physics-based</Bullet>
|
|
282
|
+
<Bullet color={CAT.teal}><strong>DAG</strong> — clean top-down tree</Bullet>
|
|
283
|
+
<Bullet color={CAT.teal}><strong>Radial</strong> — rings from center outward</Bullet>
|
|
284
|
+
<Bullet color={CAT.teal}><strong>Cluster</strong> — grouped by project</Bullet>
|
|
285
|
+
<Bullet color={CAT.teal}><strong>Spread</strong> — spaced out for screenshots</Bullet>
|
|
286
|
+
</ul>
|
|
287
|
+
|
|
288
|
+
<SectionTitle color={CAT.peach}>Color modes</SectionTitle>
|
|
289
|
+
<p className="mb-2">
|
|
290
|
+
Bottom-right panel — paint nodes by:
|
|
291
|
+
</p>
|
|
292
|
+
<ul className="space-y-1.5 mb-3">
|
|
293
|
+
<Bullet color={CAT.peach}><strong>Status</strong> — open, in progress, blocked, closed</Bullet>
|
|
294
|
+
<Bullet color={CAT.peach}><strong>Priority</strong> — P0 critical to P4 backlog</Bullet>
|
|
295
|
+
<Bullet color={CAT.peach}><strong>Owner</strong> — who created it</Bullet>
|
|
296
|
+
<Bullet color={CAT.peach}><strong>Assignee</strong> — who's working on it</Bullet>
|
|
297
|
+
<Bullet color={CAT.peach}><strong>Prefix</strong> — which project</Bullet>
|
|
298
|
+
</ul>
|
|
299
|
+
|
|
300
|
+
<SectionTitle color={CAT.mauve}>More</SectionTitle>
|
|
301
|
+
<ul className="space-y-1.5 mb-3">
|
|
302
|
+
<Bullet color={CAT.mauve}><strong>Collapse/Expand</strong> — tidy up epics into single nodes</Bullet>
|
|
303
|
+
<Bullet color={CAT.mauve}><strong>Clusters</strong> — dashed circles grouping projects when zoomed out</Bullet>
|
|
304
|
+
<Bullet color={CAT.mauve}><strong>Replay</strong> — step through your project's history</Bullet>
|
|
305
|
+
<Bullet color={CAT.mauve}><strong>Comments</strong> — leave notes on tasks (sign in first)</Bullet>
|
|
306
|
+
<Bullet color={CAT.mauve}><strong>Claim tasks</strong> — right-click to mark as yours</Bullet>
|
|
307
|
+
<Bullet color={CAT.mauve}><strong>Minimap</strong> — click to jump, drag edges to resize</Bullet>
|
|
308
|
+
<Bullet color={CAT.mauve}><strong>Auto-fit</strong> — top-left toggle to lock/unlock automatic camera reframing</Bullet>
|
|
309
|
+
<Bullet color={CAT.mauve}><strong>Copy</strong> — clipboard icon copies task with project info</Bullet>
|
|
310
|
+
</ul>
|
|
311
|
+
|
|
312
|
+
<div className="mt-5 pt-4 border-t border-zinc-100 text-xs text-zinc-400">
|
|
313
|
+
Built with{" "}
|
|
314
|
+
<a
|
|
315
|
+
href="https://github.com/GainForest/beads"
|
|
316
|
+
target="_blank"
|
|
317
|
+
rel="noopener noreferrer"
|
|
318
|
+
className="underline underline-offset-2 transition-colors"
|
|
319
|
+
style={{ color: CAT.green }}
|
|
320
|
+
>
|
|
321
|
+
beads
|
|
322
|
+
</a>{" "}
|
|
323
|
+
— open-source issue tracking for AI-native development. Heartbeads is built for the GainForest agentic workflow and open-sourced at{" "}
|
|
324
|
+
<a
|
|
325
|
+
href="https://github.com/daviddao/heartbeads"
|
|
326
|
+
target="_blank"
|
|
327
|
+
rel="noopener noreferrer"
|
|
328
|
+
className="underline underline-offset-2 transition-colors"
|
|
329
|
+
style={{ color: CAT.green }}
|
|
330
|
+
>
|
|
331
|
+
github.com/daviddao/heartbeads
|
|
332
|
+
</a>.
|
|
333
|
+
</div>
|
|
334
|
+
</div>
|
|
335
|
+
);
|
|
336
|
+
}
|
|
@@ -6,7 +6,7 @@ import remarkGfm from "remark-gfm";
|
|
|
6
6
|
import type { GraphNode } from "@/lib/types";
|
|
7
7
|
import { DescriptionModal } from "./DescriptionModal";
|
|
8
8
|
import { HeartIcon } from "@/components/HeartIcon";
|
|
9
|
-
import { formatRelativeTime } from "@/lib/utils";
|
|
9
|
+
import { formatRelativeTime, buildDescriptionCopyText } from "@/lib/utils";
|
|
10
10
|
import {
|
|
11
11
|
STATUS_LABELS,
|
|
12
12
|
STATUS_COLORS,
|
|
@@ -50,6 +50,7 @@ export default function NodeDetail({
|
|
|
50
50
|
const [replyText, setReplyText] = useState("");
|
|
51
51
|
const [isSubmittingReply, setIsSubmittingReply] = useState(false);
|
|
52
52
|
const [descriptionExpanded, setDescriptionExpanded] = useState(false);
|
|
53
|
+
const [descCopied, setDescCopied] = useState(false);
|
|
53
54
|
|
|
54
55
|
const handleStartReply = (comment: BeadsComment) => {
|
|
55
56
|
setReplyingToUri(comment.uri);
|
|
@@ -314,12 +315,36 @@ export default function NodeDetail({
|
|
|
314
315
|
<h4 className="text-xs font-semibold text-zinc-500 uppercase tracking-wider">
|
|
315
316
|
Description
|
|
316
317
|
</h4>
|
|
317
|
-
<
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
318
|
+
<div className="flex items-center gap-2">
|
|
319
|
+
<button
|
|
320
|
+
onClick={() => {
|
|
321
|
+
if (!node.description) return;
|
|
322
|
+
const repoUrl = repoUrls?.[node.prefix];
|
|
323
|
+
navigator.clipboard.writeText(buildDescriptionCopyText(node, repoUrl)).then(() => {
|
|
324
|
+
setDescCopied(true);
|
|
325
|
+
setTimeout(() => setDescCopied(false), 1500);
|
|
326
|
+
});
|
|
327
|
+
}}
|
|
328
|
+
className="text-zinc-400 hover:text-zinc-600 transition-colors"
|
|
329
|
+
title="Copy description"
|
|
330
|
+
>
|
|
331
|
+
{descCopied ? (
|
|
332
|
+
<svg className="w-3.5 h-3.5 text-emerald-500" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
|
|
333
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
|
334
|
+
</svg>
|
|
335
|
+
) : (
|
|
336
|
+
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24" strokeWidth={2}>
|
|
337
|
+
<path strokeLinecap="round" strokeLinejoin="round" d="M15.666 3.888A2.25 2.25 0 0 0 13.5 2.25h-3c-1.03 0-1.9.693-2.166 1.638m7.332 0c.055.194.084.4.084.612v0a.75.75 0 0 1-.75.75H9.75a.75.75 0 0 1-.75-.75v0c0-.212.03-.418.084-.612m7.332 0c.646.049 1.288.11 1.927.184 1.1.128 1.907 1.077 1.907 2.185V19.5a2.25 2.25 0 0 1-2.25 2.25H6.75A2.25 2.25 0 0 1 4.5 19.5V6.257c0-1.108.806-2.057 1.907-2.185a48.208 48.208 0 0 1 1.927-.184" />
|
|
338
|
+
</svg>
|
|
339
|
+
)}
|
|
340
|
+
</button>
|
|
341
|
+
<button
|
|
342
|
+
onClick={() => setDescriptionExpanded(true)}
|
|
343
|
+
className="text-[10px] text-zinc-400 hover:text-zinc-600 transition-colors"
|
|
344
|
+
>
|
|
345
|
+
View in window
|
|
346
|
+
</button>
|
|
347
|
+
</div>
|
|
323
348
|
</div>
|
|
324
349
|
<div className="text-xs text-zinc-600 leading-relaxed bg-zinc-50 rounded-lg p-3 max-h-40 overflow-y-auto custom-scrollbar border border-zinc-100 description-markdown">
|
|
325
350
|
<ReactMarkdown remarkPlugins={[remarkGfm]}>
|
|
@@ -331,7 +356,7 @@ export default function NodeDetail({
|
|
|
331
356
|
|
|
332
357
|
{/* Description expanded modal */}
|
|
333
358
|
{descriptionExpanded && node.description && (
|
|
334
|
-
<DescriptionModal node={node} onClose={() => setDescriptionExpanded(false)} />
|
|
359
|
+
<DescriptionModal node={node} onClose={() => setDescriptionExpanded(false)} repoUrl={repoUrls?.[node.prefix]} />
|
|
335
360
|
)}
|
|
336
361
|
|
|
337
362
|
{/* Blocks (issues this blocks) */}
|