diff --git a/apps/web/src/components/InteractiveAbacus.tsx b/apps/web/src/components/InteractiveAbacus.tsx
index 324fb458..1b091425 100644
--- a/apps/web/src/components/InteractiveAbacus.tsx
+++ b/apps/web/src/components/InteractiveAbacus.tsx
@@ -42,6 +42,112 @@ export function InteractiveAbacus({
+ // Handle bead clicks to toggle values
+ const handleBeadClick = useCallback((event: Event) => {
+ const target = event.target as Element
+
+ // Find the closest element with bead data attributes
+ const beadElement = target.closest('[data-bead-type]')
+ if (!beadElement) return
+
+ const beadType = beadElement.getAttribute('data-bead-type')
+ const beadColumn = parseInt(beadElement.getAttribute('data-bead-column') || '0')
+ const beadPosition = beadElement.getAttribute('data-bead-position')
+ const isActive = beadElement.getAttribute('data-bead-active') === '1'
+
+ console.log('Bead clicked:', { beadType, beadColumn, beadPosition, isActive })
+ console.log('Current value before click:', currentValue)
+
+ if (beadType === 'earth') {
+ const position = parseInt(beadPosition || '0')
+ const columnPower = Math.pow(10, beadColumn)
+ const currentDigit = Math.floor(currentValue / columnPower) % 10
+ const heavenContribution = Math.floor(currentDigit / 5) * 5
+ const earthContribution = currentDigit % 5
+ console.log('Earth bead analysis:', {
+ position,
+ columnPower,
+ currentDigit,
+ heavenContribution,
+ earthContribution
+ })
+ }
+
+ if (beadType === 'heaven') {
+ // Toggle heaven bead (worth 5)
+ const columnPower = Math.pow(10, beadColumn)
+ const heavenValue = 5 * columnPower
+
+ if (isActive) {
+ // Deactivate heaven bead - subtract 5 from this column
+ setCurrentValue(prev => Math.max(0, prev - heavenValue))
+ } else {
+ // Activate heaven bead - add 5 to this column
+ setCurrentValue(prev => prev + heavenValue)
+ }
+ } else if (beadType === 'earth' && beadPosition) {
+ // Toggle earth bead (worth 1 each)
+ const position = parseInt(beadPosition) // 0-3 where 0 is top (closest to bar), 3 is bottom
+ const columnPower = Math.pow(10, beadColumn)
+
+ // Calculate current digit in this column
+ const currentDigit = Math.floor(currentValue / columnPower) % 10
+ const heavenContribution = Math.floor(currentDigit / 5) * 5
+ const earthContribution = currentDigit % 5
+
+ let newEarthContribution: number
+
+ // Earth beads are numbered 0-3 from top to bottom (0 is closest to bar)
+ // In traditional abacus logic:
+ // - earthContribution represents how many beads are active (0-4)
+ // - Active beads are positions 0, 1, 2, ... up to (earthContribution - 1)
+ // - When you click a bead: toggle that "level" of activation
+
+ if (isActive) {
+ // This bead is currently active, so we deactivate it and all beads below it
+ // If position 2 is clicked and active, we want positions 0,1 to remain active
+ // So earthContribution should be position (2)
+ newEarthContribution = position
+ } else {
+ // This bead is currently inactive, so we activate it and all beads above it
+ // If position 2 is clicked and inactive, we want positions 0,1,2 to be active
+ // So earthContribution should be position + 1 (3)
+ newEarthContribution = position + 1
+ }
+
+ console.log('Earth bead calculation:', {
+ position,
+ isActive,
+ currentEarthContribution: earthContribution,
+ newEarthContribution
+ })
+
+ // Calculate the new digit for this column
+ const newDigit = heavenContribution + newEarthContribution
+
+ // Calculate the new total value
+ const columnContribution = Math.floor(currentValue / columnPower) % 10 * columnPower
+ const newValue = currentValue - columnContribution + (newDigit * columnPower)
+
+ setCurrentValue(Math.max(0, newValue))
+ }
+
+ // Visual feedback
+ setIsChanging(true)
+ setTimeout(() => setIsChanging(false), 150)
+ }, [currentValue])
+
+ // Add click event listener for bead interactions
+ useEffect(() => {
+ const svgContainer = svgRef.current
+ if (!svgContainer) return
+
+ svgContainer.addEventListener('click', handleBeadClick)
+ return () => {
+ svgContainer.removeEventListener('click', handleBeadClick)
+ }
+ }, [handleBeadClick])
+
// Notify parent of value changes
useMemo(() => {
onValueChange?.(currentValue)
@@ -101,7 +207,16 @@ export function InteractiveAbacus({
className={css({
width: '100%',
height: '100%',
- transition: 'all 0.3s ease'
+ transition: 'all 0.3s ease',
+ cursor: 'pointer',
+ '& [data-bead-type]': {
+ cursor: 'pointer',
+ transition: 'all 0.2s ease',
+ _hover: {
+ filter: 'brightness(1.2)',
+ transform: 'scale(1.05)'
+ }
+ }
})}
/>
@@ -213,8 +328,9 @@ export function InteractiveAbacus({
border: '1px solid',
borderColor: 'gray.200'
})}>
- How to use: Use the preset buttons below to set different values.
- The abacus will display the number using traditional soroban bead positions.
+ How to use: Click on the beads to activate or deactivate them!
+ Heaven beads (top) are worth 5 each, earth beads (bottom) are worth 1 each.
+ You can also use the preset buttons below.
diff --git a/apps/web/styled-system/chunks/src__app__games__memory-quiz__page.css b/apps/web/styled-system/chunks/src__app__games__memory-quiz__page.css
index 5b07831d..f87e9424 100644
--- a/apps/web/styled-system/chunks/src__app__games__memory-quiz__page.css
+++ b/apps/web/styled-system/chunks/src__app__games__memory-quiz__page.css
@@ -855,18 +855,6 @@
}
@media (max-width: 768px) {
- .\[\@media_\(max-width\:_768px\)\]\:gap_10px {
- gap: 10px
- }
- }
- @media (max-width: 480px) {
-
- .\[\@media_\(max-width\:_480px\)\]\:gap_8px {
- gap: 8px
- }
- }
- @media (max-width: 768px) {
-
.\[\@media_\(max-width\:_768px\)\]\:h_130px {
height: 130px
}
@@ -899,18 +887,6 @@
min-width: 90px
}
}
- @media (max-width: 768px) {
-
- .\[\@media_\(max-width\:_768px\)\]\:fs_40px {
- font-size: 40px
- }
- }
- @media (max-width: 480px) {
-
- .\[\@media_\(max-width\:_480px\)\]\:fs_32px {
- font-size: 32px
- }
- }
@media screen and (min-width: 48em) {
.md\:px_4 {
@@ -933,6 +909,9 @@
}
@media (max-width: 768px) {
+ .\[\@media_\(max-width\:_768px\)\]\:gap_10px {
+ gap: 10px
+ }
.\[\@media_\(max-width\:_768px\)\]\:h_130px {
height: 130px
}
@@ -940,6 +919,9 @@
.\[\@media_\(max-width\:_768px\)\]\:min-w_100px {
min-width: 100px
}
+ .\[\@media_\(max-width\:_768px\)\]\:fs_40px {
+ font-size: 40px
+ }
.\[\@media_\(max-width\:_768px\)\]\:gap_10px {
gap: 10px
@@ -959,6 +941,9 @@
}
@media (max-width: 480px) {
+ .\[\@media_\(max-width\:_480px\)\]\:gap_8px {
+ gap: 8px
+ }
.\[\@media_\(max-width\:_480px\)\]\:h_120px {
height: 120px
}
@@ -966,6 +951,9 @@
.\[\@media_\(max-width\:_480px\)\]\:min-w_90px {
min-width: 90px
}
+ .\[\@media_\(max-width\:_480px\)\]\:fs_32px {
+ font-size: 32px
+ }
.\[\@media_\(max-width\:_480px\)\]\:gap_8px {
gap: 8px
diff --git a/apps/web/styled-system/chunks/src__components__InteractiveAbacus.css b/apps/web/styled-system/chunks/src__components__InteractiveAbacus.css
index 9fd3ff4e..f5bcf234 100644
--- a/apps/web/styled-system/chunks/src__components__InteractiveAbacus.css
+++ b/apps/web/styled-system/chunks/src__components__InteractiveAbacus.css
@@ -1,185 +1,5 @@
@layer utilities {
- .border_blue\.600 {
- border-color: var(--colors-blue-600)
- }
-
- .bg_blue\.25 {
- background: blue.25
- }
-
- .w_6 {
- width: var(--sizes-6)
- }
-
- .h_6 {
- height: var(--sizes-6)
- }
-
- .border-t_blue\.500 {
- border-top-color: var(--colors-blue-500)
- }
-
- .rounded_full {
- border-radius: var(--radii-full)
- }
-
- .animation_spin_1s_linear_infinite {
- animation: spin 1s linear infinite
- }
-
- .bg_red\.25 {
- background: red.25
- }
-
- .rounded_md {
- border-radius: var(--radii-md)
- }
-
- .min-h_300px {
- min-height: 300px
- }
-
- .opacity_0\.6 {
- opacity: 0.6
- }
-
- .w_full {
- width: var(--sizes-full)
- }
-
- .h_full {
- height: var(--sizes-full)
- }
-
- .overflow_hidden {
- overflow: hidden
- }
-
- .bg_white {
- background: var(--colors-white)
- }
-
- .\[\&_svg\]\:w_100\% svg {
- width: 100%
- }
-
- .\[\&_svg\]\:h_100\% svg {
- height: 100%
- }
-
- .\[\&_svg\]\:max-w_100\% svg {
- max-width: 100%
- }
-
- .\[\&_svg\]\:max-h_100\% svg {
- max-height: 100%
- }
-
- .\[\&_svg\]\:object_contain svg {
- object-fit: contain
- }
-
- .\[\&_svg\]\:transition_all_0\.2s_ease svg {
- transition: all 0.2s ease
- }
-
- .gap_4 {
- gap: var(--spacing-4)
- }
-
- .mt_2 {
- margin-top: var(--spacing-2)
- }
-
- .min-w_80px {
- min-width: 80px
- }
-
- .max-w_300px {
- max-width: 300px
- }
-
- .w_24px {
- width: 24px
- }
-
- .h_8px {
- height: 8px
- }
-
- .rounded_8px {
- border-radius: 8px
- }
-
- .border_green\.600 {
- border-color: var(--colors-green-600)
- }
-
- .border_gray\.400 {
- border-color: var(--colors-gray-400)
- }
-
- .w_20px {
- width: 20px
- }
-
- .h_6px {
- height: 6px
- }
-
- .rounded_6px {
- border-radius: 6px
- }
-
- .transition_border-color_0\.2s_ease {
- transition: border-color 0.2s ease
- }
-
- .p_16px_8px {
- padding: 16px 8px
- }
-
- .gap_8px {
- gap: 8px
- }
-
- .min-h_60px {
- min-height: 60px
- }
-
- .justify_flex-end {
- justify-content: flex-end
- }
-
- .w_50px {
- width: 50px
- }
-
- .h_4px {
- height: 4px
- }
-
- .bg_gray\.800 {
- background-color: var(--colors-gray-800)
- }
-
- .rounded_2px {
- border-radius: 2px
- }
-
- .gap_4px {
- gap: 4px
- }
-
- .min-h_80px {
- min-height: 80px
- }
-
- .justify_flex-start {
- justify-content: flex-start
- }
-
.flex_column {
flex-direction: column
}
@@ -188,58 +8,6 @@
gap: var(--spacing-6)
}
- .gap_2 {
- gap: var(--spacing-2)
- }
-
- .border_amber\.800 {
- border-color: var(--colors-amber-800)
- }
-
- .max-w_400px {
- max-width: 400px
- }
-
- .border_\#d97706 {
- border-color: #d97706
- }
-
- .pos_absolute {
- position: absolute
- }
-
- .top_50\% {
- top: 50%
- }
-
- .left_50\% {
- left: 50%
- }
-
- .transform_translate\(-50\%\,_-50\%\) {
- transform: translate(-50%, -50%)
- }
-
- .pointer-events_none {
- pointer-events: none
- }
-
- .fs_2xl {
- font-size: var(--font-sizes-2xl)
- }
-
- .text_orange\.600 {
- color: var(--colors-orange-600)
- }
-
- .text-shadow_0_2px_4px_rgba\(0\,0\,0\,0\.2\) {
- text-shadow: 0 2px 4px rgba(0,0,0,0.2)
- }
-
- .z_10 {
- z-index: 10
- }
-
.w_300px {
width: 300px
}
@@ -291,6 +59,12 @@
.transition_all_0\.3s_ease {
transition: all 0.3s ease
}
+ .\[\&_\[data-bead-type\]\]\:cursor_pointer [data-bead-type] {
+ cursor: pointer
+ }
+ .\[\&_\[data-bead-type\]\]\:transition_all_0\.2s_ease [data-bead-type] {
+ transition: all 0.2s ease
+ }
.fs_3xl {
font-size: var(--font-sizes-3xl)
@@ -452,34 +226,6 @@
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15)
}
- .hover\:border_blue\.700:is(:hover, [data-hover]) {
- border-color: var(--colors-blue-700)
- }
-
- .hover\:border_green\.700:is(:hover, [data-hover]) {
- border-color: var(--colors-green-700)
- }
-
- .hover\:border_gray\.500:is(:hover, [data-hover]) {
- border-color: var(--colors-gray-500)
- }
-
- .hover\:bg_blue\.200:is(:hover, [data-hover]) {
- background: var(--colors-blue-200)
- }
-
- .hover\:transform_translateY\(-1px\):is(:hover, [data-hover]) {
- transform: translateY(-1px)
- }
-
- .hover\:border_blue\.400:is(:hover, [data-hover]) {
- border-color: var(--colors-blue-400)
- }
-
- .hover\:shadow_0_4px_12px_rgba\(0\,_0\,_0\,_0\.1\):is(:hover, [data-hover]) {
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1)
- }
-
.hover\:bg_gray\.200:is(:hover, [data-hover]) {
background: var(--colors-gray-200)
}
@@ -488,14 +234,24 @@
border-color: var(--colors-gray-400)
}
- .\[\&\:hover\]\:border_amber\.700:hover {
- border-color: var(--colors-amber-700)
+ .hover\:bg_blue\.200:is(:hover, [data-hover]) {
+ background: var(--colors-blue-200)
}
- .\[\&\:hover\]\:transform_scale\(1\.01\):hover {
- transform: scale(1.01)
+ .hover\:border_blue\.400:is(:hover, [data-hover]) {
+ border-color: var(--colors-blue-400)
}
+ .hover\:transform_translateY\(-1px\):is(:hover, [data-hover]) {
+ transform: translateY(-1px)
+ }
+ .\[\&_\[data-bead-type\]\]\:hover\:filter_brightness\(1\.2\) [data-bead-type]:is(:hover, [data-hover]) {
+ filter: brightness(1.2)
+ }
+ .\[\&_\[data-bead-type\]\]\:hover\:transform_scale\(1\.05\) [data-bead-type]:is(:hover, [data-hover]) {
+ transform: scale(1.05)
+ }
+
.active\:transform_scale\(0\.95\):is(:active, [data-active]) {
transform: scale(0.95)
}
diff --git a/apps/web/styled-system/styles.css b/apps/web/styled-system/styles.css
index 47f4c528..8b25550f 100644
--- a/apps/web/styled-system/styles.css
+++ b/apps/web/styled-system/styles.css
@@ -25,6 +25,10 @@
padding: 12px 16px
}
+ .gap_4px {
+ gap: 4px
+ }
+
.fs_11px {
font-size: 11px
}
@@ -37,6 +41,10 @@
padding: 10px 20px
}
+ .max-w_300px {
+ max-width: 300px
+ }
+
.min-w_50px {
min-width: 50px
}
@@ -77,6 +85,14 @@
transition: width 0.5s ease
}
+ .justify_flex-end {
+ justify-content: flex-end
+ }
+
+ .rounded_6px {
+ border-radius: 6px
+ }
+
.p_8px_16px {
padding: 8px 16px
}
@@ -318,6 +334,10 @@
box-shadow: 0 8px 25px rgba(40, 167, 69, 0.2)
}
+ .min-h_60px {
+ min-height: 60px
+ }
+
.p_16px_20px {
padding: 16px 20px
}
@@ -407,10 +427,26 @@
width: 100vw
}
+ .pointer-events_none {
+ pointer-events: none
+ }
+
.z_1000 {
z-index: 1000
}
+ .top_50\% {
+ top: 50%
+ }
+
+ .left_50\% {
+ left: 50%
+ }
+
+ .transform_translate\(-50\%\,_-50\%\) {
+ transform: translate(-50%, -50%)
+ }
+
.fs_72px {
font-size: 72px
}
@@ -447,6 +483,10 @@
margin: 0 auto
}
+ .justify_flex-start {
+ justify-content: flex-start
+ }
+
.text_gray\.800 {
color: var(--colors-gray-800)
}
@@ -511,6 +551,10 @@
padding: 12px 24px
}
+ .rounded_8px {
+ border-radius: 8px
+ }
+
.fs_16px {
font-size: 16px
}
@@ -619,6 +663,10 @@
--gradient-from: var(--colors-indigo-400)
}
+ .text_orange\.600 {
+ color: var(--colors-orange-600)
+ }
+
.ml_2 {
margin-left: var(--spacing-2)
}
@@ -895,6 +943,10 @@
width: var(--sizes-20)
}
+ .min-h_300px {
+ min-height: 300px
+ }
+
.max-w_sm {
max-width: var(--sizes-sm)
}
@@ -951,6 +1003,10 @@
padding: var(--spacing-6)
}
+ .max-w_400px {
+ max-width: 400px
+ }
+
.max-h_80vh {
max-height: 80vh
}
@@ -1306,146 +1362,6 @@
gap: var(--spacing-0)
}
- .border_blue\.600 {
- border-color: var(--colors-blue-600)
- }
-
- .min-h_300px {
- min-height: 300px
- }
-
- .\[\&_svg\]\:transition_all_0\.2s_ease svg {
- transition: all 0.2s ease
- }
-
- .mt_2 {
- margin-top: var(--spacing-2)
- }
-
- .min-w_80px {
- min-width: 80px
- }
-
- .max-w_300px {
- max-width: 300px
- }
-
- .w_24px {
- width: 24px
- }
-
- .h_8px {
- height: 8px
- }
-
- .rounded_8px {
- border-radius: 8px
- }
-
- .border_green\.600 {
- border-color: var(--colors-green-600)
- }
-
- .border_gray\.400 {
- border-color: var(--colors-gray-400)
- }
-
- .w_20px {
- width: 20px
- }
-
- .h_6px {
- height: 6px
- }
-
- .rounded_6px {
- border-radius: 6px
- }
-
- .transition_border-color_0\.2s_ease {
- transition: border-color 0.2s ease
- }
-
- .p_16px_8px {
- padding: 16px 8px
- }
-
- .gap_8px {
- gap: 8px
- }
-
- .min-h_60px {
- min-height: 60px
- }
-
- .justify_flex-end {
- justify-content: flex-end
- }
-
- .w_50px {
- width: 50px
- }
-
- .h_4px {
- height: 4px
- }
-
- .bg_gray\.800 {
- background-color: var(--colors-gray-800)
- }
-
- .rounded_2px {
- border-radius: 2px
- }
-
- .gap_4px {
- gap: 4px
- }
-
- .min-h_80px {
- min-height: 80px
- }
-
- .justify_flex-start {
- justify-content: flex-start
- }
-
- .border_amber\.800 {
- border-color: var(--colors-amber-800)
- }
-
- .max-w_400px {
- max-width: 400px
- }
-
- .border_\#d97706 {
- border-color: #d97706
- }
-
- .top_50\% {
- top: 50%
- }
-
- .left_50\% {
- left: 50%
- }
-
- .transform_translate\(-50\%\,_-50\%\) {
- transform: translate(-50%, -50%)
- }
-
- .pointer-events_none {
- pointer-events: none
- }
-
- .text_orange\.600 {
- color: var(--colors-orange-600)
- }
-
- .text-shadow_0_2px_4px_rgba\(0\,0\,0\,0\.2\) {
- text-shadow: 0 2px 4px rgba(0,0,0,0.2)
- }
-
.w_300px {
width: 300px
}
@@ -1494,6 +1410,14 @@
transition: all 0.3s ease
}
+ .\[\&_\[data-bead-type\]\]\:cursor_pointer [data-bead-type] {
+ cursor: pointer
+ }
+
+ .\[\&_\[data-bead-type\]\]\:transition_all_0\.2s_ease [data-bead-type] {
+ transition: all 0.2s ease
+ }
+
.px_6 {
padding-inline: var(--spacing-6)
}
@@ -2196,34 +2120,6 @@
box-shadow: 0 12px 32px rgba(0, 0, 0, 0.15)
}
- .hover\:border_blue\.700:is(:hover, [data-hover]) {
- border-color: var(--colors-blue-700)
- }
-
- .hover\:border_green\.700:is(:hover, [data-hover]) {
- border-color: var(--colors-green-700)
- }
-
- .hover\:border_gray\.500:is(:hover, [data-hover]) {
- border-color: var(--colors-gray-500)
- }
-
- .hover\:bg_blue\.200:is(:hover, [data-hover]) {
- background: var(--colors-blue-200)
- }
-
- .hover\:transform_translateY\(-1px\):is(:hover, [data-hover]) {
- transform: translateY(-1px)
- }
-
- .hover\:border_blue\.400:is(:hover, [data-hover]) {
- border-color: var(--colors-blue-400)
- }
-
- .hover\:shadow_0_4px_12px_rgba\(0\,_0\,_0\,_0\.1\):is(:hover, [data-hover]) {
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1)
- }
-
.hover\:bg_gray\.200:is(:hover, [data-hover]) {
background: var(--colors-gray-200)
}
@@ -2232,14 +2128,26 @@
border-color: var(--colors-gray-400)
}
- .\[\&\:hover\]\:border_amber\.700:hover {
- border-color: var(--colors-amber-700)
+ .hover\:bg_blue\.200:is(:hover, [data-hover]) {
+ background: var(--colors-blue-200)
}
- .\[\&\:hover\]\:transform_scale\(1\.01\):hover {
- transform: scale(1.01)
+ .hover\:border_blue\.400:is(:hover, [data-hover]) {
+ border-color: var(--colors-blue-400)
}
+ .hover\:transform_translateY\(-1px\):is(:hover, [data-hover]) {
+ transform: translateY(-1px)
+ }
+
+ .\[\&_\[data-bead-type\]\]\:hover\:filter_brightness\(1\.2\) [data-bead-type]:is(:hover, [data-hover]) {
+ filter: brightness(1.2)
+ }
+
+ .\[\&_\[data-bead-type\]\]\:hover\:transform_scale\(1\.05\) [data-bead-type]:is(:hover, [data-hover]) {
+ transform: scale(1.05)
+ }
+
.hover\:border_brand\.300:is(:hover, [data-hover]) {
border-color: var(--colors-brand-300)
}