Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 3 additions & 5 deletions frontend/src/features/canvas/Canvas.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
justify-content: center;
align-items: flex-start;
padding: 12px 0px;
height: 100vh;
height: 100%;
background-color: var(--bg-tertiary);
box-sizing: border-box;
transition: background-color 200ms ease;
position: relative;
}

/* Main SVG canvas */
Expand All @@ -16,10 +17,7 @@
height: 100%;
display: block;
background: var(--canvas-bg);
border: 1px solid var(--border-primary);
border-radius: 10px;
box-shadow: var(--shadow-sm);
transition: background-color 200ms ease, border-color 200ms ease;
transition: background-color 200ms ease;
}

/* Override memory-viz box background fills for light mode */
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/features/canvas/Canvas.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -675,7 +675,7 @@ function Canvas({
className={styles.canvas}
style={{
width: "100%",
height: canvasHeight ?? undefined,
minHeight: canvasHeight ?? undefined,
display: "block",
padding: 0,
border: 0,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
.arrowFill {
fill: var(--text-primary);
}

.diagramScroller {
overflow-x: auto;
overflow-y: hidden;
}

.diagram {
display: block;
width: auto;
min-width: 100%;
height: auto;
max-width: none;
}

.label {
fill: var(--text-primary);
font-size: 13px;
font-weight: 600;
font-family: var(--font-mono, ui-monospace, monospace);
}

.labelArrow {
stroke: var(--text-primary);
stroke-width: 1;
}

.nextLabel {
fill: var(--text-primary);
font-size: 11px;
font-family: var(--font-mono, ui-monospace, monospace);
}

.nodeRect {
fill: var(--bg-primary);
stroke: var(--text-primary);
stroke-width: 1.5;
}

.divider {
stroke: var(--text-primary);
stroke-width: 1.5;
}

.dot {
fill: var(--text-primary);
}

.nodeValue {
fill: var(--text-primary);
font-size: 15px;
font-weight: 500;
font-family: var(--font-mono, ui-monospace, monospace);
}

.linkLine {
stroke: var(--text-primary);
stroke-width: 1.5;
}

.arrowHead {
fill: var(--text-primary);
}

.cyclePath {
fill: none;
stroke: var(--accent-blue);
stroke-width: 2;
stroke-dasharray: 5 4;
}

.footerText {
fill: var(--text-tertiary);
font-size: 12px;
font-style: italic;
}

.list {
display: block;
width: 100%;
}
183 changes: 183 additions & 0 deletions frontend/src/features/canvas/components/LinkedListPreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import React, { useEffect, useId, useRef, useState } from "react";
import styles from "./LinkedListPreview.module.css";
import { LinkedListGraph } from "../utils/linkedListDetector";
import {
buildLinkedListGraphLayout,
getRectPerimeterPoint,
createEdgePath,
} from "../utils/linkedListRenderer";

interface LinkedListPreviewProps {
graph: LinkedListGraph;
}

export default function LinkedListPreview({ graph }: LinkedListPreviewProps) {
const containerRef = useRef<HTMLDivElement>(null);
const [containerWidth, setContainerWidth] = useState(600);
const rawId = useId();
const markerId = `ll-arrow-${rawId.replace(/:/g, "")}`;

useEffect(() => {
const el = containerRef.current;
if (!el) return;
const ro = new ResizeObserver(([entry]) => {
const w = entry.contentRect.width;
if (w > 0) setContainerWidth(w);
});
ro.observe(el);
return () => ro.disconnect();
}, []);

if (graph.nodes.length === 0) {
return null;
}

const layout = buildLinkedListGraphLayout(graph, containerWidth);

return (
<div className={styles.list} ref={containerRef}>
<svg
className={styles.diagram}
width={layout.width}
height={layout.height}
viewBox={`0 0 ${layout.width} ${layout.height}`}
role="img"
aria-label="Linked list visualization"
preserveAspectRatio="xMinYMin meet"
>
<defs>
<marker
id={markerId}
viewBox="0 0 10 10"
refX="9"
refY="5"
markerWidth="9"
markerHeight="9"
orient="auto-start-reverse"
markerUnits="userSpaceOnUse"
>
<path d="M0 0 L10 5 L0 10 z" className={styles.arrowFill} />
</marker>
</defs>

{layout.edges.map((edge) => {
const from = layout.byId[edge.fromId];
const to = layout.byId[edge.toId];
if (!from || !to) return null;

const start = { x: from.dotX, y: from.dotY };
const end = getRectPerimeterPoint(
{ x: to.x, y: to.y, width: to.width, height: to.height },
start
);
const d = createEdgePath(start, end.point, end.side);

return (
<path
key={`${edge.fromId}-${edge.toId}`}
d={d}
fill="none"
className={styles.linkLine}
markerEnd={`url(#${markerId})`}
/>
);
})}

{layout.nodes.map((node) => (
<g key={node.nodeId}>
{node.labels.map((label, labelIndex) => {
const lx = node.x + node.width / 2;
const labelY =
node.y - 16 - (node.labels.length - 1 - labelIndex) * 14;
const isBottomLabel = labelIndex === node.labels.length - 1;

return (
<g key={`${node.nodeId}-${label}`}>
<text
x={lx}
y={labelY}
textAnchor="middle"
className={styles.label}
>
{label}
</text>
{isBottomLabel && (
<path
d={`M ${lx} ${labelY + 4} L ${lx} ${node.y - 1}`}
fill="none"
className={styles.linkLine}
markerEnd={`url(#${markerId})`}
/>
)}
</g>
);
})}

<rect
x={node.x}
y={node.y}
width={node.width}
height={node.height}
className={styles.nodeRect}
/>
<line
x1={node.dividerX}
y1={node.y}
x2={node.dividerX}
y2={node.y + node.height}
className={styles.divider}
/>
<text
x={(node.x + node.dividerX) / 2}
y={node.y + node.height / 2}
textAnchor="middle"
dominantBaseline="central"
className={styles.nodeValue}
>
{node.value}
</text>

{node.nextText === "•" ? (
<circle
cx={node.dotX}
cy={node.y + node.height / 2}
r={2.5}
className={styles.dot}
/>
) : node.nextText === "None" ? (
<>
<path
d={`M ${node.dotX} ${node.y + node.height / 2} L ${node.x + node.width + 22} ${node.y + node.height / 2}`}
fill="none"
className={styles.linkLine}
markerEnd={`url(#${markerId})`}
/>
<text
x={node.x + node.width + 28}
y={node.y + node.height / 2}
textAnchor="start"
dominantBaseline="central"
className={styles.nextLabel}
>
None
</text>
</>
) : (
<text
x={(node.dividerX + node.x + node.width) / 2}
y={node.y + node.height / 2}
textAnchor="middle"
dominantBaseline="central"
className={styles.nextLabel}
textLength={node.nextCellWidth - 6}
lengthAdjust="spacingAndGlyphs"
>
{node.nextText}
</text>
)}
</g>
))}
</svg>
</div>
);
}
Loading