📚 Update AbacusReact examples and documentation
Auto-generated fresh examples and balanced documentation from latest component changes.
Includes comprehensive usage patterns, API documentation, and educational examples.
Files updated:
- packages/abacus-react/README.md
- packages/abacus-react/src/AbacusReact.examples.stories.tsx
🤖 Generated with GitHub Actions
Co-Authored-By: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
parent
76163a0846
commit
af209fe6ac
|
|
@ -1,26 +1,21 @@
|
|||
# @soroban/abacus-react
|
||||
|
||||
A comprehensive React component for rendering interactive Soroban (Japanese abacus) visualizations with advanced tutorial capabilities, directional gestures, and complete visual customization.
|
||||
A comprehensive React component for rendering interactive Soroban (Japanese abacus) visualizations with advanced customization and tutorial capabilities.
|
||||
|
||||
## Features
|
||||
|
||||
- 🎯 **Interactive beads** - Click to toggle or use directional drag gestures
|
||||
- 🎨 **Complete visual customization** - Style every element individually with granular control
|
||||
- 📱 **Responsive scaling** - Configurable scale factor for different display sizes
|
||||
- 🎯 **Interactive beads** - Click to toggle or use directional gestures
|
||||
- 🎨 **Complete visual customization** - Style every element individually
|
||||
- 📱 **Responsive scaling** - Configurable scale factor for different sizes
|
||||
- 🌈 **Multiple color schemes** - Monochrome, place-value, alternating, heaven-earth
|
||||
- 🎭 **Flexible bead shapes** - Diamond, square, or circle beads
|
||||
- ⚡ **React Spring animations** - Smooth bead movements and state transitions
|
||||
- 🔧 **Developer-friendly** - Comprehensive hooks, callbacks, and ref system
|
||||
- 🎓 **Tutorial system** - Built-in overlay system with tooltips and highlights
|
||||
- 🧩 **Framework-free SVG** - Complete control over rendering and styling
|
||||
- 🏗️ **Type-safe APIs** - Full TypeScript support with branded types
|
||||
- 📐 **Precise positioning** - Place-value based bead targeting system
|
||||
- 🎮 **Directional gestures** - Natural drag interactions for bead manipulation
|
||||
- 🎭 **Flexible shapes** - Diamond, square, or circle beads
|
||||
- ⚡ **React Spring animations** - Smooth bead movements and transitions
|
||||
- 🔧 **Developer-friendly** - Comprehensive hooks and callback system
|
||||
- 🎓 **Tutorial system** - Built-in overlay and guidance capabilities
|
||||
- 🧩 **Framework-free SVG** - Complete control over rendering
|
||||
|
||||
## Installation
|
||||
|
||||
### From npm (recommended)
|
||||
|
||||
```bash
|
||||
npm install @soroban/abacus-react
|
||||
# or
|
||||
|
|
@ -29,33 +24,29 @@ pnpm add @soroban/abacus-react
|
|||
yarn add @soroban/abacus-react
|
||||
```
|
||||
|
||||
### From GitHub Packages
|
||||
|
||||
```bash
|
||||
# Configure npm to use GitHub Packages for @soroban scope
|
||||
echo "@soroban:registry=https://npm.pkg.github.com" >> .npmrc
|
||||
|
||||
# Then install
|
||||
npm install @soroban/abacus-react
|
||||
```
|
||||
|
||||
The package is published to both npm and GitHub Packages simultaneously for redundancy and choice.
|
||||
|
||||
## Quick Start
|
||||
|
||||
|
||||
### Basic Usage
|
||||
|
||||
Simple static abacus display:
|
||||
Simple abacus showing a number
|
||||
|
||||
<img src="https://raw.githubusercontent.com/antialias/soroban-abacus-flashcards/main/packages/abacus-react/examples/basic-usage.svg" alt="Basic Usage">
|
||||
|
||||
```tsx
|
||||
import { AbacusReact } from "@soroban/abacus-react";
|
||||
|
||||
<AbacusReact value={123} columns={3} showNumbers={true} scaleFactor={1.0} />;
|
||||
<AbacusReact
|
||||
value={123}
|
||||
columns={3}
|
||||
showNumbers={true}
|
||||
scaleFactor={1.0}
|
||||
/>
|
||||
```
|
||||
|
||||
### Interactive Mode
|
||||
|
||||
Clickable abacus with animations and callbacks:
|
||||
Clickable abacus with animations
|
||||
|
||||
<img src="https://raw.githubusercontent.com/antialias/soroban-abacus-flashcards/main/packages/abacus-react/examples/interactive.svg" alt="Interactive Mode">
|
||||
|
||||
```tsx
|
||||
<AbacusReact
|
||||
|
|
@ -63,18 +54,19 @@ Clickable abacus with animations and callbacks:
|
|||
columns={3}
|
||||
interactive={true}
|
||||
animated={true}
|
||||
gestures={true}
|
||||
showNumbers={true}
|
||||
callbacks={{
|
||||
onValueChange: (newValue) => console.log("New value:", newValue),
|
||||
onBeadClick: (event) => console.log("Bead clicked:", event),
|
||||
onValueChange: (newValue) => console.log('New value:', newValue),
|
||||
onBeadClick: (event) => console.log('Bead clicked:', event)
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
### Custom Styling
|
||||
|
||||
Personalized colors and visual themes:
|
||||
Personalized colors and highlights
|
||||
|
||||
<img src="https://raw.githubusercontent.com/antialias/soroban-abacus-flashcards/main/packages/abacus-react/examples/custom-styling.svg" alt="Custom Styling">
|
||||
|
||||
```tsx
|
||||
<AbacusReact
|
||||
|
|
@ -82,116 +74,72 @@ Personalized colors and visual themes:
|
|||
columns={3}
|
||||
colorScheme="place-value"
|
||||
beadShape="circle"
|
||||
colorPalette="nature"
|
||||
customStyles={{
|
||||
heavenBeads: { fill: "#2ecc71", stroke: "#27ae60" },
|
||||
earthBeads: { fill: "#3498db", stroke: "#2980b9" },
|
||||
numerals: { color: "#2c3e50", fontWeight: "bold" },
|
||||
reckoningBar: { stroke: "#34495e", strokeWidth: 3 },
|
||||
heavenBeads: { fill: '#ff6b35' },
|
||||
earthBeads: { fill: '#3498db' },
|
||||
numerals: { color: '#2c3e50', fontWeight: 'bold' }
|
||||
}}
|
||||
highlightBeads={[
|
||||
{ placeValue: 2, beadType: "heaven" }, // Hundreds place heaven bead
|
||||
{ placeValue: 0, beadType: "earth", position: 1 }, // Ones place, second earth bead
|
||||
{ columnIndex: 1, beadType: 'heaven' }
|
||||
]}
|
||||
/>
|
||||
```
|
||||
|
||||
### Tutorial System
|
||||
|
||||
Educational guidance with interactive overlays:
|
||||
Educational guidance with tooltips
|
||||
|
||||
<img src="https://raw.githubusercontent.com/antialias/soroban-abacus-flashcards/main/packages/abacus-react/examples/tutorial-mode.svg" alt="Tutorial System">
|
||||
|
||||
```tsx
|
||||
<AbacusReact
|
||||
value={42}
|
||||
columns={2}
|
||||
interactive={true}
|
||||
overlays={[
|
||||
{
|
||||
id: "tutorial-tip",
|
||||
type: "tooltip",
|
||||
target: {
|
||||
type: "bead",
|
||||
columnIndex: 0,
|
||||
beadType: "earth",
|
||||
beadPosition: 1,
|
||||
},
|
||||
content: (
|
||||
<div
|
||||
style={{
|
||||
background: "#333",
|
||||
color: "white",
|
||||
padding: "8px",
|
||||
borderRadius: "4px",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
Click this bead to add 1!
|
||||
</div>
|
||||
),
|
||||
offset: { x: 0, y: -30 },
|
||||
},
|
||||
]}
|
||||
stepBeadHighlights={[
|
||||
{
|
||||
placeValue: 0,
|
||||
beadType: "earth",
|
||||
position: 1,
|
||||
stepIndex: 0,
|
||||
direction: "activate",
|
||||
order: 1,
|
||||
},
|
||||
]}
|
||||
showDirectionIndicators={true}
|
||||
overlays={[{
|
||||
id: 'tip',
|
||||
type: 'tooltip',
|
||||
target: { type: 'bead', columnIndex: 0, beadType: 'earth', beadPosition: 1 },
|
||||
content: <div>Click this bead!</div>,
|
||||
offset: { x: 0, y: -30 }
|
||||
}]}
|
||||
callbacks={{
|
||||
onBeadClick: (event) => {
|
||||
if (
|
||||
event.placeValue === 0 &&
|
||||
event.beadType === "earth" &&
|
||||
event.position === 1
|
||||
) {
|
||||
console.log("Tutorial step completed!");
|
||||
if (event.columnIndex === 0 && event.beadType === 'earth' && event.position === 1) {
|
||||
console.log('Correct!');
|
||||
}
|
||||
},
|
||||
}
|
||||
}}
|
||||
/>
|
||||
```
|
||||
|
||||
|
||||
## Core API
|
||||
|
||||
### AbacusConfig Interface
|
||||
### Basic Props
|
||||
|
||||
```tsx
|
||||
interface AbacusConfig {
|
||||
// Display
|
||||
value?: number; // 0-99999, number to display
|
||||
columns?: number | "auto"; // Number of columns or auto-calculate
|
||||
showNumbers?: boolean; // Show place value numbers below
|
||||
scaleFactor?: number; // 0.5 - 3.0, size multiplier
|
||||
showEmptyColumns?: boolean; // Display columns with value 0
|
||||
value?: number; // 0-99999, number to display
|
||||
columns?: number | 'auto'; // Number of columns or auto-calculate
|
||||
showNumbers?: boolean; // Show place value numbers
|
||||
scaleFactor?: number; // 0.5 - 3.0, size multiplier
|
||||
|
||||
// Appearance
|
||||
beadShape?: "diamond" | "square" | "circle";
|
||||
colorScheme?: "monochrome" | "place-value" | "alternating" | "heaven-earth";
|
||||
colorPalette?: "default" | "colorblind" | "mnemonic" | "grayscale" | "nature";
|
||||
hideInactiveBeads?: boolean; // Hide/show inactive beads
|
||||
beadShape?: 'diamond' | 'square' | 'circle';
|
||||
colorScheme?: 'monochrome' | 'place-value' | 'alternating' | 'heaven-earth';
|
||||
colorPalette?: 'default' | 'colorblind' | 'mnemonic' | 'grayscale' | 'nature';
|
||||
hideInactiveBeads?: boolean; // Hide/show inactive beads
|
||||
|
||||
// Interaction
|
||||
interactive?: boolean; // Enable user interactions
|
||||
animated?: boolean; // Enable React Spring animations
|
||||
gestures?: boolean; // Enable directional drag gestures
|
||||
|
||||
// Advanced
|
||||
customStyles?: AbacusCustomStyles; // Granular styling control
|
||||
callbacks?: AbacusCallbacks; // Event handlers
|
||||
overlays?: AbacusOverlay[]; // Tutorial overlay system
|
||||
highlightBeads?: BeadHighlight[]; // Highlight specific beads
|
||||
stepBeadHighlights?: StepBeadHighlight[]; // Progressive tutorial highlighting
|
||||
showDirectionIndicators?: boolean; // Show movement direction indicators
|
||||
disabledBeads?: BeadHighlight[]; // Disable specific bead interactions
|
||||
interactive?: boolean; // Enable user interactions
|
||||
animated?: boolean; // Enable animations
|
||||
gestures?: boolean; // Enable drag gestures
|
||||
}
|
||||
```
|
||||
|
||||
### Event System
|
||||
### Event Callbacks
|
||||
|
||||
```tsx
|
||||
interface AbacusCallbacks {
|
||||
|
|
@ -199,214 +147,109 @@ interface AbacusCallbacks {
|
|||
onBeadClick?: (event: BeadClickEvent) => void;
|
||||
onBeadHover?: (event: BeadClickEvent) => void;
|
||||
onBeadLeave?: (event: BeadClickEvent) => void;
|
||||
onColumnClick?: (columnIndex: number, event: React.MouseEvent) => void;
|
||||
onNumeralClick?: (
|
||||
columnIndex: number,
|
||||
value: number,
|
||||
event: React.MouseEvent,
|
||||
) => void;
|
||||
onColumnClick?: (columnIndex: number) => void;
|
||||
onNumeralClick?: (columnIndex: number, value: number) => void;
|
||||
onBeadRef?: (bead: BeadConfig, element: SVGElement | null) => void;
|
||||
}
|
||||
|
||||
interface BeadClickEvent {
|
||||
bead: BeadConfig; // Complete bead configuration
|
||||
columnIndex: number; // 0, 1, 2... (array index)
|
||||
placeValue: ValidPlaceValues; // 0=ones, 1=tens, 2=hundreds...
|
||||
beadType: "heaven" | "earth"; // Type of bead
|
||||
position: number; // Position within type (0-3 for earth)
|
||||
active: boolean; // Current activation state
|
||||
value: number; // Numeric value (1 or 5)
|
||||
event: React.MouseEvent; // Original mouse event
|
||||
columnIndex: number; // 0, 1, 2...
|
||||
beadType: 'heaven' | 'earth'; // Type of bead
|
||||
position: number; // Position within type (0-3 for earth)
|
||||
active: boolean; // Current state
|
||||
value: number; // Numeric value (1 or 5)
|
||||
bead: BeadConfig; // Full bead configuration
|
||||
}
|
||||
```
|
||||
|
||||
## Advanced Features
|
||||
## Advanced Customization
|
||||
|
||||
### Place-Value Based Targeting
|
||||
|
||||
Target beads by mathematical place value instead of visual column position:
|
||||
|
||||
```tsx
|
||||
// Target beads by place value (recommended)
|
||||
const placeValueHighlights = [
|
||||
{ placeValue: 0, beadType: "earth", position: 2 }, // Ones place, 3rd earth bead
|
||||
{ placeValue: 1, beadType: "heaven" }, // Tens place, heaven bead
|
||||
{ placeValue: 2, beadType: "earth", position: 0 }, // Hundreds place, 1st earth bead
|
||||
];
|
||||
|
||||
// Legacy column-index targeting (still supported)
|
||||
const columnHighlights = [
|
||||
{ columnIndex: 2, beadType: "earth", position: 2 }, // Rightmost column
|
||||
{ columnIndex: 1, beadType: "heaven" }, // Middle column
|
||||
{ columnIndex: 0, beadType: "earth", position: 0 }, // Leftmost column
|
||||
];
|
||||
|
||||
<AbacusReact highlightBeads={placeValueHighlights} />;
|
||||
```
|
||||
|
||||
### Progressive Tutorial Steps
|
||||
|
||||
Create multi-step interactive tutorials:
|
||||
|
||||
```tsx
|
||||
const tutorialSteps = [
|
||||
{
|
||||
placeValue: 0,
|
||||
beadType: "earth",
|
||||
position: 0,
|
||||
stepIndex: 0,
|
||||
direction: "activate",
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
placeValue: 0,
|
||||
beadType: "earth",
|
||||
position: 1,
|
||||
stepIndex: 1,
|
||||
direction: "activate",
|
||||
order: 1,
|
||||
},
|
||||
{
|
||||
placeValue: 1,
|
||||
beadType: "heaven",
|
||||
stepIndex: 2,
|
||||
direction: "activate",
|
||||
order: 1,
|
||||
},
|
||||
];
|
||||
|
||||
<AbacusReact
|
||||
stepBeadHighlights={tutorialSteps}
|
||||
currentStep={currentStepIndex}
|
||||
showDirectionIndicators={true}
|
||||
interactive={true}
|
||||
/>;
|
||||
```
|
||||
|
||||
### Granular Style Customization
|
||||
### Granular Styling
|
||||
|
||||
Target any visual element with precise control:
|
||||
|
||||
```tsx
|
||||
const advancedStyles = {
|
||||
const customStyles = {
|
||||
// Global defaults
|
||||
heavenBeads: { fill: "#e74c3c", stroke: "#c0392b" },
|
||||
earthBeads: { fill: "#3498db", stroke: "#2980b9" },
|
||||
heavenBeads: { fill: '#ff6b35' },
|
||||
earthBeads: { fill: '#3498db' },
|
||||
activeBeads: { opacity: 1.0 },
|
||||
inactiveBeads: { opacity: 0.3 },
|
||||
|
||||
// Column-specific overrides (by array index)
|
||||
// Column-specific overrides
|
||||
columns: {
|
||||
0: {
|
||||
// Leftmost column (highest place value)
|
||||
heavenBeads: { fill: "#f39c12" },
|
||||
earthBeads: { fill: "#e67e22" },
|
||||
backgroundGlow: { fill: "#fff3cd", opacity: 0.3 },
|
||||
},
|
||||
0: { // Hundreds column
|
||||
heavenBeads: { fill: '#e74c3c' },
|
||||
earthBeads: { fill: '#2ecc71' }
|
||||
}
|
||||
},
|
||||
|
||||
// Individual bead targeting (by array index)
|
||||
// Individual bead targeting
|
||||
beads: {
|
||||
1: {
|
||||
// Middle column
|
||||
heaven: { fill: "#9b59b6" },
|
||||
1: { // Middle column
|
||||
heaven: { fill: '#f39c12' },
|
||||
earth: {
|
||||
0: { fill: "#1abc9c" }, // First earth bead
|
||||
1: { fill: "#16a085" }, // Second earth bead
|
||||
2: { fill: "#17a2b8" }, // Third earth bead
|
||||
3: { fill: "#138496" }, // Fourth earth bead
|
||||
},
|
||||
},
|
||||
0: { fill: '#1abc9c' }, // First earth bead
|
||||
3: { fill: '#e67e22' } // Fourth earth bead
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// UI elements
|
||||
reckoningBar: { stroke: "#34495e", strokeWidth: 3 },
|
||||
columnPosts: { stroke: "#7f8c8d", strokeWidth: 2 },
|
||||
reckoningBar: { stroke: '#34495e', strokeWidth: 3 },
|
||||
columnPosts: { stroke: '#7f8c8d' },
|
||||
numerals: {
|
||||
color: "#2c3e50",
|
||||
fontSize: "16px",
|
||||
fontFamily: "monospace",
|
||||
fontWeight: "bold",
|
||||
},
|
||||
color: '#2c3e50',
|
||||
fontSize: '14px',
|
||||
fontFamily: 'monospace'
|
||||
}
|
||||
};
|
||||
|
||||
<AbacusReact customStyles={advancedStyles} />;
|
||||
<AbacusReact customStyles={customStyles} />
|
||||
```
|
||||
|
||||
### Overlay System
|
||||
### Tutorial and Overlay System
|
||||
|
||||
Create rich interactive educational experiences:
|
||||
Create interactive educational experiences:
|
||||
|
||||
```tsx
|
||||
const educationalOverlays = [
|
||||
const overlays = [
|
||||
{
|
||||
id: "value-explanation",
|
||||
type: "tooltip",
|
||||
target: { type: "bead", columnIndex: 0, beadType: "heaven" },
|
||||
id: 'welcome-tooltip',
|
||||
type: 'tooltip',
|
||||
target: {
|
||||
type: 'bead',
|
||||
columnIndex: 0,
|
||||
beadType: 'earth',
|
||||
beadPosition: 0
|
||||
},
|
||||
content: (
|
||||
<div className="tutorial-tooltip">
|
||||
<h4>Heaven Bead</h4>
|
||||
<p>Worth 5 in this place value</p>
|
||||
<button onClick={() => nextStep()}>Got it!</button>
|
||||
<div style={{
|
||||
background: '#333',
|
||||
color: 'white',
|
||||
padding: '8px',
|
||||
borderRadius: '4px'
|
||||
}}>
|
||||
Click me to start!
|
||||
</div>
|
||||
),
|
||||
offset: { x: 0, y: -40 },
|
||||
},
|
||||
{
|
||||
id: "direction-arrow",
|
||||
type: "arrow",
|
||||
target: {
|
||||
type: "bead",
|
||||
columnIndex: 1,
|
||||
beadType: "earth",
|
||||
beadPosition: 0,
|
||||
},
|
||||
content: <div className="arrow-down">⬇</div>,
|
||||
offset: { x: 0, y: -20 },
|
||||
},
|
||||
offset: { x: 0, y: -30 }
|
||||
}
|
||||
];
|
||||
|
||||
<AbacusReact
|
||||
overlays={educationalOverlays}
|
||||
interactive={true}
|
||||
overlays={overlays}
|
||||
highlightBeads={[
|
||||
{ columnIndex: 0, beadType: 'earth', position: 0 }
|
||||
]}
|
||||
callbacks={{
|
||||
onBeadClick: handleTutorialProgression,
|
||||
onBeadClick: (event) => {
|
||||
if (event.columnIndex === 0 && event.beadType === 'earth' && event.position === 0) {
|
||||
console.log('Tutorial step completed!');
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>;
|
||||
```
|
||||
|
||||
### Dimension Calculation Hook
|
||||
|
||||
Get exact sizing information for layout planning:
|
||||
|
||||
```tsx
|
||||
import { useAbacusDimensions } from "@soroban/abacus-react";
|
||||
|
||||
function ResponsiveAbacusContainer() {
|
||||
const dimensions = useAbacusDimensions(
|
||||
5, // columns
|
||||
1.2, // scale factor
|
||||
true, // show numbers
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: dimensions.width,
|
||||
height: dimensions.height,
|
||||
border: "1px solid #ccc",
|
||||
padding: "10px",
|
||||
}}
|
||||
>
|
||||
<AbacusReact
|
||||
columns={5}
|
||||
scaleFactor={1.2}
|
||||
showNumbers={true}
|
||||
value={12345}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
/>
|
||||
```
|
||||
|
||||
### Bead Reference System
|
||||
|
|
@ -414,7 +257,7 @@ function ResponsiveAbacusContainer() {
|
|||
Access individual bead DOM elements for advanced positioning:
|
||||
|
||||
```tsx
|
||||
function AdvancedPositioning() {
|
||||
function AdvancedExample() {
|
||||
const beadRefs = useRef(new Map<string, SVGElement>());
|
||||
|
||||
const handleBeadRef = (bead: BeadConfig, element: SVGElement | null) => {
|
||||
|
|
@ -422,21 +265,100 @@ function AdvancedPositioning() {
|
|||
if (element) {
|
||||
beadRefs.current.set(key, element);
|
||||
|
||||
// Position custom elements relative to beads
|
||||
// Now you can position tooltips, highlights, etc. precisely
|
||||
const rect = element.getBoundingClientRect();
|
||||
console.log(`Bead at column ${bead.columnIndex} positioned at:`, rect);
|
||||
console.log(`Bead at column ${bead.columnIndex} is at:`, rect);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AbacusReact callbacks={{ onBeadRef: handleBeadRef }} interactive={true} />
|
||||
<AbacusReact
|
||||
callbacks={{ onBeadRef: handleBeadRef }}
|
||||
// ... other props
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Hooks
|
||||
|
||||
### useAbacusDimensions
|
||||
|
||||
Get exact sizing information for layout planning:
|
||||
|
||||
```tsx
|
||||
import { useAbacusDimensions } from '@soroban/abacus-react';
|
||||
|
||||
function MyComponent() {
|
||||
const dimensions = useAbacusDimensions(3, 1.2); // 3 columns, 1.2x scale
|
||||
|
||||
return (
|
||||
<div style={{ width: dimensions.width, height: dimensions.height }}>
|
||||
<AbacusReact columns={3} scaleFactor={1.2} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Educational Use Cases
|
||||
|
||||
### Interactive Math Lessons
|
||||
|
||||
```tsx
|
||||
function MathLesson() {
|
||||
const [problem, setProblem] = useState({ a: 23, b: 45 });
|
||||
const [step, setStep] = useState('show-first');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h3>Add {problem.a} + {problem.b}</h3>
|
||||
|
||||
<AbacusReact
|
||||
value={step === 'show-first' ? problem.a : 0}
|
||||
interactive={step === 'add-second'}
|
||||
callbacks={{
|
||||
onValueChange: (value) => {
|
||||
if (value === problem.a + problem.b) {
|
||||
celebrate();
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Assessment Tools
|
||||
|
||||
```tsx
|
||||
function AbacusQuiz() {
|
||||
const [answers, setAnswers] = useState([]);
|
||||
|
||||
const checkAnswer = (event: BeadClickEvent) => {
|
||||
const isCorrect = validateBeadClick(event, expectedAnswer);
|
||||
recordAnswer(event, isCorrect);
|
||||
|
||||
if (isCorrect) {
|
||||
showSuccessFeedback();
|
||||
} else {
|
||||
showHint(event);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AbacusReact
|
||||
interactive={true}
|
||||
callbacks={{ onBeadClick: checkAnswer }}
|
||||
customStyles={getAnswerHighlighting(answers)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## TypeScript Support
|
||||
|
||||
Full TypeScript definitions with branded types for enhanced type safety:
|
||||
Full TypeScript definitions included:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
|
|
@ -447,191 +369,16 @@ import {
|
|||
AbacusCustomStyles,
|
||||
AbacusOverlay,
|
||||
AbacusCallbacks,
|
||||
useAbacusDimensions,
|
||||
PlaceValueBead,
|
||||
ColumnIndexBead,
|
||||
StepBeadHighlight,
|
||||
PlaceValue,
|
||||
ColumnIndex,
|
||||
ValidPlaceValues,
|
||||
EarthBeadPosition,
|
||||
} from "@soroban/abacus-react";
|
||||
useAbacusDimensions
|
||||
} from '@soroban/abacus-react';
|
||||
|
||||
// Branded types prevent mixing place values and column indices
|
||||
const placeValue: ValidPlaceValues = 2; // hundreds place
|
||||
const earthPosition: EarthBeadPosition = 3; // fourth earth bead
|
||||
|
||||
// Type-safe bead specification
|
||||
const bead: PlaceValueBead = {
|
||||
placeValue: 1, // tens place
|
||||
beadType: "earth",
|
||||
position: 2, // third earth bead
|
||||
};
|
||||
// All interfaces fully typed for excellent developer experience
|
||||
```
|
||||
|
||||
## Educational Use Cases
|
||||
|
||||
### Interactive Math Lessons
|
||||
|
||||
```tsx
|
||||
function AdditionLesson() {
|
||||
const [problem] = useState({ a: 23, b: 45 });
|
||||
const [step, setStep] = useState("show-first");
|
||||
const [userValue, setUserValue] = useState(0);
|
||||
|
||||
const checkAnswer = (newValue: number) => {
|
||||
setUserValue(newValue);
|
||||
if (newValue === problem.a + problem.b) {
|
||||
setStep("completed");
|
||||
showCelebration();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="math-lesson">
|
||||
<h3>
|
||||
Add {problem.a} + {problem.b}
|
||||
</h3>
|
||||
|
||||
<AbacusReact
|
||||
value={step === "show-first" ? problem.a : userValue}
|
||||
columns={3}
|
||||
interactive={step === "user-input"}
|
||||
animated={true}
|
||||
showNumbers={true}
|
||||
callbacks={{ onValueChange: checkAnswer }}
|
||||
highlightBeads={step === "hint" ? getHintBeads() : []}
|
||||
/>
|
||||
|
||||
{step === "completed" && (
|
||||
<div className="success">
|
||||
🎉 Correct! {problem.a} + {problem.b} = {problem.a + problem.b}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Assessment and Quizzing
|
||||
|
||||
```tsx
|
||||
function AbacusQuiz() {
|
||||
const [answers, setAnswers] = useState<BeadClickEvent[]>([]);
|
||||
const [feedback, setFeedback] = useState<string>("");
|
||||
|
||||
const validateAnswer = (event: BeadClickEvent) => {
|
||||
const isCorrect = checkBeadClick(event, expectedAnswer);
|
||||
|
||||
setAnswers((prev) => [...prev, event]);
|
||||
|
||||
if (isCorrect) {
|
||||
setFeedback("Correct! Well done.");
|
||||
advanceToNextQuestion();
|
||||
} else {
|
||||
setFeedback("Try again. Remember: this bead represents...");
|
||||
showHint(event);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="abacus-quiz">
|
||||
<AbacusReact
|
||||
value={currentQuestionValue}
|
||||
interactive={true}
|
||||
callbacks={{ onBeadClick: validateAnswer }}
|
||||
customStyles={getAnswerHighlighting(answers)}
|
||||
overlays={currentHints}
|
||||
/>
|
||||
<div className="feedback">{feedback}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Color Schemes and Accessibility
|
||||
|
||||
### Built-in Color Schemes
|
||||
|
||||
- **`monochrome`** - Single color for all beads
|
||||
- **`place-value`** - Different colors for each place value column
|
||||
- **`alternating`** - Alternating colors between columns
|
||||
- **`heaven-earth`** - Different colors for heaven vs earth beads
|
||||
|
||||
### Accessibility Palettes
|
||||
|
||||
- **`colorblind`** - High contrast, colorblind-friendly palette
|
||||
- **`grayscale`** - Monochrome grayscale for maximum compatibility
|
||||
- **`mnemonic`** - Colors that aid memory and learning
|
||||
- **`nature`** - Earth-tone palette for reduced eye strain
|
||||
|
||||
```tsx
|
||||
<AbacusReact
|
||||
colorScheme="place-value"
|
||||
colorPalette="colorblind"
|
||||
value={12345}
|
||||
columns={5}
|
||||
/>
|
||||
```
|
||||
|
||||
## Publishing and Versioning
|
||||
|
||||
This package uses [semantic-release](https://semantic-release.gitbook.io/) for automated publishing. Versions are determined by conventional commit messages:
|
||||
|
||||
### Commit Message Format
|
||||
|
||||
Use these prefixes for commits that affect the `packages/abacus-react` directory:
|
||||
|
||||
```bash
|
||||
# New features (minor version bump)
|
||||
feat(abacus-react): add gesture recognition system
|
||||
|
||||
# Bug fixes (patch version bump)
|
||||
fix(abacus-react): resolve animation timing issues
|
||||
|
||||
# Performance improvements (patch version bump)
|
||||
perf(abacus-react): optimize bead rendering performance
|
||||
|
||||
# Breaking changes (major version bump)
|
||||
feat(abacus-react)!: redesign callback API
|
||||
# or
|
||||
feat(abacus-react): change component interface
|
||||
|
||||
BREAKING CHANGE: callback functions now receive different parameters
|
||||
```
|
||||
|
||||
### Release Process
|
||||
|
||||
1. **Automatic**: Releases happen automatically when changes are pushed to `main` branch
|
||||
2. **Dual publishing**: Package is published to both npm and GitHub Packages simultaneously
|
||||
3. **Manual testing**: Run `pnpm release:dry-run` to test release without publishing
|
||||
4. **Version tags**: Releases are tagged as `abacus-react-v1.2.3` (separate from monorepo versions)
|
||||
|
||||
### Development Commands
|
||||
|
||||
```bash
|
||||
# Build the package
|
||||
pnpm build
|
||||
|
||||
# Run tests
|
||||
pnpm test:run
|
||||
|
||||
# Run Storybook locally
|
||||
pnpm storybook
|
||||
|
||||
# Test release process (dry run)
|
||||
pnpm release:dry-run
|
||||
```
|
||||
|
||||
## Live Documentation
|
||||
|
||||
- **Storybook**: [Component examples and documentation](https://antialias.github.io/soroban-abacus-flashcards/abacus-react/)
|
||||
- **Source Code**: [GitHub Repository](https://github.com/antialias/soroban-abacus-flashcards/tree/main/packages/abacus-react)
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions welcome! Please see our [contributing guidelines](../../CONTRIBUTING.md) and feel free to submit issues or pull requests.
|
||||
Contributions welcome! Please see our contributing guidelines and feel free to submit issues or pull requests.
|
||||
|
||||
## License
|
||||
|
||||
MIT License - see [LICENSE](../../LICENSE) file for details.
|
||||
MIT License - see LICENSE file for details.
|
||||
|
|
|
|||
|
|
@ -1,134 +1,133 @@
|
|||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import { AbacusReact } from "./AbacusReact";
|
||||
import React from "react";
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { AbacusReact } from './AbacusReact';
|
||||
import React from 'react';
|
||||
|
||||
const meta: Meta<typeof AbacusReact> = {
|
||||
title: "Examples/AbacusReact",
|
||||
title: 'Examples/AbacusReact',
|
||||
component: AbacusReact,
|
||||
parameters: {
|
||||
layout: "centered",
|
||||
layout: 'centered',
|
||||
docs: {
|
||||
description: {
|
||||
component:
|
||||
"Interactive Soroban (Japanese abacus) component with comprehensive customization options.",
|
||||
},
|
||||
},
|
||||
component: 'Interactive Soroban (Japanese abacus) component with comprehensive customization options.'
|
||||
}
|
||||
}
|
||||
},
|
||||
tags: ["autodocs"],
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
|
||||
export const basicUsage: Story = {
|
||||
name: "Basic Usage",
|
||||
name: 'Basic Usage',
|
||||
args: {
|
||||
value: 123,
|
||||
columns: 3,
|
||||
showNumbers: true,
|
||||
scaleFactor: 1,
|
||||
animated: false,
|
||||
},
|
||||
"value": 123,
|
||||
"columns": 3,
|
||||
"showNumbers": true,
|
||||
"scaleFactor": 1,
|
||||
"animated": false
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Simple abacus showing a number",
|
||||
},
|
||||
},
|
||||
},
|
||||
story: 'Simple abacus showing a number'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const interactive: Story = {
|
||||
name: "Interactive Mode",
|
||||
name: 'Interactive Mode',
|
||||
args: {
|
||||
value: 456,
|
||||
columns: 3,
|
||||
interactive: true,
|
||||
animated: false,
|
||||
showNumbers: true,
|
||||
},
|
||||
"value": 456,
|
||||
"columns": 3,
|
||||
"interactive": true,
|
||||
"animated": false,
|
||||
"showNumbers": true
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Clickable abacus with animations",
|
||||
},
|
||||
},
|
||||
},
|
||||
story: 'Clickable abacus with animations'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const customStyling: Story = {
|
||||
name: "Custom Styling",
|
||||
name: 'Custom Styling',
|
||||
args: {
|
||||
value: 789,
|
||||
columns: 3,
|
||||
colorScheme: "place-value",
|
||||
beadShape: "circle",
|
||||
animated: false,
|
||||
customStyles: {
|
||||
heavenBeads: {
|
||||
fill: "#ff6b35",
|
||||
},
|
||||
earthBeads: {
|
||||
fill: "#3498db",
|
||||
},
|
||||
numerals: {
|
||||
color: "#2c3e50",
|
||||
fontWeight: "bold",
|
||||
},
|
||||
"value": 789,
|
||||
"columns": 3,
|
||||
"colorScheme": "place-value",
|
||||
"beadShape": "circle",
|
||||
"animated": false,
|
||||
"customStyles": {
|
||||
"heavenBeads": {
|
||||
"fill": "#ff6b35"
|
||||
},
|
||||
highlightBeads: [
|
||||
{
|
||||
columnIndex: 1,
|
||||
beadType: "heaven",
|
||||
},
|
||||
],
|
||||
"earthBeads": {
|
||||
"fill": "#3498db"
|
||||
},
|
||||
"numerals": {
|
||||
"color": "#2c3e50",
|
||||
"fontWeight": "bold"
|
||||
}
|
||||
},
|
||||
"highlightBeads": [
|
||||
{
|
||||
"columnIndex": 1,
|
||||
"beadType": "heaven"
|
||||
}
|
||||
]
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Personalized colors and highlights",
|
||||
},
|
||||
},
|
||||
},
|
||||
story: 'Personalized colors and highlights'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const tutorialMode: Story = {
|
||||
name: "Tutorial System",
|
||||
name: 'Tutorial System',
|
||||
args: {
|
||||
value: 42,
|
||||
columns: 2,
|
||||
interactive: true,
|
||||
animated: false,
|
||||
showNumbers: true,
|
||||
},
|
||||
"value": 42,
|
||||
"columns": 2,
|
||||
"interactive": true,
|
||||
"animated": false,
|
||||
"showNumbers": true
|
||||
},
|
||||
parameters: {
|
||||
docs: {
|
||||
description: {
|
||||
story: "Educational guidance with tooltips",
|
||||
},
|
||||
},
|
||||
},
|
||||
story: 'Educational guidance with tooltips'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Advanced tutorial example (from our previous implementation)
|
||||
export const TutorialExample: Story = {
|
||||
name: "Interactive Tutorial",
|
||||
name: 'Interactive Tutorial',
|
||||
render: () => {
|
||||
const [step, setStep] = React.useState(0);
|
||||
const [feedbackMessage, setFeedbackMessage] = React.useState("");
|
||||
const [feedbackMessage, setFeedbackMessage] = React.useState('');
|
||||
|
||||
const tutorialSteps = [
|
||||
{
|
||||
instruction:
|
||||
"Click the orange highlighted bead in the ones column (leftmost)",
|
||||
highlightBeads: [{ columnIndex: 4, beadType: "earth", position: 2 }],
|
||||
value: 7,
|
||||
instruction: "Click the orange highlighted bead in the ones column (leftmost)",
|
||||
highlightBeads: [{ columnIndex: 0, beadType: 'earth', position: 2 }],
|
||||
value: 7
|
||||
},
|
||||
{
|
||||
instruction: "Click anywhere in the ones column (leftmost column)",
|
||||
highlightColumns: [0],
|
||||
value: 7,
|
||||
},
|
||||
value: 7
|
||||
}
|
||||
];
|
||||
|
||||
const currentStep = tutorialSteps[step];
|
||||
|
|
@ -138,8 +137,7 @@ export const TutorialExample: Story = {
|
|||
|
||||
if (step === 0) {
|
||||
const target = currentStep.highlightBeads?.[0];
|
||||
isCorrectTarget =
|
||||
target &&
|
||||
isCorrectTarget = target &&
|
||||
event.columnIndex === target.columnIndex &&
|
||||
event.beadType === target.beadType &&
|
||||
event.position === target.position;
|
||||
|
|
@ -149,25 +147,23 @@ export const TutorialExample: Story = {
|
|||
|
||||
if (isCorrectTarget && step < tutorialSteps.length - 1) {
|
||||
setStep(step + 1);
|
||||
setFeedbackMessage("✅ Correct! Moving to next step...");
|
||||
setTimeout(() => setFeedbackMessage(""), 2000);
|
||||
setFeedbackMessage('✅ Correct! Moving to next step...');
|
||||
setTimeout(() => setFeedbackMessage(''), 2000);
|
||||
} else if (!isCorrectTarget) {
|
||||
setFeedbackMessage("⚠️ Try clicking the highlighted area.");
|
||||
setTimeout(() => setFeedbackMessage(""), 3000);
|
||||
setFeedbackMessage('⚠️ Try clicking the highlighted area.');
|
||||
setTimeout(() => setFeedbackMessage(''), 3000);
|
||||
}
|
||||
};
|
||||
|
||||
const handleRestart = () => {
|
||||
setStep(0);
|
||||
setFeedbackMessage("");
|
||||
setFeedbackMessage('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ position: "relative", padding: "20px" }}>
|
||||
<div style={{ marginBottom: "20px", textAlign: "center" }}>
|
||||
<h3>
|
||||
Interactive Tutorial - Step {step + 1} of {tutorialSteps.length}
|
||||
</h3>
|
||||
<div style={{ position: 'relative', padding: '20px' }}>
|
||||
<div style={{ marginBottom: '20px', textAlign: 'center' }}>
|
||||
<h3>Interactive Tutorial - Step {step + 1} of {tutorialSteps.length}</h3>
|
||||
<p>{currentStep.instruction}</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -178,105 +174,91 @@ export const TutorialExample: Story = {
|
|||
animated={true}
|
||||
showNumbers={true}
|
||||
scaleFactor={1.2}
|
||||
|
||||
highlightColumns={currentStep.highlightColumns}
|
||||
highlightBeads={currentStep.highlightBeads}
|
||||
|
||||
customStyles={{
|
||||
beads: {
|
||||
0: {
|
||||
earth: {
|
||||
2:
|
||||
step === 0
|
||||
? { fill: "#ff6b35", stroke: "#d63031", strokeWidth: 2 }
|
||||
: undefined,
|
||||
},
|
||||
},
|
||||
},
|
||||
2: step === 0 ? { fill: '#ff6b35', stroke: '#d63031', strokeWidth: 2 } : undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
}}
|
||||
overlays={
|
||||
step === 0
|
||||
? [
|
||||
{
|
||||
id: "tutorial-tip",
|
||||
type: "tooltip",
|
||||
target: {
|
||||
type: "bead",
|
||||
columnIndex: 4,
|
||||
beadType: "earth",
|
||||
beadPosition: 2,
|
||||
},
|
||||
content: (
|
||||
<div
|
||||
style={{
|
||||
background: "#333",
|
||||
color: "white",
|
||||
padding: "8px",
|
||||
borderRadius: "4px",
|
||||
fontSize: "12px",
|
||||
maxWidth: "120px",
|
||||
textAlign: "center",
|
||||
}}
|
||||
>
|
||||
Click this bead!
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: "-6px",
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
borderTop: "6px solid #333",
|
||||
borderLeft: "6px solid transparent",
|
||||
borderRight: "6px solid transparent",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
offset: { x: 0, y: -50 },
|
||||
},
|
||||
]
|
||||
: []
|
||||
}
|
||||
|
||||
overlays={step === 0 ? [{
|
||||
id: 'tutorial-tip',
|
||||
type: 'tooltip',
|
||||
target: {
|
||||
type: 'bead',
|
||||
columnIndex: 0,
|
||||
beadType: 'earth',
|
||||
beadPosition: 2
|
||||
},
|
||||
content: (
|
||||
<div style={{
|
||||
background: '#333',
|
||||
color: 'white',
|
||||
padding: '8px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
maxWidth: '120px',
|
||||
textAlign: 'center'
|
||||
}}>
|
||||
Click this bead!
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
bottom: '-6px',
|
||||
left: '50%',
|
||||
transform: 'translateX(-50%)',
|
||||
borderTop: '6px solid #333',
|
||||
borderLeft: '6px solid transparent',
|
||||
borderRight: '6px solid transparent'
|
||||
}} />
|
||||
</div>
|
||||
),
|
||||
offset: { x: 0, y: -50 }
|
||||
}] : []}
|
||||
|
||||
callbacks={{
|
||||
onBeadClick: handleBeadClick,
|
||||
onBeadClick: handleBeadClick
|
||||
}}
|
||||
/>
|
||||
|
||||
<div style={{ marginTop: "20px", textAlign: "center" }}>
|
||||
<button
|
||||
onClick={handleRestart}
|
||||
style={{
|
||||
padding: "8px 16px",
|
||||
backgroundColor: "#007bff",
|
||||
color: "white",
|
||||
border: "none",
|
||||
borderRadius: "4px",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
>
|
||||
<div style={{ marginTop: '20px', textAlign: 'center' }}>
|
||||
<button onClick={handleRestart} style={{
|
||||
padding: '8px 16px',
|
||||
backgroundColor: '#007bff',
|
||||
color: 'white',
|
||||
border: 'none',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer'
|
||||
}}>
|
||||
Restart Tutorial
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{feedbackMessage && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
right: "-300px",
|
||||
top: "50%",
|
||||
transform: "translateY(-50%)",
|
||||
background: "#f8f9fa",
|
||||
border: "1px solid #dee2e6",
|
||||
borderRadius: "4px",
|
||||
padding: "12px",
|
||||
fontSize: "14px",
|
||||
maxWidth: "250px",
|
||||
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
|
||||
transition: "all 0.3s ease",
|
||||
}}
|
||||
>
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
right: '-300px',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
background: '#f8f9fa',
|
||||
border: '1px solid #dee2e6',
|
||||
borderRadius: '4px',
|
||||
padding: '12px',
|
||||
fontSize: '14px',
|
||||
maxWidth: '250px',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
|
||||
transition: 'all 0.3s ease'
|
||||
}}>
|
||||
{feedbackMessage}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Reference in New Issue