beads-map 0.3.0 → 0.3.1
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-a0493d6741516b53.js +1 -0
- package/.next/static/css/4fded26534cb91e3.css +3 -0
- package/app/page.tsx +6 -3
- package/components/BeadsGraph.tsx +157 -23
- package/lib/types.ts +66 -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 → _OvcD8YYgVPHv6Tomg-pB}/_buildManifest.js +0 -0
- /package/.next/static/{ac0cLw5kGBDWoceTBnu21 → _OvcD8YYgVPHv6Tomg-pB}/_ssgManifest.js +0 -0
package/app/page.tsx
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState, useCallback, useMemo, useRef } from "react";
|
|
4
|
-
import type { BeadsApiResponse, GraphNode, GraphLink } from "@/lib/types";
|
|
5
|
-
import {
|
|
4
|
+
import type { BeadsApiResponse, GraphNode, GraphLink, ColorMode } from "@/lib/types";
|
|
5
|
+
import { getCatppuccinPrefixColor } from "@/lib/types";
|
|
6
6
|
import { diffBeadsData, linkKey } from "@/lib/diff-beads";
|
|
7
7
|
import type { BeadsDiff } from "@/lib/diff-beads";
|
|
8
8
|
import BeadsGraph from "@/components/BeadsGraph";
|
|
@@ -189,6 +189,7 @@ export default function Home() {
|
|
|
189
189
|
const [selectedNode, setSelectedNode] = useState<GraphNode | null>(null);
|
|
190
190
|
const [hoveredNode, setHoveredNode] = useState<GraphNode | null>(null);
|
|
191
191
|
const [collapsedEpicIds, setCollapsedEpicIds] = useState<Set<string>>(new Set());
|
|
192
|
+
const [colorMode, setColorMode] = useState<ColorMode>("status");
|
|
192
193
|
const [projectName, setProjectName] = useState("Beads");
|
|
193
194
|
const [repoCount, setRepoCount] = useState(0);
|
|
194
195
|
const [repoUrls, setRepoUrls] = useState<Record<string, string>>({});
|
|
@@ -1248,6 +1249,8 @@ export default function Home() {
|
|
|
1248
1249
|
collapsedEpicIds={collapsedEpicIds}
|
|
1249
1250
|
onCollapseAll={handleCollapseAll}
|
|
1250
1251
|
onExpandAll={handleExpandAll}
|
|
1252
|
+
colorMode={colorMode}
|
|
1253
|
+
onColorModeChange={setColorMode}
|
|
1251
1254
|
/>
|
|
1252
1255
|
|
|
1253
1256
|
{/* Timeline bar — replaces legend hint when active */}
|
|
@@ -1382,7 +1385,7 @@ export default function Home() {
|
|
|
1382
1385
|
node={nodeTooltip.node}
|
|
1383
1386
|
x={nodeTooltip.x}
|
|
1384
1387
|
y={nodeTooltip.y}
|
|
1385
|
-
prefixColor={
|
|
1388
|
+
prefixColor={getCatppuccinPrefixColor(nodeTooltip.node.prefix)}
|
|
1386
1389
|
allNodes={timelineActive && timelineData ? timelineData.graphData.nodes : data.graphData.nodes}
|
|
1387
1390
|
/>
|
|
1388
1391
|
)}
|
|
@@ -10,8 +10,11 @@ import React, {
|
|
|
10
10
|
useState,
|
|
11
11
|
} from "react";
|
|
12
12
|
import { forceCollide, forceRadial, forceX, forceY } from "d3-force";
|
|
13
|
-
import type { GraphNode, GraphLink } from "@/lib/types";
|
|
14
|
-
import {
|
|
13
|
+
import type { GraphNode, GraphLink, ColorMode } from "@/lib/types";
|
|
14
|
+
import {
|
|
15
|
+
STATUS_COLORS, STATUS_LABELS,
|
|
16
|
+
COLOR_MODE_LABELS, getPersonColor, getCatppuccinPrefixColor, getPrefixLabel,
|
|
17
|
+
} from "@/lib/types";
|
|
15
18
|
|
|
16
19
|
// Lazy-load ForceGraph2D client-side (it requires window/document).
|
|
17
20
|
// We avoid next/dynamic because it wraps the component in a LoadableComponent
|
|
@@ -45,6 +48,10 @@ interface BeadsGraphProps {
|
|
|
45
48
|
onCollapseAll?: () => void;
|
|
46
49
|
/** Expand all epics at once */
|
|
47
50
|
onExpandAll?: () => void;
|
|
51
|
+
/** Current color mode for node body fill */
|
|
52
|
+
colorMode?: ColorMode;
|
|
53
|
+
/** Callback to change color mode (from legend selector) */
|
|
54
|
+
onColorModeChange?: (mode: ColorMode) => void;
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
// Node size calculation
|
|
@@ -62,14 +69,27 @@ function getNodeSize(node: GraphNode): number {
|
|
|
62
69
|
return MIN_SIZE + normalized * (MAX_SIZE - MIN_SIZE);
|
|
63
70
|
}
|
|
64
71
|
|
|
65
|
-
//
|
|
72
|
+
// Module-level color mode tracker (synced from component via useEffect)
|
|
73
|
+
let _currentColorMode: ColorMode = "status";
|
|
74
|
+
|
|
75
|
+
// Get color based on current color mode
|
|
66
76
|
function getNodeColor(node: GraphNode): string {
|
|
67
|
-
|
|
77
|
+
switch (_currentColorMode) {
|
|
78
|
+
case "owner":
|
|
79
|
+
return getPersonColor(node.createdBy);
|
|
80
|
+
case "assignee":
|
|
81
|
+
return getPersonColor(node.assignee);
|
|
82
|
+
case "prefix":
|
|
83
|
+
return getCatppuccinPrefixColor(node.prefix);
|
|
84
|
+
case "status":
|
|
85
|
+
default:
|
|
86
|
+
return STATUS_COLORS[node.status] || STATUS_COLORS.open;
|
|
87
|
+
}
|
|
68
88
|
}
|
|
69
89
|
|
|
70
|
-
// Get
|
|
71
|
-
function
|
|
72
|
-
return
|
|
90
|
+
// Get prefix color for the outer ring — uses Catppuccin palette for consistency
|
|
91
|
+
function getPrefixRingColor(node: GraphNode): string {
|
|
92
|
+
return getCatppuccinPrefixColor(node.prefix);
|
|
73
93
|
}
|
|
74
94
|
|
|
75
95
|
// Animation duration constants
|
|
@@ -215,6 +235,8 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
215
235
|
collapsedEpicIds,
|
|
216
236
|
onCollapseAll,
|
|
217
237
|
onExpandAll,
|
|
238
|
+
colorMode = "status",
|
|
239
|
+
onColorModeChange,
|
|
218
240
|
}, ref) {
|
|
219
241
|
const graphRef = useRef<any>(null);
|
|
220
242
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
@@ -254,6 +276,9 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
254
276
|
// Layout mode: "force" (physics-based) or "dag" (topological top-down)
|
|
255
277
|
const [layoutMode, setLayoutMode] = useState<LayoutMode>("dag");
|
|
256
278
|
|
|
279
|
+
// Whether to show hierarchical cluster circles/labels when zoomed out
|
|
280
|
+
const [showClusters, setShowClusters] = useState(true);
|
|
281
|
+
|
|
257
282
|
|
|
258
283
|
|
|
259
284
|
|
|
@@ -266,6 +291,9 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
266
291
|
const claimedNodeAvatarsRef = useRef<Map<string, { avatar?: string; handle: string; claimedAt: string; did?: string }>>(
|
|
267
292
|
claimedNodeAvatars || new Map()
|
|
268
293
|
);
|
|
294
|
+
// Color mode ref for paintNode (which has [] deps) to read current color mode
|
|
295
|
+
const colorModeRef = useRef<ColorMode>(colorMode);
|
|
296
|
+
|
|
269
297
|
// Callback ref for refreshing graph when avatar images finish loading
|
|
270
298
|
const avatarRefreshRef = useRef<() => void>(() => {});
|
|
271
299
|
avatarRefreshRef.current = () => refreshGraph(graphRef);
|
|
@@ -433,6 +461,41 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
433
461
|
clustersRef.current = clusters;
|
|
434
462
|
}, [nodes, links]);
|
|
435
463
|
|
|
464
|
+
// Compute dynamic legend items based on color mode and visible nodes
|
|
465
|
+
const legendItems = useMemo(() => {
|
|
466
|
+
if (colorMode === "status") return []; // handled by static STATUS_COLORS rendering
|
|
467
|
+
|
|
468
|
+
const items = new Map<string, string>(); // label -> color
|
|
469
|
+
|
|
470
|
+
for (const node of viewNodes) {
|
|
471
|
+
switch (colorMode) {
|
|
472
|
+
case "owner": {
|
|
473
|
+
const key = node.createdBy || undefined;
|
|
474
|
+
items.set(key || "Unassigned", getPersonColor(key));
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
case "assignee": {
|
|
478
|
+
const key = node.assignee || undefined;
|
|
479
|
+
items.set(key || "Unassigned", getPersonColor(key));
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
case "prefix": {
|
|
483
|
+
items.set(getPrefixLabel(node.prefix), getCatppuccinPrefixColor(node.prefix));
|
|
484
|
+
break;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Sort: "Unassigned" last, others alphabetically
|
|
490
|
+
return Array.from(items.entries())
|
|
491
|
+
.sort(([a], [b]) => {
|
|
492
|
+
if (a === "Unassigned") return 1;
|
|
493
|
+
if (b === "Unassigned") return -1;
|
|
494
|
+
return a.localeCompare(b);
|
|
495
|
+
})
|
|
496
|
+
.map(([label, color]) => ({ label, color }));
|
|
497
|
+
}, [colorMode, viewNodes]);
|
|
498
|
+
|
|
436
499
|
// Sync props into refs and trigger canvas redraw (not React re-render).
|
|
437
500
|
// Also schedules a minimap redraw so highlight state is synced there too.
|
|
438
501
|
// Uses viewLinks (respects epic collapse) for connected subgraph computation.
|
|
@@ -481,6 +544,14 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
481
544
|
refreshGraph(graphRef);
|
|
482
545
|
}, [claimedNodeAvatars]);
|
|
483
546
|
|
|
547
|
+
// Sync color mode to module-level variable and ref, trigger canvas + minimap redraw
|
|
548
|
+
useEffect(() => {
|
|
549
|
+
colorModeRef.current = colorMode;
|
|
550
|
+
_currentColorMode = colorMode;
|
|
551
|
+
refreshGraph(graphRef);
|
|
552
|
+
minimapRafRef.current = requestAnimationFrame(() => redrawMinimapRef.current());
|
|
553
|
+
}, [colorMode]);
|
|
554
|
+
|
|
484
555
|
// Avatar hover detection: mousemove on container, hit-test against avatar positions
|
|
485
556
|
useEffect(() => {
|
|
486
557
|
const container = containerRef.current;
|
|
@@ -827,7 +898,7 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
827
898
|
const graphNode = node as GraphNode;
|
|
828
899
|
const size = getNodeSize(graphNode);
|
|
829
900
|
const color = getNodeColor(graphNode);
|
|
830
|
-
const prefixColor =
|
|
901
|
+
const prefixColor = getPrefixRingColor(graphNode);
|
|
831
902
|
const isSelected = selectedNodeRef.current?.id === graphNode.id;
|
|
832
903
|
const isHovered = hoveredNodeRef.current?.id === graphNode.id;
|
|
833
904
|
const connected = connectedNodesRef.current;
|
|
@@ -1262,6 +1333,8 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
1262
1333
|
// Appears as nodes fade out (inverse of node fade).
|
|
1263
1334
|
const paintClusterLabels = useCallback(
|
|
1264
1335
|
(ctx: CanvasRenderingContext2D, globalScale: number) => {
|
|
1336
|
+
if (!showClusters) return;
|
|
1337
|
+
|
|
1265
1338
|
// Only show cluster labels when zoomed out (inverse of node fade range)
|
|
1266
1339
|
const LABEL_FADE_IN = 0.8; // starts appearing
|
|
1267
1340
|
const LABEL_FULL = 0.4; // fully visible
|
|
@@ -1321,8 +1394,8 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
1321
1394
|
}
|
|
1322
1395
|
const radius = maxDist + 30; // padding around outermost node
|
|
1323
1396
|
|
|
1324
|
-
// Use
|
|
1325
|
-
const clusterColor =
|
|
1397
|
+
// Use Catppuccin prefix color for the cluster circle (clusters always represent projects)
|
|
1398
|
+
const clusterColor = getCatppuccinPrefixColor(cluster.prefix);
|
|
1326
1399
|
|
|
1327
1400
|
// Draw subtle cluster background circle
|
|
1328
1401
|
ctx.beginPath();
|
|
@@ -1374,7 +1447,7 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
1374
1447
|
|
|
1375
1448
|
ctx.restore();
|
|
1376
1449
|
},
|
|
1377
|
-
[viewNodes, nodes] // reads clustersRef (ref), but needs viewNodes for positions
|
|
1450
|
+
[viewNodes, nodes, showClusters] // reads clustersRef (ref), but needs viewNodes for positions
|
|
1378
1451
|
);
|
|
1379
1452
|
|
|
1380
1453
|
// Node hit area
|
|
@@ -1906,13 +1979,38 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
1906
1979
|
)}
|
|
1907
1980
|
</button>
|
|
1908
1981
|
)}
|
|
1982
|
+
|
|
1983
|
+
{/* Show/hide cluster labels toggle */}
|
|
1984
|
+
<button
|
|
1985
|
+
onClick={() => setShowClusters((v) => !v)}
|
|
1986
|
+
className={`flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium backdrop-blur-sm rounded-lg border shadow-sm transition-colors ${
|
|
1987
|
+
showClusters
|
|
1988
|
+
? "bg-emerald-500 text-white border-emerald-500"
|
|
1989
|
+
: "bg-white/90 text-zinc-500 border-zinc-200 hover:text-zinc-700 hover:bg-zinc-50"
|
|
1990
|
+
}`}
|
|
1991
|
+
title={showClusters ? "Hide cluster labels" : "Show cluster labels"}
|
|
1992
|
+
>
|
|
1993
|
+
<svg
|
|
1994
|
+
className="w-3.5 h-3.5"
|
|
1995
|
+
viewBox="0 0 16 16"
|
|
1996
|
+
fill="none"
|
|
1997
|
+
stroke="currentColor"
|
|
1998
|
+
strokeWidth="1.5"
|
|
1999
|
+
>
|
|
2000
|
+
{/* Dashed circle with label lines — cluster overlay icon */}
|
|
2001
|
+
<circle cx="8" cy="8" r="6" strokeDasharray="2.5 2" strokeOpacity={showClusters ? 1 : 0.5} />
|
|
2002
|
+
<line x1="5" y1="8" x2="11" y2="8" strokeOpacity={showClusters ? 1 : 0.4} />
|
|
2003
|
+
<line x1="6" y1="10" x2="10" y2="10" strokeOpacity={showClusters ? 0.6 : 0.25} strokeWidth="1" />
|
|
2004
|
+
</svg>
|
|
2005
|
+
<span className="hidden sm:inline">Clusters</span>
|
|
2006
|
+
</button>
|
|
1909
2007
|
</div>
|
|
1910
2008
|
|
|
1911
|
-
{/* Bottom-right info panel: stats +
|
|
2009
|
+
{/* Bottom-right info panel: stats + color mode selector + legend (hidden when timeline active) */}
|
|
1912
2010
|
{!timelineActive && (
|
|
1913
2011
|
<div
|
|
1914
2012
|
className="absolute bottom-4 z-10 bg-white/90 backdrop-blur-sm rounded-lg border border-zinc-200 shadow-sm px-3 py-2 text-xs text-zinc-400 transition-[right] duration-300 ease-out"
|
|
1915
|
-
style={{ right: sidebarOpen ? "calc(360px + 1rem)" : "1rem" }}
|
|
2013
|
+
style={{ right: sidebarOpen ? "calc(360px + 1rem)" : "1rem", maxWidth: 320 }}
|
|
1916
2014
|
>
|
|
1917
2015
|
{stats && (
|
|
1918
2016
|
<div className="text-zinc-500 mb-1.5">
|
|
@@ -1924,19 +2022,55 @@ const BeadsGraph = forwardRef<BeadsGraphHandle, BeadsGraphProps>(function BeadsG
|
|
|
1924
2022
|
{stats.prefixes.length === 1 ? " project" : " projects"}
|
|
1925
2023
|
</div>
|
|
1926
2024
|
)}
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
2025
|
+
{/* Color mode segmented control */}
|
|
2026
|
+
<div className="hidden sm:flex bg-zinc-100 rounded-md overflow-hidden mb-1.5">
|
|
2027
|
+
{(["status", "owner", "assignee", "prefix"] as ColorMode[]).map((mode) => (
|
|
2028
|
+
<button
|
|
2029
|
+
key={mode}
|
|
2030
|
+
onClick={() => onColorModeChange?.(mode)}
|
|
2031
|
+
className={`flex-1 px-2 py-1 text-[10px] font-medium transition-colors ${
|
|
2032
|
+
colorMode === mode
|
|
2033
|
+
? "bg-emerald-500 text-white"
|
|
2034
|
+
: "text-zinc-500 hover:text-zinc-700 hover:bg-zinc-200/60"
|
|
2035
|
+
}`}
|
|
2036
|
+
>
|
|
2037
|
+
{COLOR_MODE_LABELS[mode]}
|
|
2038
|
+
</button>
|
|
1936
2039
|
))}
|
|
1937
2040
|
</div>
|
|
2041
|
+
{/* Dynamic legend: status dots or person/prefix dots */}
|
|
2042
|
+
<div className="hidden sm:flex flex-wrap gap-x-3 gap-y-1 mb-1.5">
|
|
2043
|
+
{colorMode === "status" ? (
|
|
2044
|
+
<>
|
|
2045
|
+
{["open", "in_progress", "blocked", "deferred", "closed"].map((status) => (
|
|
2046
|
+
<span key={status} className="flex items-center gap-1">
|
|
2047
|
+
<span
|
|
2048
|
+
className="w-2 h-2 rounded-full"
|
|
2049
|
+
style={{ backgroundColor: STATUS_COLORS[status] }}
|
|
2050
|
+
/>
|
|
2051
|
+
<span className="text-zinc-500">{STATUS_LABELS[status]}</span>
|
|
2052
|
+
</span>
|
|
2053
|
+
))}
|
|
2054
|
+
</>
|
|
2055
|
+
) : (
|
|
2056
|
+
<>
|
|
2057
|
+
{legendItems.map(({ label, color }) => (
|
|
2058
|
+
<span key={label} className="flex items-center gap-1">
|
|
2059
|
+
<span
|
|
2060
|
+
className="w-2 h-2 rounded-full flex-shrink-0"
|
|
2061
|
+
style={{ backgroundColor: color }}
|
|
2062
|
+
/>
|
|
2063
|
+
<span className="text-zinc-500 truncate max-w-[80px]">{label}</span>
|
|
2064
|
+
</span>
|
|
2065
|
+
))}
|
|
2066
|
+
</>
|
|
2067
|
+
)}
|
|
2068
|
+
</div>
|
|
1938
2069
|
<div className="hidden sm:flex flex-col gap-0.5 text-zinc-400">
|
|
1939
|
-
<span>
|
|
2070
|
+
<span>
|
|
2071
|
+
Size = importance · Ring = project
|
|
2072
|
+
{colorMode !== "status" && ` · Fill = ${COLOR_MODE_LABELS[colorMode].toLowerCase()}`}
|
|
2073
|
+
</span>
|
|
1940
2074
|
</div>
|
|
1941
2075
|
<span className="sm:hidden">Tap a node for details</span>
|
|
1942
2076
|
</div>
|
package/lib/types.ts
CHANGED
|
@@ -69,6 +69,19 @@ export interface GraphNode {
|
|
|
69
69
|
_prevStatus?: string; // Previous status value before the change (for color transition)
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// ============================================================================
|
|
73
|
+
// Color mode for legend/node fill switching
|
|
74
|
+
// ============================================================================
|
|
75
|
+
|
|
76
|
+
export type ColorMode = "status" | "owner" | "assignee" | "prefix";
|
|
77
|
+
|
|
78
|
+
export const COLOR_MODE_LABELS: Record<ColorMode, string> = {
|
|
79
|
+
status: "Status",
|
|
80
|
+
owner: "Owner",
|
|
81
|
+
assignee: "Assignee",
|
|
82
|
+
prefix: "Prefix",
|
|
83
|
+
};
|
|
84
|
+
|
|
72
85
|
export interface GraphLink {
|
|
73
86
|
source: string;
|
|
74
87
|
target: string;
|
|
@@ -137,6 +150,36 @@ export const PRIORITY_COLORS: Record<number, string> = {
|
|
|
137
150
|
4: "#d4d4d8", // zinc-300
|
|
138
151
|
};
|
|
139
152
|
|
|
153
|
+
// ============================================================================
|
|
154
|
+
// Catppuccin Latte accent palette
|
|
155
|
+
// ============================================================================
|
|
156
|
+
|
|
157
|
+
/** Catppuccin Latte accent colors — 14 visually distinct, reordered for max contrast between adjacent indices */
|
|
158
|
+
export const CATPPUCCIN_ACCENTS: string[] = [
|
|
159
|
+
"#d20f39", // Red
|
|
160
|
+
"#179299", // Teal
|
|
161
|
+
"#fe640b", // Peach
|
|
162
|
+
"#1e66f5", // Blue
|
|
163
|
+
"#40a02b", // Green
|
|
164
|
+
"#8839ef", // Mauve
|
|
165
|
+
"#df8e1d", // Yellow
|
|
166
|
+
"#209fb5", // Sapphire
|
|
167
|
+
"#ea76cb", // Pink
|
|
168
|
+
"#04a5e5", // Sky
|
|
169
|
+
"#e64553", // Maroon
|
|
170
|
+
"#7287fd", // Lavender
|
|
171
|
+
"#dd7878", // Flamingo
|
|
172
|
+
"#dc8a78", // Rosewater
|
|
173
|
+
];
|
|
174
|
+
|
|
175
|
+
export const CATPPUCCIN_ACCENT_NAMES: string[] = [
|
|
176
|
+
"Red", "Teal", "Peach", "Blue", "Green", "Mauve", "Yellow",
|
|
177
|
+
"Sapphire", "Pink", "Sky", "Maroon", "Lavender", "Flamingo", "Rosewater",
|
|
178
|
+
];
|
|
179
|
+
|
|
180
|
+
/** Catppuccin Latte Surface2 — used for "unassigned" / unknown values */
|
|
181
|
+
export const CATPPUCCIN_UNASSIGNED = "#acb0be";
|
|
182
|
+
|
|
140
183
|
export const TYPE_ICONS: Record<string, string> = {
|
|
141
184
|
bug: "\uD83D\uDC1B",
|
|
142
185
|
feature: "\u2728",
|
|
@@ -190,6 +233,29 @@ export function getPrefixColor(prefix: string): string {
|
|
|
190
233
|
return hashColor(prefix);
|
|
191
234
|
}
|
|
192
235
|
|
|
236
|
+
/**
|
|
237
|
+
* Deterministically map a person handle/name to a Catppuccin Latte accent color.
|
|
238
|
+
* Uses FNV-1a hash modulo 14 to index into CATPPUCCIN_ACCENTS.
|
|
239
|
+
* Returns CATPPUCCIN_UNASSIGNED for undefined/null/empty strings.
|
|
240
|
+
*/
|
|
241
|
+
export function getPersonColor(person: string | undefined): string {
|
|
242
|
+
if (!person) return CATPPUCCIN_UNASSIGNED;
|
|
243
|
+
let hash = 2166136261; // FNV offset basis
|
|
244
|
+
for (let i = 0; i < person.length; i++) {
|
|
245
|
+
hash ^= person.charCodeAt(i);
|
|
246
|
+
hash = (hash * 16777619) >>> 0;
|
|
247
|
+
}
|
|
248
|
+
return CATPPUCCIN_ACCENTS[hash % CATPPUCCIN_ACCENTS.length];
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Deterministically map a prefix string to a Catppuccin Latte accent color.
|
|
253
|
+
* Same approach as getPersonColor but for project prefixes.
|
|
254
|
+
*/
|
|
255
|
+
export function getCatppuccinPrefixColor(prefix: string): string {
|
|
256
|
+
return getPersonColor(prefix);
|
|
257
|
+
}
|
|
258
|
+
|
|
193
259
|
// Backward-compatible exports — delegates to dynamic functions
|
|
194
260
|
// so existing code using PREFIX_COLORS[prefix] || fallback still works.
|
|
195
261
|
// Prefer using getPrefixColor() / getPrefixLabel() directly.
|