@medalsocial/meda
Version:
Shared Meda UI shell and runtime package.
101 lines (100 loc) • 8.87 kB
JavaScript
'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]] }));
}