Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b7c455315 | ||
|
|
c38767f4d3 | ||
|
|
321d9aea10 | ||
|
|
92e1e62132 | ||
|
|
84d980bb24 | ||
|
|
892b377eb3 | ||
|
|
bc21095fa1 | ||
|
|
eb3b100056 | ||
|
|
276f6f0744 | ||
|
|
6d734f1d51 | ||
|
|
03b9b1228b | ||
|
|
10978e890b | ||
|
|
6d86281c63 | ||
|
|
0b1bff7eab | ||
|
|
f70ded30b9 | ||
|
|
c18012cb50 |
56
CHANGELOG.md
56
CHANGELOG.md
@@ -1,3 +1,59 @@
|
||||
## [4.32.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.32.0...v4.32.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** use correct dark mode styling from homepage + docs update ([c38767f](https://github.com/antialias/soroban-abacus-flashcards/commit/c38767f4d399fa2caa5cd4e0185689d0207fbdaf))
|
||||
|
||||
## [4.32.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.31.1...v4.32.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** add dark mode styling and responsive scaling to abacus ([92e1e62](https://github.com/antialias/soroban-abacus-flashcards/commit/92e1e621321039206f65b3605f5797bbdc6beafc))
|
||||
|
||||
## [4.31.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.31.0...v4.31.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** use correct AbacusReact API with direct props ([892b377](https://github.com/antialias/soroban-abacus-flashcards/commit/892b377eb3bbd555dd2566bf58e946e9faa7b9f6))
|
||||
|
||||
## [4.31.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.30.0...v4.31.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** implement interactive slider for exploring kyu & dan ranks ([eb3b100](https://github.com/antialias/soroban-abacus-flashcards/commit/eb3b1000563536d4143ba1f4ec04e59e8dd2e608))
|
||||
|
||||
## [4.30.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.29.0...v4.30.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** create true horizontal slider with abacus visualizations ([6d734f1](https://github.com/antialias/soroban-abacus-flashcards/commit/6d734f1d51f5ba1367f55923e58bd977413d754e))
|
||||
|
||||
## [4.29.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.28.0...v4.29.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** replace kyu grid with interactive slider and abacus visualizations ([10978e8](https://github.com/antialias/soroban-abacus-flashcards/commit/10978e890beee65dea78ddcce52cfe5315d58063))
|
||||
|
||||
## [4.28.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.27.0...v4.28.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** add informational footer section ([0b1bff7](https://github.com/antialias/soroban-abacus-flashcards/commit/0b1bff7eab8f5da84ae309dbda336e168c2fe3fd))
|
||||
|
||||
## [4.27.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.26.0...v4.27.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** add Dan levels ladder visualization ([c18012c](https://github.com/antialias/soroban-abacus-flashcards/commit/c18012cb505a1f2a86ebed7579b379a4d7d97f2c))
|
||||
|
||||
## [4.26.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.25.1...v4.26.0) (2025-10-20)
|
||||
|
||||
|
||||
|
||||
@@ -121,6 +121,62 @@ className="bg-blue-200 border-gray-300 text-brand-600"
|
||||
|
||||
See `.claude/GAME_THEMES.md` for standardized color theme usage in arcade games.
|
||||
|
||||
## Abacus Visualizations
|
||||
|
||||
**CRITICAL: This project uses @soroban/abacus-react for all abacus visualizations.**
|
||||
|
||||
- All abacus displays MUST use components from `@soroban/abacus-react`
|
||||
- Package location: `packages/abacus-react`
|
||||
- Main components: `AbacusReact`, `useAbacusConfig`, `useAbacusDisplay`
|
||||
- DO NOT create custom abacus visualizations
|
||||
- DO NOT manually draw abacus columns, beads, or bars
|
||||
|
||||
**Common Mistakes to Avoid:**
|
||||
- ❌ Don't create custom abacus components or SVGs
|
||||
- ❌ Don't manually render abacus beads or columns
|
||||
- ✅ Always use `AbacusReact` from `@soroban/abacus-react`
|
||||
- ✅ Use `useAbacusConfig` for abacus configuration
|
||||
- ✅ Use `useAbacusDisplay` for reading abacus state
|
||||
|
||||
**MANDATORY: Read the Docs Before Customizing**
|
||||
|
||||
**ALWAYS read the full README documentation before customizing or styling AbacusReact:**
|
||||
- Location: `packages/abacus-react/README.md`
|
||||
- Check homepage implementation: `src/app/page.tsx` (MiniAbacus component)
|
||||
- Check storybook examples: `src/stories/AbacusReact.*.stories.tsx`
|
||||
|
||||
**Key Documentation Points:**
|
||||
1. **Custom Styles**: Use `fill` (not just `stroke`) for columnPosts and reckoningBar
|
||||
2. **Props**: Use direct props like `value`, `columns`, `scaleFactor` (not config objects)
|
||||
3. **Example from Homepage:**
|
||||
```typescript
|
||||
const darkStyles = {
|
||||
columnPosts: {
|
||||
fill: 'rgba(255, 255, 255, 0.3)',
|
||||
stroke: 'rgba(255, 255, 255, 0.2)',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
reckoningBar: {
|
||||
fill: 'rgba(255, 255, 255, 0.4)',
|
||||
stroke: 'rgba(255, 255, 255, 0.25)',
|
||||
strokeWidth: 3,
|
||||
},
|
||||
}
|
||||
|
||||
<AbacusReact
|
||||
value={123}
|
||||
columns={3}
|
||||
customStyles={darkStyles}
|
||||
/>
|
||||
```
|
||||
|
||||
**Example Usage:**
|
||||
```typescript
|
||||
import { AbacusReact } from '@soroban/abacus-react'
|
||||
|
||||
<AbacusReact value={123} columns={5} scaleFactor={1.5} showNumbers={true} />
|
||||
```
|
||||
|
||||
## Known Issues
|
||||
|
||||
### @soroban/abacus-react TypeScript Module Resolution
|
||||
|
||||
@@ -1,164 +1,158 @@
|
||||
'use client'
|
||||
|
||||
import { useState } from 'react'
|
||||
import { AbacusReact } from '@soroban/abacus-react'
|
||||
import { PageWithNav } from '@/components/PageWithNav'
|
||||
import { css } from '../../../styled-system/css'
|
||||
import { container, grid, stack } from '../../../styled-system/patterns'
|
||||
import { container, stack } from '../../../styled-system/patterns'
|
||||
|
||||
// Kyu level data from the Japan Abacus Federation
|
||||
const kyuLevels = [
|
||||
// Combine all levels into one array for the slider
|
||||
const allLevels = [
|
||||
{ level: '10th Kyu', emoji: '🧒', color: 'green', digits: 2, type: 'kyu' as const },
|
||||
{ level: '9th Kyu', emoji: '🧒', color: 'green', digits: 2, type: 'kyu' as const },
|
||||
{ level: '8th Kyu', emoji: '🧒', color: 'green', digits: 3, type: 'kyu' as const },
|
||||
{ level: '7th Kyu', emoji: '🧒', color: 'green', digits: 4, type: 'kyu' as const },
|
||||
{ level: '6th Kyu', emoji: '🧑', color: 'blue', digits: 5, type: 'kyu' as const },
|
||||
{ level: '5th Kyu', emoji: '🧑', color: 'blue', digits: 6, type: 'kyu' as const },
|
||||
{ level: '4th Kyu', emoji: '🧑', color: 'blue', digits: 7, type: 'kyu' as const },
|
||||
{ level: '3rd Kyu', emoji: '🧔', color: 'violet', digits: 8, type: 'kyu' as const },
|
||||
{ level: '2nd Kyu', emoji: '🧔', color: 'violet', digits: 9, type: 'kyu' as const },
|
||||
{ level: '1st Kyu', emoji: '🧔', color: 'violet', digits: 10, type: 'kyu' as const },
|
||||
{
|
||||
level: '10th Kyu',
|
||||
emoji: '🧒',
|
||||
color: 'green',
|
||||
duration: '20 min',
|
||||
passThreshold: '30%',
|
||||
points: '60/200',
|
||||
sections: [
|
||||
{ name: 'Addition', digits: '2-digit', problems: 10, points: 10 },
|
||||
{ name: 'Subtraction', digits: '2-digit', problems: 10, points: 10 },
|
||||
],
|
||||
notes: 'No division at this level',
|
||||
level: 'Pre-1st Dan',
|
||||
name: 'Jun-Shodan',
|
||||
minScore: 90,
|
||||
emoji: '🧙',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '9th Kyu',
|
||||
emoji: '🧒',
|
||||
color: 'green',
|
||||
duration: '20 min',
|
||||
passThreshold: '60%',
|
||||
points: '120/200',
|
||||
sections: [
|
||||
{ name: 'Addition', digits: '2-digit', problems: 10, points: 10 },
|
||||
{ name: 'Subtraction', digits: '2-digit', problems: 10, points: 10 },
|
||||
{ name: 'Multiplication', digits: '1×1', problems: 10, points: 10 },
|
||||
],
|
||||
notes: 'Introduces multiplication',
|
||||
level: '1st Dan',
|
||||
name: 'Shodan',
|
||||
minScore: 100,
|
||||
emoji: '🧙',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '8th Kyu',
|
||||
emoji: '🧒',
|
||||
color: 'green',
|
||||
duration: '20 min',
|
||||
passThreshold: '60%',
|
||||
points: '120/200',
|
||||
sections: [
|
||||
{ name: 'Addition', digits: '3-digit', problems: 10, points: 10 },
|
||||
{ name: 'Subtraction', digits: '3-digit', problems: 10, points: 10 },
|
||||
{ name: 'Multiplication', digits: '2×1', problems: 10, points: 10 },
|
||||
{ name: 'Division', digits: '2÷1', problems: 10, points: 10 },
|
||||
],
|
||||
notes: 'Introduces division',
|
||||
level: '2nd Dan',
|
||||
name: 'Nidan',
|
||||
minScore: 120,
|
||||
emoji: '🧙♂️',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '7th Kyu',
|
||||
emoji: '🧒',
|
||||
color: 'green',
|
||||
duration: '20 min',
|
||||
passThreshold: '60%',
|
||||
points: '120/200',
|
||||
sections: [
|
||||
{ name: 'Addition', digits: '4-digit', problems: 10, points: 10 },
|
||||
{ name: 'Subtraction', digits: '4-digit', problems: 10, points: 10 },
|
||||
{ name: 'Multiplication', digits: '3×1', problems: 10, points: 10 },
|
||||
{ name: 'Division', digits: '3÷1', problems: 10, points: 10 },
|
||||
],
|
||||
notes: 'All four operations',
|
||||
level: '3rd Dan',
|
||||
name: 'Sandan',
|
||||
minScore: 140,
|
||||
emoji: '🧙♂️',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '6th Kyu',
|
||||
emoji: '🧑',
|
||||
color: 'blue',
|
||||
duration: '30 min',
|
||||
passThreshold: '70%',
|
||||
points: '210/300',
|
||||
sections: [
|
||||
{ name: 'Addition', digits: '5-digit', problems: 10, points: 10 },
|
||||
{ name: 'Subtraction', digits: '5-digit', problems: 10, points: 10 },
|
||||
{ name: 'Multiplication', digits: '4×2', problems: 10, points: 10 },
|
||||
{ name: 'Division', digits: '5÷2', problems: 10, points: 10 },
|
||||
],
|
||||
notes: 'Longer exam time',
|
||||
level: '4th Dan',
|
||||
name: 'Yondan',
|
||||
minScore: 160,
|
||||
emoji: '🧙♀️',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '5th Kyu',
|
||||
emoji: '🧑',
|
||||
color: 'blue',
|
||||
duration: '30 min',
|
||||
passThreshold: '70%',
|
||||
points: '210/300',
|
||||
sections: [
|
||||
{ name: 'Addition', digits: '6-digit', problems: 10, points: 10 },
|
||||
{ name: 'Subtraction', digits: '6-digit', problems: 10, points: 10 },
|
||||
{ name: 'Multiplication', digits: '5×2', problems: 10, points: 10 },
|
||||
{ name: 'Division', digits: '6÷2', problems: 10, points: 10 },
|
||||
],
|
||||
notes: 'Mid-level proficiency',
|
||||
level: '5th Dan',
|
||||
name: 'Godan',
|
||||
minScore: 180,
|
||||
emoji: '🧙♀️',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '4th Kyu',
|
||||
emoji: '🧑',
|
||||
color: 'blue',
|
||||
duration: '30 min',
|
||||
passThreshold: '70%',
|
||||
points: '210/300',
|
||||
sections: [
|
||||
{ name: 'Addition', digits: '7-digit', problems: 10, points: 10 },
|
||||
{ name: 'Subtraction', digits: '7-digit', problems: 10, points: 10 },
|
||||
{ name: 'Multiplication', digits: '6×2', problems: 10, points: 10 },
|
||||
{ name: 'Division', digits: '7÷2', problems: 10, points: 10 },
|
||||
],
|
||||
notes: 'Advanced intermediate',
|
||||
level: '6th Dan',
|
||||
name: 'Rokudan',
|
||||
minScore: 200,
|
||||
emoji: '🧝',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '3rd Kyu',
|
||||
emoji: '🧔',
|
||||
color: 'violet',
|
||||
duration: '30 min',
|
||||
passThreshold: '80%',
|
||||
points: '240/300',
|
||||
sections: [
|
||||
{ name: 'Addition', digits: '8-digit', problems: 10, points: 10 },
|
||||
{ name: 'Subtraction', digits: '8-digit', problems: 10, points: 10 },
|
||||
{ name: 'Multiplication', digits: '7×2', problems: 10, points: 10 },
|
||||
{ name: 'Division', digits: '8÷2', problems: 10, points: 10 },
|
||||
],
|
||||
notes: 'Higher pass threshold (80%)',
|
||||
level: '7th Dan',
|
||||
name: 'Nanadan',
|
||||
minScore: 220,
|
||||
emoji: '🧝',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '2nd Kyu',
|
||||
emoji: '🧔',
|
||||
color: 'violet',
|
||||
duration: '30 min',
|
||||
passThreshold: '80%',
|
||||
points: '240/300',
|
||||
sections: [
|
||||
{ name: 'Addition', digits: '9-digit', problems: 10, points: 10 },
|
||||
{ name: 'Subtraction', digits: '9-digit', problems: 10, points: 10 },
|
||||
{ name: 'Multiplication', digits: '8×2', problems: 10, points: 10 },
|
||||
{ name: 'Division', digits: '9÷2', problems: 10, points: 10 },
|
||||
],
|
||||
notes: 'Near-mastery level',
|
||||
level: '8th Dan',
|
||||
name: 'Hachidan',
|
||||
minScore: 250,
|
||||
emoji: '🧝♂️',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '1st Kyu',
|
||||
emoji: '🧔',
|
||||
color: 'violet',
|
||||
duration: '30 min',
|
||||
passThreshold: '80%',
|
||||
points: '240/300',
|
||||
sections: [
|
||||
{ name: 'Addition', digits: '10-digit', problems: 10, points: 10 },
|
||||
{ name: 'Subtraction', digits: '10-digit', problems: 10, points: 10 },
|
||||
{ name: 'Multiplication', digits: '9×2', problems: 10, points: 10 },
|
||||
{ name: 'Division', digits: '10÷2', problems: 10, points: 10 },
|
||||
],
|
||||
notes: 'Highest Kyu level before Dan',
|
||||
level: '9th Dan',
|
||||
name: 'Kudan',
|
||||
minScore: 270,
|
||||
emoji: '🧝♀️',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '10th Dan',
|
||||
name: 'Judan',
|
||||
minScore: 290,
|
||||
emoji: '👑',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
] as const
|
||||
|
||||
export default function LevelsPage() {
|
||||
const [currentIndex, setCurrentIndex] = useState(0)
|
||||
const currentLevel = allLevels[currentIndex]
|
||||
|
||||
// Calculate scale factor based on number of columns to fit the page
|
||||
// Smaller scale for more columns (Dan levels with 30 columns)
|
||||
const scaleFactor = Math.min(2.5, 20 / currentLevel.digits)
|
||||
|
||||
// Generate an interesting non-zero number to display on the abacus
|
||||
// Create a value that uses all available columns (e.g., 2 columns = 12, 3 columns = 123)
|
||||
// For larger numbers, create a pattern like 123456789012... up to the column count
|
||||
const digitPattern = '123456789'
|
||||
const displayValue = Number.parseInt(
|
||||
digitPattern.repeat(Math.ceil(currentLevel.digits / digitPattern.length)).slice(0, currentLevel.digits),
|
||||
10,
|
||||
)
|
||||
|
||||
// Dark theme styles matching the homepage
|
||||
const darkStyles = {
|
||||
columnPosts: {
|
||||
fill: 'rgba(255, 255, 255, 0.3)',
|
||||
stroke: 'rgba(255, 255, 255, 0.2)',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
reckoningBar: {
|
||||
fill: 'rgba(255, 255, 255, 0.4)',
|
||||
stroke: 'rgba(255, 255, 255, 0.25)',
|
||||
strokeWidth: 3,
|
||||
},
|
||||
}
|
||||
|
||||
return (
|
||||
<PageWithNav navTitle="Kyu & Dan Levels" navEmoji="📊">
|
||||
<div className={css({ bg: 'gray.900', minHeight: '100vh' })}>
|
||||
<div className={css({ bg: 'gray.900', minHeight: '100vh', pb: '12' })}>
|
||||
{/* Hero Section */}
|
||||
<div
|
||||
className={css({
|
||||
@@ -170,7 +164,6 @@ export default function LevelsPage() {
|
||||
overflow: 'hidden',
|
||||
})}
|
||||
>
|
||||
{/* Background pattern */}
|
||||
<div
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
@@ -184,7 +177,6 @@ export default function LevelsPage() {
|
||||
|
||||
<div className={container({ maxW: '6xl', px: '4', position: 'relative' })}>
|
||||
<div className={css({ textAlign: 'center', maxW: '5xl', mx: 'auto' })}>
|
||||
{/* Main headline */}
|
||||
<h1
|
||||
className={css({
|
||||
fontSize: { base: '3xl', md: '5xl', lg: '6xl' },
|
||||
@@ -199,7 +191,6 @@ export default function LevelsPage() {
|
||||
Understanding Kyu & Dan Levels
|
||||
</h1>
|
||||
|
||||
{/* Subtitle */}
|
||||
<p
|
||||
className={css({
|
||||
fontSize: { base: 'lg', md: 'xl' },
|
||||
@@ -210,210 +201,221 @@ export default function LevelsPage() {
|
||||
lineHeight: '1.6',
|
||||
})}
|
||||
>
|
||||
Learn about the official Japanese soroban ranking system used by the Japan Abacus
|
||||
Federation
|
||||
Slide through the complete progression from beginner to master
|
||||
</p>
|
||||
|
||||
{/* Journey progression emojis */}
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
gap: '4',
|
||||
mb: '8',
|
||||
flexWrap: 'wrap',
|
||||
})}
|
||||
>
|
||||
{[
|
||||
{ emoji: '🧒', label: '10 Kyu' },
|
||||
{ emoji: '→', label: '', isArrow: true },
|
||||
{ emoji: '🧑', label: '5 Kyu' },
|
||||
{ emoji: '→', label: '', isArrow: true },
|
||||
{ emoji: '🧔', label: '1 Kyu' },
|
||||
{ emoji: '→', label: '', isArrow: true },
|
||||
{ emoji: '🧙', label: 'Dan' },
|
||||
].map((step, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '1',
|
||||
opacity: step.isArrow ? 0.5 : 1,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: step.isArrow ? 'xl' : '4xl',
|
||||
color: step.isArrow ? 'gray.500' : 'yellow.400',
|
||||
})}
|
||||
>
|
||||
{step.emoji}
|
||||
</div>
|
||||
{step.label && (
|
||||
<div className={css({ fontSize: 'xs', color: 'gray.400' })}>{step.label}</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main content container */}
|
||||
<div className={container({ maxW: '7xl', px: '4', py: '12' })}>
|
||||
{/* Kyu Levels Section */}
|
||||
{/* Main content */}
|
||||
<div className={container({ maxW: '6xl', px: '4', py: '12' })}>
|
||||
<section className={stack({ gap: '8' })}>
|
||||
<div className={css({ textAlign: 'center' })}>
|
||||
<h2
|
||||
{/* Current Level Display */}
|
||||
<div
|
||||
className={css({
|
||||
bg: 'rgba(0, 0, 0, 0.4)',
|
||||
border: '2px solid',
|
||||
borderColor:
|
||||
currentLevel.color === 'green'
|
||||
? 'green.500'
|
||||
: currentLevel.color === 'blue'
|
||||
? 'blue.500'
|
||||
: currentLevel.color === 'violet'
|
||||
? 'violet.500'
|
||||
: 'amber.500',
|
||||
rounded: 'xl',
|
||||
p: { base: '6', md: '8' },
|
||||
})}
|
||||
>
|
||||
{/* Level Info */}
|
||||
<div className={css({ textAlign: 'center', mb: '6' })}>
|
||||
<div className={css({ fontSize: '5xl', mb: '3' })}>{currentLevel.emoji}</div>
|
||||
<h2
|
||||
className={css({
|
||||
fontSize: { base: '2xl', md: '3xl' },
|
||||
fontWeight: 'bold',
|
||||
color:
|
||||
currentLevel.color === 'green'
|
||||
? 'green.400'
|
||||
: currentLevel.color === 'blue'
|
||||
? 'blue.400'
|
||||
: currentLevel.color === 'violet'
|
||||
? 'violet.400'
|
||||
: 'amber.400',
|
||||
mb: '2',
|
||||
})}
|
||||
>
|
||||
{currentLevel.level}
|
||||
</h2>
|
||||
{'name' in currentLevel && (
|
||||
<div className={css({ fontSize: 'md', color: 'gray.400', mb: '1' })}>
|
||||
{currentLevel.name}
|
||||
</div>
|
||||
)}
|
||||
{'minScore' in currentLevel && (
|
||||
<div className={css({ fontSize: 'sm', color: 'gray.500' })}>
|
||||
Minimum Score: {currentLevel.minScore} points
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Abacus Display */}
|
||||
<div
|
||||
className={css({
|
||||
fontSize: { base: '2xl', md: '3xl' },
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
mb: '6',
|
||||
p: '6',
|
||||
bg: 'rgba(0, 0, 0, 0.3)',
|
||||
rounded: 'lg',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.700',
|
||||
overflowX: 'auto',
|
||||
})}
|
||||
>
|
||||
<AbacusReact
|
||||
value={displayValue}
|
||||
columns={currentLevel.digits}
|
||||
scaleFactor={scaleFactor}
|
||||
showNumbers={true}
|
||||
customStyles={darkStyles}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Digit Count */}
|
||||
<div className={css({ textAlign: 'center', color: 'gray.400', fontSize: 'sm' })}>
|
||||
Requires mastery of <strong>{currentLevel.digits}-digit</strong> calculations
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Slider Control */}
|
||||
<div
|
||||
className={css({
|
||||
bg: 'rgba(0, 0, 0, 0.3)',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.700',
|
||||
rounded: 'xl',
|
||||
p: '6',
|
||||
})}
|
||||
>
|
||||
<div className={css({ mb: '4', textAlign: 'center' })}>
|
||||
<h3
|
||||
className={css({ fontSize: 'lg', fontWeight: 'bold', color: 'white', mb: '2' })}
|
||||
>
|
||||
Explore All Levels
|
||||
</h3>
|
||||
<p className={css({ fontSize: 'sm', color: 'gray.400' })}>
|
||||
Drag the slider to see each rank
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Range Slider */}
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max={allLevels.length - 1}
|
||||
value={currentIndex}
|
||||
onChange={(e) => setCurrentIndex(Number(e.target.value))}
|
||||
className={css({
|
||||
w: '100%',
|
||||
h: '2',
|
||||
bg: 'gray.700',
|
||||
rounded: 'full',
|
||||
outline: 'none',
|
||||
cursor: 'pointer',
|
||||
})}
|
||||
/>
|
||||
|
||||
{/* Level Markers */}
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
mt: '4',
|
||||
fontSize: 'xs',
|
||||
color: 'gray.500',
|
||||
})}
|
||||
>
|
||||
<span>10th Kyu</span>
|
||||
<span>1st Kyu</span>
|
||||
<span>10th Dan</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Legend */}
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
gap: '6',
|
||||
justifyContent: 'center',
|
||||
p: '6',
|
||||
bg: 'rgba(0, 0, 0, 0.3)',
|
||||
rounded: 'lg',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.700',
|
||||
})}
|
||||
>
|
||||
<div className={css({ display: 'flex', alignItems: 'center', gap: '2' })}>
|
||||
<div className={css({ w: '4', h: '4', bg: 'green.500', rounded: 'sm' })} />
|
||||
<span className={css({ fontSize: 'sm', color: 'gray.300' })}>
|
||||
Beginner (10-7 Kyu)
|
||||
</span>
|
||||
</div>
|
||||
<div className={css({ display: 'flex', alignItems: 'center', gap: '2' })}>
|
||||
<div className={css({ w: '4', h: '4', bg: 'blue.500', rounded: 'sm' })} />
|
||||
<span className={css({ fontSize: 'sm', color: 'gray.300' })}>
|
||||
Intermediate (6-4 Kyu)
|
||||
</span>
|
||||
</div>
|
||||
<div className={css({ display: 'flex', alignItems: 'center', gap: '2' })}>
|
||||
<div className={css({ w: '4', h: '4', bg: 'violet.500', rounded: 'sm' })} />
|
||||
<span className={css({ fontSize: 'sm', color: 'gray.300' })}>
|
||||
Advanced (3-1 Kyu)
|
||||
</span>
|
||||
</div>
|
||||
<div className={css({ display: 'flex', alignItems: 'center', gap: '2' })}>
|
||||
<div className={css({ w: '4', h: '4', bg: 'amber.500', rounded: 'sm' })} />
|
||||
<span className={css({ fontSize: 'sm', color: 'gray.300' })}>
|
||||
Master (Dan ranks)
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Section */}
|
||||
<div
|
||||
className={css({
|
||||
bg: 'rgba(0, 0, 0, 0.4)',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.700',
|
||||
rounded: 'xl',
|
||||
p: { base: '6', md: '8' },
|
||||
})}
|
||||
>
|
||||
<h3
|
||||
className={css({
|
||||
fontSize: { base: 'xl', md: '2xl' },
|
||||
fontWeight: 'bold',
|
||||
color: 'white',
|
||||
mb: '4',
|
||||
})}
|
||||
>
|
||||
Kyu Levels (10th to 1st)
|
||||
</h2>
|
||||
<p className={css({ color: 'gray.400', fontSize: 'md', mb: '8' })}>
|
||||
Progress from beginner to advanced mastery
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Kyu Level Cards */}
|
||||
<div className={grid({ columns: { base: 1, md: 2, lg: 3 }, gap: '6' })}>
|
||||
{kyuLevels.map((kyu, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className={css({
|
||||
bg: 'rgba(0, 0, 0, 0.4)',
|
||||
border: '1px solid',
|
||||
borderColor:
|
||||
kyu.color === 'green'
|
||||
? 'green.700'
|
||||
: kyu.color === 'blue'
|
||||
? 'blue.700'
|
||||
: 'violet.700',
|
||||
rounded: 'xl',
|
||||
p: '6',
|
||||
transition: 'all 0.2s',
|
||||
_hover: {
|
||||
bg: 'rgba(0, 0, 0, 0.5)',
|
||||
borderColor:
|
||||
kyu.color === 'green'
|
||||
? 'green.500'
|
||||
: kyu.color === 'blue'
|
||||
? 'blue.500'
|
||||
: 'violet.500',
|
||||
transform: 'translateY(-2px)',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{/* Card Header */}
|
||||
<div
|
||||
className={css({ display: 'flex', alignItems: 'center', gap: '3', mb: '4' })}
|
||||
>
|
||||
<div className={css({ fontSize: '3xl' })}>{kyu.emoji}</div>
|
||||
<div>
|
||||
<h3
|
||||
className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
color:
|
||||
kyu.color === 'green'
|
||||
? 'green.400'
|
||||
: kyu.color === 'blue'
|
||||
? 'blue.400'
|
||||
: 'violet.400',
|
||||
})}
|
||||
>
|
||||
{kyu.level}
|
||||
</h3>
|
||||
<p className={css({ fontSize: 'sm', color: 'gray.400' })}>{kyu.notes}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Exam Details */}
|
||||
<div className={stack({ gap: '3' })}>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
fontSize: 'sm',
|
||||
})}
|
||||
>
|
||||
<span className={css({ color: 'gray.400' })}>Duration:</span>
|
||||
<span className={css({ color: 'white', fontWeight: '500' })}>
|
||||
{kyu.duration}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
fontSize: 'sm',
|
||||
})}
|
||||
>
|
||||
<span className={css({ color: 'gray.400' })}>Pass Threshold:</span>
|
||||
<span className={css({ color: 'amber.400', fontWeight: '600' })}>
|
||||
{kyu.passThreshold}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
fontSize: 'sm',
|
||||
})}
|
||||
>
|
||||
<span className={css({ color: 'gray.400' })}>Points Needed:</span>
|
||||
<span className={css({ color: 'white', fontWeight: '500' })}>
|
||||
{kyu.points}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sections */}
|
||||
<div
|
||||
className={css({
|
||||
mt: '4',
|
||||
pt: '4',
|
||||
borderTop: '1px solid',
|
||||
borderColor: 'gray.700',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: 'xs',
|
||||
color: 'gray.500',
|
||||
mb: '2',
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.05em',
|
||||
})}
|
||||
>
|
||||
Problem Types
|
||||
</div>
|
||||
{kyu.sections.map((section, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={css({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
fontSize: 'sm',
|
||||
py: '1',
|
||||
})}
|
||||
>
|
||||
<span className={css({ color: 'gray.300' })}>{section.name}</span>
|
||||
<span className={css({ color: 'gray.400', fontSize: 'xs' })}>
|
||||
{section.digits}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
About This Ranking System
|
||||
</h3>
|
||||
<div className={stack({ gap: '4' })}>
|
||||
<p className={css({ color: 'gray.300', lineHeight: '1.6' })}>
|
||||
This ranking system is based on the official examination structure used by the{' '}
|
||||
<strong className={css({ color: 'white' })}>Japan Abacus Federation</strong>. It
|
||||
represents a standardized progression from beginner (10th Kyu) to master level
|
||||
(10th Dan), used internationally for soroban proficiency assessment.
|
||||
</p>
|
||||
<p className={css({ color: 'gray.300', lineHeight: '1.6' })}>
|
||||
The system is designed to gradually increase in difficulty. Kyu levels progress
|
||||
from 2-digit calculations at 10th Kyu to 10-digit calculations at 1st Kyu. Dan
|
||||
levels all require mastery of 30-digit calculations, with ranks awarded based on
|
||||
exam scores.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "4.26.0",
|
||||
"version": "4.32.1",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user