refactor: consolidate abacus display context management

- Remove duplicate AbacusDisplayContext in favor of centralized abacus-react provider
- Update all components to use useAbacusDisplay and useAbacusConfig hooks from @soroban/abacus-react
- Create ClientProviders component to centralize provider setup
- Simplify context management across the application

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-09-29 09:43:35 -05:00
parent 90b9ffa0d8
commit a387b030fa
8 changed files with 33 additions and 151 deletions

View File

@@ -10,7 +10,7 @@ import { ConfigurationFormWithoutGenerate } from '@/components/ConfigurationForm
import { LivePreview } from '@/components/LivePreview'
import { GenerationProgress } from '@/components/GenerationProgress'
import { StyleControls } from '@/components/StyleControls'
import { useAbacusConfig } from '@/contexts/AbacusDisplayContext'
import { useAbacusConfig } from '@soroban/abacus-react'
// Complete, validated configuration ready for generation
export interface FlashcardConfig {

View File

@@ -7,7 +7,7 @@ import { container, stack, hstack, grid } from '../../../styled-system/patterns'
import { TypstSoroban } from '@/components/TypstSoroban'
import { InteractiveAbacus } from '@/components/InteractiveAbacus'
import { AbacusReact } from '@soroban/abacus-react'
import { useAbacusConfig } from '@/contexts/AbacusDisplayContext'
import { useAbacusConfig } from '@soroban/abacus-react'
import { TutorialPlayer } from '@/components/tutorial/TutorialPlayer'
import { getTutorialForEditor } from '@/utils/tutorialConverter'

View File

@@ -7,7 +7,7 @@ import * as RadioGroup from '@radix-ui/react-radio-group'
import * as Switch from '@radix-ui/react-switch'
import { css } from '../../styled-system/css'
import { stack, hstack } from '../../styled-system/patterns'
import { useAbacusDisplay, ColorScheme, BeadShape } from '@/contexts/AbacusDisplayContext'
import { useAbacusDisplay, ColorScheme, BeadShape } from '@soroban/abacus-react'
interface AbacusDisplayDropdownProps {
isFullscreen?: boolean

View File

@@ -0,0 +1,25 @@
'use client'
import { ReactNode } from 'react'
import { AbacusDisplayProvider } from '@soroban/abacus-react'
import { UserProfileProvider } from '@/contexts/UserProfileContext'
import { GameModeProvider } from '@/contexts/GameModeContext'
import { FullscreenProvider } from '@/contexts/FullscreenContext'
interface ClientProvidersProps {
children: ReactNode
}
export function ClientProviders({ children }: ClientProvidersProps) {
return (
<AbacusDisplayProvider>
<UserProfileProvider>
<GameModeProvider>
<FullscreenProvider>
{children}
</FullscreenProvider>
</GameModeProvider>
</UserProfileProvider>
</AbacusDisplayProvider>
)
}

View File

@@ -7,7 +7,7 @@ import * as Switch from '@radix-ui/react-switch'
import { css } from '../../styled-system/css'
import { stack, hstack, grid } from '../../styled-system/patterns'
import { FlashcardFormState } from '@/app/create/page'
import { useAbacusDisplay } from '@/contexts/AbacusDisplayContext'
import { useAbacusDisplay } from '@soroban/abacus-react'
import { useEffect } from 'react'
interface StyleControlsProps {

View File

@@ -3,7 +3,7 @@
import { useState, useEffect, useCallback, useRef, useMemo } from 'react'
import { generateSorobanSVG, getWasmStatus, triggerWasmPreload, type SorobanConfig } from '@/lib/typst-soroban'
import { css } from '../../styled-system/css'
import { useAbacusConfig } from '@/contexts/AbacusDisplayContext'
import { useAbacusConfig } from '@soroban/abacus-react'
interface TypstSorobanProps {
number: number

View File

@@ -15,7 +15,7 @@ import { TutorialUIProvider } from './TutorialUIContext'
import { CoachBar } from './CoachBar/CoachBar'
import { PedagogicalDecompositionDisplay } from './PedagogicalDecompositionDisplay'
import { DecompositionWithReasons } from './DecompositionWithReasons'
import { useAbacusDisplay } from '@/contexts/AbacusDisplayContext'
import { useAbacusDisplay } from '@soroban/abacus-react'
import './CoachBar/coachbar.css'
// Helper function to find the topmost bead with arrows
@@ -1308,6 +1308,8 @@ function TutorialPlayerContent({
colorScheme={abacusConfig.colorScheme}
beadShape={abacusConfig.beadShape}
hideInactiveBeads={abacusConfig.hideInactiveBeads}
soundEnabled={abacusConfig.soundEnabled}
soundVolume={abacusConfig.soundVolume}
highlightBeads={currentStep.highlightBeads}
stepBeadHighlights={currentStepBeads}
currentStep={currentMultiStep}

View File

@@ -1,145 +0,0 @@
'use client'
import React, { createContext, useContext, useState, useCallback, ReactNode, useEffect } from 'react'
// Abacus display configuration types
export type ColorScheme = 'monochrome' | 'place-value' | 'heaven-earth' | 'alternating'
export type BeadShape = 'diamond' | 'circle' | 'square'
export interface AbacusDisplayConfig {
colorScheme: ColorScheme
beadShape: BeadShape
hideInactiveBeads: boolean
coloredNumerals: boolean
scaleFactor: number
soundEnabled: boolean
soundVolume: number
}
export interface AbacusDisplayContextType {
config: AbacusDisplayConfig
updateConfig: (updates: Partial<AbacusDisplayConfig>) => void
resetToDefaults: () => void
}
// Default configuration - matches current create page defaults
const DEFAULT_CONFIG: AbacusDisplayConfig = {
colorScheme: 'place-value',
beadShape: 'diamond',
hideInactiveBeads: false,
coloredNumerals: false,
scaleFactor: 1.0, // Normalized for display, can be scaled per component
soundEnabled: true,
soundVolume: 0.8
}
const STORAGE_KEY = 'soroban-abacus-display-config'
// Load config from localStorage with fallback to defaults
function loadConfigFromStorage(): AbacusDisplayConfig {
if (typeof window === 'undefined') return DEFAULT_CONFIG
try {
const stored = localStorage.getItem(STORAGE_KEY)
if (stored) {
const parsed = JSON.parse(stored)
// Validate that all required fields are present and have valid values
return {
colorScheme: ['monochrome', 'place-value', 'heaven-earth', 'alternating'].includes(parsed.colorScheme)
? parsed.colorScheme : DEFAULT_CONFIG.colorScheme,
beadShape: ['diamond', 'circle', 'square'].includes(parsed.beadShape)
? parsed.beadShape : DEFAULT_CONFIG.beadShape,
hideInactiveBeads: typeof parsed.hideInactiveBeads === 'boolean'
? parsed.hideInactiveBeads : DEFAULT_CONFIG.hideInactiveBeads,
coloredNumerals: typeof parsed.coloredNumerals === 'boolean'
? parsed.coloredNumerals : DEFAULT_CONFIG.coloredNumerals,
scaleFactor: typeof parsed.scaleFactor === 'number' && parsed.scaleFactor > 0
? parsed.scaleFactor : DEFAULT_CONFIG.scaleFactor,
soundEnabled: typeof parsed.soundEnabled === 'boolean'
? parsed.soundEnabled : DEFAULT_CONFIG.soundEnabled,
soundVolume: typeof parsed.soundVolume === 'number' && parsed.soundVolume >= 0 && parsed.soundVolume <= 1
? parsed.soundVolume : DEFAULT_CONFIG.soundVolume
}
}
} catch (error) {
console.warn('Failed to load abacus config from localStorage:', error)
}
return DEFAULT_CONFIG
}
// Save config to localStorage
function saveConfigToStorage(config: AbacusDisplayConfig): void {
if (typeof window === 'undefined') return
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(config))
} catch (error) {
console.warn('Failed to save abacus config to localStorage:', error)
}
}
const AbacusDisplayContext = createContext<AbacusDisplayContextType | null>(null)
export function useAbacusDisplay() {
const context = useContext(AbacusDisplayContext)
if (!context) {
throw new Error('useAbacusDisplay must be used within an AbacusDisplayProvider')
}
return context
}
interface AbacusDisplayProviderProps {
children: ReactNode
initialConfig?: Partial<AbacusDisplayConfig>
}
export function AbacusDisplayProvider({
children,
initialConfig = {}
}: AbacusDisplayProviderProps) {
const [config, setConfig] = useState<AbacusDisplayConfig>(() => {
// Always start with defaults to ensure server/client consistency
return { ...DEFAULT_CONFIG, ...initialConfig }
})
// Load from localStorage only after hydration
useEffect(() => {
const stored = loadConfigFromStorage()
setConfig(stored)
}, [])
// Save to localStorage whenever config changes
useEffect(() => {
saveConfigToStorage(config)
}, [config])
const updateConfig = useCallback((updates: Partial<AbacusDisplayConfig>) => {
setConfig(prev => {
const newConfig = { ...prev, ...updates }
return newConfig
})
}, [])
const resetToDefaults = useCallback(() => {
setConfig(DEFAULT_CONFIG)
}, [])
const value: AbacusDisplayContextType = {
config,
updateConfig,
resetToDefaults
}
return (
<AbacusDisplayContext.Provider value={value}>
{children}
</AbacusDisplayContext.Provider>
)
}
// Convenience hook for components that need specific config values
export function useAbacusConfig() {
const { config } = useAbacusDisplay()
return config
}