diff --git a/frontend/src/components/ImageFigure.css b/frontend/src/components/ImageFigure.css index 4ffd238..286ea66 100644 --- a/frontend/src/components/ImageFigure.css +++ b/frontend/src/components/ImageFigure.css @@ -1,7 +1,14 @@ /* Small until asked for. The caption is the author's own alt text, so what a reader sees under the figure and what a screen reader is told are the same sentence — and there is no second field to keep in step with the first. */ -.imgfig { display: inline-flex; flex-direction: column; gap: 5px; margin: 10px 12px 10px 0; vertical-align: top; max-width: 100%; } +/* `align-items: start` matters. This is a column, so without it every child + stretches to the width of the widest — which is the caption — and the + thumbnail's bordered box grew to 260px around a portrait image half that + wide, leaving a white panel beside the picture inside its own frame. */ +.imgfig { + display: inline-flex; flex-direction: column; align-items: start; + gap: 5px; margin: 10px 12px 10px 0; vertical-align: top; max-width: 100%; +} .imgfig-thumb { display: block; padding: 0; cursor: zoom-in; line-height: 0; border: 1px solid var(--border); border-radius: 8px; overflow: hidden; diff --git a/frontend/src/components/ImageFigure.jsx b/frontend/src/components/ImageFigure.jsx index 5cef765..48f995f 100644 --- a/frontend/src/components/ImageFigure.jsx +++ b/frontend/src/components/ImageFigure.jsx @@ -122,7 +122,13 @@ export function ImageViewer({ src, alt = '', attemptId, onClose }) { ) } -export default function ImageFigure({ src, alt = '', attemptId, className = '' }) { +export default function ImageFigure({ src, alt = '', attemptId, className = '', + // Whether the alt text is also printed + // under the thumbnail. On a card it is + // not: the card's own words are the + // caption, and repeating them under the + // picture says the same thing twice. + showLabel = true }) { const [open, setOpen] = useState(false) if (!src) return null @@ -137,7 +143,7 @@ export default function ImageFigure({ src, alt = '', attemptId, className = '' } images to show the first paragraph. */} {label} - {label && {label}} + {showLabel && label && {label}} {open && ( diff --git a/frontend/src/pages/FlashcardStudyPage.css b/frontend/src/pages/FlashcardStudyPage.css new file mode 100644 index 0000000..cef4c58 --- /dev/null +++ b/frontend/src/pages/FlashcardStudyPage.css @@ -0,0 +1,64 @@ +/* Studying a deck. + * + * Its own file, imported by its own page. These rules lived in + * FlashcardsPage.css — the deck *list* — and each page is its own bundle, so + * opening a deck's study page directly loaded the markup with none of its + * appearance: no card, no box, no pinned verdict bar. It looked right only if + * you happened to arrive via the list. + */ +/* ── Studying a deck ────────────────────────────────────────────────── + Boxed, like the session player: the deck's header at the top, the verdict at + the foot, and only the card between them scrolls. A card with a picture on + it is taller than the window, and the two buttons the whole exercise turns + on were below the fold. */ +.fc-study-main { display: flex; flex-direction: column; gap: 12px; min-height: 0; } + +.fc-card { + /* Shrinks to the card, up to the height it has. A one-line front in a box + four hundred pixels tall is mostly emptiness; a card with a radiograph on + it scrolls inside its own frame rather than pushing the verdict buttons + off the screen. */ + flex: 0 1 auto; min-height: 180px; overflow-y: auto; + display: flex; flex-direction: column; align-items: center; justify-content: center; + gap: 10px; padding: 32px 28px; text-align: center; cursor: pointer; + background: var(--card-bg); border: 2px solid var(--border); + border-radius: var(--card-radius); box-shadow: 0 4px 20px rgba(0, 0, 0, .08); + transition: border-color .2s; +} +.fc-card.is-flipped { border-color: var(--primary); } +.fc-card .imgfig { margin: 0; } + +.fc-foot { + position: sticky; bottom: 0; z-index: 2; + padding: 10px 0 6px; background: var(--bg); + border-top: 1px solid var(--border); +} +.fc-foot-row { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; } +.fc-keys { margin: 8px 0 0; text-align: center; font-size: .74rem; color: var(--text-muted); } + +/* Named rather than styled inline: green for the one that means "done with + this", red-lettered for the one that means "again". */ +.fc-known { background: #22c55e; border-color: #22c55e; color: #fff; } +.fc-known:hover { background: #16a34a; border-color: #16a34a; } +.fc-again { color: #ef4444; border-color: #ef4444; background: none; } +.fc-again:hover { background: #fef2f2; } + +@media (min-width: 900px) { + /* Tall enough to be worth boxing: the header and the verdict stay put and + the card scrolls between them. `max-height` rather than `height`, so a + short card is short — the box is a ceiling, not a shape to fill. */ + .fc-study-main { max-height: calc(100dvh - 150px); } +} + +/* What you can do to one card in the browse list: one strip, wrapping onto a + second line on a narrow screen rather than becoming a column. */ +.fc-row-actions { display: flex; gap: 6px; flex-wrap: wrap; flex-shrink: 0; align-items: flex-start; } +.fc-row-actions .btn { white-space: nowrap; } +.fc-row-delete { color: var(--text-muted); } +.fc-row-delete:hover { color: var(--wrong-fg, #b91c1c); border-color: currentColor; } + +@media (max-width: 700px) { + /* Under the card rather than beside it: at 390px a strip of four buttons + beside the text leaves the stem two words wide. */ + .fc-row-actions { width: 100%; } +} diff --git a/frontend/src/pages/FlashcardStudyPage.jsx b/frontend/src/pages/FlashcardStudyPage.jsx index 38f71eb..c4e8623 100644 --- a/frontend/src/pages/FlashcardStudyPage.jsx +++ b/frontend/src/pages/FlashcardStudyPage.jsx @@ -1,6 +1,7 @@ import { useState, useEffect, useCallback, useMemo } from 'react' import { useParams, useNavigate, Link } from 'react-router-dom' import api from '../api/client' +import './FlashcardStudyPage.css' import RichText from '../components/RichText' import ArticleSplitPane from '../components/ArticleSplitPane' import { SplitViewProvider } from '../context/SplitViewContext' @@ -208,7 +209,7 @@ export default function FlashcardStudyPage() { other figure. */} {currentCard.image_path && (
- +
)} {!flipped && ( diff --git a/frontend/src/pages/FlashcardsPage.css b/frontend/src/pages/FlashcardsPage.css index b4ca40c..45846f6 100644 --- a/frontend/src/pages/FlashcardsPage.css +++ b/frontend/src/pages/FlashcardsPage.css @@ -23,55 +23,3 @@ .fc-study.has-split { grid-template-columns: minmax(0, 1fr); } .fc-study.has-split .fc-study-main { display: none; } } - -/* ── Studying a deck ────────────────────────────────────────────────── - Boxed, like the session player: the deck's header at the top, the verdict at - the foot, and only the card between them scrolls. A card with a picture on - it is taller than the window, and the two buttons the whole exercise turns - on were below the fold. */ -.fc-study-main { display: flex; flex-direction: column; gap: 12px; min-height: 0; } - -.fc-card { - flex: 1; min-height: 0; overflow-y: auto; - display: flex; flex-direction: column; align-items: center; justify-content: center; - gap: 10px; padding: 32px 28px; text-align: center; cursor: pointer; - background: var(--card-bg); border: 2px solid var(--border); - border-radius: var(--card-radius); box-shadow: 0 4px 20px rgba(0, 0, 0, .08); - transition: border-color .2s; -} -.fc-card.is-flipped { border-color: var(--primary); } -.fc-card .imgfig { margin: 0; } - -.fc-foot { - position: sticky; bottom: 0; z-index: 2; - padding: 10px 0 6px; background: var(--bg); - border-top: 1px solid var(--border); -} -.fc-foot-row { display: flex; gap: 8px; justify-content: center; flex-wrap: wrap; } -.fc-keys { margin: 8px 0 0; text-align: center; font-size: .74rem; color: var(--text-muted); } - -/* Named rather than styled inline: green for the one that means "done with - this", red-lettered for the one that means "again". */ -.fc-known { background: #22c55e; border-color: #22c55e; color: #fff; } -.fc-known:hover { background: #16a34a; border-color: #16a34a; } -.fc-again { color: #ef4444; border-color: #ef4444; background: none; } -.fc-again:hover { background: #fef2f2; } - -@media (min-width: 900px) { - /* Tall enough to be worth boxing: the header and the verdict stay put and - the card scrolls between them. Below this the page simply scrolls. */ - .fc-study-main { height: calc(100dvh - 150px); } -} - -/* What you can do to one card in the browse list: one strip, wrapping onto a - second line on a narrow screen rather than becoming a column. */ -.fc-row-actions { display: flex; gap: 6px; flex-wrap: wrap; flex-shrink: 0; align-items: flex-start; } -.fc-row-actions .btn { white-space: nowrap; } -.fc-row-delete { color: var(--text-muted); } -.fc-row-delete:hover { color: var(--wrong-fg, #b91c1c); border-color: currentColor; } - -@media (max-width: 700px) { - /* Under the card rather than beside it: at 390px a strip of four buttons - beside the text leaves the stem two words wide. */ - .fc-row-actions { width: 100%; } -} diff --git a/frontend/src/pages/FlashcardsPage.jsx b/frontend/src/pages/FlashcardsPage.jsx index 37f55c2..cf45bc9 100644 --- a/frontend/src/pages/FlashcardsPage.jsx +++ b/frontend/src/pages/FlashcardsPage.jsx @@ -326,7 +326,7 @@ export default function FlashcardsPage() { {studyCard.image_path && (
- +
)}