Compare commits

..

5 Commits

Author SHA1 Message Date
semantic-release-bot
0c46f3a7ba chore(abacus-react): release v1.4.0 [skip ci]
# [1.4.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v1.3.0...abacus-react-v1.4.0) (2025-09-29)

### Bug Fixes

* export missing hooks and types from @soroban/abacus-react package ([423ba55](423ba55350))
* migrate viewport from metadata to separate viewport export ([1fe12c4](1fe12c4837))

### Features

* add middleware for pathname header support in [@nav](https://github.com/nav) fallback ([b7e7c4b](b7e7c4beff))
* implement [@nav](https://github.com/nav) parallel routes for game name display in mini navigation ([885fc72](885fc725dc))
2025-09-29 13:54:44 +00:00
Thomas Hallock
1fe12c4837 fix: migrate viewport from metadata to separate viewport export
Move viewport configuration from metadata export to dedicated viewport
export in root layout, following Next.js App Router best practices.

Resolves deprecation warning:
"Unsupported metadata viewport is configured in metadata export"

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-29 08:54:01 -05:00
Thomas Hallock
b7e7c4beff feat: add middleware for pathname header support in @nav fallback
Add Next.js middleware to set x-pathname header on all requests,
enabling Server Components to access pathname for route-based
navigation fallback when @nav slots are not available.

This supports the AppNav component's fallback mechanism for
routes that don't have specific @nav parallel route definitions.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-29 08:54:01 -05:00
Thomas Hallock
423ba55350 fix: export missing hooks and types from @soroban/abacus-react package
- Export useAbacusConfig and useAbacusDisplay hooks from AbacusContext
- Export getDefaultAbacusConfig function and AbacusDisplayProvider component
- Export ColorScheme, BeadShape, ColorPalette, AbacusDisplayConfig, and AbacusDisplayContextType types

Resolves import errors in web components that were trying to import these
hooks but they weren't being exported from the package's main index file.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-29 08:54:01 -05:00
Thomas Hallock
885fc725dc feat: implement @nav parallel routes for game name display in mini navigation
- Add @nav/default.tsx for fallback nav content
- Add @nav/games/matching/page.tsx for Memory Pairs game nav
- Add @nav/games/memory-quiz/page.tsx for Memory Lightning game nav
- Update AppNav to use @nav slot content with header-based fallback
- Remove debug logging from navigation components

The @nav parallel routes pattern allows each game route to declare its own
navigation content server-side, keeping nav content colocated with routes
while avoiding client-side state management or lazy loading.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-29 08:54:01 -05:00
9 changed files with 174 additions and 122 deletions

View File

@@ -0,0 +1,3 @@
export default function DefaultNav() {
return null // No navigation content for routes without specific @nav slots
}

View File

@@ -0,0 +1,14 @@
export default function MatchingNav() {
return (
<h1 style={{
fontSize: '18px',
fontWeight: 'bold',
background: 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)',
backgroundClip: 'text',
color: 'transparent',
margin: 0
}}>
🧩 Memory Pairs
</h1>
)
}

View File

@@ -0,0 +1,14 @@
export default function MemoryQuizNav() {
return (
<h1 style={{
fontSize: '18px',
fontWeight: 'bold',
background: 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)',
backgroundClip: 'text',
color: 'transparent',
margin: 0
}}>
🧠 Memory Lightning
</h1>
)
}

View File

