feat: create interactive gallery replicating original Typst design

- Add comprehensive gallery component matching original gallery layout
- Implement tabbed interface with Basic, Advanced, and Debug examples
- Include live statistics tracking and reset functionality
- Support all 13 original gallery configurations with full interactivity
- Add responsive styling matching original design aesthetic

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-09-18 08:18:26 -05:00
parent 528cac50a8
commit 1bcfd22f17

View File

@@ -0,0 +1,645 @@
import React, { useState, useCallback } from 'react';
import { AbacusReact, useAbacusDimensions, BeadConfig } from './AbacusReact';
// Gallery configuration mapping from the original examples
const GALLERY_EXAMPLES = [
{
id: 'basic-5',
title: 'Basic Number 5',
subtitle: 'Simple representation of 5',
value: 5,
config: {
columns: 1,
beadShape: 'diamond' as const,
colorScheme: 'monochrome' as const,
scaleFactor: 1,
animated: true,
draggable: true
}
},
{
id: 'colorful-123',
title: 'Colorful 123',
subtitle: 'Multi-column with place value colors',
value: 123,
config: {
columns: 3,
beadShape: 'diamond' as const,
colorScheme: 'place-value' as const,
colorPalette: 'default' as const,
scaleFactor: 1,
animated: true,
draggable: true
}
},
{
id: 'circles-42',
title: 'Circle Beads - 42',
subtitle: 'Different bead shape demonstration',
value: 42,
config: {
columns: 2,
beadShape: 'circle' as const,
colorScheme: 'place-value' as const,
colorPalette: 'default' as const,
scaleFactor: 1.2,
animated: true,
draggable: true
}
},
{
id: 'large-7',
title: 'Large Scale - 7',
subtitle: 'Larger scale for better visibility',
value: 7,
config: {
columns: 1,
beadShape: 'diamond' as const,
colorScheme: 'place-value' as const,
colorPalette: 'default' as const,
scaleFactor: 2,
animated: true,
draggable: true
}
},
{
id: 'compact-999',
title: 'Compact 999',
subtitle: 'Square beads with alternating colors',
value: 999,
config: {
columns: 3,
beadShape: 'square' as const,
colorScheme: 'alternating' as const,
scaleFactor: 0.8,
animated: true,
draggable: true
}
},
{
id: 'educational-1234',
title: 'Educational 1234',
subtitle: 'Four-digit educational example',
value: 1234,
config: {
columns: 4,
beadShape: 'circle' as const,
colorScheme: 'place-value' as const,
colorPalette: 'mnemonic' as const,
scaleFactor: 0.9,
animated: true,
draggable: true
}
},
{
id: 'crop-single-1',
title: 'Single Digit',
subtitle: 'Minimal single column design',
value: 1,
config: {
columns: 1,
beadShape: 'diamond' as const,
colorScheme: 'monochrome' as const,
scaleFactor: 0.8,
animated: true,
draggable: true
}
},
{
id: 'crop-quad-9999',
title: 'Four 9s',
subtitle: 'Maximum value demonstration',
value: 9999,
config: {
columns: 4,
beadShape: 'diamond' as const,
colorScheme: 'place-value' as const,
colorPalette: 'colorblind' as const,
scaleFactor: 0.9,
animated: true,
draggable: true
}
},
{
id: 'crop-large-scale-0',
title: 'Large Zero',
subtitle: 'Empty abacus representation',
value: 0,
config: {
columns: 1,
beadShape: 'circle' as const,
colorScheme: 'monochrome' as const,
scaleFactor: 2,
hideInactiveBeads: false,
animated: true,
draggable: true
}
},
{
id: 'crop-hidden-inactive-555',
title: 'Hidden Inactive',
subtitle: 'Clean look with hidden inactive beads',
value: 555,
config: {
columns: 3,
beadShape: 'diamond' as const,
colorScheme: 'place-value' as const,
colorPalette: 'nature' as const,
hideInactiveBeads: true,
scaleFactor: 1.4,
animated: true,
draggable: true
}
},
{
id: 'crop-mixed-geometry-321',
title: 'Mixed Geometry',
subtitle: 'Demonstrating various configurations',
value: 321,
config: {
columns: 3,
beadShape: 'circle' as const,
colorScheme: 'place-value' as const,
colorPalette: 'colorblind' as const,
scaleFactor: 1.1,
animated: true,
draggable: true
}
},
{
id: 'debug-89',
title: 'Debug: 89',
subtitle: 'Two-digit debugging example',
value: 89,
config: {
columns: 2,
beadShape: 'diamond' as const,
colorScheme: 'place-value' as const,
colorPalette: 'default' as const,
scaleFactor: 1,
animated: true,
draggable: true
}
},
{
id: 'debug-456',
title: 'Debug: 456',
subtitle: 'Three-digit debugging example',
value: 456,
config: {
columns: 3,
beadShape: 'circle' as const,
colorScheme: 'place-value' as const,
colorPalette: 'default' as const,
scaleFactor: 0.8,
animated: true,
draggable: true
}
}
];
interface InteractiveAbacusCardProps {
example: typeof GALLERY_EXAMPLES[0];
onValueChange?: (newValue: number) => void;
onBeadClick?: (bead: BeadConfig) => void;
}
const InteractiveAbacusCard: React.FC<InteractiveAbacusCardProps> = ({
example,
onValueChange,
onBeadClick
}) => {
const [currentValue, setCurrentValue] = useState(example.value);
const [clickCount, setClickCount] = useState(0);
const dimensions = useAbacusDimensions(
example.config.columns === 'auto' ? Math.max(1, currentValue.toString().length) : example.config.columns,
example.config.scaleFactor || 1
);
const handleValueChange = useCallback((newValue: number) => {
setCurrentValue(newValue);
onValueChange?.(newValue);
}, [onValueChange]);
const handleBeadClick = useCallback((bead: BeadConfig) => {
setClickCount(prev => prev + 1);
onBeadClick?.(bead);
}, [onBeadClick]);
const resetValue = useCallback(() => {
setCurrentValue(example.value);
setClickCount(0);
}, [example.value]);
return (
<div className="gallery-card">
<div className="card-header">
<div className="card-title">
<h3>{example.title}</h3>
<p>{example.subtitle}</p>
</div>
<div className="card-controls">
<div className="value-display">
Value: <strong>{currentValue}</strong>
</div>
<div className="interaction-stats">
Clicks: <strong>{clickCount}</strong>
</div>
<button className="reset-btn" onClick={resetValue} title="Reset to original value">
</button>
</div>
</div>
<div className="card-content">
<div
className="abacus-container"
style={{
width: `${dimensions.width}px`,
height: `${dimensions.height}px`,
margin: '0 auto'
}}
>
<AbacusReact
value={currentValue}
{...example.config}
onClick={handleBeadClick}
onValueChange={handleValueChange}
/>
</div>
</div>
<div className="card-footer">
<div className="config-info">
<span className="config-tag">{example.config.beadShape}</span>
<span className="config-tag">{example.config.colorScheme}</span>
<span className="config-tag">×{example.config.scaleFactor}</span>
</div>
</div>
</div>
);
};
const InteractiveGallery: React.FC = () => {
const [selectedTab, setSelectedTab] = useState('basic');
const [globalStats, setGlobalStats] = useState({
totalClicks: 0,
totalValueChanges: 0,
activeExample: null as string | null
});
const categorizedExamples = {
basic: GALLERY_EXAMPLES.filter(ex =>
['basic-5', 'colorful-123', 'circles-42', 'large-7'].includes(ex.id)
),
advanced: GALLERY_EXAMPLES.filter(ex =>
['compact-999', 'educational-1234', 'crop-hidden-inactive-555', 'crop-mixed-geometry-321'].includes(ex.id)
),
debug: GALLERY_EXAMPLES.filter(ex =>
['crop-single-1', 'crop-quad-9999', 'crop-large-scale-0', 'debug-89', 'debug-456'].includes(ex.id)
)
};
const handleGlobalValueChange = useCallback((newValue: number) => {
setGlobalStats(prev => ({
...prev,
totalValueChanges: prev.totalValueChanges + 1
}));
}, []);
const handleGlobalBeadClick = useCallback((bead: BeadConfig) => {
setGlobalStats(prev => ({
...prev,
totalClicks: prev.totalClicks + 1
}));
}, []);
const currentExamples = categorizedExamples[selectedTab as keyof typeof categorizedExamples];
return (
<div className="interactive-gallery">
<style>{`
.interactive-gallery {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #333;
background: #f5f5f5;
padding: 20px;
min-height: 100vh;
}
.container {
max-width: 1400px;
margin: 0 auto;
}
.header {
text-align: center;
margin-bottom: 40px;
padding: 40px 20px;
background: white;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.header h1 {
font-size: 2.5rem;
margin-bottom: 10px;
color: #2c3e50;
}
.header p {
font-size: 1.1rem;
color: #666;
margin-bottom: 20px;
}
.stats {
background: white;
padding: 15px 20px;
border-radius: 8px;
margin-bottom: 30px;
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
text-align: center;
}
.stats-info {
color: #666;
font-size: 0.9rem;
}
.global-stats {
display: flex;
justify-content: center;
gap: 30px;
margin-top: 10px;
}
.stat-item {
display: flex;
align-items: center;
gap: 5px;
}
.tabs {
background: white;
border-radius: 12px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
overflow: hidden;
margin-bottom: 30px;
}
.tab-nav {
display: flex;
border-bottom: 1px solid #eee;
}
.tab-button {
flex: 1;
padding: 20px;
background: none;
border: none;
cursor: pointer;
font-size: 1.1rem;
font-weight: 600;
color: #666;
transition: all 0.3s;
position: relative;
}
.tab-button:hover {
background: #f8f9fa;
color: #333;
}
.tab-button.active {
color: #2c3e50;
background: #f8f9fa;
}
.tab-button.active::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 3px;
background: #3498db;
}
.gallery-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 30px;
padding: 30px;
}
.gallery-card {
background: white;
border-radius: 12px;
box-shadow: 0 4px 15px rgba(0,0,0,0.1);
overflow: hidden;
transition: all 0.3s ease;
}
.gallery-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
}
.card-header {
padding: 20px;
border-bottom: 1px solid #eee;
display: flex;
justify-content: space-between;
align-items: flex-start;
}
.card-title h3 {
margin: 0 0 5px 0;
color: #2c3e50;
font-size: 1.3rem;
}
.card-title p {
margin: 0;
color: #666;
font-size: 0.9rem;
}
.card-controls {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 5px;
}
.value-display, .interaction-stats {
font-size: 0.85rem;
color: #666;
}
.reset-btn {
background: #3498db;
color: white;
border: none;
border-radius: 4px;
padding: 5px 10px;
cursor: pointer;
font-size: 1.2rem;
transition: background 0.2s;
}
.reset-btn:hover {
background: #2980b9;
}
.card-content {
padding: 30px;
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
background: #fafafa;
}
.abacus-container {
border: 2px solid rgba(0,0,0,0.1);
border-radius: 8px;
background: white;
padding: 10px;
}
.card-footer {
padding: 15px 20px;
background: #f8f9fa;
border-top: 1px solid #eee;
}
.config-info {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.config-tag {
background: #e9ecef;
color: #495057;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.8rem;
font-weight: 500;
}
.tutorial-box {
background: #e8f4fd;
border: 1px solid #bee5eb;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
text-align: center;
}
.tutorial-box h3 {
color: #0c5460;
margin-bottom: 10px;
}
.tutorial-box p {
color: #0c5460;
margin: 0;
}
@media (max-width: 768px) {
.gallery-grid {
grid-template-columns: 1fr;
padding: 20px;
}
.card-header {
flex-direction: column;
align-items: stretch;
gap: 15px;
}
.card-controls {
flex-direction: row;
justify-content: space-between;
align-items: center;
}
}
`}</style>
<div className="container">
<div className="header">
<h1>🧮 Interactive Soroban Gallery</h1>
<p>Click and drag the beads to explore how a Japanese abacus works!</p>
<div className="global-stats">
<div className="stat-item">
<span>Total Interactions:</span>
<strong>{globalStats.totalClicks}</strong>
</div>
<div className="stat-item">
<span>Value Changes:</span>
<strong>{globalStats.totalValueChanges}</strong>
</div>
</div>
</div>
<div className="stats">
<div className="stats-info">
<strong>{GALLERY_EXAMPLES.length}</strong> interactive examples
All abaci are fully interactive with drag and click support
Generated with React + TypeScript
</div>
</div>
<div className="tutorial-box">
<h3>🎯 How to Interact</h3>
<p>
<strong>Click</strong> beads to toggle their positions <strong>Drag</strong> beads for tactile feedback
<strong>Reset</strong> button restores original values Each interaction updates the value in real-time
</p>
</div>
<div className="tabs">
<div className="tab-nav">
<button
className={`tab-button ${selectedTab === 'basic' ? 'active' : ''}`}
onClick={() => setSelectedTab('basic')}
>
📚 Basic Examples
</button>
<button
className={`tab-button ${selectedTab === 'advanced' ? 'active' : ''}`}
onClick={() => setSelectedTab('advanced')}
>
🎨 Advanced Features
</button>
<button
className={`tab-button ${selectedTab === 'debug' ? 'active' : ''}`}
onClick={() => setSelectedTab('debug')}
>
🔧 Debug & Edge Cases
</button>
</div>
<div className="gallery-grid">
{currentExamples.map((example) => (
<InteractiveAbacusCard
key={example.id}
example={example}
onValueChange={handleGlobalValueChange}
onBeadClick={handleGlobalBeadClick}
/>
))}
</div>
</div>
</div>
</div>
);
};
export default InteractiveGallery;