diff --git a/packages/memory-graph/src/__tests__/legend.test.tsx b/packages/memory-graph/src/__tests__/legend.test.tsx
new file mode 100644
index 00000000..f9cafcb9
--- /dev/null
+++ b/packages/memory-graph/src/__tests__/legend.test.tsx
@@ -0,0 +1,42 @@
+import { cleanup, fireEvent, render, screen } from "@testing-library/react"
+import { afterEach, describe, expect, it } from "vitest"
+import { Legend } from "../components/legend"
+import { DEFAULT_COLORS } from "../constants"
+
+afterEach(cleanup)
+
+describe("Legend", () => {
+ it("keeps disclosure state and motion behavior in sync", () => {
+ const { container } = render(
+ ,
+ )
+
+ const legendButton = screen.getByRole("button", { name: "Legend" })
+ const legendReveal = container.querySelector(".mg-legend-reveal")
+
+ expect(legendButton.getAttribute("aria-expanded")).toBe("false")
+ expect(legendReveal?.getAttribute("data-expanded")).toBe("false")
+ expect(
+ document.getElementById("supermemory-graph-legend-motion")?.textContent,
+ ).toContain("prefers-reduced-motion")
+
+ fireEvent.click(legendButton)
+
+ expect(legendButton.getAttribute("aria-expanded")).toBe("true")
+ expect(legendReveal?.getAttribute("data-expanded")).toBe("true")
+ })
+
+ it("exposes the connections disclosure state", () => {
+ render()
+ fireEvent.click(screen.getByRole("button", { name: "Legend" }))
+
+ const connectionsButton = screen.getByRole("button", {
+ name: /Connections/,
+ })
+ expect(connectionsButton.getAttribute("aria-expanded")).toBe("true")
+
+ fireEvent.click(connectionsButton)
+
+ expect(connectionsButton.getAttribute("aria-expanded")).toBe("false")
+ })
+})
diff --git a/packages/memory-graph/src/components/legend.tsx b/packages/memory-graph/src/components/legend.tsx
index c65073f4..14a4d27a 100644
--- a/packages/memory-graph/src/components/legend.tsx
+++ b/packages/memory-graph/src/components/legend.tsx
@@ -1,4 +1,4 @@
-import { memo, useState } from "react"
+import { memo, useEffect, useState } from "react"
import { DEFAULT_LABELS } from "../constants"
import type {
GraphEdge,
@@ -161,25 +161,6 @@ function ChevronDownIcon({ color }: { color: string }) {
)
}
-function ChevronRightIcon({ color }: { color: string }) {
- return (
-
- )
-}
-
function StatRow({
icon,
label,
@@ -241,6 +222,7 @@ function StatRow({
return (
{count}
- {expandable && expanded && children && (
- {children}
+ {expandable && children && (
+
+ {children}
+
)}
)
}
+const LEGEND_MOTION_STYLE_ID = "supermemory-graph-legend-motion"
+const LEGEND_MOTION_STYLES = `
+.mg-legend-reveal {
+ display: grid;
+ grid-template-rows: 0fr;
+ opacity: 0;
+ transition:
+ grid-template-rows 200ms cubic-bezier(0.23, 1, 0.32, 1),
+ opacity 200ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+.mg-legend-reveal[data-expanded="true"] {
+ grid-template-rows: 1fr;
+ opacity: 1;
+}
+.mg-legend-chevron {
+ display: inline-flex;
+ flex-shrink: 0;
+ transition: transform 200ms cubic-bezier(0.23, 1, 0.32, 1);
+}
+@media (prefers-reduced-motion: reduce) {
+ .mg-legend-reveal,
+ .mg-legend-chevron {
+ transition: none;
+ }
+}
+`
+
+function LegendChevron({
+ color,
+ expanded,
+}: {
+ color: string
+ expanded: boolean
+}) {
+ return (
+
+
+
+ )
+}
+
+function LegendReveal({
+ expanded,
+ children,
+}: {
+ expanded: boolean
+ children: React.ReactNode
+}) {
+ return (
+
+ )
+}
+
function countEdgesByType(edges: GraphEdge[], edgeType: GraphEdge["edgeType"]) {
return edges.filter((edge) => edge.edgeType === edgeType).length
}
@@ -306,6 +345,14 @@ export const Legend = memo(function Legend({
const [isExpanded, setIsExpanded] = useState(false)
const [connectionsExpanded, setConnectionsExpanded] = useState(true)
+ useEffect(() => {
+ if (document.getElementById(LEGEND_MOTION_STYLE_ID)) return
+ const style = document.createElement("style")
+ style.id = LEGEND_MOTION_STYLE_ID
+ style.textContent = LEGEND_MOTION_STYLES
+ document.head.appendChild(style)
+ }, [])
+
const memoryCount = nodes.filter((n) => n.type === "memory").length
const documentCount = nodes.filter((n) => n.type === "document").length
const connectionCount = edges.length
@@ -324,7 +371,6 @@ export const Legend = memo(function Legend({
const hasClusterColors = nodes.some((node) => node.clusterColor)
const outerStyle: React.CSSProperties = {
- overflow: "hidden",
width: compact ? "min(214px, calc(100vw - 32px))" : 214,
maxWidth: "100%",
}
@@ -408,7 +454,7 @@ export const Legend = memo(function Legend({
}
const expandedContentStyle: React.CSSProperties = {
- marginTop: 16,
+ paddingTop: 16,
display: "flex",
flexDirection: "column",
gap: 16,
@@ -427,19 +473,16 @@ export const Legend = memo(function Legend({
- {isExpanded && (
+
{/* Statistics section */}
@@ -672,7 +715,7 @@ export const Legend = memo(function Legend({
- )}
+