Compare commits

...

15 Commits

Author SHA1 Message Date
semantic-release-bot
1d4419364a chore(release): 4.45.0 [skip ci]
## [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](cce8980e17))
2025-10-20 17:39:58 +00:00
Thomas Hallock
cce8980e17 feat(branding): rebrand navigation from 'Soroban Generator' to 'Abaci One'
Changes:
- Add new subtitle data file with 75 three-word rhyming options
- Update AppNavBar to display "🧮 Abaci One" with random subtitle
- Implement Radix UI tooltip showing subtitle description on hover
- Use useMemo for performance (subtitle won't change on re-renders)
- Clean, minimal design with italic subtitle and help cursor

Implementation:
- Created `/src/data/abaciOneSubtitles.ts` with subtitle data structure
- Updated AppNavBar imports to include Radix Tooltip and subtitle util
- Wrapped navigation in Tooltip.Provider with 200ms delay
- Logo displays vertically with brand name and subtitle
- Tooltip shows description like "blaze through bead races" on hover

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 12:38:38 -05:00
semantic-release-bot
4bcce2a8db chore(release): 4.44.3 [skip ci]
## [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](29d20a6c07))
* **levels:** use uniform padding on operator box grid ([2818fd1](2818fd15ca))
2025-10-20 16:40:18 +00:00
Thomas Hallock
2818fd15ca fix(levels): use uniform padding on operator box grid
- Replaced separate pl, pr, py with uniform p: '2'
- Ensures equal padding on all sides (left, right, top, bottom)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 11:39:09 -05:00
Thomas Hallock
29d20a6c07 fix(levels): reduce operator box sizes and remove divider line
- Reduced box dimensions from 200x180px to 170x150px for better fit
- Reduced grid gap from '3' to '2' for tighter layout
- Reduced box padding from '4' to '3' and gap from '2' to '1.5'
- Reduced maxW from 480px to 400px
- Changed my (margins) to py (padding) for more control
- Removed borderRight divider line between operator boxes and abacus

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 11:38:14 -05:00
semantic-release-bot
be2c3f63b0 chore(release): 4.44.2 [skip ci]
## [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](aa0bdcf686))
2025-10-20 16:37:17 +00:00
Thomas Hallock
aa0bdcf686 fix(levels): match top/bottom margins to left padding on kyu detail boxes
- Reduced my (vertical margins) from '6' to '2' to match pl (left padding)
- Ensures consistent spacing on all sides of the detail box grid

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 11:36:11 -05:00
semantic-release-bot
baea602000 chore(release): 4.44.1 [skip ci]
## [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](05dd0b30d3))
2025-10-20 16:35:46 +00:00
Thomas Hallock
05dd0b30d3 fix(levels): add fixed dimensions and margins to kyu detail boxes
- Set fixed width (200px) and height (180px) for operator boxes to prevent shifting
- Add vertical margins (my: 6) to grid container for better spacing
- Ensures boxes stay in consistent positions when sliding through levels
- Large enough to accommodate longest content (11 digits)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 11:34:32 -05:00
semantic-release-bot
4febf5905b chore(release): 4.44.0 [skip ci]
## [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](6739d59f2b))
2025-10-20 16:31:02 +00:00
Thomas Hallock
6739d59f2b feat(levels): redesign kyu details with larger operators and prominent digits
- Increase operator icon size from xl to 4xl for better visibility
- Center-align card layout with operator icon at top
- Extract and prominently display digit count in 2xl bold text
- Change hover effect from slide to scale for centered design
- Increase base font size from sm to md

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 11:29:47 -05:00
semantic-release-bot
cb20019c16 chore(release): 4.43.2 [skip ci]
## [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](d90b5d5532))
2025-10-20 16:29:27 +00:00
Thomas Hallock
d90b5d5532 refactor(levels): remove Time and Pass sections from kyu details
Remove the Time and Pass requirement cards since we don't have actual tests
implemented. Now only showing Add/Sub, Multiply, and Divide requirements.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 11:28:19 -05:00
semantic-release-bot
7028db0263 chore(release): 4.43.1 [skip ci]
## [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](fa3b73c691))
2025-10-20 16:28:08 +00:00
Thomas Hallock
fa3b73c691 fix(levels): use two-column grid for kyu details to prevent clipping
Change from single-column flex to two-column grid layout to avoid vertical
overflow. Increased max width to 480px to accommodate the wider layout.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-20 11:27:01 -05:00
5 changed files with 375 additions and 131 deletions

View File

@@ -1,3 +1,53 @@
## [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)

View File

@@ -196,54 +196,64 @@ function getLevelDetailsKey(levelName: string): string | null {
function parseKyuDetails(rawText: string) {
const lines = rawText.split('\n').filter((line) => line.trim() && !line.includes('shuzan.jp'))
const sections: Array<{ icon: string; label: string; value: string }> = []
// Always return sections in consistent order: Add/Sub, Multiply, Divide
const sections: Array<{
type: 'addSub' | 'multiply' | 'divide'
icon: string
label: string
digits: string | null
rows: string | null
chars: string | null
problems: string | null
}> = [
{
type: 'addSub',
icon: '',
label: 'Add/Sub',
digits: null,
rows: null,
chars: null,
problems: null,
},
{
type: 'multiply',
icon: '✖️',
label: 'Multiply',
digits: null,
rows: null,
chars: null,
problems: null,
},
{
type: 'divide',
icon: '➗',
label: 'Divide',
digits: null,
rows: null,
chars: null,
problems: null,
},
]
for (const line of lines) {
if (line.includes('Add/Sub:')) {
// Parse addition/subtraction requirements
const match = line.match(/(\d+)-digit.*?(\d+)口.*?(\d+)字/)
if (match) {
sections.push({
icon: '',
label: 'Add/Sub',
value: `${match[1]}-digit, ${match[2]} rows, ${match[3]} chars`,
})
sections[0].digits = match[1]
sections[0].rows = match[2]
sections[0].chars = match[3]
}
} else if (line.includes('×:')) {
// Parse multiplication requirements
const match = line.match(/(\d+) digits.*?\((\d+)/)
if (match) {
sections.push({
icon: '✖️',
label: 'Multiply',
value: `${match[1]}-digit total (${match[2]} problems)`,
})
sections[1].digits = match[1]
sections[1].problems = match[2]
}
} else if (line.includes('÷:')) {
// Parse division requirements
const match = line.match(/(\d+) digits.*?\((\d+)/)
if (match) {
sections.push({
icon: '➗',
label: 'Divide',
value: `${match[1]}-digit total (${match[2]} problems)`,
})
}
} else if (line.includes('Time:')) {
// Parse time and pass requirements
const timeMatch = line.match(/(\d+) min/)
const passMatch = line.match(/≥\s*(\d+)\/(\d+)/)
if (timeMatch && passMatch) {
sections.push({
icon: '⏱️',
label: 'Time',
value: `${timeMatch[1]} minutes`,
})
sections.push({
icon: '✅',
label: 'Pass',
value: `${passMatch[1]}/${passMatch[2]} pts`,
})
sections[2].digits = match[1]
sections[2].problems = match[2]
}
}
}
@@ -670,77 +680,108 @@ export default function LevelsPage() {
const sections = rawText ? parseKyuDetails(rawText) : []
// Use consistent sizing across all levels
const sizing = { fontSize: 'sm', gap: '3', iconSize: 'xl' }
const sizing = { fontSize: 'md', gap: '3', iconSize: '4xl' }
return sections.length > 0 ? (
<div
className={css({
flex: '0 0 auto',
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
gap: sizing.gap,
pr: '4',
pl: '2',
borderRight: '1px solid',
borderColor: 'gray.600',
maxW: '320px',
display: 'grid',
gridTemplateColumns: 'repeat(2, 1fr)',
gap: '2',
p: '2',
maxW: '400px',
alignContent: 'center',
})}
>
{sections.map((section, idx) => (
<div
key={idx}
className={css({
bg: 'rgba(0, 0, 0, 0.4)',
border: '1px solid',
borderColor: 'gray.700',
rounded: 'md',
p: '2',
transition: 'all 0.2s',
_hover: {
borderColor: 'gray.500',
transform: 'translateX(4px)',
},
})}
>
{sections.map((section, idx) => {
const hasData = section.digits !== null
const levelColor =
currentLevel.color === 'green'
? 'green.300'
: currentLevel.color === 'blue'
? 'blue.300'
: 'violet.300'
return (
<div
key={idx}
className={css({
bg: hasData ? 'rgba(0, 0, 0, 0.4)' : 'rgba(0, 0, 0, 0.2)',
border: '1px solid',
borderColor: hasData ? 'gray.700' : 'gray.800',
rounded: 'md',
p: '3',
transition: 'all 0.2s',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
gap: '2',
mb: '1',
justifyContent: 'center',
gap: '1.5',
opacity: hasData ? 1 : 0.3,
width: '170px',
height: '150px',
_hover: hasData
? {
borderColor: 'gray.500',
transform: 'scale(1.05)',
}
: {},
})}
>
<span className={css({ fontSize: sizing.iconSize })}>
<span className={css({ fontSize: sizing.iconSize, lineHeight: '1' })}>
{section.icon}
</span>
<span
{hasData && section.digits && (
<>
<div
className={css({
fontSize: '2xl',
fontWeight: 'bold',
color: levelColor,
})}
>
{section.digits} digits
</div>
{(section.rows || section.chars) && (
<div
className={css({
fontSize: 'sm',
color: 'gray.400',
textAlign: 'center',
})}
>
{section.rows && `${section.rows} rows`}
{section.rows && section.chars && ' • '}
{section.chars && `${section.chars} chars`}
</div>
)}
{section.problems && (
<div
className={css({
fontSize: 'sm',
color: 'gray.400',
textAlign: 'center',
})}
>
{section.problems} problems
</div>
)}
</>
)}
<div
className={css({
fontSize: sizing.fontSize,
fontWeight: 'semibold',
color:
currentLevel.color === 'green'
? 'green.300'
: currentLevel.color === 'blue'
? 'blue.300'
: 'violet.300',
fontSize: 'xs',
color: hasData ? 'gray.500' : 'gray.700',
textAlign: 'center',
fontWeight: hasData ? 'normal' : 'bold',
})}
>
{section.label}
</span>
</div>
</div>
<div
className={css({
fontSize: sizing.fontSize,
color: 'gray.400',
lineHeight: '1.4',
pl: sizing.gap === '3' ? '6' : sizing.gap === '2' ? '5' : '4',
})}
>
{section.value}
</div>
</div>
))}
)
})}
</div>
) : null
})()}

View File

@@ -1,13 +1,15 @@
'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, { 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'
interface AppNavBarProps {
@@ -514,6 +516,9 @@ export function AppNavBar({ variant = 'full', navSlot }: AppNavBarProps) {
const isArcadePage = pathname?.startsWith('/arcade')
const { isFullscreen, toggleFullscreen, exitFullscreen } = useFullscreen()
// Select a random subtitle once on mount (performance: won't change on re-renders)
const subtitle = useMemo(() => getRandomSubtitle(), [])
// Auto-detect variant based on context
const actualVariant = variant === 'full' && (isGamePage || isArcadePage) ? 'minimal' : variant
@@ -533,53 +538,104 @@ export function AppNavBar({ variant = 'full', navSlot }: AppNavBarProps) {
}
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: '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({
display: 'flex',
flexDirection: 'column',
gap: '0',
textDecoration: 'none',
_hover: { '& > .brand-name': { color: 'brand.900' } },
})}
>
<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 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}>
Create
</NavLink>
<NavLink href="/guide" currentPath={pathname}>
Guide
</NavLink>
<NavLink href="/games" currentPath={pathname}>
Games
</NavLink>
</nav>
{/* Abacus Style Dropdown */}
<AbacusDisplayDropdown isFullscreen={false} />
{/* Abacus Style Dropdown */}
<AbacusDisplayDropdown isFullscreen={false} />
</div>
</div>
</div>
</div>
</header>
</header>
</Tooltip.Provider>
)
}

View 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]
}

View File

@@ -1,6 +1,6 @@
{
"name": "soroban-monorepo",
"version": "4.43.0",
"version": "4.45.0",
"private": true,
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
"workspaces": [