diff --git a/.Jules/palette.md b/.Jules/palette.md
index 697428c..89d1fdc 100644
--- a/.Jules/palette.md
+++ b/.Jules/palette.md
@@ -107,3 +107,7 @@
## 2025-06-25 - [Copy-to-Clipboard UX and Immediate Feedback]
**Learning:** Adding a copy-to-clipboard feature for key user identifiers (like email or IDs) significantly reduces friction in user workflows. Implementing this with `navigator.clipboard.writeText` and providing immediate visual feedback via a toast notification ('Correo copiado al portapapeles') confirms success. Using icon-only buttons with explicit `aria-label` and `title` ensures the feature is both accessible and intuitive.
**Action:** Always provide a copy-to-clipboard option for static text that users frequently need to reuse, and ensure immediate feedback is provided upon action.
+
+## 2025-01-15 - [Keyboard Efficiency and Discoverability in Flashcards]
+**Learning:** For high-repetition tasks like flashcard study, keyboard shortcuts (Space/Enter for flip, 1-4 for rating) dramatically improve user flow and "flow state" immersion. However, shortcuts must be discoverable; adding bracketed visual hints like "[Espacio]" or "[1]" directly to button labels, along with descriptive "aria-label" and "title" attributes, ensures accessibility and lowers the learning curve for power users.
+**Action:** Always include keyboard shortcuts for core repetitive loops and provide integrated visual hints for those shortcuts to ensure they aren't "hidden" features.
diff --git a/src/v2/pages/V2FlashcardStudy.jsx b/src/v2/pages/V2FlashcardStudy.jsx
index c21524a..8d93873 100644
--- a/src/v2/pages/V2FlashcardStudy.jsx
+++ b/src/v2/pages/V2FlashcardStudy.jsx
@@ -236,10 +236,10 @@ const Flashcard = ({ card, isFlipped, onFlip }) => (
// Quality rating buttons (SM-2 mapped to UI)
const QualityButtons = ({ onRate, disabled }) => {
const buttons = [
- { quality: 1, label: 'Otra vez', sublabel: '< 1 día', color: '#ba1a1a', icon: 'replay' },
- { quality: 3, label: 'Difícil', sublabel: '2-3 días', color: '#9c4247', icon: 'sentiment_dissatisfied' },
- { quality: 4, label: 'Bien', sublabel: '4-6 días', color: '#0fa397', icon: 'sentiment_satisfied' },
- { quality: 5, label: 'Fácil', sublabel: '7+ días', color: '#4a6360', icon: 'sentiment_very_satisfied' }
+ { quality: 1, label: 'Otra vez', shortcut: '1', sublabel: '< 1 día', color: '#ba1a1a', icon: 'replay' },
+ { quality: 3, label: 'Difícil', shortcut: '2', sublabel: '2-3 días', color: '#9c4247', icon: 'sentiment_dissatisfied' },
+ { quality: 4, label: 'Bien', shortcut: '3 / Espacio', sublabel: '4-6 días', color: '#0fa397', icon: 'sentiment_satisfied' },
+ { quality: 5, label: 'Fácil', shortcut: '4', sublabel: '7+ días', color: '#4a6360', icon: 'sentiment_very_satisfied' }
];
return (
@@ -255,13 +255,14 @@ const QualityButtons = ({ onRate, disabled }) => {
opacity: disabled ? 0.5 : 1,
border: 'none'
}}
- aria-label={`Calificar como ${btn.label}`}
+ aria-label={`Calificar como ${btn.label} (atajo: tecla ${btn.shortcut})`}
+ title={`Atajo: ${btn.shortcut}`}
>
{btn.icon}
- {btn.label}
+ {btn.label} [{btn.quality === 4 ? '3' : btn.shortcut}]
{btn.sublabel}
@@ -400,6 +401,37 @@ const V2FlashcardStudy = () => {
const handleGoHome = useCallback(() => {
history.push('/dashboard');
}, [history]);
+
+ // Keyboard shortcuts
+ useEffect(() => {
+ const handleKeyDown = (e) => {
+ // Don't trigger if any overlay is active or if user is typing (though no inputs here yet)
+ if (loading || isSubmitting || isSessionComplete || !currentCard) return;
+
+ const { key } = e;
+
+ if (!isFlipped) {
+ if (key === ' ' || key === 'Enter') {
+ e.preventDefault();
+ handleFlip();
+ }
+ } else {
+ if (key === '1') {
+ handleRate(1);
+ } else if (key === '2') {
+ handleRate(3);
+ } else if (key === '3' || key === ' ' || key === 'Enter') {
+ e.preventDefault();
+ handleRate(4);
+ } else if (key === '4') {
+ handleRate(5);
+ }
+ }
+ };
+
+ window.addEventListener('keydown', handleKeyDown);
+ return () => window.removeEventListener('keydown', handleKeyDown);
+ }, [loading, isSubmitting, isSessionComplete, currentCard, isFlipped, handleFlip, handleRate]);
// Loading state
if (loading) {
@@ -501,9 +533,11 @@ const V2FlashcardStudy = () => {
)}
diff --git a/src/v2/pages/V2FlashcardStudy.test.jsx b/src/v2/pages/V2FlashcardStudy.test.jsx
index 75fed30..0d370a2 100644
--- a/src/v2/pages/V2FlashcardStudy.test.jsx
+++ b/src/v2/pages/V2FlashcardStudy.test.jsx
@@ -143,4 +143,37 @@ describe('V2FlashcardStudy', () => {
expect(screen.getByText('¡Todo al día!')).toBeTruthy();
});
});
+
+ it('handles keyboard shortcuts for flipping and rating', async () => {
+ render(
+
+
+
+ );
+
+ await waitFor(() => screen.getByText('¿Cuál es la tríada de Virchow?'));
+
+ // Press Space to flip
+ fireEvent.keyDown(window, { key: ' ' });
+ await waitFor(() => {
+ expect(screen.getByText(/Estasis venosa/)).toBeTruthy();
+ });
+
+ // Press '4' to rate as Easy (Quality 5)
+ fireEvent.keyDown(window, { key: '4' });
+ await waitFor(() => {
+ expect(screen.getByText('Agente causal más común de epiglotitis')).toBeTruthy();
+ });
+
+ // Press ' ' to rate as Good (Quality 4) - after flipping
+ fireEvent.keyDown(window, { key: ' ' }); // Flip second card
+ await waitFor(() => {
+ expect(screen.getByText(/Haemophilus influenzae/)).toBeTruthy();
+ });
+
+ fireEvent.keyDown(window, { key: ' ' }); // Rate as Good
+ await waitFor(() => {
+ expect(screen.getByText('Signo de Murphy positivo indica...')).toBeTruthy();
+ });
+ });
});