feat(practice): add inline practice panel for browse mode debugging
Add PracticePreview component that allows practicing any problem while in browse mode without affecting session state. The practice panel displays inline below the problem card with a clear header indicating it doesn't affect the session, preventing UX confusion. - Add PracticePreview component with keyboard and numpad input support - Add inline mode to PracticePreview for embedded display - Update BrowseModeView to show practice panel below problem card - Toggle button switches between "Practice This Problem" / "Close Practice Panel" 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
3c52e607b3
commit
c0764ccd85
|
|
@ -9,12 +9,13 @@
|
|||
|
||||
'use client'
|
||||
|
||||
import { useMemo } from 'react'
|
||||
import { useMemo, useState } from 'react'
|
||||
import { useTheme } from '@/contexts/ThemeContext'
|
||||
import type { ProblemSlot, SessionPart, SessionPlan, SlotResult } from '@/db/schema/session-plans'
|
||||
import { css } from '../../../styled-system/css'
|
||||
import { calculateAutoPauseInfo } from './autoPauseCalculator'
|
||||
import { DetailedProblemCard } from './DetailedProblemCard'
|
||||
import { PracticePreview } from './PracticePreview'
|
||||
|
||||
/**
|
||||
* Flattened problem item with all context needed for display
|
||||
|
|
@ -72,8 +73,6 @@ export interface BrowseModeViewProps {
|
|||
browseIndex: number
|
||||
/** The actual current practice problem index (to highlight) */
|
||||
currentPracticeIndex: number
|
||||
/** Called when user wants to exit browse mode and practice the current problem */
|
||||
onExitBrowse?: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -87,15 +86,13 @@ function getResultForProblem(
|
|||
return results.find((r) => r.partNumber === partNumber && r.slotIndex === slotIndex)
|
||||
}
|
||||
|
||||
export function BrowseModeView({
|
||||
plan,
|
||||
browseIndex,
|
||||
currentPracticeIndex,
|
||||
onExitBrowse,
|
||||
}: BrowseModeViewProps) {
|
||||
export function BrowseModeView({ plan, browseIndex, currentPracticeIndex }: BrowseModeViewProps) {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const isDark = resolvedTheme === 'dark'
|
||||
|
||||
// Practice preview mode - when true, show interactive practice interface
|
||||
const [isPracticing, setIsPracticing] = useState(false)
|
||||
|
||||
// Build linear problem list
|
||||
const linearProblems = useMemo(() => buildLinearProblemList(plan.parts), [plan.parts])
|
||||
|
||||
|
|
@ -181,71 +178,116 @@ export function BrowseModeView({
|
|||
problemNumber={browseIndex + 1}
|
||||
/>
|
||||
|
||||
{/* Action Button */}
|
||||
{/* Action Button - Toggle practice mode */}
|
||||
<div
|
||||
data-element="browse-action"
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
gap: '0.75rem',
|
||||
padding: '0.5rem 0',
|
||||
})}
|
||||
>
|
||||
{isCurrentPractice && onExitBrowse && (
|
||||
<button
|
||||
type="button"
|
||||
data-action="practice-this-problem"
|
||||
onClick={onExitBrowse}
|
||||
className={css({
|
||||
padding: '0.75rem 1.5rem',
|
||||
fontSize: '1rem',
|
||||
fontWeight: 'bold',
|
||||
borderRadius: '8px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: isDark ? 'green.600' : 'green.500',
|
||||
color: 'white',
|
||||
transition: 'all 0.15s ease',
|
||||
_hover: {
|
||||
backgroundColor: isDark ? 'green.500' : 'green.600',
|
||||
transform: 'scale(1.02)',
|
||||
},
|
||||
_active: {
|
||||
transform: 'scale(0.98)',
|
||||
},
|
||||
})}
|
||||
>
|
||||
Practice This Problem
|
||||
</button>
|
||||
)}
|
||||
{isCompleted && (
|
||||
<div
|
||||
data-status="completed"
|
||||
className={css({
|
||||
padding: '0.5rem 1rem',
|
||||
fontSize: '0.875rem',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: isDark ? 'gray.700' : 'gray.200',
|
||||
color: isDark ? 'gray.400' : 'gray.600',
|
||||
})}
|
||||
>
|
||||
This problem was already completed
|
||||
</div>
|
||||
)}
|
||||
{isUpcoming && (
|
||||
<div
|
||||
data-status="upcoming"
|
||||
className={css({
|
||||
padding: '0.5rem 1rem',
|
||||
fontSize: '0.875rem',
|
||||
borderRadius: '6px',
|
||||
backgroundColor: isDark ? 'gray.700' : 'gray.200',
|
||||
color: isDark ? 'gray.400' : 'gray.600',
|
||||
})}
|
||||
>
|
||||
This problem hasn't been reached yet
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
data-action={isPracticing ? 'close-practice' : 'practice-this-problem'}
|
||||
onClick={() => setIsPracticing((prev) => !prev)}
|
||||
className={css({
|
||||
padding: '0.75rem 1.5rem',
|
||||
fontSize: '1rem',
|
||||
fontWeight: 'bold',
|
||||
borderRadius: '8px',
|
||||
border: isPracticing ? '2px solid' : 'none',
|
||||
borderColor: isPracticing ? (isDark ? 'gray.500' : 'gray.400') : undefined,
|
||||
cursor: 'pointer',
|
||||
backgroundColor: isPracticing ? 'transparent' : isDark ? 'green.600' : 'green.500',
|
||||
color: isPracticing ? (isDark ? 'gray.300' : 'gray.600') : 'white',
|
||||
transition: 'all 0.15s ease',
|
||||
_hover: {
|
||||
backgroundColor: isPracticing
|
||||
? isDark
|
||||
? 'gray.700'
|
||||
: 'gray.100'
|
||||
: isDark
|
||||
? 'green.500'
|
||||
: 'green.600',
|
||||
transform: 'scale(1.02)',
|
||||
},
|
||||
_active: {
|
||||
transform: 'scale(0.98)',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{isPracticing ? 'Close Practice Panel' : 'Practice This Problem'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Inline Practice Preview - shown when practicing */}
|
||||
{isPracticing && (
|
||||
<div
|
||||
data-element="practice-panel"
|
||||
className={css({
|
||||
padding: '1rem',
|
||||
backgroundColor: isDark ? 'blue.950' : 'blue.50',
|
||||
borderRadius: '12px',
|
||||
border: '2px solid',
|
||||
borderColor: isDark ? 'blue.800' : 'blue.200',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.5rem',
|
||||
marginBottom: '1rem',
|
||||
paddingBottom: '0.75rem',
|
||||
borderBottom: '1px solid',
|
||||
borderColor: isDark ? 'blue.800' : 'blue.200',
|
||||
})}
|
||||
>
|
||||
<span
|
||||
className={css({
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 'bold',
|
||||
color: isDark ? 'blue.200' : 'blue.700',
|
||||
})}
|
||||
>
|
||||
Practice Panel
|
||||
</span>
|
||||
<span
|
||||
className={css({
|
||||
fontSize: '0.75rem',
|
||||
color: isDark ? 'blue.400' : 'blue.500',
|
||||
})}
|
||||
>
|
||||
(does not affect session)
|
||||
</span>
|
||||
</div>
|
||||
<PracticePreview
|
||||
slot={currentItem.slot}
|
||||
part={currentItem.part}
|
||||
problemNumber={browseIndex + 1}
|
||||
onBack={() => setIsPracticing(false)}
|
||||
inline
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Status indicator */}
|
||||
{(isCompleted || isUpcoming || isCurrentPractice) && (
|
||||
<div
|
||||
data-element="status-indicator"
|
||||
className={css({
|
||||
textAlign: 'center',
|
||||
fontSize: '0.75rem',
|
||||
color: isDark ? 'gray.500' : 'gray.500',
|
||||
})}
|
||||
>
|
||||
{isCurrentPractice && '(Current problem in session)'}
|
||||
{isCompleted && '(Already completed in session)'}
|
||||
{isUpcoming && '(Not yet reached in session)'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,447 @@
|
|||
/**
|
||||
* Practice Preview Component
|
||||
*
|
||||
* A sandbox practice interface for debugging any problem without
|
||||
* affecting the actual session state. Shows the full practice UI
|
||||
* (problem, abacus, input) and lets you submit answers to see feedback.
|
||||
*/
|
||||
|
||||
'use client'
|
||||
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { useTheme } from '@/contexts/ThemeContext'
|
||||
import type { ProblemSlot, SessionPart } from '@/db/schema/session-plans'
|
||||
import { css } from '../../../styled-system/css'
|
||||
import { AbacusDock } from '../AbacusDock'
|
||||
import { useHasPhysicalKeyboard } from './hooks/useDeviceDetection'
|
||||
import { NumericKeypad } from './NumericKeypad'
|
||||
import { VerticalProblem } from './VerticalProblem'
|
||||
|
||||
export interface PracticePreviewProps {
|
||||
/** The problem slot to practice */
|
||||
slot: ProblemSlot
|
||||
/** The part this problem belongs to */
|
||||
part: SessionPart
|
||||
/** Problem number for display */
|
||||
problemNumber: number
|
||||
/** Called when user wants to exit practice preview */
|
||||
onBack: () => void
|
||||
/** If true, renders in a more compact inline style without header */
|
||||
inline?: boolean
|
||||
}
|
||||
|
||||
export function PracticePreview({
|
||||
slot,
|
||||
part,
|
||||
problemNumber,
|
||||
onBack,
|
||||
inline = false,
|
||||
}: PracticePreviewProps) {
|
||||
const { resolvedTheme } = useTheme()
|
||||
const isDark = resolvedTheme === 'dark'
|
||||
|
||||
// User's answer (as string for input)
|
||||
const [userAnswer, setUserAnswer] = useState<string>('')
|
||||
// Whether the answer has been submitted
|
||||
const [isSubmitted, setIsSubmitted] = useState(false)
|
||||
// Track if answer was correct
|
||||
const [wasCorrect, setWasCorrect] = useState<boolean | null>(null)
|
||||
|
||||
const problem = slot.problem
|
||||
const correctAnswer = problem?.answer
|
||||
|
||||
// Handle digit input
|
||||
const handleDigit = useCallback(
|
||||
(digit: string) => {
|
||||
if (isSubmitted || correctAnswer === undefined) return
|
||||
setUserAnswer((prev) => {
|
||||
// Max digits based on correct answer length + 1
|
||||
const maxDigits = String(correctAnswer).length + 1
|
||||
if (prev.length >= maxDigits) return prev
|
||||
return prev + digit
|
||||
})
|
||||
},
|
||||
[isSubmitted, correctAnswer]
|
||||
)
|
||||
|
||||
// Handle backspace
|
||||
const handleBackspace = useCallback(() => {
|
||||
if (isSubmitted) return
|
||||
setUserAnswer((prev) => prev.slice(0, -1))
|
||||
}, [isSubmitted])
|
||||
|
||||
// Handle submit
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (!userAnswer || isSubmitted || correctAnswer === undefined) return
|
||||
const numericAnswer = parseInt(userAnswer, 10)
|
||||
const correct = numericAnswer === correctAnswer
|
||||
setWasCorrect(correct)
|
||||
setIsSubmitted(true)
|
||||
}, [userAnswer, isSubmitted, correctAnswer])
|
||||
|
||||
// Handle retry (reset state)
|
||||
const handleRetry = useCallback(() => {
|
||||
setUserAnswer('')
|
||||
setIsSubmitted(false)
|
||||
setWasCorrect(null)
|
||||
}, [])
|
||||
|
||||
// Physical keyboard support
|
||||
const hasPhysicalKeyboard = useHasPhysicalKeyboard()
|
||||
const canAcceptInput = !isSubmitted && correctAnswer !== undefined
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasPhysicalKeyboard || !canAcceptInput) return
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
// Handle digit keys (0-9)
|
||||
if (/^[0-9]$/.test(e.key)) {
|
||||
e.preventDefault()
|
||||
handleDigit(e.key)
|
||||
return
|
||||
}
|
||||
|
||||
// Handle backspace
|
||||
if (e.key === 'Backspace') {
|
||||
e.preventDefault()
|
||||
handleBackspace()
|
||||
return
|
||||
}
|
||||
|
||||
// Handle enter for submit
|
||||
if (e.key === 'Enter' && userAnswer) {
|
||||
e.preventDefault()
|
||||
handleSubmit()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||
}, [hasPhysicalKeyboard, canAcceptInput, handleDigit, handleBackspace, handleSubmit, userAnswer])
|
||||
|
||||
// If no problem generated yet, show a message
|
||||
if (!problem) {
|
||||
return (
|
||||
<div
|
||||
data-component="practice-preview"
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
padding: '1rem',
|
||||
maxWidth: '600px',
|
||||
margin: '0 auto',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
data-element="preview-header"
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '1rem',
|
||||
padding: '0.5rem',
|
||||
backgroundColor: isDark ? 'blue.900' : 'blue.50',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid',
|
||||
borderColor: isDark ? 'blue.700' : 'blue.200',
|
||||
})}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
data-action="back-to-browse"
|
||||
onClick={onBack}
|
||||
className={css({
|
||||
padding: '0.5rem 1rem',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 'bold',
|
||||
borderRadius: '6px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: isDark ? 'gray.600' : 'gray.300',
|
||||
color: isDark ? 'white' : 'gray.800',
|
||||
})}
|
||||
>
|
||||
← Back
|
||||
</button>
|
||||
<div
|
||||
className={css({
|
||||
flex: 1,
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 'bold',
|
||||
color: isDark ? 'blue.200' : 'blue.700',
|
||||
})}
|
||||
>
|
||||
Practice Preview - Problem #{problemNumber}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
padding: '2rem',
|
||||
textAlign: 'center',
|
||||
color: isDark ? 'gray.400' : 'gray.600',
|
||||
backgroundColor: isDark ? 'gray.800' : 'gray.100',
|
||||
borderRadius: '8px',
|
||||
})}
|
||||
>
|
||||
This problem hasn't been generated yet.
|
||||
<br />
|
||||
Problems are generated when reached in the session.
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-component="practice-preview"
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
padding: '1rem',
|
||||
maxWidth: '600px',
|
||||
margin: '0 auto',
|
||||
})}
|
||||
>
|
||||
{/* Header with back button - hidden in inline mode */}
|
||||
{!inline && (
|
||||
<div
|
||||
data-element="preview-header"
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '1rem',
|
||||
padding: '0.5rem',
|
||||
backgroundColor: isDark ? 'blue.900' : 'blue.50',
|
||||
borderRadius: '8px',
|
||||
border: '1px solid',
|
||||
borderColor: isDark ? 'blue.700' : 'blue.200',
|
||||
})}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
data-action="back-to-browse"
|
||||
onClick={onBack}
|
||||
className={css({
|
||||
padding: '0.5rem 1rem',
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 'bold',
|
||||
borderRadius: '6px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: isDark ? 'gray.600' : 'gray.300',
|
||||
color: isDark ? 'white' : 'gray.800',
|
||||
transition: 'all 0.15s ease',
|
||||
_hover: {
|
||||
backgroundColor: isDark ? 'gray.500' : 'gray.400',
|
||||
},
|
||||
})}
|
||||
>
|
||||
← Back
|
||||
</button>
|
||||
<div
|
||||
className={css({
|
||||
flex: 1,
|
||||
fontSize: '0.875rem',
|
||||
fontWeight: 'bold',
|
||||
color: isDark ? 'blue.200' : 'blue.700',
|
||||
})}
|
||||
>
|
||||
Practice Preview - Problem #{problemNumber}
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: '0.75rem',
|
||||
color: isDark ? 'blue.300' : 'blue.600',
|
||||
padding: '0.25rem 0.5rem',
|
||||
backgroundColor: isDark ? 'blue.800' : 'blue.100',
|
||||
borderRadius: '4px',
|
||||
})}
|
||||
>
|
||||
{part.type}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Debug info - hidden in inline mode */}
|
||||
{!inline && (
|
||||
<div
|
||||
data-element="debug-info"
|
||||
className={css({
|
||||
fontSize: '0.75rem',
|
||||
color: isDark ? 'gray.400' : 'gray.500',
|
||||
padding: '0.5rem',
|
||||
backgroundColor: isDark ? 'gray.800' : 'gray.100',
|
||||
borderRadius: '4px',
|
||||
fontFamily: 'monospace',
|
||||
})}
|
||||
>
|
||||
Answer: {correctAnswer} | Format: {part.format} | Purpose: {slot.purpose}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Problem display */}
|
||||
<div
|
||||
data-element="problem-area"
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '1.5rem',
|
||||
padding: '2rem',
|
||||
backgroundColor: isDark ? 'gray.800' : 'white',
|
||||
borderRadius: '12px',
|
||||
border: '1px solid',
|
||||
borderColor: isDark ? 'gray.700' : 'gray.200',
|
||||
})}
|
||||
>
|
||||
<VerticalProblem
|
||||
terms={problem.terms}
|
||||
userAnswer={userAnswer}
|
||||
isCompleted={isSubmitted}
|
||||
correctAnswer={correctAnswer}
|
||||
size="large"
|
||||
generationTrace={problem.generationTrace}
|
||||
/>
|
||||
|
||||
{/* Feedback */}
|
||||
{isSubmitted && (
|
||||
<div
|
||||
data-element="feedback"
|
||||
className={css({
|
||||
padding: '0.75rem 1.5rem',
|
||||
borderRadius: '8px',
|
||||
fontSize: '1.25rem',
|
||||
fontWeight: 'bold',
|
||||
backgroundColor: wasCorrect
|
||||
? isDark
|
||||
? 'green.900'
|
||||
: 'green.100'
|
||||
: isDark
|
||||
? 'red.900'
|
||||
: 'red.100',
|
||||
color: wasCorrect
|
||||
? isDark
|
||||
? 'green.200'
|
||||
: 'green.700'
|
||||
: isDark
|
||||
? 'red.200'
|
||||
: 'red.700',
|
||||
border: '2px solid',
|
||||
borderColor: wasCorrect
|
||||
? isDark
|
||||
? 'green.700'
|
||||
: 'green.300'
|
||||
: isDark
|
||||
? 'red.700'
|
||||
: 'red.300',
|
||||
})}
|
||||
>
|
||||
{wasCorrect ? '✓ Correct!' : `✗ Incorrect (answer: ${correctAnswer})`}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Input area */}
|
||||
{!isSubmitted && (
|
||||
<div
|
||||
data-element="input-area"
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '1rem',
|
||||
alignItems: 'center',
|
||||
})}
|
||||
>
|
||||
{/* Current answer display */}
|
||||
<div
|
||||
className={css({
|
||||
fontSize: '2rem',
|
||||
fontWeight: 'bold',
|
||||
minHeight: '3rem',
|
||||
padding: '0.5rem 1rem',
|
||||
backgroundColor: isDark ? 'gray.700' : 'gray.100',
|
||||
borderRadius: '8px',
|
||||
minWidth: '120px',
|
||||
textAlign: 'center',
|
||||
color: isDark ? 'white' : 'gray.900',
|
||||
})}
|
||||
>
|
||||
{userAnswer || '_'}
|
||||
</div>
|
||||
|
||||
{/* Numpad */}
|
||||
<NumericKeypad
|
||||
onDigit={handleDigit}
|
||||
onBackspace={handleBackspace}
|
||||
onSubmit={handleSubmit}
|
||||
disabled={!userAnswer}
|
||||
showSubmitButton={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* After submit - retry button */}
|
||||
{isSubmitted && (
|
||||
<div
|
||||
data-element="retry-area"
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
gap: '1rem',
|
||||
})}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
data-action="retry"
|
||||
onClick={handleRetry}
|
||||
className={css({
|
||||
padding: '0.75rem 1.5rem',
|
||||
fontSize: '1rem',
|
||||
fontWeight: 'bold',
|
||||
borderRadius: '8px',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: isDark ? 'blue.600' : 'blue.500',
|
||||
color: 'white',
|
||||
transition: 'all 0.15s ease',
|
||||
_hover: {
|
||||
backgroundColor: isDark ? 'blue.500' : 'blue.600',
|
||||
},
|
||||
})}
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
{!inline && (
|
||||
<button
|
||||
type="button"
|
||||
data-action="back-after-submit"
|
||||
onClick={onBack}
|
||||
className={css({
|
||||
padding: '0.75rem 1.5rem',
|
||||
fontSize: '1rem',
|
||||
fontWeight: 'bold',
|
||||
borderRadius: '8px',
|
||||
border: '2px solid',
|
||||
borderColor: isDark ? 'gray.500' : 'gray.400',
|
||||
cursor: 'pointer',
|
||||
backgroundColor: 'transparent',
|
||||
color: isDark ? 'gray.300' : 'gray.600',
|
||||
transition: 'all 0.15s ease',
|
||||
_hover: {
|
||||
backgroundColor: isDark ? 'gray.700' : 'gray.100',
|
||||
},
|
||||
})}
|
||||
>
|
||||
Back to Browse
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Abacus dock - hidden in inline mode */}
|
||||
{!inline && <AbacusDock />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue