diff --git a/TODO.md b/TODO.md
new file mode 100644
index 00000000..bb2d7d8f
--- /dev/null
+++ b/TODO.md
@@ -0,0 +1,26 @@
+# TODO: Fix ESLint Issues
+
+## Step 1: Fix scripts/sitemap-generator.js
+
+- Replace unused 'e' in catch block with underscore.
+
+## Step 2: Fix simulations/test.jsx - Remove unused imports
+
+- Remove 'useMemo' from React imports.
+- Remove 'getTimeScale' from Time.js imports.
+- Remove 'PHYSICS_CONTROLS' and 'VISUALIZATION_CONTROLS' from config imports.
+
+## Step 3: Fix simulations/test.jsx - Fix useEffect dependencies
+
+- Add 'inputs.timeScale' to the first useEffect dependency array.
+- Extract 'simData["Total Energy"]' to a variable for the warnings useEffect.
+- Add 'simData' to the warnings useEffect dependency array.
+
+## Step 4: Fix simulations/test.jsx - Refactor warnings logic
+
+- Move warnings calculation from useEffect to useMemo to avoid setState in effect.
+- Update the component to use warnings from useMemo.
+
+## Step 5: Run ESLint to verify fixes
+
+- Execute `npm run lint` to check if all issues are resolved.
diff --git a/app/(core)/components/CollapsibleSection.tsx b/app/(core)/components/CollapsibleSection.tsx
new file mode 100644
index 00000000..4dec77a1
--- /dev/null
+++ b/app/(core)/components/CollapsibleSection.tsx
@@ -0,0 +1,45 @@
+// CollapsibleSection.tsx
+"use client";
+import React, { useState } from "react";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faChevronDown, faChevronUp } from "@fortawesome/free-solid-svg-icons";
+
+interface CollapsibleSectionProps {
+ title: string;
+ children: React.ReactNode;
+ defaultExpanded?: boolean;
+ icon?: React.ReactNode;
+ className?: string;
+}
+
+export default function CollapsibleSection({
+ title,
+ children,
+ defaultExpanded = false,
+ icon,
+ className = "",
+}: CollapsibleSectionProps) {
+ const [isExpanded, setIsExpanded] = useState(defaultExpanded);
+
+ return (
+
+
+ {isExpanded &&
{children}
}
+
+ );
+}
diff --git a/app/(core)/components/EducationalTheorySection.tsx b/app/(core)/components/EducationalTheorySection.tsx
new file mode 100644
index 00000000..618a29ae
--- /dev/null
+++ b/app/(core)/components/EducationalTheorySection.tsx
@@ -0,0 +1,63 @@
+// EducationalTheorySection.tsx
+"use client";
+import React from "react";
+import LearningObjectives from "./LearningObjectives.tsx";
+import PhysicsEquations from "./PhysicsEquations.tsx";
+import PhysicsWarnings from "./PhysicsWarnings.tsx";
+import GuidedExperiments from "./GuidedExperiments.tsx";
+
+interface EducationalTheorySectionProps {
+ learningObjectives: {
+ title: string;
+ goals: string[];
+ variables: string[];
+ };
+ physicsEquations: Array<{
+ name: string;
+ formula: string;
+ description?: string;
+ }>;
+ warnings: Array<{
+ id: string;
+ message: string;
+ severity: "warning" | "error";
+ }>;
+ guidedExperiments: Array<{
+ id: string;
+ name: string;
+ description: string;
+ instructions: string[];
+ question?: string;
+ parameters: Record;
+ }>;
+ onApplyExperiment: (
+ params: Record
+ ) => void;
+}
+
+export default function EducationalTheorySection({
+ learningObjectives,
+ physicsEquations,
+ warnings,
+ guidedExperiments,
+ onApplyExperiment,
+}: EducationalTheorySectionProps) {
+ return (
+
+
+
+
+
+ {warnings.length > 0 &&
}
+
+
+
+ );
+}
diff --git a/app/(core)/components/GuidedExperiments.tsx b/app/(core)/components/GuidedExperiments.tsx
new file mode 100644
index 00000000..81b1120f
--- /dev/null
+++ b/app/(core)/components/GuidedExperiments.tsx
@@ -0,0 +1,76 @@
+// GuidedExperiments.tsx
+"use client";
+import React, { useState } from "react";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faCheck } from "@fortawesome/free-solid-svg-icons";
+
+interface Experiment {
+ id: string;
+ name: string;
+ description: string;
+ instructions: string[];
+ question?: string;
+ parameters: Record;
+}
+
+interface GuidedExperimentsProps {
+ experiments: Experiment[];
+ onApplyExperiment: (
+ params: Record
+ ) => void;
+}
+
+export default function GuidedExperiments({
+ experiments,
+ onApplyExperiment,
+}: GuidedExperimentsProps) {
+ const [activeExperiment, setActiveExperiment] = useState(null);
+
+ const handleApply = (experiment: Experiment) => {
+ onApplyExperiment(experiment.parameters);
+ setActiveExperiment(experiment.id);
+ };
+
+ return (
+
+
+ {experiments.map((experiment) => (
+
+
+
{experiment.name}
+
+
+
{experiment.description}
+
+
Instructions:
+
+ {experiment.instructions.map((instruction, i) => (
+ -
+ {instruction}
+
+ ))}
+
+
+ {experiment.question && (
+
+ Question: {experiment.question}
+
+ )}
+
+ ))}
+
+
+ );
+}
diff --git a/app/(core)/components/Layout.jsx b/app/(core)/components/Layout.jsx
index 737afe31..5cf0cf07 100644
--- a/app/(core)/components/Layout.jsx
+++ b/app/(core)/components/Layout.jsx
@@ -18,8 +18,8 @@ export default function Layout({
)}
{showGradient && }
diff --git a/app/(core)/components/LearningObjectives.tsx b/app/(core)/components/LearningObjectives.tsx
new file mode 100644
index 00000000..1d573bc4
--- /dev/null
+++ b/app/(core)/components/LearningObjectives.tsx
@@ -0,0 +1,35 @@
+// LearningObjectives.tsx
+"use client";
+import React from "react";
+
+interface LearningObjectivesProps {
+ goals: string[];
+ variables: string[];
+}
+
+export default function LearningObjectives({
+ goals,
+ variables,
+}: LearningObjectivesProps) {
+ return (
+
+
+
Learning Goals
+
+ {goals.map((goal, i) => (
+ - {goal}
+ ))}
+
+
+
+
+
Key Variables
+
+ {variables.map((variable, i) => (
+ - {variable}
+ ))}
+
+
+
+ );
+}
diff --git a/app/(core)/components/PhysicsEquations.tsx b/app/(core)/components/PhysicsEquations.tsx
new file mode 100644
index 00000000..917ed48d
--- /dev/null
+++ b/app/(core)/components/PhysicsEquations.tsx
@@ -0,0 +1,28 @@
+// PhysicsEquations.tsx
+"use client";
+import React from "react";
+
+interface PhysicsEquationsProps {
+ equations: Array<{
+ name: string;
+ formula: string;
+ description?: string;
+ }>;
+}
+
+export default function PhysicsEquations({ equations }: PhysicsEquationsProps) {
+ return (
+
+
+ {equations.map((eq, i) => (
+
+
{eq.formula}
+ {eq.description && (
+
{eq.description}
+ )}
+
+ ))}
+
+
+ );
+}
diff --git a/app/(core)/components/PhysicsWarnings.tsx b/app/(core)/components/PhysicsWarnings.tsx
new file mode 100644
index 00000000..4c34dca2
--- /dev/null
+++ b/app/(core)/components/PhysicsWarnings.tsx
@@ -0,0 +1,33 @@
+// PhysicsWarnings.tsx
+"use client";
+import React from "react";
+import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
+import { faExclamationTriangle } from "@fortawesome/free-solid-svg-icons";
+
+interface Warning {
+ id: string;
+ message: string;
+ severity: "warning" | "error";
+}
+
+interface PhysicsWarningsProps {
+ warnings: Warning[];
+}
+
+export default function PhysicsWarnings({ warnings }: PhysicsWarningsProps) {
+ if (warnings.length === 0) return null;
+
+ return (
+
+ {warnings.map((warning) => (
+
+
+ {warning.message}
+
+ ))}
+
+ );
+}
diff --git a/app/(core)/components/SimulationLayout.jsx b/app/(core)/components/SimulationLayout.jsx
index fc9b8473..acccdcbe 100644
--- a/app/(core)/components/SimulationLayout.jsx
+++ b/app/(core)/components/SimulationLayout.jsx
@@ -16,6 +16,7 @@ export default function SimulationLayout({
onLoad,
children,
dynamicInputs,
+ hideDefaultControls = false,
}) {
const theory = useMemo(() => {
const chapter = chapters.find((ch) => ch.link === simulation);
@@ -42,13 +43,15 @@ export default function SimulationLayout({
{/* 1. Render the Canvas */}
{children}
- {/* 2. Render the Main Controls */}
-
+ {/* 2. Render the Main Controls (unless hidden) */}
+ {!hideDefaultControls && (
+
+ )}
{/* 3. Render the Dynamic Inputs */}
{dynamicInputs}
diff --git a/app/(core)/components/inputs/DynamicInputs.jsx b/app/(core)/components/inputs/DynamicInputs.jsx
index e87bd821..947bd3dd 100644
--- a/app/(core)/components/inputs/DynamicInputs.jsx
+++ b/app/(core)/components/inputs/DynamicInputs.jsx
@@ -3,66 +3,124 @@ import CheckboxInput from "./CheckboxInput.jsx";
import ColorInput from "./ColorInput.jsx";
import SelectInput from "./SelectInput.jsx";
-export default function DynamicInputs({ config, values, onChange }) {
- return (
-
- {config.map((field) => {
- const commonProps = {
- name: field.name,
- label: field.label,
- };
+function renderInput(field, values, onChange) {
+ // Check if field should be conditionally rendered
+ if (field.showIf) {
+ const condition = field.showIf;
+ const shouldShow = condition.every(({ key, value }) => {
+ if (value === true) return values[key] === true;
+ if (value === false) return values[key] === false;
+ return values[key] === value;
+ });
+ if (!shouldShow) return null;
+ }
+
+ const commonProps = {
+ name: field.name,
+ label: field.label,
+ };
- if (field.type === "number") {
- return (
-
onChange(field.name, Number(e.target.value))}
- />
- );
- }
+ if (field.type === "number") {
+ return (
+ onChange(field.name, Number(e.target.value))}
+ />
+ );
+ }
- if (field.type === "checkbox") {
- return (
- onChange(field.name, e.target.checked)}
- />
- );
- }
+ if (field.type === "checkbox") {
+ return (
+ onChange(field.name, e.target.checked)}
+ />
+ );
+ }
- if (field.type === "color") {
- return (
- onChange(field.name, e.target.value)}
- />
- );
- }
+ if (field.type === "color") {
+ return (
+ onChange(field.name, e.target.value)}
+ />
+ );
+ }
+
+ if (field.type === "select") {
+ return (
+ onChange(field.name, e.target.value)}
+ />
+ );
+ }
+ return null;
+}
- if (field.type === "select") {
- return (
- onChange(field.name, e.target.value)}
- />
- );
- }
- return null;
- })}
+export default function DynamicInputs({
+ config,
+ values,
+ onChange,
+ grouped = false,
+}) {
+ // If grouped, separate by category
+ if (grouped) {
+ const physicsControls = config.filter((f) => f.category === "physics");
+ const visualizationControls = config.filter(
+ (f) => f.category === "visualization"
+ );
+ const otherControls = config.filter((f) => !f.category);
+
+ return (
+ <>
+ {physicsControls.length > 0 && (
+
+
Physics Controls
+
+ {physicsControls.map((field) =>
+ renderInput(field, values, onChange)
+ )}
+
+
+ )}
+ {visualizationControls.length > 0 && (
+
+
Visualization Controls
+
+ {visualizationControls.map((field) =>
+ renderInput(field, values, onChange)
+ )}
+
+
+ )}
+ {otherControls.length > 0 && (
+
+ {otherControls.map((field) => renderInput(field, values, onChange))}
+
+ )}
+ >
+ );
+ }
+
+ // Default: render all together
+ return (
+
+ {config.map((field) => renderInput(field, values, onChange))}
);
}
diff --git a/app/(core)/components/inputs/SelectInput.jsx b/app/(core)/components/inputs/SelectInput.jsx
index a645fa81..6fddbae3 100644
--- a/app/(core)/components/inputs/SelectInput.jsx
+++ b/app/(core)/components/inputs/SelectInput.jsx
@@ -3,20 +3,37 @@ import React from "react";
import PropTypes from "prop-types";
function SelectInput({ label, name, options, value, onChange, placeholder }) {
+ const handleChange = (e) => {
+ const selectedValue = e.target.value;
+ // Convert to number if the value is numeric (for gravity and similar numeric selects)
+ const numericValue =
+ !isNaN(selectedValue) &&
+ selectedValue !== "" &&
+ !isNaN(parseFloat(selectedValue))
+ ? parseFloat(selectedValue)
+ : selectedValue;
+ // Create a synthetic event object that matches what onChange expects
+ const syntheticEvent = {
+ target: {
+ name,
+ value: numericValue,
+ },
+ };
+ onChange(syntheticEvent);
+ };
+
return (
-
- {label && (
-
- )}
+
+