From d46758db58f61a3697f92917226bd18b461a8d56 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 4 May 2026 15:45:38 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=8E=A8=20Palette:=20Add=20keyboard=20shor?= =?UTF-8?q?tcuts=20to=20flashcard=20study?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Implements keyboard shortcuts for faster study flow (Space/Enter to flip, 1-4 to rate). - Adds visual shortcut hints (e.g., [Espacio], [1]) to buttons for discoverability. - Enhances accessibility with descriptive ARIA labels and titles for shortcuts. - Includes unit tests verifying shortcut functionality. - Records UX learnings in palette journal. Co-authored-by: godie <227743+godie@users.noreply.github.com> --- .Jules/palette.md | 4 +++ src/v2/pages/V2FlashcardStudy.jsx | 48 ++++++++++++++++++++++---- src/v2/pages/V2FlashcardStudy.test.jsx | 33 ++++++++++++++++++ 3 files changed, 78 insertions(+), 7 deletions(-) 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.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(); + }); + }); });