One-pill study chrome, toggling chat pickers, readable homework steps

UI batch from feedback:

- The study/review/exam/homework chrome is now a single 58px pill: round
  close button, title + position over the progress bar, chips (or the exam
  timer) at the far end — everything on the centre line, so nothing crowds
  the rounded corners any more. Homework guide gets the same recipe.
- The immersive feeds carry z-[45] so they paint over the app chrome (on
  /exam the header used to cover the top of the chrome), and the page chrome
  behind an open overlay (.app-header, .segmented, .tabbar) is hidden while
  the transparent overlay is up, so it can no longer ghost through the glass.
  The guide's disclaimer moved inside the overlay with it.
- Chat: the context and model chips toggle their own popover (pressed state,
  aria-expanded), opening one closes the other, a press outside closes it —
  no more being forced to pick something to get out.
- Homework: the model's plain-text step explanations/hints/verdicts keep
  their line breaks (whitespace-pre-wrap) — a step used to collapse into one
  run-on paragraph with the whole solution inside it; the guide prompt now
  says the explanation is coaching, 2-4 sentences.
- The four glacier blobs are at 25% of their old opacity (0.045 light,
  0.1 dark).

Tests: phase-2 chrome geometry + exam layering checks (62), chat desktop
picker checks (101), the standing suites all green. README: decisions 54-56,
decision 23 updated. SW -> freezmo-v44.
This commit is contained in:
Martin Dvořák 2026-10-12 14:21:11 +00:00
commit 9d204b85be
16 changed files with 314 additions and 118 deletions

View file

@ -5,6 +5,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { ActivityRenderer } from "@/components/activities";
import { PromptCard, QuestionText } from "@/components/activities/shared";
import { AiDisclaimer } from "@/components/AiDisclaimer";
import { XIcon } from "@/components/icons";
import { Button, ProgressBar } from "@/components/ui";
import { AskButton } from "@/components/chat/AskButton";
@ -138,32 +139,37 @@ export function HomeworkGuide({
};
return (
<div className="frost-bg fixed inset-0 z-[45] flex flex-col">
<div className="frost-bg overlay-open fixed inset-0 z-[45] flex flex-col">
{/* chrome ------------------------------------------------------- */}
{/* Same pill as the study feed: round close button, title + step above
the bar (nothing crowds the rounded ends). */}
<header className="pt-safe absolute inset-x-0 top-0 z-30 px-3">
<div className="study-chrome mx-auto max-w-xl space-y-1.5 rounded-full px-3 py-2">
<div className="flex items-center gap-3">
<button
type="button"
onClick={onClose}
aria-label={t("homework.guide.close")}
className="grid size-9 shrink-0 place-items-center rounded-xl bg-[var(--surface-sunken)] text-[var(--text)] transition hover:bg-[var(--hover)]"
>
<XIcon className="size-4" />
</button>
<div className="min-w-0 flex-1">
<ProgressBar
value={progressValue}
max={slideCount}
label={t("homework.guide.stepOf", { n: progressValue, total: slideCount })}
/>
<div className="study-chrome mx-auto flex max-w-xl items-center gap-2.5 rounded-full px-2.5 py-2">
<button
type="button"
onClick={onClose}
aria-label={t("homework.guide.close")}
title={t("homework.guide.close")}
className="grid size-10 shrink-0 place-items-center rounded-full bg-[var(--surface-sunken)] text-[var(--text)] transition hover:bg-[var(--hover)]"
>
<XIcon className="size-4" />
</button>
<div className="min-w-0 flex-1">
<div className="flex items-baseline justify-between gap-3">
<span className="min-w-0 truncate text-[13px] font-semibold leading-tight text-[var(--text)]">
{record.title}
</span>
<span className="shrink-0 text-[11.5px] font-bold tabular-nums text-[var(--text-muted)]">
{onSolution ? t("homework.guide.solution") : t("homework.guide.stepOf", { n: index + 1, total: steps.length })}
</span>
</div>
</div>
<div className="flex items-center justify-between gap-2 text-[11px] font-semibold text-[var(--text-muted)]">
<span className="min-w-0 truncate">{record.title}</span>
<span className="shrink-0">
{onSolution ? t("homework.guide.solution") : t("homework.guide.stepOf", { n: index + 1, total: steps.length })}
</span>
<ProgressBar
className="mt-1.5"
value={progressValue}
max={slideCount}
label={t("homework.guide.stepOf", { n: progressValue, total: slideCount })}
/>
</div>
</div>
</header>
@ -189,7 +195,10 @@ export function HomeworkGuide({
<PromptCard>
<QuestionText>{step.title}</QuestionText>
<p className="mt-2 text-[15px] leading-relaxed text-frost-700 dark:text-frost-200">
{/* The model writes plain text with short lines and numbered
lists (see HOMEWORK_BASE_RULES) — keep its line breaks so
the step reads as steps, not one run-on paragraph. */}
<p className="mt-2 whitespace-pre-wrap break-words text-[15px] leading-relaxed text-frost-700 dark:text-frost-200">
{explained[stepIndex] ?? step.explanation}
</p>
<div className="mt-2 flex flex-wrap items-center gap-2">
@ -221,7 +230,7 @@ export function HomeworkGuide({
className="glass-chip rounded-2xl p-3 text-sm leading-relaxed text-[var(--text)]"
>
<span className="font-bold">{t("homework.guide.hintN", { n: hintIndex + 1 })}: </span>
{hint}
<span className="whitespace-pre-wrap break-words">{hint}</span>
</motion.div>
))}
{revealedCount(stepIndex) < step.hints.length ? (
@ -261,6 +270,7 @@ export function HomeworkGuide({
<span aria-hidden>→</span>
</Button>
)}
<AiDisclaimer />
</div>
</section>
))}
@ -282,6 +292,7 @@ export function HomeworkGuide({
</p>
</PromptCard>
<p className="text-[12.5px] text-[var(--text-subtle)]">{t("homework.guide.solutionNote")}</p>
<AiDisclaimer />
<div className="flex flex-wrap items-start gap-2">
<CopyButton text={solution} />
<AskButton