UNPKG

@medalsocial/meda

Version:

Shared Meda UI shell and runtime package.

81 lines (80 loc) 3.76 kB
'use client'; import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { useRef } from 'react'; import { cn } from '../../lib/utils.js'; import { useMedaShell } from '../shell-provider.js'; import { useShellViewport } from '../use-shell-viewport.js'; /** * Mobile-only bottom navigation bar. * * Renders 4 buttons sourced from `ctx.mobileBottomNav` (provider-controlled, * overrideable per-app via `mobileBottomNav` prop). Each button click opens * the corresponding drawer via `ctx.mobileDrawer.setOpen`. * * The Menu button supports long-press (500ms) to open the command palette. * * Hidden on non-mobile viewports and when the right panel is in fullscreen mode. */ export function MobileBottomNav({ items, className }) { const ctx = useMedaShell(); const band = useShellViewport(); if (band !== 'mobile') return null; if (ctx.panel.mode === 'fullscreen') return null; const resolvedItems = items ?? ctx.mobileBottomNav; if (resolvedItems.length === 0) return null; return (_jsx("nav", { "data-testid": "mobile-bottom-nav", "aria-label": "Mobile navigation", className: cn('flex h-[calc(var(--shell-bottom-nav-height)+env(safe-area-inset-bottom))] items-start justify-around border-t border-border bg-card px-1 pb-[env(safe-area-inset-bottom)]', className), children: resolvedItems.map((item) => (_jsx(MobileBottomNavButton, { item: item }, item.id))) })); } /** Long-press threshold in milliseconds */ const LONG_PRESS_DURATION = 500; /* v8 ignore next — v8 phantom duplicate function record for MobileBottomNavButton */ function MobileBottomNavButton({ item }) { const ctx = useMedaShell(); const Icon = item.icon; const label = typeof item.label === 'function' ? item.label() : item.label; const isMenu = item.id === 'menu'; const longPressTimerRef = useRef(null); const longPressFiredRef = useRef(false); const cancelTimer = () => { if (longPressTimerRef.current !== null) { clearTimeout(longPressTimerRef.current); longPressTimerRef.current = null; } }; const handlePointerDown = () => { if (!isMenu) return; longPressFiredRef.current = false; longPressTimerRef.current = setTimeout(() => { longPressFiredRef.current = true; longPressTimerRef.current = null; ctx.commandPalette.setOpen(true); }, LONG_PRESS_DURATION); }; const handlePointerUp = () => { cancelTimer(); }; const handlePointerLeave = () => { cancelTimer(); }; const handlePointerCancel = () => { cancelTimer(); }; const handleClick = () => { if (isMenu && longPressFiredRef.current) { // Long-press already fired — suppress the regular click action longPressFiredRef.current = false; return; } if (typeof item.opens === 'string') { ctx.mobileDrawer.setOpen(item.opens); } else { // Custom render fn — open a custom drawer keyed by the item's id ctx.mobileDrawer.setOpen(item.id); } }; return (_jsxs("button", { type: "button", onPointerDown: handlePointerDown, onPointerUp: handlePointerUp, onPointerLeave: handlePointerLeave, onPointerCancel: handlePointerCancel, onClick: handleClick, "aria-label": label, className: "flex h-[var(--shell-bottom-nav-height)] flex-1 flex-col items-center justify-center gap-0.5 rounded-xl px-1 text-muted-foreground transition-colors hover:text-foreground active:bg-accent", children: [_jsx(Icon, { size: 20, "aria-hidden": "true" }), _jsx("span", { className: "font-medium text-[10px] leading-none", children: label })] })); }