This commit is contained in:
Mahesh Sanikommu 2026-09-26 20:20:52 +00:00 • committed by GitHub
commit 1a23701057
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 122 additions and 37 deletions

View file

@ -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(
<Legend colors={DEFAULT_COLORS} edges={[]} nodes={[]} />,
)
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(<Legend colors={DEFAULT_COLORS} edges={[]} nodes={[]} />)
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")
})
})

View file

@ -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 (
<svg
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke={color}
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
style={{ flexShrink: 0 }}
aria-hidden="true"
>
<path d="m9 18 6-6-6-6" />
</svg>
)
}
function StatRow({
icon,
label,
@ -241,6 +222,7 @@ function StatRow({
return (
<div style={{ display: "flex", flexDirection: "column" }}>
<button
aria-expanded={expandable ? expanded : undefined}
onClick={expandable ? onToggle : undefined}
style={buttonStyle}
type="button"
@ -248,22 +230,79 @@ function StatRow({
<div style={leftStyle}>
{icon}
<span style={labelStyle}>{label}</span>
{expandable &&
(expanded ? (
<ChevronDownIcon color={colors.textMuted} />
) : (
<ChevronRightIcon color={colors.textMuted} />
))}
{expandable && (
<LegendChevron color={colors.textMuted} expanded={expanded} />
)}
</div>
<span style={countStyle}>{count}</span>
</button>
{expandable && expanded && children && (
<div style={childrenContainerStyle}>{children}</div>
{expandable && children && (
<LegendReveal expanded={expanded}>
<div style={childrenContainerStyle}>{children}</div>
</LegendReveal>
)}
</div>
)
}
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 (
<span
className="mg-legend-chevron"
style={{ transform: expanded ? "rotate(0deg)" : "rotate(-90deg)" }}
>
<ChevronDownIcon color={color} />
</span>
)
}
function LegendReveal({
expanded,
children,
}: {
expanded: boolean
children: React.ReactNode
}) {
return (
<div className="mg-legend-reveal" data-expanded={expanded}>
<div style={{ minHeight: 0, overflow: "hidden" }}>{children}</div>
</div>
)
}
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({
<div style={cardStyle}>
<div style={{ padding: 12 }}>
<button
aria-expanded={isExpanded}
onClick={() => setIsExpanded(!isExpanded)}
style={headerBtnStyle}
type="button"
>
{isExpanded ? (
<ChevronDownIcon color={colors.textPrimary} />
) : (
<ChevronRightIcon color={colors.textPrimary} />
)}
<LegendChevron color={colors.textPrimary} expanded={isExpanded} />
<span style={headerTextStyle}>Legend</span>
</button>
{isExpanded && (
<LegendReveal expanded={isExpanded}>
<div style={expandedContentStyle}>
{/* Statistics section */}
<div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
@ -672,7 +715,7 @@ export const Legend = memo(function Legend({
</div>
</div>
</div>
)}
</LegendReveal>
</div>
</div>
</div>