Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acfb0dac0a | ||
|
|
0fbde53039 | ||
|
|
90bbe6fbb7 | ||
|
|
a03e73c849 | ||
|
|
f3dce84532 |
19
CHANGELOG.md
19
CHANGELOG.md
@@ -1,3 +1,22 @@
|
||||
## [4.35.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.34.0...v4.35.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** replace slider thumb with diamond-shaped abacus beads ([0fbde53](https://github.com/antialias/soroban-abacus-flashcards/commit/0fbde53039d3ea000c6a3be492b733479e7bf47c))
|
||||
|
||||
## [4.34.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.33.8...v4.34.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** redesign slider with abacus-themed beads ([f3dce84](https://github.com/antialias/soroban-abacus-flashcards/commit/f3dce84532fa706e4ec9551facde2055a060ee13))
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **levels:** convert to Radix UI Slider with abacus theme ([a03e73c](https://github.com/antialias/soroban-abacus-flashcards/commit/a03e73c849c5da4337f26a74b8f12b617c66068e))
|
||||
|
||||
## [4.33.8](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.33.7...v4.33.8) (2025-10-20)
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
'use client'
|
||||
|
||||
import { useRef, useState } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useSpring, animated } from '@react-spring/web'
|
||||
import * as Slider from '@radix-ui/react-slider'
|
||||
import { AbacusReact } from '@soroban/abacus-react'
|
||||
import { PageWithNav } from '@/components/PageWithNav'
|
||||
import { css } from '../../../styled-system/css'
|
||||
@@ -122,7 +123,6 @@ const allLevels = [
|
||||
|
||||
export default function LevelsPage() {
|
||||
const [currentIndex, setCurrentIndex] = useState(0)
|
||||
const sliderRef = useRef<HTMLInputElement>(null)
|
||||
const currentLevel = allLevels[currentIndex]
|
||||
|
||||
// Calculate scale factor based on number of columns to fit the page
|
||||
@@ -136,23 +136,6 @@ export default function LevelsPage() {
|
||||
config: { tension: 280, friction: 60 },
|
||||
})
|
||||
|
||||
// Handle mouse/touch move over slider for hover interaction
|
||||
const handleSliderHover = (
|
||||
e: React.MouseEvent<HTMLDivElement> | React.TouchEvent<HTMLDivElement>
|
||||
) => {
|
||||
if (!sliderRef.current) return
|
||||
|
||||
const rect = sliderRef.current.getBoundingClientRect()
|
||||
const clientX = 'touches' in e ? e.touches[0].clientX : e.clientX
|
||||
const x = clientX - rect.left
|
||||
const percentage = Math.max(0, Math.min(1, x / rect.width))
|
||||
const newIndex = Math.round(percentage * (allLevels.length - 1))
|
||||
|
||||
if (newIndex !== currentIndex) {
|
||||
setCurrentIndex(newIndex)
|
||||
}
|
||||
}
|
||||
|
||||
// Generate an interesting non-zero number to display on the abacus
|
||||
// Use a suffix pattern so rightmost digits stay constant as columns increase
|
||||
// This prevents beads from shifting: ones always 9, tens always 8, etc.
|
||||
@@ -300,35 +283,133 @@ export default function LevelsPage() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Range Slider with hover support */}
|
||||
{/* Abacus-themed Radix Slider */}
|
||||
<div className={css({ mb: '6', px: { base: '2', md: '8' } })}>
|
||||
<div className={css({ mb: '3', textAlign: 'center' })}>
|
||||
<p className={css({ fontSize: 'sm', color: 'gray.400' })}>
|
||||
Hover, drag, or touch the slider to explore all levels
|
||||
Drag or click the beads to explore all levels
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
onMouseMove={handleSliderHover}
|
||||
onTouchMove={handleSliderHover}
|
||||
className={css({ position: 'relative' })}
|
||||
>
|
||||
<input
|
||||
ref={sliderRef}
|
||||
type="range"
|
||||
min="0"
|
||||
<div className={css({ position: 'relative', py: '6' })}>
|
||||
<Slider.Root
|
||||
value={[currentIndex]}
|
||||
onValueChange={([value]) => setCurrentIndex(value)}
|
||||
min={0}
|
||||
max={allLevels.length - 1}
|
||||
value={currentIndex}
|
||||
onChange={(e) => setCurrentIndex(Number(e.target.value))}
|
||||
step={1}
|
||||
className={css({
|
||||
w: '100%',
|
||||
h: '2',
|
||||
bg: 'gray.700',
|
||||
rounded: 'full',
|
||||
outline: 'none',
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
userSelect: 'none',
|
||||
touchAction: 'none',
|
||||
w: 'full',
|
||||
h: '12',
|
||||
cursor: 'pointer',
|
||||
})}
|
||||
/>
|
||||
>
|
||||
{/* Decorative diamond beads for all levels */}
|
||||
{allLevels.map((level, index) => {
|
||||
const isActive = index === currentIndex
|
||||
const isDan = 'color' in level && level.color === 'violet'
|
||||
const size = isActive ? 14 : 8
|
||||
const beadColor = isActive
|
||||
? isDan
|
||||
? '#8b5cf6'
|
||||
: '#22c55e'
|
||||
: isDan
|
||||
? 'rgba(139, 92, 246, 0.4)'
|
||||
: 'rgba(34, 197, 94, 0.4)'
|
||||
|
||||
return (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: `${(index / (allLevels.length - 1)) * 100}%`,
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: `${size}px`,
|
||||
height: `${size}px`,
|
||||
transition: 'all 0.2s',
|
||||
pointerEvents: 'none',
|
||||
zIndex: 0,
|
||||
filter: isActive
|
||||
? `drop-shadow(0 0 6px ${isDan ? 'rgba(139, 92, 246, 0.6)' : 'rgba(34, 197, 94, 0.6)'})`
|
||||
: 'none',
|
||||
}}
|
||||
>
|
||||
<svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
|
||||
<polygon
|
||||
points={`0,${size / 2} ${size / 2},0 ${size},${size / 2} ${size / 2},${size}`}
|
||||
fill={beadColor}
|
||||
stroke={
|
||||
isActive
|
||||
? isDan
|
||||
? '#c4b5fd'
|
||||
: '#86efac'
|
||||
: 'rgba(255, 255, 255, 0.3)'
|
||||
}
|
||||
strokeWidth="0.5"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
|
||||
<Slider.Track
|
||||
className={css({
|
||||
bg: 'rgba(255, 255, 255, 0.2)',
|
||||
position: 'relative',
|
||||
flexGrow: 1,
|
||||
rounded: 'full',
|
||||
h: '3px',
|
||||
})}
|
||||
>
|
||||
<Slider.Range className={css({ display: 'none' })} />
|
||||
</Slider.Track>
|
||||
|
||||
<Slider.Thumb
|
||||
className={css({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
w: '28px',
|
||||
h: '28px',
|
||||
bg: 'transparent',
|
||||
cursor: 'grab',
|
||||
transition: 'all 0.2s',
|
||||
zIndex: 10,
|
||||
_hover: { transform: 'scale(1.15)' },
|
||||
_focus: {
|
||||
outline: 'none',
|
||||
transform: 'scale(1.15)',
|
||||
},
|
||||
_active: { cursor: 'grabbing' },
|
||||
})}
|
||||
>
|
||||
<svg width="28" height="28" viewBox="0 0 28 28">
|
||||
{/* Diamond bead matching abacus style */}
|
||||
<polygon
|
||||
points="0,14 14,0 28,14 14,28"
|
||||
fill={currentLevel.color === 'violet' ? '#8b5cf6' : '#22c55e'}
|
||||
stroke="#000"
|
||||
strokeWidth="0.8"
|
||||
/>
|
||||
{/* Inner highlight for depth */}
|
||||
<polygon
|
||||
points="3,14 14,3 25,14 14,25"
|
||||
fill={
|
||||
currentLevel.color === 'violet'
|
||||
? 'rgba(139, 92, 246, 0.3)'
|
||||
: 'rgba(34, 197, 94, 0.3)'
|
||||
}
|
||||
stroke="none"
|
||||
/>
|
||||
</svg>
|
||||
</Slider.Thumb>
|
||||
</Slider.Root>
|
||||
</div>
|
||||
|
||||
{/* Level Markers */}
|
||||
@@ -336,7 +417,7 @@ export default function LevelsPage() {
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
mt: '3',
|
||||
mt: '1',
|
||||
fontSize: 'xs',
|
||||
color: 'gray.500',
|
||||
})}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "4.33.8",
|
||||
"version": "4.35.0",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user