mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-01 02:01:40 +00:00
Merge 7a854a8a5e into cfa6c7cb17
This commit is contained in:
commit
1a23701057
2 changed files with 122 additions and 37 deletions
42
packages/memory-graph/src/__tests__/legend.test.tsx
Normal file
42
packages/memory-graph/src/__tests__/legend.test.tsx
Normal 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")
|
||||
})
|
||||
})
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue