fix: the study page had no styles of its own, and a thumbnail stretched to its caption
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 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TqXevQJhxFrM7jJg82cgZN
This commit is contained in:
parent
0a2f7d08db
commit
2e4e4000a5
6 changed files with 83 additions and 57 deletions
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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. */}
|
||||
<img src={uploadUrl(src, attemptId, 256)} alt={label} loading="lazy" decoding="async" />
|
||||
</button>
|
||||
{label && <span className="imgfig-label">{label}</span>}
|
||||
{showLabel && label && <span className="imgfig-label">{label}</span>}
|
||||
</span>
|
||||
|
||||
{open && (
|
||||
|
|
|
|||
64
frontend/src/pages/FlashcardStudyPage.css
Normal file
64
frontend/src/pages/FlashcardStudyPage.css
Normal file
|
|
@ -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%; }
|
||||
}
|
||||
|
|
@ -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 && (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<ImageFigure src={currentCard.image_path} alt={currentCard.front} />
|
||||
<ImageFigure src={currentCard.image_path} alt={currentCard.front} showLabel={false} />
|
||||
</div>
|
||||
)}
|
||||
{!flipped && (
|
||||
|
|
|
|||
|
|
@ -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%; }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -326,7 +326,7 @@ export default function FlashcardsPage() {
|
|||
<RichText value={flipped ? studyCard.back : studyCard.front} linkArticles />
|
||||
{studyCard.image_path && (
|
||||
<div style={{ marginTop: 10 }}>
|
||||
<ImageFigure src={studyCard.image_path} alt={studyCard.front} />
|
||||
<ImageFigure src={studyCard.image_path} alt={studyCard.front} showLabel={false} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue