mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-28 05:25:33 +00:00
feat(memory-graph): configurable labels + popover z-index (#1201)
This commit is contained in:
parent
9487a928f6
commit
d77b6eb6d9
10 changed files with 437 additions and 79 deletions
154
packages/memory-graph/src/__tests__/labels-layering.test.ts
Normal file
154
packages/memory-graph/src/__tests__/labels-layering.test.ts
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
/**
|
||||
* Tests for configurable labels and hover popover layering.
|
||||
*
|
||||
* Follows the same pattern as node-hover-popover.test.tsx: source-code
|
||||
* assertions plus pure-logic tests on the exported defaults and merge
|
||||
* behaviour, since hook-bearing components can't be mounted in this
|
||||
* workspace's vitest environment.
|
||||
*/
|
||||
|
||||
import { readFileSync } from "node:fs"
|
||||
import { resolve } from "node:path"
|
||||
import { describe, expect, it } from "vitest"
|
||||
import { DEFAULT_HOVER_POPOVER_Z_INDEX, DEFAULT_LABELS } from "../constants"
|
||||
import type { MemoryGraphLabels, ResolvedMemoryGraphLabels } from "../types"
|
||||
|
||||
const popoverSrc = readFileSync(
|
||||
resolve(__dirname, "../components/node-hover-popover.tsx"),
|
||||
"utf-8",
|
||||
)
|
||||
const legendSrc = readFileSync(
|
||||
resolve(__dirname, "../components/legend.tsx"),
|
||||
"utf-8",
|
||||
)
|
||||
const graphSrc = readFileSync(
|
||||
resolve(__dirname, "../components/memory-graph.tsx"),
|
||||
"utf-8",
|
||||
)
|
||||
const indexSrc = readFileSync(resolve(__dirname, "../index.tsx"), "utf-8")
|
||||
const loadingSrc = readFileSync(
|
||||
resolve(__dirname, "../components/loading-indicator.tsx"),
|
||||
"utf-8",
|
||||
)
|
||||
|
||||
describe("DEFAULT_LABELS — preserves original copy", () => {
|
||||
it("matches every previously hardcoded string", () => {
|
||||
expect(DEFAULT_LABELS.documentGroup).toBe("Documents")
|
||||
expect(DEFAULT_LABELS.documentTypeFallback).toBe("document")
|
||||
expect(DEFAULT_LABELS.documentSourceEdge).toBe("Document source")
|
||||
expect(DEFAULT_LABELS.documentToMemoryEdge).toBe("Document to memory")
|
||||
expect(DEFAULT_LABELS.documentIdLabel).toBe("Document")
|
||||
expect(DEFAULT_LABELS.viewDocument).toBe("View document")
|
||||
expect(DEFAULT_LABELS.goToDocument).toBe("Go to document")
|
||||
expect(DEFAULT_LABELS.nextDocument).toBe("Next document")
|
||||
expect(DEFAULT_LABELS.previousDocument).toBe("Prev document")
|
||||
expect(DEFAULT_LABELS.showMemory).toBe("Go to memory")
|
||||
})
|
||||
|
||||
it("memoryCount formats counts like the original template", () => {
|
||||
expect(DEFAULT_LABELS.memoryCount(0)).toBe("0 memories")
|
||||
expect(DEFAULT_LABELS.memoryCount(5)).toBe("5 memories")
|
||||
})
|
||||
|
||||
it("loadingMoreDocuments formats counts like the original template", () => {
|
||||
expect(DEFAULT_LABELS.loadingMoreDocuments(12)).toBe(
|
||||
"Loading more documents... (12)",
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe("label merge — partial overrides win, defaults fill the rest", () => {
|
||||
it("spread merge overrides only the provided keys", () => {
|
||||
const overrides: MemoryGraphLabels = {
|
||||
documentGroup: "Sources",
|
||||
viewDocument: "View source",
|
||||
}
|
||||
const resolved: ResolvedMemoryGraphLabels = {
|
||||
...DEFAULT_LABELS,
|
||||
...overrides,
|
||||
}
|
||||
expect(resolved.documentGroup).toBe("Sources")
|
||||
expect(resolved.viewDocument).toBe("View source")
|
||||
expect(resolved.nextDocument).toBe("Next document")
|
||||
expect(resolved.memoryCount(3)).toBe("3 memories")
|
||||
})
|
||||
|
||||
it("custom memoryCount function replaces the default", () => {
|
||||
const resolved: ResolvedMemoryGraphLabels = {
|
||||
...DEFAULT_LABELS,
|
||||
memoryCount: (count) => `${count} facts`,
|
||||
}
|
||||
expect(resolved.memoryCount(2)).toBe("2 facts")
|
||||
})
|
||||
})
|
||||
|
||||
describe("hover popover layering", () => {
|
||||
it("default z-index stays at the previous hardcoded value", () => {
|
||||
expect(DEFAULT_HOVER_POPOVER_Z_INDEX).toBe(100)
|
||||
})
|
||||
|
||||
it("popover overlay uses the zIndex prop, not a literal", () => {
|
||||
expect(popoverSrc).not.toContain("zIndex: 100")
|
||||
const overlayIdx = popoverSrc.indexOf("const overlayStyle")
|
||||
const overlayEnd = popoverSrc.indexOf("}", overlayIdx)
|
||||
expect(popoverSrc.slice(overlayIdx, overlayEnd)).toContain("zIndex,")
|
||||
})
|
||||
|
||||
it("popover defaults zIndex to DEFAULT_HOVER_POPOVER_Z_INDEX", () => {
|
||||
expect(popoverSrc).toContain("zIndex = DEFAULT_HOVER_POPOVER_Z_INDEX")
|
||||
})
|
||||
|
||||
it("MemoryGraph resolves layering.hoverPopoverZIndex with fallback", () => {
|
||||
expect(graphSrc).toContain(
|
||||
"layering?.hoverPopoverZIndex ?? DEFAULT_HOVER_POPOVER_Z_INDEX",
|
||||
)
|
||||
expect(graphSrc).toContain("zIndex={hoverPopoverZIndex}")
|
||||
})
|
||||
})
|
||||
|
||||
describe("no user-facing document strings remain hardcoded", () => {
|
||||
it("popover uses labels for all document-facing copy", () => {
|
||||
expect(popoverSrc).not.toContain('"View document"')
|
||||
expect(popoverSrc).not.toContain('"Go to document"')
|
||||
expect(popoverSrc).not.toContain('"Next document"')
|
||||
expect(popoverSrc).not.toContain('"Prev document"')
|
||||
expect(popoverSrc).not.toContain('label="Document"')
|
||||
expect(popoverSrc).not.toContain('|| "document"')
|
||||
expect(popoverSrc).toContain("labels.documentTypeFallback")
|
||||
expect(popoverSrc).toContain("labels.memoryCount(")
|
||||
expect(popoverSrc).toContain("labels.documentIdLabel")
|
||||
expect(popoverSrc).toContain("labels.viewDocument")
|
||||
expect(popoverSrc).toContain("labels.goToDocument")
|
||||
expect(popoverSrc).toContain("labels.showMemory")
|
||||
expect(popoverSrc).toContain("labels.nextDocument")
|
||||
expect(popoverSrc).toContain("labels.previousDocument")
|
||||
})
|
||||
|
||||
it("legend uses labels for group and edge copy", () => {
|
||||
expect(legendSrc).not.toContain('label="Documents"')
|
||||
expect(legendSrc).not.toContain("Document source</span>")
|
||||
expect(legendSrc).not.toContain("Document to memory")
|
||||
expect(legendSrc).toContain("labels.documentGroup")
|
||||
expect(legendSrc).toContain("labels.documentSourceEdge")
|
||||
expect(legendSrc).toContain("labels.documentToMemoryEdge")
|
||||
})
|
||||
|
||||
it("loading indicator uses labels for the loading-more copy", () => {
|
||||
expect(loadingSrc).not.toContain("Loading more documents")
|
||||
expect(loadingSrc).toContain("labels.loadingMoreDocuments(totalLoaded)")
|
||||
})
|
||||
|
||||
it("MemoryGraph merges label overrides and passes them down", () => {
|
||||
expect(graphSrc).toContain("{ ...DEFAULT_LABELS, ...labelOverrides }")
|
||||
expect(graphSrc).toContain("labels={resolvedLabels}")
|
||||
})
|
||||
})
|
||||
|
||||
describe("public API exports", () => {
|
||||
it("index exports the new constants and types", () => {
|
||||
expect(indexSrc).toContain("DEFAULT_LABELS")
|
||||
expect(indexSrc).toContain("DEFAULT_HOVER_POPOVER_Z_INDEX")
|
||||
expect(indexSrc).toContain("MemoryGraphLabels")
|
||||
expect(indexSrc).toContain("MemoryGraphLayering")
|
||||
})
|
||||
})
|
||||
|
|
@ -209,8 +209,8 @@ describe("'View document' button — render guard in JSX", () => {
|
|||
expect(src).toContain("onClick={() => onOpenDocument(documentId)}")
|
||||
})
|
||||
|
||||
it("button label text is 'View document'", () => {
|
||||
expect(src).toContain('label="View document"')
|
||||
it("button label comes from configurable labels", () => {
|
||||
expect(src).toContain("label={labels.viewDocument}")
|
||||
})
|
||||
|
||||
it("button icon is the EyeIcon component (not a string shortcut key)", () => {
|
||||
|
|
|
|||
101
packages/memory-graph/src/__tests__/popover-render.e2e.test.tsx
Normal file
101
packages/memory-graph/src/__tests__/popover-render.e2e.test.tsx
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
/**
|
||||
* Mounted-render verification for configurable labels and popover layering.
|
||||
*/
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { cleanup, render } from "@testing-library/react"
|
||||
import { afterEach, describe, expect, it } from "vitest"
|
||||
|
||||
afterEach(cleanup)
|
||||
import { NodeHoverPopover } from "../components/node-hover-popover"
|
||||
import { DEFAULT_COLORS, DEFAULT_LABELS } from "../constants"
|
||||
import type { GraphNode, ResolvedMemoryGraphLabels } from "../types"
|
||||
|
||||
const documentNode: GraphNode = {
|
||||
id: "doc-1",
|
||||
type: "document",
|
||||
x: 0,
|
||||
y: 0,
|
||||
data: {
|
||||
id: "doc-1",
|
||||
title: "Doc title",
|
||||
summary: "Doc summary",
|
||||
type: "",
|
||||
createdAt: "2026-01-01",
|
||||
updatedAt: "2026-01-01",
|
||||
memories: [],
|
||||
},
|
||||
size: 40,
|
||||
borderColor: "#fff",
|
||||
isHovered: false,
|
||||
isDragging: false,
|
||||
}
|
||||
|
||||
function renderPopover(
|
||||
labels?: ResolvedMemoryGraphLabels,
|
||||
zIndex?: number,
|
||||
onOpenDocument?: (id: string) => void,
|
||||
) {
|
||||
return render(
|
||||
<NodeHoverPopover
|
||||
colors={DEFAULT_COLORS}
|
||||
labels={labels}
|
||||
node={documentNode}
|
||||
nodeRadius={20}
|
||||
screenX={100}
|
||||
screenY={100}
|
||||
zIndex={zIndex}
|
||||
onOpenDocument={onOpenDocument}
|
||||
/>,
|
||||
)
|
||||
}
|
||||
|
||||
describe("NodeHoverPopover mounted render", () => {
|
||||
it("renders default document copy when no labels are passed", () => {
|
||||
const { container, getByText } = renderPopover(
|
||||
undefined,
|
||||
undefined,
|
||||
() => {},
|
||||
)
|
||||
getByText("View document")
|
||||
getByText("Go to memory")
|
||||
getByText("Next document")
|
||||
getByText("Prev document")
|
||||
getByText("document")
|
||||
getByText("Document")
|
||||
getByText("0 memories")
|
||||
const overlay = container.firstChild as HTMLElement
|
||||
expect(overlay.style.zIndex).toBe("100")
|
||||
})
|
||||
|
||||
it("renders custom source-facing copy and custom z-index", () => {
|
||||
const labels: ResolvedMemoryGraphLabels = {
|
||||
...DEFAULT_LABELS,
|
||||
documentGroup: "Sources",
|
||||
documentTypeFallback: "source",
|
||||
documentIdLabel: "Source",
|
||||
viewDocument: "View source",
|
||||
goToDocument: "Go to source",
|
||||
nextDocument: "Next source",
|
||||
previousDocument: "Prev source",
|
||||
showMemory: "Show memory",
|
||||
memoryCount: (count) => `${count} facts`,
|
||||
}
|
||||
const { container, getByText, queryByText } = renderPopover(
|
||||
labels,
|
||||
30,
|
||||
() => {},
|
||||
)
|
||||
getByText("View source")
|
||||
getByText("Show memory")
|
||||
getByText("Next source")
|
||||
getByText("Prev source")
|
||||
getByText("source")
|
||||
getByText("Source")
|
||||
getByText("0 facts")
|
||||
expect(queryByText("View document")).toBeNull()
|
||||
const overlay = container.firstChild as HTMLElement
|
||||
expect(overlay.style.zIndex).toBe("30")
|
||||
})
|
||||
})
|
||||
|
|
@ -1,11 +1,18 @@
|
|||
import { memo, useState } from "react"
|
||||
import type { GraphEdge, GraphNode, GraphThemeColors } from "../types"
|
||||
import { DEFAULT_LABELS } from "../constants"
|
||||
import type {
|
||||
GraphEdge,
|
||||
GraphNode,
|
||||
GraphThemeColors,
|
||||
ResolvedMemoryGraphLabels,
|
||||
} from "../types"
|
||||
|
||||
interface LegendProps {
|
||||
nodes?: GraphNode[]
|
||||
edges?: GraphEdge[]
|
||||
isLoading?: boolean
|
||||
colors: GraphThemeColors
|
||||
labels?: ResolvedMemoryGraphLabels
|
||||
hoveredNode?: string | null
|
||||
compact?: boolean
|
||||
maxHeight?: number
|
||||
|
|
@ -291,6 +298,7 @@ export const Legend = memo(function Legend({
|
|||
edges = [],
|
||||
isLoading: _isLoading = false,
|
||||
colors,
|
||||
labels = DEFAULT_LABELS,
|
||||
hoveredNode,
|
||||
compact = false,
|
||||
maxHeight,
|
||||
|
|
@ -462,7 +470,7 @@ export const Legend = memo(function Legend({
|
|||
}}
|
||||
/>
|
||||
}
|
||||
label="Documents"
|
||||
label={labels.documentGroup}
|
||||
colors={colors}
|
||||
/>
|
||||
<StatRow
|
||||
|
|
@ -523,12 +531,14 @@ export const Legend = memo(function Legend({
|
|||
<div style={rowStyle}>
|
||||
<div style={rowLeftStyle}>
|
||||
<LineIcon color={colors.edgeDerives} />
|
||||
<span style={edgeLabelStyle}>Document source</span>
|
||||
<span style={edgeLabelStyle}>
|
||||
{labels.documentSourceEdge}
|
||||
</span>
|
||||
</div>
|
||||
<span style={countStyle}>{derivesCount}</span>
|
||||
</div>
|
||||
<div style={edgeDescriptionStyle}>
|
||||
Document to memory
|
||||
{labels.documentToMemoryEdge}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,10 @@
|
|||
import { memo } from "react"
|
||||
import type { GraphThemeColors, LoadingIndicatorProps } from "../types"
|
||||
import { DEFAULT_LABELS } from "../constants"
|
||||
import type {
|
||||
GraphThemeColors,
|
||||
LoadingIndicatorProps,
|
||||
ResolvedMemoryGraphLabels,
|
||||
} from "../types"
|
||||
|
||||
const spinKeyframes = `
|
||||
@keyframes mg-spin {
|
||||
|
|
@ -18,70 +23,82 @@ function injectSpinStyle() {
|
|||
}
|
||||
|
||||
export const LoadingIndicator = memo<
|
||||
LoadingIndicatorProps & { colors?: GraphThemeColors }
|
||||
>(({ isLoading, isLoadingMore, totalLoaded, colors }) => {
|
||||
if (!isLoading && !isLoadingMore) return null
|
||||
|
||||
injectSpinStyle()
|
||||
|
||||
const containerStyle: React.CSSProperties = {
|
||||
position: "absolute",
|
||||
zIndex: 30,
|
||||
overflow: "hidden",
|
||||
top: 16,
|
||||
left: 16,
|
||||
borderRadius: 12,
|
||||
border: `1px solid ${colors?.controlBorder ?? "#2A2F36"}`,
|
||||
backgroundColor: colors?.controlBg ?? "#1a1f29",
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
paddingTop: 12,
|
||||
paddingBottom: 12,
|
||||
boxShadow: "0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1)",
|
||||
LoadingIndicatorProps & {
|
||||
colors?: GraphThemeColors
|
||||
labels?: ResolvedMemoryGraphLabels
|
||||
}
|
||||
>(
|
||||
({
|
||||
isLoading,
|
||||
isLoadingMore,
|
||||
totalLoaded,
|
||||
colors,
|
||||
labels = DEFAULT_LABELS,
|
||||
}) => {
|
||||
if (!isLoading && !isLoadingMore) return null
|
||||
|
||||
const flexStyle: React.CSSProperties = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
}
|
||||
injectSpinStyle()
|
||||
|
||||
const spinnerStyle: React.CSSProperties = {
|
||||
width: 16,
|
||||
height: 16,
|
||||
animation: "mg-spin 1s linear infinite",
|
||||
color: colors?.accent ?? "#3B73B8",
|
||||
flexShrink: 0,
|
||||
}
|
||||
const containerStyle: React.CSSProperties = {
|
||||
position: "absolute",
|
||||
zIndex: 30,
|
||||
overflow: "hidden",
|
||||
top: 16,
|
||||
left: 16,
|
||||
borderRadius: 12,
|
||||
border: `1px solid ${colors?.controlBorder ?? "#2A2F36"}`,
|
||||
backgroundColor: colors?.controlBg ?? "#1a1f29",
|
||||
paddingLeft: 16,
|
||||
paddingRight: 16,
|
||||
paddingTop: 12,
|
||||
paddingBottom: 12,
|
||||
boxShadow:
|
||||
"0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1)",
|
||||
}
|
||||
|
||||
const textStyle: React.CSSProperties = {
|
||||
fontSize: 14,
|
||||
color: colors?.textSecondary ?? "#e2e8f0",
|
||||
}
|
||||
const flexStyle: React.CSSProperties = {
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 8,
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<div style={flexStyle}>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
role="img"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
style={spinnerStyle}
|
||||
>
|
||||
<title>Loading</title>
|
||||
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" />
|
||||
</svg>
|
||||
<span style={textStyle}>
|
||||
{isLoading
|
||||
? "Loading memory graph..."
|
||||
: `Loading more documents... (${totalLoaded})`}
|
||||
</span>
|
||||
const spinnerStyle: React.CSSProperties = {
|
||||
width: 16,
|
||||
height: 16,
|
||||
animation: "mg-spin 1s linear infinite",
|
||||
color: colors?.accent ?? "#3B73B8",
|
||||
flexShrink: 0,
|
||||
}
|
||||
|
||||
const textStyle: React.CSSProperties = {
|
||||
fontSize: 14,
|
||||
color: colors?.textSecondary ?? "#e2e8f0",
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={containerStyle}>
|
||||
<div style={flexStyle}>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="none"
|
||||
role="img"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
viewBox="0 0 24 24"
|
||||
style={spinnerStyle}
|
||||
>
|
||||
<title>Loading</title>
|
||||
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83" />
|
||||
</svg>
|
||||
<span style={textStyle}>
|
||||
{isLoading
|
||||
? "Loading memory graph..."
|
||||
: labels.loadingMoreDocuments(totalLoaded)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
LoadingIndicator.displayName = "LoadingIndicator"
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import {
|
|||
DENSE_GRAPH_STATIC_THRESHOLD,
|
||||
ForceSimulation,
|
||||
} from "../canvas/simulation"
|
||||
import { DEFAULT_HOVER_POPOVER_Z_INDEX, DEFAULT_LABELS } from "../constants"
|
||||
import { VersionChainIndex } from "../canvas/version-chain"
|
||||
import type { ViewportState } from "../canvas/viewport"
|
||||
import { useGraphData } from "../hooks/use-graph-data"
|
||||
|
|
@ -11,6 +12,7 @@ import type {
|
|||
GraphApiDocument,
|
||||
GraphThemeColors,
|
||||
MemoryGraphProps,
|
||||
ResolvedMemoryGraphLabels,
|
||||
} from "../types"
|
||||
import { GraphCanvas } from "./graph-canvas"
|
||||
import { Legend } from "./legend"
|
||||
|
|
@ -37,7 +39,15 @@ export function MemoryGraph({
|
|||
colors: colorOverrides,
|
||||
totalCount,
|
||||
onOpenDocument,
|
||||
labels: labelOverrides,
|
||||
layering,
|
||||
}: MemoryGraphProps) {
|
||||
const resolvedLabels = useMemo<ResolvedMemoryGraphLabels>(
|
||||
() => ({ ...DEFAULT_LABELS, ...labelOverrides }),
|
||||
[labelOverrides],
|
||||
)
|
||||
const hoverPopoverZIndex =
|
||||
layering?.hoverPopoverZIndex ?? DEFAULT_HOVER_POPOVER_Z_INDEX
|
||||
const resolvedColors = useGraphTheme(colorOverrides)
|
||||
const colors = useMemo<GraphThemeColors>(
|
||||
() =>
|
||||
|
|
@ -397,8 +407,8 @@ export function MemoryGraph({
|
|||
}, [containerSize.width, graphFitHeight])
|
||||
|
||||
// Wrap onOpenDocument to dismiss the popover before opening the modal.
|
||||
// Without this, the popover (z-index: 100) stays mounted on top of the
|
||||
// document modal (z-50), obscuring it and intercepting clicks.
|
||||
// Without this, the popover overlay stays mounted on top of the
|
||||
// document modal, obscuring it and intercepting clicks.
|
||||
const handleOpenDocument = useCallback(
|
||||
(documentId: string) => {
|
||||
setSelectedNode(null)
|
||||
|
|
@ -748,6 +758,7 @@ export function MemoryGraph({
|
|||
<LoadingIndicator
|
||||
isLoading={isLoading}
|
||||
isLoadingMore={isLoadingMore}
|
||||
labels={resolvedLabels}
|
||||
totalLoaded={totalCount ?? documents.length}
|
||||
colors={colors}
|
||||
/>
|
||||
|
|
@ -783,7 +794,9 @@ export function MemoryGraph({
|
|||
<NodeHoverPopover
|
||||
colors={colors}
|
||||
containerBounds={containerBounds ?? undefined}
|
||||
labels={resolvedLabels}
|
||||
node={activeNodeData}
|
||||
zIndex={hoverPopoverZIndex}
|
||||
nodeRadius={activePopoverPosition.nodeRadius}
|
||||
onNavigateDown={navigateDown}
|
||||
onNavigateNext={navigateNext}
|
||||
|
|
@ -812,6 +825,7 @@ export function MemoryGraph({
|
|||
<Legend
|
||||
colors={colors}
|
||||
edges={edges}
|
||||
labels={resolvedLabels}
|
||||
hoveredNode={hoveredNode}
|
||||
compact={isCompactViewport}
|
||||
isLoading={isLoading}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||
import type { ChainEntry } from "../canvas/version-chain"
|
||||
import { DEFAULT_HOVER_POPOVER_Z_INDEX, DEFAULT_LABELS } from "../constants"
|
||||
import type {
|
||||
DocumentNodeData,
|
||||
GraphNode,
|
||||
GraphThemeColors,
|
||||
MemoryNodeData,
|
||||
ResolvedMemoryGraphLabels,
|
||||
} from "../types"
|
||||
|
||||
export interface NodeHoverPopoverProps {
|
||||
|
|
@ -15,6 +17,8 @@ export interface NodeHoverPopoverProps {
|
|||
containerBounds?: DOMRect
|
||||
versionChain?: ChainEntry[] | null
|
||||
colors: GraphThemeColors
|
||||
labels?: ResolvedMemoryGraphLabels
|
||||
zIndex?: number
|
||||
onNavigateNext?: () => void
|
||||
onNavigatePrev?: () => void
|
||||
onNavigateUp?: () => void
|
||||
|
|
@ -310,6 +314,8 @@ export const NodeHoverPopover = memo<NodeHoverPopoverProps>(
|
|||
containerBounds,
|
||||
versionChain,
|
||||
colors,
|
||||
labels = DEFAULT_LABELS,
|
||||
zIndex = DEFAULT_HOVER_POPOVER_Z_INDEX,
|
||||
onNavigateNext,
|
||||
onNavigatePrev,
|
||||
onNavigateUp,
|
||||
|
|
@ -419,7 +425,7 @@ export const NodeHoverPopover = memo<NodeHoverPopoverProps>(
|
|||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
zIndex: 100,
|
||||
zIndex,
|
||||
}
|
||||
|
||||
const svgStyle: React.CSSProperties = {
|
||||
|
|
@ -605,7 +611,7 @@ export const NodeHoverPopover = memo<NodeHoverPopoverProps>(
|
|||
color: colors.popoverTextSecondary,
|
||||
}}
|
||||
>
|
||||
{docData?.type || "document"}
|
||||
{docData?.type || labels.documentTypeFallback}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
|
|
@ -613,7 +619,7 @@ export const NodeHoverPopover = memo<NodeHoverPopoverProps>(
|
|||
color: colors.popoverTextSecondary,
|
||||
}}
|
||||
>
|
||||
{docData?.memories?.length ?? 0} memories
|
||||
{labels.memoryCount(docData?.memories?.length ?? 0)}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
|
|
@ -623,7 +629,11 @@ export const NodeHoverPopover = memo<NodeHoverPopoverProps>(
|
|||
{isMemory ? (
|
||||
<CopyableId colors={colors} label="Memory" value={node.id} />
|
||||
) : (
|
||||
<CopyableId colors={colors} label="Document" value={node.id} />
|
||||
<CopyableId
|
||||
colors={colors}
|
||||
label={labels.documentIdLabel}
|
||||
value={node.id}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -633,7 +643,7 @@ export const NodeHoverPopover = memo<NodeHoverPopoverProps>(
|
|||
<NavButton
|
||||
colors={colors}
|
||||
icon={<EyeIcon color={colors.popoverTextMuted} />}
|
||||
label="View document"
|
||||
label={labels.viewDocument}
|
||||
onClick={() => onOpenDocument(documentId)}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -641,7 +651,7 @@ export const NodeHoverPopover = memo<NodeHoverPopoverProps>(
|
|||
<NavButton
|
||||
colors={colors}
|
||||
icon="↑"
|
||||
label={hasChain ? "Older version" : "Go to document"}
|
||||
label={hasChain ? "Older version" : labels.goToDocument}
|
||||
onClick={onNavigateUp}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -649,20 +659,20 @@ export const NodeHoverPopover = memo<NodeHoverPopoverProps>(
|
|||
<NavButton
|
||||
colors={colors}
|
||||
icon="↓"
|
||||
label={isMemory ? "Newer version" : "Go to memory"}
|
||||
label={isMemory ? "Newer version" : labels.showMemory}
|
||||
onClick={onNavigateDown}
|
||||
/>
|
||||
)}
|
||||
<NavButton
|
||||
colors={colors}
|
||||
icon="→"
|
||||
label={isMemory ? "Next memory" : "Next document"}
|
||||
label={isMemory ? "Next memory" : labels.nextDocument}
|
||||
onClick={onNavigateNext}
|
||||
/>
|
||||
<NavButton
|
||||
colors={colors}
|
||||
icon="←"
|
||||
label={isMemory ? "Prev memory" : "Prev document"}
|
||||
label={isMemory ? "Prev memory" : labels.previousDocument}
|
||||
onClick={onNavigatePrev}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,22 @@
|
|||
import type { GraphThemeColors } from "./types"
|
||||
import type { GraphThemeColors, ResolvedMemoryGraphLabels } from "./types"
|
||||
|
||||
export const DEFAULT_LABELS: ResolvedMemoryGraphLabels = {
|
||||
documentGroup: "Documents",
|
||||
documentTypeFallback: "document",
|
||||
documentSourceEdge: "Document source",
|
||||
documentToMemoryEdge: "Document to memory",
|
||||
memoryCount: (count: number) => `${count} memories`,
|
||||
documentIdLabel: "Document",
|
||||
viewDocument: "View document",
|
||||
goToDocument: "Go to document",
|
||||
nextDocument: "Next document",
|
||||
previousDocument: "Prev document",
|
||||
showMemory: "Go to memory",
|
||||
loadingMoreDocuments: (count: number) =>
|
||||
`Loading more documents... (${count})`,
|
||||
}
|
||||
|
||||
export const DEFAULT_HOVER_POPOVER_Z_INDEX = 100
|
||||
|
||||
export const MEMORY_BORDER_KEYS = {
|
||||
forgotten: "memBorderForgotten",
|
||||
|
|
|
|||
|
|
@ -13,11 +13,20 @@ export { SpatialIndex } from "./canvas/hit-test"
|
|||
export { VersionChainIndex } from "./canvas/version-chain"
|
||||
|
||||
// Constants
|
||||
export { DEFAULT_COLORS, FORCE_CONFIG, GRAPH_SETTINGS } from "./constants"
|
||||
export {
|
||||
DEFAULT_COLORS,
|
||||
DEFAULT_HOVER_POPOVER_Z_INDEX,
|
||||
DEFAULT_LABELS,
|
||||
FORCE_CONFIG,
|
||||
GRAPH_SETTINGS,
|
||||
} from "./constants"
|
||||
|
||||
// Types
|
||||
export type {
|
||||
MemoryGraphProps,
|
||||
MemoryGraphLabels,
|
||||
MemoryGraphLayering,
|
||||
ResolvedMemoryGraphLabels,
|
||||
GraphNode,
|
||||
GraphEdge,
|
||||
GraphThemeColors,
|
||||
|
|
|
|||
|
|
@ -132,6 +132,27 @@ export interface GraphThemeColors {
|
|||
controlBorder: string
|
||||
}
|
||||
|
||||
export interface MemoryGraphLabels {
|
||||
documentGroup?: string
|
||||
documentTypeFallback?: string
|
||||
documentSourceEdge?: string
|
||||
documentToMemoryEdge?: string
|
||||
memoryCount?: (count: number) => string
|
||||
documentIdLabel?: string
|
||||
viewDocument?: string
|
||||
goToDocument?: string
|
||||
nextDocument?: string
|
||||
previousDocument?: string
|
||||
showMemory?: string
|
||||
loadingMoreDocuments?: (count: number) => string
|
||||
}
|
||||
|
||||
export type ResolvedMemoryGraphLabels = Required<MemoryGraphLabels>
|
||||
|
||||
export interface MemoryGraphLayering {
|
||||
hoverPopoverZIndex?: number
|
||||
}
|
||||
|
||||
export interface GraphCanvasProps {
|
||||
nodes: GraphNode[]
|
||||
edges: GraphEdge[]
|
||||
|
|
@ -195,6 +216,10 @@ export interface MemoryGraphProps {
|
|||
totalCount?: number
|
||||
/** Callback when user wants to view full document content */
|
||||
onOpenDocument?: (documentId: string) => void
|
||||
/** Custom user-facing labels (partial) - merged with defaults */
|
||||
labels?: MemoryGraphLabels
|
||||
/** Overlay layering controls */
|
||||
layering?: MemoryGraphLayering
|
||||
}
|
||||
|
||||
export interface ChainEntry {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue