Navigation restructure: five-tab bottom bar, no phone drawer
Drawer removed (phones only; desktop rail untouched) - MobileSidebar, sidebarOpen/useSidebarOpen, MenuIcon and the drawer's back-gesture layer are gone; the shared nav content now serves only the desktop rail (dead mobile branches deleted). No drawer-specific CSS existed; the three drawer strings (header.openMenu, sidebar.closeMenu, common.menu) and library.importAria are removed. - The header drops the hamburger: logo + name on the left, the single theme chip on the right. Five-tab bottom bar - Library / Progress / New deck (prominent centre +) / Exam / Settings, in that order; Progress and Exam reuse the existing /stats and /exam screens, Settings keeps opening the dialog. Sliding pill, icon bounce, press feedback, re-tap-to-top and safe-area padding kept; every item is >=44px (72px wide at 360px). - New short tab.* keys (EN + CS) so all five labels stay on one line at 360px — 'Zkouškový režim' became 'Zkouška'. No ellipsis anywhere. - The bar is mounted once in AppHeader so library, progress, exam and share get it; the wizards keep their own bottom action bar and the tab bar steps aside there, exactly as before. Re-homed drawer destinations - Smart Review card loses the Exam link (it is a tab now); Review due and Mistakes stay. - New deck screen (and /homework) get a Study deck / Make my homework segmented switch; the library's homework empty-state button stays. - The phone import icon becomes a ... button opening a sheet with Import JSON and New folder (back-gesture and Escape close it); folder chips still appear only once at least one folder exists, and folder creation stays in the New deck options step. Verified: mobile suite 134 checks (no hamburger/drawer, tab order and 44px targets, label matrix at 360/390 x EN/CS x dark/light x reduced motion, per-tab routing, deep links, Back from every tab, the ... sheet with folder creation) plus regression 74/74, wizard 83/83, declutter 116/116, animation 8/8, native 35/35, i18n 25/25, phase1 38/38, phase2 43/43, phase3 20/20, freeze-loading 6/6. Key parity 740/740. Lint + build clean. SW freezmo-v28.
This commit is contained in:
parent
4513364019
commit
8557b04621
19 changed files with 357 additions and 325 deletions
|
|
@ -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<string | null>(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<string | null>(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" |
|
|||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => importRef.current?.click()}
|
||||
aria-label={t("library.importAria")}
|
||||
title={t("library.import")}
|
||||
onClick={() => setSheetOpen(true)}
|
||||
aria-label={t("library.moreActions")}
|
||||
title={t("library.moreActions")}
|
||||
className="touch-target grid size-11 shrink-0 place-items-center rounded-2xl border border-[var(--glass-border)] bg-[var(--surface-strong)] text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-[var(--text)] sm:hidden"
|
||||
>
|
||||
<UploadIcon className="size-4" />
|
||||
<EllipsisIcon className="size-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 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 ? (
|
||||
<FolderBar
|
||||
className="lg:hidden"
|
||||
folders={folderList}
|
||||
|
|
@ -288,12 +324,6 @@ export function LibraryScreen({ initialTab = "decks" }: { initialTab?: "decks" |
|
|||
>
|
||||
{t("review.mistakesTitle")} · {plural(mistakes, "review.mistakes")}
|
||||
</Link>
|
||||
<Link
|
||||
href="/exam"
|
||||
className="btn-ghost touch-target inline-flex h-10 items-center rounded-xl px-3.5 text-[13px] font-semibold transition"
|
||||
>
|
||||
{t("exam.title")}
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
@ -498,6 +528,93 @@ export function LibraryScreen({ initialTab = "decks" }: { initialTab?: "decks" |
|
|||
</motion.div>
|
||||
</AnimatePresence>
|
||||
|
||||
{/* phone "⋯" sheet: Import JSON + New folder ---------------------- */}
|
||||
<AnimatePresence>
|
||||
{sheetOpen ? (
|
||||
<motion.div
|
||||
className="fixed inset-0 z-50 flex flex-col justify-end sm:hidden"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t("common.cancel")}
|
||||
onClick={closeSheet}
|
||||
className="absolute inset-0 bg-black/40 backdrop-blur-sm"
|
||||
/>
|
||||
<motion.div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-label={t("library.moreTitle")}
|
||||
initial={{ y: 300 }}
|
||||
animate={{ y: 0 }}
|
||||
exit={{ y: 300 }}
|
||||
transition={springSheet}
|
||||
className="glass-panel relative z-10 rounded-t-3xl px-4 pb-safe pt-3"
|
||||
>
|
||||
<div className="sheet-grabber" aria-hidden />
|
||||
<h2 className="mb-3 text-[15px] font-semibold text-[var(--text)]">{t("library.moreTitle")}</h2>
|
||||
|
||||
{folderCreating ? (
|
||||
<div className="space-y-2 pb-4">
|
||||
<input
|
||||
autoFocus
|
||||
value={folderDraft}
|
||||
onChange={(event) => 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 ? (
|
||||
<p className="text-[12px] font-medium text-rose-600 dark:text-rose-300">{folderError}</p>
|
||||
) : null}
|
||||
<div className="flex gap-2">
|
||||
<Button className="touch-target flex-1" onClick={submitFolderFromSheet}>
|
||||
{t("common.add")}
|
||||
</Button>
|
||||
<Button variant="ghost" className="touch-target" onClick={() => setFolderCreating(false)}>
|
||||
{t("common.cancel")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-1 pb-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
closeSheet();
|
||||
importRef.current?.click();
|
||||
}}
|
||||
className="nav-row touch-target"
|
||||
>
|
||||
<UploadIcon className="size-4 shrink-0 opacity-70" />
|
||||
<span className="flex-1 text-left">{t("library.import")}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setFolderDraft("");
|
||||
setFolderError(null);
|
||||
setFolderCreating(true);
|
||||
}}
|
||||
className="nav-row touch-target"
|
||||
>
|
||||
<FolderIcon className="size-4 shrink-0 opacity-70" />
|
||||
<span className="flex-1 text-left">{t("common.newFolder")}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue