UNPKG

@medalsocial/meda

Version:

Shared Meda UI shell and runtime package.

133 lines (132 loc) 11.6 kB
'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 })] })] })); }