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
package/lib/types.ts
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Core data types matching .beads/issues.jsonl schema
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
export interface BeadDependency {
|
|
6
|
+
issue_id: string;
|
|
7
|
+
depends_on_id: string;
|
|
8
|
+
type: "blocks" | "parent-child" | "relates_to";
|
|
9
|
+
created_at: string;
|
|
10
|
+
created_by?: string;
|
|
11
|
+
metadata?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface BeadIssue {
|
|
15
|
+
id: string;
|
|
16
|
+
title: string;
|
|
17
|
+
description?: string;
|
|
18
|
+
status: "open" | "in_progress" | "blocked" | "deferred" | "closed";
|
|
19
|
+
priority: number; // 0 (critical) to 4 (backlog)
|
|
20
|
+
issue_type: "task" | "bug" | "feature" | "chore" | "epic";
|
|
21
|
+
owner?: string;
|
|
22
|
+
assignee?: string;
|
|
23
|
+
labels?: string[];
|
|
24
|
+
created_at: string;
|
|
25
|
+
created_by?: string;
|
|
26
|
+
updated_at: string;
|
|
27
|
+
closed_at?: string;
|
|
28
|
+
close_reason?: string;
|
|
29
|
+
dependencies?: BeadDependency[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// ============================================================================
|
|
33
|
+
// Graph data types for visualization
|
|
34
|
+
// ============================================================================
|
|
35
|
+
|
|
36
|
+
export interface GraphNode {
|
|
37
|
+
[key: string]: unknown; // index signature for react-force-graph compatibility
|
|
38
|
+
id: string;
|
|
39
|
+
title: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
status: BeadIssue["status"];
|
|
42
|
+
priority: number;
|
|
43
|
+
issueType: BeadIssue["issue_type"];
|
|
44
|
+
owner?: string;
|
|
45
|
+
assignee?: string;
|
|
46
|
+
createdBy?: string;
|
|
47
|
+
createdAt: string;
|
|
48
|
+
updatedAt: string;
|
|
49
|
+
closedAt?: string;
|
|
50
|
+
closeReason?: string;
|
|
51
|
+
prefix: string; // e.g. "my-app", "backend", "frontend"
|
|
52
|
+
|
|
53
|
+
// Computed
|
|
54
|
+
blockerCount: number; // issues this blocks
|
|
55
|
+
dependentCount: number; // issues that depend on this
|
|
56
|
+
blockerIds: string[]; // IDs of issues this blocks
|
|
57
|
+
dependentIds: string[]; // IDs of issues blocking this
|
|
58
|
+
|
|
59
|
+
// Force-graph position (set by simulation)
|
|
60
|
+
x?: number;
|
|
61
|
+
y?: number;
|
|
62
|
+
fx?: number;
|
|
63
|
+
fy?: number;
|
|
64
|
+
|
|
65
|
+
// Animation metadata (set by live-update merge logic, consumed by paintNode)
|
|
66
|
+
_spawnTime?: number; // Date.now() when this node first appeared (for pop-in animation)
|
|
67
|
+
_removeTime?: number; // Date.now() when this node was marked for removal (for shrink-out)
|
|
68
|
+
_changedAt?: number; // Date.now() when status/priority changed (for ripple animation)
|
|
69
|
+
_prevStatus?: string; // Previous status value before the change (for color transition)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// Color mode for legend/node fill switching
|
|
74
|
+
// ============================================================================
|
|
75
|
+
|
|
76
|
+
export type ColorMode = "status" | "priority" | "owner" | "assignee" | "prefix";
|
|
77
|
+
|
|
78
|
+
export const COLOR_MODE_LABELS: Record<ColorMode, string> = {
|
|
79
|
+
status: "Status",
|
|
80
|
+
priority: "Priority",
|
|
81
|
+
owner: "Owner",
|
|
82
|
+
assignee: "Assignee",
|
|
83
|
+
prefix: "Prefix",
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export interface GraphLink {
|
|
87
|
+
source: string;
|
|
88
|
+
target: string;
|
|
89
|
+
type: "blocks" | "parent-child" | "relates_to";
|
|
90
|
+
createdAt?: string; // ISO 8601 from BeadDependency.created_at (for timeline replay)
|
|
91
|
+
|
|
92
|
+
// Animation metadata (set by live-update merge logic, consumed by paintLink)
|
|
93
|
+
_spawnTime?: number; // Date.now() when this link first appeared (for fade-in animation)
|
|
94
|
+
_removeTime?: number; // Date.now() when this link was marked for removal (for fade-out)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface GraphData {
|
|
98
|
+
nodes: GraphNode[];
|
|
99
|
+
links: GraphLink[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface BeadsApiResponse {
|
|
103
|
+
issues: BeadIssue[];
|
|
104
|
+
dependencies: BeadDependency[];
|
|
105
|
+
graphData: GraphData;
|
|
106
|
+
stats: {
|
|
107
|
+
total: number;
|
|
108
|
+
open: number;
|
|
109
|
+
inProgress: number;
|
|
110
|
+
blocked: number;
|
|
111
|
+
closed: number;
|
|
112
|
+
actionable: number;
|
|
113
|
+
edges: number;
|
|
114
|
+
prefixes: string[];
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ============================================================================
|
|
119
|
+
// Status and priority display helpers
|
|
120
|
+
// ============================================================================
|
|
121
|
+
|
|
122
|
+
export const STATUS_COLORS: Record<string, string> = {
|
|
123
|
+
open: "#10b981", // emerald-500
|
|
124
|
+
in_progress: "#f59e0b", // amber-500
|
|
125
|
+
blocked: "#ef4444", // red-500
|
|
126
|
+
deferred: "#8b5cf6", // violet-500
|
|
127
|
+
closed: "#a1a1aa", // zinc-400
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export const STATUS_LABELS: Record<string, string> = {
|
|
131
|
+
open: "Open",
|
|
132
|
+
in_progress: "In Progress",
|
|
133
|
+
blocked: "Blocked",
|
|
134
|
+
deferred: "Deferred",
|
|
135
|
+
closed: "Closed",
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export const PRIORITY_LABELS: Record<number, string> = {
|
|
139
|
+
0: "P0 - Critical",
|
|
140
|
+
1: "P1 - High",
|
|
141
|
+
2: "P2 - Medium",
|
|
142
|
+
3: "P3 - Low",
|
|
143
|
+
4: "P4 - Backlog",
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export const PRIORITY_COLORS: Record<number, string> = {
|
|
147
|
+
0: "#ef4444", // red
|
|
148
|
+
1: "#f97316", // orange
|
|
149
|
+
2: "#3b82f6", // blue
|
|
150
|
+
3: "#a1a1aa", // zinc
|
|
151
|
+
4: "#d4d4d8", // zinc-300
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// ============================================================================
|
|
155
|
+
// Catppuccin Latte accent palette
|
|
156
|
+
// ============================================================================
|
|
157
|
+
|
|
158
|
+
/** Catppuccin Latte accent colors — 14 visually distinct, reordered for max contrast between adjacent indices */
|
|
159
|
+
export const CATPPUCCIN_ACCENTS: string[] = [
|
|
160
|
+
"#d20f39", // Red
|
|
161
|
+
"#179299", // Teal
|
|
162
|
+
"#fe640b", // Peach
|
|
163
|
+
"#1e66f5", // Blue
|
|
164
|
+
"#40a02b", // Green
|
|
165
|
+
"#8839ef", // Mauve
|
|
166
|
+
"#df8e1d", // Yellow
|
|
167
|
+
"#209fb5", // Sapphire
|
|
168
|
+
"#ea76cb", // Pink
|
|
169
|
+
"#04a5e5", // Sky
|
|
170
|
+
"#e64553", // Maroon
|
|
171
|
+
"#7287fd", // Lavender
|
|
172
|
+
"#dd7878", // Flamingo
|
|
173
|
+
"#dc8a78", // Rosewater
|
|
174
|
+
];
|
|
175
|
+
|
|
176
|
+
export const CATPPUCCIN_ACCENT_NAMES: string[] = [
|
|
177
|
+
"Red", "Teal", "Peach", "Blue", "Green", "Mauve", "Yellow",
|
|
178
|
+
"Sapphire", "Pink", "Sky", "Maroon", "Lavender", "Flamingo", "Rosewater",
|
|
179
|
+
];
|
|
180
|
+
|
|
181
|
+
/** Catppuccin Latte Surface2 — used for "unassigned" / unknown values */
|
|
182
|
+
export const CATPPUCCIN_UNASSIGNED = "#acb0be";
|
|
183
|
+
|
|
184
|
+
export const TYPE_ICONS: Record<string, string> = {
|
|
185
|
+
bug: "\uD83D\uDC1B",
|
|
186
|
+
feature: "\u2728",
|
|
187
|
+
task: "\uD83D\uDCDD",
|
|
188
|
+
epic: "\uD83C\uDFAF",
|
|
189
|
+
chore: "\uD83D\uDD27",
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// ============================================================================
|
|
193
|
+
// Dynamic prefix label and color generation
|
|
194
|
+
// ============================================================================
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Generate a deterministic color from a string using a hash-based hue.
|
|
198
|
+
* Uses FNV-1a for better distribution across the hue wheel.
|
|
199
|
+
*/
|
|
200
|
+
function hashColor(str: string): string {
|
|
201
|
+
let hash = 2166136261; // FNV offset basis
|
|
202
|
+
for (let i = 0; i < str.length; i++) {
|
|
203
|
+
hash ^= str.charCodeAt(i);
|
|
204
|
+
hash = (hash * 16777619) >>> 0; // FNV prime, keep as uint32
|
|
205
|
+
}
|
|
206
|
+
const hue = hash % 360;
|
|
207
|
+
return `hsl(${hue}, 65%, 55%)`;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Humanize a prefix string into a display label.
|
|
212
|
+
* "my-lib" -> "My Lib", "backend" -> "Backend"
|
|
213
|
+
*/
|
|
214
|
+
function humanizePrefix(prefix: string): string {
|
|
215
|
+
return prefix
|
|
216
|
+
.split(/[-_]/)
|
|
217
|
+
.map((w) => w.charAt(0).toUpperCase() + w.slice(1))
|
|
218
|
+
.join(" ");
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get a display label for a prefix. Returns a human-readable name
|
|
223
|
+
* auto-generated from the prefix string.
|
|
224
|
+
*/
|
|
225
|
+
export function getPrefixLabel(prefix: string): string {
|
|
226
|
+
return humanizePrefix(prefix);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Get a color for a prefix. Generates a deterministic color
|
|
231
|
+
* from the prefix string using hash-based hue.
|
|
232
|
+
*/
|
|
233
|
+
export function getPrefixColor(prefix: string): string {
|
|
234
|
+
return hashColor(prefix);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Deterministically map a person handle/name to a Catppuccin Latte accent color.
|
|
239
|
+
* Uses FNV-1a hash modulo 14 to index into CATPPUCCIN_ACCENTS.
|
|
240
|
+
* Returns CATPPUCCIN_UNASSIGNED for undefined/null/empty strings.
|
|
241
|
+
*/
|
|
242
|
+
export function getPersonColor(person: string | undefined): string {
|
|
243
|
+
if (!person) return CATPPUCCIN_UNASSIGNED;
|
|
244
|
+
let hash = 2166136261; // FNV offset basis
|
|
245
|
+
for (let i = 0; i < person.length; i++) {
|
|
246
|
+
hash ^= person.charCodeAt(i);
|
|
247
|
+
hash = (hash * 16777619) >>> 0;
|
|
248
|
+
}
|
|
249
|
+
return CATPPUCCIN_ACCENTS[hash % CATPPUCCIN_ACCENTS.length];
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Deterministically map a prefix string to a Catppuccin Latte accent color.
|
|
254
|
+
* Same approach as getPersonColor but for project prefixes.
|
|
255
|
+
*/
|
|
256
|
+
export function getCatppuccinPrefixColor(prefix: string): string {
|
|
257
|
+
return getPersonColor(prefix);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Backward-compatible exports — delegates to dynamic functions
|
|
261
|
+
// so existing code using PREFIX_COLORS[prefix] || fallback still works.
|
|
262
|
+
// Prefer using getPrefixColor() / getPrefixLabel() directly.
|
|
263
|
+
export const PREFIX_LABELS = new Proxy({} as Record<string, string>, {
|
|
264
|
+
get: (_target, prop: string) => getPrefixLabel(prop),
|
|
265
|
+
has: () => true,
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
export const PREFIX_COLORS = new Proxy({} as Record<string, string>, {
|
|
269
|
+
get: (_target, prop: string) => getPrefixColor(prop),
|
|
270
|
+
has: () => true,
|
|
271
|
+
});
|
package/lib/utils.ts
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for heartbeads.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { getPrefixLabel } from "@/lib/types";
|
|
6
|
+
import type { GraphNode } from "@/lib/types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Formats an ISO date string as a human-readable relative time.
|
|
10
|
+
* - < 60s: "just now"
|
|
11
|
+
* - < 60m: "Xm ago"
|
|
12
|
+
* - < 24h: "Xh ago"
|
|
13
|
+
* - < 7d: "Xd ago"
|
|
14
|
+
* - else: "Mon DD" (e.g. "Feb 10")
|
|
15
|
+
*/
|
|
16
|
+
export function formatRelativeTime(isoString: string): string {
|
|
17
|
+
const date = new Date(isoString);
|
|
18
|
+
const now = new Date();
|
|
19
|
+
const seconds = Math.floor((now.getTime() - date.getTime()) / 1000);
|
|
20
|
+
if (seconds < 60) return "just now";
|
|
21
|
+
const minutes = Math.floor(seconds / 60);
|
|
22
|
+
if (minutes < 60) return `${minutes}m ago`;
|
|
23
|
+
const hours = Math.floor(minutes / 60);
|
|
24
|
+
if (hours < 24) return `${hours}h ago`;
|
|
25
|
+
const days = Math.floor(hours / 24);
|
|
26
|
+
if (days < 7) return `${days}d ago`;
|
|
27
|
+
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build a copy-pasteable text for a node's description with metadata header.
|
|
32
|
+
* Format:
|
|
33
|
+
* [Project Name] issue-id
|
|
34
|
+
* https://github.com/org/repo (if available)
|
|
35
|
+
*
|
|
36
|
+
* <description>
|
|
37
|
+
*/
|
|
38
|
+
export function buildDescriptionCopyText(
|
|
39
|
+
node: GraphNode,
|
|
40
|
+
repoUrl?: string,
|
|
41
|
+
): string {
|
|
42
|
+
const lines: string[] = [];
|
|
43
|
+
lines.push(`[${getPrefixLabel(node.prefix)}] ${node.id}`);
|
|
44
|
+
if (repoUrl) lines.push(repoUrl);
|
|
45
|
+
lines.push("");
|
|
46
|
+
lines.push(node.description || "");
|
|
47
|
+
return lines.join("\n");
|
|
48
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { watch, existsSync } from "fs";
|
|
2
|
+
import { join, dirname, basename } from "path";
|
|
3
|
+
import { getAdditionalRepoPaths } from "./parse-beads";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Get all issues.jsonl file paths that should be watched.
|
|
7
|
+
* Returns the primary path plus any additional repo paths from config.yaml.
|
|
8
|
+
*/
|
|
9
|
+
export function getWatchPaths(beadsDir: string): string[] {
|
|
10
|
+
const paths: string[] = [];
|
|
11
|
+
|
|
12
|
+
// Primary JSONL
|
|
13
|
+
const primary = join(beadsDir, "issues.jsonl");
|
|
14
|
+
if (existsSync(primary)) paths.push(primary);
|
|
15
|
+
|
|
16
|
+
// Additional repo JSONLs
|
|
17
|
+
const additionalRepos = getAdditionalRepoPaths(beadsDir);
|
|
18
|
+
for (const repoPath of additionalRepos) {
|
|
19
|
+
const jsonlPath = join(repoPath, ".beads", "issues.jsonl");
|
|
20
|
+
if (existsSync(jsonlPath)) paths.push(jsonlPath);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return paths;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Watch all issues.jsonl files for a beads project.
|
|
28
|
+
* Discovers files from the primary .beads dir and config.yaml repos.additional.
|
|
29
|
+
* Debounces rapid changes (bd often writes multiple times per command).
|
|
30
|
+
*
|
|
31
|
+
* @param beadsDir - Absolute path to the primary .beads/ directory
|
|
32
|
+
* @param onChange - Callback fired when any watched file changes (after debounce)
|
|
33
|
+
* @param debounceMs - Debounce interval in milliseconds (default: 300)
|
|
34
|
+
* @returns Cleanup function that closes all watchers
|
|
35
|
+
*/
|
|
36
|
+
export function watchBeadsFiles(
|
|
37
|
+
beadsDir: string,
|
|
38
|
+
onChange: () => void,
|
|
39
|
+
debounceMs = 300
|
|
40
|
+
): () => void {
|
|
41
|
+
const filePaths = getWatchPaths(beadsDir);
|
|
42
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
43
|
+
const watchers: ReturnType<typeof watch>[] = [];
|
|
44
|
+
|
|
45
|
+
// Deduplicate directories — multiple files may share the same .beads/ dir
|
|
46
|
+
const dirToFiles = new Map<string, Set<string>>();
|
|
47
|
+
for (const filePath of filePaths) {
|
|
48
|
+
const dir = dirname(filePath);
|
|
49
|
+
const file = basename(filePath);
|
|
50
|
+
if (!dirToFiles.has(dir)) dirToFiles.set(dir, new Set());
|
|
51
|
+
dirToFiles.get(dir)!.add(file);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
console.log(
|
|
55
|
+
`[heartbeads] Watching ${filePaths.length} file(s) in ${dirToFiles.size} dir(s) for changes`
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const debouncedOnChange = () => {
|
|
59
|
+
if (timer) clearTimeout(timer);
|
|
60
|
+
timer = setTimeout(() => {
|
|
61
|
+
console.log("[heartbeads] File change detected, pushing update");
|
|
62
|
+
onChange();
|
|
63
|
+
}, debounceMs);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// Watch directories instead of individual files.
|
|
67
|
+
// This is far more reliable on macOS: fs.watch on a file breaks when the
|
|
68
|
+
// file is atomically replaced (write-tmp + rename), which is how bd and
|
|
69
|
+
// many editors write. Watching the directory catches renames reliably.
|
|
70
|
+
for (const [dir, fileNames] of dirToFiles) {
|
|
71
|
+
try {
|
|
72
|
+
const watcher = watch(dir, { persistent: true }, (_eventType, filename) => {
|
|
73
|
+
// Filter: only react to changes to our target files
|
|
74
|
+
if (filename && fileNames.has(filename)) {
|
|
75
|
+
debouncedOnChange();
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
watchers.push(watcher);
|
|
79
|
+
console.log(`[heartbeads] Watching dir: ${dir} for [${[...fileNames].join(", ")}]`);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
console.warn(`[heartbeads] Failed to watch ${dir}:`, err);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (filePaths.length === 0) {
|
|
86
|
+
console.warn("[heartbeads] No issues.jsonl files found to watch");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Return cleanup function
|
|
90
|
+
return () => {
|
|
91
|
+
if (timer) clearTimeout(timer);
|
|
92
|
+
for (const w of watchers) {
|
|
93
|
+
w.close();
|
|
94
|
+
}
|
|
95
|
+
console.log("[heartbeads] File watchers closed");
|
|
96
|
+
};
|
|
97
|
+
}
|
package/next.config.mjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "heartbeads",
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "Interactive dependency graph viewer for beads (bd) issues",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"beads",
|
|
7
|
+
"bd",
|
|
8
|
+
"issue-tracking",
|
|
9
|
+
"dependency-graph",
|
|
10
|
+
"visualization"
|
|
11
|
+
],
|
|
12
|
+
"repository": {
|
|
13
|
+
"type": "git",
|
|
14
|
+
"url": "https://github.com/GainForest/heartbeads.git"
|
|
15
|
+
},
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"bin": {
|
|
18
|
+
"heartbeads": "./bin/heartbeads.mjs"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"app",
|
|
22
|
+
"components",
|
|
23
|
+
"hooks",
|
|
24
|
+
"lib",
|
|
25
|
+
"bin",
|
|
26
|
+
"scripts",
|
|
27
|
+
"public",
|
|
28
|
+
".next/server",
|
|
29
|
+
".next/static",
|
|
30
|
+
".next/BUILD_ID",
|
|
31
|
+
".next/build-manifest.json",
|
|
32
|
+
".next/app-build-manifest.json",
|
|
33
|
+
".next/app-path-routes-manifest.json",
|
|
34
|
+
".next/export-marker.json",
|
|
35
|
+
".next/images-manifest.json",
|
|
36
|
+
".next/package.json",
|
|
37
|
+
".next/prerender-manifest.json",
|
|
38
|
+
".next/react-loadable-manifest.json",
|
|
39
|
+
".next/required-server-files.json",
|
|
40
|
+
".next/routes-manifest.json",
|
|
41
|
+
".next/next-minimal-server.js.nft.json",
|
|
42
|
+
".next/next-server.js.nft.json",
|
|
43
|
+
"next.config.mjs",
|
|
44
|
+
"tailwind.config.ts",
|
|
45
|
+
"tsconfig.json",
|
|
46
|
+
"postcss.config.mjs",
|
|
47
|
+
"package.json"
|
|
48
|
+
],
|
|
49
|
+
"scripts": {
|
|
50
|
+
"dev": "next dev",
|
|
51
|
+
"build": "next build",
|
|
52
|
+
"start": "next start",
|
|
53
|
+
"lint": "next lint",
|
|
54
|
+
"prepublishOnly": "next build"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@atproto/api": "^0.18.20",
|
|
58
|
+
"@atproto/jwk-jose": "^0.1.11",
|
|
59
|
+
"@atproto/oauth-client-node": "^0.3.16",
|
|
60
|
+
"@atproto/syntax": "^0.4.3",
|
|
61
|
+
"@types/d3-force": "^3.0.10",
|
|
62
|
+
"d3-force": "^3.0.0",
|
|
63
|
+
"iron-session": "^8.0.4",
|
|
64
|
+
"next": "^14.2.21",
|
|
65
|
+
"react": "^18.3.1",
|
|
66
|
+
"react-dom": "^18.3.1",
|
|
67
|
+
"react-force-graph-2d": "^1.25.7",
|
|
68
|
+
"react-markdown": "^10.1.0",
|
|
69
|
+
"remark-gfm": "^4.0.1",
|
|
70
|
+
"yaml": "^2.8.2"
|
|
71
|
+
},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@types/node": "^20.17.0",
|
|
74
|
+
"@types/react": "^18.3.12",
|
|
75
|
+
"@types/react-dom": "^18.3.1",
|
|
76
|
+
"autoprefixer": "^10.4.20",
|
|
77
|
+
"postcss": "^8.4.49",
|
|
78
|
+
"tailwindcss": "^3.4.16",
|
|
79
|
+
"typescript": "^5.7.2"
|
|
80
|
+
}
|
|
81
|
+
}
|
package/public/image.png
ADDED
|
Binary file
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Generate an ES256 JWK private key for ATProto OAuth confidential client authentication.
|
|
5
|
+
*
|
|
6
|
+
* Usage: node scripts/generate-jwk.js
|
|
7
|
+
*
|
|
8
|
+
* Copy the output and add it to your .env file as ATPROTO_JWK_PRIVATE
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const crypto = require('crypto')
|
|
12
|
+
|
|
13
|
+
async function generateJWK() {
|
|
14
|
+
// Generate an EC key pair using P-256 curve (required for ES256)
|
|
15
|
+
const { privateKey } = await crypto.subtle.generateKey(
|
|
16
|
+
{
|
|
17
|
+
name: 'ECDSA',
|
|
18
|
+
namedCurve: 'P-256',
|
|
19
|
+
},
|
|
20
|
+
true, // extractable
|
|
21
|
+
['sign', 'verify']
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
// Export as JWK
|
|
25
|
+
const jwk = await crypto.subtle.exportKey('jwk', privateKey)
|
|
26
|
+
|
|
27
|
+
// Add key ID and algorithm
|
|
28
|
+
jwk.kid = `key-${Date.now()}`
|
|
29
|
+
jwk.alg = 'ES256'
|
|
30
|
+
jwk.use = 'sig'
|
|
31
|
+
|
|
32
|
+
console.log('\n=== ES256 Private Key (JWK) ===\n')
|
|
33
|
+
console.log('Add this to your .env file as ATPROTO_JWK_PRIVATE:\n')
|
|
34
|
+
console.log(JSON.stringify(jwk))
|
|
35
|
+
console.log('\n')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
generateJWK().catch(console.error)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { Config } from "tailwindcss";
|
|
2
|
+
|
|
3
|
+
const config: Config = {
|
|
4
|
+
content: [
|
|
5
|
+
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
|
6
|
+
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
|
7
|
+
],
|
|
8
|
+
theme: {
|
|
9
|
+
extend: {
|
|
10
|
+
fontFamily: {
|
|
11
|
+
sans: [
|
|
12
|
+
"Inter",
|
|
13
|
+
"system-ui",
|
|
14
|
+
"-apple-system",
|
|
15
|
+
"BlinkMacSystemFont",
|
|
16
|
+
"Segoe UI",
|
|
17
|
+
"Roboto",
|
|
18
|
+
"sans-serif",
|
|
19
|
+
],
|
|
20
|
+
mono: ["JetBrains Mono", "Fira Code", "monospace"],
|
|
21
|
+
},
|
|
22
|
+
colors: {
|
|
23
|
+
beads: {
|
|
24
|
+
50: "#ecfdf5",
|
|
25
|
+
100: "#d1fae5",
|
|
26
|
+
200: "#a7f3d0",
|
|
27
|
+
300: "#6ee7b7",
|
|
28
|
+
400: "#34d399",
|
|
29
|
+
500: "#10b981",
|
|
30
|
+
600: "#059669",
|
|
31
|
+
700: "#047857",
|
|
32
|
+
800: "#065f46",
|
|
33
|
+
900: "#064e3b",
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
plugins: [],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export default config;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
4
|
+
"allowJs": true,
|
|
5
|
+
"skipLibCheck": true,
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"target": "es2017",
|
|
9
|
+
"downlevelIteration": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"module": "esnext",
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"resolveJsonModule": true,
|
|
14
|
+
"isolatedModules": true,
|
|
15
|
+
"jsx": "preserve",
|
|
16
|
+
"incremental": true,
|
|
17
|
+
"plugins": [{ "name": "next" }],
|
|
18
|
+
"paths": {
|
|
19
|
+
"@/*": ["./*"]
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
23
|
+
"exclude": ["node_modules"]
|
|
24
|
+
}
|