@@ -1,43 +1,34 @@
import type { Metadata } from 'next'
import type { Metadata, Viewport } from 'next'
import './globals.css'
import { AbacusDisplayProvider } from '@/contexts/AbacusDisplayContext'
import { UserProfileProvider } from '@/contexts/UserProfileContext'
import { GameModeProvider } from '@/contexts/GameModeContext'
import { FullscreenProvider } from '@/contexts/FullscreenContext'
import { GameThemeProvider } from '@/contexts/GameThemeContext'
import { AppNavBar } from '@/components/AppNavBar'
import { ClientProviders } from '@/components/ClientProviders'
import { AppNav } from '@/components/AppNav'
export const metadata: Metadata = {
title: 'Soroban Flashcard Generator',
description: 'Create beautiful, educational soroban flashcards with authentic Japanese abacus representations',
viewport: {
width: 'device-width',
initialScale: 1,
maximumScale: 1,
userScalable: false,
},
}
export const viewport: Viewport = {
width: 'device-width',
initialScale: 1,
maximumScale: 1,
userScalable: false,
}
export default function RootLayout({
children,
nav,
}: {
children: React.ReactNode
nav: React.ReactNode
}) {
return (
<html lang="en">
<body>
<AbacusDisplayProvider>
<UserProfileProvider>
<GameModeProvider>
<FullscreenProvider>
<GameThemeProvider>
<AppNavBar />
{children}
</GameThemeProvider>
</FullscreenProvider>
</GameModeProvider>
</UserProfileProvider>
</AbacusDisplayProvider>
<ClientProviders>
<AppNav>{nav}</AppNav>
{children}
</ClientProviders>
</body>
</html>
)

View File

@@ -0,0 +1,52 @@
import React from 'react'
import { headers } from 'next/headers'
import { AppNavBar } from './AppNavBar'
interface AppNavProps {
children: React.ReactNode
}
function getNavContentForPath(pathname: string): React.ReactNode {
// Route-based nav content - no lazy loading needed
if (pathname === '/games/matching' || pathname.startsWith('/arcade') && pathname.includes('matching')) {
return (
<h1 style={{
fontSize: '18px',
fontWeight: 'bold',
background: 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)',
backgroundClip: 'text',
color: 'transparent',
margin: 0
}}>
🧩 Memory Pairs
</h1>
)
}
if (pathname === '/games/memory-quiz' || pathname.startsWith('/arcade') && pathname.includes('memory-quiz')) {
return (
<h1 style={{
fontSize: '18px',
fontWeight: 'bold',
background: 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)',
backgroundClip: 'text',
color: 'transparent',
margin: 0
}}>
🧠 Memory Lightning
</h1>
)
}
return null
}
export function AppNav({ children }: AppNavProps) {
const headersList = headers()
const pathname = headersList.get('x-pathname') || ''
// Use @nav slot content if available, otherwise fall back to route-based detection
const navContent = children || getNavContentForPath(pathname)
return <AppNavBar navSlot={navContent} />
}

View File

