UNPKG

@medalsocial/meda

Version:

Shared Meda UI shell and runtime package.

118 lines (117 loc) 7.57 kB
'use client'; import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime"; import { createElement, isValidElement } from 'react'; import { cn } from '../../lib/utils.js'; import { useMedaShell } from '../shell-provider.js'; import { useShellViewport } from '../use-shell-viewport.js'; /** The key the workspace sheet listens on (reuses the single mobile-drawer state). */ export const WORKSPACE_SHEET_KEY = 'workspace-sheet'; function renderIcon(icon, size) { if (icon == null) return null; if (isValidElement(icon)) return icon; if (typeof icon === 'function' || typeof icon === 'object') { // Lucide component (function) or forwardRef/memo object. return createElement(icon, { size, 'aria-hidden': true }); } return null; } /** * Mobile dock — pinned destinations plus a workspace-sheet trigger. Two shapes: * * - `pill` (default): a floating, centered pill of icon-only slots, with any * `emphasis: 'brand'` item (e.g. Pilot) as a standalone circle beside it. * - `bar`: a flat, full-width bottom bar of evenly-spread slots — each a larger * icon + label with a tinted active state — mirroring the native app. The * brand item sits inline as the last slot (a filled circle). * * Hidden on non-mobile viewports and when the right panel is fullscreen. */ export function MobileDock({ items, activeTo, renderLink, className, variant = 'pill', }) { const ctx = useMedaShell(); const band = useShellViewport(); if (band !== 'mobile') return null; if (ctx.panel.mode === 'fullscreen') return null; if (items.length === 0) return null; const runAction = (item) => { if (item.action === 'open-sheet') ctx.mobileDrawer.setOpen(WORKSPACE_SHEET_KEY); else if (item.action === 'open-ai') ctx.mobileDrawer.setOpen('ai-drawer'); else if (item.action === 'open-command-palette') ctx.commandPalette.setOpen(true); }; // ── Full-width bar (native-app style) ────────────────────────────────── if (variant === 'bar') { const renderBarSlot = (item) => { const label = typeof item.label === 'function' ? item.label() : item.label; const isActive = Boolean(item.to && activeTo && item.to === activeTo); const isBrand = item.emphasis === 'brand'; const iconEl = isBrand ? (_jsx("span", { className: "flex size-8 items-center justify-center rounded-full bg-[#5B2D8C] text-white", children: renderIcon(item.icon, 20) })) : (renderIcon(item.icon, 25)); let toneClass = 'text-muted-foreground hover:text-foreground'; if (isBrand) toneClass = 'text-muted-foreground'; else if (isActive) toneClass = 'font-medium text-primary'; const slotClass = cn('flex w-full flex-col items-center justify-center gap-1 py-2 text-[11px] leading-none transition-colors', toneClass); const children = (_jsxs(_Fragment, { children: [_jsxs("span", { className: "relative flex items-center justify-center", children: [iconEl, item.badge ? (_jsx("span", { className: "absolute -top-0.5 -right-1 size-2 rounded-full bg-primary ring-2 ring-card" })) : null] }), _jsx("span", { children: label })] })); if (item.to && !item.action) { if (renderLink) { return renderLink({ to: item.to, isActive, className: slotClass, children, onNavigate: () => undefined, linkProps: { href: item.to, className: slotClass, 'aria-label': label, 'aria-current': isActive ? 'page' : undefined, }, }); } return (_jsx("a", { href: item.to, className: slotClass, "aria-label": label, "aria-current": isActive ? 'page' : undefined, children: children })); } return (_jsx("button", { type: "button", onClick: () => runAction(item), "aria-label": label, className: slotClass, children: children })); }; return (_jsx("nav", { "data-testid": "mobile-dock", "aria-label": "Dock", className: cn('absolute inset-x-0 bottom-0 z-30 flex items-stretch border-t border-border bg-card pb-[env(safe-area-inset-bottom)]', className), children: items.map((item) => (_jsx("div", { className: "flex flex-1", children: renderBarSlot(item) }, item.id))) })); } // ── Floating pill (default) ──────────────────────────────────────────── const pillItems = items.filter((item) => item.emphasis !== 'brand'); const brandItems = items.filter((item) => item.emphasis === 'brand'); const renderSlot = (item, size) => { const label = typeof item.label === 'function' ? item.label() : item.label; const isActive = Boolean(item.to && activeTo && item.to === activeTo); const icon = renderIcon(item.icon, size); const badge = item.badge ? (_jsx("span", { className: "absolute top-0 right-0 size-2 rounded-full bg-primary ring-2 ring-card" })) : null; // Route link if (item.to && !item.action) { const className = cn('relative flex items-center justify-center rounded-full p-1 transition-colors', isActive ? 'text-foreground' : 'text-muted-foreground hover:text-foreground'); const children = (_jsxs(_Fragment, { children: [icon, badge] })); if (renderLink) { return renderLink({ to: item.to, isActive, className, children, onNavigate: () => undefined, linkProps: { href: item.to, className, 'aria-label': label, 'aria-current': isActive ? 'page' : undefined, }, }); } return (_jsx("a", { href: item.to, className: className, "aria-label": label, "aria-current": isActive ? 'page' : undefined, children: children })); } // Action slot return (_jsxs("button", { type: "button", onClick: () => runAction(item), "aria-label": label, className: "relative flex items-center justify-center rounded-full p-1 text-muted-foreground transition-colors hover:text-foreground", children: [icon, badge] })); }; return (_jsxs("div", { "data-testid": "mobile-dock", className: cn('pointer-events-none absolute inset-x-0 bottom-0 z-30 flex items-center justify-center gap-2.5 pb-[calc(env(safe-area-inset-bottom)+12px)] pt-2', className), children: [_jsx("nav", { "aria-label": "Dock", className: "pointer-events-auto flex items-center gap-[18px] rounded-full border border-border bg-card px-4 py-2.5 shadow-[0_6px_20px_rgba(20,18,60,0.14)]", children: pillItems.map((item) => (_jsx("span", { className: "flex", children: renderSlot(item, 22) }, item.id))) }), brandItems.map((item) => (_jsx("span", { className: "pointer-events-auto flex size-11 items-center justify-center rounded-full bg-[#5B2D8C] text-white shadow-[0_6px_20px_rgba(91,45,140,0.35)]", children: renderSlot(item, 22) }, item.id)))] })); }