Compare commits

..

10 Commits

Author SHA1 Message Date
semantic-release-bot
d84bf9c845 chore(release): 2.0.7 [skip ci]
## [2.0.7](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.0.6...v2.0.7) (2025-10-07)

### Bug Fixes

* preserve workspace node_modules in Docker for hoisted mode ([4f8aaf0](4f8aaf04aa))
2025-10-07 15:15:43 +00:00
Thomas Hallock
4f8aaf04aa fix: preserve workspace node_modules in Docker for hoisted mode
With hoisted mode, each workspace needs its own node_modules folder
(containing symlinks). Only ignore root /node_modules.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 10:14:47 -05:00
semantic-release-bot
a43c8654e1 chore(release): 2.0.6 [skip ci]
## [2.0.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.0.5...v2.0.6) (2025-10-07)

### Bug Fixes

* ignore nested node_modules in Docker ([f554592](f554592272))
2025-10-07 15:11:09 +00:00
Thomas Hallock
f554592272 fix: ignore nested node_modules in Docker
Add **/node_modules pattern to prevent Docker overlay conflicts
when hoisted mode creates nested symlink structures.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 10:10:23 -05:00
semantic-release-bot
b073b9e1ec chore(release): 2.0.5 [skip ci]
## [2.0.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.0.4...v2.0.5) (2025-10-07)

### Bug Fixes

* use .npmrc in Docker for hoisted mode consistency ([2df8cdc](2df8cdc88e))
2025-10-07 15:06:45 +00:00
Thomas Hallock
2df8cdc88e fix: use .npmrc in Docker for hoisted mode consistency
The pnpm lockfile was generated with hoisted mode, so Docker must
also use hoisted mode to match the module resolution paths.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 10:05:56 -05:00
semantic-release-bot
e73afdb913 chore(release): 2.0.4 [skip ci]
## [2.0.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.0.3...v2.0.4) (2025-10-07)

### Bug Fixes

* remove .npmrc in Docker to avoid hoisted mode issues ([2a77d75](2a77d755b7))
2025-10-07 15:02:36 +00:00
Thomas Hallock
2a77d755b7 fix: remove .npmrc in Docker to avoid hoisted mode issues
Docker builds should use default pnpm isolated mode, not hoisted mode
which causes tsup module resolution failures.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 10:01:39 -05:00
semantic-release-bot
f4ab0ff9ba chore(release): 2.0.3 [skip ci]
## [2.0.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.0.2...v2.0.3) (2025-10-07)

### Bug Fixes

* remove duplicate PlayerStatusBar story file from arcade ([4e721f7](4e721f765a))
2025-10-07 14:58:20 +00:00
Thomas Hallock
4e721f765a fix: remove duplicate PlayerStatusBar story file from arcade
Remove apps/web/src/app/arcade/matching/components/PlayerStatusBar.stories.tsx
to fix Storybook build error about duplicate story IDs.

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-07 09:57:17 -05:00
5 changed files with 41 additions and 459 deletions

View File

@@ -1,5 +1,7 @@
# Ignore development files
node_modules
# NOTE: With hoisted mode, we need the workspace node_modules symlinks
# Only ignore root node_modules
/node_modules
.next
.git
.github

View File