@@ -1,91 +1,26 @@
'use client'
import React from 'react'
import Link from 'next/link'
import { usePathname, useRouter } from 'next/navigation'
import { css } from '../../styled-system/css'
import { container, hstack } from '../../styled-system/patterns'
import { AbacusDisplayDropdown } from './AbacusDisplayDropdown'
import { useFullscreen } from '../contexts/FullscreenContext'
import { useGameTheme } from '../contexts/GameThemeContext'
interface AppNavBarProps {
variant?: 'full' | 'minimal'
navSlot?: React.ReactNode
}
export function AppNavBar({ variant = 'full' }: AppNavBarProps) {
export function AppNavBar({ variant = 'full', navSlot }: AppNavBarProps) {
const pathname = usePathname()
const router = useRouter()
const isGamePage = pathname?.startsWith('/games')
const isArcadePage = pathname?.startsWith('/arcade')
const { isFullscreen, toggleFullscreen, exitFullscreen } = useFullscreen()
const { theme: gameTheme, isHydrated } = useGameTheme()
// Route-based theme detection as fallback for page reloads
const getRouteBasedTheme = () => {
if (pathname === '/games/memory-quiz') {
return {
gameName: "Memory Lightning",
backgroundColor: "linear-gradient(to bottom right, #f0fdf4, #eff6ff)"
}
}
if (pathname === '/games/matching') {
return {
gameName: "Memory Pairs",
backgroundColor: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)"
}
}
return null
}
// Use context theme if available, otherwise fall back to route-based detection
const currentTheme = gameTheme || getRouteBasedTheme()
// Helper function to get themed background colors
const getThemedBackground = (opacity: number = 0.85) => {
// Apply theming for route-based themes immediately, or after hydration for context themes
if (currentTheme?.backgroundColor && (getRouteBasedTheme() || isHydrated)) {
const color = currentTheme.backgroundColor
if (color.startsWith('#')) {
// Convert hex to rgba
const hex = color.slice(1)
const r = parseInt(hex.slice(0, 2), 16)
const g = parseInt(hex.slice(2, 4), 16)
const b = parseInt(hex.slice(4, 6), 16)
return `rgba(${r}, ${g}, ${b}, ${opacity})`
} else if (color.startsWith('rgb')) {
// Handle rgb/rgba formats
const match = color.match(/rgba?\(([^)]+)\)/)
if (match) {
const values = match[1].split(',').map(v => v.trim())
if (values.length >= 3) {
return `rgba(${values[0]}, ${values[1]}, ${values[2]}, ${opacity})`
}
}
} else if (color.startsWith('linear-gradient')) {
// Extract colors from gradient and use dominant color
const hexMatch = color.match(/#[0-9a-fA-F]{6}/g)
if (hexMatch && hexMatch.length > 0) {
// Use the first color from the gradient
const hex = hexMatch[0].slice(1)
const r = parseInt(hex.slice(0, 2), 16)
const g = parseInt(hex.slice(2, 4), 16)
const b = parseInt(hex.slice(4, 6), 16)
return `rgba(${r}, ${g}, ${b}, ${opacity})`
}
// Fallback: try to extract rgb values
const rgbMatch = color.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/g)
if (rgbMatch && rgbMatch.length > 0) {
const values = rgbMatch[0].match(/\d+/g)
if (values && values.length >= 3) {
return `rgba(${values[0]}, ${values[1]}, ${values[2]}, ${opacity})`
}
}
// Final fallback for gradients
return isFullscreen ? `rgba(0, 0, 0, ${opacity})` : `rgba(255, 255, 255, ${opacity})`
}
}
return isFullscreen ? 'rgba(0, 0, 0, 0.85)' : 'white'
}
// Auto-detect variant based on context
const actualVariant = variant === 'full' && (isGamePage || isArcadePage) ? 'minimal' : variant
@@ -103,42 +38,36 @@ export function AppNavBar({ variant = 'full' }: AppNavBarProps) {
transition: 'all 0.3s ease'
})}>
<div className={hstack({ gap: '2' })}>
{/* Game branding (fullscreen only) */}
{isFullscreen && (isArcadePage || isGamePage) && (
{/* Game branding from slot */}
{navSlot && (
<div
style={{
display: 'flex',
alignItems: 'center',
gap: '12px',
padding: '8px 16px',
background: currentTheme ? getThemedBackground(0.85) : 'rgba(0, 0, 0, 0.85)',
border: '1px solid rgba(255, 255, 255, 0.1)',
background: isFullscreen ? 'rgba(0, 0, 0, 0.85)' : 'white',
border: isFullscreen ? '1px solid rgba(255, 255, 255, 0.1)' : '1px solid #e5e7eb',
borderRadius: '8px',
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
backdropFilter: 'blur(15px)'
backdropFilter: isFullscreen ? 'blur(15px)' : 'none'
}}
>
<h1 className={css({
fontSize: 'lg',
fontWeight: 'bold',
background: gameTheme ? 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)' : 'linear-gradient(135deg, #60a5fa, #a78bfa, #f472b6)',
backgroundClip: 'text',
color: 'transparent'
})}>
🕹 {currentTheme?.gameName || (isArcadePage ? 'Arcade' : 'Game')}
</h1>
<div className={css({
px: '2',
py: '1',
background: 'rgba(34, 197, 94, 0.2)',
border: '1px solid rgba(34, 197, 94, 0.3)',
rounded: 'full',
fontSize: 'xs',
color: 'green.300',
fontWeight: 'semibold'
})}>
FULLSCREEN
</div>
{navSlot}
{isFullscreen && (
<div className={css({
px: '2',
py: '1',
background: 'rgba(34, 197, 94, 0.2)',
border: '1px solid rgba(34, 197, 94, 0.3)',
rounded: 'full',
fontSize: 'xs',
color: 'green.300',
fontWeight: 'semibold'
})}>
FULLSCREEN
</div>
)}
</div>
)}
@@ -149,7 +78,7 @@ export function AppNavBar({ variant = 'full' }: AppNavBarProps) {
alignItems: 'center',
gap: '8px',
padding: '8px 12px',
background: currentTheme ? getThemedBackground(isFullscreen ? 0.85 : 1) : (isFullscreen ? 'rgba(0, 0, 0, 0.85)' : 'white'),
background: isFullscreen ? 'rgba(0, 0, 0, 0.85)' : 'white',
border: isFullscreen ? '1px solid rgba(255, 255, 255, 0.1)' : '1px solid #e5e7eb',
borderRadius: '8px',
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',
@@ -198,7 +127,7 @@ export function AppNavBar({ variant = 'full' }: AppNavBarProps) {
alignItems: 'center',
gap: '8px',
padding: '8px 12px',
background: currentTheme ? getThemedBackground(isFullscreen ? 0.85 : 1) : (isFullscreen ? 'rgba(0, 0, 0, 0.85)' : 'white'),
background: isFullscreen ? 'rgba(0, 0, 0, 0.85)' : 'white',
border: isFullscreen ? '1px solid rgba(255, 255, 255, 0.1)' : '1px solid #e5e7eb',
borderRadius: '8px',
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.1)',

View File

@@ -0,0 +1,21 @@
import { NextRequest, NextResponse } from 'next/server'
export function middleware(request: NextRequest) {
// Add pathname to headers so Server Components can access it
const response = NextResponse.next()
response.headers.set('x-pathname', request.nextUrl.pathname)
return response
}
export const config = {
matcher: [
/*
* Match all request paths except for the ones starting with:
* - api (API routes)
* - _next/static (static files)
* - _next/image (image optimization files)
* - favicon.ico (favicon file)
*/
'/((?!api|_next/static|_next/image|favicon.ico).*)',
],
}

View File

@@ -1,3 +1,17 @@
# [1.4.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v1.3.0...abacus-react-v1.4.0) (2025-09-29)
### Bug Fixes
* export missing hooks and types from @soroban/abacus-react package ([423ba55](https://github.com/antialias/soroban-abacus-flashcards/commit/423ba5535023928f1e0198b2bd01c3c6cf7ee848))
* migrate viewport from metadata to separate viewport export ([1fe12c4](https://github.com/antialias/soroban-abacus-flashcards/commit/1fe12c4837b1229d0f0ab93c55d0ffb504eb8721))
### Features
* add middleware for pathname header support in [@nav](https://github.com/nav) fallback ([b7e7c4b](https://github.com/antialias/soroban-abacus-flashcards/commit/b7e7c4beff1e37e90e9e20a890c5af7a134a7fca))
* implement [@nav](https://github.com/nav) parallel routes for game name display in mini navigation ([885fc72](https://github.com/antialias/soroban-abacus-flashcards/commit/885fc725dc0bb41bbb5e500c2c907c6182192854))
# [1.3.0](https://github.com/antialias/soroban-abacus-flashcards/compare/abacus-react-v1.2.0...abacus-react-v1.3.0) (2025-09-29)

View File

@@ -3,4 +3,18 @@ export type {
AbacusConfig,
BeadConfig,
AbacusDimensions
} from './AbacusReact';
} from './AbacusReact';
export {
useAbacusConfig,
useAbacusDisplay,
getDefaultAbacusConfig,
AbacusDisplayProvider
} from './AbacusContext';
export type {
ColorScheme,
BeadShape,
ColorPalette,
AbacusDisplayConfig,
AbacusDisplayContextType
} from './AbacusContext';