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
|
|
@ -4,7 +4,7 @@ import { motion, useReducedMotion } from "framer-motion";
|
|||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
import { GearIcon, HomeIcon, PlusIcon } from "@/components/icons";
|
||||
import { CompassIcon, FlameIcon, GearIcon, HomeIcon, PlusIcon } from "@/components/icons";
|
||||
import { haptic } from "@/lib/haptics";
|
||||
import { useI18n } from "@/lib/i18n";
|
||||
import { springSnappy } from "@/lib/motion";
|
||||
|
|
@ -12,20 +12,25 @@ import { openSettings } from "@/lib/ui-events";
|
|||
import { cn } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* iOS-style bottom tab bar. Rendered on the library and create screens on
|
||||
* phones; hidden on desktop (the header carries the same actions there) and on
|
||||
* the full-screen study feed.
|
||||
* iOS-style bottom tab bar, phones only: Library · Progress · New deck (the
|
||||
* prominent centre button) · Exam mode · Settings. The active item sits on a
|
||||
* sliding pill (shared `layoutId`) and its icon gives a small bounce when you
|
||||
* land on it; re-tapping the active tab scrolls back to the top.
|
||||
*
|
||||
* The active item sits on a sliding pill (shared `layoutId`), and its icon
|
||||
* gives a small bounce when you land on it.
|
||||
* Labels use the short `tab.*` keys so they fit on one line at 360px in both
|
||||
* languages — no ellipsis, no wrapping.
|
||||
*/
|
||||
export function TabBar() {
|
||||
const pathname = usePathname();
|
||||
const { t } = useI18n();
|
||||
const reduceMotion = useReducedMotion();
|
||||
|
||||
/* The wizards keep their own bottom action bar (Cancel / Next / Freeze) —
|
||||
the flow is deliberately focused, so the tab bar steps aside there. */
|
||||
const inWizard = pathname === "/create" || pathname === "/homework";
|
||||
|
||||
const item =
|
||||
"relative flex flex-1 flex-col items-center justify-center gap-1 py-2 text-[11px] font-medium transition-colors";
|
||||
"relative flex min-h-[52px] min-w-0 flex-1 flex-col items-center justify-center gap-1 py-2 text-[10.5px] font-medium leading-none transition-colors";
|
||||
const active = "text-[var(--text)]";
|
||||
const idle = "text-[var(--text-subtle)]";
|
||||
|
||||
|
|
@ -53,58 +58,67 @@ export function TabBar() {
|
|||
);
|
||||
|
||||
const label = (text: string, isActive: boolean) => (
|
||||
<span className={cn("relative z-10", isActive ? active : idle)}>{text}</span>
|
||||
<span className={cn("relative z-10 whitespace-nowrap", isActive ? active : idle)}>{text}</span>
|
||||
);
|
||||
|
||||
const libraryActive = pathname === "/";
|
||||
const createActive = pathname === "/create";
|
||||
const isActive = (key: "library" | "progress" | "create" | "exam") => {
|
||||
if (key === "library") return pathname === "/";
|
||||
if (key === "progress") return pathname === "/stats";
|
||||
// The homework wizard is the other half of the New deck screen.
|
||||
if (key === "create") return pathname === "/create" || pathname === "/homework";
|
||||
return pathname === "/exam";
|
||||
};
|
||||
|
||||
const link = (
|
||||
key: "library" | "progress" | "create" | "exam",
|
||||
href: string,
|
||||
labelText: string,
|
||||
iconNode: React.ReactNode,
|
||||
) => {
|
||||
const current = isActive(key);
|
||||
return (
|
||||
<Link
|
||||
key={key}
|
||||
href={href}
|
||||
aria-current={current ? "page" : undefined}
|
||||
className={cn(item, current ? active : idle)}
|
||||
onClick={(event) => {
|
||||
haptic("tap");
|
||||
// Re-tapping the active tab scrolls back to the top.
|
||||
if (current) {
|
||||
event.preventDefault();
|
||||
window.scrollTo({ top: 0, behavior: reduceMotion ? "auto" : "smooth" });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{pill(current)}
|
||||
{icon(iconNode, current, key)}
|
||||
{label(labelText, current)}
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
|
||||
if (inWizard) return null;
|
||||
|
||||
return (
|
||||
<nav
|
||||
aria-label={t("tabbar.main")}
|
||||
className="tabbar fixed inset-x-0 bottom-0 z-40 flex items-stretch pb-safe sm:hidden"
|
||||
>
|
||||
<Link
|
||||
href="/"
|
||||
aria-current={libraryActive ? "page" : undefined}
|
||||
className={cn(item, libraryActive ? active : idle)}
|
||||
onClick={(event) => {
|
||||
haptic("tap");
|
||||
// Re-tapping the active tab scrolls back to the top.
|
||||
if (libraryActive) {
|
||||
event.preventDefault();
|
||||
window.scrollTo({ top: 0, behavior: reduceMotion ? "auto" : "smooth" });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{pill(libraryActive)}
|
||||
{icon(<HomeIcon className="size-6" />, libraryActive, "library")}
|
||||
{label(t("common.library"), libraryActive)}
|
||||
</Link>
|
||||
<Link
|
||||
href="/create"
|
||||
aria-current={createActive ? "page" : undefined}
|
||||
className={cn(item, createActive ? active : idle)}
|
||||
onClick={(event) => {
|
||||
haptic("tap");
|
||||
if (createActive) {
|
||||
event.preventDefault();
|
||||
window.scrollTo({ top: 0, behavior: reduceMotion ? "auto" : "smooth" });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{pill(createActive)}
|
||||
{icon(
|
||||
<span className="grad-ice grid size-6 place-items-center rounded-full text-[#06111f]">
|
||||
<PlusIcon className="size-4" />
|
||||
</span>,
|
||||
createActive,
|
||||
"create",
|
||||
)}
|
||||
{label(t("common.newDeck"), createActive)}
|
||||
</Link>
|
||||
{link("library", "/", t("tab.library"), <HomeIcon className="size-6" />)}
|
||||
{link("progress", "/stats", t("tab.progress"), <FlameIcon className="size-6" />)}
|
||||
{link(
|
||||
"create",
|
||||
"/create",
|
||||
t("tab.new"),
|
||||
<span className="grad-ice grid size-7 place-items-center rounded-full text-[#06111f]">
|
||||
<PlusIcon className="size-4" />
|
||||
</span>,
|
||||
)}
|
||||
{link("exam", "/exam", t("tab.exam"), <CompassIcon className="size-6" />)}
|
||||
<button
|
||||
type="button"
|
||||
aria-haspopup="dialog"
|
||||
onClick={() => {
|
||||
haptic("tap");
|
||||
openSettings();
|
||||
|
|
@ -112,7 +126,7 @@ export function TabBar() {
|
|||
className={cn(item, idle)}
|
||||
>
|
||||
{icon(<GearIcon className="size-6" />, false, "settings")}
|
||||
{label(t("common.settings"), false)}
|
||||
{label(t("tab.settings"), false)}
|
||||
</button>
|
||||
</nav>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue