diff --git a/app/lessons/unit-1-fractions/divide/page.tsx b/app/lessons/unit-1-fractions/divide/page.tsx
index 836d530..0d24fe5 100644
--- a/app/lessons/unit-1-fractions/divide/page.tsx
+++ b/app/lessons/unit-1-fractions/divide/page.tsx
@@ -16,7 +16,7 @@ export default function DividePage() {
{topic.label}
diff --git a/components/explorers/conversion-explorer.tsx b/components/explorers/conversion-explorer.tsx
index 4923a65..62bd957 100644
--- a/components/explorers/conversion-explorer.tsx
+++ b/components/explorers/conversion-explorer.tsx
@@ -5,6 +5,7 @@ import { Card } from "@/components/ui/card";
import { StepControls } from "./step-controls";
import { MathDisplay } from "@/components/math/math-display";
import { simplify, toKatex, gcd } from "@/lib/math/fractions";
+import { parseStrictDecimal, parseStrictInt } from "@/lib/math/validation";
type ConvertMode = "decToFrac" | "fracToDec";
@@ -161,16 +162,16 @@ export function ConversionExplorer() {
try {
let s: Step[];
if (mode === "decToFrac") {
- const val = parseFloat(decInput);
- if (isNaN(val) || val < 0 || val >= 10) {
+ const val = parseStrictDecimal(decInput);
+ if (val === null || val < 0 || val >= 10) {
setError("Enter a valid decimal between 0 and 10.");
return;
}
s = buildDecToFracSteps(decInput.trim());
} else {
- const n = parseInt(numInput);
- const d = parseInt(denInput);
- if (isNaN(n) || isNaN(d) || d === 0) {
+ const n = parseStrictInt(numInput);
+ const d = parseStrictInt(denInput);
+ if (n === null || d === null || d === 0) {
setError("Enter a valid fraction (denominator ≠ 0).");
return;
}
diff --git a/components/explorers/decimal-arithmetic-explorer.tsx b/components/explorers/decimal-arithmetic-explorer.tsx
index 2efcc82..d7c6629 100644
--- a/components/explorers/decimal-arithmetic-explorer.tsx
+++ b/components/explorers/decimal-arithmetic-explorer.tsx
@@ -227,8 +227,12 @@ function ColumnArithmetic({
);
}
-export function DecimalArithmeticExplorer() {
- const [op, setOp] = useState
("add");
+export function DecimalArithmeticExplorer({
+ initialOperation = "add",
+}: {
+ initialOperation?: DecOp;
+} = {}) {
+ const [op, setOp] = useState(initialOperation);
const [aInput, setAInput] = useState("12.45");
const [bInput, setBInput] = useState("3.7");
const [error, setError] = useState("");
diff --git a/components/explorers/fraction-operation-explorer.tsx b/components/explorers/fraction-operation-explorer.tsx
index 4c71fad..5217ca1 100644
--- a/components/explorers/fraction-operation-explorer.tsx
+++ b/components/explorers/fraction-operation-explorer.tsx
@@ -174,8 +174,12 @@ function FractionBar({
);
}
-export function FractionOperationExplorer() {
- const [op, setOp] = useState("add");
+export function FractionOperationExplorer({
+ initialOperation = "add",
+}: {
+ initialOperation?: Operation;
+} = {}) {
+ const [op, setOp] = useState(initialOperation);
const [n1, setN1] = useState("1");
const [d1, setD1] = useState("3");
const [n2, setN2] = useState("1");
diff --git a/components/explorers/fraction-quantity-explorer.tsx b/components/explorers/fraction-quantity-explorer.tsx
index b2e80b9..0391779 100644
--- a/components/explorers/fraction-quantity-explorer.tsx
+++ b/components/explorers/fraction-quantity-explorer.tsx
@@ -121,8 +121,12 @@ function QuantityBar({ filled, label }: { filled: number; label?: string }) {
);
}
-export function FractionQuantityExplorer() {
- const [mode, setMode] = useState("fractionOf");
+export function FractionQuantityExplorer({
+ initialMode = "fractionOf",
+}: {
+ initialMode?: FQMode;
+} = {}) {
+ const [mode, setMode] = useState(initialMode);
const [num, setNum] = useState("3");
const [den, setDen] = useState("4");
const [quantity, setQuantity] = useState("80");
diff --git a/components/explorers/ratio-explorer.tsx b/components/explorers/ratio-explorer.tsx
index e0717f2..ce13cd1 100644
--- a/components/explorers/ratio-explorer.tsx
+++ b/components/explorers/ratio-explorer.tsx
@@ -182,8 +182,12 @@ function RatioBar({
);
}
-export function RatioExplorer() {
- const [mode, setMode] = useState("divide");
+export function RatioExplorer({
+ initialMode = "divide",
+}: {
+ initialMode?: RatioMode;
+} = {}) {
+ const [mode, setMode] = useState(initialMode);
const [partA, setPartA] = useState("2");
const [partB, setPartB] = useState("3");
const [partC, setPartC] = useState("");
diff --git a/components/explorers/rounding-explorer.tsx b/components/explorers/rounding-explorer.tsx
index 165b06d..437ec74 100644
--- a/components/explorers/rounding-explorer.tsx
+++ b/components/explorers/rounding-explorer.tsx
@@ -3,6 +3,7 @@
import { useState, useCallback } from "react";
import { Card } from "@/components/ui/card";
import { StepControls } from "./step-controls";
+import { parseStrictDecimal } from "@/lib/math/validation";
type RoundTarget = "whole" | "1dp" | "2dp" | "1sf" | "2sf" | "3sf";
@@ -384,9 +385,9 @@ export function RoundingExplorer() {
function handleGo() {
setError("");
const trimmed = input.trim();
- const val = parseFloat(trimmed);
- if (isNaN(val)) {
- setError("Enter a valid number.");
+ const val = parseStrictDecimal(trimmed);
+ if (val === null) {
+ setError("Enter a valid number (digits and decimal point only).");
return;
}
if (val < 0) {
diff --git a/components/explorers/standard-form-explorer.tsx b/components/explorers/standard-form-explorer.tsx
index cbacb77..84c1282 100644
--- a/components/explorers/standard-form-explorer.tsx
+++ b/components/explorers/standard-form-explorer.tsx
@@ -337,10 +337,11 @@ export function StandardFormExplorer() {
{totalSteps === 0
? "Number is already in standard form position — power = 0"
- : `Step ${currentStep} of ${totalSteps}`}
+ : `Step ${currentStep + 1} of ${totalSteps}`}
{/* Digit row */}
+
{display.digits.map((ch, i) => {
const isLeadingZero = i < fz && fz !== -1;
@@ -372,6 +373,7 @@ export function StandardFormExplorer() {
}}
/>
+
{/* Direction label */}
{dirText && (
@@ -403,7 +405,7 @@ export function StandardFormExplorer() {
{/* Controls */}
{
- const tag = (e.target as HTMLElement).tagName;
+ const el = e.target as HTMLElement;
+ const tag = el.tagName;
if (["INPUT", "TEXTAREA", "SELECT"].includes(tag)) return;
+ if (el.closest("button, [role='button'], a")) return;
if (e.key === "ArrowRight" || e.key === " ") {
e.preventDefault();
diff --git a/lib/math/validation.ts b/lib/math/validation.ts
index df868f3..bf6a3c4 100644
--- a/lib/math/validation.ts
+++ b/lib/math/validation.ts
@@ -1,6 +1,20 @@
import { gcd } from "./fractions";
import { simplifyRatio } from "./ratios";
+/** Parse a strict decimal string (digits and optional single dot). Rejects scientific notation, trailing letters, etc. */
+export function parseStrictDecimal(input: string): number | null {
+ const s = input.trim();
+ if (!/^\d+(\.\d+)?$/.test(s)) return null;
+ return parseFloat(s);
+}
+
+/** Parse a strict integer string (digits only, no dot or letters). */
+export function parseStrictInt(input: string): number | null {
+ const s = input.trim();
+ if (!/^\d+$/.test(s)) return null;
+ return parseInt(s, 10);
+}
+
export type AnswerResult =
| { correct: true; simplified: boolean }
| { correct: false; message: string };