@@ -1,3 +1,38 @@
## [2.0.7](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.0.6...v2.0.7) (2025-10-07)
### Bug Fixes
* preserve workspace node_modules in Docker for hoisted mode ([4f8aaf0](https://github.com/antialias/soroban-abacus-flashcards/commit/4f8aaf04aadda11ce9ec470dec44f78062929e77))
## [2.0.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.0.5...v2.0.6) (2025-10-07)
### Bug Fixes
* ignore nested node_modules in Docker ([f554592](https://github.com/antialias/soroban-abacus-flashcards/commit/f554592272c2e92d7f1ec6550211518de9c3242f))
## [2.0.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.0.4...v2.0.5) (2025-10-07)
### Bug Fixes
* use .npmrc in Docker for hoisted mode consistency ([2df8cdc](https://github.com/antialias/soroban-abacus-flashcards/commit/2df8cdc88ed03b6b04642a3441e17c6fda11d2a5))
## [2.0.4](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.0.3...v2.0.4) (2025-10-07)
### Bug Fixes
* remove .npmrc in Docker to avoid hoisted mode issues ([2a77d75](https://github.com/antialias/soroban-abacus-flashcards/commit/2a77d755b7820b5b6b52ea99db418e6d071d726e))
## [2.0.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.0.2...v2.0.3) (2025-10-07)
### Bug Fixes
* remove duplicate PlayerStatusBar story file from arcade ([4e721f7](https://github.com/antialias/soroban-abacus-flashcards/commit/4e721f765a29fe8628d4e34ef94cdf5728eea3dc))
## [2.0.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v2.0.1...v2.0.2) (2025-10-07)

View File

@@ -10,14 +10,14 @@ RUN npm install -g pnpm@9.15.4 turbo@1.10.0
WORKDIR /app
# Copy package files for dependency resolution
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json ./
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml turbo.json .npmrc ./
COPY apps/web/package.json ./apps/web/
COPY packages/core/client/node/package.json ./packages/core/client/node/
COPY packages/core/client/typescript/package.json ./packages/core/client/typescript/
COPY packages/abacus-react/package.json ./packages/abacus-react/
COPY packages/templates/package.json ./packages/templates/
# Install dependencies
# Install dependencies (will use .npmrc with hoisted mode)
RUN pnpm install --frozen-lockfile
# Builder stage

View File

@@ -1,455 +0,0 @@
import type { Meta, StoryObj } from '@storybook/react'
import React, { useEffect } from 'react'
import { css } from '../../../../../styled-system/css'
import { gamePlurals } from '../../../../utils/pluralization'
// Inject the celebration animations for Storybook
const celebrationAnimations = `
@keyframes gentle-pulse {
0%, 100% {
box-shadow: 0 0 0 2px white, 0 0 0 6px rgba(102, 126, 234, 0.3), 0 12px 32px rgba(0,0,0,0.1);
}
50% {
box-shadow: 0 0 0 2px white, 0 0 0 6px rgba(102, 126, 234, 0.5), 0 12px 32px rgba(0,0,0,0.2);
}
}
@keyframes gentle-bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-3px);
}
}
@keyframes gentle-sway {
0%, 100% { transform: rotate(-2deg) scale(1); }
50% { transform: rotate(2deg) scale(1.05); }
}
@keyframes breathe {
0%, 100% { transform: scale(1); }
50% { transform: scale(1.03); }
}
@keyframes float {
0%, 100% { transform: translateY(0px); }
50% { transform: translateY(-6px); }
}
@keyframes turn-entrance {
0% {
transform: scale(0.8) rotate(-10deg);
opacity: 0.6;
}
50% {
transform: scale(1.1) rotate(5deg);
opacity: 1;
}
100% {
transform: scale(1.08) rotate(0deg);
opacity: 1;
}
}
@keyframes streak-pulse {
0%, 100% {
opacity: 0.9;
transform: scale(1);
}
50% {
opacity: 1;
transform: scale(1.05);
}
}
@keyframes great-celebration {
0% {
transform: scale(1.08) translateY(-4px);
box-shadow: 0 0 0 2px white, 0 0 0 6px #22c55e40, 0 12px 32px rgba(0,0,0,0.2);
}
50% {
transform: scale(1.12) translateY(-6px);
box-shadow: 0 0 0 2px white, 0 0 0 8px #22c55e60, 0 15px 35px rgba(34,197,94,0.3);
}
100% {
transform: scale(1.08) translateY(-4px);
box-shadow: 0 0 0 2px white, 0 0 0 6px #22c55e40, 0 12px 32px rgba(0,0,0,0.2);
}
}
@keyframes epic-celebration {
0% {
transform: scale(1.08) translateY(-4px);
box-shadow: 0 0 0 2px white, 0 0 0 6px #f97316, 0 12px 32px rgba(0,0,0,0.2);
}
25% {
transform: scale(1.15) translateY(-8px) rotate(2deg);
box-shadow: 0 0 0 3px white, 0 0 0 10px #f97316, 0 18px 40px rgba(249,115,22,0.4);
}
75% {
transform: scale(1.15) translateY(-8px) rotate(-2deg);
box-shadow: 0 0 0 3px white, 0 0 0 10px #f97316, 0 18px 40px rgba(249,115,22,0.4);
}
100% {
transform: scale(1.08) translateY(-4px);
box-shadow: 0 0 0 2px white, 0 0 0 6px #f97316, 0 12px 32px rgba(0,0,0,0.2);
}
}
@keyframes legendary-celebration {
0% {
transform: scale(1.08) translateY(-4px);
box-shadow: 0 0 0 2px white, 0 0 0 6px #a855f7, 0 12px 32px rgba(0,0,0,0.2);
}
20% {
transform: scale(1.2) translateY(-12px) rotate(5deg);
box-shadow: 0 0 0 4px gold, 0 0 0 12px #a855f7, 0 25px 50px rgba(168,85,247,0.5);
}
40% {
transform: scale(1.18) translateY(-10px) rotate(-3deg);
box-shadow: 0 0 0 3px gold, 0 0 0 10px #a855f7, 0 20px 45px rgba(168,85,247,0.4);
}
60% {
transform: scale(1.22) translateY(-14px) rotate(3deg);
box-shadow: 0 0 0 4px gold, 0 0 0 12px #a855f7, 0 25px 50px rgba(168,85,247,0.5);
}
80% {
transform: scale(1.15) translateY(-8px) rotate(-1deg);
box-shadow: 0 0 0 3px gold, 0 0 0 8px #a855f7, 0 18px 40px rgba(168,85,247,0.3);
}
100% {
transform: scale(1.08) translateY(-4px);
box-shadow: 0 0 0 2px white, 0 0 0 6px #a855f7, 0 12px 32px rgba(0,0,0,0.2);
}
}
`
// Component to inject animations
const AnimationProvider = ({ children }: { children: React.ReactNode }) => {
useEffect(() => {
if (typeof document !== 'undefined' && !document.getElementById('celebration-animations')) {
const style = document.createElement('style')
style.id = 'celebration-animations'
style.textContent = celebrationAnimations
document.head.appendChild(style)
}
}, [])
return <>{children}</>
}
const meta: Meta = {
title: 'Games/Matching/PlayerStatusBar',
parameters: {
layout: 'centered',
docs: {
description: {
component: `
The PlayerStatusBar component displays the current state of players in the matching game.
It shows different layouts for single player vs multiplayer modes and includes escalating
celebration effects for consecutive matching pairs.
## Features
- Single player mode with epic styling
- Multiplayer mode with competitive grid layout
- Escalating celebration animations based on consecutive matches:
- 2+ matches: Great celebration (green)
- 3+ matches: Epic celebration (orange)
- 5+ matches: Legendary celebration (purple with gold accents)
- Real-time turn indicators
- Score tracking and progress display
- Responsive design for mobile and desktop
## Animation Preview
The animations demonstrate different celebration levels that activate when players get consecutive matches.
`
}
}
},
decorators: [
(Story) => (
<AnimationProvider>
<div className={css({
width: '800px',
maxWidth: '90vw',
padding: '20px',
background: 'linear-gradient(135deg, #f8fafc, #e2e8f0)',
minHeight: '400px'
})}>
<Story />
</div>
</AnimationProvider>
)
]
}
export default meta
type Story = StoryObj<typeof meta>
// Create a mock player card component that showcases the animations
const MockPlayerCard = ({
emoji,
name,
score,
consecutiveMatches,
isCurrentPlayer = true,
celebrationLevel
}: {
emoji: string
name: string
score: number
consecutiveMatches: number
isCurrentPlayer?: boolean
celebrationLevel: 'normal' | 'great' | 'epic' | 'legendary'
}) => {
const playerColor = celebrationLevel === 'legendary' ? '#a855f7' :
celebrationLevel === 'epic' ? '#f97316' :
celebrationLevel === 'great' ? '#22c55e' : '#3b82f6'
return (
<div className={css({
display: 'flex',
alignItems: 'center',
gap: { base: '3', md: '4' },
p: isCurrentPlayer ? { base: '4', md: '6' } : { base: '2', md: '3' },
rounded: isCurrentPlayer ? '2xl' : 'lg',
background: isCurrentPlayer
? `linear-gradient(135deg, ${playerColor}15, ${playerColor}25, ${playerColor}15)`
: 'white',
border: isCurrentPlayer ? '4px solid' : '2px solid',
borderColor: isCurrentPlayer ? playerColor : 'gray.200',
boxShadow: isCurrentPlayer
? `0 0 0 2px white, 0 0 0 6px ${playerColor}40, 0 12px 32px rgba(0,0,0,0.2)`
: '0 2px 4px rgba(0,0,0,0.1)',
transition: 'all 0.6s cubic-bezier(0.175, 0.885, 0.32, 1.275)',
position: 'relative',
transform: isCurrentPlayer ? 'scale(1.08) translateY(-4px)' : 'scale(1)',
zIndex: isCurrentPlayer ? 10 : 1,
animation: isCurrentPlayer
? (celebrationLevel === 'legendary' ? 'legendary-celebration 0.8s ease-out, turn-entrance 0.6s ease-out'
: celebrationLevel === 'epic' ? 'epic-celebration 0.7s ease-out, turn-entrance 0.6s ease-out'
: celebrationLevel === 'great' ? 'great-celebration 0.6s ease-out, turn-entrance 0.6s ease-out'
: 'turn-entrance 0.6s ease-out')
: 'none'
})}>
{/* Player emoji */}
<div className={css({
fontSize: isCurrentPlayer ? { base: '3xl', md: '5xl' } : { base: 'lg', md: 'xl' },
flexShrink: 0,
animation: isCurrentPlayer
? 'float 3s ease-in-out infinite'
: 'breathe 5s ease-in-out infinite',
transform: isCurrentPlayer ? 'scale(1.2)' : 'scale(1)',
transition: 'all 0.6s cubic-bezier(0.4, 0, 0.2, 1)',
textShadow: isCurrentPlayer ? '0 0 20px currentColor' : 'none'
})}>
{emoji}
</div>
{/* Player info */}
<div className={css({
flex: 1,
minWidth: 0
})}>
<div className={css({
fontSize: isCurrentPlayer ? { base: 'md', md: 'lg' } : { base: 'xs', md: 'sm' },
fontWeight: 'black',
color: isCurrentPlayer ? 'gray.900' : 'gray.700',
textShadow: isCurrentPlayer ? '0 0 10px currentColor' : 'none'
})}>
{name}
</div>
<div className={css({
fontSize: isCurrentPlayer ? { base: 'sm', md: 'md' } : { base: '2xs', md: 'xs' },
color: isCurrentPlayer ? playerColor : 'gray.500',
fontWeight: isCurrentPlayer ? 'black' : 'semibold'
})}>
{gamePlurals.pair(score)}
{isCurrentPlayer && (
<span className={css({
color: 'red.600',
fontWeight: 'black',
fontSize: isCurrentPlayer ? { base: 'sm', md: 'lg' } : 'inherit',
textShadow: '0 0 15px currentColor'
})}>
{' • Your turn'}
</span>
)}
{consecutiveMatches > 1 && (
<div className={css({
fontSize: { base: '2xs', md: 'xs' },
color: celebrationLevel === 'legendary' ? 'purple.600' :
celebrationLevel === 'epic' ? 'orange.600' :
celebrationLevel === 'great' ? 'green.600' : 'gray.500',
fontWeight: 'black',
animation: isCurrentPlayer ? 'streak-pulse 1s ease-in-out infinite' : 'none',
textShadow: isCurrentPlayer ? '0 0 10px currentColor' : 'none'
})}>
🔥 {consecutiveMatches} streak!
</div>
)}
</div>
</div>
{/* Epic score display */}
{isCurrentPlayer && (
<div className={css({
background: 'linear-gradient(135deg, #ff6b6b, #ee5a24)',
color: 'white',
px: { base: '3', md: '4' },
py: { base: '2', md: '3' },
rounded: 'xl',
fontSize: { base: 'lg', md: 'xl' },
fontWeight: 'black',
boxShadow: '0 4px 15px rgba(238, 90, 36, 0.4)',
animation: 'gentle-bounce 1.5s ease-in-out infinite',
textShadow: '0 0 10px rgba(255,255,255,0.8)'
})}>
{score}
</div>
)}
</div>
)
}
// Normal celebration level
export const NormalPlayer: Story = {
render: () => (
<MockPlayerCard
emoji="🚀"
name="Solo Champion"
score={3}
consecutiveMatches={0}
celebrationLevel="normal"
/>
)
}
// Great celebration level
export const GreatStreak: Story = {
render: () => (
<MockPlayerCard
emoji="🎯"
name="Streak Master"
score={5}
consecutiveMatches={2}
celebrationLevel="great"
/>
)
}
// Epic celebration level
export const EpicStreak: Story = {
render: () => (
<MockPlayerCard
emoji="🔥"
name="Epic Matcher"
score={7}
consecutiveMatches={4}
celebrationLevel="epic"
/>
)
}
// Legendary celebration level
export const LegendaryStreak: Story = {
render: () => (
<MockPlayerCard
emoji="👑"
name="Legend"
score={8}
consecutiveMatches={6}
celebrationLevel="legendary"
/>
)
}
// All levels showcase
export const AllCelebrationLevels: Story = {
render: () => (
<div className={css({ display: 'flex', flexDirection: 'column', gap: '20px' })}>
<h3 className={css({ textAlign: 'center', fontSize: '24px', fontWeight: 'bold', marginBottom: '20px' })}>
Consecutive Match Celebration Levels
</h3>
<div className={css({ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(380px, 1fr))', gap: '20px' })}>
{/* Normal */}
<div>
<h4 className={css({ textAlign: 'center', marginBottom: '10px', fontSize: '16px', fontWeight: 'bold' })}>
Normal (0-1 matches)
</h4>
<MockPlayerCard
emoji="🚀"
name="Solo Champion"
score={3}
consecutiveMatches={0}
celebrationLevel="normal"
/>
</div>
{/* Great */}
<div>
<h4 className={css({ textAlign: 'center', marginBottom: '10px', color: 'green.600', fontSize: '16px', fontWeight: 'bold' })}>
Great (2+ matches)
</h4>
<MockPlayerCard
emoji="🎯"
name="Streak Master"
score={5}
consecutiveMatches={2}
celebrationLevel="great"
/>
</div>
{/* Epic */}
<div>
<h4 className={css({ textAlign: 'center', marginBottom: '10px', color: 'orange.600', fontSize: '16px', fontWeight: 'bold' })}>
Epic (3+ matches)
</h4>
<MockPlayerCard
emoji="🔥"
name="Epic Matcher"
score={7}
consecutiveMatches={4}
celebrationLevel="epic"
/>
</div>
{/* Legendary */}
<div>
<h4 className={css({ textAlign: 'center', marginBottom: '10px', color: 'purple.600', fontSize: '16px', fontWeight: 'bold' })}>
Legendary (5+ matches)
</h4>
<MockPlayerCard
emoji="👑"
name="Legend"
score={8}
consecutiveMatches={6}
celebrationLevel="legendary"
/>
</div>
</div>
<div className={css({
textAlign: 'center',
marginTop: '20px',
padding: '16px',
background: 'rgba(255,255,255,0.8)',
borderRadius: '12px',
border: '1px solid rgba(0,0,0,0.1)'
})}>
<p className={css({ fontSize: '14px', color: 'gray.700', margin: 0 })}>
These animations trigger when a player gets consecutive matching pairs in the memory matching game.
The celebrations get more intense as the streak grows, providing visual feedback and excitement!
</p>
</div>
</div>
),
parameters: {
layout: 'fullscreen'
}
}

View File

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