@medalsocial/meda
Version:
Shared Meda UI shell and runtime package.
77 lines (76 loc) • 7.14 kB
JavaScript
'use client';
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
import { LayoutGrid, Menu, PanelTop, Sparkles } from 'lucide-react';
import { useContext } from 'react';
import { cn } from '../lib/utils.js';
import { CommandPalette, CommandRegistryContext } from './command-palette.js';
import { ContextRail } from './context-rail.js';
import { IconRail } from './icon-rail.js';
import { MobileBottomNav } from './internal/mobile-bottom-nav.js';
import { MobileDock } from './internal/mobile-dock.js';
import { MobileDrawers } from './internal/mobile-drawers.js';
import { MobileHeader } from './internal/mobile-header.js';
import { MobileWorkspaceSheet } from './internal/mobile-workspace-sheet.js';
import { useResolvedPanelViews } from './panel-views-provider.js';
import { RightPanel } from './right-panel.js';
import { ShellHeader } from './shell-header.js';
import { ShellMain } from './shell-main.js';
import { useShellViewport } from './use-shell-viewport.js';
const EMPTY_PANEL_VIEWS = [];
export function AppShellWorkspace({ iconRail, contextRail, rightPanel, workspace, appTabs, globalActions, headerCenter, headerLeading, banners, mainLayout, mainClassName, builtInCommandPalette = true, mobileNav, children, }) {
const viewport = useShellViewport();
const isMobile = viewport === 'mobile';
const staticPanelViews = rightPanel?.panelViews ?? EMPTY_PANEL_VIEWS;
const resolvedRightPanel = useResolvedPanelViews(staticPanelViews, rightPanel?.defaultView);
// Derive the bottom-nav items from the variant config so each button maps
// to a drawer that actually has content. Menu is always available because
// the mobile drawer now carries workspace actions and the theme toggle.
const navItems = buildMobileNavItems(contextRail, resolvedRightPanel.panelViews);
const hasDrawerContent = navItems.length > 0;
// Mobile menu drawer needs both main and utility items — desktop IconRail
// shows both, so dropping utilityItems here would orphan items like Help/
// Settings on mobile. Concat preserves discoverability; visual separation
// (divider in the drawer between main and utility) is a future polish.
const mobileMenuItems = iconRail ? [...iconRail.mainItems, ...(iconRail.utilityItems ?? [])] : [];
// ONE tree shape across both viewports. ShellMain stays at a fixed position
// in its parent's children array so React preserves its subtree (and the
// user's `children` state) when useShellViewport flips after mount or on
// rotation. Conditional siblings render as `false` rather than disappearing.
//
// h-screen (not h-full) so this stays a bounded scroll container even when
// rendered without an explicit-height ancestor (tests, direct imports). The
// <AppShell> wrapper already enforces h-screen for the workspace variant, so
// nested viewport-height divs collapse cleanly — no double-scroll.
const commandRegistry = useContext(CommandRegistryContext);
const shell = (_jsxs("div", { className: "flex h-svh flex-col", children: [isMobile ? (_jsx(MobileHeader, { globalActions: globalActions, headerCenter: headerCenter })) : (_jsx(ShellHeader, { globalActions: globalActions, headerCenter: headerCenter, headerLeading: headerLeading, appTabsRenderLink: appTabs?.renderLink, workspaceMenuItems: workspace?.menuItems, workspaceMenuFooter: workspace?.menuFooter, showPanelToggle: resolvedRightPanel.panelViews.length > 0, panelViews: resolvedRightPanel.panelViews })), banners ? (_jsx("div", { "data-meda-banners": "", className: "flex-shrink-0", children: banners })) : (false), _jsxs("div", { className: "relative flex flex-1 overflow-hidden", children: [!isMobile && iconRail && (_jsx(IconRail, { mainItems: iconRail.mainItems, utilityItems: iconRail.utilityItems, footer: iconRail.footer, activeId: iconRail.activeId, renderLink: iconRail.renderLink, labelVisibility: iconRail.labelVisibility })), !isMobile && contextRail && (_jsx(ContextRail, { appId: contextRail.appId, module: contextRail.module, activeItemId: contextRail.activeItemId, renderLink: contextRail.renderLink, header: contextRail.header, scroll: contextRail.scroll })), _jsx(ShellMain, { layout: mainLayout ?? 'workspace', className: cn(mainClassName,
// Pad the scroll area so the floating dock never covers content.
isMobile && mobileNav && 'pb-[calc(env(safe-area-inset-bottom)+80px)]'), children: children }), !isMobile && resolvedRightPanel.panelViews.length > 0 && (_jsx(RightPanel, { panelViews: staticPanelViews, defaultView: rightPanel?.defaultView }))] }), isMobile && mobileNav ? (_jsxs(_Fragment, { children: [_jsx(MobileDock, { items: mobileNav.dock, activeTo: mobileNav.activeTo, renderLink: mobileNav.renderLink, variant: mobileNav.variant }), _jsx(MobileWorkspaceSheet, { tree: mobileNav.tree, activeTo: mobileNav.activeTo, renderLink: mobileNav.renderLink, workspaceMenuItems: workspace?.menuItems, workspaceMenuFooter: workspace?.menuFooter })] })) : (_jsxs(_Fragment, { children: [isMobile && hasDrawerContent && _jsx(MobileBottomNav, { items: navItems }), isMobile && hasDrawerContent && (_jsx(MobileDrawers, { menuItems: mobileMenuItems, menuActiveId: iconRail?.activeId, menuRenderLink: iconRail?.renderLink, workspaceMenuItems: workspace?.menuItems, workspaceMenuFooter: workspace?.menuFooter, module: contextRail?.module, moduleAppId: contextRail?.appId, moduleActiveItemId: contextRail?.activeItemId, moduleRenderLink: contextRail?.renderLink, moduleHeader: contextRail?.header, moduleScroll: contextRail?.scroll, sectionTabs: headerLeading, panelViews: resolvedRightPanel.panelViews, defaultView: resolvedRightPanel.defaultView }))] }))] }));
// Backwards-compatible default: meda provides the built-in CommandPalette
// (and its CommandRegistryContext) so consumers calling useCommands()/
// useCommandGroup() under the workspace shell keep working. When a registry is
// already present, or the consumer opts out via builtInCommandPalette={false}
// (e.g. apps that ship their own palette), render the bare shell to avoid a
// duplicate dialog.
if (commandRegistry || !builtInCommandPalette) {
return shell;
}
return _jsx(CommandPalette, { children: shell });
}
function buildMobileNavItems(contextRail, panelViews) {
const items = [
{ id: 'menu', label: 'Menu', icon: Menu, opens: 'menu-drawer' },
];
if (contextRail?.module &&
((contextRail.module.items ?? []).length > 0 || Boolean(contextRail.module.render))) {
items.push({ id: 'module', label: 'Module', icon: LayoutGrid, opens: 'module-drawer' });
}
// Panels button only when there's an actual view to render — empty
// panelViews would open an empty drawer (dead-end tap).
if (panelViews.length > 0) {
items.push({ id: 'panels', label: 'Panels', icon: PanelTop, opens: 'panels-drawer' });
if (panelViews.some((v) => v.id === 'ai')) {
items.push({ id: 'ai', label: 'AI', icon: Sparkles, opens: 'ai-drawer' });
}
}
return items;
}