diff --git a/app/deck/[id]/page.tsx b/app/deck/[id]/page.tsx
index bb6eac8..5927fad 100644
--- a/app/deck/[id]/page.tsx
+++ b/app/deck/[id]/page.tsx
@@ -72,7 +72,10 @@ export default function DeckPage() {
return (
-
+ {/* The feed is a fixed overlay, but its content still needs a landmark. */}
+
+
+
);
}
diff --git a/app/globals.css b/app/globals.css
index 4357eb8..c5d4249 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -647,6 +647,28 @@ body:has(.overlay-open) :is(.app-header, .segmented, .tabbar) {
background: var(--hover);
color: var(--text);
}
+
+/* The rail's two creation actions ("New deck", "New homework") are buttons,
+ not nav rows: once the library is full the empty-state CTAs are long gone,
+ and on desktop the rail is the only place left to start something. */
+.nav-action {
+ display: flex;
+ align-items: center;
+ gap: 0.625rem;
+ width: 100%;
+ border-radius: 0.75rem;
+ padding: 0.5rem 0.625rem;
+ font-size: 13.5px;
+ font-weight: 600;
+ color: var(--text);
+ background: var(--surface-strong);
+ border: 1px solid var(--glass-border);
+ transition: background-color 160ms ease, border-color 160ms ease;
+}
+.nav-action:hover {
+ background: var(--hover);
+ border-color: var(--nav-active-border);
+}
.nav-row[aria-current="page"],
.nav-row[aria-pressed="true"] {
background: var(--nav-active-bg);
diff --git a/app/manifest.ts b/app/manifest.ts
index 205c105..26d61e4 100644
--- a/app/manifest.ts
+++ b/app/manifest.ts
@@ -55,7 +55,7 @@ export default function manifest(): MetadataRoute.Manifest {
],
shortcuts: [
{ name: "New deck", short_name: "New", url: "/create" },
- { name: "Make my homework", short_name: "Homework", url: "/homework" },
+ { name: "New homework", short_name: "Homework", url: "/homework" },
{ name: "Library", short_name: "Library", url: "/" },
],
/* Share text and links from other apps (GET). Files are POSTed to the
diff --git a/components/DataSection.tsx b/components/DataSection.tsx
index bc33178..1bf771d 100644
--- a/components/DataSection.tsx
+++ b/components/DataSection.tsx
@@ -78,6 +78,8 @@ export function DataSection() {
void pick(event.target.files?.[0])}
diff --git a/components/DeckCard.tsx b/components/DeckCard.tsx
index e42fce4..3a8621a 100644
--- a/components/DeckCard.tsx
+++ b/components/DeckCard.tsx
@@ -182,12 +182,12 @@ export function DeckCard({
) : (
-
{truncate(deck.title, 90)}
-
+
)}
{/* Phones: one compact meta line. */}
diff --git a/components/HomeworkCard.tsx b/components/HomeworkCard.tsx
index 43019e7..c085181 100644
--- a/components/HomeworkCard.tsx
+++ b/components/HomeworkCard.tsx
@@ -62,12 +62,12 @@ export function HomeworkCard({
-
{truncate(record.title, 90)}
-
+
{record.mode === "make" ? : }
diff --git a/components/LearningSection.tsx b/components/LearningSection.tsx
index ebe20ef..0043eaf 100644
--- a/components/LearningSection.tsx
+++ b/components/LearningSection.tsx
@@ -25,7 +25,12 @@ function urlBase64ToUint8Array(base64: string): Uint8Array {
* the server has VAPID keys configured. Without them the reminder block is
* hidden completely, exactly as if the feature did not exist.
*/
-export function LearningSection() {
+/**
+ * `level` keeps the heading order right in both places it is used: h2 on the
+ * library page (under its h1) and h3 inside the settings dialog (under its h2
+ * title).
+ */
+export function LearningSection({ level = 2 }: { level?: 2 | 3 } = {}) {
const { t } = useI18n();
const dailyGoal = useSyncExternalStore(
settingsStore.subscribe,
@@ -154,7 +159,11 @@ export function LearningSection() {
return (
-
{t("settings.learning")}
+ {level === 3 ? (
+
{t("settings.learning")}
+ ) : (
+
{t("settings.learning")}
+ )}
{t("settings.dailyGoal")}
diff --git a/components/LibraryScreen.tsx b/components/LibraryScreen.tsx
index ac7cc77..4e1bba6 100644
--- a/components/LibraryScreen.tsx
+++ b/components/LibraryScreen.tsx
@@ -220,6 +220,8 @@ export function LibraryScreen({ initialTab = "decks" }: { initialTab?: "decks" |
{
diff --git a/components/ReportQuestionButton.tsx b/components/ReportQuestionButton.tsx
index b26686a..a2cef9c 100644
--- a/components/ReportQuestionButton.tsx
+++ b/components/ReportQuestionButton.tsx
@@ -3,22 +3,31 @@
import { haptic } from "@/lib/haptics";
import { useI18n } from "@/lib/i18n";
import { reportQuestion, unreportQuestion } from "@/lib/question-reports";
-import type { Activity, Deck } from "@/lib/schemas";
+import type { Activity } from "@/lib/schemas";
import { showToast } from "@/lib/toast";
/**
* The red "!" report control that sits under the card (bottom-right of the
- * exercise): records the question, sends the operator a copy and skips past
- * it. Undo lives in the toast. 44px, because it is a thumb target.
+ * exercise): records the question, sends the operator a copy and — in the
+ * study feeds — skips past it. Undo lives in the toast. 44px, because it is a
+ * thumb target.
+ *
+ * `deckId` is a deck id in the feeds and the homework record's id for a
+ * homework exercise; `onSkip` is omitted where there is no queue to skip.
*/
export function ReportQuestionButton({
- deck,
+ deckId,
+ deckTitle,
activity,
onSkip,
+ labelKey = "feed.report",
}: {
- deck: Deck;
+ deckId: string;
+ deckTitle?: string;
activity: Activity;
- onSkip: () => void;
+ onSkip?: () => void;
+ /** i18n key for the accessible name ("Report question" / "Report exercise"). */
+ labelKey?: string;
}) {
const { t } = useI18n();
@@ -27,16 +36,16 @@ export function ReportQuestionButton({
type="button"
onClick={() => {
haptic("tap");
- reportQuestion({ deck, activity });
+ reportQuestion({ deckId, deckTitle, activity });
showToast({
message: t("feed.reported"),
actionLabel: t("toast.undo"),
- onAction: () => unreportQuestion(deck.id, activity.id),
+ onAction: () => unreportQuestion(deckId, activity.id),
});
- onSkip();
+ onSkip?.();
}}
- aria-label={t("feed.report")}
- title={t("feed.report")}
+ aria-label={t(labelKey)}
+ title={t(labelKey)}
className="grid size-11 shrink-0 place-items-center rounded-xl border-2 border-rose-400/80 text-base font-black leading-none text-rose-600 transition hover:bg-rose-50 dark:border-rose-400/50 dark:text-rose-300 dark:hover:bg-rose-400/10"
>
!
diff --git a/components/ReviewClient.tsx b/components/ReviewClient.tsx
index aabd2e6..dfb4598 100644
--- a/components/ReviewClient.tsx
+++ b/components/ReviewClient.tsx
@@ -81,36 +81,39 @@ export function ReviewClient({ mistakes, deckId }: { mistakes: boolean; deckId?:
const sessionItems: SessionItem[] = session.map(({ deck, activity }) => ({ deck, activity }));
return (
- router.push("/")}
- onAnswered={(item, correct) => {
- haptic(correct ? "success" : "error");
- recordAnswer({
- deckId: item.deck.id,
- deckTitle: item.deck.title,
- activityId: item.activity.id,
- type: item.activity.type,
- topic: item.activity.topic,
- correct,
- });
- }}
- renderResults={({ correct, total, score }) => (
-
{
setSkipped((current) => [...current, item.activity.id]);
diff --git a/components/SettingsDialog.tsx b/components/SettingsDialog.tsx
index 3096328..8db0245 100644
--- a/components/SettingsDialog.tsx
+++ b/components/SettingsDialog.tsx
@@ -407,7 +407,7 @@ export function SettingsDialog({
{/* ------------------------------------------- data & sync */}
-
+
@@ -746,7 +746,7 @@ export function SettingsDialog({
{/* ------------------------------------------- data & sync */}
-
+
diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx
index 0950082..bba0f29 100644
--- a/components/Sidebar.tsx
+++ b/components/Sidebar.tsx
@@ -137,23 +137,24 @@ function NavContent({ collapsed = false }: { collapsed?: boolean }) {
{!collapsed ? {t("sidebar.collapse")} : null}
- {/* new deck: a quiet row, not a competing primary button ---------- */}
+ {/* new deck: a button, not a nav row — it stays visible when the
+ library is already full (see .nav-action in globals.css) --------- */}
-
+
{!collapsed ? {t("common.newDeck")} : null}
- {/* make my homework: same quiet row --------------------------------- */}
+ {/* new homework: the same button ------------------------------------ */}
-
+
{!collapsed ? {t("homework.nav")} : null}
diff --git a/components/StudyFeed.tsx b/components/StudyFeed.tsx
index b86aa45..b9d8a6d 100644
--- a/components/StudyFeed.tsx
+++ b/components/StudyFeed.tsx
@@ -332,7 +332,8 @@ export function StudyFeed({ deck }: { deck: Deck }) {
{
setSkipped((current) => [...current, activity.id]);
diff --git a/components/UploadScreen.tsx b/components/UploadScreen.tsx
index 4558cae..7dca7bb 100644
--- a/components/UploadScreen.tsx
+++ b/components/UploadScreen.tsx
@@ -650,6 +650,8 @@ export function UploadScreen() {
{
diff --git a/components/homework/HomeworkGuide.tsx b/components/homework/HomeworkGuide.tsx
index a7af976..0ad75ef 100644
--- a/components/homework/HomeworkGuide.tsx
+++ b/components/homework/HomeworkGuide.tsx
@@ -6,6 +6,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 { ReportQuestionButton } from "@/components/ReportQuestionButton";
import { XIcon } from "@/components/icons";
import { Button, ProgressBar } from "@/components/ui";
import { AskButton } from "@/components/chat/AskButton";
@@ -246,7 +247,7 @@ export function HomeworkGuide({
{/* checkpoint or a plain next button */}
{step.checkpoint ? (
-
+
{t("homework.guide.tryIt")}
@@ -263,6 +264,16 @@ export function HomeworkGuide({
}}
onNext={() => goTo(stepIndex + 1)}
/>
+ {/* Same report control as the study feeds, under the
+ exercise (the homework record is the "deck" here). */}
+