diff --git a/packages/abacus-react/ENHANCEMENT_PLAN.md b/packages/abacus-react/ENHANCEMENT_PLAN.md
new file mode 100644
index 00000000..ea3a1a47
--- /dev/null
+++ b/packages/abacus-react/ENHANCEMENT_PLAN.md
@@ -0,0 +1,402 @@
+# Abacus-React Feature Enhancement Plan
+
+## Executive Summary
+
+The web application has developed numerous custom patterns and workarounds for styling, layout, and interactions with the abacus component. These patterns reveal gaps in the abacus-react API that, if addressed, would significantly improve developer experience and reduce code duplication across the application.
+
+## Priority 1: Critical Features (High Impact, High Frequency)
+
+### 1. **Inline "Mini Abacus" Component**
+**Location**: `apps/web/src/app/arcade/complement-race/components/AbacusTarget.tsx`
+
+**Current Implementation**:
+```tsx
+
+```
+
+**Problem**: Creating an inline mini-abacus for displaying single digits requires multiple props and style overrides. This pattern appears throughout game UIs.
+
+**Proposed Solution**: Add a `variant` prop with preset configurations:
+
+```tsx
+// Native API proposal
+
+
+// Or more granular:
+
+```
+
+**Benefits**:
+- Single prop instead of 5+
+- Consistent inline abacus appearance across the app
+- Better semantic intent
+
+---
+
+### 2. **Theme-Aware Styling Presets**
+**Locations**:
+- `MyAbacus.tsx` (lines 60-85) - structural & trophy styles
+- `HeroAbacus.tsx` (lines 20-32) - structural styles
+- `LevelSliderDisplay.tsx` (lines 263-275) - dark theme styles
+
+**Current Pattern**: Every component defines custom style objects for structural elements:
+
+```tsx
+const structuralStyles = {
+ columnPosts: {
+ fill: 'rgb(255, 255, 255)',
+ stroke: 'rgb(200, 200, 200)',
+ strokeWidth: 2,
+ },
+ reckoningBar: {
+ fill: 'rgb(255, 255, 255)',
+ stroke: 'rgb(200, 200, 200)',
+ strokeWidth: 3,
+ },
+}
+```
+
+**Problem**: Manual style object creation for common themes is repetitive and error-prone.
+
+**Proposed Solution**: Add theme presets to abacus-react:
+
+```tsx
+// Native API proposal
+
+
+// Or expose theme constants
+import { ABACUS_THEMES } from '@soroban/abacus-react'
+
+```
+
+**Benefits**:
+- Eliminates ~30 lines of style definitions per component
+- Ensures visual consistency
+- Makes theme switching trivial
+
+---
+
+### 3. **Scaling Containers & Responsive Layouts**
+**Locations**:
+- `HeroAbacus.tsx` (lines 133-138) - manual scale transforms
+- `MyAbacus.tsx` (lines 214-218) - responsive scale values
+- `LevelSliderDisplay.tsx` (lines 370-379) - dynamic scale calculation
+
+**Current Pattern**: Components manually wrap abacus in transform containers:
+
+```tsx
+
+```
+
+**Problem**: Manual transform handling requires extra DOM nesting, breaks click boundaries, and makes centering complex.
+
+**Proposed Solution**: Enhanced `scaleFactor` with responsive breakpoints:
+
+```tsx
+// Native API proposal
+
+```
+
+**Benefits**:
+- Eliminates wrapper divs
+- Proper click/hover boundaries
+- Built-in responsive scaling
+
+---
+
+## Priority 2: Developer Experience Improvements
+
+### 4. **Bead Diff Calculation System**
+**Location**: `apps/web/src/utils/beadDiff.ts` (285 lines) + `abacusInstructionGenerator.ts` (400+ lines)
+
+**Current Implementation**: Complex utilities to calculate which beads need to move between states:
+
+```tsx
+// Current external pattern
+import { calculateBeadDiffFromValues } from '@/utils/beadDiff'
+
+const diff = calculateBeadDiffFromValues(fromValue, toValue)
+const stepBeadHighlights = diff.changes.map(change => ({
+ placeValue: change.placeValue,
+ beadType: change.beadType,
+ direction: change.direction,
+ // ...
+}))
+```
+
+**Problem**: Tutorial/game developers need to calculate bead movements manually. This core logic belongs in abacus-react.
+
+**Proposed Solution**: Add a diff calculation hook:
+
+```tsx
+// Native API proposal
+import { useAbacusDiff } from '@soroban/abacus-react'
+
+function Tutorial() {
+ const diff = useAbacusDiff(startValue, targetValue)
+
+ return (
+
+ )
+}
+```
+
+**Benefits**:
+- Centralizes "diff" algorithm
+- Eliminates ~500 lines of application code
+- Better tested and maintained
+
+---
+
+### 5. **Tutorial/Step Context Provider**
+**Location**: `apps/web/src/components/tutorial/TutorialContext.tsx`
+
+**Current Pattern**: Apps need to implement complex state management for multi-step tutorial flows with reducer patterns, event tracking, and error handling.
+
+**Problem**: Tutorial infrastructure is duplicated across components. The logic for tracking progress through abacus instruction steps is tightly coupled to application code.
+
+**Proposed Solution**: Add optional tutorial/stepper context to abacus-react:
+
+```tsx
+// Native API proposal
+import { AbacusReact, AbacusTutorial } from '@soroban/abacus-react'
+
+ { /* analytics */ }}
+ onComplete={() => { /* celebration */ }}
+>
+
+
+```
+
+**Benefits**:
+- Reusable tutorial infrastructure
+- Built-in progress tracking and validation
+- Could power educational features across projects
+
+---
+
+## Priority 3: Nice-to-Have Enhancements
+
+### 6. **Animation Speed Configuration**
+**Location**: `LevelSliderDisplay.tsx` (lines 306-345)
+
+**Current Pattern**: Applications control animation speed by rapidly changing the value prop:
+
+```tsx
+const intervalMs = 500 - danProgress * 490 // 500ms down to 10ms
+setInterval(() => {
+ setAnimatedDigits(prev => {
+ // Rapidly change digits to simulate calculation
+ })
+}, intervalMs)
+```
+
+**Problem**: "Rapid calculation" animation requires external interval management.
+
+**Proposed Solution**: Add animation speed prop:
+
+```tsx
+// Native API proposal
+
+```
+
+**Benefits**:
+- Smoother animations with internal management
+- Consistent timing across the app
+
+---
+
+### 7. **Draggable/Positionable Abacus Cards**
+**Location**: `InteractiveFlashcards.tsx`
+
+**Current Pattern**: Complex drag-and-drop implementation wrapped around each AbacusReact instance with pointer capture, offset tracking, and rotation.
+
+**Problem**: Making abacus instances draggable requires significant boilerplate.
+
+**Proposed Solution**: This is probably too specific to remain external. However, a ref-based API to get bounding boxes would help:
+
+```tsx
+// Possible improvement
+const abacusRef = useAbacusRef()
+
+
+
+// abacusRef.current.getBoundingBox() for drag calculations
+```
+
+---
+
+### 8. **Column Highlighting for Multi-Step Problems**
+**Location**: Tutorial system extensively
+
+**Current Pattern**: Manual column highlighting based on place values with custom overlay positioning logic.
+
+**Problem**: Highlighting specific columns (e.g., "the tens column") requires external overlay management.
+
+**Proposed Solution**: Add native column highlighting:
+
+```tsx
+// Native API proposal
+
+```
+
+---
+
+## Priority 4: Documentation & Exports
+
+### 9. **Utility Functions & Types**
+**Current State**: Apps re-implement utilities for working with abacus states:
+- `numberToAbacusState()` - convert numbers to bead states
+- `calculateBeadChanges()` - diff algorithm
+- `ValidPlaceValues` type - imported but limited
+
+**Proposed Solution**: Export more utilities from abacus-react:
+
+```tsx
+// Expanded exports
+export {
+ // Utilities
+ numberToAbacusState,
+ abacusStateToNumber,
+ calculateBeadDiff,
+ validateAbacusValue,
+
+ // Types
+ AbacusState,
+ BeadState,
+ PlaceValue,
+
+ // Hooks
+ useAbacusDiff,
+ useAbacusValidation,
+ useAbacusState,
+}
+```
+
+---
+
+## Implementation Roadmap
+
+### Phase 1 (Immediate) ✅ COMPLETED
+1. ✅ Add `frameVisible={false}` prop
+2. ✅ Add `compact` prop/variant
+3. ✅ Export theme presets (ABACUS_THEMES constant)
+
+### Phase 2 (Short-term) ✅ COMPLETED
+4. ⏸️ Enhanced `scaleFactor` with responsive object support (DEFERRED - too complex, low priority)
+5. ✅ Export utility functions (numberToAbacusState, calculateBeadDiff, etc.)
+
+### Phase 3 (Medium-term) ✅ COMPLETED
+6. ✅ Add `useAbacusDiff` hook
+7. ✅ Add native column highlighting with `highlightColumns` and `columnLabels` props
+
+### Phase 4 (Long-term - Future)
+8. 📋 Consider tutorial context provider (needs more research)
+9. 📋 Animation speed controls
+
+## Completed Features Summary
+
+### New Props
+- `frameVisible?: boolean` - Show/hide column posts and reckoning bar
+- `compact?: boolean` - Compact layout for inline display (implies frameVisible=false)
+- `highlightColumns?: number[]` - Highlight specific columns by index
+- `columnLabels?: string[]` - Optional labels for columns
+
+### New Exports
+- `ABACUS_THEMES` - Pre-defined theme presets (light, dark, trophy, translucent, solid, traditional)
+- `AbacusThemeName` type - TypeScript type for theme names
+
+### New Utility Functions
+- `numberToAbacusState(value, maxPlaces)` - Convert number to bead positions
+- `abacusStateToNumber(state)` - Convert bead positions to number
+- `calculateBeadChanges(startState, targetState)` - Calculate bead differences
+- `calculateBeadDiff(fromState, toState)` - Full diff with order and directions
+- `calculateBeadDiffFromValues(from, to, maxPlaces)` - Convenience wrapper
+- `validateAbacusValue(value, maxPlaces)` - Validate number ranges
+- `areStatesEqual(state1, state2)` - Compare states
+
+### New Hooks
+- `useAbacusDiff(fromValue, toValue, maxPlaces)` - Calculate bead differences for tutorials
+- `useAbacusState(value, maxPlaces)` - Convert number to abacus state (memoized)
+
+### New Types
+- `BeadState` - Bead state in a single column
+- `AbacusState` - Complete abacus state
+- `BeadDiffResult` - Single bead movement result
+- `BeadDiffOutput` - Complete diff output
+- `PlaceValueBasedBead` - Internal place-value based bead type
+
+---
+
+## Metrics & Impact
+
+**Code Reduction Estimate**:
+- Eliminates ~800-1000 lines of repetitive application code
+- Reduces component complexity by ~40% for tutorial/game components
+
+**Developer Experience**:
+- Faster onboarding for new features using abacus
+- More consistent UX across application
+- Better TypeScript support and autocomplete
+
+**Maintenance**:
+- Centralized logic easier to test and debug
+- Single source of truth for abacus behavior
+- Easier to add new features (e.g., sound effects for different themes)
+
+---
+
+## Questions for Discussion
+
+1. Should we split these into separate packages (e.g., `@soroban/abacus-tutorial`)?
+2. Which theme presets should be included by default?
+3. Should responsive scaling use CSS media queries or JS breakpoints?
+4. How much tutorial logic belongs in the core library vs. app code?
diff --git a/packages/abacus-react/INTEGRATION_SUMMARY.md b/packages/abacus-react/INTEGRATION_SUMMARY.md
new file mode 100644
index 00000000..c31cd1f4
--- /dev/null
+++ b/packages/abacus-react/INTEGRATION_SUMMARY.md
@@ -0,0 +1,162 @@
+# Integration Summary
+
+## ✅ Completed: Apps/Web Integration with Abacus-React Enhancements
+
+### Features Implemented & Integrated
+
+#### 1. **Theme Presets (ABACUS_THEMES)**
+**Status:** ✅ Fully integrated
+
+**Files Updated:**
+- `apps/web/src/components/MyAbacus.tsx` - Now uses `ABACUS_THEMES.light` and `ABACUS_THEMES.trophy`
+- `apps/web/src/components/HeroAbacus.tsx` - Now uses `ABACUS_THEMES.light`
+- `apps/web/src/components/LevelSliderDisplay.tsx` - Now uses `ABACUS_THEMES.dark`
+
+**Code Eliminated:** ~60 lines of duplicate theme style definitions
+
+---
+
+#### 2. **Compact Prop**
+**Status:** ✅ Fully integrated
+
+**Files Updated:**
+- `apps/web/src/app/arcade/complement-race/components/AbacusTarget.tsx` - Now uses `compact={true}`
+
+**Before:**
+```tsx
+
+```
+
+**After:**
+```tsx
+
+```
+
+---
+
+#### 3. **Utility Functions**
+**Status:** ✅ Fully integrated
+
+**Files Updated:**
+- `apps/web/src/utils/beadDiff.ts` - Now re-exports from abacus-react
+- `apps/web/src/utils/abacusInstructionGenerator.ts` - Now re-exports from abacus-react
+- `apps/web/src/components/tutorial/TutorialPlayer.tsx` - Imports `calculateBeadDiffFromValues` from abacus-react
+- `apps/web/src/components/tutorial/TutorialEditor.tsx` - Imports `calculateBeadDiffFromValues` from abacus-react
+
+**Exports from abacus-react:**
+- `numberToAbacusState()`
+- `abacusStateToNumber()`
+- `calculateBeadChanges()`
+- `calculateBeadDiff()`
+- `calculateBeadDiffFromValues()`
+- `validateAbacusValue()`
+- `areStatesEqual()`
+
+**Code Eliminated:** ~200+ lines of duplicate utility implementations
+
+---
+
+#### 4. **React Hooks**
+**Status:** ✅ Exported and ready to use
+
+**Available Hooks:**
+- `useAbacusDiff(fromValue, toValue, maxPlaces)` - Memoized bead diff calculation
+- `useAbacusState(value, maxPlaces)` - Memoized state conversion
+
+**Not yet used in app** (available for future tutorials)
+
+---
+
+#### 5. **Column Highlighting**
+**Status:** ✅ Implemented, not yet used
+
+**New Props:**
+- `highlightColumns?: number[]` - Highlight specific columns
+- `columnLabels?: string[]` - Add educational labels above columns
+
+**Usage Example:**
+```tsx
+
+```
+
+---
+
+### Code Deduplication Summary
+
+**Total Lines Eliminated:** ~260-300 lines
+
+**Breakdown:**
+- Theme style definitions: ~60 lines
+- Utility function implementations: ~200 lines
+- Custom styles for inline abacus: ~5-10 lines per usage
+
+---
+
+### Remaining Work (Optional Future Enhancements)
+
+1. Use `highlightColumns` and `columnLabels` in tutorial components
+2. Replace manual bead diff calculations with `useAbacusDiff` hook in interactive tutorials
+3. Use `useAbacusState` for state inspection in debugging/development tools
+4. Consider implementing `frameVisible` toggles in settings pages
+
+---
+
+### Files Modified
+
+**packages/abacus-react:**
+- `src/AbacusReact.tsx` - Added new props (frameVisible, compact, highlightColumns, columnLabels)
+- `src/AbacusThemes.ts` - **NEW FILE** - 6 theme presets
+- `src/AbacusUtils.ts` - **NEW FILE** - Core utility functions
+- `src/AbacusHooks.ts` - **NEW FILE** - React hooks
+- `src/index.ts` - Updated exports
+- `src/AbacusReact.themes-and-utilities.stories.tsx` - **NEW FILE** - Storybook demos
+- `README.md` - Updated with new features documentation
+- `ENHANCEMENT_PLAN.md` - Updated with completion status
+
+**apps/web:**
+- `src/components/MyAbacus.tsx` - Using ABACUS_THEMES
+- `src/components/HeroAbacus.tsx` - Using ABACUS_THEMES
+- `src/components/LevelSliderDisplay.tsx` - Using ABACUS_THEMES
+- `src/app/arcade/complement-race/components/AbacusTarget.tsx` - Using compact prop
+- `src/components/tutorial/TutorialPlayer.tsx` - Importing from abacus-react
+- `src/components/tutorial/TutorialEditor.tsx` - Importing from abacus-react
+- `src/utils/beadDiff.ts` - Re-exports from abacus-react
+- `src/utils/abacusInstructionGenerator.ts` - Re-exports from abacus-react
+
+---
+
+### Testing
+
+✅ Build successful for packages/abacus-react
+✅ TypeScript compilation passes for integrated files
+✅ Runtime tests confirm functions work correctly
+✅ Storybook stories demonstrate all new features
+
+---
+
+### Next Steps
+
+1. Monitor app for any runtime issues with the new integrations
+2. Consider using hooks in future tutorial implementations
+3. Explore using column highlighting in educational content
+4. Document best practices for theme usage in the app
diff --git a/packages/abacus-react/README.md b/packages/abacus-react/README.md
index 57c28c51..50bda30c 100644
--- a/packages/abacus-react/README.md
+++ b/packages/abacus-react/README.md
@@ -86,34 +86,98 @@ Personalized colors and highlights
/>
```
+### Theme Presets
+
+Use pre-defined themes for quick styling:
+
+```tsx
+import { AbacusReact, ABACUS_THEMES } from '@soroban/abacus-react';
+
+// Available themes: 'light', 'dark', 'trophy', 'translucent', 'solid', 'traditional'
+
+
+
+
+
+```
+
+**Available Themes:**
+- `light` - Solid white frame with subtle gray accents (best for light backgrounds)
+- `dark` - Translucent white with subtle glow (best for dark backgrounds)
+- `trophy` - Golden frame with warm tones (best for achievements/rewards)
+- `translucent` - Nearly invisible frame (best for inline/minimal UI)
+- `solid` - Black frame (best for high contrast/educational contexts)
+- `traditional` - Brown wooden appearance (best for traditional soroban aesthetic)
+
+### Compact/Inline Display
+
+Create mini abacus displays for inline use:
+
+```tsx
+// Compact mode - automatically hides frame and optimizes spacing
+
+
+// Or manually control frame visibility
+
+```
+
### Tutorial System
-Educational guidance with tooltips
+Educational guidance with tooltips and column highlighting
```tsx
Click this bead!,
+ target: { type: 'bead', columnIndex: 1, beadType: 'earth', beadPosition: 1 },
+ content: Click this bead in the tens column!
,
offset: { x: 0, y: -30 }
}]}
callbacks={{
onBeadClick: (event) => {
- if (event.columnIndex === 0 && event.beadType === 'earth' && event.position === 1) {
- console.log('Correct!');
+ if (event.columnIndex === 1 && event.beadType === 'earth' && event.position === 1) {
+ console.log('Correct! You clicked the tens column.');
}
}
}}
/>
```
+**Column Highlighting:**
+- `highlightColumns` - Array of column indices to highlight (e.g., `[0, 2]` highlights first and third columns)
+- `columnLabels` - Optional labels displayed above each column (indexed left to right)
+
## 3D Enhancement
Make the abacus feel tangible and satisfying with three progressive levels of 3D effects.
@@ -209,10 +273,18 @@ interface AbacusConfig {
colorPalette?: 'default' | 'colorblind' | 'mnemonic' | 'grayscale' | 'nature';
hideInactiveBeads?: boolean; // Hide/show inactive beads
+ // Layout & Frame
+ frameVisible?: boolean; // Show/hide column posts and reckoning bar
+ compact?: boolean; // Compact layout (implies frameVisible=false)
+
// Interaction
interactive?: boolean; // Enable user interactions
animated?: boolean; // Enable animations
gestures?: boolean; // Enable drag gestures
+
+ // Tutorial Features
+ highlightColumns?: number[]; // Highlight specific columns by index
+ columnLabels?: string[]; // Optional labels for columns
}
```
@@ -359,6 +431,60 @@ function AdvancedExample() {
## Hooks
+### useAbacusDiff
+
+Calculate bead differences between values for tutorials and animations:
+
+```tsx
+import { useAbacusDiff } from '@soroban/abacus-react';
+
+function Tutorial() {
+ const [currentValue, setCurrentValue] = useState(5);
+ const targetValue = 15;
+
+ // Get diff information: which beads need to move
+ const diff = useAbacusDiff(currentValue, targetValue);
+
+ return (
+
+
{diff.summary}
{/* "add heaven bead in tens column, then..." */}
+
+
Changes needed: {diff.changes.length}
+
+ );
+}
+```
+
+**Returns:**
+- `changes` - Array of bead movements with direction and order
+- `highlights` - Bead highlight data for stepBeadHighlights prop
+- `hasChanges` - Boolean indicating if any changes needed
+- `summary` - Human-readable description of changes (e.g., "add heaven bead in ones column")
+
+### useAbacusState
+
+Convert numbers to abacus bead states:
+
+```tsx
+import { useAbacusState } from '@soroban/abacus-react';
+
+function BeadAnalyzer() {
+ const value = 123;
+ const state = useAbacusState(value);
+
+ // Check bead positions
+ const onesHasHeaven = state[0].heavenActive; // false (3 < 5)
+ const tensEarthCount = state[1].earthActive; // 2 (20 = 2 tens)
+
+ return Ones column heaven bead: {onesHasHeaven ? 'active' : 'inactive'}
;
+}
+```
+
### useAbacusDimensions
Get exact sizing information for layout planning:
@@ -377,6 +503,92 @@ function MyComponent() {
}
```
+## Utility Functions
+
+Low-level functions for working with abacus states and calculations:
+
+### numberToAbacusState
+
+Convert a number to bead positions:
+
+```tsx
+import { numberToAbacusState } from '@soroban/abacus-react';
+
+const state = numberToAbacusState(123, 5); // 5 columns
+// Returns: {
+// 0: { heavenActive: false, earthActive: 3 }, // ones = 3
+// 1: { heavenActive: false, earthActive: 2 }, // tens = 2
+// 2: { heavenActive: true, earthActive: 0 }, // hundreds = 1
+// ...
+// }
+```
+
+### abacusStateToNumber
+
+Convert bead positions back to a number:
+
+```tsx
+import { abacusStateToNumber } from '@soroban/abacus-react';
+
+const state = {
+ 0: { heavenActive: false, earthActive: 3 },
+ 1: { heavenActive: false, earthActive: 2 },
+ 2: { heavenActive: true, earthActive: 0 }
+};
+
+const value = abacusStateToNumber(state); // 123
+```
+
+### calculateBeadDiff
+
+Calculate the exact bead movements needed between two states:
+
+```tsx
+import { calculateBeadDiff, numberToAbacusState } from '@soroban/abacus-react';
+
+const fromState = numberToAbacusState(5);
+const toState = numberToAbacusState(15);
+const diff = calculateBeadDiff(fromState, toState);
+
+console.log(diff.summary); // "add heaven bead in tens column"
+console.log(diff.changes); // Detailed array of movements with order
+```
+
+### calculateBeadDiffFromValues
+
+Convenience wrapper for calculating diff from numbers:
+
+```tsx
+import { calculateBeadDiffFromValues } from '@soroban/abacus-react';
+
+const diff = calculateBeadDiffFromValues(42, 57);
+// Equivalent to: calculateBeadDiff(numberToAbacusState(42), numberToAbacusState(57))
+```
+
+### validateAbacusValue
+
+Check if a value is within the supported range:
+
+```tsx
+import { validateAbacusValue } from '@soroban/abacus-react';
+
+const result = validateAbacusValue(123456, 5); // 5 columns max
+console.log(result.isValid); // false
+console.log(result.error); // "Value exceeds maximum for 5 columns (max: 99999)"
+```
+
+### areStatesEqual
+
+Compare two abacus states:
+
+```tsx
+import { areStatesEqual, numberToAbacusState } from '@soroban/abacus-react';
+
+const state1 = numberToAbacusState(123);
+const state2 = numberToAbacusState(123);
+const isEqual = areStatesEqual(state1, state2); // true
+```
+
## Educational Use Cases
### Interactive Math Lessons
@@ -439,14 +651,37 @@ Full TypeScript definitions included:
```tsx
import {
+ // Components
AbacusReact,
+
+ // Hooks
+ useAbacusDiff,
+ useAbacusState,
+ useAbacusDimensions,
+
+ // Utility Functions
+ numberToAbacusState,
+ abacusStateToNumber,
+ calculateBeadDiff,
+ calculateBeadDiffFromValues,
+ validateAbacusValue,
+ areStatesEqual,
+
+ // Theme Presets
+ ABACUS_THEMES,
+
+ // Types
AbacusConfig,
BeadConfig,
BeadClickEvent,
AbacusCustomStyles,
AbacusOverlay,
AbacusCallbacks,
- useAbacusDimensions
+ AbacusState,
+ BeadState,
+ BeadDiffResult,
+ BeadDiffOutput,
+ AbacusThemeName
} from '@soroban/abacus-react';
// All interfaces fully typed for excellent developer experience