From 0ebb3ea0703cd2e5917333814b49d3dac57c4c3e Mon Sep 17 00:00:00 2001 From: clacina Date: Tue, 28 Apr 2026 09:03:00 -0700 Subject: [PATCH 1/3] =?UTF-8?q?=E2=9C=A8=20feat:=20redesign=20category=20s?= =?UTF-8?q?elector=20and=20align=20session=20header=20over=20card?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a two-column layout to the category selector with an ASL resources panel, emoji icons and Tippy tooltips on each category button, and moves the session title/description inside the content column so it aligns with the flashcard card beneath it. --- package.json | 4 +- src/App.css | 65 ++++++++++++++++++++++++++--- src/components/FlashcardSession.jsx | 14 +++---- src/components/TermInput.jsx | 37 +++++++++++----- yarn.lock | 19 +++++++++ 5 files changed, 113 insertions(+), 26 deletions(-) diff --git a/package.json b/package.json index a0d9e6e..333f6a7 100644 --- a/package.json +++ b/package.json @@ -12,9 +12,11 @@ "test:ui": "vitest --ui" }, "dependencies": { + "@tippyjs/react": "^4.2.6", "axios": "^1.15.0", "react": "^19.2.4", - "react-dom": "^19.2.4" + "react-dom": "^19.2.4", + "tippy.js": "^6.3.7" }, "devDependencies": { "@eslint/js": "^9.39.4", diff --git a/src/App.css b/src/App.css index 23d3eba..8520808 100644 --- a/src/App.css +++ b/src/App.css @@ -56,7 +56,7 @@ .term-input__body { display: grid; - grid-template-columns: 220px 1fr; + grid-template-columns: 1fr 1fr; gap: 40px; align-items: start; flex-grow: 1; @@ -69,9 +69,50 @@ /* ── Category buttons ─────────────────────────────────── */ .term-input__categories { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; + + @media (max-width: 640px) { + grid-template-columns: 1fr; + } +} + +/* ── Resources panel ──────────────────────────────────── */ + +.term-input__resources { + padding: 20px 24px; + background: var(--code-bg); + border: 1px solid var(--border); + border-radius: 12px; +} + +.term-input__resources-heading { + margin: 0 0 14px; + font-size: 15px; + font-weight: 700; + color: var(--text-h); + letter-spacing: 0.02em; +} + +.term-input__resources-list { + margin: 0; + padding: 0; + list-style: none; display: flex; flex-direction: column; - gap: 12px; + gap: 10px; + + a { + color: var(--accent); + text-decoration: none; + font-size: 14px; + line-height: 1.4; + + &:hover { + text-decoration: underline; + } + } } .btn-category { @@ -85,8 +126,17 @@ font-weight: 600; text-align: left; cursor: pointer; + display: flex; + align-items: center; + gap: 10px; transition: border-color 0.2s, box-shadow 0.2s, background-color 0.2s; + .btn-category__icon { + font-size: 20px; + line-height: 1; + flex-shrink: 0; + } + &:hover { background: var(--accent-bg); border-color: var(--accent-border); @@ -191,8 +241,11 @@ .flashcard-session-header { display: flex; - align-items: flex-start; - gap: 16px; + flex-direction: column; + gap: 4px; + text-align: center; + width: 100%; + max-width: 560px; } .flashcard-session-title { @@ -258,8 +311,8 @@ align-items: center; justify-content: center; width: 100%; + max-width: 560px; border-radius: 16px; - padding: 24px 32px; box-sizing: border-box; text-align: center; transition: background-color 0.25s ease; @@ -271,7 +324,7 @@ letter-spacing: -0.02em; overflow-wrap: break-word; word-break: break-word; - line-height: 1.6; + line-height: 1; /*overflow: hidden;*/ max-height: 100%; } diff --git a/src/components/FlashcardSession.jsx b/src/components/FlashcardSession.jsx index da6238a..4233f73 100644 --- a/src/components/FlashcardSession.jsx +++ b/src/components/FlashcardSession.jsx @@ -61,17 +61,13 @@ export function FlashcardSession({terms, cardColors, onBack, title, description} return (
-
- -
-

{title}

-

{description}

-
-
+
+
+

{title}

+

{description}

+
{localTerms[currentIndex].term}
diff --git a/src/components/TermInput.jsx b/src/components/TermInput.jsx index fbc5dde..76c0c68 100644 --- a/src/components/TermInput.jsx +++ b/src/components/TermInput.jsx @@ -1,4 +1,6 @@ import {useState, useEffect} from "react"; +import Tippy from "@tippyjs/react"; +import "tippy.js/dist/tippy.css"; import terms from "../data/terms.json" with {type: "json"}; import other_terms from "../data/terms2.json" with {type: "json"}; @@ -7,13 +9,13 @@ import verbs from "../data/verbs.json" with {type: "json"}; import axios from "axios"; const CATEGORIES = [ - {title: "Finger Spelling", description: "Practice spelling words letter by letter using ASL handshapes.", terms: terms}, - {title: "Numbers", description: "Learn to sign numbers in American Sign Language.", terms: terms}, - {title: "ASL Level I && II Class Terms", description: "Core vocabulary from ASL Level I and II coursework.", terms: terms}, - {title: "Questions", description: "Essential question words and phrases used in ASL conversation.", terms: questions}, - {title: "Verbs", description: "Common action words and verbs in American Sign Language.", terms: verbs}, - {title: "Other Terms", description: "Additional vocabulary terms for expanding your ASL knowledge.", terms: other_terms}, - {title: "Renaissance Faire Terms", description: "Specialized vocabulary for Renaissance Faire settings.", terms: terms}, + {icon: "🖐️", title: "Finger Spelling", description: "Practice spelling words letter by letter using ASL handshapes.", terms: terms}, + {icon: "🔢", title: "Numbers", description: "Learn to sign numbers in American Sign Language.", terms: terms}, + {icon: "📚", title: "ASL Level I && II Class Terms", description: "Core vocabulary from ASL Level I and II coursework.", terms: terms}, + {icon: "❓", title: "Questions", description: "Essential question words and phrases used in ASL conversation.", terms: questions}, + {icon: "⚡", title: "Verbs", description: "Common action words and verbs in American Sign Language.", terms: verbs}, + {icon: "🗂️", title: "Other Terms", description: "Additional vocabulary terms for expanding your ASL knowledge.", terms: other_terms}, + {icon: "⚔️", title: "Renaissance Faire Terms", description: "Specialized vocabulary for Renaissance Faire settings.", terms: terms}, ]; const webResources = [ @@ -78,12 +80,27 @@ export function TermInput({onStart}) {
{CATEGORIES.map(category => ( - + + + ))} {error &&

{error}

}
+
+

ASL Resources

+ +
); diff --git a/yarn.lock b/yarn.lock index 468cae5..f72f62e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -380,6 +380,11 @@ resolved "https://registry.yarnpkg.com/@oxc-project/types/-/types-0.124.0.tgz#1dfd7b3fbb98febc2f91b505f48c940db73c8701" integrity sha512-VBFWMTBvHxS11Z5Lvlr3IWgrwhMTXV+Md+EQF0Xf60+wAdsGFTBx7X7K/hP4pi8N7dcm1RvcHwDxZ16Qx8keUg== +"@popperjs/core@^2.9.0": + version "2.11.8" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.8.tgz#6b79032e760a0899cd4204710beede972a3a185f" + integrity sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A== + "@rolldown/binding-android-arm64@1.0.0-rc.15": version "1.0.0-rc.15" resolved "https://registry.yarnpkg.com/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.15.tgz#ca20574c469ade7b941f90c9af5e83e7c67f06b7" @@ -507,6 +512,13 @@ dependencies: "@babel/runtime" "^7.12.5" +"@tippyjs/react@^4.2.6": + version "4.2.6" + resolved "https://registry.yarnpkg.com/@tippyjs/react/-/react-4.2.6.tgz#971677a599bf663f20bb1c60a62b9555b749cc71" + integrity sha512-91RicDR+H7oDSyPycI13q3b7o4O60wa2oRbjlz2fyRLmHImc4vyDwuUP8NtZaN0VARJY5hybvDYrFzhY9+Lbyw== + dependencies: + tippy.js "^6.3.1" + "@tybys/wasm-util@^0.10.1": version "0.10.1" resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414" @@ -1761,6 +1773,13 @@ tinyrainbow@^3.1.0: resolved "https://registry.yarnpkg.com/tinyrainbow/-/tinyrainbow-3.1.0.tgz#1d8a623893f95cf0a2ddb9e5d11150e191409421" integrity sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw== +tippy.js@^6.3.1, tippy.js@^6.3.7: + version "6.3.7" + resolved "https://registry.yarnpkg.com/tippy.js/-/tippy.js-6.3.7.tgz#8ccfb651d642010ed9a32ff29b0e9e19c5b8c61c" + integrity sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ== + dependencies: + "@popperjs/core" "^2.9.0" + tldts-core@^7.0.28: version "7.0.28" resolved "https://registry.yarnpkg.com/tldts-core/-/tldts-core-7.0.28.tgz#28c256edae2ed177b2a8338a51caf81d41580ecf" From 4c7e1acdbe48c1afbfd57ef525efadf8c8bd947e Mon Sep 17 00:00:00 2001 From: clacina Date: Tue, 28 Apr 2026 09:07:41 -0700 Subject: [PATCH 2/3] planning mobile layout --- _plans/adjust-for-mobile.txt | 1 + 1 file changed, 1 insertion(+) create mode 100644 _plans/adjust-for-mobile.txt diff --git a/_plans/adjust-for-mobile.txt b/_plans/adjust-for-mobile.txt new file mode 100644 index 0000000..efab492 --- /dev/null +++ b/_plans/adjust-for-mobile.txt @@ -0,0 +1 @@ +Adjust the contents of the app so it is responsive and will display well on phones and tablets. Ensure the video is always visible and hide the other controls as necessary. Shrink controls where possible but leave the video playback as the main feature. \ No newline at end of file From d5746c976bbb896940303e48350317a567bd415f Mon Sep 17 00:00:00 2001 From: clacina Date: Tue, 28 Apr 2026 09:17:43 -0700 Subject: [PATCH 3/3] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20mobile-responsive?= =?UTF-8?q?=20layout=20with=20bottom-sheet=20term=20drawer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Makes the flashcard session and category selector usable on phones and tablets. The term list becomes a bottom-sheet drawer on mobile (toggled by a "📋 Terms" button), the session header hides to maximize video space, and all controls stay reachable within 44px touch targets. --- _specs/mobile-responsive-layout.md | 48 ++++++++++ src/App.css | 142 +++++++++++++++++++++++++++- src/components/FlashcardSession.jsx | 33 ++++--- 3 files changed, 208 insertions(+), 15 deletions(-) create mode 100644 _specs/mobile-responsive-layout.md diff --git a/_specs/mobile-responsive-layout.md b/_specs/mobile-responsive-layout.md new file mode 100644 index 0000000..4c7be2b --- /dev/null +++ b/_specs/mobile-responsive-layout.md @@ -0,0 +1,48 @@ +# Spec for mobile-responsive-layout + +branch: claude/feature/mobile-responsive-layout + +## Summary +Make the entire app responsive so it displays well on phones and tablets. The video playback is the primary feature and must always be visible. Secondary controls (navigation, term list, shuffle, etc.) should be shrunk or hidden as needed to prioritize the video on smaller screens. + +## Functional Requirements +- The flashcard session screen must display the video at a usable size on all screen sizes down to 375px wide (iPhone SE) +- The term selector list should be hidden on mobile and accessible via a collapsible/toggle control +- Navigation buttons (Prev, Next, Shuffle) should be visible but compact on mobile +- The position indicator (e.g. "3 / 45") should remain visible on mobile +- The Back button should remain accessible on all screen sizes +- The session title and description should be visible but may be reduced in font size on mobile +- The category selector (TermInput) layout should stack to a single column on mobile, with the resources panel below the categories +- The ASL resources panel can be collapsed or hidden on mobile to save space +- On tablets (768px–1024px), a two-column layout may be preserved where it fits; fall back gracefully where it does not +- Touch targets (buttons) must be at least 44×44px on mobile per accessibility guidelines + +## Possible Edge Cases +- Very long term names may overflow the flashcard card on narrow screens +- The video iframe aspect ratio must be preserved and never clipped on any viewport +- The term selector list, when toggled open on mobile, should not push the video off screen +- Category button grid may have an odd number of items — the last cell must not stretch awkwardly + +## Acceptance Criteria +- Loading the app on a 375px-wide viewport shows the flashcard video without horizontal scroll +- The video occupies the majority of the vertical space on mobile in the session view +- The term list is hidden by default on mobile and can be revealed via a clearly labeled toggle +- All interactive controls remain reachable and meet 44px minimum touch target size +- No horizontal overflow at 375px, 768px, or 1024px viewport widths +- The category selector page is usable on mobile with no overlapping or clipped elements +- Tablet layout (768px) makes sensible use of the available horizontal space + +## Open Questions +- Should the term list toggle be a drawer/overlay or an inline expand on mobile? + - drawer/overlay +- Should the session title/description be hidden entirely on mobile to maximize video space, or just reduced? + - hidden entirely +- Is there a preferred breakpoint for "tablet" behavior, or should we use 640px and 1024px as the two breakpoints? + - use 640px and 1024px as the two breakpoints. + +## 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: +- TermInput renders in single-column layout when viewport width is below the mobile breakpoint +- FlashcardSession renders the video element at all viewport sizes +- Term selector toggle button is present in the DOM on mobile viewports +- No critical UI elements are hidden on mobile that should remain accessible (Back button, nav controls) diff --git a/src/App.css b/src/App.css index 8520808..af88c49 100644 --- a/src/App.css +++ b/src/App.css @@ -450,21 +450,155 @@ } } -/* ── Term select ──────────────────────────────────────── */ +/* ── Term drawer (sidebar on desktop, overlay on mobile) ── */ -.term-select { +.term-drawer { flex: 0 0 200px; align-self: stretch; + display: flex; + flex-direction: column; + overflow: hidden; + border: 1px solid var(--border); + border-radius: 8px; + + @media (max-width: 1024px) { + flex: 0 0 160px; + } + + @media (max-width: 640px) { + position: fixed; + bottom: 0; + left: 0; + right: 0; + height: 60vh; + z-index: 200; + display: none; + flex-direction: column; + background: var(--bg); + border: none; + border-top: 1px solid var(--border); + border-radius: 16px 16px 0 0; + } +} + +.term-drawer--open { + @media (max-width: 640px) { + display: flex; + } +} + +.term-drawer__backdrop { + display: none; + + @media (max-width: 640px) { + display: none; + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.4); + z-index: 199; + + .term-drawer--open & { + display: block; + } + } +} + +.term-drawer__panel { + display: flex; + flex-direction: column; + height: 100%; + overflow: hidden; +} + +.term-drawer__header { + display: none; + + @media (max-width: 640px) { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid var(--border); + flex-shrink: 0; + font-size: 15px; + font-weight: 600; + color: var(--text-h); + } +} + +.term-drawer__close { + background: none; + border: none; + font-size: 18px; + cursor: pointer; + color: var(--text); + padding: 4px 8px; + line-height: 1; + + &:hover { + color: var(--text-h); + } +} + +/* ── Term select ──────────────────────────────────────── */ + +.term-select { + flex-grow: 1; overflow-y: auto; background: var(--code-bg); color: var(--text); - border: 1px solid var(--border); - border-radius: 8px; + border: none; + border-radius: 0; font-size: 15px; padding: 4px; cursor: pointer; + width: 100%; option { padding: 4px 8px; } } + +/* ── Mobile responsive overrides ─────────────────────── */ + +@media (max-width: 640px) { + .flashcard-session-body { + flex-direction: column; + gap: 8px; + } + + .flashcard-session-header { + display: none; + } + + .flashcard-video { + max-width: 100%; + } + + .flashcard-card { + padding: 12px 16px; + min-height: 40px; + } + + .flashcard-term { + font-size: clamp(18px, 5vw, 48px); + } + + .btn-nav { + padding: 8px 14px; + } + + .btn-nav--terms { + display: inline-flex; + } +} + +.btn-nav--terms { + display: none; +} + +@media (max-width: 1024px) { + .flashcard-video { + max-width: 100%; + } +} diff --git a/src/components/FlashcardSession.jsx b/src/components/FlashcardSession.jsx index 4233f73..3533b13 100644 --- a/src/components/FlashcardSession.jsx +++ b/src/components/FlashcardSession.jsx @@ -6,6 +6,7 @@ export function FlashcardSession({terms, cardColors, onBack, title, description} const [currentIndex, setCurrentIndex] = useState(0); const [localTerms, setLocalTerms] = useState(terms); const [localColors, setLocalColors] = useState(cardColors); + const [termDrawerOpen, setTermDrawerOpen] = useState(false); const selectRef = useRef(null); const goNext = useCallback(() => { @@ -89,19 +90,29 @@ export function FlashcardSession({terms, cardColors, onBack, title, description} + +
+
+
+
setTermDrawerOpen(false)} /> +
+
+ Select a Term + +
+
-
);