@medalsocial/meda
Version:
Shared Meda UI shell and runtime package.
133 lines (132 loc) • 11.6 kB
JavaScript
'use client';
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { ChevronDown, Monitor, Moon, PanelRight, Sun } from 'lucide-react';
import { createElement, Fragment, isValidElement } from 'react';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '../components/ui/dropdown-menu.js';
import { cn } from '../lib/utils.js';
import { useMedaShell } from './shell-provider.js';
import { useTheme } from './theme.js';
import { useShellViewport } from './use-shell-viewport.js';
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 ThemeToggleMenuItem() {
const { theme, setTheme } = useTheme();
const Icon = THEME_ICON[theme];
return (_jsxs(DropdownMenuItem, { onClick: () => setTheme(NEXT_THEME[theme]), children: [_jsx(Icon, { size: 16, "aria-hidden": "true" }), THEME_LABEL[theme]] }));
}
/* v8 ignore next 9 — v8 phantom duplicate function record for renderShellIcon */
function renderShellIcon(icon) {
if (icon == null)
return null;
if (isValidElement(icon))
return icon;
if (typeof icon === 'function') {
return createElement(icon, { size: 16, 'aria-hidden': true });
}
// forwardRef/memo components are objects carrying a `$$typeof` symbol —
// treat them like Lucide components. Plain ReactNode objects (arrays,
// iterables, promises) have no `$$typeof` and are rendered as-is so they
// don't crash createElement with "Element type is invalid".
if (typeof icon === 'object' && icon !== null) {
const candidate = icon;
if (candidate.$$typeof != null) {
return createElement(icon, {
size: 16,
'aria-hidden': true,
});
}
}
return icon;
}
function renderConfiguredItem(item) {
const handleSelect = () => item.onClick?.();
// Childless anchor — Base UI merges the DropdownMenuItem's children into the
// cloned render element. Passing children here would override the icon +
// label children below and configured icons would silently disappear.
// biome-ignore lint/a11y/useAnchorContent: children are injected at render time by Base UI's `render` prop
const renderLink = item.href != null ? _jsx("a", { href: item.href }) : undefined;
return (_jsxs(DropdownMenuItem, { render: renderLink, "data-variant": item.variant ?? 'default', className: item.variant === 'destructive' ? 'text-destructive' : undefined, onClick: handleSelect, children: [renderShellIcon(item.icon), item.label] }));
}
/* v8 ignore next — v8 phantom duplicate function record for WorkspaceSwitcher (default params) */
export function WorkspaceSwitcher({ menuItems, menuFooter, workspaceMenuFooter, } = {}) {
const { workspace, workspaces } = useMedaShell();
const resolvedFooter = menuFooter ?? workspaceMenuFooter;
const useConfiguredItems = Array.isArray(menuItems);
return (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { render: _jsx("button", { type: "button", className: "flex min-w-0 items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm font-semibold hover:bg-accent" }), children: [workspace.icon != null && (_jsx("span", { className: "inline-flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-lg bg-muted text-foreground ring-1 ring-border/70", "aria-hidden": "true", children: workspace.icon })), _jsx("span", { className: "max-w-[13rem] truncate", children: workspace.name }), _jsx(ChevronDown, { size: 16, "aria-hidden": "true" })] }), _jsxs(DropdownMenuContent, { className: "min-w-[240px]", children: [!useConfiguredItems && workspaces.length > 0 && (_jsxs(_Fragment, { children: [workspaces.map((ws) => (_jsxs(DropdownMenuItem, { children: [ws.icon != null && (_jsx("span", { className: "inline-flex size-7 shrink-0 items-center justify-center overflow-hidden rounded-md bg-muted text-foreground ring-1 ring-border/70", "aria-hidden": "true", children: ws.icon })), ws.name] }, ws.id))), _jsx(DropdownMenuSeparator, {})] })), useConfiguredItems ? (menuItems.map((item) => (_jsxs(Fragment, { children: [renderConfiguredItem(item), item.separatorAfter && _jsx(DropdownMenuSeparator, {})] }, item.id)))) : (_jsxs(_Fragment, { children: [_jsx(DropdownMenuItem, { children: "Manage workspaces" }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { children: "Settings" }), _jsx(DropdownMenuItem, { children: "Profile" })] })), _jsx(DropdownMenuSeparator, {}), _jsx(ThemeToggleMenuItem, {}), !useConfiguredItems && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { children: "Sign out" })] })), resolvedFooter] })] }));
}
/* v8 ignore next — v8 phantom duplicate function record for AppTabs (default params) */
export function AppTabs({ renderLink } = {}) {
const { apps, activeAppId, setActiveApp } = useMedaShell();
return (_jsx("nav", { "aria-label": "Applications", className: "flex items-center", children: apps.map((app) => {
const isActive = app.id === activeAppId;
const className = cn('flex items-center gap-1.5 px-3 py-2 text-sm font-medium transition-colors', isActive
? 'border-b-2 border-primary text-foreground'
: 'text-muted-foreground hover:text-foreground');
const children = (_jsxs(_Fragment, { children: [renderShellIcon(app.icon), app.label] }));
const handleClick = () => {
setActiveApp(app.id);
};
if (renderLink && app.to) {
return (_jsx(Fragment, { children: renderLink({
app,
isActive,
className,
children,
linkProps: {
href: app.to,
className,
children,
'aria-current': isActive ? 'page' : undefined,
onClick: handleClick,
},
}) }, app.id));
}
return (_jsx("button", { type: "button", "aria-current": isActive ? 'page' : undefined, onClick: handleClick,
/* v8 ignore next — onMouseEnter placeholder lambda is never invoked in tests */
onMouseEnter: () => undefined, className: className, children: children }, app.id));
}) }));
}
export function PanelToggle({ panelViews = [] } = {}) {
const { panel } = useMedaShell();
const isOpen = panel.mode !== 'closed';
// No registered views → a single button that just opens/closes the panel.
if (panelViews.length === 0) {
return (_jsx("button", { type: "button", "aria-label": isOpen ? 'Close right panel' : 'Open right panel', "aria-pressed": isOpen, onClick: () => panel.setMode(isOpen ? 'closed' : 'panel'), className: cn('inline-flex h-9 items-center gap-1.5 rounded-[10px] bg-accent px-2.5 text-muted-foreground transition-colors hover:text-foreground', isOpen && 'text-foreground'), children: _jsx(PanelRight, { size: 18, "aria-hidden": "true" }) }));
}
// Clicking a view toggles it: selecting the already-open view closes the
// panel; selecting any other view opens the panel focused on that view.
const handleSelect = (viewId) => {
if (isOpen && panel.activeView === viewId) {
panel.close();
}
else {
panel.focus(viewId);
}
};
// ONE grouped control: the whole pill (panel icon + chevron) is a single
// dropdown trigger — not two separate buttons.
return (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { render: _jsx("button", { type: "button", "aria-label": "Right panel views", className: cn('inline-flex h-9 items-center gap-1.5 rounded-[10px] bg-accent px-2.5 text-muted-foreground transition-colors hover:text-foreground data-[popup-open]:text-foreground', isOpen && 'text-foreground') }), children: [_jsx(PanelRight, { size: 18, "aria-hidden": "true" }), _jsx(ChevronDown, { size: 14, "aria-hidden": "true" })] }), _jsxs(DropdownMenuContent, { align: "end", sideOffset: 8, className: "w-64 rounded-xl p-1.5", children: [_jsx("div", { className: "px-2 pt-1.5 pb-1 font-bold text-[10.5px] uppercase tracking-wider text-muted-foreground", children: "Panel views" }), panelViews.map((view) => {
const Icon = view.icon;
const active = isOpen && panel.activeView === view.id;
return (_jsxs(DropdownMenuItem, { onClick: () => handleSelect(view.id), className: cn('gap-2.5 rounded-[9px] px-2.5 py-2 text-sm', active && 'text-foreground'), children: [_jsx("span", { className: cn('grid h-[26px] w-[26px] shrink-0 place-items-center rounded-[7px] transition-colors', active ? 'bg-primary/15 text-primary' : 'bg-muted text-muted-foreground'), children: Icon != null ? _jsx(Icon, { size: 16, "aria-hidden": "true" }) : null }), _jsx("span", { className: "flex-1 font-medium", children: view.label }), active ? _jsx("span", { className: "size-1.5 rounded-full bg-primary" }) : null] }, view.id));
})] })] }));
}
/* v8 ignore next — v8 phantom duplicate function record for ShellHeader (default params) */
export function ShellHeader({ globalActions, headerCenter, headerLeading, appTabsRenderLink, className, workspaceMenuItems, workspaceMenuFooter, showPanelToggle = true, panelViews = [], } = {}) {
const band = useShellViewport();
if (band === 'mobile')
return null;
// Leading content (e.g. section tabs) sits in the LEFT region just after
// the WorkspaceSwitcher — separated by whitespace only (no divider line),
// matching the design prototype's topbar spacing.
const leadingRegion = headerLeading != null ? (_jsx("div", { className: "ml-2 flex min-w-0 items-center", children: headerLeading })) : null;
if (headerCenter !== undefined) {
return (_jsxs("header", { className: cn('grid h-[var(--shell-header-height)] w-full grid-cols-[minmax(0,1fr)_auto_minmax(0,1fr)] items-center', 'gap-4 bg-background px-4', className), children: [_jsxs("div", { className: "flex min-w-0 items-center gap-2 justify-self-start", children: [_jsx(WorkspaceSwitcher, { menuItems: workspaceMenuItems, menuFooter: workspaceMenuFooter }), leadingRegion] }), _jsx("div", { className: "flex min-w-0 items-center justify-center justify-self-center", children: headerCenter }), _jsxs("div", { className: "flex min-w-0 items-center justify-end gap-2 justify-self-end", children: [globalActions, showPanelToggle && _jsx(PanelToggle, { panelViews: panelViews })] })] }));
}
return (_jsxs("header", { className: cn('flex h-[var(--shell-header-height)] w-full items-center justify-between', 'gap-4 bg-background px-4', className), children: [_jsxs("div", { className: "flex min-w-0 shrink-0 items-center gap-2", children: [_jsx(WorkspaceSwitcher, { menuItems: workspaceMenuItems, menuFooter: workspaceMenuFooter }), leadingRegion] }), _jsx("div", { className: "flex min-w-0 flex-1 items-center", children: _jsx(AppTabs, { renderLink: appTabsRenderLink }) }), _jsxs("div", { className: "flex shrink-0 items-center gap-2", children: [globalActions, showPanelToggle && _jsx(PanelToggle, { panelViews: panelViews })] })] }));
}