feat: add auto-submit on correct answer + Newton poem blog post
Practice session improvements:
- Auto-submit when correct answer entered with ≤2 corrections
- Show celebration animation ("Perfect!") before auto-submit
- Display prefix sum checkmarks/arrows before clicking "Get Help"
New blog post: "The Fluxion of Fortune"
- Poem about Newton losing money in the South Sea Bubble
- Hero image of Newton with his calculations and sinking ships
- Custom CSS for properly centered x-bar notation
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
4f25e1fd52
commit
2f7cb03c3f
|
|
@ -131,11 +131,14 @@
|
||||||
"Bash(docs/DAILY_PRACTICE_SYSTEM.md )",
|
"Bash(docs/DAILY_PRACTICE_SYSTEM.md )",
|
||||||
"Bash(../../README.md )",
|
"Bash(../../README.md )",
|
||||||
"Bash(.claude/CLAUDE.md)",
|
"Bash(.claude/CLAUDE.md)",
|
||||||
"Bash(mcp__sqlite__describe_table:*)"
|
"Bash(mcp__sqlite__describe_table:*)",
|
||||||
|
"Bash(ls:*)"
|
||||||
],
|
],
|
||||||
"deny": [],
|
"deny": [],
|
||||||
"ask": []
|
"ask": []
|
||||||
},
|
},
|
||||||
"enableAllProjectMcpServers": true,
|
"enableAllProjectMcpServers": true,
|
||||||
"enabledMcpjsonServers": ["sqlite"]
|
"enabledMcpjsonServers": [
|
||||||
|
"sqlite"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,45 @@
|
||||||
|
---
|
||||||
|
title: "The Fluxion of Fortune"
|
||||||
|
description: "for Isaac, who mastered the Mint but not the market"
|
||||||
|
author: "Abaci.one Team"
|
||||||
|
publishedAt: "2025-12-07"
|
||||||
|
updatedAt: "2025-12-07"
|
||||||
|
tags: ["poetry", "history", "mathematics", "newton", "calculus"]
|
||||||
|
featured: false
|
||||||
|
---
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Pronunciation Guide:** ẋ is "ex dot" · <span style="position: relative;">x<span style="position: absolute; top: -0.25em; left: 0; right: 0; text-align: center;">̄</span></span> is "ex bar"
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
In Woolsthorpe nights of candle-glow,
|
||||||
|
He watched the apple fall just so—
|
||||||
|
A whisper of ẋ murmured low,
|
||||||
|
A hint of what he'd one day know.
|
||||||
|
|
||||||
|
He carved the world in curves of light,
|
||||||
|
Bent stars with math he birthed outright;
|
||||||
|
But markets?—aye, a different fight,
|
||||||
|
Whose tides he failed to fluxion right.
|
||||||
|
|
||||||
|
For coin obeys no cosmic plot,
|
||||||
|
It drifts where fevered humors rot;
|
||||||
|
He'd trace a comet's path on the spot—
|
||||||
|
Yet never guessed his stocks were naught.
|
||||||
|
|
||||||
|
He could have tamed each rising bar,
|
||||||
|
Mapped fortunes by <span style="position: relative;">x<span style="position: absolute; top: -0.25em; left: 0; right: 0; text-align: center;">̄</span></span> afar,
|
||||||
|
And steered his purse like some bright star—
|
||||||
|
Not wrecked upon a South Sea scar.
|
||||||
|
|
||||||
|
But genius trips where mortals trot,
|
||||||
|
And even Newton, spared no blot,
|
||||||
|
Might've saved his gold—had he used ẋ
|
||||||
|
To see the crash begot.
|
||||||
|
|
||||||
|
And thus his wealth slipped through the sieve,
|
||||||
|
With no derivative to give;
|
||||||
|
He mastered laws by which stars live,
|
||||||
|
But found the Market... negative.
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 493 KiB |
|
|
@ -1,6 +1,8 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
|
import { useTheme } from '@/contexts/ThemeContext'
|
||||||
|
import type { StudentHelpSettings } from '@/db/schema/players'
|
||||||
import type {
|
import type {
|
||||||
GeneratedProblem,
|
GeneratedProblem,
|
||||||
ProblemConstraints,
|
ProblemConstraints,
|
||||||
|
|
@ -10,8 +12,6 @@ import type {
|
||||||
SessionPlan,
|
SessionPlan,
|
||||||
SlotResult,
|
SlotResult,
|
||||||
} from '@/db/schema/session-plans'
|
} from '@/db/schema/session-plans'
|
||||||
import type { StudentHelpSettings } from '@/db/schema/players'
|
|
||||||
import { useTheme } from '@/contexts/ThemeContext'
|
|
||||||
import { usePracticeHelp } from '@/hooks/usePracticeHelp'
|
import { usePracticeHelp } from '@/hooks/usePracticeHelp'
|
||||||
import { createBasicSkillSet, type SkillSet } from '@/types/tutorial'
|
import { createBasicSkillSet, type SkillSet } from '@/types/tutorial'
|
||||||
import {
|
import {
|
||||||
|
|
@ -220,6 +220,10 @@ export function ActiveSession({
|
||||||
const [confirmedTermCount, setConfirmedTermCount] = useState(0)
|
const [confirmedTermCount, setConfirmedTermCount] = useState(0)
|
||||||
// Which term we're currently showing help for (null = not showing help)
|
// Which term we're currently showing help for (null = not showing help)
|
||||||
const [helpTermIndex, setHelpTermIndex] = useState<number | null>(null)
|
const [helpTermIndex, setHelpTermIndex] = useState<number | null>(null)
|
||||||
|
// Track corrections for auto-submit (allow 1 correction, then require manual submit)
|
||||||
|
const [correctionCount, setCorrectionCount] = useState(0)
|
||||||
|
// Track if auto-submit was triggered (for celebration animation)
|
||||||
|
const [autoSubmitTriggered, setAutoSubmitTriggered] = useState(false)
|
||||||
|
|
||||||
const hasPhysicalKeyboard = useHasPhysicalKeyboard()
|
const hasPhysicalKeyboard = useHasPhysicalKeyboard()
|
||||||
|
|
||||||
|
|
@ -374,35 +378,17 @@ export function ActiveSession({
|
||||||
}
|
}
|
||||||
}, [currentPart, currentSlot, currentPartIndex, currentSlotIndex, currentProblem])
|
}, [currentPart, currentSlot, currentPartIndex, currentSlotIndex, currentProblem])
|
||||||
|
|
||||||
// Handle keyboard input
|
|
||||||
useEffect(() => {
|
|
||||||
if (!hasPhysicalKeyboard || isPaused || !currentProblem || isSubmitting) return
|
|
||||||
|
|
||||||
const handleKeyDown = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === 'Backspace' || e.key === 'Delete') {
|
|
||||||
e.preventDefault()
|
|
||||||
setUserAnswer((prev) => prev.slice(0, -1))
|
|
||||||
} else if (e.key === 'Enter') {
|
|
||||||
e.preventDefault()
|
|
||||||
handleSubmit()
|
|
||||||
} else if (/^[0-9]$/.test(e.key)) {
|
|
||||||
setUserAnswer((prev) => prev + e.key)
|
|
||||||
} else if (e.key === '-' && userAnswer.length === 0) {
|
|
||||||
// Allow negative sign at start
|
|
||||||
setUserAnswer('-')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener('keydown', handleKeyDown)
|
|
||||||
return () => document.removeEventListener('keydown', handleKeyDown)
|
|
||||||
}, [hasPhysicalKeyboard, isPaused, currentProblem, isSubmitting, userAnswer])
|
|
||||||
|
|
||||||
const handleDigit = useCallback((digit: string) => {
|
const handleDigit = useCallback((digit: string) => {
|
||||||
setUserAnswer((prev) => prev + digit)
|
setUserAnswer((prev) => prev + digit)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleBackspace = useCallback(() => {
|
const handleBackspace = useCallback(() => {
|
||||||
setUserAnswer((prev) => prev.slice(0, -1))
|
setUserAnswer((prev) => {
|
||||||
|
if (prev.length > 0) {
|
||||||
|
setCorrectionCount((c) => c + 1)
|
||||||
|
}
|
||||||
|
return prev.slice(0, -1)
|
||||||
|
})
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// Handle "Get Help" button - show help for the next term
|
// Handle "Get Help" button - show help for the next term
|
||||||
|
|
@ -494,6 +480,8 @@ export function ActiveSession({
|
||||||
setConfirmedTermCount(0)
|
setConfirmedTermCount(0)
|
||||||
setHelpTermIndex(null)
|
setHelpTermIndex(null)
|
||||||
setIsSubmitting(false)
|
setIsSubmitting(false)
|
||||||
|
setCorrectionCount(0)
|
||||||
|
setAutoSubmitTriggered(false)
|
||||||
},
|
},
|
||||||
isCorrect ? 500 : 1500
|
isCorrect ? 500 : 1500
|
||||||
)
|
)
|
||||||
|
|
@ -510,6 +498,55 @@ export function ActiveSession({
|
||||||
helpActions,
|
helpActions,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
// Auto-submit when correct answer is entered on first attempt (allow minor corrections)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!currentProblem || isSubmitting || feedback !== 'none' || !userAnswer) return
|
||||||
|
// Allow up to 2 backspaces (one typo fix), but no more
|
||||||
|
if (correctionCount > 2) return
|
||||||
|
|
||||||
|
const answerNum = parseInt(userAnswer, 10)
|
||||||
|
if (Number.isNaN(answerNum)) return
|
||||||
|
|
||||||
|
// Check if answer matches
|
||||||
|
if (answerNum === currentProblem.problem.answer) {
|
||||||
|
// Trigger auto-submit with celebration
|
||||||
|
setAutoSubmitTriggered(true)
|
||||||
|
// Small delay to show the celebration animation before submitting
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
handleSubmit()
|
||||||
|
}, 400)
|
||||||
|
return () => clearTimeout(timer)
|
||||||
|
}
|
||||||
|
}, [userAnswer, currentProblem, isSubmitting, feedback, correctionCount, handleSubmit])
|
||||||
|
|
||||||
|
// Handle keyboard input (placed after handleSubmit to avoid temporal dead zone)
|
||||||
|
useEffect(() => {
|
||||||
|
if (!hasPhysicalKeyboard || isPaused || !currentProblem || isSubmitting) return
|
||||||
|
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (e.key === 'Backspace' || e.key === 'Delete') {
|
||||||
|
e.preventDefault()
|
||||||
|
setUserAnswer((prev) => {
|
||||||
|
if (prev.length > 0) {
|
||||||
|
setCorrectionCount((c) => c + 1)
|
||||||
|
}
|
||||||
|
return prev.slice(0, -1)
|
||||||
|
})
|
||||||
|
} else if (e.key === 'Enter') {
|
||||||
|
e.preventDefault()
|
||||||
|
handleSubmit()
|
||||||
|
} else if (/^[0-9]$/.test(e.key)) {
|
||||||
|
setUserAnswer((prev) => prev + e.key)
|
||||||
|
} else if (e.key === '-' && userAnswer.length === 0) {
|
||||||
|
// Allow negative sign at start
|
||||||
|
setUserAnswer('-')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
|
return () => document.removeEventListener('keydown', handleKeyDown)
|
||||||
|
}, [hasPhysicalKeyboard, isPaused, currentProblem, isSubmitting, userAnswer, handleSubmit])
|
||||||
|
|
||||||
const handlePause = useCallback(() => {
|
const handlePause = useCallback(() => {
|
||||||
setIsPaused(true)
|
setIsPaused(true)
|
||||||
onPause?.()
|
onPause?.()
|
||||||
|
|
@ -891,6 +928,12 @@ export function ActiveSession({
|
||||||
size="large"
|
size="large"
|
||||||
confirmedTermCount={confirmedTermCount}
|
confirmedTermCount={confirmedTermCount}
|
||||||
currentHelpTermIndex={helpTermIndex ?? undefined}
|
currentHelpTermIndex={helpTermIndex ?? undefined}
|
||||||
|
detectedPrefixIndex={
|
||||||
|
matchedPrefixIndex >= 0 && matchedPrefixIndex < prefixSums.length - 1
|
||||||
|
? matchedPrefixIndex
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
autoSubmitPending={autoSubmitTriggered}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<LinearProblem
|
<LinearProblem
|
||||||
|
|
@ -951,6 +994,71 @@ export function ActiveSession({
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Provenance breakdown - shows which digits come from the term */}
|
||||||
|
{helpState.content?.beadSteps && helpState.content.beadSteps.length > 0 && (
|
||||||
|
<div
|
||||||
|
data-element="provenance-breakdown"
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: '0.5rem',
|
||||||
|
marginBottom: '0.75rem',
|
||||||
|
padding: '0.5rem',
|
||||||
|
backgroundColor: isDark ? 'gray.800' : 'white',
|
||||||
|
borderRadius: '8px',
|
||||||
|
border: '1px solid',
|
||||||
|
borderColor: isDark ? 'purple.700' : 'purple.200',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{/* Group steps by unique provenance to show digit breakdown */}
|
||||||
|
{(() => {
|
||||||
|
const seenPlaces = new Set<string>()
|
||||||
|
return helpState.content?.beadSteps
|
||||||
|
.filter((step) => {
|
||||||
|
if (!step.provenance) return false
|
||||||
|
const key = `${step.provenance.rhsPlace}-${step.provenance.rhsDigit}`
|
||||||
|
if (seenPlaces.has(key)) return false
|
||||||
|
seenPlaces.add(key)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
.map((step, idx) => {
|
||||||
|
const prov = step.provenance
|
||||||
|
if (!prov) return null
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
key={idx}
|
||||||
|
data-element="provenance-chip"
|
||||||
|
className={css({
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.25rem',
|
||||||
|
padding: '0.25rem 0.5rem',
|
||||||
|
backgroundColor: isDark ? 'purple.800' : 'purple.100',
|
||||||
|
borderRadius: '6px',
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={css({
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: isDark ? 'purple.200' : 'purple.800',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{prov.rhsDigit}
|
||||||
|
</span>
|
||||||
|
<span className={css({ color: isDark ? 'gray.400' : 'gray.600' })}>
|
||||||
|
{prov.rhsPlaceName}
|
||||||
|
</span>
|
||||||
|
<span className={css({ color: isDark ? 'gray.500' : 'gray.400' })}>
|
||||||
|
= {prov.rhsValue}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<HelpAbacus
|
<HelpAbacus
|
||||||
currentValue={helpContext.currentValue}
|
currentValue={helpContext.currentValue}
|
||||||
targetValue={helpContext.targetValue}
|
targetValue={helpContext.targetValue}
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ interface VerticalProblemProps {
|
||||||
confirmedTermCount?: number
|
confirmedTermCount?: number
|
||||||
/** Index of the term currently being helped with (highlighted) */
|
/** Index of the term currently being helped with (highlighted) */
|
||||||
currentHelpTermIndex?: number
|
currentHelpTermIndex?: number
|
||||||
|
/** Detected prefix index - shows preview checkmarks/arrow before user clicks "Get Help" */
|
||||||
|
detectedPrefixIndex?: number
|
||||||
|
/** Whether auto-submit is about to trigger (shows celebration animation) */
|
||||||
|
autoSubmitPending?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -40,6 +44,8 @@ export function VerticalProblem({
|
||||||
size = 'normal',
|
size = 'normal',
|
||||||
confirmedTermCount = 0,
|
confirmedTermCount = 0,
|
||||||
currentHelpTermIndex,
|
currentHelpTermIndex,
|
||||||
|
detectedPrefixIndex,
|
||||||
|
autoSubmitPending = false,
|
||||||
}: VerticalProblemProps) {
|
}: VerticalProblemProps) {
|
||||||
const { resolvedTheme } = useTheme()
|
const { resolvedTheme } = useTheme()
|
||||||
const isDark = resolvedTheme === 'dark'
|
const isDark = resolvedTheme === 'dark'
|
||||||
|
|
@ -113,29 +119,52 @@ export function VerticalProblem({
|
||||||
// Term status for highlighting
|
// Term status for highlighting
|
||||||
const isConfirmed = index < confirmedTermCount
|
const isConfirmed = index < confirmedTermCount
|
||||||
const isCurrentHelp = index === currentHelpTermIndex
|
const isCurrentHelp = index === currentHelpTermIndex
|
||||||
|
// Preview states - shown when user's input matches a prefix sum (before clicking "Get Help")
|
||||||
|
const isPreviewConfirmed =
|
||||||
|
detectedPrefixIndex !== undefined && index <= detectedPrefixIndex && !isConfirmed
|
||||||
|
const isPreviewNext =
|
||||||
|
detectedPrefixIndex !== undefined &&
|
||||||
|
index === detectedPrefixIndex + 1 &&
|
||||||
|
!isCurrentHelp &&
|
||||||
|
detectedPrefixIndex < terms.length - 1 // Don't show if prefix is the full answer
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
key={index}
|
key={index}
|
||||||
data-element="term-row"
|
data-element="term-row"
|
||||||
data-term-status={isConfirmed ? 'confirmed' : isCurrentHelp ? 'current' : 'pending'}
|
data-term-status={
|
||||||
|
isConfirmed
|
||||||
|
? 'confirmed'
|
||||||
|
: isCurrentHelp
|
||||||
|
? 'current'
|
||||||
|
: isPreviewConfirmed
|
||||||
|
? 'preview-confirmed'
|
||||||
|
: isPreviewNext
|
||||||
|
? 'preview-next'
|
||||||
|
: 'pending'
|
||||||
|
}
|
||||||
className={css({
|
className={css({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '2px',
|
gap: '2px',
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
// Confirmed terms are dimmed with checkmark
|
// Confirmed terms are dimmed with checkmark
|
||||||
opacity: isConfirmed ? 0.5 : 1,
|
opacity: isConfirmed ? 0.5 : isPreviewConfirmed ? 0.7 : 1,
|
||||||
// Current help term is highlighted
|
// Current help term is highlighted
|
||||||
backgroundColor: isCurrentHelp
|
backgroundColor: isCurrentHelp
|
||||||
? isDark
|
? isDark
|
||||||
? 'purple.800'
|
? 'purple.800'
|
||||||
: 'purple.100'
|
: 'purple.100'
|
||||||
: 'transparent',
|
: isPreviewNext
|
||||||
borderRadius: isCurrentHelp ? '4px' : '0',
|
? isDark
|
||||||
padding: isCurrentHelp ? '2px 4px' : '0',
|
? 'yellow.900'
|
||||||
marginLeft: isCurrentHelp ? '-4px' : '0',
|
: 'yellow.50'
|
||||||
marginRight: isCurrentHelp ? '-4px' : '0',
|
: 'transparent',
|
||||||
|
borderRadius: isCurrentHelp || isPreviewNext ? '4px' : '0',
|
||||||
|
padding: isCurrentHelp || isPreviewNext ? '2px 4px' : '0',
|
||||||
|
marginLeft: isCurrentHelp || isPreviewNext ? '-4px' : '0',
|
||||||
|
marginRight: isCurrentHelp || isPreviewNext ? '-4px' : '0',
|
||||||
|
transition: 'all 0.2s ease',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{/* Checkmark for confirmed terms */}
|
{/* Checkmark for confirmed terms */}
|
||||||
|
|
@ -153,6 +182,22 @@ export function VerticalProblem({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Preview checkmark for detected prefix terms (shown in muted color with subtle pulse) */}
|
||||||
|
{isPreviewConfirmed && (
|
||||||
|
<div
|
||||||
|
data-element="preview-check"
|
||||||
|
className={css({
|
||||||
|
position: 'absolute',
|
||||||
|
left: '-1.5rem',
|
||||||
|
color: isDark ? 'yellow.400' : 'yellow.600',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
opacity: 0.8,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
✓
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Arrow indicator for current help term */}
|
{/* Arrow indicator for current help term */}
|
||||||
{isCurrentHelp && (
|
{isCurrentHelp && (
|
||||||
<div
|
<div
|
||||||
|
|
@ -168,6 +213,21 @@ export function VerticalProblem({
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Preview arrow for next term after detected prefix */}
|
||||||
|
{isPreviewNext && (
|
||||||
|
<div
|
||||||
|
data-element="preview-arrow"
|
||||||
|
className={css({
|
||||||
|
position: 'absolute',
|
||||||
|
left: '-1.5rem',
|
||||||
|
color: isDark ? 'yellow.400' : 'yellow.600',
|
||||||
|
fontSize: '0.875rem',
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
→
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Operator column (only show minus for negative) */}
|
{/* Operator column (only show minus for negative) */}
|
||||||
<div
|
<div
|
||||||
data-element="operator"
|
data-element="operator"
|
||||||
|
|
@ -226,12 +286,45 @@ export function VerticalProblem({
|
||||||
{/* Answer row */}
|
{/* Answer row */}
|
||||||
<div
|
<div
|
||||||
data-element="answer-row"
|
data-element="answer-row"
|
||||||
|
data-auto-submit={autoSubmitPending ? 'pending' : undefined}
|
||||||
className={css({
|
className={css({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
gap: '2px',
|
gap: '2px',
|
||||||
|
position: 'relative',
|
||||||
|
// Auto-submit celebration animation
|
||||||
|
...(autoSubmitPending && {
|
||||||
|
animation: 'successPulse 0.3s ease-out',
|
||||||
|
}),
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
|
{/* Auto-submit celebration indicator */}
|
||||||
|
{autoSubmitPending && (
|
||||||
|
<div
|
||||||
|
data-element="auto-submit-indicator"
|
||||||
|
className={css({
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-1.5rem',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
gap: '0.25rem',
|
||||||
|
padding: '0.125rem 0.5rem',
|
||||||
|
backgroundColor: isDark ? 'green.700' : 'green.100',
|
||||||
|
borderRadius: '999px',
|
||||||
|
fontSize: '0.625rem',
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: isDark ? 'green.200' : 'green.700',
|
||||||
|
whiteSpace: 'nowrap',
|
||||||
|
animation: 'bounceIn 0.3s ease-out',
|
||||||
|
zIndex: 10,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<span>✓</span>
|
||||||
|
<span>Perfect!</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{/* Equals sign column */}
|
{/* Equals sign column */}
|
||||||
<div
|
<div
|
||||||
data-element="equals"
|
data-element="equals"
|
||||||
|
|
@ -268,36 +361,46 @@ export function VerticalProblem({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
backgroundColor: isCompleted
|
backgroundColor: autoSubmitPending
|
||||||
? isCorrect
|
? isDark
|
||||||
? isDark
|
? 'green.800'
|
||||||
? 'green.800'
|
: 'green.100'
|
||||||
: 'green.100'
|
: isCompleted
|
||||||
|
? isCorrect
|
||||||
|
? isDark
|
||||||
|
? 'green.800'
|
||||||
|
: 'green.100'
|
||||||
|
: isDark
|
||||||
|
? 'red.800'
|
||||||
|
: 'red.100'
|
||||||
: isDark
|
: isDark
|
||||||
? 'red.800'
|
? 'gray.700'
|
||||||
: 'red.100'
|
: 'white',
|
||||||
: isDark
|
|
||||||
? 'gray.700'
|
|
||||||
: 'white',
|
|
||||||
borderRadius: '4px',
|
borderRadius: '4px',
|
||||||
border: isEmpty && !isCompleted ? '1px dashed' : '1px solid',
|
border:
|
||||||
borderColor: isCompleted
|
isEmpty && !isCompleted && !autoSubmitPending ? '1px dashed' : '1px solid',
|
||||||
? isCorrect
|
borderColor: autoSubmitPending
|
||||||
? isDark
|
? isDark
|
||||||
? 'green.600'
|
? 'green.500'
|
||||||
: 'green.300'
|
: 'green.400'
|
||||||
: isDark
|
: isCompleted
|
||||||
? 'red.600'
|
? isCorrect
|
||||||
: 'red.300'
|
? isDark
|
||||||
: isEmpty
|
? 'green.600'
|
||||||
? isFocused
|
: 'green.300'
|
||||||
? 'blue.400'
|
: isDark
|
||||||
|
? 'red.600'
|
||||||
|
: 'red.300'
|
||||||
|
: isEmpty
|
||||||
|
? isFocused
|
||||||
|
? 'blue.400'
|
||||||
|
: isDark
|
||||||
|
? 'gray.600'
|
||||||
|
: 'gray.300'
|
||||||
: isDark
|
: isDark
|
||||||
? 'gray.600'
|
? 'gray.600'
|
||||||
: 'gray.300'
|
: 'gray.300',
|
||||||
: isDark
|
transition: 'all 0.15s ease-out',
|
||||||
? 'gray.600'
|
|
||||||
: 'gray.300',
|
|
||||||
color: isCompleted
|
color: isCompleted
|
||||||
? isCorrect
|
? isCorrect
|
||||||
? isDark
|
? isDark
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue