beads-map 0.3.0 → 0.3.3
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 +5 -5
- 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-4a4f07fcb5bd4637.js +1 -0
- package/.next/static/css/df2737696baac0fa.css +3 -0
- package/README.md +21 -11
- package/app/page.tsx +113 -7
- package/components/BeadTooltip.tsx +26 -2
- package/components/BeadsGraph.tsx +460 -234
- 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 +67 -0
- package/lib/utils.ts +23 -0
- package/package.json +1 -1
- package/.next/static/chunks/app/page-68492e6aaf15a6dd.js +0 -1
- package/.next/static/css/c854bc2280bc4b27.css +0 -3
- /package/.next/static/{ac0cLw5kGBDWoceTBnu21 → bsmkR-2y8Ra7VuoNZWLzB}/_buildManifest.js +0 -0
- /package/.next/static/{ac0cLw5kGBDWoceTBnu21 → bsmkR-2y8Ra7VuoNZWLzB}/_ssgManifest.js +0 -0
|
@@ -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) */}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState, useCallback } from "react";
|
|
4
|
+
|
|
5
|
+
export interface TutorialStep {
|
|
6
|
+
target: string;
|
|
7
|
+
title: string;
|
|
8
|
+
description: string;
|
|
9
|
+
padding?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const TUTORIAL_STEPS: TutorialStep[] = [
|
|
13
|
+
{
|
|
14
|
+
target: "graph",
|
|
15
|
+
title: "The Dependency Graph",
|
|
16
|
+
description:
|
|
17
|
+
"Each circle is a task or issue. The flowing particles between them show the direction of dependencies \u2014 what needs to happen before something else can start. Bigger circles mean more connections.",
|
|
18
|
+
padding: 0,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
target: "layouts",
|
|
22
|
+
title: "Layout Modes",
|
|
23
|
+
description:
|
|
24
|
+
"Switch how the graph is arranged. Force is organic and physics-based. DAG gives you a clean top-down tree. Radial spreads nodes in rings. Cluster groups by project. Spread spaces everything out for screenshots.",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
target: "view-controls",
|
|
28
|
+
title: "View Controls",
|
|
29
|
+
description:
|
|
30
|
+
"Collapse or expand epic groups, toggle cluster label overlays, and control auto-fit. When auto-fit is on (green), the camera re-centers after every update. Turn it off to stay focused on a specific area while data streams in.",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
target: "legend",
|
|
34
|
+
title: "Color Modes & Legend",
|
|
35
|
+
description:
|
|
36
|
+
"Color nodes by Status, Priority, Owner, Assignee, or Prefix. The ring around each node always shows which project it belongs to.",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
target: "minimap",
|
|
40
|
+
title: "Minimap",
|
|
41
|
+
description:
|
|
42
|
+
"A bird\u2019s-eye view of your entire graph. Click anywhere to jump there. Drag the edges to resize it.",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
target: "search",
|
|
46
|
+
title: "Search",
|
|
47
|
+
description:
|
|
48
|
+
"Press Cmd+F (or Ctrl+F) to search by name, ID, owner, assignee, or commenter. Arrow keys to navigate, Enter to focus.",
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
target: "graph",
|
|
52
|
+
title: "Interacting with Nodes",
|
|
53
|
+
description:
|
|
54
|
+
"Click a node to see details. Hover for a quick tooltip. Right-click for actions like viewing descriptions, commenting, claiming tasks, or collapsing epics.",
|
|
55
|
+
padding: 0,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
target: "nav-pills",
|
|
59
|
+
title: "Navigation Bar",
|
|
60
|
+
description:
|
|
61
|
+
"Replay steps through your project\u2019s history. Comments shows conversations. Activity is a real-time feed. And Learn brings you right back here.",
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
interface TutorialOverlayProps {
|
|
66
|
+
step: number | null;
|
|
67
|
+
onNext: () => void;
|
|
68
|
+
onPrev: () => void;
|
|
69
|
+
onEnd: () => void;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface Rect {
|
|
73
|
+
left: number;
|
|
74
|
+
top: number;
|
|
75
|
+
width: number;
|
|
76
|
+
height: number;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function TutorialOverlay({
|
|
80
|
+
step,
|
|
81
|
+
onNext,
|
|
82
|
+
onEnd,
|
|
83
|
+
}: TutorialOverlayProps) {
|
|
84
|
+
const [targetRect, setTargetRect] = useState<Rect | null>(null);
|
|
85
|
+
|
|
86
|
+
const updateRect = useCallback(() => {
|
|
87
|
+
if (step === null) {
|
|
88
|
+
setTargetRect(null);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
const currentStep = TUTORIAL_STEPS[step];
|
|
92
|
+
if (!currentStep) {
|
|
93
|
+
setTargetRect(null);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const el = document.querySelector(
|
|
97
|
+
`[data-tutorial="${currentStep.target}"]`
|
|
98
|
+
);
|
|
99
|
+
if (!el) {
|
|
100
|
+
setTargetRect(null);
|
|
101
|
+
} else {
|
|
102
|
+
const r = el.getBoundingClientRect();
|
|
103
|
+
const padding = currentStep.padding ?? 8;
|
|
104
|
+
setTargetRect({
|
|
105
|
+
left: r.left - padding,
|
|
106
|
+
top: r.top - padding,
|
|
107
|
+
width: r.width + padding * 2,
|
|
108
|
+
height: r.height + padding * 2,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}, [step]);
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
updateRect();
|
|
115
|
+
const handleResize = () => updateRect();
|
|
116
|
+
window.addEventListener("resize", handleResize);
|
|
117
|
+
const timer = setTimeout(updateRect, 150);
|
|
118
|
+
return () => {
|
|
119
|
+
window.removeEventListener("resize", handleResize);
|
|
120
|
+
clearTimeout(timer);
|
|
121
|
+
};
|
|
122
|
+
}, [updateRect]);
|
|
123
|
+
|
|
124
|
+
if (step === null) return null;
|
|
125
|
+
|
|
126
|
+
const isLast = step === TUTORIAL_STEPS.length - 1;
|
|
127
|
+
|
|
128
|
+
const handleClick = () => {
|
|
129
|
+
if (isLast) {
|
|
130
|
+
onEnd();
|
|
131
|
+
} else {
|
|
132
|
+
onNext();
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
// The sidebar is z-[60] during tutorial, so it naturally sits above this z-[55] overlay.
|
|
137
|
+
// Clicking the dark area advances/ends the tutorial. The spotlight cutout is visual only.
|
|
138
|
+
return (
|
|
139
|
+
<>
|
|
140
|
+
{/* Full-screen clickable dark overlay */}
|
|
141
|
+
<div
|
|
142
|
+
className="fixed inset-0 z-[55] cursor-pointer"
|
|
143
|
+
onClick={handleClick}
|
|
144
|
+
>
|
|
145
|
+
{/* SVG with mask to punch a transparent hole for the spotlight */}
|
|
146
|
+
<svg className="absolute inset-0 w-full h-full" xmlns="http://www.w3.org/2000/svg">
|
|
147
|
+
<defs>
|
|
148
|
+
<mask id="tutorial-mask">
|
|
149
|
+
<rect width="100%" height="100%" fill="white" />
|
|
150
|
+
{targetRect && (
|
|
151
|
+
<rect
|
|
152
|
+
x={targetRect.left}
|
|
153
|
+
y={targetRect.top}
|
|
154
|
+
width={targetRect.width}
|
|
155
|
+
height={targetRect.height}
|
|
156
|
+
rx={8}
|
|
157
|
+
ry={8}
|
|
158
|
+
fill="black"
|
|
159
|
+
/>
|
|
160
|
+
)}
|
|
161
|
+
</mask>
|
|
162
|
+
</defs>
|
|
163
|
+
<rect
|
|
164
|
+
width="100%"
|
|
165
|
+
height="100%"
|
|
166
|
+
fill="rgba(0,0,0,0.45)"
|
|
167
|
+
mask="url(#tutorial-mask)"
|
|
168
|
+
/>
|
|
169
|
+
</svg>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
{/* Pulsing ring around spotlight target */}
|
|
173
|
+
{targetRect && (
|
|
174
|
+
<div
|
|
175
|
+
className="fixed rounded-lg ring-2 ring-emerald-400/60 animate-pulse z-[56]"
|
|
176
|
+
style={{
|
|
177
|
+
left: targetRect.left,
|
|
178
|
+
top: targetRect.top,
|
|
179
|
+
width: targetRect.width,
|
|
180
|
+
height: targetRect.height,
|
|
181
|
+
pointerEvents: "none",
|
|
182
|
+
}}
|
|
183
|
+
/>
|
|
184
|
+
)}
|
|
185
|
+
</>
|
|
186
|
+
);
|
|
187
|
+
}
|