Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e52d907087 | ||
|
|
c0fa926d16 | ||
|
|
1fd0474cd5 | ||
|
|
bf37eb1928 | ||
|
|
9f56c9728c | ||
|
|
0f51366fd5 | ||
|
|
fe1e8979c8 | ||
|
|
92148a4cf8 | ||
|
|
7088a7096a | ||
|
|
5f0ad14133 | ||
|
|
73f8f637cd | ||
|
|
f32480a0f9 | ||
|
|
11aa44d882 | ||
|
|
30e16c8e5a | ||
|
|
86357b3d7a | ||
|
|
ad1ad690f0 | ||
|
|
53475cf40e | ||
|
|
424f41d4bf | ||
|
|
4c6939807e | ||
|
|
b8235be612 | ||
|
|
86dee31c9a | ||
|
|
b401bb5fa4 | ||
|
|
7666b0aea9 | ||
|
|
39afa455de | ||
|
|
a58f7b78b0 | ||
|
|
1c001e07b7 | ||
|
|
8893675b36 | ||
|
|
4254459238 | ||
|
|
89b90723b7 | ||
|
|
6e5aec858f | ||
|
|
5611d148aa | ||
|
|
d5f60ce9d2 | ||
|
|
284fc90a53 | ||
|
|
e54ea20dbe | ||
|
|
415a1fb1fa | ||
|
|
60b3a788b3 | ||
|
|
62ff067bb9 | ||
|
|
3d774c8d82 | ||
|
|
61403f2f50 | ||
|
|
aa297d4ef7 | ||
|
|
712d318e7c | ||
|
|
cd3eb61cb5 |
140
CHANGELOG.md
140
CHANGELOG.md
@@ -1,3 +1,143 @@
|
||||
## [4.63.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.63.0...v4.63.1) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **flashcards:** increase rotation sensitivity 10x for visible grab point physics ([c0fa926](https://github.com/antialias/soroban-abacus-flashcards/commit/c0fa926d16d02c1bfe880b7f0056a760e8461b3b))
|
||||
|
||||
## [4.63.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.62.1...v4.63.0) (2025-10-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **flashcards:** add grab point physics for realistic rotation ([bf37eb1](https://github.com/antialias/soroban-abacus-flashcards/commit/bf37eb1928de8d07673234e2faa1fa6268c45686))
|
||||
|
||||
## [4.62.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.62.0...v4.62.1) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **flashcards:** improve shadow speed logging with separate throttling ([0f51366](https://github.com/antialias/soroban-abacus-flashcards/commit/0f51366fd56540e691df4931b6350c03043484f1))
|
||||
|
||||
## [4.62.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.61.3...v4.62.0) (2025-10-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **flashcards:** add dynamic shadow based on drag speed ([92148a4](https://github.com/antialias/soroban-abacus-flashcards/commit/92148a4cf87e828ba2e5ec1740fb51d9667c1d73))
|
||||
|
||||
## [4.61.3](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.61.2...v4.61.3) (2025-10-21)
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **flashcards:** completely rewrite drag-drop with simple approach ([5f0ad14](https://github.com/antialias/soroban-abacus-flashcards/commit/5f0ad14133340d073e861f5721cb48e1abab03ff))
|
||||
|
||||
## [4.61.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.61.1...v4.61.2) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **flashcards:** use explicit per-property configs to fix decay physics ([f32480a](https://github.com/antialias/soroban-abacus-flashcards/commit/f32480a0f9153285341e5a28078840abc0590873))
|
||||
|
||||
## [4.61.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.61.0...v4.61.1) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **flashcards:** fix position snap-back by using api.set before decay ([30e16c8](https://github.com/antialias/soroban-abacus-flashcards/commit/30e16c8e5ac3bb25f2d54cf715dc6fb45adc4fcc))
|
||||
|
||||
## [4.61.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.60.0...v4.61.0) (2025-10-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **flashcards:** enable unbounded drag and position persistence ([ad1ad69](https://github.com/antialias/soroban-abacus-flashcards/commit/ad1ad690f014257b5a3c3f599e794205a11d286f))
|
||||
|
||||
## [4.60.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.59.1...v4.60.0) (2025-10-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **homepage:** significantly increase mobile hero abacus size ([424f41d](https://github.com/antialias/soroban-abacus-flashcards/commit/424f41d4bfc1ddea068f8c110b495ebd5c0bb455))
|
||||
|
||||
## [4.59.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.59.0...v4.59.1) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** adjust hero abacus scale for optimal sizing across devices ([86dee31](https://github.com/antialias/soroban-abacus-flashcards/commit/86dee31c9a51ca0712f1b4181a4899d25374d403))
|
||||
* **homepage:** reduce mobile abacus scale to prevent scroll hint overlap ([b8235be](https://github.com/antialias/soroban-abacus-flashcards/commit/b8235be612c3f1dbd0da2b6cd1a935001b7dac9b))
|
||||
|
||||
## [4.59.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.58.2...v4.59.0) (2025-10-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **homepage:** increase hero abacus size for better visibility ([7666b0a](https://github.com/antialias/soroban-abacus-flashcards/commit/7666b0aea949f2432a4d0f4648c1a366af3ea6d2))
|
||||
|
||||
## [4.58.2](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.58.1...v4.58.2) (2025-10-21)
|
||||
|
||||
|
||||
### Code Refactoring
|
||||
|
||||
* **navbar:** prevent subtitle wrap and remove abacus emoji ([a58f7b7](https://github.com/antialias/soroban-abacus-flashcards/commit/a58f7b78b0020c85da523c36fdf6d70ad069736a))
|
||||
|
||||
## [4.58.1](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.58.0...v4.58.1) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **navbar:** apply glassmorphism to transparent mode, not scrolled mode ([8893675](https://github.com/antialias/soroban-abacus-flashcards/commit/8893675b36b1c1534c6fe7e57fa7e0cc55f198d6))
|
||||
|
||||
## [4.58.0](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.57.10...v4.58.0) (2025-10-21)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **navbar:** add glassmorphism effect to nav links when scrolled ([89b9072](https://github.com/antialias/soroban-abacus-flashcards/commit/89b90723b7a3fc9ed12da3ba8718fccb6ce0760f))
|
||||
|
||||
## [4.57.10](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.57.9...v4.57.10) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **navbar:** remove border artifact and add 10px bottom fade ([d5f60ce](https://github.com/antialias/soroban-abacus-flashcards/commit/d5f60ce9d2fbc2a870b3bb96f5365a0e04e0afc4))
|
||||
|
||||
|
||||
### Styles
|
||||
|
||||
* **navbar:** improve theming to match homepage dark aesthetic ([284fc90](https://github.com/antialias/soroban-abacus-flashcards/commit/284fc90a53f5f4868a3e41421760ebc813be12b5))
|
||||
|
||||
## [4.57.9](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.57.8...v4.57.9) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** add overflow hidden to Your Journey section ([415a1fb](https://github.com/antialias/soroban-abacus-flashcards/commit/415a1fb1faa263c9d69b4e781ce22da235ca2b66))
|
||||
|
||||
## [4.57.8](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.57.7...v4.57.8) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** adjust responsive breakpoints to prevent skill card clipping ([62ff067](https://github.com/antialias/soroban-abacus-flashcards/commit/62ff067bb956b17a9b3569eadc2a32abd24c27b8))
|
||||
|
||||
## [4.57.7](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.57.6...v4.57.7) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** restructure layout to center 1400px wide demo section ([61403f2](https://github.com/antialias/soroban-abacus-flashcards/commit/61403f2f506557b57716a298d4dc481d7853552f))
|
||||
* **homepage:** set min-width 1400px on container and remove max-width ([aa297d4](https://github.com/antialias/soroban-abacus-flashcards/commit/aa297d4ef7559473a147934766bfa3868552f58d))
|
||||
|
||||
## [4.57.6](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.57.5...v4.57.6) (2025-10-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **homepage:** increase skill cards container width to prevent title wrapping ([cd3eb61](https://github.com/antialias/soroban-abacus-flashcards/commit/cd3eb61cb59e6faef37fbf609f37f7e2dc302e72))
|
||||
|
||||
## [4.57.5](https://github.com/antialias/soroban-abacus-flashcards/compare/v4.57.4...v4.57.5) (2025-10-21)
|
||||
|
||||
|
||||
|
||||
@@ -122,239 +122,239 @@ export default function HomePage() {
|
||||
{/* Hero Section with Large Interactive Abacus */}
|
||||
<HeroAbacus />
|
||||
|
||||
{/* Main content container */}
|
||||
<div className={container({ maxW: '7xl', px: '4', py: '12' })}>
|
||||
{/* Learn by Doing Section - with inline tutorial demo */}
|
||||
<section className={stack({ gap: '8', mb: '16' })}>
|
||||
<div className={css({ textAlign: 'center' })}>
|
||||
<h2
|
||||
className={css({
|
||||
fontSize: { base: '2xl', md: '3xl' },
|
||||
fontWeight: 'bold',
|
||||
color: 'white',
|
||||
mb: '2',
|
||||
})}
|
||||
>
|
||||
Learn by Doing
|
||||
</h2>
|
||||
<p className={css({ color: 'gray.400', fontSize: 'md', maxW: '2xl', mx: 'auto' })}>
|
||||
Interactive tutorials teach you step-by-step. Try this example right now:
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Live demo and learning objectives */}
|
||||
<div
|
||||
{/* Learn by Doing Section - with inline tutorial demo */}
|
||||
<section className={stack({ gap: '8', mb: '16', px: '4', py: '12' })}>
|
||||
<div className={css({ textAlign: 'center' })}>
|
||||
<h2
|
||||
className={css({
|
||||
bg: 'rgba(0, 0, 0, 0.4)',
|
||||
rounded: 'xl',
|
||||
p: '8',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.700',
|
||||
shadow: 'lg',
|
||||
maxW: '1200px',
|
||||
mx: 'auto',
|
||||
fontSize: { base: '2xl', md: '3xl' },
|
||||
fontWeight: 'bold',
|
||||
color: 'white',
|
||||
mb: '2',
|
||||
})}
|
||||
>
|
||||
Learn by Doing
|
||||
</h2>
|
||||
<p className={css({ color: 'gray.400', fontSize: 'md', maxW: '2xl', mx: 'auto' })}>
|
||||
Interactive tutorials teach you step-by-step. Try this example right now:
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Live demo and learning objectives */}
|
||||
<div
|
||||
className={css({
|
||||
bg: 'rgba(0, 0, 0, 0.4)',
|
||||
rounded: 'xl',
|
||||
p: '8',
|
||||
border: '1px solid',
|
||||
borderColor: 'gray.700',
|
||||
shadow: 'lg',
|
||||
minW: { base: '100%', xl: '1400px' },
|
||||
mx: 'auto',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: { base: 'column', xl: 'row' },
|
||||
gap: '8',
|
||||
alignItems: { base: 'center', xl: 'flex-start' },
|
||||
})}
|
||||
>
|
||||
{/* Tutorial on the left */}
|
||||
<div
|
||||
className={css({
|
||||
display: 'flex',
|
||||
flexDirection: { base: 'column', md: 'row' },
|
||||
gap: '8',
|
||||
alignItems: { base: 'center', md: 'flex-start' },
|
||||
flex: '1',
|
||||
minW: { base: '100%', xl: '500px' },
|
||||
maxW: { base: '100%', xl: '500px' },
|
||||
})}
|
||||
>
|
||||
{/* Tutorial on the left */}
|
||||
<div
|
||||
className={css({
|
||||
flex: '1',
|
||||
minW: { base: '100%', md: '500px' },
|
||||
maxW: { base: '100%', md: '500px' },
|
||||
})}
|
||||
>
|
||||
<TutorialPlayer
|
||||
key={selectedTutorial.id}
|
||||
tutorial={selectedTutorial}
|
||||
isDebugMode={false}
|
||||
showDebugPanel={false}
|
||||
hideNavigation={true}
|
||||
hideTooltip={true}
|
||||
silentErrors={true}
|
||||
abacusColumns={1}
|
||||
theme="dark"
|
||||
/>
|
||||
</div>
|
||||
<TutorialPlayer
|
||||
key={selectedTutorial.id}
|
||||
tutorial={selectedTutorial}
|
||||
isDebugMode={false}
|
||||
showDebugPanel={false}
|
||||
hideNavigation={true}
|
||||
hideTooltip={true}
|
||||
silentErrors={true}
|
||||
abacusColumns={1}
|
||||
theme="dark"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* What you'll learn on the right */}
|
||||
<div
|
||||
{/* What you'll learn on the right */}
|
||||
<div
|
||||
className={css({
|
||||
flex: '0 0 auto',
|
||||
w: { base: '100%', lg: '800px' },
|
||||
})}
|
||||
>
|
||||
<h3
|
||||
className={css({
|
||||
flex: '0 0 auto',
|
||||
w: { base: '100%', lg: '650px' },
|
||||
fontSize: '2xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'white',
|
||||
mb: '6',
|
||||
})}
|
||||
>
|
||||
<h3
|
||||
className={css({
|
||||
fontSize: '2xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'white',
|
||||
mb: '6',
|
||||
})}
|
||||
>
|
||||
What You'll Learn
|
||||
</h3>
|
||||
<div className={grid({ columns: { base: 1, lg: 2 }, gap: '5' })}>
|
||||
{[
|
||||
{
|
||||
title: '📖 Read and set numbers',
|
||||
desc: 'Master abacus number representation from zero to thousands',
|
||||
example: '0-9999',
|
||||
badge: 'Foundation',
|
||||
values: [0, 1, 2, 3, 4, 5, 10, 50, 100, 500, 999],
|
||||
columns: 3,
|
||||
},
|
||||
{
|
||||
title: '🤝 Friends techniques',
|
||||
desc: 'Add and subtract using complement pairs and mental shortcuts',
|
||||
example: '5 = 2+3',
|
||||
badge: 'Core',
|
||||
values: [2, 5, 3],
|
||||
columns: 1,
|
||||
},
|
||||
{
|
||||
title: '✖️ Multiply & divide',
|
||||
desc: 'Fluent multi-digit calculations with advanced techniques',
|
||||
example: '12×34',
|
||||
badge: 'Advanced',
|
||||
values: [12, 24, 36, 48],
|
||||
columns: 2,
|
||||
},
|
||||
{
|
||||
title: '🧠 Mental calculation',
|
||||
desc: 'Visualize and compute without the physical tool (Anzan)',
|
||||
example: 'Speed math',
|
||||
badge: 'Expert',
|
||||
values: [7, 14, 21, 28, 35],
|
||||
columns: 2,
|
||||
},
|
||||
].map((skill, i) => {
|
||||
const isSelected = i === selectedSkillIndex
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
onClick={() => setSelectedSkillIndex(i)}
|
||||
className={css({
|
||||
What You'll Learn
|
||||
</h3>
|
||||
<div className={grid({ columns: { base: 1, lg: 2 }, gap: '5' })}>
|
||||
{[
|
||||
{
|
||||
title: '📖 Read and set numbers',
|
||||
desc: 'Master abacus number representation from zero to thousands',
|
||||
example: '0-9999',
|
||||
badge: 'Foundation',
|
||||
values: [0, 1, 2, 3, 4, 5, 10, 50, 100, 500, 999],
|
||||
columns: 3,
|
||||
},
|
||||
{
|
||||
title: '🤝 Friends techniques',
|
||||
desc: 'Add and subtract using complement pairs and mental shortcuts',
|
||||
example: '5 = 2+3',
|
||||
badge: 'Core',
|
||||
values: [2, 5, 3],
|
||||
columns: 1,
|
||||
},
|
||||
{
|
||||
title: '✖️ Multiply & divide',
|
||||
desc: 'Fluent multi-digit calculations with advanced techniques',
|
||||
example: '12×34',
|
||||
badge: 'Advanced',
|
||||
values: [12, 24, 36, 48],
|
||||
columns: 2,
|
||||
},
|
||||
{
|
||||
title: '🧠 Mental calculation',
|
||||
desc: 'Visualize and compute without the physical tool (Anzan)',
|
||||
example: 'Speed math',
|
||||
badge: 'Expert',
|
||||
values: [7, 14, 21, 28, 35],
|
||||
columns: 2,
|
||||
},
|
||||
].map((skill, i) => {
|
||||
const isSelected = i === selectedSkillIndex
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
onClick={() => setSelectedSkillIndex(i)}
|
||||
className={css({
|
||||
bg: isSelected
|
||||
? 'linear-gradient(135deg, rgba(250, 204, 21, 0.15), rgba(250, 204, 21, 0.08))'
|
||||
: 'linear-gradient(135deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.03))',
|
||||
borderRadius: 'xl',
|
||||
p: { base: '4', lg: '5' },
|
||||
border: '1px solid',
|
||||
borderColor: isSelected
|
||||
? 'rgba(250, 204, 21, 0.4)'
|
||||
: 'rgba(255, 255, 255, 0.15)',
|
||||
boxShadow: isSelected
|
||||
? '0 6px 16px rgba(250, 204, 21, 0.2)'
|
||||
: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
||||
transition: 'all 0.2s',
|
||||
cursor: 'pointer',
|
||||
_hover: {
|
||||
bg: isSelected
|
||||
? 'linear-gradient(135deg, rgba(250, 204, 21, 0.15), rgba(250, 204, 21, 0.08))'
|
||||
: 'linear-gradient(135deg, rgba(255, 255, 255, 0.06), rgba(255, 255, 255, 0.03))',
|
||||
borderRadius: 'xl',
|
||||
p: { base: '4', lg: '5' },
|
||||
border: '1px solid',
|
||||
? 'linear-gradient(135deg, rgba(250, 204, 21, 0.2), rgba(250, 204, 21, 0.12))'
|
||||
: 'linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05))',
|
||||
borderColor: isSelected
|
||||
? 'rgba(250, 204, 21, 0.4)'
|
||||
: 'rgba(255, 255, 255, 0.15)',
|
||||
? 'rgba(250, 204, 21, 0.5)'
|
||||
: 'rgba(255, 255, 255, 0.25)',
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: isSelected
|
||||
? '0 6px 16px rgba(250, 204, 21, 0.2)'
|
||||
: '0 4px 12px rgba(0, 0, 0, 0.3)',
|
||||
transition: 'all 0.2s',
|
||||
cursor: 'pointer',
|
||||
_hover: {
|
||||
? '0 8px 20px rgba(250, 204, 21, 0.3)'
|
||||
: '0 6px 16px rgba(0, 0, 0, 0.4)',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div className={hstack({ gap: '3', alignItems: 'flex-start' })}>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: '3xl',
|
||||
width: { base: '120px', lg: '150px' },
|
||||
minHeight: { base: '115px', lg: '140px' },
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
bg: isSelected
|
||||
? 'linear-gradient(135deg, rgba(250, 204, 21, 0.2), rgba(250, 204, 21, 0.12))'
|
||||
: 'linear-gradient(135deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05))',
|
||||
borderColor: isSelected
|
||||
? 'rgba(250, 204, 21, 0.5)'
|
||||
: 'rgba(255, 255, 255, 0.25)',
|
||||
transform: 'translateY(-2px)',
|
||||
boxShadow: isSelected
|
||||
? '0 8px 20px rgba(250, 204, 21, 0.3)'
|
||||
: '0 6px 16px rgba(0, 0, 0, 0.4)',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div className={hstack({ gap: '3', alignItems: 'flex-start' })}>
|
||||
? 'rgba(250, 204, 21, 0.15)'
|
||||
: 'rgba(255, 255, 255, 0.1)',
|
||||
borderRadius: 'lg',
|
||||
})}
|
||||
>
|
||||
<MiniAbacus values={skill.values} columns={skill.columns} />
|
||||
</div>
|
||||
<div className={stack({ gap: '2', flex: '1', minWidth: '0' })}>
|
||||
<div
|
||||
className={css({
|
||||
fontSize: '3xl',
|
||||
width: { base: '120px', lg: '150px' },
|
||||
minHeight: { base: '115px', lg: '140px' },
|
||||
display: 'flex',
|
||||
className={hstack({
|
||||
gap: '2',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
textAlign: 'center',
|
||||
bg: isSelected
|
||||
? 'rgba(250, 204, 21, 0.15)'
|
||||
: 'rgba(255, 255, 255, 0.1)',
|
||||
borderRadius: 'lg',
|
||||
flexWrap: 'wrap',
|
||||
})}
|
||||
>
|
||||
<MiniAbacus values={skill.values} columns={skill.columns} />
|
||||
</div>
|
||||
<div className={stack({ gap: '2', flex: '1', minWidth: '0' })}>
|
||||
<div
|
||||
className={hstack({
|
||||
gap: '2',
|
||||
alignItems: 'center',
|
||||
flexWrap: 'wrap',
|
||||
className={css({
|
||||
color: 'white',
|
||||
fontSize: 'md',
|
||||
fontWeight: 'bold',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
color: 'white',
|
||||
fontSize: 'md',
|
||||
fontWeight: 'bold',
|
||||
})}
|
||||
>
|
||||
{skill.title}
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
bg: 'rgba(250, 204, 21, 0.2)',
|
||||
color: 'yellow.400',
|
||||
fontSize: '2xs',
|
||||
fontWeight: 'semibold',
|
||||
px: '2',
|
||||
py: '0.5',
|
||||
borderRadius: 'md',
|
||||
})}
|
||||
>
|
||||
{skill.badge}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
color: 'gray.300',
|
||||
fontSize: 'xs',
|
||||
lineHeight: '1.5',
|
||||
})}
|
||||
>
|
||||
{skill.desc}
|
||||
{skill.title}
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
bg: 'rgba(250, 204, 21, 0.2)',
|
||||
color: 'yellow.400',
|
||||
fontSize: 'xs',
|
||||
fontFamily: 'mono',
|
||||
fontSize: '2xs',
|
||||
fontWeight: 'semibold',
|
||||
mt: '1',
|
||||
bg: 'rgba(250, 204, 21, 0.1)',
|
||||
px: '2',
|
||||
py: '1',
|
||||
py: '0.5',
|
||||
borderRadius: 'md',
|
||||
w: 'fit-content',
|
||||
})}
|
||||
>
|
||||
{skill.example}
|
||||
{skill.badge}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
color: 'gray.300',
|
||||
fontSize: 'xs',
|
||||
lineHeight: '1.5',
|
||||
})}
|
||||
>
|
||||
{skill.desc}
|
||||
</div>
|
||||
<div
|
||||
className={css({
|
||||
color: 'yellow.400',
|
||||
fontSize: 'xs',
|
||||
fontFamily: 'mono',
|
||||
fontWeight: 'semibold',
|
||||
mt: '1',
|
||||
bg: 'rgba(250, 204, 21, 0.1)',
|
||||
px: '2',
|
||||
py: '1',
|
||||
borderRadius: 'md',
|
||||
w: 'fit-content',
|
||||
})}
|
||||
>
|
||||
{skill.example}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Main content container */}
|
||||
<div className={container({ maxW: '7xl', px: '4', py: '12' })}>
|
||||
{/* Current Offerings Section */}
|
||||
<section className={stack({ gap: '6', mb: '16' })}>
|
||||
<div className={css({ textAlign: 'center' })}>
|
||||
@@ -397,7 +397,7 @@ export default function HomePage() {
|
||||
</section>
|
||||
|
||||
{/* Progression Visualization */}
|
||||
<section className={stack({ gap: '6', mb: '16' })}>
|
||||
<section className={stack({ gap: '6', mb: '16', overflow: 'hidden' })}>
|
||||
<div className={css({ textAlign: 'center' })}>
|
||||
<h2
|
||||
className={css({
|
||||
|
||||
@@ -576,10 +576,11 @@ export function AppNavBar({ variant = 'full', navSlot }: AppNavBarProps) {
|
||||
<Tooltip.Provider delayDuration={200}>
|
||||
<header
|
||||
className={css({
|
||||
bg: isTransparent ? 'transparent' : 'white',
|
||||
shadow: isTransparent ? 'none' : 'sm',
|
||||
bg: isTransparent ? 'transparent' : 'rgba(0, 0, 0, 0.5)',
|
||||
backdropFilter: isTransparent ? 'none' : 'blur(12px)',
|
||||
shadow: isTransparent ? 'none' : 'lg',
|
||||
borderBottom: isTransparent ? 'none' : '1px solid',
|
||||
borderColor: isTransparent ? 'transparent' : 'gray.200',
|
||||
borderColor: isTransparent ? 'transparent' : 'rgba(139, 92, 246, 0.2)',
|
||||
position: 'fixed',
|
||||
top: 0,
|
||||
left: 0,
|
||||
@@ -599,7 +600,7 @@ export function AppNavBar({ variant = 'full', navSlot }: AppNavBarProps) {
|
||||
flexDirection: 'column',
|
||||
gap: '0',
|
||||
textDecoration: 'none',
|
||||
_hover: { '& > .brand-name': { color: 'brand.900' } },
|
||||
_hover: { '& > .brand-name': { color: 'rgba(196, 181, 253, 1)' } },
|
||||
opacity: 0,
|
||||
animation: 'fadeIn 0.3s ease-out forwards',
|
||||
})}
|
||||
@@ -608,10 +609,10 @@ export function AppNavBar({ variant = 'full', navSlot }: AppNavBarProps) {
|
||||
className={css({
|
||||
fontSize: 'xl',
|
||||
fontWeight: 'bold',
|
||||
color: 'brand.800',
|
||||
color: 'rgba(255, 255, 255, 0.95)',
|
||||
})}
|
||||
>
|
||||
🧮 Abaci One
|
||||
Abaci One
|
||||
</span>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
@@ -619,10 +620,11 @@ export function AppNavBar({ variant = 'full', navSlot }: AppNavBarProps) {
|
||||
className={css({
|
||||
fontSize: 'xs',
|
||||
fontWeight: 'medium',
|
||||
color: 'brand.600',
|
||||
color: 'rgba(196, 181, 253, 0.8)',
|
||||
fontStyle: 'italic',
|
||||
cursor: 'help',
|
||||
_hover: { color: 'brand.700' },
|
||||
whiteSpace: 'nowrap',
|
||||
_hover: { color: 'rgba(196, 181, 253, 1)' },
|
||||
})}
|
||||
>
|
||||
{subtitle.text}
|
||||
@@ -717,6 +719,9 @@ function NavLink({
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
style={{
|
||||
backdropFilter: isTransparent ? 'blur(8px)' : 'none',
|
||||
}}
|
||||
className={css({
|
||||
px: { base: '4', md: '3' },
|
||||
py: { base: '3', md: '2' },
|
||||
@@ -729,24 +734,33 @@ function NavLink({
|
||||
? 'white'
|
||||
: 'rgba(255, 255, 255, 0.8)'
|
||||
: isActive
|
||||
? 'brand.700'
|
||||
: 'gray.600',
|
||||
? 'rgba(196, 181, 253, 1)'
|
||||
: 'rgba(209, 213, 219, 0.9)',
|
||||
bg: isTransparent
|
||||
? isActive
|
||||
? 'rgba(255, 255, 255, 0.15)'
|
||||
: 'transparent'
|
||||
? 'rgba(255, 255, 255, 0.2)'
|
||||
: 'rgba(255, 255, 255, 0.08)'
|
||||
: isActive
|
||||
? 'brand.50'
|
||||
? 'rgba(139, 92, 246, 0.2)'
|
||||
: 'transparent',
|
||||
border: isTransparent ? '1px solid' : 'none',
|
||||
borderColor: isTransparent
|
||||
? isActive
|
||||
? 'rgba(255, 255, 255, 0.3)'
|
||||
: 'rgba(255, 255, 255, 0.15)'
|
||||
: 'transparent',
|
||||
rounded: 'lg',
|
||||
transition: 'all',
|
||||
textDecoration: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxShadow: isTransparent ? '0 2px 8px rgba(0, 0, 0, 0.2)' : 'none',
|
||||
_hover: {
|
||||
color: isTransparent ? 'white' : isActive ? 'brand.800' : 'gray.900',
|
||||
bg: isTransparent ? 'rgba(255, 255, 255, 0.2)' : isActive ? 'brand.100' : 'gray.50',
|
||||
color: isTransparent ? 'white' : 'rgba(196, 181, 253, 1)',
|
||||
bg: isTransparent ? 'rgba(255, 255, 255, 0.25)' : 'rgba(139, 92, 246, 0.25)',
|
||||
borderColor: isTransparent ? 'rgba(255, 255, 255, 0.4)' : 'transparent',
|
||||
boxShadow: isTransparent ? '0 4px 12px rgba(0, 0, 0, 0.3)' : 'none',
|
||||
},
|
||||
})}
|
||||
>
|
||||
|
||||
@@ -131,7 +131,7 @@ export function HeroAbacus() {
|
||||
>
|
||||
<div
|
||||
className={css({
|
||||
transform: { base: 'scale(2)', md: 'scale(3)', lg: 'scale(4)' },
|
||||
transform: { base: 'scale(3.5)', md: 'scale(3.5)', lg: 'scale(4.25)' },
|
||||
transformOrigin: 'center center',
|
||||
transition: 'all 0.5s cubic-bezier(0.4, 0, 0.2, 1)',
|
||||
})}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
'use client'
|
||||
|
||||
import { AbacusReact } from '@soroban/abacus-react'
|
||||
import { useDrag } from '@use-gesture/react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { animated, config, to, useSpring } from '@react-spring/web'
|
||||
import { css } from '../../styled-system/css'
|
||||
|
||||
interface Flashcard {
|
||||
@@ -16,12 +14,11 @@ interface Flashcard {
|
||||
}
|
||||
|
||||
/**
|
||||
* InteractiveFlashcards - A fun, physics-based flashcard display
|
||||
* Users can drag and throw flashcards around with realistic momentum
|
||||
* InteractiveFlashcards - A fun flashcard display where you can drag cards around
|
||||
* Cards stay where you drop them - simple and intuitive
|
||||
*/
|
||||
export function InteractiveFlashcards() {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
// Generate 8-15 random flashcards (client-side only to avoid hydration errors)
|
||||
const [cards, setCards] = useState<Flashcard[]>([])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -77,7 +74,7 @@ export function InteractiveFlashcards() {
|
||||
maxW: '1200px',
|
||||
mx: 'auto',
|
||||
height: { base: '400px', md: '500px' },
|
||||
overflow: 'hidden',
|
||||
overflow: 'visible',
|
||||
bg: 'rgba(0, 0, 0, 0.3)',
|
||||
rounded: 'xl',
|
||||
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||
@@ -95,190 +92,195 @@ interface DraggableCardProps {
|
||||
}
|
||||
|
||||
function DraggableCard({ card }: DraggableCardProps) {
|
||||
// Track the card's current position in state (separate from the animation values)
|
||||
const currentPositionRef = useRef({
|
||||
x: card.initialX,
|
||||
y: card.initialY,
|
||||
rotation: card.initialRotation,
|
||||
})
|
||||
|
||||
const [{ x, y, rotation, scale }, api] = useSpring(() => ({
|
||||
x: card.initialX,
|
||||
y: card.initialY,
|
||||
rotation: card.initialRotation,
|
||||
scale: 1,
|
||||
config: config.wobbly,
|
||||
}))
|
||||
|
||||
// Track position - starts at initial, updates when dragged
|
||||
const [position, setPosition] = useState({ x: card.initialX, y: card.initialY })
|
||||
const [rotation, setRotation] = useState(card.initialRotation) // Now dynamic!
|
||||
const [zIndex, setZIndex] = useState(card.zIndex)
|
||||
const cardRef = useRef<HTMLDivElement>(null)
|
||||
const dragOffsetRef = useRef({ x: 0, y: 0 })
|
||||
const lastVelocityRef = useRef({ vx: 0, vy: 0 })
|
||||
const velocityHistoryRef = useRef<Array<{ vx: number; vy: number }>>([])
|
||||
const [transformOrigin, setTransformOrigin] = useState('center center')
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [dragSpeed, setDragSpeed] = useState(0) // Speed for dynamic shadow
|
||||
|
||||
const bind = useDrag(
|
||||
({ down, movement: [mx, my], velocity: [vx, vy], direction: [dx, dy], first, xy }) => {
|
||||
// Bring card to front when dragging
|
||||
if (down) {
|
||||
setZIndex(1000)
|
||||
// Track drag state
|
||||
const dragStartRef = useRef<{ x: number; y: number; cardX: number; cardY: number } | null>(null)
|
||||
const grabOffsetRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 }) // Offset from card center where grabbed
|
||||
const baseRotationRef = useRef(card.initialRotation) // Starting rotation
|
||||
const lastMoveTimeRef = useRef<number>(0)
|
||||
const lastMovePositionRef = useRef<{ x: number; y: number }>({ x: 0, y: 0 })
|
||||
const lastLogTimeRef = useRef<number>(0) // Separate throttling for logging
|
||||
const cardRef = useRef<HTMLDivElement>(null) // Reference to card element
|
||||
|
||||
// Calculate drag offset from card center on first touch
|
||||
if (first && cardRef.current) {
|
||||
const cardRect = cardRef.current.getBoundingClientRect()
|
||||
const cardWidth = cardRect.width
|
||||
const cardHeight = cardRect.height
|
||||
const handlePointerDown = (e: React.PointerEvent) => {
|
||||
setIsDragging(true)
|
||||
setZIndex(1000) // Bring to front
|
||||
setDragSpeed(0)
|
||||
|
||||
// xy is in viewport coordinates, convert to position relative to card
|
||||
const clickRelativeToCard = {
|
||||
x: xy[0] - cardRect.left,
|
||||
y: xy[1] - cardRect.top,
|
||||
}
|
||||
// Capture the pointer
|
||||
e.currentTarget.setPointerCapture(e.pointerId)
|
||||
|
||||
// Calculate offset from card center
|
||||
const cardCenterX = cardWidth / 2
|
||||
const cardCenterY = cardHeight / 2
|
||||
const offsetX = clickRelativeToCard.x - cardCenterX
|
||||
const offsetY = clickRelativeToCard.y - cardCenterY
|
||||
|
||||
dragOffsetRef.current = { x: offsetX, y: offsetY }
|
||||
|
||||
// Convert offset to transform-origin (50% + offset as percentage of card size)
|
||||
const originX = 50 + (offsetX / cardWidth) * 100
|
||||
const originY = 50 + (offsetY / cardHeight) * 100
|
||||
const transformOriginValue = `${originX}% ${originY}%`
|
||||
|
||||
console.log(
|
||||
`Drag start: click at (${clickRelativeToCard.x.toFixed(0)}, ${clickRelativeToCard.y.toFixed(0)}) in card, offset from center: (${offsetX.toFixed(0)}, ${offsetY.toFixed(0)}), origin: ${transformOriginValue}`
|
||||
)
|
||||
|
||||
setTransformOrigin(transformOriginValue)
|
||||
velocityHistoryRef.current = []
|
||||
}
|
||||
|
||||
// Smooth velocity by averaging last 3 frames
|
||||
velocityHistoryRef.current.push({ vx, vy })
|
||||
if (velocityHistoryRef.current.length > 3) {
|
||||
velocityHistoryRef.current.shift()
|
||||
}
|
||||
|
||||
const avgVx =
|
||||
velocityHistoryRef.current.reduce((sum, v) => sum + v.vx, 0) /
|
||||
velocityHistoryRef.current.length
|
||||
const avgVy =
|
||||
velocityHistoryRef.current.reduce((sum, v) => sum + v.vy, 0) /
|
||||
velocityHistoryRef.current.length
|
||||
|
||||
// Calculate rotation based on smoothed velocity and drag offset
|
||||
const velocityAngle = Math.atan2(avgVy, avgVx) * (180 / Math.PI)
|
||||
const offsetAngle =
|
||||
Math.atan2(dragOffsetRef.current.y, dragOffsetRef.current.x) * (180 / Math.PI)
|
||||
|
||||
// Card rotates to align with movement direction, offset by where we're grabbing
|
||||
const targetRotation = velocityAngle - offsetAngle + 90
|
||||
|
||||
const speed = Math.sqrt(avgVx * avgVx + avgVy * avgVy)
|
||||
|
||||
// Store smoothed velocity for throw
|
||||
lastVelocityRef.current = { vx: avgVx, vy: avgVy }
|
||||
|
||||
const finalRotation = speed > 0.01 ? targetRotation : currentPositionRef.current.rotation
|
||||
|
||||
api.start({
|
||||
x: currentPositionRef.current.x + mx,
|
||||
y: currentPositionRef.current.y + my,
|
||||
scale: 1.1,
|
||||
rotation: finalRotation,
|
||||
immediate: (key) => key !== 'rotation', // Position immediate, rotation smooth
|
||||
config: { tension: 200, friction: 30 }, // Smoother rotation spring
|
||||
})
|
||||
} else {
|
||||
// On release, reset transform origin to center
|
||||
setTransformOrigin('center center')
|
||||
|
||||
// On release, apply momentum with decay physics
|
||||
const throwVelocityX = lastVelocityRef.current.vx * 1000
|
||||
const throwVelocityY = lastVelocityRef.current.vy * 1000
|
||||
|
||||
// Calculate final rotation based on throw direction
|
||||
const throwAngle = Math.atan2(throwVelocityY, throwVelocityX) * (180 / Math.PI)
|
||||
|
||||
api.start({
|
||||
x: {
|
||||
from: currentPositionRef.current.x + mx,
|
||||
velocity: throwVelocityX,
|
||||
decay: true,
|
||||
},
|
||||
y: {
|
||||
from: currentPositionRef.current.y + my,
|
||||
velocity: throwVelocityY,
|
||||
decay: true,
|
||||
},
|
||||
scale: 1,
|
||||
rotation: throwAngle + 90, // Card aligns with throw direction
|
||||
config: config.wobbly,
|
||||
onChange: (result) => {
|
||||
// Update current position as card settles
|
||||
if (result.value.x !== undefined) {
|
||||
currentPositionRef.current.x = result.value.x
|
||||
}
|
||||
if (result.value.y !== undefined) {
|
||||
currentPositionRef.current.y = result.value.y
|
||||
}
|
||||
if (result.value.rotation !== undefined) {
|
||||
currentPositionRef.current.rotation = result.value.rotation
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
{
|
||||
// Prevent scrolling when dragging
|
||||
preventDefault: true,
|
||||
filterTaps: true,
|
||||
// Record where the drag started (pointer position and card position)
|
||||
dragStartRef.current = {
|
||||
x: e.clientX,
|
||||
y: e.clientY,
|
||||
cardX: position.x,
|
||||
cardY: position.y,
|
||||
}
|
||||
)
|
||||
|
||||
// Calculate grab offset from card center
|
||||
if (cardRef.current) {
|
||||
const rect = cardRef.current.getBoundingClientRect()
|
||||
const cardCenterX = rect.left + rect.width / 2
|
||||
const cardCenterY = rect.top + rect.height / 2
|
||||
grabOffsetRef.current = {
|
||||
x: e.clientX - cardCenterX,
|
||||
y: e.clientY - cardCenterY,
|
||||
}
|
||||
console.log(
|
||||
`[GrabPoint] Grabbed at offset: (${grabOffsetRef.current.x.toFixed(0)}, ${grabOffsetRef.current.y.toFixed(0)})px from center`
|
||||
)
|
||||
}
|
||||
|
||||
// Store the current rotation as the base for this drag
|
||||
baseRotationRef.current = rotation
|
||||
|
||||
// Initialize velocity tracking
|
||||
const now = Date.now()
|
||||
lastMoveTimeRef.current = now
|
||||
lastMovePositionRef.current = { x: e.clientX, y: e.clientY }
|
||||
lastLogTimeRef.current = now
|
||||
|
||||
console.log('[Shadow] Drag started, speed reset to 0')
|
||||
}
|
||||
|
||||
const handlePointerMove = (e: React.PointerEvent) => {
|
||||
if (!isDragging || !dragStartRef.current) return
|
||||
|
||||
// Calculate how far the pointer has moved since drag started
|
||||
const deltaX = e.clientX - dragStartRef.current.x
|
||||
const deltaY = e.clientY - dragStartRef.current.y
|
||||
|
||||
// Calculate velocity for dynamic shadow
|
||||
const now = Date.now()
|
||||
const timeDelta = now - lastMoveTimeRef.current
|
||||
|
||||
if (timeDelta > 0) {
|
||||
// Distance moved since last frame
|
||||
const distX = e.clientX - lastMovePositionRef.current.x
|
||||
const distY = e.clientY - lastMovePositionRef.current.y
|
||||
const distance = Math.sqrt(distX * distX + distY * distY)
|
||||
|
||||
// Speed in pixels per millisecond, then convert to reasonable scale
|
||||
const speed = distance / timeDelta
|
||||
const scaledSpeed = Math.min(speed * 100, 100) // Cap at 100 for reasonable shadow size
|
||||
|
||||
setDragSpeed(scaledSpeed)
|
||||
|
||||
// Log occasionally (every ~200ms) to avoid console spam
|
||||
const timeSinceLastLog = now - lastLogTimeRef.current
|
||||
if (timeSinceLastLog > 200) {
|
||||
console.log(
|
||||
`[Shadow] Speed: ${scaledSpeed.toFixed(1)}, distance: ${distance.toFixed(0)}px, timeDelta: ${timeDelta}ms`
|
||||
)
|
||||
lastLogTimeRef.current = now
|
||||
}
|
||||
|
||||
lastMoveTimeRef.current = now
|
||||
lastMovePositionRef.current = { x: e.clientX, y: e.clientY }
|
||||
}
|
||||
|
||||
// Calculate rotation based on grab point physics
|
||||
// Cross product of grab offset and drag direction determines rotation
|
||||
// If grabbed on left and dragged right → clockwise rotation
|
||||
// If grabbed on right and dragged left → counter-clockwise rotation
|
||||
const crossProduct = grabOffsetRef.current.x * deltaY - grabOffsetRef.current.y * deltaX
|
||||
const rotationInfluence = crossProduct / 500 // Reduced scale factor for more visible rotation
|
||||
const newRotation = baseRotationRef.current + rotationInfluence
|
||||
|
||||
// Clamp rotation to prevent excessive spinning
|
||||
const clampedRotation = Math.max(-45, Math.min(45, newRotation))
|
||||
setRotation(clampedRotation)
|
||||
|
||||
// Log rotation changes occasionally (same throttle as shadow logging)
|
||||
const timeSinceLastLog = now - lastLogTimeRef.current
|
||||
if (timeSinceLastLog > 200) {
|
||||
console.log(
|
||||
`[GrabPoint] Rotation: ${clampedRotation.toFixed(1)}° (influence: ${rotationInfluence.toFixed(1)}°, cross: ${crossProduct.toFixed(0)})`
|
||||
)
|
||||
}
|
||||
|
||||
// Update card position
|
||||
setPosition({
|
||||
x: dragStartRef.current.cardX + deltaX,
|
||||
y: dragStartRef.current.cardY + deltaY,
|
||||
})
|
||||
}
|
||||
|
||||
const handlePointerUp = (e: React.PointerEvent) => {
|
||||
setIsDragging(false)
|
||||
dragStartRef.current = null
|
||||
|
||||
console.log('[Shadow] Drag released, speed decaying to 0')
|
||||
console.log(
|
||||
`[GrabPoint] Final rotation: ${rotation.toFixed(1)}° (base was ${baseRotationRef.current.toFixed(1)}°)`
|
||||
)
|
||||
|
||||
// Gradually decay speed back to 0 for smooth shadow transition
|
||||
const decayInterval = setInterval(() => {
|
||||
setDragSpeed((prev) => {
|
||||
const newSpeed = prev * 0.8 // Decay by 20% each frame
|
||||
if (newSpeed < 1) {
|
||||
clearInterval(decayInterval)
|
||||
return 0
|
||||
}
|
||||
return newSpeed
|
||||
})
|
||||
}, 50) // Update every 50ms
|
||||
|
||||
// Release the pointer capture
|
||||
e.currentTarget.releasePointerCapture(e.pointerId)
|
||||
}
|
||||
|
||||
// Calculate dynamic shadow based on drag speed
|
||||
// Base shadow: 0 8px 24px rgba(0, 0, 0, 0.3)
|
||||
// Fast drag: 0 32px 64px rgba(0, 0, 0, 0.6)
|
||||
const shadowY = 8 + (dragSpeed / 100) * 24 // 8px to 32px
|
||||
const shadowBlur = 24 + (dragSpeed / 100) * 40 // 24px to 64px
|
||||
const shadowOpacity = 0.3 + (dragSpeed / 100) * 0.3 // 0.3 to 0.6
|
||||
const boxShadow = `0 ${shadowY}px ${shadowBlur}px rgba(0, 0, 0, ${shadowOpacity})`
|
||||
|
||||
return (
|
||||
<animated.div
|
||||
<div
|
||||
ref={cardRef}
|
||||
{...bind()}
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerMove={handlePointerMove}
|
||||
onPointerUp={handlePointerUp}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: 0,
|
||||
top: 0,
|
||||
transform: to(
|
||||
[x, y, rotation, scale],
|
||||
(x, y, r, s) => `translate(${x}px, ${y}px) rotate(${r}deg) scale(${s})`
|
||||
),
|
||||
transformOrigin,
|
||||
transform: `translate(${position.x}px, ${position.y}px) rotate(${rotation}deg) scale(${isDragging ? 1.05 : 1})`,
|
||||
zIndex,
|
||||
touchAction: 'none',
|
||||
cursor: 'grab',
|
||||
cursor: isDragging ? 'grabbing' : 'grab',
|
||||
transition: isDragging ? 'none' : 'transform 0.2s ease-out',
|
||||
}}
|
||||
className={css({
|
||||
userSelect: 'none',
|
||||
_active: {
|
||||
cursor: 'grabbing',
|
||||
},
|
||||
})}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
boxShadow, // Dynamic shadow based on drag speed
|
||||
}}
|
||||
className={css({
|
||||
bg: 'white',
|
||||
rounded: 'lg',
|
||||
p: '4',
|
||||
boxShadow: '0 8px 24px rgba(0, 0, 0, 0.3)',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '2',
|
||||
minW: '120px',
|
||||
border: '2px solid rgba(0, 0, 0, 0.1)',
|
||||
transition: 'box-shadow 0.2s',
|
||||
_hover: {
|
||||
boxShadow: '0 12px 32px rgba(0, 0, 0, 0.4)',
|
||||
},
|
||||
transition: 'box-shadow 0.1s', // Quick transition for responsive feel
|
||||
})}
|
||||
>
|
||||
{/* Abacus visualization */}
|
||||
@@ -303,6 +305,6 @@ function DraggableCard({ card }: DraggableCardProps) {
|
||||
{card.number}
|
||||
</div>
|
||||
</div>
|
||||
</animated.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "soroban-monorepo",
|
||||
"version": "4.57.5",
|
||||
"version": "4.63.1",
|
||||
"private": true,
|
||||
"description": "Beautiful Soroban Flashcard Generator - Monorepo",
|
||||
"workspaces": [
|
||||
|
||||
Reference in New Issue
Block a user