+ {/* Step backward */}
+
+
+ {/* Play/Pause */}
+
+ {isPlaying && (
+
+ )}
+
+
+
+ {/* Step forward */}
+
+
+ {/* Progress */}
+
+
+
+
1 ? `${(currentStep / (totalSteps - 1)) * 100}%` : '0%',
+ }}
+ />
+
+
onStepChange(Number(e.target.value))}
+ disabled={disabled}
+ className="absolute inset-0 w-full h-full opacity-0 cursor-pointer disabled:cursor-default"
+ />
+
+
+ {totalSteps > 0 ? `${currentStep + 1}/${totalSteps}` : '\u2014'}
+
+
+
+ {/* Speed */}
+
+ onSpeedChange(Number(e.target.value))}
+ className="w-10"
+ aria-label={t.speed}
+ />
+ {speed}x
+
+
+ )
+}
+
+function ComparePanel({
+ locale,
+ side,
+ syncSpeed,
+ onSyncSpeedChange,
+ isSynced,
+}: {
+ locale: Locale
+ side: 'left' | 'right'
+ syncSpeed: number
+ onSyncSpeedChange: (s: number) => void
+ isSynced: boolean
+}) {
+ const t = translations[locale]
+ const {
+ selectedAlgorithm,
+ steps,
+ currentStep,
+ setCurrentStep,
+ isPlaying,
+ speed,
+ setSpeed,
+ selectAlgorithm: selectAlgorithmBase,
+ stepForward,
+ stepBackward,
+ togglePlay,
+ currentStepData,
+ } = usePlayback(locale)
+
+ const selectAlgorithm = useCallback(
+ (algo: Algorithm) => {
+ selectAlgorithmBase(algo)
+ },
+ [selectAlgorithmBase],
+ )
+
+ const handleSpeedChange = useCallback(
+ (s: number) => {
+ if (isSynced) {
+ onSyncSpeedChange(s)
+ } else {
+ setSpeed(s)
+ }
+ },
+ [isSynced, onSyncSpeedChange, setSpeed],
+ )
+
+ const effectiveSpeed = isSynced ? syncSpeed : speed
+
+ const renderVisualization = () => {
+ if (!selectedAlgorithm || !currentStepData) {
+ return (
+
+
+
{t.chooseAlgorithm}
+
+ )
+ }
+
+ switch (selectedAlgorithm.visualization) {
+ case 'array':
+ return
+ case 'graph':
+ return
+ case 'matrix':
+ return
+ case 'concept':
+ return
+ default:
+ return null
+ }
+ }
+
+ return (
+
+ {/* Selector */}
+
+
+
+
+ {/* Visualization */}
+
+ {renderVisualization()}
+
+
+ {/* Step description */}
+ {currentStepData?.description && (
+
+
+
+ {t.step.replace('{n}', String(currentStep + 1))}
+
+ {currentStepData.description}
+
+
+ )}
+
+ {/* Controls */}
+
+
+ {/* Algorithm info */}
+ {selectedAlgorithm && (
+
+
+ {selectedAlgorithm.name}
+
+ {selectedAlgorithm.difficulty}
+
+
+
+ )}
+
+ )
+}
+
+export default function CompareView({ locale, onExit }: CompareViewProps) {
+ const t = translations[locale]
+ const [isSynced, setIsSynced] = useState(false)
+ const [syncSpeed, setSyncSpeed] = useState(2)
+
+ return (
+
+ {/* Compare header */}
+
+
+
+ {t.compareModeTitle}
+
+
+
+ {/* Sync button */}
+
+
+ {/* Exit button */}
+
+
+
+
+ {/* Panels */}
+
+ {/* Left panel */}
+
+
+
+
+ {/* VS divider */}
+
+
+ {/* Mobile VS divider */}
+
+
+ {/* Right panel */}
+
+
+
+
+
+ )
+}
diff --git a/src/components/Header.tsx b/src/components/Header.tsx
index 69e1236..2ab3288 100644
--- a/src/components/Header.tsx
+++ b/src/components/Header.tsx
@@ -25,6 +25,8 @@ interface HeaderProps {
isMobile?: boolean
onToggleMobileSidebar?: () => void
onToggleMobileCodePanel?: () => void
+ // Compare mode
+ onCompare?: () => void
}
function getLocaleUrl(targetLocale: Locale, algorithmId?: string) {
@@ -55,6 +57,7 @@ export default function Header({
isMobile = false,
onToggleMobileSidebar,
onToggleMobileCodePanel,
+ onCompare,
}: HeaderProps) {
return (
)}
+ {onCompare && (
+
+ )}