From 2e4e4000a5f2fc213b17f947ec5d971138b505d6 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 13 Sep 2026 03:21:47 +0200 Subject: [PATCH] fix: the study page had no styles of its own, and a thumbnail stretched to its caption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two things, and the first explains why the deck looked unstyled on your screen and boxed on mine. **The study page's appearance lived in the deck *list*'s stylesheet.** Each page is its own bundle, so opening `/flashcards/:id/study` directly loaded the markup with none of it: no card, no border, no pinned verdict bar — just left-aligned text on the page background. It looked right only if you happened to arrive from the list, which had already pulled that CSS in. The study page has its own stylesheet now. **The thumbnail's frame stretched to the width of its caption.** A figure is a column — picture, then caption — and a column stretches its children to the widest of them by default. A portrait radiograph half the caption's width therefore sat in a bordered box the caption's width, with a white panel beside it inside its own frame. That is the white panel in the screenshot. Also: the card shrinks to its content up to the height it has, rather than filling it — a one-line front in a box four hundred pixels tall is mostly emptiness — and a card no longer prints its own front text under its picture, where it said the same thing twice. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN --- frontend/src/components/ImageFigure.css | 9 +++- frontend/src/components/ImageFigure.jsx | 10 +++- frontend/src/pages/FlashcardStudyPage.css | 64 +++++++++++++++++++++++ frontend/src/pages/FlashcardStudyPage.jsx | 3 +- frontend/src/pages/FlashcardsPage.css | 52 ------------------ frontend/src/pages/FlashcardsPage.jsx | 2 +- 6 files changed, 83 insertions(+), 57 deletions(-) create mode 100644 frontend/src/pages/FlashcardStudyPage.css 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 && (
- +
)}