Add Phase 1 Pocket Pascal UI: home, pantry, week plan, and plate.
Replace the hello-world shell with the elevated design system, bilingual screens, recipe/extras data, and the interactive Mein Teller view. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
88
handover/component-library/components/BottomNav.tsx
Normal file
88
handover/component-library/components/BottomNav.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import type { ReactNode } from "react";
|
||||
import { cx } from "../lib/cx";
|
||||
|
||||
export interface NavItem {
|
||||
key: string;
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
active?: boolean;
|
||||
onClick?: () => void;
|
||||
href?: string;
|
||||
}
|
||||
|
||||
export interface BottomNavProps {
|
||||
items: NavItem[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Floating Glass Pill bottom navigation.
|
||||
*
|
||||
* - Fixed to the bottom, centered, floats above content (not edge-to-edge).
|
||||
* - Respects `env(safe-area-inset-bottom)` so it clears the iOS home indicator.
|
||||
* - Active item gets the accent gradient applied directly to icon + label
|
||||
* (bg-clip-text) rather than a pill behind it, so it reads against the
|
||||
* glass background instead of competing with it.
|
||||
*/
|
||||
export function BottomNav({ items, className }: BottomNavProps) {
|
||||
return (
|
||||
<nav
|
||||
className={cx("fixed inset-x-0 bottom-0 z-50 flex justify-center pointer-events-none", className)}
|
||||
style={{ paddingBottom: "calc(env(safe-area-inset-bottom) + 12px)" }}
|
||||
>
|
||||
<div
|
||||
className="relative flex items-center gap-1 rounded-full px-2 py-2 pointer-events-auto"
|
||||
style={{ boxShadow: "var(--shadow-ambient)" }}
|
||||
>
|
||||
<div
|
||||
className="absolute inset-0 rounded-full"
|
||||
style={{
|
||||
background: "var(--glass-fill)",
|
||||
backdropFilter: "blur(var(--glass-blur))",
|
||||
WebkitBackdropFilter: "blur(var(--glass-blur))",
|
||||
}}
|
||||
/>
|
||||
<div className="glass-border pointer-events-none absolute inset-0 rounded-full" />
|
||||
|
||||
{items.map((item) => {
|
||||
const content = (
|
||||
<>
|
||||
<span
|
||||
aria-hidden
|
||||
className={cx(
|
||||
"text-xl leading-none",
|
||||
item.active ? "bg-clip-text text-transparent" : "text-[var(--ink-soft)]"
|
||||
)}
|
||||
style={item.active ? { backgroundImage: "var(--gradient-accent)" } : undefined}
|
||||
>
|
||||
{item.icon}
|
||||
</span>
|
||||
<span
|
||||
className={cx(
|
||||
"font-body text-[10px] font-bold leading-none",
|
||||
item.active ? "bg-clip-text text-transparent" : "text-[var(--ink-soft)]"
|
||||
)}
|
||||
style={item.active ? { backgroundImage: "var(--gradient-accent)" } : undefined}
|
||||
>
|
||||
{item.label}
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
const itemClass = cx(
|
||||
"relative z-10 flex min-w-[48px] min-h-[48px] flex-col items-center justify-center gap-0.5",
|
||||
"elevation-transition rounded-full px-3 no-select active:translate-y-px active:brightness-95"
|
||||
);
|
||||
return item.href ? (
|
||||
<a key={item.key} href={item.href} className={itemClass}>
|
||||
{content}
|
||||
</a>
|
||||
) : (
|
||||
<button key={item.key} type="button" onClick={item.onClick} className={itemClass}>
|
||||
{content}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user