diff --git a/frontend/src/components/ImageFigure.css b/frontend/src/components/ImageFigure.css index b65a102..a13c09f 100644 --- a/frontend/src/components/ImageFigure.css +++ b/frontend/src/components/ImageFigure.css @@ -9,6 +9,8 @@ } .imgfig-thumb:hover { border-color: var(--primary); } .imgfig-thumb img { display: block; max-width: 260px; max-height: 200px; width: auto; height: auto; } +.imgfig-thumb.is-vector { width: min(100%, 260px); } +.imgfig-thumb.is-vector img { width: 100%; height: auto; } .imgfig-label { max-width: 260px; font-size: 0.78rem; line-height: 1.4; color: var(--text-muted); } /* ── The viewer ─────────────────────────────────────────────────────── @@ -57,6 +59,11 @@ its content rather than a flex child that stretches. */ .imgfig-frame { position: relative; display: block; line-height: 0; cursor: default; } .imgfig-frame img { display: block; max-width: 100%; max-height: calc(100dvh - 110px); width: auto; height: auto; } +/* A drawing has no size of its own, only a shape, so it is given the width it + is allowed and takes its height from that. Line art scales; a photograph + would be blown up by the same rule, which is why this is not the default. */ +.imgfig-frame.is-vector { width: min(100%, 900px); } +.imgfig-frame.is-vector img { width: 100%; height: auto; max-height: calc(100dvh - 160px); object-fit: contain; } .imgov { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; } @media (max-width: 820px) { @@ -67,6 +74,10 @@ .imgfig-desc { border-right: 0; border-bottom: 1px solid rgba(255, 255, 255, .1); padding: 16px 16px 14px; gap: 10px; } .imgfig-stage { padding: 12px; } .imgfig-frame img { max-height: none; } + /* Stacked, so the picture has the width of the screen and whatever height + its shape asks for — the row it sits in scrolls with the description. */ + .imgfig-frame.is-vector { width: 100%; } + .imgfig-frame.is-vector img { max-height: none; } } @media (max-width: 560px) { diff --git a/frontend/src/components/ImageFigure.jsx b/frontend/src/components/ImageFigure.jsx index 1a16247..a8057ce 100644 --- a/frontend/src/components/ImageFigure.jsx +++ b/frontend/src/components/ImageFigure.jsx @@ -23,6 +23,13 @@ import './ImageFigure.css' * Anything an educator has marked on the image is off until the learner turns * it on. Marks shown before they have looked answer the question for them. */ +//: An SVG written with a viewBox and no width or height has a ratio but no +//: intrinsic size. Chrome falls back to 300×150; Safari resolves it to nothing +//: at all inside a shrink-to-fit box, which is why a diagram opened on an +//: iPhone was a caption above an empty screen. Told apart here rather than in +//: CSS, which cannot see a file extension, and given a width to work from. +const isVector = (src) => /\.svg(\?|#|$)/i.test(String(src || '')) + export function ImageViewer({ src, alt = '', attemptId, onClose }) { const [asset, setAsset] = useState(null) const [marked, setMarked] = useState(false) @@ -88,7 +95,7 @@ export function ImageViewer({ src, alt = '', attemptId, onClose }) { { if (event.target === event.currentTarget) onClose?.() }}> - + {title {marked && hasShapes && } @@ -107,7 +114,7 @@ export default function ImageFigure({ src, alt = '', attemptId, className = '' } return ( <> - )} {current.attending_tip && ( @@ -1798,6 +1809,7 @@ const timerStarted = timeLeft !== null )}
+ {/* Not a pencil. The pencil beside it opens the note, and + two identical glyphs an inch apart doing different things + is how a thumb picks the wrong one. */}
diff --git a/frontend/src/pages/QuizPage.test.jsx b/frontend/src/pages/QuizPage.test.jsx index 2b3c4dc..8b3ed5b 100644 --- a/frontend/src/pages/QuizPage.test.jsx +++ b/frontend/src/pages/QuizPage.test.jsx @@ -297,7 +297,7 @@ describe('quiz player', () => { // Nothing was answered by it: not in the rail, not in the count, and not // in what is handed in. expect(document.querySelectorAll('.quiz-rail-item')[0].className).not.toMatch(/is-done/) - expect(within(document.querySelector('.quiz-header-card')).getByText('0 answered')).toBeInTheDocument() + expect(within(document.querySelector('.quiz-header-card')).getByText('0 of 2 answered')).toBeInTheDocument() // Nor can an option be picked afterwards — that would be copying, not // answering, and it would count. await userEvent.click(inCard().getByText('Second answer').closest('.option')) diff --git a/frontend/src/pages/QuizPlayer.css b/frontend/src/pages/QuizPlayer.css index e8480e1..978c54c 100644 --- a/frontend/src/pages/QuizPlayer.css +++ b/frontend/src/pages/QuizPlayer.css @@ -990,3 +990,17 @@ body:has(.quiz-player.is-exam-chrome) .site-footer { display: none; } /* The level, in the rail. Pushed to the right so a column of them lines up and can be read down rather than hunted for. */ .quiz-rail-diff-marks { margin-left: auto; flex: none; } + +/* ── The session's own header, on a phone ───────────────────────────── + Gone. The title, the mode and how much is answered are all in the drawer + behind the menu button, and the bar above the question says which question + this is. On a 390px screen those two rows were a fifth of the height of the + scroller, repeated above every question. */ +@media (max-width: 820px) { + .quiz-header-card { display: none; } +} + +/* The last line of an explanation sat exactly on the bar at the foot of the + player, with nothing between the two. A scroller that ends flush against a + sticky bar reads as content cut off rather than content finished. */ +.quiz-layout { padding-bottom: 14px; }