Compare commits
67 Commits
abacus-rea
...
v4.48.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3564bd51dc | ||
|
|
cc315645de | ||
|
|
035d8312c7 | ||
|
|
5f9b2dfe2b | ||
|
|
1bfde8fb25 | ||
|
|
48647e4fb5 | ||
|
|
318f9469a0 | ||
|
|
4bace36561 | ||
|
|
8c2ddca28d | ||
|
|
eff44b3ad1 | ||
|
|
f81b88ae30 | ||
|
|
71b1b933b5 | ||
|
|
92d50673e5 | ||
|
|
c229faffac | ||
|
|
463841e191 | ||
|
|
3a3706cc6f | ||
|
|
721dfe426d | ||
|
|
9dba75c9d9 | ||
|
|
f9a7cb7f05 | ||
|
|
ae7463d917 | ||
|
|
02b6c70b7a | ||
|
|
4b72e0c561 | ||
|
|
e0b6a2e88b | ||
|
|
230f1dcd86 | ||
|
|
d8ec64280e | ||
|
|
1d4419364a | ||
|
|
cce8980e17 | ||
|
|
4bcce2a8db | ||
|
|
2818fd15ca | ||
|
|
29d20a6c07 | ||
|
|
be2c3f63b0 | ||
|
|
aa0bdcf686 | ||
|
|
baea602000 | ||
|
|
05dd0b30d3 | ||
|
|
4febf5905b | ||
|
|
6739d59f2b | ||
|
|
cb20019c16 | ||
|
|
d90b5d5532 | ||
|
|
7028db0263 | ||
|
|
fa3b73c691 | ||
|
|
fd4d25c2d1 | ||
|
|
6501b073b1 | ||
|
|
9b4d9c21df | ||
|
|
53d23f19bc | ||
|
|
6c14012b97 | ||
|
|
c650ffa193 | ||
|
|
28834e8a3e | ||
|
|
8681b17340 | ||
|
|
d52cc608eb | ||
|
|
6f89d9e274 | ||
|
|
a8cc2bc0f0 | ||
|
|
9dff3e7b7b | ||
|
|
92fedb698d | ||
|
|
1e90d6c620 | ||
|
|
5fcb7925eb | ||
|
|
41eaed24fc | ||
|
|
de5f36481b | ||
|
|
e5ffe3927e | ||
|
|
7c47fcdc54 | ||
|
|
4f4c73577a | ||
|
|
1e6459f9c1 | ||
|
|
477a0b367e | ||
|
|
8751649233 | ||
|
|
07c783a794 | ||
|
|
ca8cef1c36 | ||
|
|
0d47664f9f | ||
|
|
1e5467fad4 |
241
CHANGELOG.md
241
CHANGELOG.md
@@ -1,3 +1,244 @@
|
||||
## [4.48.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.48.1...v4.48.2) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **nav:** ensure nav bar appears above tutorial tooltips ([cc31564](https://github.com/antialias/soroban-abacus-flashcards/commit/cc315645de30218d1b034da3e130458fe2961a69))
|
||||
|
||||
|
||||
### Styles
|
||||
|
||||
* **hero:** unify background with rest of homepage ([035d831](https://github.com/antialias/soroban-abacus-flashcards/commit/035d8312c707cbf5b0e2a725d7b1d8ff406f842d))
|
||||
|
||||
## [4.48.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.48.0...v4.48.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **hero:** prevent SSR hydration mismatch for subtitle ([1bfde8f](https://github.com/antialias/soroban-abacus-flashcards/commit/1bfde8fb251b227ccd2528bfe1c47acffd79fa49))
|
||||
|
||||
## [4.48.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.47.2...v4.48.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **hero:** persist random subtitle per-session ([318f946](https://github.com/antialias/soroban-abacus-flashcards/commit/318f9469a0805c200c55ce4024a95fd7b8dbe6a2))
|
||||
|
||||
## [4.47.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.47.1...v4.47.2) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **nav:** prevent thrashing by using fixed position always ([eff44b3](https://github.com/antialias/soroban-abacus-flashcards/commit/eff44b3ad1ea0535c6965ad58012f9275cb143ec))
|
||||
* **nav:** remove unnecessary borders from transparent nav ([8c2ddca](https://github.com/antialias/soroban-abacus-flashcards/commit/8c2ddca28dbdd7743227eed4d19a9a8f662a72b5))
|
||||
|
||||
## [4.47.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.47.0...v4.47.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **hero:** prevent nav thrashing with hysteresis ([71b1b93](https://github.com/antialias/soroban-abacus-flashcards/commit/71b1b933b598c0a6a8aef1bc9f8c598c1871b2eb))
|
||||
|
||||
## [4.47.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.46.2...v4.47.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **nav:** add transparent nav bar with borders when hero visible ([463841e](https://github.com/antialias/soroban-abacus-flashcards/commit/463841e1910f4ddb9af662f036e4efb867836a83))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **hero:** use Number.isNaN instead of global isNaN ([c229faf](https://github.com/antialias/soroban-abacus-flashcards/commit/c229faffac525f3eebeb12155cb5ca4dff744472))
|
||||
|
||||
|
||||
### Styles
|
||||
|
||||
* **hero-abacus:** add purple bead colors for dark theme ([721dfe4](https://github.com/antialias/soroban-abacus-flashcards/commit/721dfe426db4fe259f6cdeac587d008339df769b))
|
||||
* **hero:** adjust spacing between title, subtitle, and abacus ([3a3706c](https://github.com/antialias/soroban-abacus-flashcards/commit/3a3706cc6fb694c7762f065f4ab4996bb8608dc4))
|
||||
|
||||
## [4.46.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.46.1...v4.46.2) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **types:** properly type HomeHeroContext in AppNavBar ([f9a7cb7](https://github.com/antialias/soroban-abacus-flashcards/commit/f9a7cb7f05dfddf291d89212a77ba1c11c00c9c7))
|
||||
|
||||
## [4.46.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.46.0...v4.46.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **hero-abacus:** restructure layout to prevent visual overlap ([02b6c70](https://github.com/antialias/soroban-abacus-flashcards/commit/02b6c70b7a52f7de2954e5e0efddbed64d419d6c))
|
||||
|
||||
## [4.46.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.45.0...v4.46.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **homepage:** add full-page hero abacus with scroll-based nav transition ([d8ec642](https://github.com/antialias/soroban-abacus-flashcards/commit/d8ec64280ec0c2f44f2fd9c72a93a882481f650b))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** improve hero abacus sizing and layout ([230f1dc](https://github.com/antialias/soroban-abacus-flashcards/commit/230f1dcd866e5b3625e19f7400f5eae478fe7d0c))
|
||||
|
||||
|
||||
### Styles
|
||||
|
||||
* **hero-abacus:** apply dark theme to match homepage styling ([e0b6a2e](https://github.com/antialias/soroban-abacus-flashcards/commit/e0b6a2e88b3ebbaae41ed54f23f9e514604d2262))
|
||||
|
||||
## [4.45.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.44.3...v4.45.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **branding:** rebrand navigation from 'Soroban Generator' to 'Abaci One' ([cce8980](https://github.com/antialias/soroban-abacus-flashcards/commit/cce8980e177da1b3c344e46561d928ed98b86f6c))
|
||||
|
||||
## [4.44.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.44.2...v4.44.3) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** reduce operator box sizes and remove divider line ([29d20a6](https://github.com/antialias/soroban-abacus-flashcards/commit/29d20a6c0741e7427f2bb64bc9c3e950b1a3238a))
|
||||
* **levels:** use uniform padding on operator box grid ([2818fd1](https://github.com/antialias/soroban-abacus-flashcards/commit/2818fd15cacac78de6d86ba769b9b2a02800ed1e))
|
||||
|
||||
## [4.44.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.44.1...v4.44.2) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** match top/bottom margins to left padding on kyu detail boxes ([aa0bdcf](https://github.com/antialias/soroban-abacus-flashcards/commit/aa0bdcf686adcbfd1a145cf67121181d1f1194d9))
|
||||
|
||||
## [4.44.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.44.0...v4.44.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** add fixed dimensions and margins to kyu detail boxes ([05dd0b3](https://github.com/antialias/soroban-abacus-flashcards/commit/05dd0b30d3c397b82b7b7cc93a5ea575f3aada6d))
|
||||
|
||||
## [4.44.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.43.2...v4.44.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** redesign kyu details with larger operators and prominent digits ([6739d59](https://github.com/antialias/soroban-abacus-flashcards/commit/6739d59f2b6189a98570e23e04c20d86d774ccce))
|
||||
|
||||
## [4.43.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.43.1...v4.43.2) (2025-10-20)
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **levels:** remove Time and Pass sections from kyu details ([d90b5d5](https://github.com/antialias/soroban-abacus-flashcards/commit/d90b5d55322e75dd28b95376614663a506c829d4))
|
||||
|
||||
## [4.43.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.43.0...v4.43.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** use two-column grid for kyu details to prevent clipping ([fa3b73c](https://github.com/antialias/soroban-abacus-flashcards/commit/fa3b73c69169b4694201ffa19ae3f8b5a68dfe32))
|
||||
|
||||
## [4.43.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.42.1...v4.43.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** add structured kyu exam details with card UI ([6501b07](https://github.com/antialias/soroban-abacus-flashcards/commit/6501b073b100a00982cff1ca3140921e74f31a9c))
|
||||
|
||||
## [4.42.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.42.0...v4.42.1) (2025-10-20)
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **levels:** store kyu data verbatim, add formatting layer ([53d23f1](https://github.com/antialias/soroban-abacus-flashcards/commit/53d23f19bc06459462afb76ed94d9b99d583a32d))
|
||||
|
||||
## [4.42.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.41.0...v4.42.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** add kyu level details display with English translations ([c650ffa](https://github.com/antialias/soroban-abacus-flashcards/commit/c650ffa1935fe370d37190b2843c0deecdcce8e7))
|
||||
|
||||
## [4.41.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.40.1...v4.41.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** right-align abacus display ([8681b17](https://github.com/antialias/soroban-abacus-flashcards/commit/8681b17340e757cf04d17f884a780a251645bb33))
|
||||
|
||||
## [4.40.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.40.0...v4.40.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** increase animation speed to 10ms for 10th Dan ([6f89d9e](https://github.com/antialias/soroban-abacus-flashcards/commit/6f89d9e274082908fc090a9c0ba310f2cb06f014))
|
||||
|
||||
## [4.40.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.39.1...v4.40.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** progressive animation speed for Dan levels ([9dff3e7](https://github.com/antialias/soroban-abacus-flashcards/commit/9dff3e7b7b1ca46ea7f19a48135124b80c5182c0))
|
||||
|
||||
## [4.39.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.39.0...v4.39.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** improve slider tick spacing to use full width ([1e90d6c](https://github.com/antialias/soroban-abacus-flashcards/commit/1e90d6c6207f29084a8dc96ccfbb1013a1a62271))
|
||||
|
||||
## [4.39.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.38.1...v4.39.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** add auto-advance slider with hover pause ([41eaed2](https://github.com/antialias/soroban-abacus-flashcards/commit/41eaed24fce510bab7fd03fa2e39e829b33a7346))
|
||||
|
||||
## [4.38.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.38.0...v4.38.1) (2025-10-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** adjust slider text positioning to prevent emoji overlap ([e5ffe39](https://github.com/antialias/soroban-abacus-flashcards/commit/e5ffe3927edfb1baea7ddd216507e081f50e5d2c))
|
||||
|
||||
## [4.38.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.37.0...v4.38.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** add animated calculation effect to abacus display ([4f4c735](https://github.com/antialias/soroban-abacus-flashcards/commit/4f4c73577a944518c093b3208a85482909fe3064))
|
||||
|
||||
## [4.37.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.36.0...v4.37.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** add hover tracking to slider for real-time level preview ([477a0b3](https://github.com/antialias/soroban-abacus-flashcards/commit/477a0b367e32749b865b5a5405846e86d5bcef6a))
|
||||
|
||||
## [4.36.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.35.1...v4.36.0) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **levels:** make emoji tick marks clickable and remove redundant UI ([07c783a](https://github.com/antialias/soroban-abacus-flashcards/commit/07c783a79454f50e7302b19684be6d2e5930154d))
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **levels:** add smooth CSS transitions for slider thumb movement ([ca8cef1](https://github.com/antialias/soroban-abacus-flashcards/commit/ca8cef1c36efeb1c8c214c74f8bd383f9295be3b))
|
||||
|
||||
## [4.35.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.35.0...v4.35.1) (2025-10-20)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **abacus-react:** export StandaloneBead component wired to AbacusDisplayContext ([0146ce1](https://github.com/antialias/soroban-abacus-flashcards/commit/0146ce1e67da27a24cbaa8338ba6a1a6befd6bd3))
|
||||
|
||||
|
||||
### Performance Improvements
|
||||
|
||||
* **levels:** speed up slider animations for more responsive feel ([1e5467f](https://github.com/antialias/soroban-abacus-flashcards/commit/1e5467fad4e27b832300c49b4f73547dc47598b0))
|
||||
|
||||
## [4.35.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.34.0...v4.35.0) (2025-10-20)
|
||||
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,15 +1,41 @@
|
||||
'use client'
|
||||
|
||||
import * as DropdownMenu from '@radix-ui/react-dropdown-menu'
|
||||
import * as Tooltip from '@radix-ui/react-tooltip'
|
||||
import Link from 'next/link'
|
||||
import { usePathname, useRouter } from 'next/navigation'
|
||||
import React, { useState } from 'react'
|
||||
import React, { useContext, useMemo, useState } from 'react'
|
||||
import { css } from '../../styled-system/css'
|
||||
import { container, hstack } from '../../styled-system/patterns'
|
||||
import { Z_INDEX } from '../constants/zIndex'
|
||||
import { useFullscreen } from '../contexts/FullscreenContext'
|
||||
import { getRandomSubtitle } from '../data/abaciOneSubtitles'
|
||||
import { AbacusDisplayDropdown } from './AbacusDisplayDropdown'
|
||||
|
||||
// Import HomeHeroContext for optional usage
|
||||
import type { Subtitle } from '../data/abaciOneSubtitles'
|
||||
|
||||
type HomeHeroContextValue = {
|
||||
subtitle: Subtitle
|
||||
isHeroVisible: boolean
|
||||
} | null
|
||||
|
||||
// HomeHeroContext - imported dynamically to avoid circular deps
|
||||
let HomeHeroContextModule: any = null
|
||||
try {
|
||||
HomeHeroContextModule = require('../contexts/HomeHeroContext')
|
||||
} catch {
|
||||
// Context not available
|
||||
}
|
||||
|
||||
const HomeHeroContext: React.Context<HomeHeroContextValue> =
|
||||
HomeHeroContextModule?.HomeHeroContext || React.createContext<HomeHeroContextValue>(null)
|
||||
|
||||
// Use HomeHeroContext without requiring it
|
||||
function useOptionalHomeHero(): HomeHeroContextValue {
|
||||
return useContext(HomeHeroContext)
|
||||
}
|
||||
|
||||
interface AppNavBarProps {
|
||||
variant?: 'full' | 'minimal'
|
||||
navSlot?: React.ReactNode
|
||||
@@ -514,6 +540,17 @@ export function AppNavBar({ variant = 'full', navSlot }: AppNavBarProps) {
|
||||
const isArcadePage = pathname?.startsWith('/arcade')
|
||||
const { isFullscreen, toggleFullscreen, exitFullscreen } = useFullscreen()
|
||||
|
||||
// Try to get home hero context (if on homepage)
|
||||
const homeHero = useOptionalHomeHero()
|
||||
|
||||
// Select a random subtitle once on mount (performance: won't change on re-renders)
|
||||
// Use homeHero subtitle if available, otherwise generate one
|
||||
const fallbackSubtitle = useMemo(() => getRandomSubtitle(), [])
|
||||
const subtitle = homeHero?.subtitle || fallbackSubtitle
|
||||
|
||||
// Show branding unless we're on homepage with visible hero
|
||||
const showBranding = !homeHero || !homeHero.isHeroVisible
|
||||
|
||||
// Auto-detect variant based on context
|
||||
const actualVariant = variant === 'full' && (isGamePage || isArcadePage) ? 'minimal' : variant
|
||||
|
||||
@@ -532,54 +569,135 @@ export function AppNavBar({ variant = 'full', navSlot }: AppNavBarProps) {
|
||||
)
|
||||
}
|
||||
|
||||
// Check if we should use transparent styling (when hero is visible)
|
||||
const isTransparent = homeHero?.isHeroVisible
|
||||
|
||||
return (
|
||||
<header
|
||||
className={css({
|
||||
bg: 'white',
|
||||
shadow: 'sm',
|
||||
borderBottom: '1px solid',
|
||||
borderColor: 'gray.200',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 30,
|
||||
})}
|
||||
>
|
||||
<div className={container({ maxW: '7xl', px: '4', py: '3' })}>
|
||||
<div className={hstack({ justify: 'space-between', alignItems: 'center' })}>
|
||||
{/* Logo */}
|
||||
<Link
|
||||
href="/"
|
||||
className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'brand.800',
|
||||
textDecoration: 'none',
|
||||
_hover: { color: 'brand.900' },
|
||||
})}
|
||||
>
|
||||
🧮 Soroban Generator
|
||||
</Link>
|
||||
<Tooltip.Provider delayDuration={200}>
|
||||
<header
|
||||
className={css({
|
||||
bg: isTransparent ? 'transparent' : 'white',
|
||||
shadow: isTransparent ? 'none' : 'sm',
|
||||
borderBottom: isTransparent ? 'none' : '1px solid',
|
||||
borderColor: isTransparent ? 'transparent' : 'gray.200',
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 1000,
|
||||
transition: 'all 0.3s ease',
|
||||
})}
|
||||
>
|
||||
<div className={container({ maxW: '7xl', px: '4', py: '3' })}>
|
||||
<div className={hstack({ justify: 'space-between', alignItems: 'center' })}>
|
||||
{/* Logo - conditionally shown based on hero visibility */}
|
||||
{showBranding ? (
|
||||
<Link
|
||||
href="/"
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '0',
|
||||
textDecoration: 'none',
|
||||
_hover: { '& > .brand-name': { color: 'brand.900' } },
|
||||
opacity: 0,
|
||||
animation: 'fadeIn 0.3s ease-out forwards',
|
||||
})}
|
||||
>
|
||||
<span
|
||||
className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'brand.800',
|
||||
})}
|
||||
>
|
||||
🧮 Abaci One
|
||||
</span>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<span
|
||||
className={css({
|
||||
fontSize: 'xs',
|
||||
fontWeight: 'medium',
|
||||
color: 'brand.600',
|
||||
fontStyle: 'italic',
|
||||
cursor: 'help',
|
||||
_hover: { color: 'brand.700' },
|
||||
})}
|
||||
>
|
||||
{subtitle.text}
|
||||
</span>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Portal>
|
||||
<Tooltip.Content
|
||||
side="bottom"
|
||||
align="start"
|
||||
sideOffset={4}
|
||||
className={css({
|
||||
bg: 'gray.900',
|
||||
color: 'white',
|
||||
px: '3',
|
||||
py: '2',
|
||||
rounded: 'md',
|
||||
fontSize: 'sm',
|
||||
maxW: '250px',
|
||||
shadow: 'lg',
|
||||
zIndex: 50,
|
||||
})}
|
||||
>
|
||||
{subtitle.description}
|
||||
<Tooltip.Arrow
|
||||
className={css({
|
||||
fill: 'gray.900',
|
||||
})}
|
||||
/>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Portal>
|
||||
</Tooltip.Root>
|
||||
</Link>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
|
||||
<div className={hstack({ gap: '6', alignItems: 'center' })}>
|
||||
{/* Navigation Links */}
|
||||
<nav className={hstack({ gap: '4' })}>
|
||||
<NavLink href="/create" currentPath={pathname}>
|
||||
Create
|
||||
</NavLink>
|
||||
<NavLink href="/guide" currentPath={pathname}>
|
||||
Guide
|
||||
</NavLink>
|
||||
<NavLink href="/games" currentPath={pathname}>
|
||||
Games
|
||||
</NavLink>
|
||||
</nav>
|
||||
<div className={hstack({ gap: '6', alignItems: 'center' })}>
|
||||
{/* Navigation Links */}
|
||||
<nav className={hstack({ gap: '4' })}>
|
||||
<NavLink href="/create" currentPath={pathname} isTransparent={isTransparent}>
|
||||
Create
|
||||
</NavLink>
|
||||
<NavLink href="/guide" currentPath={pathname} isTransparent={isTransparent}>
|
||||
Guide
|
||||
</NavLink>
|
||||
<NavLink href="/games" currentPath={pathname} isTransparent={isTransparent}>
|
||||
Games
|
||||
</NavLink>
|
||||
</nav>
|
||||
|
||||
{/* Abacus Style Dropdown */}
|
||||
<AbacusDisplayDropdown isFullscreen={false} />
|
||||
{/* Abacus Style Dropdown */}
|
||||
<AbacusDisplayDropdown isFullscreen={false} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</header>
|
||||
|
||||
{/* Keyframes for fade-in animation */}
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-5px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</Tooltip.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -587,10 +705,12 @@ function NavLink({
|
||||
href,
|
||||
currentPath,
|
||||
children,
|
||||
isTransparent,
|
||||
}: {
|
||||
href: string
|
||||
currentPath: string | null
|
||||
children: React.ReactNode
|
||||
isTransparent?: boolean
|
||||
}) {
|
||||
const isActive = currentPath === href || (href !== '/' && currentPath?.startsWith(href))
|
||||
|
||||
@@ -604,8 +724,20 @@ function NavLink({
|
||||
minW: { base: '44px', md: 'auto' },
|
||||
fontSize: 'sm',
|
||||
fontWeight: 'medium',
|
||||
color: isActive ? 'brand.700' : 'gray.600',
|
||||
bg: isActive ? 'brand.50' : 'transparent',
|
||||
color: isTransparent
|
||||
? isActive
|
||||
? 'white'
|
||||
: 'rgba(255, 255, 255, 0.8)'
|
||||
: isActive
|
||||
? 'brand.700'
|
||||
: 'gray.600',
|
||||
bg: isTransparent
|
||||
? isActive
|
||||
? 'rgba(255, 255, 255, 0.15)'
|
||||
: 'transparent'
|
||||
: isActive
|
||||
? 'brand.50'
|
||||
: 'transparent',
|
||||
rounded: 'lg',
|
||||
transition: 'all',
|
||||
textDecoration: 'none',
|
||||
@@ -613,8 +745,8 @@ function NavLink({
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
_hover: {
|
||||
color: isActive ? 'brand.800' : 'gray.900',
|
||||
bg: isActive ? 'brand.100' : 'gray.50',
|
||||
color: isTransparent ? 'white' : isActive ? 'brand.800' : 'gray.900',
|
||||
bg: isTransparent ? 'rgba(255, 255, 255, 0.2)' : isActive ? 'brand.100' : 'gray.50',
|
||||
},
|
||||
})}
|
||||
>
|
||||
|
||||
189
apps/web/src/components/HeroAbacus.tsx
Normal file
189
apps/web/src/components/HeroAbacus.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { AbacusReact, useAbacusConfig } from '@soroban/abacus-react'
|
||||
import { css } from '../../styled-system/css'
|
||||
import { useHomeHero } from '../contexts/HomeHeroContext'
|
||||
|
||||
export function HeroAbacus() {
|
||||
const {
|
||||
subtitle,
|
||||
abacusValue,
|
||||
setAbacusValue,
|
||||
setIsHeroVisible,
|
||||
isAbacusLoaded,
|
||||
isSubtitleLoaded,
|
||||
} = useHomeHero()
|
||||
const appConfig = useAbacusConfig()
|
||||
const heroRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// Styling for structural elements (solid, no translucency)
|
||||
const structuralStyles = {
|
||||
columnPosts: {
|
||||
fill: 'rgb(255, 255, 255)',
|
||||
stroke: 'rgb(200, 200, 200)',
|
||||
strokeWidth: 2,
|
||||
},
|
||||
reckoningBar: {
|
||||
fill: 'rgb(255, 255, 255)',
|
||||
stroke: 'rgb(200, 200, 200)',
|
||||
strokeWidth: 3,
|
||||
},
|
||||
}
|
||||
|
||||
// Detect when hero scrolls out of view
|
||||
useEffect(() => {
|
||||
if (!heroRef.current) return
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
([entry]) => {
|
||||
// Hero is visible if more than 20% is in viewport
|
||||
setIsHeroVisible(entry.intersectionRatio > 0.2)
|
||||
},
|
||||
{
|
||||
threshold: [0, 0.2, 0.5, 1],
|
||||
}
|
||||
)
|
||||
|
||||
observer.observe(heroRef.current)
|
||||
|
||||
return () => observer.disconnect()
|
||||
}, [setIsHeroVisible])
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={heroRef}
|
||||
className={css({
|
||||
height: '100vh',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
bg: 'gray.900',
|
||||
position: 'relative',
|
||||
overflow: 'hidden',
|
||||
px: '4',
|
||||
py: '12',
|
||||
})}
|
||||
>
|
||||
{/* Background pattern */}
|
||||
<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',
|
||||
})}
|
||||
/>
|
||||
|
||||
{/* Title and Subtitle Section - DIRECT CHILD */}
|
||||
<div
|
||||
className={css({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '2',
|
||||
zIndex: 10,
|
||||
})}
|
||||
>
|
||||
<h1
|
||||
className={css({
|
||||
fontSize: { base: '4xl', md: '6xl', lg: '7xl' },
|
||||
fontWeight: 'bold',
|
||||
background: 'linear-gradient(135deg, #fbbf24 0%, #f59e0b 50%, #fbbf24 100%)',
|
||||
backgroundClip: 'text',
|
||||
color: 'transparent',
|
||||
})}
|
||||
>
|
||||
Abaci One
|
||||
</h1>
|
||||
<p
|
||||
className={css({
|
||||
fontSize: { base: 'xl', md: '2xl' },
|
||||
fontWeight: 'medium',
|
||||
color: 'purple.300',
|
||||
fontStyle: 'italic',
|
||||
marginBottom: '8',
|
||||
opacity: isSubtitleLoaded ? 1 : 0,
|
||||
transition: 'opacity 0.5s ease-in-out',
|
||||
})}
|
||||
>
|
||||
{subtitle.text}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Large Interactive Abacus - DIRECT CHILD */}
|
||||
<div
|
||||
className={css({
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
flex: '1',
|
||||
width: '100%',
|
||||
zIndex: 10,
|
||||
opacity: isAbacusLoaded ? 1 : 0,
|
||||
transition: 'opacity 0.5s ease-in-out',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
transform: { base: 'scale(2)', md: 'scale(3)', lg: 'scale(4)' },
|
||||
transformOrigin: 'center center',
|
||||
transition: 'all 0.5s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
})}
|
||||
>
|
||||
<AbacusReact
|
||||
value={abacusValue}
|
||||
columns={4}
|
||||
beadShape={appConfig.beadShape}
|
||||
showNumbers={true}
|
||||
interactive={true}
|
||||
animated={true}
|
||||
customStyles={structuralStyles}
|
||||
onValueChange={setAbacusValue}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subtle hint to scroll - DIRECT CHILD */}
|
||||
<div
|
||||
className={css({
|
||||
position: 'relative',
|
||||
fontSize: 'sm',
|
||||
color: 'gray.400',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '2',
|
||||
animation: 'bounce 2s ease-in-out infinite',
|
||||
zIndex: 10,
|
||||
})}
|
||||
>
|
||||
<span>Scroll to explore</span>
|
||||
<span>↓</span>
|
||||
</div>
|
||||
|
||||
{/* Keyframes for bounce animation */}
|
||||
<style
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
@keyframes bounce {
|
||||
0%, 100% {
|
||||
transform: translateY(0);
|
||||
opacity: 0.7;
|
||||
}
|
||||
50% {
|
||||
transform: translateY(-10px);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
127
apps/web/src/contexts/HomeHeroContext.tsx
Normal file
127
apps/web/src/contexts/HomeHeroContext.tsx
Normal file
@@ -0,0 +1,127 @@
|
||||
'use client'
|
||||
|
||||
import type React from 'react'
|
||||
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import type { Subtitle } from '../data/abaciOneSubtitles'
|
||||
import { subtitles } from '../data/abaciOneSubtitles'
|
||||
|
||||
interface HomeHeroContextValue {
|
||||
subtitle: Subtitle
|
||||
abacusValue: number
|
||||
setAbacusValue: (value: number) => void
|
||||
isHeroVisible: boolean
|
||||
setIsHeroVisible: (visible: boolean) => void
|
||||
isAbacusLoaded: boolean
|
||||
isSubtitleLoaded: boolean
|
||||
}
|
||||
|
||||
const HomeHeroContext = createContext<HomeHeroContextValue | null>(null)
|
||||
|
||||
export { HomeHeroContext }
|
||||
|
||||
export function HomeHeroProvider({ children }: { children: React.ReactNode }) {
|
||||
// Use first subtitle for SSR, then select random one on client mount
|
||||
const [subtitle, setSubtitle] = useState<Subtitle>(subtitles[0])
|
||||
const [isSubtitleLoaded, setIsSubtitleLoaded] = useState(false)
|
||||
|
||||
// Select random subtitle only on client side, persist per-session
|
||||
useEffect(() => {
|
||||
// Check if we have a stored subtitle index for this session
|
||||
const storedIndex = sessionStorage.getItem('heroSubtitleIndex')
|
||||
|
||||
if (storedIndex !== null) {
|
||||
// Use the stored subtitle index
|
||||
const index = parseInt(storedIndex, 10)
|
||||
if (!Number.isNaN(index) && index >= 0 && index < subtitles.length) {
|
||||
setSubtitle(subtitles[index])
|
||||
setIsSubtitleLoaded(true)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a new random index and store it
|
||||
const randomIndex = Math.floor(Math.random() * subtitles.length)
|
||||
sessionStorage.setItem('heroSubtitleIndex', randomIndex.toString())
|
||||
setSubtitle(subtitles[randomIndex])
|
||||
setIsSubtitleLoaded(true)
|
||||
}, [])
|
||||
|
||||
// Shared abacus value - always start at 0 for SSR/hydration consistency
|
||||
const [abacusValue, setAbacusValue] = useState(0)
|
||||
const [isAbacusLoaded, setIsAbacusLoaded] = useState(false)
|
||||
const isLoadingFromStorage = useRef(false)
|
||||
|
||||
// Load from sessionStorage after mount (client-only, no hydration mismatch)
|
||||
useEffect(() => {
|
||||
console.log('[HeroAbacus] Loading from sessionStorage...')
|
||||
isLoadingFromStorage.current = true // Block saves during load
|
||||
|
||||
const saved = sessionStorage.getItem('heroAbacusValue')
|
||||
console.log('[HeroAbacus] Saved value from storage:', saved)
|
||||
|
||||
if (saved) {
|
||||
const parsedValue = parseInt(saved, 10)
|
||||
console.log('[HeroAbacus] Parsed value:', parsedValue)
|
||||
if (!Number.isNaN(parsedValue)) {
|
||||
console.log('[HeroAbacus] Setting abacus value to:', parsedValue)
|
||||
setAbacusValue(parsedValue)
|
||||
}
|
||||
} else {
|
||||
console.log('[HeroAbacus] No saved value found, staying at 0')
|
||||
}
|
||||
|
||||
// Use setTimeout to ensure the value has been set before we allow saves
|
||||
setTimeout(() => {
|
||||
isLoadingFromStorage.current = false
|
||||
setIsAbacusLoaded(true)
|
||||
console.log('[HeroAbacus] Load complete, allowing saves now and fading in')
|
||||
}, 0)
|
||||
}, [])
|
||||
|
||||
// Persist value to sessionStorage when it changes (but skip during load)
|
||||
useEffect(() => {
|
||||
console.log(
|
||||
'[HeroAbacus] Save effect triggered. Value:',
|
||||
abacusValue,
|
||||
'isLoadingFromStorage:',
|
||||
isLoadingFromStorage.current
|
||||
)
|
||||
|
||||
if (!isLoadingFromStorage.current) {
|
||||
console.log('[HeroAbacus] Saving to sessionStorage:', abacusValue)
|
||||
sessionStorage.setItem('heroAbacusValue', abacusValue.toString())
|
||||
console.log(
|
||||
'[HeroAbacus] Saved successfully. Storage now contains:',
|
||||
sessionStorage.getItem('heroAbacusValue')
|
||||
)
|
||||
} else {
|
||||
console.log('[HeroAbacus] Skipping save (currently loading from storage)')
|
||||
}
|
||||
}, [abacusValue])
|
||||
|
||||
// Track hero visibility for nav branding
|
||||
const [isHeroVisible, setIsHeroVisible] = useState(true)
|
||||
|
||||
const value = useMemo(
|
||||
() => ({
|
||||
subtitle,
|
||||
abacusValue,
|
||||
setAbacusValue,
|
||||
isHeroVisible,
|
||||
setIsHeroVisible,
|
||||
isAbacusLoaded,
|
||||
isSubtitleLoaded,
|
||||
}),
|
||||
[subtitle, abacusValue, isHeroVisible, isAbacusLoaded, isSubtitleLoaded]
|
||||
)
|
||||
|
||||
return <HomeHeroContext.Provider value={value}>{children}</HomeHeroContext.Provider>
|
||||
}
|
||||
|
||||
export function useHomeHero() {
|
||||
const context = useContext(HomeHeroContext)
|
||||
if (!context) {
|
||||
throw new Error('useHomeHero must be used within HomeHeroProvider')
|
||||
}
|
||||
return context
|
||||
}
|
||||
97
apps/web/src/data/abaciOneSubtitles.ts
Normal file
97
apps/web/src/data/abaciOneSubtitles.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
/**
|
||||
* Abaci One subtitle options with descriptions
|
||||
* Three-word rhyming subtitles for the main app navigation
|
||||
*/
|
||||
|
||||
export interface Subtitle {
|
||||
text: string
|
||||
description: string
|
||||
}
|
||||
|
||||
export const subtitles: Subtitle[] = [
|
||||
{ text: 'Speed Bead Lead', description: 'blaze through bead races' },
|
||||
{ text: 'Rod Mod Nod', description: 'tweak rod technique, approval earned' },
|
||||
{ text: 'Grid Kid Lid', description: 'lock in neat grid habits' },
|
||||
{ text: 'Count Mount Amount', description: 'stack up that number sense' },
|
||||
{ text: 'Stack Track Tack', description: 'line up beads, lock in sums' },
|
||||
{ text: 'Quick Flick Trick', description: 'rapid-fire bead tactics' },
|
||||
{ text: 'Flash Dash Math', description: 'fly through numeric challenges' },
|
||||
{ text: 'Slide Glide Pride', description: 'smooth soroban strokes' },
|
||||
{ text: 'Shift Sift Gift', description: 'sort beads, reveal talent' },
|
||||
{ text: 'Beat Seat Meet', description: 'compete head-to-head' },
|
||||
{ text: 'Brain Train Gain', description: 'mental math muscle building' },
|
||||
{ text: 'Flow Show Pro', description: 'demonstrate soroban mastery' },
|
||||
{ text: 'Fast Blast Past', description: 'surpass speed limits' },
|
||||
{ text: 'Snap Tap Map', description: 'chart your calculation path' },
|
||||
{ text: 'Row Grow Know', description: 'advance through structured drills' },
|
||||
{ text: 'Drill Skill Thrill', description: 'practice that excites' },
|
||||
{ text: 'Think Link Sync', description: 'connect mind and beads' },
|
||||
{ text: 'Boost Joust Roost', description: 'power up, compete, settle in' },
|
||||
{ text: 'Add Grad Rad', description: 'level up addition awesomely' },
|
||||
{ text: 'Sum Fun Run', description: 'enjoy the arithmetic sprint' },
|
||||
{ text: 'Track Stack Pack', description: 'organize solutions systematically' },
|
||||
{ text: 'Beat Neat Feat', description: 'clean victories, impressive wins' },
|
||||
{ text: 'Math Path Wrath', description: 'dominate numeric challenges' },
|
||||
{ text: 'Spark Mark Arc', description: 'ignite progress, track growth' },
|
||||
{ text: 'Race Pace Ace', description: 'speed up, master it' },
|
||||
{ text: 'Flex Hex Reflex', description: 'adapt calculations instantly' },
|
||||
{ text: 'Glide Pride Stride', description: 'smooth confident progress' },
|
||||
{ text: 'Flash Dash Smash', description: 'speed through, crush totals' },
|
||||
{ text: 'Stack Attack Jack', description: 'aggressive bead strategies' },
|
||||
{ text: 'Quick Pick Click', description: 'rapid bead selection' },
|
||||
{ text: 'Snap Map Tap', description: 'visualize and execute' },
|
||||
{ text: 'Mind Find Grind', description: 'discover mental endurance' },
|
||||
{ text: 'Flip Skip Rip', description: 'fast transitions, tear through' },
|
||||
{ text: 'Blend Trend Send', description: 'mix methods, share progress' },
|
||||
{ text: 'Power Tower Hour', description: 'build skills intensively' },
|
||||
{ text: 'Launch Staunch Haunch', description: 'start strong, stay firm' },
|
||||
{ text: 'Rush Crush Hush', description: 'speed quietly dominates' },
|
||||
{ text: 'Swipe Stripe Hype', description: 'sleek moves, excitement' },
|
||||
{ text: 'Train Gain Sustain', description: 'build lasting ability' },
|
||||
{ text: 'Frame Claim Flame', description: 'structure your fire' },
|
||||
{ text: 'Streak Peak Tweak', description: 'hot runs, optimize performance' },
|
||||
{ text: 'Edge Pledge Wedge', description: 'commit to precision' },
|
||||
{ text: 'Pace Grace Space', description: 'rhythm, elegance, room to grow' },
|
||||
{ text: 'Link Think Brink', description: 'connect at breakthrough edge' },
|
||||
{ text: 'Quest Test Best', description: 'challenge yourself to excel' },
|
||||
{ text: 'Drive Thrive Arrive', description: 'push hard, succeed, reach goals' },
|
||||
{ text: 'Smart Start Chart', description: 'begin wisely, track progress' },
|
||||
{ text: 'Boost Coast Toast', description: 'accelerate, cruise, celebrate' },
|
||||
{ text: 'Spark Dark Embark', description: 'ignite before dawn journeys' },
|
||||
{ text: 'Blaze Graze Amaze', description: 'burn through, touch lightly, wow' },
|
||||
{ text: 'Shift Drift Gift', description: 'adapt smoothly, reveal talent' },
|
||||
{ text: 'Zone Hone Own', description: 'focus, refine, claim mastery' },
|
||||
{ text: 'Vault Halt Exalt', description: 'leap high, pause, celebrate' },
|
||||
{ text: 'Peak Seek Streak', description: 'find heights, maintain momentum' },
|
||||
{ text: 'Glow Show Grow', description: 'shine, display, expand' },
|
||||
{ text: 'Scope Hope Rope', description: 'survey possibilities, climb up' },
|
||||
{ text: 'Core Score More', description: 'fundamentals yield better results' },
|
||||
{ text: 'Rank Bank Thank', description: 'earn status, save wins, appreciate' },
|
||||
{ text: 'Merge Surge Verge', description: 'combine forces, power up, edge closer' },
|
||||
{ text: 'Bold Gold Hold', description: 'brave attempts, prize rewards, maintain' },
|
||||
{ text: 'Rise Prize Wise', description: 'ascend, win, learn' },
|
||||
{ text: 'Move Groove Prove', description: 'act, find rhythm, demonstrate' },
|
||||
{ text: 'Trust Thrust Adjust', description: 'believe, push, refine' },
|
||||
{ text: 'Beam Dream Team', description: 'radiate, aspire, collaborate' },
|
||||
{ text: 'Spin Win Grin', description: 'rotate beads, succeed, smile' },
|
||||
{ text: 'String Ring Bring', description: 'connect, cycle, deliver' },
|
||||
{ text: 'Clear Gear Steer', description: 'focus, equip, direct' },
|
||||
{ text: 'Path Math Aftermath', description: 'route, calculate, results' },
|
||||
{ text: 'Play Slay Day', description: 'engage, dominate, own it' },
|
||||
{ text: 'Code Mode Road', description: 'pattern, style, journey' },
|
||||
{ text: 'Craft Draft Shaft', description: 'build, sketch, core structure' },
|
||||
{ text: 'Light Might Fight', description: 'illuminate, empower, compete' },
|
||||
{ text: 'Stream Dream Extreme', description: 'flow, envision, push limits' },
|
||||
{ text: 'Claim Frame Aim', description: 'assert, structure, target' },
|
||||
{ text: 'Chart Smart Start', description: 'map, intelligent, begin' },
|
||||
{ text: 'Bright Flight Height', description: 'brilliant, soar, elevation' },
|
||||
]
|
||||
|
||||
/**
|
||||
* Get a random subtitle from the list
|
||||
* Uses current timestamp as seed for variety across sessions
|
||||
*/
|
||||
export function getRandomSubtitle(): Subtitle {
|
||||
const index = Math.floor(Math.random() * subtitles.length)
|
||||
return subtitles[index]
|
||||
}
|
||||
123
apps/web/src/data/kyuLevelDetails.ts
Normal file
123
apps/web/src/data/kyuLevelDetails.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
/**
|
||||
* Detailed requirements for each Kyu level in the Soroban certification system
|
||||
* Source: shuzan.jp
|
||||
*
|
||||
* Note: Stored verbatim from source. Display formatting/translation happens in the UI layer.
|
||||
*/
|
||||
|
||||
export const kyuLevelDetails = {
|
||||
'10-kyu': `Add/Sub: 2-digit, 5口, 10字
|
||||
|
||||
×: 実+法 = 3 digits (20 problems)
|
||||
|
||||
Time: 20 min; Pass ≥ 60/200.
|
||||
shuzan.jp`,
|
||||
|
||||
'9-kyu': `Add/Sub: 2-digit, 5口, 10字
|
||||
|
||||
×: 実+法 = 3 digits (20)
|
||||
|
||||
Time: 20 min; Pass ≥ 120/200. (If only one part clears, it's treated as 10-kyu per federation notes.)
|
||||
shuzan.jp`,
|
||||
|
||||
'8-kyu': `Add/Sub: 2-digit, 8口, 16字
|
||||
|
||||
×: 実+法 = 4 digits (10)
|
||||
|
||||
÷: 法+商 = 3 digits (10)
|
||||
|
||||
Time: 20 min; Pass ≥ 120/200.
|
||||
shuzan.jp`,
|
||||
|
||||
'7-kyu': `Add/Sub: 2-digit, 10口, 20字
|
||||
|
||||
×: 実+法 = 4 digits (10)
|
||||
|
||||
÷: 法+商 = 4 digits (10)
|
||||
|
||||
Time: 20 min; Pass ≥ 120/200.
|
||||
shuzan.jp`,
|
||||
|
||||
'6-kyu': `Add/Sub: 10口, 30字
|
||||
|
||||
×: 実+法 = 5 digits (20)
|
||||
|
||||
÷: 法+商 = 4 digits (20)
|
||||
|
||||
Time: 30 min; Pass ≥ 210/300.
|
||||
shuzan.jp`,
|
||||
|
||||
'5-kyu': `Add/Sub: 10口, 40字
|
||||
|
||||
×: 実+法 = 6 digits (20)
|
||||
|
||||
÷: 法+商 = 5 digits (20)
|
||||
|
||||
Time: 30 min; Pass ≥ 210/300.
|
||||
shuzan.jp`,
|
||||
|
||||
'4-kyu': `Add/Sub: 10口, 50字
|
||||
|
||||
×: 実+法 = 7 digits (20)
|
||||
|
||||
÷: 法+商 = 6 digits (20)
|
||||
|
||||
Time: 30 min; Pass ≥ 210/300.
|
||||
shuzan.jp`,
|
||||
|
||||
'Pre-3-kyu': `Add/Sub: 10口, 50字 ×5題 and 10口, 60字 ×5題 (total 10)
|
||||
|
||||
×: 実+法 = 7 digits (20)
|
||||
|
||||
÷: 法+商 = 6 digits (20)
|
||||
|
||||
Time: 30 min; Pass ≥ 240/300.
|
||||
shuzan.jp`,
|
||||
|
||||
'3-kyu': `Add/Sub: 10口, 60字
|
||||
|
||||
×: 実+法 = 7 digits (20)
|
||||
|
||||
÷: 法+商 = 6 digits (20)
|
||||
|
||||
Time: 30 min; Pass ≥ 240/300.
|
||||
shuzan.jp`,
|
||||
|
||||
'Pre-2-kyu': `Add/Sub: 10口, 70字
|
||||
|
||||
×: 実+法 = 8 digits (20)
|
||||
|
||||
÷: 法+商 = 7 digits (20)
|
||||
|
||||
Time: 30 min; Pass ≥ 240/300.
|
||||
shuzan.jp`,
|
||||
|
||||
'2-kyu': `Add/Sub: 10口, 80字
|
||||
|
||||
×: 実+法 = 9 digits (20)
|
||||
|
||||
÷: 法+商 = 8 digits (20)
|
||||
|
||||
Time: 30 min; Pass ≥ 240/300.
|
||||
shuzan.jp`,
|
||||
|
||||
'Pre-1-kyu': `Add/Sub: 10口, 90字
|
||||
|
||||
×: 実+法 = 10 digits (20)
|
||||
|
||||
÷: 法+商 = 9 digits (20)
|
||||
|
||||
Time: 30 min; Pass ≥ 240/300.
|
||||
shuzan.jp`,
|
||||
|
||||
'1-kyu': `Add/Sub: 10口, 100字
|
||||
|
||||
×: 実+法 = 11 digits (20)
|
||||
|
||||
÷: 法+商 = 10 digits (20)
|
||||
|
||||
Time: 30 min; Pass ≥ 240/300.
|
||||
shuzan.jp`,
|
||||
} as const
|
||||
|
||||
export type KyuLevel = keyof typeof kyuLevelDetails
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "4.35.0",
|
||||
"version": "4.48.2",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user