UNPKG

@medalsocial/meda

Version:

Shared Meda UI shell and runtime package.

101 lines (100 loc) 8.87 kB
'use client'; import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime"; import { ChevronDown, ChevronRight, ChevronUp, Monitor, Moon, Search, Sun, } from 'lucide-react'; import { createElement, Fragment, isValidElement, useState } from 'react'; import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle, } from '../../components/ui/drawer.js'; import { cn } from '../../lib/utils.js'; import { useMedaShell } from '../shell-provider.js'; import { useTheme } from '../theme.js'; import { WORKSPACE_SHEET_KEY } from './mobile-dock.js'; function renderIcon(icon, size) { if (icon == null) return null; if (isValidElement(icon)) return icon; if (typeof icon === 'function' || typeof icon === 'object') { return createElement(icon, { size, 'aria-hidden': true }); } return null; } const rowClass = 'flex w-full items-center gap-3 rounded-[10px] px-2 py-2.5 text-left text-[13.5px] transition-colors hover:bg-accent'; /** * The one mobile workspace sheet — a calm, scannable list of the whole nav * tree that replaces the four drawers. Modules are light group labels, * submodules are rows: a `›` navigates, a `⌄` reveals a slim chip row of * preset views (accordion, one open at a time). Filters, layout, and saved * views deliberately stay on the page, not here. Opened from the dock's * workspace-selector slot; a workspace header on top surfaces the account menu. */ export function MobileWorkspaceSheet({ tree, activeTo, renderLink, workspaceMenuItems, workspaceMenuFooter, }) { const ctx = useMedaShell(); const open = ctx.mobileDrawer.open === WORKSPACE_SHEET_KEY; const close = () => ctx.mobileDrawer.setOpen(null); const [expandedId, setExpandedId] = useState(null); const [accountOpen, setAccountOpen] = useState(false); const navLink = (to, label, className, children, isActive) => { if (renderLink) { return renderLink({ to, isActive, className, children, onNavigate: close, linkProps: { href: to, className, 'aria-label': label, 'aria-current': isActive ? 'page' : undefined, onClick: close, }, }); } return (_jsx("a", { href: to, className: className, "aria-label": label, onClick: close, children: children })); }; const renderRow = (item) => { const isActive = Boolean(activeTo && item.to === activeTo); const hasViews = Boolean(item.views && item.views.length > 0); const icon = renderIcon(item.icon, 17); const badge = item.badge ? (_jsx("span", { className: "rounded-full bg-accent px-1.5 py-0.5 text-[11px] text-muted-foreground", children: item.badge })) : null; // Plain navigate row — no presets. if (!hasViews) { return navLink(item.to, item.label, cn(rowClass, isActive ? 'text-foreground' : 'text-foreground/90'), _jsxs(_Fragment, { children: [_jsx("span", { className: "shrink-0 text-muted-foreground", children: icon }), _jsx("span", { className: "flex-1", children: item.label }), badge, _jsx(ChevronRight, { className: "size-4 shrink-0 text-muted-foreground/50", "aria-hidden": "true" })] }), isActive); } // Expandable row — reveals a slim chip row of preset views. const isOpen = expandedId === item.id; return (_jsxs(Fragment, { children: [_jsxs("button", { type: "button", onClick: () => setExpandedId(isOpen ? null : item.id), "aria-expanded": isOpen, className: cn(rowClass, isOpen ? 'text-primary' : 'text-foreground/90'), children: [_jsx("span", { className: cn('shrink-0', isOpen ? 'text-primary' : 'text-muted-foreground'), children: icon }), _jsx("span", { className: "flex-1", children: item.label }), badge, isOpen ? (_jsx(ChevronUp, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": "true" })) : (_jsx(ChevronDown, { className: "size-4 shrink-0 text-muted-foreground", "aria-hidden": "true" }))] }), isOpen ? (_jsx("div", { className: "flex flex-wrap gap-1.5 pt-1 pb-2 pl-[37px]", children: item.views?.map((view) => { const viewActive = Boolean(activeTo && view.to === activeTo); return (_jsx(Fragment, { children: navLink(view.to, `${item.label} · ${view.label}`, cn('rounded-full px-3 py-1.5 text-[12px] transition-colors', viewActive ? 'bg-primary/12 text-primary' : 'border border-border text-muted-foreground hover:text-foreground'), view.label, viewActive) }, view.id)); }) })) : null] }, item.id)); }; return (_jsx(Drawer, { open: open, onOpenChange: (o) => !o && close(), direction: "bottom", children: _jsxs(DrawerContent, { showOverlay: true, className: "max-h-[86vh] rounded-t-[22px] p-0 data-[vaul-drawer-direction=bottom]:mt-0 data-[vaul-drawer-direction=bottom]:max-h-[86vh]", children: [_jsxs(DrawerHeader, { className: "sr-only", children: [_jsx(DrawerTitle, { children: "Navigation" }), _jsx(DrawerDescription, { children: "Jump to any area of the workspace." })] }), _jsxs("div", { className: "min-h-0 flex-1 overflow-y-auto px-3 pt-1 pb-[calc(env(safe-area-inset-bottom)+12px)]", children: [_jsxs("button", { type: "button", onClick: () => setAccountOpen((v) => !v), "aria-expanded": accountOpen, className: "flex w-full items-center gap-2.5 rounded-[10px] px-1.5 py-2 text-left hover:bg-accent", children: [_jsx("span", { className: "inline-flex size-7 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted ring-1 ring-border/70", children: ctx.workspace.icon }), _jsx("span", { className: "min-w-0 flex-1 truncate font-medium text-sm", children: ctx.workspace.name }), accountOpen ? (_jsx(ChevronUp, { className: "size-4 text-muted-foreground", "aria-hidden": "true" })) : (_jsx(ChevronDown, { className: "size-4 text-muted-foreground", "aria-hidden": "true" }))] }), accountOpen ? (_jsxs("div", { className: "mb-1 flex flex-col gap-0.5 border-b pb-2", children: [workspaceMenuItems?.map((item) => (_jsxs(Fragment, { children: [_jsx(WorkspaceMenuRow, { item: item, onClose: close }), item.separatorAfter ? _jsx("div", { className: "my-1 h-px bg-border" }) : null] }, item.id))), _jsx(ThemeRow, {}), workspaceMenuFooter] })) : null, _jsxs("button", { type: "button", onClick: () => { close(); ctx.commandPalette.setOpen(true); }, className: "my-2 flex w-full items-center gap-2 rounded-[10px] border px-3 py-2.5 text-left text-muted-foreground text-sm", children: [_jsx(Search, { className: "size-4", "aria-hidden": "true" }), "Search\u2026"] }), tree.groups.map((group) => (_jsxs("section", { "aria-label": group.label, children: [_jsx("p", { className: "px-2 pt-3 pb-1 text-[11px] text-muted-foreground", children: group.label }), _jsx("div", { className: "flex flex-col gap-0.5", children: group.items.map((item) => (_jsx(Fragment, { children: renderRow(item) }, item.id))) })] }, group.id))), tree.footerItems && tree.footerItems.length > 0 ? (_jsxs(_Fragment, { children: [_jsx("div", { className: "mx-2 my-2 h-px bg-border" }), _jsx("div", { className: "flex flex-col gap-0.5", children: tree.footerItems.map((item) => (_jsx(Fragment, { children: renderRow(item) }, item.id))) })] })) : null] })] }) })); } function WorkspaceMenuRow({ item, onClose }) { const icon = renderIcon(item.icon, 16); const className = cn(rowClass, item.variant === 'destructive' && 'text-destructive'); const handleClick = () => { item.onClick?.(); onClose(); }; if (item.href != null) { return (_jsxs("a", { href: item.href, className: className, onClick: handleClick, children: [_jsx("span", { className: "shrink-0 text-muted-foreground", children: icon }), item.label] })); } return (_jsxs("button", { type: "button", className: className, onClick: handleClick, children: [_jsx("span", { className: "shrink-0 text-muted-foreground", children: icon }), item.label] })); } const NEXT_THEME = { light: 'dark', dark: 'system', system: 'light' }; const THEME_ICON = { light: Sun, dark: Moon, system: Monitor }; const THEME_LABEL = { light: 'Switch to dark theme', dark: 'Switch to system theme', system: 'Switch to light theme', }; function ThemeRow() { const { theme, setTheme } = useTheme(); const Icon = THEME_ICON[theme]; return (_jsxs("button", { type: "button", className: rowClass, onClick: () => setTheme(NEXT_THEME[theme]), children: [_jsx("span", { className: "shrink-0 text-muted-foreground", children: _jsx(Icon, { size: 16, "aria-hidden": "true" }) }), THEME_LABEL[theme]] })); }