+
+ {/* ── Expression input ──────────────────────────────────── */}
+
+
+
+ Expression
+
+
+ Enter to evaluate · Shift+Enter for newline
+
+
+
+
+
+ {/* ── Quick insert keys ─────────────────────────────────── */}
+
+
+
+ Insert
+
+
+
+
+ {visibleKeys.map((k) => (
+
+ ))}
+
+
+
+ {/* ── Variables ─────────────────────────────────────────── */}
+
+
+
+ Variables
+
+
+
+
+ {Object.keys(variables).length === 0 && !showAddVar && (
+
+ Define variables like x = 5 by evaluating assignments
+
+ )}
+
+
+ {Object.entries(variables).map(([name, val]) => (
+
+ insertAtCursor(name)}
+ title="Insert into expression"
+ >
+ {name}
+
+ =
+ {val}
+
+
+ ))}
+
+
+ {showAddVar && (
+
+
setNewVarName(e.target.value)}
+ placeholder="name"
+ className="w-16 bg-transparent border border-border/40 rounded px-2 py-1 text-xs font-mono outline-none focus:border-primary/50 transition-colors"
+ />
+
=
+
setNewVarValue(e.target.value)}
+ placeholder="value"
+ onKeyDown={(e) => e.key === 'Enter' && addVar()}
+ className="flex-1 bg-transparent border border-border/40 rounded px-2 py-1 text-xs font-mono outline-none focus:border-primary/50 transition-colors"
+ />
+
+
+
+ )}
+
+
+ {/* ── History ───────────────────────────────────────────── */}
+
+
+
+ History
+
+ {history.length > 0 && (
+
+ )}
+
+
+ {history.length === 0 ? (
+
No calculations yet
+ ) : (
+
+ {history.map((entry) => (
+
+ ))}
+
+ )}
+
+
+
+ );
+}
diff --git a/components/calculate/GraphCanvas.tsx b/components/calculate/GraphCanvas.tsx
new file mode 100644
index 0000000..d784ea4
--- /dev/null
+++ b/components/calculate/GraphCanvas.tsx
@@ -0,0 +1,370 @@
+'use client';
+
+import { useRef, useEffect, useCallback, useState } from 'react';
+import type { GraphFunction } from '@/lib/calculate/store';
+import { sampleFunction, evaluateAt } from '@/lib/calculate/math-engine';
+
+interface ViewState {
+ xMin: number;
+ xMax: number;
+ yMin: number;
+ yMax: number;
+}
+
+interface Props {
+ functions: GraphFunction[];
+ variables: Record