+
diff --git a/app/exam/page.tsx b/app/exam/page.tsx
index 9db4110..46b4796 100644
--- a/app/exam/page.tsx
+++ b/app/exam/page.tsx
@@ -1,7 +1,7 @@
import { ApiKeyGate } from "@/components/ApiKeyGate";
import { AppHeader } from "@/components/AppHeader";
import { ExamScreen } from "@/components/exam/ExamScreen";
-import { MobileSidebar, Sidebar, SidebarOffset } from "@/components/Sidebar";
+import { Sidebar, SidebarOffset } from "@/components/Sidebar";
export const metadata = {
title: "Exam mode",
@@ -13,7 +13,6 @@ export default function ExamPage() {
-
diff --git a/app/homework/page.tsx b/app/homework/page.tsx
index 7e10e0c..39412a5 100644
--- a/app/homework/page.tsx
+++ b/app/homework/page.tsx
@@ -4,7 +4,8 @@ import { ApiKeyGate } from "@/components/ApiKeyGate";
import { AppHeader } from "@/components/AppHeader";
import { HomeworkScreen } from "@/components/homework/HomeworkScreen";
import { RouteTransition } from "@/components/RouteTransition";
-import { MobileSidebar, Sidebar, SidebarOffset } from "@/components/Sidebar";
+import { Sidebar, SidebarOffset } from "@/components/Sidebar";
+import { WizardModeSwitch } from "@/components/WizardModeSwitch";
export const metadata = {
title: "Make my homework",
@@ -25,10 +26,10 @@ export default async function HomeworkPage({
-
+
diff --git a/app/page.tsx b/app/page.tsx
index 4a8d0bf..1b3eeaa 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -2,8 +2,7 @@ import { ApiKeyGate } from "@/components/ApiKeyGate";
import { AppHeader } from "@/components/AppHeader";
import { LibraryScreen } from "@/components/LibraryScreen";
import { RouteTransition } from "@/components/RouteTransition";
-import { MobileSidebar, Sidebar, SidebarOffset } from "@/components/Sidebar";
-import { TabBar } from "@/components/TabBar";
+import { Sidebar, SidebarOffset } from "@/components/Sidebar";
export default async function HomePage({
searchParams,
@@ -16,7 +15,6 @@ export default async function HomePage({
-
diff --git a/app/share/page.tsx b/app/share/page.tsx
index 704fd02..ffb66f5 100644
--- a/app/share/page.tsx
+++ b/app/share/page.tsx
@@ -1,7 +1,7 @@
import { ApiKeyGate } from "@/components/ApiKeyGate";
import { AppHeader } from "@/components/AppHeader";
import { ShareChooser } from "@/components/ShareChooser";
-import { MobileSidebar, Sidebar, SidebarOffset } from "@/components/Sidebar";
+import { Sidebar, SidebarOffset } from "@/components/Sidebar";
export const metadata = {
title: "Add to Freezmo",
@@ -18,7 +18,6 @@ export default async function SharePage({
-
= { [UNORGANIZED_ID]: 0 };
- for (const folder of sortedFolders) map[folder.id] = 0;
- for (const deck of decks) {
- const key = deck.folderId && map[deck.folderId] !== undefined ? deck.folderId : UNORGANIZED_ID;
- map[key] = (map[key] ?? 0) + 1;
- }
- return map;
- }, [decks, sortedFolders]);
-
const submit = () => {
const result = createFolder(draft);
if ("error" in result) {
@@ -82,20 +64,15 @@ function NavContent({
setCreating(false);
};
- const select = (id: string) => {
- folderSelection.set(id);
- onNavigate?.();
- };
-
/** Counts of 0 are noise — only show a count when there is something in it. */
const count = (value: number) =>
!collapsed && value > 0 ? {value} : null;
- /** One shared view row (All decks / Unorganized) for both nav layouts. */
+ /** One shared view row (All decks / Unorganized). */
const viewRow = (id: string, label: string, icon: ReactNode, trailing: ReactNode) => (
@@ -24,7 +22,6 @@ export default async function HomePage({
-
diff --git a/app/stats/page.tsx b/app/stats/page.tsx
index d0d6c7d..984d241 100644
--- a/app/stats/page.tsx
+++ b/app/stats/page.tsx
@@ -1,6 +1,6 @@
import { ApiKeyGate } from "@/components/ApiKeyGate";
import { AppHeader } from "@/components/AppHeader";
-import { MobileSidebar, Sidebar, SidebarOffset } from "@/components/Sidebar";
+import { Sidebar, SidebarOffset } from "@/components/Sidebar";
import { StatsScreen } from "@/components/StatsScreen";
export const metadata = {
@@ -13,7 +13,6 @@ export default function StatsPage() {
-
{t("review.mistakesTitle")} · {plural(mistakes, "review.mistakes")}
-
- {t("exam.title")}
-
@@ -498,6 +528,93 @@ export function LibraryScreen({ initialTab = "decks" }: { initialTab?: "decks" |
+ {/* phone "⋯" sheet: Import JSON + New folder ---------------------- */}
+
+ {sheetOpen ? (
+
+
+
+
+
+
+ ) : null}
+
+
);
}
diff --git a/components/Sidebar.tsx b/components/Sidebar.tsx
index 5cea6ee..0ed7791 100644
--- a/components/Sidebar.tsx
+++ b/components/Sidebar.tsx
@@ -2,7 +2,6 @@
import Link from "next/link";
import { useMemo, useState, useSyncExternalStore, type ReactNode } from "react";
-import { AnimatePresence, motion } from "framer-motion";
import {
ChevronLeftIcon,
@@ -21,26 +20,19 @@ import { useTheme } from "@/components/ThemeToggle";
import { createFolder, folderStore } from "@/lib/folders";
import { haptic } from "@/lib/haptics";
import { useI18n } from "@/lib/i18n";
-import { springSheet } from "@/lib/motion";
-import { ALL_FOLDERS, folderSelection, sidebarOpen, useSelectedFolder, useSidebarOpen } from "@/lib/nav";
+import { ALL_FOLDERS, folderSelection, useSelectedFolder } from "@/lib/nav";
import { UNORGANIZED_ID } from "@/lib/schemas";
import { deckStore } from "@/lib/storage";
import { openSettings } from "@/lib/ui-events";
-import { useBackClose } from "@/lib/use-back-close";
import { cn } from "@/lib/utils";
const COLLAPSE_KEY = "freezmo.sidebarCollapsed";
-/** Shared nav content for the desktop rail and the mobile drawer. */
-function NavContent({
- collapsed = false,
- onNavigate,
- desktop = false,
-}: {
- collapsed?: boolean;
- onNavigate?: () => void;
- desktop?: boolean;
-}) {
+/**
+ * Desktop navigation rail (lg and up). Phones use the bottom tab bar instead —
+ * there is no drawer any more, so this is the only consumer of the rail.
+ */
+function NavContent({ collapsed = false }: { collapsed?: boolean }) {
const decks = useSyncExternalStore(deckStore.subscribe, deckStore.getSnapshot, deckStore.getServerSnapshot);
const folders = useSyncExternalStore(
folderStore.subscribe,
@@ -60,16 +52,6 @@ function NavContent({
[folders],
);
- const counts = useMemo(() => {
- const map: Record
diff --git a/components/AppHeader.tsx b/components/AppHeader.tsx
index a5fea35..74f9417 100644
--- a/components/AppHeader.tsx
+++ b/components/AppHeader.tsx
@@ -3,18 +3,17 @@
import Link from "next/link";
import { useEffect, useState } from "react";
-import { MenuIcon } from "@/components/icons";
import { SettingsDialog } from "@/components/SettingsDialog";
+import { TabBar } from "@/components/TabBar";
import { ThemeToggle } from "@/components/ThemeToggle";
import { useI18n } from "@/lib/i18n";
-import { sidebarOpen } from "@/lib/nav";
import { OPEN_SETTINGS_EVENT } from "@/lib/ui-events";
import { cn } from "@/lib/utils";
/**
- * Minimal, sticky frosted top bar: brand on the left, quiet actions on the
- * right. The phone layout keeps the brand compact and relies on the tab bar
- * for navigation.
+ * Minimal, sticky frosted top bar: brand on the left, theme chip on the
+ * right. Phones navigate with the bottom tab bar (also mounted here, so every
+ * screen that has a header gets it); the desktop rail carries navigation.
*/
export function AppHeader({ className, compact = false }: { className?: string; compact?: boolean }) {
const { t } = useI18n();
@@ -48,41 +47,34 @@ export function AppHeader({ className, compact = false }: { className?: string;
)}
>
- {/* Mobile folder switcher; the sidebar covers desktop. Hidden while
- there are no folders — the drawer's “New folder” is the way in. */}
- {tab === "decks" && !isEmpty && hasFolders ? (
+ {/* Mobile folder switcher; the desktop rail covers desktop. Shown only
+ once at least one folder exists — creating one lives in the ⋯ sheet
+ and in the New deck options step. */}
+ {tab === "decks" && hasFolders ? (
-
-
- {/* eslint-disable-next-line @next/next/no-img-element -- static app icon, no optimisation needed */}
-
-
-
- Freezmo
-
+ {/* eslint-disable-next-line @next/next/no-img-element -- static app icon, no optimisation needed */}
+
+
+ Freezmo
{!compact ? (
-
+
{t("header.tagline")}
) : null}
-
setSettingsOpen(false)}
diff --git a/components/LibraryScreen.tsx b/components/LibraryScreen.tsx
index 9f37127..604f0da 100644
--- a/components/LibraryScreen.tsx
+++ b/components/LibraryScreen.tsx
@@ -7,20 +7,21 @@ import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from "reac
import { DeckCard } from "@/components/DeckCard";
import { FolderBar } from "@/components/FolderBar";
import { HomeworkCard } from "@/components/HomeworkCard";
-import { FolderIcon, UploadIcon } from "@/components/icons";
+import { EllipsisIcon, FolderIcon, UploadIcon } from "@/components/icons";
import { SegmentedPill } from "@/components/SegmentedPill";
import { Button, EmptyState, ProgressBar } from "@/components/ui";
-import { folderStore, renameFolder } from "@/lib/folders";
+import { createFolder, folderStore, renameFolder } from "@/lib/folders";
import { deleteHomework, homeworkStore, sortHomework } from "@/lib/homework";
import { learningStore } from "@/lib/history";
import { useI18n } from "@/lib/i18n";
import { computeStats, dueCount, mistakeCount } from "@/lib/learning";
import { settingsStore } from "@/lib/settings";
import { ALL_FOLDERS, folderSelection, useSelectedFolder } from "@/lib/nav";
-import { springGentle } from "@/lib/motion";
+import { springGentle, springSheet } from "@/lib/motion";
import { UNORGANIZED_ID, type Deck } from "@/lib/schemas";
import { deckStore, deleteFolder, importDeck, sortDecks, upsertDeck } from "@/lib/storage";
import { backupNudgeDue, downloadBackup, snoozeBackupNudge } from "@/lib/backup";
+import { useBackClose } from "@/lib/use-back-close";
import { useIsPhone } from "@/lib/use-phone";
import { cn } from "@/lib/utils";
@@ -51,6 +52,40 @@ export function LibraryScreen({ initialTab = "decks" }: { initialTab?: "decks" |
const [editingFolder, setEditingFolder] = useState<{ id: string; name: string } | null>(null);
const [confirmDelete, setConfirmDelete] = useState(null);
const [nudgeDue, setNudgeDue] = useState(false);
+ /** Phone "⋯" sheet: Import JSON + New folder. */
+ const [sheetOpen, setSheetOpen] = useState(false);
+ const [folderCreating, setFolderCreating] = useState(false);
+ const [folderDraft, setFolderDraft] = useState("");
+ const [folderError, setFolderError] = useState(null);
+
+ useBackClose(sheetOpen, () => setSheetOpen(false));
+
+ useEffect(() => {
+ if (!sheetOpen) return;
+ const onKey = (event: KeyboardEvent) => {
+ if (event.key === "Escape") setSheetOpen(false);
+ };
+ window.addEventListener("keydown", onKey);
+ return () => window.removeEventListener("keydown", onKey);
+ }, [sheetOpen]);
+
+ const closeSheet = () => {
+ setSheetOpen(false);
+ setFolderCreating(false);
+ setFolderDraft("");
+ setFolderError(null);
+ };
+
+ const submitFolderFromSheet = () => {
+ const result = createFolder(folderDraft);
+ if ("error" in result) {
+ setFolderError(result.error);
+ return;
+ }
+ setSelected(result.folder.id);
+ setMessage({ tone: "ok", text: t("library.folderCreated") });
+ closeSheet();
+ };
useEffect(() => {
setNudgeDue(backupNudgeDue(decks.length));
@@ -202,18 +237,19 @@ export function LibraryScreen({ initialTab = "decks" }: { initialTab?: "decks" |
- {/* Settings lives in the tab bar and the drawer; the old header
- gear was the same control a third time. */}
+
+ {t("library.moreTitle")}
+ + {folderCreating ? ( +
+ setFolderDraft(event.target.value)}
+ onKeyDown={(event) => {
+ event.stopPropagation();
+ if (event.key === "Enter") submitFolderFromSheet();
+ }}
+ placeholder={t("common.folderName")}
+ aria-label={t("common.newFolderName")}
+ maxLength={60}
+ className="w-full rounded-xl border border-[var(--hairline)] bg-[var(--surface)] px-3 py-2.5 text-[15px] text-[var(--text)] outline-none focus:border-accent-400"
+ />
+ {folderError ? (
+
+ ) : (
+ {folderError}
+ ) : null} +
+
+
+
+
+
+
+
+ )}
+