mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-02 05:24:56 +08:00
fix(mcp): fix broken memory graph and improve UI design (#837)
## Summary - Fixed memory graph stuck on "Loading graph data..." caused by field name mismatch after package rewrite (`memoryEntries` vs `memories`, `type` vs `documentType`) - Improved MCP app UI to match console-v2 design: golden derives edges (#FBBF24), edge glow pass, node hover effects, dot mode at low zoom, vertical controls with keyboard shortcuts, expandable legend - Synced local `memory-graph` edgeDerives constant with published npm version ## Test plan - [ ] Run `cd apps/mcp && bun run dev`, connect via Claude Desktop - [ ] Verify graph renders with golden derives edges and blue dashed extends edges - [ ] Verify node hover/selection glow effects - [ ] Verify Fit (Z), Center (C), zoom (+/-) keyboard shortcuts - [ ] Verify legend expands/collapses
This commit is contained in:
+59
-33
@@ -24,43 +24,69 @@
|
||||
</div>
|
||||
|
||||
<div id="controls">
|
||||
<button id="fit-btn" title="Fit to view">⊞</button>
|
||||
<button id="zoom-in" title="Zoom in">+</button>
|
||||
<button id="zoom-out" title="Zoom out">−</button>
|
||||
<button type="button" id="fit-btn" title="Fit to view">
|
||||
<span>Fit</span>
|
||||
<kbd>Z</kbd>
|
||||
</button>
|
||||
<button type="button" id="center-btn" title="Center graph">
|
||||
<span>Center</span>
|
||||
<kbd>C</kbd>
|
||||
</button>
|
||||
<div id="zoom-row">
|
||||
<span id="zoom-display">100%</span>
|
||||
<button type="button" id="zoom-out" title="Zoom out">−</button>
|
||||
<button type="button" id="zoom-in" title="Zoom in">+</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="legend">
|
||||
<div class="legend-row">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12"><polygon points="6,1.5 10.4,3.75 10.4,8.25 6,10.5 1.6,8.25 1.6,3.75" fill="#0D2034" stroke="#3B73B8" stroke-width="1.2"/></svg>
|
||||
<span>Memory</span>
|
||||
<div id="legend-toggle">
|
||||
<span>Legend</span>
|
||||
<svg class="legend-chevron" width="10" height="10" viewBox="0 0 10 10" aria-hidden="true"><path d="M3 2l4 3-4 3" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>
|
||||
</div>
|
||||
<div class="legend-row">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12"><polygon points="6,1.5 10.4,3.75 10.4,8.25 6,10.5 1.6,8.25 1.6,3.75" fill="#0D2034" stroke="#10B981" stroke-width="1.2"/></svg>
|
||||
<span>Recent (<1d)</span>
|
||||
</div>
|
||||
<div class="legend-row">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12"><polygon points="6,1.5 10.4,3.75 10.4,8.25 6,10.5 1.6,8.25 1.6,3.75" fill="#0D2034" stroke="#F59E0B" stroke-width="1.2"/></svg>
|
||||
<span>Expiring</span>
|
||||
</div>
|
||||
<div class="legend-row">
|
||||
<svg width="12" height="12" viewBox="0 0 12 12"><polygon points="6,1.5 10.4,3.75 10.4,8.25 6,10.5 1.6,8.25 1.6,3.75" fill="#0D2034" stroke="#EF4444" stroke-width="1.2"/></svg>
|
||||
<span>Forgotten</span>
|
||||
</div>
|
||||
<div class="legend-row">
|
||||
<div class="legend-doc"></div>
|
||||
<span>Document</span>
|
||||
</div>
|
||||
<div class="legend-row">
|
||||
<div class="legend-line" style="border-color: #4A5568;"></div>
|
||||
<span>Doc → Memory</span>
|
||||
</div>
|
||||
<div class="legend-row">
|
||||
<div class="legend-line" style="border-color: #8B5CF6; border-width: 2px;"></div>
|
||||
<span>Version</span>
|
||||
</div>
|
||||
<div class="legend-row">
|
||||
<div class="legend-line dashed" style="border-color: #00D4B8;"></div>
|
||||
<span>Similarity</span>
|
||||
<div id="legend-body">
|
||||
<div class="legend-group">
|
||||
<div class="legend-row">
|
||||
<svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><rect x="1" y="1" width="12" height="12" rx="3" fill="var(--doc-fill)" stroke="var(--doc-stroke)" stroke-width="1.2"/></svg>
|
||||
<span>Documents</span>
|
||||
<span class="legend-count" id="legend-doc-count"></span>
|
||||
</div>
|
||||
<div class="legend-row">
|
||||
<svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><polygon points="7,1.5 12,4 12,9.5 7,12 2,9.5 2,4" fill="var(--hex-fill)" stroke="#3B73B8" stroke-width="1.2"/></svg>
|
||||
<span>Memories</span>
|
||||
<span class="legend-count" id="legend-mem-count"></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="legend-divider"></div>
|
||||
<div class="legend-group">
|
||||
<div class="legend-row">
|
||||
<svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><polygon points="7,1.5 12,4 12,9.5 7,12 2,9.5 2,4" fill="var(--hex-fill)" stroke="#10B981" stroke-width="1.5"/></svg>
|
||||
<span>Recent</span>
|
||||
</div>
|
||||
<div class="legend-row">
|
||||
<svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><polygon points="7,1.5 12,4 12,9.5 7,12 2,9.5 2,4" fill="var(--hex-fill)" stroke="#F59E0B" stroke-width="1.5"/></svg>
|
||||
<span>Expiring</span>
|
||||
</div>
|
||||
<div class="legend-row">
|
||||
<svg width="12" height="12" viewBox="0 0 14 14" aria-hidden="true"><polygon points="7,1.5 12,4 12,9.5 7,12 2,9.5 2,4" fill="var(--hex-fill)" stroke="#EF4444" stroke-width="1.5"/></svg>
|
||||
<span>Forgotten</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="legend-divider"></div>
|
||||
<div class="legend-group">
|
||||
<div class="legend-row">
|
||||
<div class="legend-line" style="border-color: #FBBF24;"></div>
|
||||
<span>Derives</span>
|
||||
</div>
|
||||
<div class="legend-row">
|
||||
<div class="legend-line" style="border-color: #A78BFA; border-width: 2px;"></div>
|
||||
<span>Updates</span>
|
||||
</div>
|
||||
<div class="legend-row">
|
||||
<div class="legend-line dashed" style="border-color: #38BDF8;"></div>
|
||||
<span>Extends</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+155
-46
@@ -1,8 +1,17 @@
|
||||
#graph {
|
||||
width: 100%;
|
||||
height: 600px;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
background-image: radial-gradient(
|
||||
circle,
|
||||
var(--text-muted) 0.5px,
|
||||
transparent 0.5px
|
||||
);
|
||||
background-size: 16px 16px;
|
||||
}
|
||||
|
||||
/* Loading */
|
||||
#loading {
|
||||
display: flex;
|
||||
position: absolute;
|
||||
@@ -31,6 +40,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Stats badge */
|
||||
#stats {
|
||||
position: fixed;
|
||||
top: 12px;
|
||||
@@ -38,20 +48,26 @@
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
z-index: 10;
|
||||
padding: 4px 10px;
|
||||
padding: 6px 12px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 6px;
|
||||
border-radius: 10px;
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Popup */
|
||||
#popup {
|
||||
display: none;
|
||||
position: fixed;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
border-radius: 12px;
|
||||
padding: 14px;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||
box-shadow:
|
||||
0 10px 15px -3px rgba(0, 0, 0, 0.1),
|
||||
0 4px 6px -4px rgba(0, 0, 0, 0.1);
|
||||
z-index: 100;
|
||||
min-width: 220px;
|
||||
max-width: 360px;
|
||||
@@ -65,26 +81,23 @@
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 6px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
#popup-type.document {
|
||||
background: rgba(59, 115, 184, 0.15);
|
||||
color: #3b73b8;
|
||||
}
|
||||
|
||||
#popup-type.memory {
|
||||
background: rgba(59, 115, 184, 0.15);
|
||||
color: #3b73b8;
|
||||
}
|
||||
|
||||
#popup-type.forgotten {
|
||||
background: rgba(239, 68, 68, 0.15);
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
#popup-type.latest {
|
||||
background: rgba(16, 185, 129, 0.15);
|
||||
color: #10b981;
|
||||
@@ -92,7 +105,7 @@
|
||||
|
||||
#popup-title {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
font-size: 13px;
|
||||
margin-bottom: 6px;
|
||||
color: var(--text);
|
||||
word-wrap: break-word;
|
||||
@@ -113,77 +126,173 @@
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* Controls - vertical stack, bottom left, above legend */
|
||||
#controls {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
right: 16px;
|
||||
bottom: 72px;
|
||||
left: 16px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 6px;
|
||||
z-index: 10;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
z-index: 15;
|
||||
}
|
||||
|
||||
#controls button {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
#controls > button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 8px;
|
||||
color: var(--text);
|
||||
font-size: 16px;
|
||||
border-radius: 9999px;
|
||||
color: var(--text-muted);
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
padding: 8px 12px;
|
||||
white-space: nowrap;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
transition: opacity 0.15s;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#controls > button:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
#controls > button kbd {
|
||||
font-family: inherit;
|
||||
font-size: 10px;
|
||||
font-weight: 500;
|
||||
background: var(--border);
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
#zoom-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 9999px;
|
||||
padding: 6px 10px;
|
||||
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
#zoom-row button {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
background: var(--bg-secondary);
|
||||
color: var(--text-muted);
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition:
|
||||
background 0.15s,
|
||||
border-color 0.15s;
|
||||
box-shadow: none;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
#controls button:hover {
|
||||
border-color: var(--accent);
|
||||
#zoom-row button:hover {
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
#controls button:active {
|
||||
background: var(--border);
|
||||
#zoom-display {
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
min-width: 36px;
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
/* Legend - bottom left, single expandable card, 214px wide */
|
||||
#legend {
|
||||
position: fixed;
|
||||
bottom: 16px;
|
||||
left: 16px;
|
||||
width: 214px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 10px;
|
||||
padding: 10px 14px;
|
||||
z-index: 10;
|
||||
font-size: 11px;
|
||||
border-radius: 12px;
|
||||
z-index: 20;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
box-shadow:
|
||||
0 4px 6px -1px rgba(0, 0, 0, 0.1),
|
||||
0 2px 4px -2px rgba(0, 0, 0, 0.1);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#legend-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
font-weight: 600;
|
||||
font-size: 12px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.legend-chevron {
|
||||
transition: transform 0.2s;
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
#legend.collapsed .legend-chevron {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
#legend-body {
|
||||
padding: 0 12px 12px;
|
||||
overflow: hidden;
|
||||
max-height: 400px;
|
||||
transition:
|
||||
max-height 0.25s ease,
|
||||
padding 0.25s ease,
|
||||
opacity 0.2s;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
#legend.collapsed #legend-body {
|
||||
max-height: 0;
|
||||
padding: 0 12px;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.legend-group {
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.legend-divider {
|
||||
height: 1px;
|
||||
background: var(--border);
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
.legend-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 4px;
|
||||
padding: 3px 0;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.legend-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.legend-doc {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 2px;
|
||||
background: var(--doc-fill);
|
||||
border: 1.5px solid var(--doc-stroke);
|
||||
flex-shrink: 0;
|
||||
.legend-count {
|
||||
margin-left: auto;
|
||||
font-size: 11px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.legend-line {
|
||||
width: 16px;
|
||||
width: 18px;
|
||||
height: 0;
|
||||
border-top: 1.5px solid;
|
||||
flex-shrink: 0;
|
||||
|
||||
+445
-81
@@ -45,10 +45,10 @@ interface GraphApiDocument {
|
||||
id: string
|
||||
title: string | null
|
||||
summary: string | null
|
||||
documentType: string
|
||||
type: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
memories: GraphApiMemory[]
|
||||
memoryEntries: GraphApiMemory[]
|
||||
}
|
||||
|
||||
interface ToolResultData {
|
||||
@@ -100,18 +100,25 @@ const MEMORY_BORDER = {
|
||||
}
|
||||
|
||||
const EDGE_COLORS = {
|
||||
dark: {
|
||||
derives: "#38BDF8",
|
||||
updates: "#A78BFA",
|
||||
extends: "#2DD4BF",
|
||||
},
|
||||
light: {
|
||||
derives: "#7DD3FC",
|
||||
updates: "#A78BFA",
|
||||
extends: "#5EEAD4",
|
||||
},
|
||||
dark: { derives: "#FBBF24", updates: "#A78BFA", extends: "#38BDF8" },
|
||||
light: { derives: "#FBBF24", updates: "#A78BFA", extends: "#38BDF8" },
|
||||
}
|
||||
|
||||
const EDGE_OPACITY: Record<string, number> = {
|
||||
derives: 0.4,
|
||||
updates: 0.7,
|
||||
extends: 0.55,
|
||||
}
|
||||
const EDGE_WIDTH: Record<string, number> = {
|
||||
derives: 1.2,
|
||||
updates: 2,
|
||||
extends: 1.5,
|
||||
}
|
||||
|
||||
// Node sizes
|
||||
const MEM_RADIUS = 12
|
||||
const DOC_SIZE = 28
|
||||
|
||||
const SEVEN_DAYS_MS = 7 * 24 * 60 * 60 * 1000
|
||||
const ONE_DAY_MS = 24 * 60 * 60 * 1000
|
||||
const CLUSTER_SPREAD = 120
|
||||
@@ -121,20 +128,32 @@ const CLUSTER_SPREAD = 120
|
||||
// =============================================================================
|
||||
let isDark = true
|
||||
let selectedNode: GraphNode | null = null
|
||||
let hoveredNode: GraphNode | null = null
|
||||
|
||||
// =============================================================================
|
||||
// DOM References
|
||||
// DOM References (elements are guaranteed to exist in mcp-app.html)
|
||||
// =============================================================================
|
||||
// biome-ignore lint/style/noNonNullAssertion: DOM element guaranteed to exist in HTML
|
||||
const container = document.getElementById("graph")!
|
||||
// biome-ignore lint/style/noNonNullAssertion: DOM element guaranteed to exist in HTML
|
||||
const popup = document.getElementById("popup")!
|
||||
// biome-ignore lint/style/noNonNullAssertion: DOM element guaranteed to exist in HTML
|
||||
const popupType = document.getElementById("popup-type")!
|
||||
// biome-ignore lint/style/noNonNullAssertion: DOM element guaranteed to exist in HTML
|
||||
const popupTitle = document.getElementById("popup-title")!
|
||||
// biome-ignore lint/style/noNonNullAssertion: DOM element guaranteed to exist in HTML
|
||||
const popupContent = document.getElementById("popup-content")!
|
||||
// biome-ignore lint/style/noNonNullAssertion: DOM element guaranteed to exist in HTML
|
||||
const popupMeta = document.getElementById("popup-meta")!
|
||||
// biome-ignore lint/style/noNonNullAssertion: DOM element guaranteed to exist in HTML
|
||||
const loadingEl = document.getElementById("loading")!
|
||||
// biome-ignore lint/style/noNonNullAssertion: DOM element guaranteed to exist in HTML
|
||||
const statsEl = document.getElementById("stats")!
|
||||
// biome-ignore lint/style/noNonNullAssertion: DOM element guaranteed to exist in HTML
|
||||
const zoomInBtn = document.getElementById("zoom-in")!
|
||||
// biome-ignore lint/style/noNonNullAssertion: DOM element guaranteed to exist in HTML
|
||||
const zoomOutBtn = document.getElementById("zoom-out")!
|
||||
// biome-ignore lint/style/noNonNullAssertion: DOM element guaranteed to exist in HTML
|
||||
const fitBtn = document.getElementById("fit-btn")!
|
||||
|
||||
// =============================================================================
|
||||
@@ -180,7 +199,7 @@ function transformData(data: ToolResultData): {
|
||||
const nodeIds = new Set<string>()
|
||||
for (const doc of data.documents) {
|
||||
nodeIds.add(doc.id)
|
||||
for (const mem of doc.memories) nodeIds.add(mem.id)
|
||||
for (const mem of doc.memoryEntries) nodeIds.add(mem.id)
|
||||
}
|
||||
|
||||
for (const doc of data.documents) {
|
||||
@@ -190,16 +209,17 @@ function transformData(data: ToolResultData): {
|
||||
nodeType: "document",
|
||||
title: doc.title || "Untitled",
|
||||
summary: doc.summary,
|
||||
docType: doc.documentType,
|
||||
docType: doc.type,
|
||||
createdAt: doc.createdAt,
|
||||
memoryCount: doc.memories.length,
|
||||
memoryCount: doc.memoryEntries.length,
|
||||
x: pos.x,
|
||||
y: pos.y,
|
||||
} as DocumentNode)
|
||||
|
||||
const memCount = doc.memories.length
|
||||
const memCount = doc.memoryEntries.length
|
||||
for (let i = 0; i < memCount; i++) {
|
||||
const mem = doc.memories[i]!
|
||||
// biome-ignore lint/style/noNonNullAssertion: index is always valid within loop bounds
|
||||
const mem = doc.memoryEntries[i]!
|
||||
const angle = (i / memCount) * 2 * Math.PI
|
||||
|
||||
nodes.push({
|
||||
@@ -255,27 +275,145 @@ function transformData(data: ToolResultData): {
|
||||
// =============================================================================
|
||||
// Drawing
|
||||
// =============================================================================
|
||||
function drawHexagon(
|
||||
function hexPath(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
radius: number,
|
||||
strokeColor: string,
|
||||
) {
|
||||
ctx.beginPath()
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const angle = (Math.PI / 3) * i - Math.PI / 6
|
||||
const px = x + radius * Math.cos(angle)
|
||||
const py = y + radius * Math.sin(angle)
|
||||
if (i === 0) ctx.moveTo(px, py)
|
||||
else ctx.lineTo(px, py)
|
||||
ctx.lineTo(x + radius * Math.cos(angle), y + radius * Math.sin(angle))
|
||||
}
|
||||
ctx.closePath()
|
||||
ctx.fillStyle = isDark ? "#0D2034" : "#E8F0FE"
|
||||
}
|
||||
|
||||
function lightenColor(hex: string, amount: number): string {
|
||||
const h = hex.replace("#", "")
|
||||
if (h.length !== 6) return hex
|
||||
const r = Math.min(
|
||||
255,
|
||||
Number.parseInt(h.substring(0, 2), 16) + Math.round(255 * amount),
|
||||
)
|
||||
const g = Math.min(
|
||||
255,
|
||||
Number.parseInt(h.substring(2, 4), 16) + Math.round(255 * amount),
|
||||
)
|
||||
const b = Math.min(
|
||||
255,
|
||||
Number.parseInt(h.substring(4, 6), 16) + Math.round(255 * amount),
|
||||
)
|
||||
return `#${r.toString(16).padStart(2, "0")}${g.toString(16).padStart(2, "0")}${b.toString(16).padStart(2, "0")}`
|
||||
}
|
||||
|
||||
function drawMemoryNode(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
radius: number,
|
||||
screenSize: number,
|
||||
borderColor: string,
|
||||
isHovered: boolean,
|
||||
isSelected: boolean,
|
||||
isForgotten: boolean,
|
||||
isLatest: boolean,
|
||||
) {
|
||||
const accent = isDark ? "#3B73B8" : "#2563eb"
|
||||
const memFill = isDark ? "#0D2034" : "#E8F0FE"
|
||||
|
||||
// Dot mode at very small screen sizes
|
||||
if (screenSize < 8) {
|
||||
const r = Math.max(2, screenSize * 0.45)
|
||||
// Glow halo
|
||||
ctx.save()
|
||||
ctx.globalAlpha = 0.25
|
||||
ctx.beginPath()
|
||||
ctx.arc(x, y, r * 2.5, 0, Math.PI * 2)
|
||||
ctx.fillStyle = borderColor
|
||||
ctx.fill()
|
||||
ctx.restore()
|
||||
// Dot
|
||||
ctx.beginPath()
|
||||
ctx.arc(x, y, r, 0, Math.PI * 2)
|
||||
ctx.fillStyle = memFill
|
||||
ctx.fill()
|
||||
ctx.strokeStyle = borderColor
|
||||
ctx.lineWidth = 1.5
|
||||
ctx.stroke()
|
||||
return
|
||||
}
|
||||
|
||||
// Superseded (non-latest) memory: dimmed with dashed border
|
||||
if (!isLatest && !isSelected && !isHovered) {
|
||||
ctx.save()
|
||||
ctx.globalAlpha = 0.5
|
||||
hexPath(ctx, x, y, radius)
|
||||
ctx.fillStyle = memFill
|
||||
ctx.fill()
|
||||
ctx.strokeStyle = borderColor
|
||||
ctx.lineWidth = 1
|
||||
ctx.setLineDash([3, 3])
|
||||
ctx.stroke()
|
||||
ctx.setLineDash([])
|
||||
// Strikethrough
|
||||
const sr = radius * 0.55
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x - sr, y - sr)
|
||||
ctx.lineTo(x + sr, y + sr)
|
||||
ctx.strokeStyle = isDark ? "#94a3b8" : "#64748b"
|
||||
ctx.lineWidth = 1.5
|
||||
ctx.stroke()
|
||||
ctx.restore()
|
||||
return
|
||||
}
|
||||
|
||||
// Shadow for hover/selected
|
||||
if (isSelected || isHovered) {
|
||||
ctx.save()
|
||||
ctx.shadowColor = isSelected ? accent : isDark ? "#3B73B8" : "#2563eb"
|
||||
ctx.shadowBlur = isSelected ? 18 : 12
|
||||
hexPath(ctx, x, y, radius)
|
||||
ctx.fillStyle = memFill
|
||||
ctx.fill()
|
||||
ctx.restore()
|
||||
|
||||
// Dashed glow ring
|
||||
ctx.save()
|
||||
const scale = isSelected ? 1.15 : 1.1
|
||||
hexPath(ctx, x, y, radius * scale)
|
||||
ctx.strokeStyle = accent
|
||||
ctx.lineWidth = isSelected ? 2 : 1.5
|
||||
ctx.globalAlpha = isSelected ? 0.8 : 0.5
|
||||
ctx.setLineDash(isSelected ? [3, 3] : [4, 4])
|
||||
ctx.stroke()
|
||||
ctx.setLineDash([])
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
// Main hexagon
|
||||
hexPath(ctx, x, y, radius)
|
||||
ctx.fillStyle = isHovered ? (isDark ? "#112840" : "#dbeafe") : memFill
|
||||
ctx.fill()
|
||||
ctx.strokeStyle = strokeColor
|
||||
ctx.lineWidth = 1.5
|
||||
ctx.strokeStyle = isSelected ? accent : borderColor
|
||||
ctx.lineWidth = isSelected ? 2.5 : isHovered ? 2 : 1.5
|
||||
ctx.stroke()
|
||||
|
||||
// Forgotten X icon
|
||||
if (isForgotten && radius > 7) {
|
||||
const iconR = radius * 0.3
|
||||
ctx.save()
|
||||
ctx.lineCap = "round"
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(x - iconR, y - iconR)
|
||||
ctx.lineTo(x + iconR, y + iconR)
|
||||
ctx.moveTo(x + iconR, y - iconR)
|
||||
ctx.lineTo(x - iconR, y + iconR)
|
||||
ctx.strokeStyle = MEMORY_BORDER.forgotten
|
||||
ctx.lineWidth = Math.max(1.5, radius / 10)
|
||||
ctx.stroke()
|
||||
ctx.restore()
|
||||
}
|
||||
}
|
||||
|
||||
function drawDocumentNode(
|
||||
@@ -283,66 +421,157 @@ function drawDocumentNode(
|
||||
x: number,
|
||||
y: number,
|
||||
size: number,
|
||||
screenSize: number,
|
||||
isHovered: boolean,
|
||||
isSelected: boolean,
|
||||
) {
|
||||
const accent = isDark ? "#3B73B8" : "#2563eb"
|
||||
const docFill = isDark ? "#1B1F24" : "#F1F5F9"
|
||||
const docStroke = isDark ? "#2A2F36" : "#CBD5E1"
|
||||
const half = size / 2
|
||||
// Outer rounded rect
|
||||
const cornerR = Math.round(8 * (size / 50))
|
||||
|
||||
// Dot mode at very small screen sizes
|
||||
if (screenSize < 8) {
|
||||
const s = Math.max(3, screenSize)
|
||||
ctx.fillStyle = docFill
|
||||
ctx.fillRect(x - s / 2, y - s / 2, s, s)
|
||||
return
|
||||
}
|
||||
|
||||
// Shadow for hover/selected
|
||||
if (isSelected || isHovered) {
|
||||
ctx.save()
|
||||
ctx.shadowColor = accent
|
||||
ctx.shadowBlur = isSelected ? 16 : 10
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(x - half, y - half, size, size, cornerR)
|
||||
ctx.fillStyle = docFill
|
||||
ctx.fill()
|
||||
ctx.restore()
|
||||
|
||||
// Dashed glow ring
|
||||
ctx.save()
|
||||
const scale = isSelected ? 1.15 : 1.1
|
||||
const gh = (size * scale) / 2
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(
|
||||
x - gh,
|
||||
y - gh,
|
||||
size * scale,
|
||||
size * scale,
|
||||
Math.round(8 * ((size * scale) / 50)),
|
||||
)
|
||||
ctx.strokeStyle = accent
|
||||
ctx.lineWidth = isSelected ? 2 : 1.5
|
||||
ctx.globalAlpha = isSelected ? 0.8 : 0.5
|
||||
ctx.setLineDash(isSelected ? [3, 3] : [4, 4])
|
||||
ctx.stroke()
|
||||
ctx.setLineDash([])
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
// Outer rect with gradient
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(x - half, y - half, size, size, 3)
|
||||
ctx.fillStyle = isDark ? "#1B1F24" : "#F1F5F9"
|
||||
ctx.roundRect(x - half, y - half, size, size, cornerR)
|
||||
const gradient = ctx.createLinearGradient(
|
||||
x - half,
|
||||
y - half,
|
||||
x + half,
|
||||
y + half,
|
||||
)
|
||||
gradient.addColorStop(0, docFill)
|
||||
gradient.addColorStop(1, lightenColor(docFill, 0.08))
|
||||
ctx.fillStyle = gradient
|
||||
ctx.fill()
|
||||
ctx.strokeStyle = isDark ? "#2A2F36" : "#CBD5E1"
|
||||
ctx.lineWidth = 1.5
|
||||
ctx.strokeStyle = isSelected || isHovered ? accent : docStroke
|
||||
ctx.lineWidth = isSelected ? 2.5 : isHovered ? 1.5 : 1
|
||||
ctx.stroke()
|
||||
|
||||
// Inner icon area
|
||||
const iconSize = size * 0.5
|
||||
const iconHalf = iconSize / 2
|
||||
// Inner area
|
||||
const innerSize = size * 0.72
|
||||
const innerHalf = innerSize / 2
|
||||
const innerCornerR = Math.round(6 * (size / 50))
|
||||
ctx.beginPath()
|
||||
ctx.roundRect(x - iconHalf, y - iconHalf, iconSize, iconSize, 2)
|
||||
ctx.roundRect(
|
||||
x - innerHalf,
|
||||
y - innerHalf,
|
||||
innerSize,
|
||||
innerSize,
|
||||
innerCornerR,
|
||||
)
|
||||
ctx.fillStyle = isDark ? "#13161A" : "#E2E8F0"
|
||||
ctx.fill()
|
||||
|
||||
// Document icon (page with fold)
|
||||
const iconS = size * 0.35
|
||||
const iconColor = isDark ? "#3B73B8" : "#2563eb"
|
||||
const w = iconS * 0.7
|
||||
const h = iconS * 0.85
|
||||
const fold = iconS * 0.2
|
||||
const ix = x - w / 2
|
||||
const iy = y - h / 2
|
||||
ctx.save()
|
||||
ctx.strokeStyle = iconColor
|
||||
ctx.lineWidth = Math.max(1, iconS / 12)
|
||||
ctx.lineCap = "round"
|
||||
ctx.lineJoin = "round"
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(ix, iy)
|
||||
ctx.lineTo(ix + w - fold, iy)
|
||||
ctx.lineTo(ix + w, iy + fold)
|
||||
ctx.lineTo(ix + w, iy + h)
|
||||
ctx.lineTo(ix, iy + h)
|
||||
ctx.closePath()
|
||||
ctx.stroke()
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(ix + w - fold, iy)
|
||||
ctx.lineTo(ix + w - fold, iy + fold)
|
||||
ctx.lineTo(ix + w, iy + fold)
|
||||
ctx.stroke()
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Force Graph Setup
|
||||
// =============================================================================
|
||||
function getLinkColor(link: GraphLink): string {
|
||||
const palette = isDark ? EDGE_COLORS.dark : EDGE_COLORS.light
|
||||
return palette[link.edgeType] || palette["derives"]
|
||||
}
|
||||
|
||||
const graph = new ForceGraph<GraphNode, GraphLink>(container)
|
||||
.nodeId("id")
|
||||
.nodeCanvasObject(
|
||||
(node: GraphNode, ctx: CanvasRenderingContext2D, globalScale: number) => {
|
||||
// biome-ignore lint/style/noNonNullAssertion: force-graph guarantees x/y during render
|
||||
const x = node.x!
|
||||
// biome-ignore lint/style/noNonNullAssertion: force-graph guarantees x/y during render
|
||||
const y = node.y!
|
||||
const isHovered = hoveredNode?.id === node.id
|
||||
const isSelected = selectedNode?.id === node.id
|
||||
|
||||
// Dim non-connected nodes when something is selected
|
||||
if (selectedNode && !isSelected && !isHovered) {
|
||||
ctx.globalAlpha = 0.3
|
||||
}
|
||||
|
||||
if (node.nodeType === "memory") {
|
||||
const mem = node as MemoryNode
|
||||
drawHexagon(ctx, x, y, 10, mem.borderColor)
|
||||
|
||||
if (globalScale > 2) {
|
||||
const label = mem.memory.slice(0, 30)
|
||||
ctx.font = `${Math.max(4, 10 / globalScale)}px system-ui, sans-serif`
|
||||
ctx.fillStyle = isDark ? "#94a3b8" : "#64748b"
|
||||
ctx.textAlign = "center"
|
||||
ctx.textBaseline = "top"
|
||||
ctx.fillText(label, x, y + 12)
|
||||
}
|
||||
const screenSize = MEM_RADIUS * 2 * globalScale
|
||||
drawMemoryNode(
|
||||
ctx,
|
||||
x,
|
||||
y,
|
||||
MEM_RADIUS,
|
||||
screenSize,
|
||||
mem.borderColor,
|
||||
isHovered,
|
||||
isSelected,
|
||||
mem.isForgotten,
|
||||
mem.isLatest,
|
||||
)
|
||||
} else {
|
||||
const doc = node as DocumentNode
|
||||
drawDocumentNode(ctx, x, y, 22)
|
||||
|
||||
if (globalScale > 1.2) {
|
||||
const label = (doc.title || "").slice(0, 25)
|
||||
ctx.font = `600 ${Math.max(4, 11 / globalScale)}px system-ui, sans-serif`
|
||||
ctx.fillStyle = isDark ? "#e2e8f0" : "#1e293b"
|
||||
ctx.textAlign = "center"
|
||||
ctx.textBaseline = "top"
|
||||
ctx.fillText(label, x, y + 14)
|
||||
}
|
||||
const screenSize = DOC_SIZE * globalScale
|
||||
drawDocumentNode(ctx, x, y, DOC_SIZE, screenSize, isHovered, isSelected)
|
||||
}
|
||||
|
||||
ctx.globalAlpha = 1
|
||||
},
|
||||
)
|
||||
.nodeCanvasObjectMode(() => "replace")
|
||||
@@ -351,29 +580,92 @@ const graph = new ForceGraph<GraphNode, GraphLink>(container)
|
||||
ctx.fillStyle = color
|
||||
ctx.beginPath()
|
||||
ctx.arc(
|
||||
// biome-ignore lint/style/noNonNullAssertion: force-graph guarantees x/y during render
|
||||
node.x!,
|
||||
// biome-ignore lint/style/noNonNullAssertion: force-graph guarantees x/y during render
|
||||
node.y!,
|
||||
node.nodeType === "document" ? 12 : 11,
|
||||
node.nodeType === "document" ? DOC_SIZE / 2 + 1 : MEM_RADIUS + 1,
|
||||
0,
|
||||
Math.PI * 2,
|
||||
)
|
||||
ctx.fill()
|
||||
},
|
||||
)
|
||||
.linkWidth((link: GraphLink) => {
|
||||
if (link.edgeType === "updates") return 2
|
||||
if (link.edgeType === "extends") return 0.5
|
||||
return 1
|
||||
})
|
||||
.linkColor(getLinkColor)
|
||||
.linkLineDash((link: GraphLink) => {
|
||||
if (link.edgeType === "extends") return [4, 2]
|
||||
return null as unknown as number[]
|
||||
})
|
||||
.linkDirectionalArrowLength((link: GraphLink) =>
|
||||
link.edgeType === "updates" ? 4 : 0,
|
||||
.linkCanvasObject(
|
||||
(link: GraphLink, ctx: CanvasRenderingContext2D, globalScale: number) => {
|
||||
const source = link.source as GraphNode
|
||||
const target = link.target as GraphNode
|
||||
if (!source.x || !source.y || !target.x || !target.y) return
|
||||
|
||||
const { edgeType } = link
|
||||
const palette = isDark ? EDGE_COLORS.dark : EDGE_COLORS.light
|
||||
const color = palette[edgeType] || palette.derives
|
||||
const width = EDGE_WIDTH[edgeType] || 1.2
|
||||
const opacity = EDGE_OPACITY[edgeType] || 0.4
|
||||
const isDimmed = !!selectedNode
|
||||
|
||||
// Culling: extends edges at very low zoom
|
||||
if (edgeType === "extends" && globalScale < 0.08) return
|
||||
|
||||
const dimFactor = isDimmed ? 0.3 : 1
|
||||
const isExtends = edgeType === "extends"
|
||||
|
||||
// Glow pass (behind main edge)
|
||||
if (!isDimmed) {
|
||||
ctx.save()
|
||||
ctx.globalAlpha = edgeType === "updates" ? opacity * 0.4 : opacity * 0.3
|
||||
ctx.strokeStyle = color
|
||||
ctx.lineWidth = edgeType === "updates" ? width + 2 : width + 1.5
|
||||
if (isExtends) ctx.setLineDash([6, 4])
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(source.x, source.y)
|
||||
ctx.lineTo(target.x, target.y)
|
||||
ctx.stroke()
|
||||
if (isExtends) ctx.setLineDash([])
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
// Main edge
|
||||
ctx.save()
|
||||
ctx.globalAlpha = opacity * dimFactor
|
||||
ctx.strokeStyle = color
|
||||
ctx.lineWidth = width
|
||||
if (isExtends) ctx.setLineDash([6, 4])
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(source.x, source.y)
|
||||
ctx.lineTo(target.x, target.y)
|
||||
ctx.stroke()
|
||||
if (isExtends) ctx.setLineDash([])
|
||||
ctx.restore()
|
||||
|
||||
// Arrowhead for updates edges
|
||||
if (edgeType === "updates") {
|
||||
const arrowSize = Math.max(6, 8 * globalScale)
|
||||
const angle = Math.atan2(target.y - source.y, target.x - source.x)
|
||||
ctx.save()
|
||||
ctx.globalAlpha = opacity * 0.6 * dimFactor
|
||||
ctx.fillStyle = color
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(target.x, target.y)
|
||||
ctx.lineTo(
|
||||
target.x - arrowSize * Math.cos(angle - Math.PI / 6),
|
||||
target.y - arrowSize * Math.sin(angle - Math.PI / 6),
|
||||
)
|
||||
ctx.lineTo(
|
||||
target.x - arrowSize * Math.cos(angle + Math.PI / 6),
|
||||
target.y - arrowSize * Math.sin(angle + Math.PI / 6),
|
||||
)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
ctx.restore()
|
||||
}
|
||||
},
|
||||
)
|
||||
.linkDirectionalArrowRelPos(1)
|
||||
.linkCanvasObjectMode(() => "replace")
|
||||
.onNodeHover((node: GraphNode | null) => {
|
||||
hoveredNode = node
|
||||
container.style.cursor = node ? "pointer" : "default"
|
||||
})
|
||||
.onNodeClick(handleNodeClick)
|
||||
.onBackgroundClick(() => hidePopup())
|
||||
.d3Force(
|
||||
@@ -454,10 +746,33 @@ function showPopup(node: GraphNode, x: number, y: number) {
|
||||
|
||||
popup.style.display = "block"
|
||||
|
||||
// Smart quadrant positioning (right > left > below > above)
|
||||
const rect = popup.getBoundingClientRect()
|
||||
const gap = 15
|
||||
const left = x < window.innerWidth / 2 ? x + gap : x - rect.width - gap
|
||||
const top = y < window.innerHeight / 2 ? y + gap : y - rect.height - gap
|
||||
const gap = 24
|
||||
const vw = window.innerWidth
|
||||
const vh = window.innerHeight
|
||||
let left: number
|
||||
let top: number
|
||||
|
||||
// Try right
|
||||
if (x + gap + rect.width < vw - 8) {
|
||||
left = x + gap
|
||||
} else if (x - gap - rect.width > 8) {
|
||||
// Try left
|
||||
left = x - gap - rect.width
|
||||
} else {
|
||||
// Fallback center
|
||||
left = Math.max(8, (vw - rect.width) / 2)
|
||||
}
|
||||
|
||||
if (y - rect.height / 2 > 8 && y + rect.height / 2 < vh - 8) {
|
||||
top = y - rect.height / 2
|
||||
} else if (y + gap + rect.height < vh - 8) {
|
||||
top = y + gap
|
||||
} else {
|
||||
top = y - gap - rect.height
|
||||
}
|
||||
|
||||
popup.style.left = `${Math.max(8, left)}px`
|
||||
popup.style.top = `${Math.max(8, top)}px`
|
||||
}
|
||||
@@ -470,7 +785,12 @@ function hidePopup() {
|
||||
// =============================================================================
|
||||
// Controls
|
||||
// =============================================================================
|
||||
const ZOOM_FACTOR = 1.5
|
||||
const ZOOM_FACTOR = 1.3
|
||||
// biome-ignore lint/style/noNonNullAssertion: DOM element guaranteed to exist in HTML
|
||||
const centerBtn = document.getElementById("center-btn")!
|
||||
// biome-ignore lint/style/noNonNullAssertion: DOM element guaranteed to exist in HTML
|
||||
const zoomDisplay = document.getElementById("zoom-display")!
|
||||
|
||||
zoomInBtn.addEventListener("click", () =>
|
||||
graph.zoom(graph.zoom() * ZOOM_FACTOR, 200),
|
||||
)
|
||||
@@ -478,11 +798,49 @@ zoomOutBtn.addEventListener("click", () =>
|
||||
graph.zoom(graph.zoom() / ZOOM_FACTOR, 200),
|
||||
)
|
||||
fitBtn.addEventListener("click", () => graph.zoomToFit(400, 40))
|
||||
centerBtn.addEventListener("click", () => graph.centerAt(0, 0, 400))
|
||||
|
||||
// Update zoom display
|
||||
graph.onZoom(({ k }) => {
|
||||
zoomDisplay.textContent = `${Math.round(k * 100)}%`
|
||||
})
|
||||
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") hidePopup()
|
||||
const tag = (e.target as HTMLElement).tagName
|
||||
if (tag === "INPUT" || tag === "TEXTAREA") return
|
||||
|
||||
switch (e.key) {
|
||||
case "Escape":
|
||||
hidePopup()
|
||||
break
|
||||
case "z":
|
||||
case "Z":
|
||||
graph.zoomToFit(400, 40)
|
||||
break
|
||||
case "c":
|
||||
case "C":
|
||||
graph.centerAt(0, 0, 400)
|
||||
break
|
||||
case "+":
|
||||
case "=":
|
||||
graph.zoom(graph.zoom() * ZOOM_FACTOR, 200)
|
||||
break
|
||||
case "-":
|
||||
case "_":
|
||||
graph.zoom(graph.zoom() / ZOOM_FACTOR, 200)
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
// Legend toggle
|
||||
// biome-ignore lint/style/noNonNullAssertion: DOM element guaranteed to exist in HTML
|
||||
const legendEl = document.getElementById("legend")!
|
||||
// biome-ignore lint/style/noNonNullAssertion: DOM element guaranteed to exist in HTML
|
||||
const legendToggle = document.getElementById("legend-toggle")!
|
||||
legendToggle.addEventListener("click", () =>
|
||||
legendEl.classList.toggle("collapsed"),
|
||||
)
|
||||
|
||||
// =============================================================================
|
||||
// Theme
|
||||
// =============================================================================
|
||||
@@ -529,6 +887,12 @@ app.ontoolresult = (result: CallToolResult) => {
|
||||
|
||||
statsEl.textContent = `${docCount} docs \u00b7 ${memCount} memories \u00b7 ${links.length} connections`
|
||||
|
||||
// Update legend counts
|
||||
const docCountEl = document.getElementById("legend-doc-count")
|
||||
const memCountEl = document.getElementById("legend-mem-count")
|
||||
if (docCountEl) docCountEl.textContent = String(docCount)
|
||||
if (memCountEl) memCountEl.textContent = String(memCount)
|
||||
|
||||
graph.graphData({ nodes, links })
|
||||
|
||||
// Fit to view after layout stabilizes
|
||||
|
||||
@@ -2951,7 +2951,7 @@
|
||||
|
||||
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg=="],
|
||||
|
||||
"force-graph": ["force-graph@1.51.1", "", { "dependencies": { "@tweenjs/tween.js": "18 - 25", "accessor-fn": "1", "bezier-js": "3 - 6", "canvas-color-tracker": "^1.3", "d3-array": "1 - 3", "d3-drag": "2 - 3", "d3-force-3d": "2 - 3", "d3-scale": "1 - 4", "d3-scale-chromatic": "1 - 3", "d3-selection": "2 - 3", "d3-zoom": "2 - 3", "float-tooltip": "^1.7", "index-array-by": "1", "kapsule": "^1.16", "lodash-es": "4" } }, "sha512-uEEX8iRzgq1IKRISOw6RrB2RLMhcI25xznQYrCTVvxZHZZ+A2jH6qIolYuwavVxAMi64pFp2yZm4KFVdD993cg=="],
|
||||
"force-graph": ["force-graph@1.51.2", "", { "dependencies": { "@tweenjs/tween.js": "18 - 25", "accessor-fn": "1", "bezier-js": "3 - 6", "canvas-color-tracker": "^1.3", "d3-array": "1 - 3", "d3-drag": "2 - 3", "d3-force-3d": "2 - 3", "d3-scale": "1 - 4", "d3-scale-chromatic": "1 - 3", "d3-selection": "2 - 3", "d3-zoom": "2 - 3", "float-tooltip": "^1.7", "index-array-by": "1", "kapsule": "^1.16", "lodash-es": "4" } }, "sha512-zZNdMqx8qIQGurgnbgYIUsdXxSfvhfRSIdncsKGv/twUOZpwCsk9hPHmdjdcme1+epATgb41G0rkIGHJ0Wydng=="],
|
||||
|
||||
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ export const DEFAULT_COLORS: GraphThemeColors = {
|
||||
textPrimary: "#ffffff",
|
||||
textSecondary: "#e2e8f0",
|
||||
textMuted: "#94a3b8",
|
||||
edgeDerives: "#7094B8",
|
||||
edgeDerives: "#FBBF24",
|
||||
edgeUpdates: "#A78BFA",
|
||||
edgeExtends: "#38BDF8",
|
||||
memBorderForgotten: "#EF4444",
|
||||
|
||||
Reference in New Issue
Block a user