diff --git a/_specs/mobile-horizontal-layout.md b/_specs/mobile-horizontal-layout.md new file mode 100644 index 0000000..f4a75cc --- /dev/null +++ b/_specs/mobile-horizontal-layout.md @@ -0,0 +1,53 @@ +# Spec for mobile-horizontal-layout + +branch: claude/feature/mobile-horizontal-layout +figma_component (if used): N/A + +## Summary + +When a device screen is less than 1024px wide and in landscape (horizontal) orientation, the `FlashcardSession` view should switch from its current vertical layout to a compact 3-column layout: controls on the left, video in the center, and the term list on the right. The header (`flashcard-session-header` div) should be hidden in this mode to reclaim vertical space. + +## Functional Requirements + +- Detect when the viewport width is less than 1024px AND the device is in landscape (horizontal) orientation. +- In that state, hide the `flashcard-session-header` div entirely. +- Switch the session layout to a 3-column arrangement in this order: controls column | video column | term-list column. +- Revert to the default layout when the screen is wider than 1024px or switches back to portrait orientation. +- The layout change must respond dynamically as the user rotates the device (no page reload required). +- Each column should be appropriately sized so all three are visible without horizontal scrolling. +- The video column should take the most space, with controls and term list given narrower fixed or proportional widths. + +## Possible Edge Cases + +- Device rotates while a card is being shown — the active card and playback state should be preserved. +- Very narrow landscape screens (e.g. older phones) where 3 columns may still be tight — columns should not overflow; use overflow hidden or scroll per column as appropriate. +- User resizes a desktop browser window to below 1024px while in a non-landscape orientation — the 3-column layout should NOT activate; it requires both conditions simultaneously. +- Term list with many items in the narrow right column — the column should scroll independently. + +## Acceptance Criteria + +- [ ] On a device or browser window narrower than 1024px in landscape orientation, the `flashcard-session-header` div is not visible. +- [ ] On a device or browser window narrower than 1024px in landscape orientation, the session displays three side-by-side columns: controls | video | term list. +- [ ] Rotating back to portrait (or resizing above 1024px) restores the original layout and re-shows the header. +- [ ] No horizontal scrollbar appears in the 3-column layout. +- [ ] Active flashcard state is preserved through orientation changes. +- [ ] The 3-column layout does not activate on a portrait screen narrower than 1024px. + +## Open Questions + +- Should the controls column show all existing controls (navigation buttons, color picker, etc.) or a reduced set to save space? + - reduces set to save space +- Should the term list column be scrollable independently or clipped? + - independently +- Is there a minimum height required before the 3-column layout kicks in, in addition to the width and orientation check? + - Optimize so the video is the largest it can be + +## Testing Guidelines + +Create a test file(s) in the ./tests folder for the new feature, and create meaningful tests for the following cases, without going too heavy: + +- Verify that the `flashcard-session-header` is hidden when width < 1024px and orientation is landscape. +- Verify that the `flashcard-session-header` is visible when width >= 1024px regardless of orientation. +- Verify that the `flashcard-session-header` is visible when orientation is portrait and width < 1024px. +- Verify that the 3-column layout class or style is applied under the mobile-landscape condition. +- Verify that the default layout is restored when orientation changes back to portrait. diff --git a/src/App.css b/src/App.css index 1241c7b..c7aea02 100644 --- a/src/App.css +++ b/src/App.css @@ -607,6 +607,75 @@ } } +/* ── Mobile landscape 3-column layout ────────────────── */ + +.fcs-landscape { + display: grid; + grid-template-columns: 120px 1fr 140px; + gap: 8px; + flex-grow: 1; + min-height: 0; + overflow: hidden; +} + +.fcs-landscape__controls { + display: flex; + flex-direction: column; + gap: 8px; + overflow: hidden; +} + +.fcs-landscape__controls .flashcard-card { + flex: 1 1 0; + min-height: 40px; + max-width: 100%; + border-radius: 10px; + padding: 8px; +} + +.fcs-landscape__controls .flashcard-term { + font-size: clamp(14px, 3vw, 24px); +} + +.fcs-landscape__video { + display: flex; + align-items: stretch; + overflow: hidden; +} + +.fcs-landscape__video .flashcard-video { + width: 100%; + max-width: 100%; + aspect-ratio: unset; + flex: 1 1 0; + min-height: 0; +} + +.fcs-landscape__termlist { + overflow-y: auto; + overflow-x: hidden; + display: flex; + flex-direction: column; +} + +.fcs-landscape__termlist .term-select { + flex-grow: 1; + overflow-y: auto; + font-size: 13px; +} + +.fcs-landscape__nav { + display: flex; + flex-direction: column; + gap: 6px; +} + +.fcs-landscape__nav .btn-nav { + padding: 6px 10px; + font-size: 13px; + width: 100%; +} + /* ── App banner ───────────────────────────────────────── */ .app-banner { diff --git a/src/components/FlashcardSession.jsx b/src/components/FlashcardSession.jsx index ecbfca2..6128c12 100644 --- a/src/components/FlashcardSession.jsx +++ b/src/components/FlashcardSession.jsx @@ -9,8 +9,24 @@ export function FlashcardSession({terms, cardColors, onBack, title, description} const [localTerms, setLocalTerms] = useState(terms); const [localColors, setLocalColors] = useState(cardColors); const [termDrawerOpen, setTermDrawerOpen] = useState(false); + const [isMobileHorizontal, setIsMobileHorizontal] = useState(false); const selectRef = useRef(null); + useEffect(() => { + function check() { + setIsMobileHorizontal( + window.innerWidth < 1024 && window.matchMedia('(orientation: landscape)').matches + ); + } + check(); + window.addEventListener('resize', check); + window.addEventListener('orientationchange', check); + return () => { + window.removeEventListener('resize', check); + window.removeEventListener('orientationchange', check); + }; + }, []); + const goNext = useCallback(() => { setCurrentIndex(i => (i + 1) % localTerms.length); }, [localTerms.length]); @@ -72,6 +88,63 @@ export function FlashcardSession({terms, cardColors, onBack, title, description} const fg = contrastColor(bg); const playbackUrl = getPlaybackUrl(); + if (isMobileHorizontal) { + return ( +
{currentIndex + 1} / {localTerms.length}
+