Compare commits
55 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
321d9aea10 | ||
|
|
92e1e62132 | ||
|
|
84d980bb24 | ||
|
|
892b377eb3 | ||
|
|
bc21095fa1 | ||
|
|
eb3b100056 | ||
|
|
276f6f0744 | ||
|
|
6d734f1d51 | ||
|
|
03b9b1228b | ||
|
|
10978e890b | ||
|
|
6d86281c63 | ||
|
|
0b1bff7eab | ||
|
|
f70ded30b9 | ||
|
|
c18012cb50 | ||
|
|
25a3356547 | ||
|
|
6463a3b2f6 | ||
|
|
7f6c486e9c | ||
|
|
3f30810271 | ||
|
|
4968e2c846 | ||
|
|
39b1e7de16 | ||
|
|
2a0e469e83 | ||
|
|
b3541e6b8a | ||
|
|
4b04e8673d | ||
|
|
75a84dc148 | ||
|
|
514d07ecb5 | ||
|
|
e37ee87ea3 | ||
|
|
38ef16a8f9 | ||
|
|
2f0304eb81 | ||
|
|
1da9ed1ce6 | ||
|
|
c5103d049f | ||
|
|
c4066d6879 | ||
|
|
1432afd6e6 | ||
|
|
1fa0df85f7 | ||
|
|
8baeba6987 | ||
|
|
e028e342ad | ||
|
|
263237a152 | ||
|
|
4ec1b952f2 | ||
|
|
aa9d389540 | ||
|
|
d1423420e6 | ||
|
|
47640f3486 | ||
|
|
cb7595c95b | ||
|
|
8b4eceebfa | ||
|
|
850fd33943 | ||
|
|
8835e1c57a | ||
|
|
b635ed1c2d | ||
|
|
4e6cecfe27 | ||
|
|
d52ba6373a | ||
|
|
002c2888ac | ||
|
|
5d85e898d6 | ||
|
|
eed890dc81 | ||
|
|
57fabffe60 | ||
|
|
89fb670f93 | ||
|
|
8e51390018 | ||
|
|
e7e54619ae | ||
|
|
9c51cc94ee |
181
CHANGELOG.md
181
CHANGELOG.md
@@ -1,3 +1,184 @@
|
||||
## [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)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** add kyu level data and cards ([6463a3b](https://github.com/antialias/soroban-abacus-flashcards/commit/6463a3b2f6371ebebac1048197fb44178997d2ef))
|
||||
|
||||
## [4.25.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.25.0...v4.25.1) (2025-10-20)
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **homepage:** make entire "Your Journey" card clickable ([3f30810](https://github.com/antialias/soroban-abacus-flashcards/commit/3f30810271418f3acf3df17e41d9a897a3312c34))
|
||||
|
||||
## [4.25.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.24.3...v4.25.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** add Kyu & Dan levels page with homepage link ([39b1e7d](https://github.com/antialias/soroban-abacus-flashcards/commit/39b1e7de16f15412c91cf648c714e31e2de7a6bc))
|
||||
|
||||
|
||||
### Styles
|
||||
|
||||
* **homepage:** adjust journey emoji sizing and spacing ([2a0e469](https://github.com/antialias/soroban-abacus-flashcards/commit/2a0e469e83b99c88f091bfecd770e0b4c1cb6310))
|
||||
|
||||
## [4.24.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.24.2...v4.24.3) (2025-10-20)
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **homepage:** align all skill icon panes horizontally ([4b04e86](https://github.com/antialias/soroban-abacus-flashcards/commit/4b04e8673da228863d4ec1869897ee431fa3d753))
|
||||
|
||||
## [4.24.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.24.1...v4.24.2) (2025-10-20)
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **homepage:** tighten mini abacus vertical padding ([514d07e](https://github.com/antialias/soroban-abacus-flashcards/commit/514d07ecb5f65a3c0982b8e90994e1c17ebaa59c))
|
||||
|
||||
## [4.24.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.24.0...v4.24.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** adjust mini abacus container height ([c4066d6](https://github.com/antialias/soroban-abacus-flashcards/commit/c4066d687925bbe7737ebfeefdada7365ff97c6c))
|
||||
* **homepage:** fix MiniAbacus runtime error and improve sizing ([1fa0df8](https://github.com/antialias/soroban-abacus-flashcards/commit/1fa0df85f7d3988cbc61701d89476419ccf0a13c))
|
||||
* **homepage:** use correct AbacusReact API and fix clipping/styling issues ([1432afd](https://github.com/antialias/soroban-abacus-flashcards/commit/1432afd6e6bd547bd0da76dbeea1c2b71244826f))
|
||||
* **homepage:** use direct conditionals for mini abacus padding ([38ef16a](https://github.com/antialias/soroban-abacus-flashcards/commit/38ef16a8f91f8ab4ad0d717b0321e2002636fafb))
|
||||
|
||||
|
||||
### Styles
|
||||
|
||||
* **homepage:** add more padding around mini abacus ([c5103d0](https://github.com/antialias/soroban-abacus-flashcards/commit/c5103d049f73a8f7ef26915edfbef9ea56d59094))
|
||||
* **homepage:** balance mini abacus padding horizontally and vertically ([2f0304e](https://github.com/antialias/soroban-abacus-flashcards/commit/2f0304eb81cdf84c21b0554c9cd4bd5478896dd8))
|
||||
* **homepage:** increase mini abacus padding to '5' ([1da9ed1](https://github.com/antialias/soroban-abacus-flashcards/commit/1da9ed1ce6995c605622fc2863f248e5e91ab9c3))
|
||||
|
||||
## [4.24.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.23.0...v4.24.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **homepage:** add animated mini abacus to "Read and set numbers" card ([e028e34](https://github.com/antialias/soroban-abacus-flashcards/commit/e028e342ad4bc01491e05a4ba074628155926fd8))
|
||||
|
||||
## [4.23.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.22.0...v4.23.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **homepage:** add more visual embellishments to learning cards ([4ec1b95](https://github.com/antialias/soroban-abacus-flashcards/commit/4ec1b952f202d50f6db287c41732ec65ca17c142))
|
||||
|
||||
## [4.22.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.21.1...v4.22.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **homepage:** enhance "What You'll Learn" with visual cards ([d142342](https://github.com/antialias/soroban-abacus-flashcards/commit/d1423420e653b26b2f89d9d17ae5d597807d6979))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **tutorial:** reduce tooltip z-index to scroll under nav bar ([47640f3](https://github.com/antialias/soroban-abacus-flashcards/commit/47640f3486c6d4a7107d59bdcce043f76fabbb1d))
|
||||
|
||||
## [4.21.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.21.0...v4.21.1) (2025-10-20)
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **homepage:** rearrange tutorial demo layout side by side ([8b4ecee](https://github.com/antialias/soroban-abacus-flashcards/commit/8b4eceebfaaaf07e38ea64c7fe015aec86ac754f))
|
||||
|
||||
## [4.21.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.20.7...v4.21.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **tutorial:** add silentErrors prop to suppress error messages ([8835e1c](https://github.com/antialias/soroban-abacus-flashcards/commit/8835e1c57ab8adcecefe0db082360dd98fbfaac7))
|
||||
|
||||
## [4.20.7](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.20.6...v4.20.7) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **home:** use Panda CSS token() for dynamic colors and center arrows properly ([d52ba63](https://github.com/antialias/soroban-abacus-flashcards/commit/d52ba6373a4577655dc1e5f5ff4926af7f7d96c3))
|
||||
|
||||
## [4.20.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.20.5...v4.20.6) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** use inline styles for journey level colors ([5d85e89](https://github.com/antialias/soroban-abacus-flashcards/commit/5d85e898d65d44d8d09bee952fad44b5a9c0cd20)), closes [#4ade80](https://github.com/antialias/soroban-abacus-flashcards/issues/4ade80) [#60a5](https://github.com/antialias/soroban-abacus-flashcards/issues/60a5) [#a78](https://github.com/antialias/soroban-abacus-flashcards/issues/a78) [#fbbf24](https://github.com/antialias/soroban-abacus-flashcards/issues/fbbf24)
|
||||
|
||||
## [4.20.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.20.4...v4.20.5) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **docker:** include Panda CSS styled-system in production image ([57fabff](https://github.com/antialias/soroban-abacus-flashcards/commit/57fabffe605d953b4a4d7e05032401cbf1ab2d14))
|
||||
|
||||
## [4.20.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.20.3...v4.20.4) (2025-10-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** use inline styles for Your Journey text contrast ([8e51390](https://github.com/antialias/soroban-abacus-flashcards/commit/8e5139001818d7013e1b2654ac707f7429316d58)), closes [#e5e7](https://github.com/antialias/soroban-abacus-flashcards/issues/e5e7) [#e5e7](https://github.com/antialias/soroban-abacus-flashcards/issues/e5e7) [#9ca3](https://github.com/antialias/soroban-abacus-flashcards/issues/9ca3) [#d1d5](https://github.com/antialias/soroban-abacus-flashcards/issues/d1d5)
|
||||
|
||||
## [4.20.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.20.2...v4.20.3) (2025-10-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** use explicit RGBA colors for Your Journey text ([9c51cc9](https://github.com/antialias/soroban-abacus-flashcards/commit/9c51cc94eec4efcab9c0b9d1190f5b79c0c7d365))
|
||||
|
||||
## [4.20.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.20.1...v4.20.2) (2025-10-19)
|
||||
|
||||
|
||||
|
||||
@@ -59,6 +59,9 @@ RUN adduser --system --uid 1001 nextjs
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/.next ./apps/web/.next
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/public ./apps/web/public
|
||||
|
||||
# Copy Panda CSS generated styles
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/styled-system ./apps/web/styled-system
|
||||
|
||||
# Copy server files (compiled from TypeScript)
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/server.js ./apps/web/
|
||||
COPY --from=builder --chown=nextjs:nodejs /app/apps/web/dist ./apps/web/dist
|
||||
|
||||
@@ -121,6 +121,31 @@ 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
|
||||
|
||||
**Example Usage:**
|
||||
```typescript
|
||||
import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react'
|
||||
|
||||
const config = useAbacusConfig({ columns: 5 })
|
||||
<AbacusReact config={config} initialNumber={123} />
|
||||
```
|
||||
|
||||
## Known Issues
|
||||
|
||||
### @soroban/abacus-react TypeScript Module Resolution
|
||||
|
||||
76
apps/web/.claude/PANDA_CSS_DYNAMIC_TOKENS.md
Normal file
76
apps/web/.claude/PANDA_CSS_DYNAMIC_TOKENS.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# Panda CSS Dynamic Token Usage
|
||||
|
||||
## Problem: Dynamic Color Tokens Not Working
|
||||
|
||||
When using Panda CSS, color tokens like `blue.400`, `purple.400`, etc. don't work when used dynamically through variables in the `css()` function.
|
||||
|
||||
## Root Cause
|
||||
|
||||
Panda CSS's `css()` function requires **static values at build time**. It cannot process dynamic token references like:
|
||||
|
||||
```typescript
|
||||
// ❌ This doesn't work
|
||||
const color = 'blue.400'
|
||||
css({ color: color }) // Panda can't resolve this at build time
|
||||
```
|
||||
|
||||
The `css()` function performs static analysis during the build process to generate CSS classes. It cannot handle runtime-dynamic token paths.
|
||||
|
||||
## Solution: Use the `token()` Function
|
||||
|
||||
Panda CSS provides a `token()` function specifically for resolving token paths to their actual values at runtime:
|
||||
|
||||
```typescript
|
||||
import { token } from '../../styled-system/tokens'
|
||||
|
||||
// ✅ This works
|
||||
const stages = [
|
||||
{ level: '10 Kyu', label: 'Beginner', color: 'colors.green.400' },
|
||||
{ level: '5 Kyu', label: 'Intermediate', color: 'colors.blue.400' },
|
||||
{ level: '1 Kyu', label: 'Advanced', color: 'colors.violet.400' },
|
||||
{ level: 'Dan', label: 'Master', color: 'colors.amber.400' },
|
||||
] as const
|
||||
|
||||
// Use with inline styles, not css()
|
||||
<div style={{ color: token(stage.color) }}>
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
1. **Use `as const`**: TypeScript needs the array marked as `const` so the token strings are treated as literal types, not generic strings. The `token()` function expects the `Token` literal type.
|
||||
|
||||
2. **Use inline styles**: When using `token()`, apply colors via the `style` prop, not through the `css()` function:
|
||||
```typescript
|
||||
// ✅ Correct
|
||||
<div style={{ color: token(stage.color) }}>
|
||||
|
||||
// ❌ Won't work
|
||||
<div className={css({ color: token(stage.color) })}>
|
||||
```
|
||||
|
||||
3. **Static tokens in css()**: For static usage, you CAN use tokens directly in `css()`:
|
||||
```typescript
|
||||
// ✅ This works because it's static
|
||||
css({ color: 'blue.400' })
|
||||
```
|
||||
|
||||
## How token() Works
|
||||
|
||||
The `token()` function:
|
||||
- Takes a token path like `"colors.blue.400"`
|
||||
- Looks it up in the generated token registry (`styled-system/tokens/index.mjs`)
|
||||
- Returns the actual CSS value (e.g., `"#60a5fa"`)
|
||||
- Happens at runtime, not build time
|
||||
|
||||
## Token Type Definition
|
||||
|
||||
The `Token` type is a union of all valid token paths:
|
||||
```typescript
|
||||
type Token = "colors.blue.400" | "colors.green.400" | "colors.violet.400" | ...
|
||||
```
|
||||
|
||||
This is defined in `styled-system/tokens/tokens.d.ts`.
|
||||
|
||||
## Reference Implementation
|
||||
|
||||
See `src/app/page.tsx` lines 404-434 for a working example of dynamic token usage in the "Your Journey" section.
|
||||
408
apps/web/src/app/levels/page.tsx
Normal file
408
apps/web/src/app/levels/page.tsx
Normal file
@@ -0,0 +1,408 @@
|
||||
'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, stack } from '../../../styled-system/patterns'
|
||||
|
||||
// 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: 'Pre-1st Dan',
|
||||
name: 'Jun-Shodan',
|
||||
minScore: 90,
|
||||
emoji: '🧙',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '1st Dan',
|
||||
name: 'Shodan',
|
||||
minScore: 100,
|
||||
emoji: '🧙',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '2nd Dan',
|
||||
name: 'Nidan',
|
||||
minScore: 120,
|
||||
emoji: '🧙♂️',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '3rd Dan',
|
||||
name: 'Sandan',
|
||||
minScore: 140,
|
||||
emoji: '🧙♂️',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '4th Dan',
|
||||
name: 'Yondan',
|
||||
minScore: 160,
|
||||
emoji: '🧙♀️',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '5th Dan',
|
||||
name: 'Godan',
|
||||
minScore: 180,
|
||||
emoji: '🧙♀️',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '6th Dan',
|
||||
name: 'Rokudan',
|
||||
minScore: 200,
|
||||
emoji: '🧝',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '7th Dan',
|
||||
name: 'Nanadan',
|
||||
minScore: 220,
|
||||
emoji: '🧝',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
level: '8th Dan',
|
||||
name: 'Hachidan',
|
||||
minScore: 250,
|
||||
emoji: '🧝♂️',
|
||||
color: 'amber',
|
||||
digits: 30,
|
||||
type: 'dan' as const,
|
||||
},
|
||||
{
|
||||
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)
|
||||
|
||||
return (
|
||||
<PageWithNav navTitle="Kyu & Dan Levels" navEmoji="📊">
|
||||
<div className={css({ bg: 'gray.900', minHeight: '100vh', pb: '12' })}>
|
||||
{/* Hero Section */}
|
||||
<div
|
||||
className={css({
|
||||
background:
|
||||
'linear-gradient(135deg, rgba(17, 24, 39, 1) 0%, rgba(124, 58, 237, 0.3) 50%, rgba(17, 24, 39, 1) 100%)',
|
||||
color: 'white',
|
||||
py: { base: '12', md: '16' },
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
opacity: 0.1,
|
||||
backgroundImage:
|
||||
'radial-gradient(circle at 2px 2px, rgba(255, 255, 255, 0.15) 1px, transparent 0)',
|
||||
backgroundSize: '40px 40px',
|
||||
})}
|
||||
/>
|
||||
|
||||
<div className={container({ maxW: '6xl', px: '4', position: 'relative' })}>
|
||||
<div className={css({ textAlign: 'center', maxW: '5xl', mx: 'auto' })}>
|
||||
<h1
|
||||
className={css({
|
||||
fontSize: { base: '3xl', md: '5xl', lg: '6xl' },
|
||||
fontWeight: 'bold',
|
||||
mb: '4',
|
||||
lineHeight: 'tight',
|
||||
background: 'linear-gradient(135deg, #fbbf24 0%, #f59e0b 50%, #fbbf24 100%)',
|
||||
backgroundClip: 'text',
|
||||
color: 'transparent',
|
||||
})}
|
||||
>
|
||||
Understanding Kyu & Dan Levels
|
||||
</h1>
|
||||
|
||||
<p
|
||||
className={css({
|
||||
fontSize: { base: 'lg', md: 'xl' },
|
||||
color: 'gray.300',
|
||||
mb: '6',
|
||||
maxW: '3xl',
|
||||
mx: 'auto',
|
||||
lineHeight: '1.6',
|
||||
})}
|
||||
>
|
||||
Slide through the complete progression from beginner to master
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Main content */}
|
||||
<div className={container({ maxW: '6xl', px: '4', py: '12' })}>
|
||||
<section className={stack({ gap: '8' })}>
|
||||
{/* 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({
|
||||
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={0}
|
||||
columns={currentLevel.digits}
|
||||
scaleFactor={scaleFactor}
|
||||
showNumbers={true}
|
||||
customStyles={{
|
||||
reckoningBar: { stroke: '#9ca3af', strokeWidth: 3 },
|
||||
columnPosts: { stroke: '#6b7280' },
|
||||
numerals: { color: '#d1d5db', fontSize: '14px', fontWeight: 'normal' },
|
||||
heavenBeads: { fill: '#60a5fa' },
|
||||
earthBeads: { fill: '#34d399' },
|
||||
}}
|
||||
/>
|
||||
</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',
|
||||
})}
|
||||
>
|
||||
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>
|
||||
</div>
|
||||
</PageWithNav>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +1,65 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react'
|
||||
import { PageWithNav } from '@/components/PageWithNav'
|
||||
import { TutorialPlayer } from '@/components/tutorial/TutorialPlayer'
|
||||
import { getTutorialForEditor } from '@/utils/tutorialConverter'
|
||||
import { css } from '../../styled-system/css'
|
||||
import { container, grid, hstack, stack } from '../../styled-system/patterns'
|
||||
import { token } from '../../styled-system/tokens'
|
||||
|
||||
// Mini abacus that cycles through random 3-digit numbers
|
||||
function MiniAbacus() {
|
||||
const [currentValue, setCurrentValue] = useState(123)
|
||||
const appConfig = useAbacusConfig()
|
||||
|
||||
useEffect(() => {
|
||||
// Cycle through random 3-digit numbers every 2.5 seconds
|
||||
const interval = setInterval(() => {
|
||||
const randomNum = Math.floor(Math.random() * 1000) // 0-999
|
||||
setCurrentValue(randomNum)
|
||||
}, 2500)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [])
|
||||
|
||||
// Dark theme styles for the abacus
|
||||
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 (
|
||||
<div
|
||||
className={css({
|
||||
width: '75px',
|
||||
height: '80px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
})}
|
||||
>
|
||||
<div className={css({ transform: 'scale(0.6)', transformOrigin: 'center center' })}>
|
||||
<AbacusReact
|
||||
value={currentValue}
|
||||
columns={3}
|
||||
beadShape={appConfig.beadShape}
|
||||
customStyles={darkStyles}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
// Extract just the "Friends of 5" step (2+3=5) for homepage demo
|
||||
@@ -215,54 +269,172 @@ export default function HomePage() {
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.700',
|
||||
shadow: 'lg',
|
||||
maxW: '900px',
|
||||
maxW: '1200px',
|
||||
mx: 'auto',
|
||||
})}
|
||||
>
|
||||
{/* What you'll learn - above tutorial */}
|
||||
<div
|
||||
className={css({
|
||||
mb: '8',
|
||||
pb: '6',
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'gray.700',
|
||||
display: 'flex',
|
||||
flexDirection: { base: 'column', md: 'row' },
|
||||
gap: '8',
|
||||
alignItems: { base: 'center', md: 'flex-start' },
|
||||
})}
|
||||
>
|
||||
<h3
|
||||
{/* Tutorial on the left */}
|
||||
<div className={css({ flex: '1' })}>
|
||||
<TutorialPlayer
|
||||
tutorial={friendsOf5Tutorial}
|
||||
isDebugMode={false}
|
||||
showDebugPanel={false}
|
||||
hideNavigation={true}
|
||||
hideTooltip={true}
|
||||
silentErrors={true}
|
||||
abacusColumns={1}
|
||||
theme="dark"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* What you'll learn on the right */}
|
||||
<div
|
||||
className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'white',
|
||||
mb: '4',
|
||||
textAlign: 'center',
|
||||
flex: '0 0 auto',
|
||||
minW: '340px',
|
||||
maxW: { base: '100%', md: '420px' },
|
||||
})}
|
||||
>
|
||||
What You'll Learn
|
||||
</h3>
|
||||
<div className={grid({ columns: { base: 1, sm: 2 }, gap: '3' })}>
|
||||
{[
|
||||
'Read and set numbers on an abacus',
|
||||
'Add and subtract with "friends" techniques',
|
||||
'Multiply and divide fluently',
|
||||
'Calculate mentally without the abacus',
|
||||
].map((skill, i) => (
|
||||
<div key={i} className={hstack({ gap: '3' })}>
|
||||
<span className={css({ color: 'yellow.400', fontSize: 'lg' })}>✓</span>
|
||||
<span className={css({ color: 'gray.300', fontSize: 'sm' })}>{skill}</span>
|
||||
</div>
|
||||
))}
|
||||
<h3
|
||||
className={css({
|
||||
fontSize: '2xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'white',
|
||||
mb: '6',
|
||||
})}
|
||||
>
|
||||
What You'll Learn
|
||||
</h3>
|
||||
<div className={stack({ gap: '5' })}>
|
||||
{[
|
||||
{
|
||||
icon: '🔢',
|
||||
title: 'Read and set numbers',
|
||||
desc: 'Master abacus number representation from zero to thousands',
|
||||
example: '0-9999',
|
||||
badge: 'Foundation',
|
||||
},
|
||||
{
|
||||
icon: '🤝',
|
||||
title: 'Friends techniques',
|
||||
desc: 'Add and subtract using complement pairs and mental shortcuts',
|
||||
example: '5 = 2+3',
|
||||
badge: 'Core',
|
||||
},
|
||||
{
|
||||
icon: '✖️➗',
|
||||
title: 'Multiply & divide',
|
||||
desc: 'Fluent multi-digit calculations with advanced techniques',
|
||||
example: '12×34',
|
||||
badge: 'Advanced',
|
||||
},
|
||||
{
|
||||
icon: '🧠',
|
||||
title: 'Mental calculation',
|
||||
desc: 'Visualize and compute without the physical tool (Anzan)',
|
||||
example: 'Speed math',
|
||||
badge: 'Expert',
|
||||
},
|
||||
].map((skill, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={css({
|
||||
bg: 'linear-gradient(135deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.03))',
|
||||
borderRadius: 'xl',
|
||||
p: '4',
|
||||
border: '1px solid',
|
||||
borderColor: 'rgba(255, 255, 255, 0.15)',
|
||||
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
||||
transition: 'all 0.2s',
|
||||
_hover: {
|
||||
bg: 'linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05))',
|
||||
borderColor: 'rgba(255, 255, 255, 0.25)',
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 6px 16px rgba(0, 0, 0, 0.4)',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div className={hstack({ gap: '3', alignItems: 'flex-start' })}>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: '3xl',
|
||||
width: '75px',
|
||||
height: '115px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
bg: 'rgba(255, 255, 255, 0.1)',
|
||||
borderRadius: 'lg',
|
||||
})}
|
||||
>
|
||||
{i === 0 ? <MiniAbacus /> : skill.icon}
|
||||
</div>
|
||||
<div className={stack({ gap: '2', flex: '1' })}>
|
||||
<div className={hstack({ gap: '2', alignItems: 'center' })}>
|
||||
<div
|
||||
className={css({
|
||||
color: 'white',
|
||||
fontSize: 'md',
|
||||
fontWeight: 'bold',
|
||||
})}
|
||||
>
|
||||
{skill.title}
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
bg: 'rgba(250, 204, 21, 0.2)',
|
||||
color: 'yellow.400',
|
||||
fontSize: '2xs',
|
||||
fontWeight: 'semibold',
|
||||
px: '2',
|
||||
py: '0.5',
|
||||
borderRadius: 'md',
|
||||
})}
|
||||
>
|
||||
{skill.badge}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
color: 'gray.300',
|
||||
fontSize: 'xs',
|
||||
lineHeight: '1.5',
|
||||
})}
|
||||
>
|
||||
{skill.desc}
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
color: 'yellow.400',
|
||||
fontSize: 'xs',
|
||||
fontFamily: 'mono',
|
||||
fontWeight: 'semibold',
|
||||
mt: '1',
|
||||
bg: 'rgba(250, 204, 21, 0.1)',
|
||||
px: '2',
|
||||
py: '1',
|
||||
borderRadius: 'md',
|
||||
w: 'fit-content',
|
||||
})}
|
||||
>
|
||||
{skill.example}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<TutorialPlayer
|
||||
tutorial={friendsOf5Tutorial}
|
||||
isDebugMode={false}
|
||||
showDebugPanel={false}
|
||||
hideNavigation={true}
|
||||
hideTooltip={true}
|
||||
abacusColumns={2}
|
||||
theme="dark"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -379,20 +551,47 @@ export default function HomePage() {
|
||||
>
|
||||
Your Journey
|
||||
</h2>
|
||||
<p className={css({ color: 'gray.200', fontSize: 'md' })}>
|
||||
Progress from beginner to master
|
||||
</p>
|
||||
<p style={{ color: '#e5e7eb', fontSize: '16px' }}>Progress from beginner to master</p>
|
||||
</div>
|
||||
|
||||
<div
|
||||
<Link
|
||||
href="/levels"
|
||||
className={css({
|
||||
bg: 'rgba(0, 0, 0, 0.4)',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.700',
|
||||
rounded: 'xl',
|
||||
p: '8',
|
||||
display: 'block',
|
||||
transition: 'all 0.2s',
|
||||
cursor: 'pointer',
|
||||
position: 'relative',
|
||||
_hover: {
|
||||
bg: 'rgba(0, 0, 0, 0.5)',
|
||||
borderColor: 'violet.500',
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: '0 8px 16px rgba(124, 58, 237, 0.2)',
|
||||
},
|
||||
})}
|
||||
>
|
||||
{/* Subtle arrow indicator */}
|
||||
<div
|
||||
className={css({
|
||||
position: 'absolute',
|
||||
top: '4',
|
||||
right: '4',
|
||||
fontSize: 'xl',
|
||||
color: 'gray.500',
|
||||
transition: 'all 0.2s',
|
||||
_groupHover: {
|
||||
color: 'violet.400',
|
||||
transform: 'translateX(4px)',
|
||||
},
|
||||
})}
|
||||
>
|
||||
→
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
@@ -402,39 +601,67 @@ export default function HomePage() {
|
||||
flexWrap: 'wrap',
|
||||
})}
|
||||
>
|
||||
{[
|
||||
{ level: '10 Kyu', label: 'Beginner', color: 'green.400' },
|
||||
{ level: '5 Kyu', label: 'Intermediate', color: 'blue.400' },
|
||||
{ level: '1 Kyu', label: 'Advanced', color: 'purple.400' },
|
||||
{ level: 'Dan', label: 'Master', color: 'yellow.400' },
|
||||
].map((stage, i) => (
|
||||
{(
|
||||
[
|
||||
{ level: '10 Kyu', label: 'Beginner', color: 'colors.green.400', emoji: '🧒' },
|
||||
{
|
||||
level: '5 Kyu',
|
||||
label: 'Intermediate',
|
||||
color: 'colors.blue.400',
|
||||
emoji: '🧑',
|
||||
},
|
||||
{ level: '1 Kyu', label: 'Advanced', color: 'colors.violet.400', emoji: '🧔' },
|
||||
{ level: 'Dan', label: 'Master', color: 'colors.amber.400', emoji: '🧙' },
|
||||
] as const
|
||||
).map((stage, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className={stack({
|
||||
gap: '2',
|
||||
gap: '0',
|
||||
textAlign: 'center',
|
||||
flex: '1',
|
||||
position: 'relative',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: '5xl',
|
||||
mb: '0',
|
||||
})}
|
||||
>
|
||||
{stage.emoji}
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
color: stage.color,
|
||||
mt: '-2',
|
||||
})}
|
||||
style={{ color: token(stage.color) }}
|
||||
>
|
||||
{stage.level}
|
||||
</div>
|
||||
<div className={css({ fontSize: 'sm', color: 'gray.200' })}>{stage.label}</div>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: 'sm',
|
||||
color: 'gray.300',
|
||||
})}
|
||||
>
|
||||
{stage.label}
|
||||
</div>
|
||||
{i < 3 && (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: '100%',
|
||||
marginLeft: '0.5rem',
|
||||
top: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
fontSize: '20px',
|
||||
color: '#9ca3af',
|
||||
}}
|
||||
className={css({
|
||||
display: { base: 'none', md: 'block' },
|
||||
position: 'absolute',
|
||||
right: '-50%',
|
||||
fontSize: 'xl',
|
||||
color: 'gray.400',
|
||||
})}
|
||||
>
|
||||
→
|
||||
@@ -444,17 +671,19 @@ export default function HomePage() {
|
||||
))}
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
fontSize: '14px',
|
||||
color: '#d1d5db',
|
||||
fontStyle: 'italic',
|
||||
}}
|
||||
className={css({
|
||||
mt: '6',
|
||||
textAlign: 'center',
|
||||
fontSize: 'sm',
|
||||
color: 'gray.300',
|
||||
fontStyle: 'italic',
|
||||
})}
|
||||
>
|
||||
You'll progress through all these levels eventually ↑
|
||||
Click to learn about the official Japanese ranking system →
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</section>
|
||||
|
||||
{/* Additional Tools Section */}
|
||||
|
||||
@@ -217,6 +217,7 @@ interface TutorialPlayerProps {
|
||||
showDebugPanel?: boolean
|
||||
hideNavigation?: boolean
|
||||
hideTooltip?: boolean
|
||||
silentErrors?: boolean
|
||||
abacusColumns?: number
|
||||
theme?: 'light' | 'dark'
|
||||
onStepChange?: (stepIndex: number, step: TutorialStep) => void
|
||||
@@ -233,6 +234,7 @@ function TutorialPlayerContent({
|
||||
showDebugPanel = false,
|
||||
hideNavigation = false,
|
||||
hideTooltip = false,
|
||||
silentErrors = false,
|
||||
abacusColumns = 5,
|
||||
theme = 'light',
|
||||
onStepChange,
|
||||
@@ -638,7 +640,7 @@ function TutorialPlayerContent({
|
||||
maxWidth: '200px',
|
||||
minWidth: '150px',
|
||||
wordBreak: 'break-word',
|
||||
zIndex: 1000,
|
||||
zIndex: 50,
|
||||
opacity: 0.95,
|
||||
transition: 'all 0.3s ease',
|
||||
transform: showCelebration ? 'scale(1.05)' : 'scale(1)',
|
||||
@@ -909,7 +911,7 @@ function TutorialPlayerContent({
|
||||
)
|
||||
})
|
||||
|
||||
if (!isCorrectBead) {
|
||||
if (!isCorrectBead && !silentErrors) {
|
||||
const errorMessage = "That's not the highlighted bead. Try clicking the highlighted bead."
|
||||
dispatch({
|
||||
type: 'SET_ERROR',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "4.20.2",
|
||||
"version": "4.32.0",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user