mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-30 01:51:28 +00:00
fix(memory-graph): distinguish document links from derives relations (#1701)
Document-to-memory links and actual `derives` relations were both emitted as `derives`, so they shared the same color and legend entry. This separates structural document links into a `document` edge type, adds a dedicated theme color with `--graph-edge-document` support, and updates force-layout and level-of-detail handling to preserve existing structural behavior. The package and MCP widget legends/themes now distinguish document links from derived-memory relations. Adds regression coverage for edge classification and validates the package plus its MCP consumer. <!-- capy-badge:start --> <a href="https://capy.ai/thread/jam_01M36F4MPFXZCA8J14T53YY029"><picture><source media="(prefers-color-scheme: dark)" srcset="https://capy.ai/badge/accent-dark.svg"><source media="(prefers-color-scheme: light)" srcset="https://capy.ai/badge/accent-light.svg"><img alt="Open in Capy" src="https://capy.ai/badge/accent-light.svg"></picture></a> <!-- capy-badge:end -->
This commit is contained in:
parent
0e12f0b3a6
commit
cfa6c7cb17
14 changed files with 95 additions and 55 deletions
|
|
@ -196,6 +196,7 @@
|
|||
--graph-text-primary: #171717;
|
||||
--graph-text-secondary: #5f5f5f;
|
||||
--graph-text-muted: #737373;
|
||||
--graph-edge-document: #d9d9d4;
|
||||
--graph-edge-derives: #b8b8b3;
|
||||
--graph-edge-updates: #2e6fe6;
|
||||
--graph-edge-extends: #0e9488;
|
||||
|
|
@ -298,6 +299,7 @@
|
|||
--graph-text-primary: #eef2f7;
|
||||
--graph-text-secondary: #a9b4c2;
|
||||
--graph-text-muted: #6f7d8f;
|
||||
--graph-edge-document: #2b3a4e;
|
||||
--graph-edge-derives: #5b6675;
|
||||
--graph-edge-updates: #4ba0fa;
|
||||
--graph-edge-extends: #2dd4bf;
|
||||
|
|
@ -400,6 +402,7 @@
|
|||
--graph-text-primary: #eef2f7;
|
||||
--graph-text-secondary: #a9b4c2;
|
||||
--graph-text-muted: #6f7d8f;
|
||||
--graph-edge-document: #2b3a4e;
|
||||
--graph-edge-derives: #5b6675;
|
||||
--graph-edge-updates: #4ba0fa;
|
||||
--graph-edge-extends: #2dd4bf;
|
||||
|
|
|
|||
|
|
@ -15,10 +15,7 @@ import { useApp } from "../hooks/useApp"
|
|||
import { useHostContext } from "../hooks/useHostContext"
|
||||
import { ArrowsIn, ArrowsOut } from "../lib/icons"
|
||||
|
||||
// GraphThemeColors key → the --graph-* CSS variable it resolves from. Same
|
||||
// mapping as the package's internal useGraphTheme, but we drive it ourselves
|
||||
// so we can re-read at a reliable time (see useGraphColors).
|
||||
const GRAPH_VARS: Record<keyof GraphThemeColors, string> = {
|
||||
const GRAPH_VARS = {
|
||||
bg: "--graph-bg",
|
||||
docFill: "--graph-doc-fill",
|
||||
docStroke: "--graph-doc-stroke",
|
||||
|
|
@ -30,6 +27,7 @@ const GRAPH_VARS: Record<keyof GraphThemeColors, string> = {
|
|||
textPrimary: "--graph-text-primary",
|
||||
textSecondary: "--graph-text-secondary",
|
||||
textMuted: "--graph-text-muted",
|
||||
edgeDocument: "--graph-edge-document",
|
||||
edgeDerives: "--graph-edge-derives",
|
||||
edgeUpdates: "--graph-edge-updates",
|
||||
edgeExtends: "--graph-edge-extends",
|
||||
|
|
@ -45,23 +43,18 @@ const GRAPH_VARS: Record<keyof GraphThemeColors, string> = {
|
|||
popoverTextMuted: "--graph-popover-text-muted",
|
||||
controlBg: "--graph-control-bg",
|
||||
controlBorder: "--graph-control-border",
|
||||
}
|
||||
} satisfies Record<Exclude<keyof GraphThemeColors, "dotColor">, string>
|
||||
|
||||
function readGraphColors(): GraphThemeColors {
|
||||
const s = getComputedStyle(document.documentElement)
|
||||
const out = {} as GraphThemeColors
|
||||
for (const key of Object.keys(GRAPH_VARS) as (keyof GraphThemeColors)[]) {
|
||||
for (const key of Object.keys(GRAPH_VARS) as (keyof typeof GRAPH_VARS)[]) {
|
||||
out[key] = s.getPropertyValue(GRAPH_VARS[key]).trim() || DEFAULT_COLORS[key]
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Read the graph palette from CSS, and re-read whenever the document theme
|
||||
// flips. We can't rely on the package's own useGraphTheme (it watches `class`),
|
||||
// nor on the `theme` prop alone: the host themes via `data-theme`, and in
|
||||
// standalone/Studio the attribute can change without the prop changing. So we
|
||||
// observe `data-theme`/`class` on <html> directly and re-read after the next
|
||||
// paint (rAF), by which point the --graph-* values are current.
|
||||
// Host themes can update CSS variables after render, so re-read after theme mutations.
|
||||
function useGraphColors(theme: string): GraphThemeColors {
|
||||
const [colors, setColors] = useState<GraphThemeColors>(readGraphColors)
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -71,6 +71,8 @@ Console mode uses the supplied theme colors for its surface, 16px dot grid, docu
|
|||
|
||||
Set `colors.dotColor` or the `--graph-dot` CSS variable to style the dot grid independently of text. When neither is set, the grid uses `textMuted`.
|
||||
|
||||
Document-to-memory links use `colors.edgeDocument` or `--graph-edge-document`, while derived-memory relations use `colors.edgeDerives` or `--graph-edge-derives`.
|
||||
|
||||
For paginated initial loading, pass `hasMore` and `isLoadingMore` alongside `documents`. In both variants, new batches gradually warm the force layout from the existing node positions. Initial and appended nodes relax until their movement stays low, then cool automatically; a tick limit bounds settling for layouts that keep drifting. The initial view smoothly follows the changing bounds until loading and settling finish. Manual interaction immediately cancels automatic camera movement. Clicking a node selects it without restarting the forces; dragging warms the layout until release, including release outside the canvas. Changing the document selection starts a new fit; the Fit control remains available at any time. The existing static layout safeguard for more than 6,000 nodes remains in place.
|
||||
|
||||
## Documentation
|
||||
|
|
|
|||
|
|
@ -20,7 +20,10 @@
|
|||
|
||||
import { describe, it, expect } from "vitest"
|
||||
import type { GraphApiDocument, GraphApiMemory } from "../types"
|
||||
import { getEdgeVisualProps } from "../hooks/use-graph-data"
|
||||
import {
|
||||
computeEdges as computeGraphEdges,
|
||||
getEdgeVisualProps,
|
||||
} from "../hooks/use-graph-data"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pure extraction of edges useMemo from use-graph-data.ts
|
||||
|
|
@ -48,14 +51,13 @@ function computeEdges(documents: GraphApiDocument[]): ComputedEdge[] {
|
|||
for (const mem of doc.memories) allNodeIds.add(mem.id)
|
||||
}
|
||||
|
||||
// 1. Derives edges: document -> memory (structural)
|
||||
for (const doc of documents) {
|
||||
for (const mem of doc.memories) {
|
||||
result.push({
|
||||
id: `dm-${doc.id}-${mem.id}`,
|
||||
source: doc.id,
|
||||
target: mem.id,
|
||||
edgeType: "derives",
|
||||
edgeType: "document",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -574,12 +576,8 @@ describe("MCP transformData: forward-reference fix (pre-populated nodeIds)", ()
|
|||
})
|
||||
})
|
||||
|
||||
// ===========================================================================
|
||||
// (6) Structural correctness: derives edges always created, doc->mem
|
||||
// ===========================================================================
|
||||
|
||||
describe("use-graph-data edges: derives edges always present for all doc->mem pairs", () => {
|
||||
it("creates exactly one derives edge per memory across multiple documents", () => {
|
||||
describe("use-graph-data edges: document edges always present for all doc->mem pairs", () => {
|
||||
it("creates exactly one document edge per memory across multiple documents", () => {
|
||||
const docs = [
|
||||
makeDoc("d1", [makeMem({ id: "m1" }), makeMem({ id: "m2" })]),
|
||||
makeDoc("d2", [
|
||||
|
|
@ -589,10 +587,9 @@ describe("use-graph-data edges: derives edges always present for all doc->mem pa
|
|||
]),
|
||||
]
|
||||
const edges = computeEdges(docs)
|
||||
const derivesEdges = edges.filter((e) => e.edgeType === "derives")
|
||||
// 2 memories in d1 + 3 in d2 = 5 derives edges
|
||||
expect(derivesEdges.length).toBe(5)
|
||||
expect(derivesEdges.map((e) => e.target).sort()).toEqual([
|
||||
const documentEdges = edges.filter((e) => e.edgeType === "document")
|
||||
expect(documentEdges.length).toBe(5)
|
||||
expect(documentEdges.map((e) => e.target).sort()).toEqual([
|
||||
"m1",
|
||||
"m2",
|
||||
"m3",
|
||||
|
|
@ -618,6 +615,27 @@ describe("use-graph-data edges: derives edges always present for all doc->mem pa
|
|||
})
|
||||
})
|
||||
|
||||
describe("document and derives edge types", () => {
|
||||
it("keeps document links separate from derives relations", () => {
|
||||
const docs = [
|
||||
makeDoc("d1", [
|
||||
makeMem({ id: "source" }),
|
||||
makeMem({
|
||||
id: "derived",
|
||||
memoryRelations: { source: "derives" },
|
||||
}),
|
||||
]),
|
||||
]
|
||||
|
||||
const edges = computeGraphEdges(docs)
|
||||
|
||||
expect(edges.filter((edge) => edge.edgeType === "document")).toHaveLength(2)
|
||||
expect(
|
||||
edges.find((edge) => edge.id === "rel-source-derived")?.edgeType,
|
||||
).toBe("derives")
|
||||
})
|
||||
})
|
||||
|
||||
// ===========================================================================
|
||||
// (7) getEdgeVisualProps: the MemoryRelation type is the canonical source
|
||||
// ===========================================================================
|
||||
|
|
|
|||
|
|
@ -80,8 +80,8 @@ describe("relation edge level-of-detail helpers", () => {
|
|||
expect(getRelationEdgeStride(1040, 0.1)).toBe(4)
|
||||
})
|
||||
|
||||
test("always draws structural derives edges", () => {
|
||||
expect(shouldDrawRelationEdge("edge-1", "derives", 10)).toBe(true)
|
||||
test("always draws structural document edges", () => {
|
||||
expect(shouldDrawRelationEdge("edge-1", "document", 10)).toBe(true)
|
||||
})
|
||||
|
||||
test("deterministically samples non-structural relation edges", () => {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ function makeEdge(source: string, target: string): GraphEdge {
|
|||
source,
|
||||
target,
|
||||
visualProps: { opacity: 0.5, thickness: 1.5 },
|
||||
edgeType: "derives",
|
||||
edgeType: "document",
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,8 +21,8 @@ const edgeBatches = new Map<string, PreparedEdge[]>()
|
|||
const RELATION_LOD_ZOOM = 0.5
|
||||
const RELATION_LOD_MAX_BACKGROUND_EDGES = 260
|
||||
const RELATION_LOD_DENSE_COUNT = 180
|
||||
const DERIVES_LOD_ZOOM = 0.38
|
||||
const DERIVES_LOD_MAX_BACKGROUND_EDGES = 3200
|
||||
const DOCUMENT_LOD_ZOOM = 0.38
|
||||
const DOCUMENT_LOD_MAX_BACKGROUND_EDGES = 3200
|
||||
const DENSE_POINT_THRESHOLD = 25000
|
||||
const DENSE_POINT_ZOOM = 0.42
|
||||
|
||||
|
|
@ -83,6 +83,8 @@ function edgeStyle(
|
|||
edge: GraphEdge,
|
||||
colors: GraphThemeColors,
|
||||
): { color: string; width: number; opacity: number } {
|
||||
if (edge.edgeType === "document")
|
||||
return { color: colors.edgeDocument, width: 1.2, opacity: 0.4 }
|
||||
if (edge.edgeType === "derives")
|
||||
return { color: colors.edgeDerives, width: 1.2, opacity: 0.4 }
|
||||
if (edge.edgeType === "updates")
|
||||
|
|
@ -104,14 +106,17 @@ export function getRelationEdgeStride(
|
|||
return Math.ceil(relationEdgeCount / RELATION_LOD_MAX_BACKGROUND_EDGES)
|
||||
}
|
||||
|
||||
function getDerivesEdgeStride(derivesEdgeCount: number, zoom: number): number {
|
||||
function getDocumentEdgeStride(
|
||||
documentEdgeCount: number,
|
||||
zoom: number,
|
||||
): number {
|
||||
if (
|
||||
zoom >= DERIVES_LOD_ZOOM ||
|
||||
derivesEdgeCount <= DERIVES_LOD_MAX_BACKGROUND_EDGES
|
||||
zoom >= DOCUMENT_LOD_ZOOM ||
|
||||
documentEdgeCount <= DOCUMENT_LOD_MAX_BACKGROUND_EDGES
|
||||
) {
|
||||
return 1
|
||||
}
|
||||
return Math.ceil(derivesEdgeCount / DERIVES_LOD_MAX_BACKGROUND_EDGES)
|
||||
return Math.ceil(documentEdgeCount / DOCUMENT_LOD_MAX_BACKGROUND_EDGES)
|
||||
}
|
||||
|
||||
export function shouldDrawRelationEdge(
|
||||
|
|
@ -119,7 +124,7 @@ export function shouldDrawRelationEdge(
|
|||
edgeType: string,
|
||||
stride: number,
|
||||
): boolean {
|
||||
if (edgeType === "derives" || stride <= 1) return true
|
||||
if (edgeType === "document" || stride <= 1) return true
|
||||
return hashString(edgeId) % stride === 0
|
||||
}
|
||||
|
||||
|
|
@ -135,7 +140,7 @@ function applyRelationLevelOfDetail(
|
|||
hasFocus: boolean,
|
||||
hasActiveHover: boolean,
|
||||
) {
|
||||
if (edgeType === "derives") return { style, glow: true }
|
||||
if (edgeType === "document") return { style, glow: true }
|
||||
if (hasFocus || hasActiveHover) {
|
||||
const isUpdate = edgeType === "updates"
|
||||
const minOpacity = hasActiveHover ? 0.9 : 0.76
|
||||
|
|
@ -213,17 +218,17 @@ function drawEdges(
|
|||
const margin = 100
|
||||
const hasDim = state.selectedNodeId !== null && state.dimProgress > 0
|
||||
const relationEdgeCount = edges.reduce(
|
||||
(count, edge) => count + (edge.edgeType === "derives" ? 0 : 1),
|
||||
(count, edge) => count + (edge.edgeType === "document" ? 0 : 1),
|
||||
0,
|
||||
)
|
||||
const derivesEdgeCount = edges.length - relationEdgeCount
|
||||
const documentEdgeCount = edges.length - relationEdgeCount
|
||||
const relationStride = getRelationEdgeStride(relationEdgeCount, viewport.zoom)
|
||||
const derivesStride = getDerivesEdgeStride(derivesEdgeCount, viewport.zoom)
|
||||
const documentStride = getDocumentEdgeStride(documentEdgeCount, viewport.zoom)
|
||||
|
||||
const prepared: PreparedEdge[] = []
|
||||
|
||||
for (const edge of edges) {
|
||||
const edgeType = edge.edgeType ?? "derives"
|
||||
const edgeType = edge.edgeType ?? "document"
|
||||
const srcId = typeof edge.source === "string" ? edge.source : edge.source.id
|
||||
const tgtId = typeof edge.target === "string" ? edge.target : edge.target.id
|
||||
const hoverConnected =
|
||||
|
|
@ -235,7 +240,7 @@ function drawEdges(
|
|||
const activeConnected = hoverConnected || selectedConnected
|
||||
const shouldAlwaysDrawActiveUpdate =
|
||||
edgeType === "updates" && activeConnected
|
||||
const edgeStride = edgeType === "derives" ? derivesStride : relationStride
|
||||
const edgeStride = edgeType === "document" ? documentStride : relationStride
|
||||
if (
|
||||
!shouldAlwaysDrawActiveUpdate &&
|
||||
!hasDim &&
|
||||
|
|
@ -248,7 +253,7 @@ function drawEdges(
|
|||
const tgt = nodeMap.get(tgtId)
|
||||
if (!src || !tgt) continue
|
||||
|
||||
if (edgeType === "derives") {
|
||||
if (edgeType === "document") {
|
||||
const mem = src.type === "memory" ? src : tgt
|
||||
if (mem.size * viewport.zoom < 3) continue
|
||||
}
|
||||
|
|
@ -297,9 +302,9 @@ function drawEdges(
|
|||
)
|
||||
let style = edgeDetail.style
|
||||
let glow = edgeDetail.glow
|
||||
if (edgeType === "derives" && derivesStride > 1 && !activeConnected) {
|
||||
if (edgeType === "document" && documentStride > 1 && !activeConnected) {
|
||||
const zoomFactor = clampNumber(
|
||||
viewport.zoom / DERIVES_LOD_ZOOM,
|
||||
viewport.zoom / DOCUMENT_LOD_ZOOM,
|
||||
0.08,
|
||||
0.32,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -12,9 +12,6 @@ export class ForceSimulation {
|
|||
this.destroy()
|
||||
|
||||
try {
|
||||
// Only use structural edges (derives, updates) for the force layout.
|
||||
// "extends" edges are visual-only -- they connect documents sharing a
|
||||
// spaceId but should not pull documents together into a single mass.
|
||||
const structuralEdges = edges.filter((e) => e.edgeType !== "extends")
|
||||
|
||||
this.sim = d3
|
||||
|
|
@ -29,12 +26,12 @@ export class ForceSimulation {
|
|||
.forceLink<GraphNode, GraphEdge>(structuralEdges)
|
||||
.id((d) => d.id)
|
||||
.distance((link) =>
|
||||
link.edgeType === "derives"
|
||||
link.edgeType === "document"
|
||||
? getDocMemoryDistance(link)
|
||||
: FORCE_CONFIG.linkDistance,
|
||||
)
|
||||
.strength((link) => {
|
||||
if (link.edgeType === "derives")
|
||||
if (link.edgeType === "document")
|
||||
return FORCE_CONFIG.linkStrength.docMemory
|
||||
if (link.edgeType === "updates")
|
||||
return FORCE_CONFIG.linkStrength.version
|
||||
|
|
|
|||
|
|
@ -309,6 +309,7 @@ export const Legend = memo(function Legend({
|
|||
const memoryCount = nodes.filter((n) => n.type === "memory").length
|
||||
const documentCount = nodes.filter((n) => n.type === "document").length
|
||||
const connectionCount = edges.length
|
||||
const documentEdgeCount = countEdgesByType(edges, "document")
|
||||
const derivesCount = countEdgesByType(edges, "derives")
|
||||
const updatesCount = countEdgesByType(edges, "updates")
|
||||
const extendsCount = countEdgesByType(edges, "extends")
|
||||
|
|
@ -531,17 +532,29 @@ export const Legend = memo(function Legend({
|
|||
<div>
|
||||
<div style={rowStyle}>
|
||||
<div style={rowLeftStyle}>
|
||||
<LineIcon color={colors.edgeDerives} />
|
||||
<LineIcon color={colors.edgeDocument} />
|
||||
<span style={edgeLabelStyle}>
|
||||
{labels.documentSourceEdge}
|
||||
</span>
|
||||
</div>
|
||||
<span style={countStyle}>{derivesCount}</span>
|
||||
<span style={countStyle}>{documentEdgeCount}</span>
|
||||
</div>
|
||||
<div style={edgeDescriptionStyle}>
|
||||
{labels.documentToMemoryEdge}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={rowStyle}>
|
||||
<div style={rowLeftStyle}>
|
||||
<LineIcon color={colors.edgeDerives} />
|
||||
<span style={edgeLabelStyle}>Derives</span>
|
||||
</div>
|
||||
<span style={countStyle}>{derivesCount}</span>
|
||||
</div>
|
||||
<div style={edgeDescriptionStyle}>
|
||||
Memory derived from another memory
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div style={rowStyle}>
|
||||
<div style={rowLeftStyle}>
|
||||
|
|
|
|||
|
|
@ -73,6 +73,7 @@ export const DEFAULT_COLORS: GraphThemeColors = {
|
|||
textPrimary: "#ffffff",
|
||||
textSecondary: "#e2e8f0",
|
||||
textMuted: "#94a3b8",
|
||||
edgeDocument: "#64748B",
|
||||
edgeDerives: "#FBBF24",
|
||||
edgeUpdates: "#9B8AE6",
|
||||
edgeExtends: "#94A3B8",
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ export function getMemoryBorderColor(
|
|||
|
||||
export function getEdgeVisualProps(edgeType: string) {
|
||||
switch (edgeType) {
|
||||
case "document":
|
||||
case "derives":
|
||||
return { opacity: 0.4, thickness: 1.2 }
|
||||
case "updates":
|
||||
|
|
@ -413,15 +414,14 @@ export function computeEdges(documents: GraphApiDocument[]): GraphEdge[] {
|
|||
for (const mem of doc.memories) allNodeIds.add(mem.id)
|
||||
}
|
||||
|
||||
// 1. Derives edges: document -> memory (structural)
|
||||
for (const doc of documents) {
|
||||
for (const mem of doc.memories) {
|
||||
result.push({
|
||||
id: `dm-${doc.id}-${mem.id}`,
|
||||
source: doc.id,
|
||||
target: mem.id,
|
||||
visualProps: getEdgeVisualProps("derives"),
|
||||
edgeType: "derives",
|
||||
visualProps: getEdgeVisualProps("document"),
|
||||
edgeType: "document",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,10 @@ function resolveColors(): GraphThemeColors {
|
|||
DEFAULT_COLORS.textSecondary,
|
||||
),
|
||||
textMuted: readCssVar("--graph-text-muted", DEFAULT_COLORS.textMuted),
|
||||
edgeDocument: readCssVar(
|
||||
"--graph-edge-document",
|
||||
readCssVar("--graph-text-muted", DEFAULT_COLORS.edgeDocument),
|
||||
),
|
||||
edgeDerives: readCssVar("--graph-edge-derives", DEFAULT_COLORS.edgeDerives),
|
||||
edgeUpdates: readCssVar("--graph-edge-updates", DEFAULT_COLORS.edgeUpdates),
|
||||
edgeExtends: readCssVar("--graph-edge-extends", DEFAULT_COLORS.edgeExtends),
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export type {
|
|||
ResolvedMemoryGraphLabels,
|
||||
GraphNode,
|
||||
GraphEdge,
|
||||
GraphEdgeType,
|
||||
GraphThemeColors,
|
||||
GraphCanvasProps,
|
||||
GraphApiDocument,
|
||||
|
|
|
|||
|
|
@ -42,6 +42,8 @@ export interface GraphApiEdge {
|
|||
edgeType: MemoryRelation
|
||||
}
|
||||
|
||||
export type GraphEdgeType = MemoryRelation | "document"
|
||||
|
||||
// Typed node data
|
||||
|
||||
export interface DocumentNodeData {
|
||||
|
|
@ -100,7 +102,7 @@ export interface GraphEdge {
|
|||
opacity: number
|
||||
thickness: number
|
||||
}
|
||||
edgeType: MemoryRelation
|
||||
edgeType: GraphEdgeType
|
||||
}
|
||||
|
||||
export interface GraphThemeColors {
|
||||
|
|
@ -116,6 +118,7 @@ export interface GraphThemeColors {
|
|||
textPrimary: string
|
||||
textSecondary: string
|
||||
textMuted: string
|
||||
edgeDocument: string
|
||||
edgeDerives: string
|
||||
edgeUpdates: string
|
||||
edgeExtends: string
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue