@medalsocial/meda 2.4.1 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,9 +8,17 @@ export interface AppShellWorkspaceProps {
8
8
  appTabs?: AppShellAppTabsConfig;
9
9
  globalActions?: ReactNode;
10
10
  headerCenter?: ReactNode;
11
+ headerLeading?: ReactNode;
11
12
  banners?: ReactNode;
12
13
  mainLayout?: ShellMainLayout;
13
14
  mainClassName?: string;
15
+ /**
16
+ * Whether to render the built-in `CommandPalette` (providing the
17
+ * `CommandRegistryContext`) when no registry is already in context. Defaults
18
+ * to `true` for backwards compatibility; set `false` to opt out when the host
19
+ * app ships its own palette.
20
+ */
21
+ builtInCommandPalette?: boolean;
14
22
  children: ReactNode;
15
23
  }
16
- export declare function AppShellWorkspace({ iconRail, contextRail, rightPanel, workspace, appTabs, globalActions, headerCenter, banners, mainLayout, mainClassName, children, }: AppShellWorkspaceProps): import("react/jsx-runtime").JSX.Element;
24
+ export declare function AppShellWorkspace({ iconRail, contextRail, rightPanel, workspace, appTabs, globalActions, headerCenter, headerLeading, banners, mainLayout, mainClassName, builtInCommandPalette, children, }: AppShellWorkspaceProps): import("react/jsx-runtime").JSX.Element;
@@ -14,7 +14,7 @@ import { ShellHeader } from './shell-header.js';
14
14
  import { ShellMain } from './shell-main.js';
15
15
  import { useShellViewport } from './use-shell-viewport.js';
16
16
  const EMPTY_PANEL_VIEWS = [];
17
- export function AppShellWorkspace({ iconRail, contextRail, rightPanel, workspace, appTabs, globalActions, headerCenter, banners, mainLayout, mainClassName, children, }) {
17
+ export function AppShellWorkspace({ iconRail, contextRail, rightPanel, workspace, appTabs, globalActions, headerCenter, headerLeading, banners, mainLayout, mainClassName, builtInCommandPalette = true, children, }) {
18
18
  const viewport = useShellViewport();
19
19
  const isMobile = viewport === 'mobile';
20
20
  const staticPanelViews = rightPanel?.panelViews ?? EMPTY_PANEL_VIEWS;
@@ -39,8 +39,17 @@ export function AppShellWorkspace({ iconRail, contextRail, rightPanel, workspace
39
39
  // <AppShell> wrapper already enforces h-screen for the workspace variant, so
40
40
  // nested viewport-height divs collapse cleanly — no double-scroll.
41
41
  const commandRegistry = useContext(CommandRegistryContext);
42
- const shell = (_jsxs("div", { className: "flex h-screen flex-col", children: [isMobile ? (_jsx(MobileHeader, { globalActions: globalActions })) : (_jsx(ShellHeader, { globalActions: globalActions, headerCenter: headerCenter, appTabsRenderLink: appTabs?.renderLink, workspaceMenuItems: workspace?.menuItems, workspaceMenuFooter: workspace?.menuFooter })), 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 })), !isMobile && contextRail && (_jsx(ContextRail, { appId: contextRail.appId, module: contextRail.module, activeItemId: contextRail.activeItemId, header: contextRail.header, scroll: contextRail.scroll })), _jsx(ShellMain, { layout: mainLayout ?? 'workspace', className: mainClassName, children: children }), !isMobile && resolvedRightPanel.panelViews.length > 0 && (_jsx(RightPanel, { panelViews: staticPanelViews, defaultView: rightPanel?.defaultView }))] }), 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, moduleHeader: contextRail?.header, moduleScroll: contextRail?.scroll, panelViews: resolvedRightPanel.panelViews, defaultView: resolvedRightPanel.defaultView }))] }));
43
- return commandRegistry ? shell : _jsx(CommandPalette, { children: shell });
42
+ 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: mainClassName, children: children }), !isMobile && resolvedRightPanel.panelViews.length > 0 && (_jsx(RightPanel, { panelViews: staticPanelViews, defaultView: rightPanel?.defaultView }))] }), 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 }))] }));
43
+ // Backwards-compatible default: meda provides the built-in CommandPalette
44
+ // (and its CommandRegistryContext) so consumers calling useCommands()/
45
+ // useCommandGroup() under the workspace shell keep working. When a registry is
46
+ // already present, or the consumer opts out via builtInCommandPalette={false}
47
+ // (e.g. apps that ship their own palette), render the bare shell to avoid a
48
+ // duplicate dialog.
49
+ if (commandRegistry || !builtInCommandPalette) {
50
+ return shell;
51
+ }
52
+ return _jsx(CommandPalette, { children: shell });
44
53
  }
45
54
  function buildMobileNavItems(contextRail, panelViews) {
46
55
  const items = [
@@ -32,6 +32,11 @@ export type AppShellProps = AppShellBaseProps & ({
32
32
  * application tabs when provided.
33
33
  */
34
34
  headerCenter?: ReactNode;
35
+ /**
36
+ * Optional leading content rendered in the LEFT header region immediately
37
+ * after the workspace switcher. On mobile, surfaced inside the menu drawer.
38
+ */
39
+ headerLeading?: ReactNode;
35
40
  /**
36
41
  * Optional chrome-level content rendered below the header and above
37
42
  * the workspace rail row.
@@ -46,6 +51,13 @@ export type AppShellProps = AppShellBaseProps & ({
46
51
  * Optional className for the workspace shell's main scroll region.
47
52
  */
48
53
  mainClassName?: string;
54
+ /**
55
+ * Whether meda renders its built-in `CommandPalette` (and provides the
56
+ * `CommandRegistryContext`) when no command registry is already present.
57
+ * Defaults to `true` for backwards compatibility. Set `false` when the
58
+ * host app ships its own command palette to avoid a duplicate dialog.
59
+ */
60
+ builtInCommandPalette?: boolean;
49
61
  } | {
50
62
  variant: 'chat';
51
63
  globalActions?: ReactNode;
@@ -10,13 +10,13 @@ export function AppShell(props) {
10
10
  // Auth lets the form scroll past viewport (signup, dense forms, high zoom);
11
11
  // workspace and chat fix the chrome to viewport height and let inner regions
12
12
  // scroll independently.
13
- const heightClass = props.variant === 'auth' ? 'min-h-screen' : 'h-screen overflow-hidden';
13
+ const heightClass = props.variant === 'auth' ? 'min-h-screen' : 'h-svh overflow-hidden';
14
14
  const wrapper = (content) => (_jsx("div", { "data-meda-app": activeAppId, "data-meda-workspace": workspace.id, "data-meda-variant": props.variant, className: cn(heightClass, 'bg-background text-foreground', props.className), children: content }));
15
15
  switch (props.variant) {
16
16
  case 'auth':
17
17
  return wrapper(_jsx(AppShellAuth, { ...resolveAuthConfig(props), children: props.children }));
18
18
  case 'workspace':
19
- return wrapper(_jsx(AppShellWorkspace, { iconRail: props.iconRail, contextRail: props.contextRail, rightPanel: props.rightPanel, workspace: props.workspace, appTabs: props.appTabs, globalActions: props.globalActions, headerCenter: props.headerCenter, banners: props.banners, mainLayout: props.mainLayout, mainClassName: props.mainClassName, children: props.children }));
19
+ return wrapper(_jsx(AppShellWorkspace, { iconRail: props.iconRail, contextRail: props.contextRail, rightPanel: props.rightPanel, workspace: props.workspace, appTabs: props.appTabs, globalActions: props.globalActions, headerCenter: props.headerCenter, headerLeading: props.headerLeading, banners: props.banners, mainLayout: props.mainLayout, mainClassName: props.mainClassName, builtInCommandPalette: props.builtInCommandPalette, children: props.children }));
20
20
  case 'chat':
21
21
  return wrapper(_jsx(AppShellChat, { globalActions: props.globalActions, children: props.children }));
22
22
  }
@@ -34,5 +34,5 @@ function resolveAuthConfig(props) {
34
34
  };
35
35
  }
36
36
  export function AppShellBody({ children, className }) {
37
- return (_jsx("div", { className: cn('relative flex h-[calc(100vh-var(--shell-header-height))] overflow-hidden', className), children: children }));
37
+ return (_jsx("div", { className: cn('relative flex h-[calc(100svh-var(--shell-header-height))] overflow-hidden', className), children: children }));
38
38
  }
@@ -14,7 +14,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
14
14
  * TODO(phase-11): replace pointer-events handle with <ResizableShellPanel>
15
15
  * once <AppShellBody> ships as a ResizableShell Group.
16
16
  */
17
- import { PanelLeftClose, PanelLeftOpen } from 'lucide-react';
17
+ import { ChevronLeft, ChevronRight } from 'lucide-react';
18
18
  import { Fragment, useId, useRef, useState } from 'react';
19
19
  import { cn } from '../lib/utils.js';
20
20
  import { useMedaShell } from './shell-provider.js';
@@ -24,11 +24,16 @@ import { useShellViewport } from './use-shell-viewport.js';
24
24
  // ---------------------------------------------------------------------------
25
25
  const MIN_WIDTH = 240;
26
26
  const MAX_WIDTH = 420;
27
- function ResizeHandle({ currentWidth, onResize, onCommit }) {
27
+ function ContextRailSeam({ railId, currentWidth, onResize, onCommit }) {
28
+ const ctx = useMedaShell();
29
+ const collapsed = ctx.contextRail.collapsed;
30
+ const Icon = collapsed ? ChevronRight : ChevronLeft;
28
31
  const startWidthRef = useRef(currentWidth);
29
32
  const startXRef = useRef(0);
30
33
  const draggingRef = useRef(false);
31
34
  const handlePointerDown = (e) => {
35
+ if (collapsed)
36
+ return; // no resize while collapsed
32
37
  // setPointerCapture may not be available in all test environments (jsdom)
33
38
  try {
34
39
  e.currentTarget.setPointerCapture(e.pointerId);
@@ -53,24 +58,16 @@ function ResizeHandle({ currentWidth, onResize, onCommit }) {
53
58
  const next = Math.min(MAX_WIDTH, Math.max(MIN_WIDTH, startWidthRef.current + (e.clientX - startXRef.current)));
54
59
  onCommit(next);
55
60
  };
56
- return (_jsx("div", { role: "separator", "aria-orientation": "vertical", "aria-label": "Resize context rail", "aria-valuenow": currentWidth, "aria-valuemin": MIN_WIDTH, "aria-valuemax": MAX_WIDTH, tabIndex: 0, className: cn('absolute top-0 right-0 h-full w-1 cursor-col-resize', 'opacity-0 transition-opacity hover:opacity-100 hover:bg-ring'), onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp }));
57
- }
58
- // ---------------------------------------------------------------------------
59
- // ContextRailToggle — chevron button that flips ctx.contextRail.collapsed
60
- // ---------------------------------------------------------------------------
61
- function ContextRailToggle({ railId }) {
62
- const ctx = useMedaShell();
63
- const collapsed = ctx.contextRail.collapsed;
64
- // Lucide PanelLeft* icons render the chevron-with-wall pattern (similar
65
- // to Unifi). The wall reinforces "this is a sidebar toggle" rather than
66
- // a generic navigation chevron.
67
- const Icon = collapsed ? PanelLeftOpen : PanelLeftClose;
68
- return (_jsx("button", { type: "button", onClick: () => ctx.contextRail.setCollapsed(!collapsed), "aria-label": collapsed ? 'Expand sidebar' : 'Collapse sidebar', "aria-expanded": !collapsed, "aria-controls": railId, "data-testid": "context-rail-toggle", className: cn(
69
- // Pull-tab: 28w × 40h. Big enough to read the icon at a glance and to
70
- // grab confidently — matches Unifi's reference. Flat left edge
71
- // attached to the rail (no left border), rounded right. The whole tab
72
- // sticks fully out of the rail's outer edge.
73
- 'absolute top-3 -right-7 z-20 inline-flex h-10 w-7 items-center justify-center', 'before:absolute before:-inset-1 before:content-[""]', 'rounded-r-md border border-l-0 border-border bg-card text-muted-foreground', 'shadow-[2px_0_4px_rgb(0_0_0_/_0.08)]', 'hover:bg-accent hover:text-foreground', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'), children: _jsx(Icon, { size: 18, "aria-hidden": true }) }));
61
+ // Grip reveal: invisible until the rail is hovered (expanded); faint and
62
+ // reachable when collapsed so the rail can always be reopened.
63
+ const reveal = collapsed
64
+ ? 'pointer-events-auto opacity-55'
65
+ : 'pointer-events-none opacity-0 group-hover/seam:pointer-events-auto group-hover/seam:opacity-100';
66
+ // ONE element: a full-height col-resize separator straddling the boundary,
67
+ // carrying the single seam line (brand + glow on hover/focus) and the grip
68
+ // pill. Dragging the seam resizes; clicking the grip (which stops pointer
69
+ // propagation) collapses no separate resize handle, so only one line.
70
+ return (_jsxs("div", { "data-testid": "context-rail-seam", "data-collapsed": collapsed ? 'true' : 'false', onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp, className: cn('group/seam absolute top-0 right-0 bottom-0 z-20 w-3 translate-x-1/2', collapsed ? 'cursor-default' : 'cursor-col-resize'), children: [_jsx("span", { role: "separator", "aria-orientation": "vertical", "aria-label": "Resize context rail", "aria-valuenow": currentWidth, "aria-valuemin": MIN_WIDTH, "aria-valuemax": MAX_WIDTH, className: cn('pointer-events-none absolute inset-y-0 left-1/2 w-[2px] -translate-x-1/2 transition-[background-color,box-shadow] duration-150', collapsed ? 'bg-shell-border' : 'bg-transparent', 'group-hover/seam:bg-[var(--color-brand-400)] group-hover/seam:shadow-[0_0_14px_-1px_var(--color-brand-400)]', 'group-focus-visible/seam:bg-[var(--color-brand-400)] group-focus-visible/seam:shadow-[0_0_14px_-1px_var(--color-brand-400)]') }), _jsx("button", { type: "button", onPointerDown: (e) => e.stopPropagation(), onClick: () => ctx.contextRail.setCollapsed(!collapsed), "aria-label": collapsed ? 'Expand sidebar' : 'Collapse sidebar', "aria-expanded": !collapsed, "aria-controls": railId, "data-testid": "context-rail-toggle", className: cn('absolute top-1/2 left-1/2 grid h-12 w-[21px] -translate-x-1/2 -translate-y-1/2 place-items-center rounded-full border bg-card text-[var(--color-brand-400)]', 'border-[color-mix(in_oklab,var(--color-brand-400)_26%,transparent)]', 'transition-[opacity,transform,box-shadow,border-color] duration-150', collapsed ? 'scale-90' : 'scale-[.82]', reveal, 'group-hover/seam:scale-100 group-hover/seam:border-[var(--color-brand-400)] group-hover/seam:opacity-100 group-hover/seam:shadow-[0_6px_18px_-4px_color-mix(in_oklab,var(--color-brand-400)_65%,transparent)]', 'focus-visible:scale-100 focus-visible:border-[var(--color-brand-400)] focus-visible:opacity-100 focus-visible:outline-none focus-visible:shadow-[0_0_0_3px_color-mix(in_oklab,var(--color-brand-400)_35%,transparent)]'), children: _jsx(Icon, { size: 14, "aria-hidden": true }) })] }));
74
71
  }
75
72
  // ---------------------------------------------------------------------------
76
73
  // ContextRail
@@ -115,7 +112,7 @@ export function ContextRail({ appId, module, hidden = false, collapsible = true,
115
112
  // transition then so the rail snaps to the cursor instead of lagging
116
113
  // behind it.
117
114
  const isDragging = displayWidth !== null;
118
- return (_jsxs("aside", { id: railId, "data-testid": "context-rail", "aria-label": module.label, className: cn('relative h-full shrink-0 border-r border-shell-border bg-shell-context', !isDragging && 'transition-[width] duration-200 ease-in-out motion-reduce:transition-none', collapsed && 'w-0', className), style: { width: collapsed ? 0 : width }, children: [collapsible && _jsx(ContextRailToggle, { railId: railId }), _jsxs("div", { className: "flex h-full min-w-0 flex-col overflow-hidden", "aria-hidden": collapsed, inert: collapsed || undefined, children: [showHeader && (_jsxs("div", { className: "shrink-0 border-b border-shell-border px-4 py-3", children: [_jsx("h2", { className: "text-sm font-semibold text-foreground", children: module.label }), module.description && (_jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: module.description }))] })), _jsxs("div", { "data-meda-context-rail-scroll-area": "", className: cn('min-h-0 flex-1', scroll === 'auto' ? 'overflow-y-auto overflow-x-hidden' : 'overflow-hidden'), children: [items.length > 0 && (_jsx("nav", { "aria-label": `${module.label} navigation`, className: "flex flex-col gap-0.5 p-2", children: items.map((item) => {
115
+ return (_jsxs("aside", { id: railId, "data-testid": "context-rail", "aria-label": module.label, className: cn('group/rail relative h-full shrink-0 bg-shell-context', !isDragging && 'transition-[width] duration-200 ease-in-out motion-reduce:transition-none', collapsed && 'w-0', className), style: { width: collapsed ? 0 : width }, children: [collapsible && (_jsx(ContextRailSeam, { railId: railId, currentWidth: width, onResize: handleResize, onCommit: handleCommit })), _jsxs("div", { className: "flex h-full min-w-0 flex-col overflow-hidden", "aria-hidden": collapsed, inert: collapsed || undefined, children: [showHeader && (_jsxs("div", { className: "shrink-0 border-b border-shell-border px-4 py-3", children: [_jsx("h2", { className: "text-sm font-semibold text-foreground", children: module.label }), module.description && (_jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: module.description }))] })), _jsxs("div", { "data-meda-context-rail-scroll-area": "", className: cn('min-h-0 flex-1', scroll === 'auto' ? 'overflow-y-auto overflow-x-hidden' : 'overflow-hidden'), children: [items.length > 0 && (_jsx("nav", { "aria-label": `${module.label} navigation`, className: "flex flex-col gap-0.5 p-2", children: items.map((item) => {
119
116
  const isActive = item.id === activeItemId;
120
117
  const klass = cn('flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors', isActive
121
118
  ? 'bg-primary/10 text-primary'
@@ -138,5 +135,5 @@ export function ContextRail({ appId, module, hidden = false, collapsible = true,
138
135
  }) }, item.id));
139
136
  }
140
137
  return _jsx("a", { ...linkProps }, item.id);
141
- }) })), module.render?.({ workspaceId: ctx.workspace.id, appId })] })] }), !collapsed && (_jsx(ResizeHandle, { currentWidth: width, onResize: handleResize, onCommit: handleCommit }))] }));
138
+ }) })), module.render?.({ workspaceId: ctx.workspace.id, appId })] })] })] }));
142
139
  }
@@ -14,12 +14,14 @@ export interface IconRailRenderLinkArgs {
14
14
  children: ReactNode;
15
15
  linkProps: AnchorHTMLAttributes<HTMLAnchorElement>;
16
16
  }
17
+ export type IconRailLabelVisibility = 'tooltip' | 'visible';
17
18
  export interface IconRailProps {
18
19
  mainItems: IconRailItem[];
19
20
  utilityItems?: IconRailItem[];
20
21
  footer?: ReactNode;
21
22
  activeId?: string;
22
23
  renderLink?: (args: IconRailRenderLinkArgs) => ReactNode;
24
+ labelVisibility?: IconRailLabelVisibility;
23
25
  className?: string;
24
26
  }
25
27
  export interface RailDividerProps {
@@ -27,4 +29,4 @@ export interface RailDividerProps {
27
29
  onToggle: () => void;
28
30
  }
29
31
  export declare function RailDivider({ pinnedBottom, onToggle }: RailDividerProps): import("react/jsx-runtime").JSX.Element;
30
- export declare function IconRail({ mainItems, utilityItems, footer, activeId, renderLink, className, }: IconRailProps): import("react/jsx-runtime").JSX.Element | null;
32
+ export declare function IconRail({ mainItems, utilityItems, footer, activeId, renderLink, labelVisibility, className, }: IconRailProps): import("react/jsx-runtime").JSX.Element | null;
@@ -1,34 +1,48 @@
1
1
  'use client';
2
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { ChevronDown, ChevronUp } from 'lucide-react';
4
- import { useState } from 'react';
4
+ import { Fragment, useState } from 'react';
5
5
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, } from '../components/ui/tooltip.js';
6
6
  import { cn } from '../lib/utils.js';
7
7
  import { useShellViewport } from './use-shell-viewport.js';
8
8
  // ---------------------------------------------------------------------------
9
9
  // Item styling
10
10
  // ---------------------------------------------------------------------------
11
- function itemClass(isActive) {
11
+ function itemClass(isActive, labelVisibility) {
12
+ if (labelVisibility === 'visible') {
13
+ return cn('group relative flex min-h-[4rem] w-full flex-col items-center justify-start gap-1 px-1 py-1 text-center transition-colors', isActive ? 'text-foreground' : 'text-muted-foreground hover:text-foreground');
14
+ }
12
15
  return cn('group relative flex h-11 w-11 items-center justify-center rounded-xl transition-colors', isActive
13
- ? 'bg-primary/12 text-primary'
16
+ ? 'bg-primary text-primary-foreground shadow-sm ring-2 ring-primary/60'
14
17
  : 'text-muted-foreground hover:bg-accent hover:text-foreground');
15
18
  }
19
+ function iconFrameClass(isActive) {
20
+ return cn('relative inline-flex h-11 w-11 items-center justify-center rounded-xl transition-colors', isActive
21
+ ? 'bg-primary text-primary-foreground shadow-sm ring-2 ring-primary/60'
22
+ : 'group-hover:bg-accent group-hover:text-foreground');
23
+ }
24
+ function railClass(labelVisibility, className) {
25
+ return cn('flex h-full shrink-0 flex-col items-center bg-shell-rail', labelVisibility === 'visible'
26
+ ? 'w-[var(--shell-rail-label-width)] px-2 py-4'
27
+ : 'w-[var(--shell-rail-width)] py-3.5', className);
28
+ }
16
29
  export function RailDivider({ pinnedBottom, onToggle }) {
17
30
  return (_jsx("button", { type: "button", "data-testid": "rail-divider", onClick: onToggle, "aria-label": pinnedBottom ? 'Pull utility items up' : 'Push utility items down', className: cn('my-3 flex h-6 w-8 items-center justify-center rounded-md', 'text-muted-foreground hover:bg-accent hover:text-foreground transition-colors'), children: pinnedBottom ? (_jsx(ChevronUp, { size: 14, "aria-hidden": "true" })) : (_jsx(ChevronDown, { size: 14, "aria-hidden": "true" })) }));
18
31
  }
19
32
  // ---------------------------------------------------------------------------
20
33
  // IconRail
21
34
  // ---------------------------------------------------------------------------
22
- export function IconRail({ mainItems, utilityItems = [], footer, activeId, renderLink, className, }) {
35
+ export function IconRail({ mainItems, utilityItems = [], footer, activeId, renderLink, labelVisibility = 'tooltip', className, }) {
23
36
  const band = useShellViewport();
24
37
  const [pinnedBottom, setPinnedBottom] = useState(true);
25
38
  if (band === 'mobile')
26
39
  return null;
40
+ const showLabels = labelVisibility === 'visible';
27
41
  const renderItem = (item) => {
28
42
  const isActive = item.id === activeId;
29
- const klass = itemClass(isActive);
43
+ const klass = itemClass(isActive, labelVisibility);
30
44
  const IconComp = item.icon;
31
- const inner = (_jsxs(_Fragment, { children: [_jsx(IconComp, { size: 22, "aria-hidden": "true" }), item.badge ? _jsx("span", { className: "absolute right-1 top-1", children: item.badge }) : null] }));
45
+ const inner = showLabels ? (_jsxs(_Fragment, { children: [_jsxs("span", { "data-slot": "icon-rail-icon-frame", className: iconFrameClass(isActive), children: [_jsx(IconComp, { size: 26, "aria-hidden": "true" }), item.badge ? _jsx("span", { className: "absolute right-1 top-1", children: item.badge }) : null] }), _jsx("span", { "data-slot": "icon-rail-label", className: "max-w-full truncate text-[12px] leading-4", children: item.label })] })) : (_jsxs(_Fragment, { children: [_jsx(IconComp, { size: 22, "aria-hidden": "true" }), item.badge ? _jsx("span", { className: "absolute right-1 top-1", children: item.badge }) : null] }));
32
46
  const linkProps = {
33
47
  href: item.to,
34
48
  'aria-label': item.label,
@@ -39,7 +53,11 @@ export function IconRail({ mainItems, utilityItems = [], footer, activeId, rende
39
53
  const linkContent = renderLink ? (
40
54
  // renderLink consumers receive the className so they can apply it themselves
41
55
  renderLink({ item, isActive, className: klass, children: inner, linkProps })) : (_jsx("a", { ...linkProps }));
42
- return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { render: _jsx("span", { "data-testid": `icon-rail-trigger-${item.id}`, className: klass, children: linkContent }) }), _jsx(TooltipContent, { side: "right", children: item.label })] }, item.id));
56
+ const trigger = (_jsx("span", { "data-testid": `icon-rail-trigger-${item.id}`, className: klass, children: linkContent }));
57
+ if (showLabels) {
58
+ return _jsx(Fragment, { children: trigger }, item.id);
59
+ }
60
+ return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { render: trigger }), _jsx(TooltipContent, { side: "right", children: item.label })] }, item.id));
43
61
  };
44
- return (_jsx(TooltipProvider, { children: _jsxs("nav", { "data-testid": "icon-rail", "aria-label": "Primary", className: cn('flex h-full w-[var(--shell-rail-width)] shrink-0 flex-col items-center bg-shell-rail py-3.5', className), children: [_jsx("div", { className: "flex flex-col items-center gap-1", children: mainItems.map(renderItem) }), utilityItems.length > 0 && (_jsxs(_Fragment, { children: [_jsx(RailDivider, { pinnedBottom: pinnedBottom, onToggle: () => setPinnedBottom((prev) => !prev) }), _jsx("div", { "data-testid": "utility-items-wrapper", className: cn('flex flex-col items-center gap-1', pinnedBottom && 'mt-auto'), children: utilityItems.map(renderItem) })] })), footer && (_jsx("div", { className: cn('pt-3', pinnedBottom || utilityItems.length === 0 ? 'mt-auto' : ''), children: footer }))] }) }));
62
+ return (_jsx(TooltipProvider, { children: _jsxs("nav", { "data-testid": "icon-rail", "aria-label": "Primary", className: railClass(labelVisibility, className), children: [_jsx("div", { className: cn('flex flex-col items-center', showLabels ? 'w-full gap-1' : 'gap-1'), children: mainItems.map(renderItem) }), utilityItems.length > 0 && (_jsxs(_Fragment, { children: [_jsx(RailDivider, { pinnedBottom: pinnedBottom, onToggle: () => setPinnedBottom((prev) => !prev) }), _jsx("div", { "data-testid": "utility-items-wrapper", className: cn('flex flex-col items-center', showLabels ? 'w-full gap-1' : 'gap-1', pinnedBottom && 'mt-auto'), children: utilityItems.map(renderItem) })] })), footer && (_jsx("div", { className: cn('pt-3', pinnedBottom || utilityItems.length === 0 ? 'mt-auto' : ''), children: footer }))] }) }));
45
63
  }
@@ -7,7 +7,7 @@ export { ContextRail } from './context-rail.js';
7
7
  export type { DragModeBannerProps } from './drag-mode-banner.js';
8
8
  export { DragModeBanner } from './drag-mode-banner.js';
9
9
  export * as Extras from './extras/index.js';
10
- export type { IconRailItem, IconRailProps, IconRailRenderLinkArgs } from './icon-rail.js';
10
+ export type { IconRailItem, IconRailLabelVisibility, IconRailProps, IconRailRenderLinkArgs, } from './icon-rail.js';
11
11
  export { IconRail, RailDivider } from './icon-rail.js';
12
12
  export type { ShellStorageAdapter } from './layout-state.js';
13
13
  export { createLocalStorageAdapter } from './layout-state.js';
@@ -20,7 +20,7 @@ export type { RailDropZonesProps } from './rail-drop-zones.js';
20
20
  export { RailDropZones } from './rail-drop-zones.js';
21
21
  export { ResizableHandle, ResizableShell, ResizableShellPanel } from './resizable-shell.js';
22
22
  export { RightPanel } from './right-panel.js';
23
- export type { AppTabsProps } from './shell-header.js';
23
+ export type { AppTabsProps, PanelToggleProps } from './shell-header.js';
24
24
  export { AppTabs, PanelToggle, ShellHeader, WorkspaceSwitcher } from './shell-header.js';
25
25
  export { ShellMain } from './shell-main.js';
26
26
  export type { MedaShellProviderProps } from './shell-provider.js';
@@ -25,7 +25,7 @@ export function MobileBottomNav({ items, className }) {
25
25
  const resolvedItems = items ?? ctx.mobileBottomNav;
26
26
  if (resolvedItems.length === 0)
27
27
  return null;
28
- return (_jsx("nav", { "data-testid": "mobile-bottom-nav", "aria-label": "Mobile navigation", className: cn('flex h-[var(--shell-bottom-nav-height)] items-center justify-around border-t border-border bg-card', className), children: resolvedItems.map((item) => (_jsx(MobileBottomNavButton, { item: item }, item.id))) }));
28
+ return (_jsx("nav", { "data-testid": "mobile-bottom-nav", "aria-label": "Mobile navigation", className: cn('flex h-[calc(var(--shell-bottom-nav-height)+env(safe-area-inset-bottom))] items-start justify-around border-t border-border bg-card px-1 pb-[env(safe-area-inset-bottom)]', className), children: resolvedItems.map((item) => (_jsx(MobileBottomNavButton, { item: item }, item.id))) }));
29
29
  }
30
30
  /** Long-press threshold in milliseconds */
31
31
  const LONG_PRESS_DURATION = 500;
@@ -76,5 +76,5 @@ function MobileBottomNavButton({ item }) {
76
76
  ctx.mobileDrawer.setOpen(item.id);
77
77
  }
78
78
  };
79
- return (_jsxs("button", { type: "button", onPointerDown: handlePointerDown, onPointerUp: handlePointerUp, onPointerLeave: handlePointerLeave, onPointerCancel: handlePointerCancel, onClick: handleClick, "aria-label": label, className: "flex h-full flex-1 flex-col items-center justify-center gap-0.5 text-muted-foreground hover:text-foreground", children: [_jsx(Icon, { size: 20, "aria-hidden": "true" }), _jsx("span", { className: "text-[10px]", children: label })] }));
79
+ return (_jsxs("button", { type: "button", onPointerDown: handlePointerDown, onPointerUp: handlePointerUp, onPointerLeave: handlePointerLeave, onPointerCancel: handlePointerCancel, onClick: handleClick, "aria-label": label, className: "flex h-[var(--shell-bottom-nav-height)] flex-1 flex-col items-center justify-center gap-0.5 rounded-xl px-1 text-muted-foreground transition-colors hover:text-foreground active:bg-accent", children: [_jsx(Icon, { size: 20, "aria-hidden": "true" }), _jsx("span", { className: "font-medium text-[10px] leading-none", children: label })] }));
80
80
  }
@@ -1,4 +1,5 @@
1
1
  import { type ReactNode } from 'react';
2
+ import type { ContextRailProps } from '../context-rail.js';
2
3
  import type { IconRailItem, IconRailProps } from '../icon-rail.js';
3
4
  import type { ContextModule, ContextRailHeader, ContextRailScroll, PanelView, WorkspaceMenuItem } from '../types.js';
4
5
  export interface MobileDrawersProps {
@@ -16,10 +17,19 @@ export interface MobileDrawersProps {
16
17
  module?: ContextModule;
17
18
  /** App id used when rendering module custom content. */
18
19
  moduleAppId?: string;
20
+ /** Active module item id, sourced from context rail config. */
21
+ moduleActiveItemId?: string;
22
+ /** Custom module link renderer, sourced from context rail config. */
23
+ moduleRenderLink?: ContextRailProps['renderLink'];
19
24
  /** Header behavior mirrored from the desktop ContextRail. */
20
25
  moduleHeader?: ContextRailHeader;
21
26
  /** Scroll behavior mirrored from the desktop ContextRail. */
22
27
  moduleScroll?: ContextRailScroll;
28
+ /**
29
+ * Desktop header "leading" content (section tabs) surfaced under the menu
30
+ * drawer on mobile. Anchors inside auto-close the drawer on click.
31
+ */
32
+ sectionTabs?: ReactNode;
23
33
  /** Panels drawer source. */
24
34
  panelViews?: PanelView[];
25
35
  /**
@@ -37,4 +47,4 @@ export interface MobileDrawersProps {
37
47
  * any custom-content drawers. Mount once near the AppShell root; drawers
38
48
  * open/close via `ctx.mobileDrawer.open` provider state.
39
49
  */
40
- export declare function MobileDrawers({ menuItems, menuActiveId, menuRenderLink, workspaceMenuItems, workspaceMenuFooter, module, moduleAppId, moduleHeader, moduleScroll, panelViews, defaultView, customContent, }: MobileDrawersProps): import("react/jsx-runtime").JSX.Element;
50
+ export declare function MobileDrawers({ menuItems, menuActiveId, menuRenderLink, workspaceMenuItems, workspaceMenuFooter, module, moduleAppId, moduleActiveItemId, moduleRenderLink, moduleHeader, moduleScroll, sectionTabs, panelViews, defaultView, customContent, }: MobileDrawersProps): import("react/jsx-runtime").JSX.Element;
@@ -12,7 +12,7 @@ import { useTheme } from '../theme.js';
12
12
  * open/close via `ctx.mobileDrawer.open` provider state.
13
13
  */
14
14
  /* v8 ignore next — v8 phantom duplicate function record for MobileDrawers */
15
- export function MobileDrawers({ menuItems = [], menuActiveId, menuRenderLink, workspaceMenuItems, workspaceMenuFooter, module, moduleAppId, moduleHeader, moduleScroll, panelViews = [], defaultView, customContent = {}, }) {
15
+ export function MobileDrawers({ menuItems = [], menuActiveId, menuRenderLink, workspaceMenuItems, workspaceMenuFooter, module, moduleAppId, moduleActiveItemId, moduleRenderLink, moduleHeader, moduleScroll, sectionTabs, panelViews = [], defaultView, customContent = {}, }) {
16
16
  const ctx = useMedaShell();
17
17
  const open = ctx.mobileDrawer.open;
18
18
  const setOpen = ctx.mobileDrawer.setOpen;
@@ -25,16 +25,25 @@ export function MobileDrawers({ menuItems = [], menuActiveId, menuRenderLink, wo
25
25
  workspaceId: ctx.workspace.id,
26
26
  appId: moduleAppId ?? ctx.activeAppId,
27
27
  };
28
- return (_jsxs(_Fragment, { children: [_jsx(MenuDrawer, { open: open === 'menu-drawer', onClose: close, items: menuItems, activeId: menuActiveId, renderLink: menuRenderLink, workspaceItems: workspaceMenuItems, workspaceFooter: workspaceMenuFooter }), _jsx(ModuleDrawer, { open: open === 'module-drawer', onClose: close, module: module, renderCtx: moduleRenderCtx, header: moduleHeader, scroll: moduleScroll }), _jsx(PanelsDrawer, { open: open === 'panels-drawer', onClose: close, panelViews: panelViews, defaultView: defaultView, renderCtx: renderCtx }), _jsx(AiDrawer, { open: open === 'ai-drawer', onClose: close, panelViews: panelViews, renderCtx: renderCtx }), Object.entries(customContent).map(([id, renderFn]) => (_jsx(Drawer, { open: open === id, onOpenChange: (o) => !o && close(), direction: "bottom", children: _jsx(DrawerContent, { children: renderFn(close) }) }, id)))] }));
28
+ return (_jsxs(_Fragment, { children: [_jsx(MenuDrawer, { open: open === 'menu-drawer', onClose: close, items: menuItems, activeId: menuActiveId, renderLink: menuRenderLink, workspaceItems: workspaceMenuItems, workspaceFooter: workspaceMenuFooter, sectionTabs: sectionTabs }), _jsx(ModuleDrawer, { open: open === 'module-drawer', onClose: close, module: module, activeItemId: moduleActiveItemId, renderCtx: moduleRenderCtx, renderLink: moduleRenderLink, header: moduleHeader, scroll: moduleScroll }), _jsx(PanelsDrawer, { open: open === 'panels-drawer', onClose: close, panelViews: panelViews, defaultView: defaultView, renderCtx: renderCtx }), _jsx(AiDrawer, { open: open === 'ai-drawer', onClose: close, panelViews: panelViews, renderCtx: renderCtx }), Object.entries(customContent).map(([id, renderFn]) => (_jsx(Drawer, { open: open === id, onOpenChange: (o) => !o && close(), direction: "bottom", children: _jsx(DrawerContent, { children: renderFn(close) }) }, id)))] }));
29
29
  }
30
30
  // ---------------------------------------------------------------------------
31
31
  // Internal sub-drawers
32
32
  // ---------------------------------------------------------------------------
33
- function MenuDrawer({ open, onClose, items, activeId, renderLink, workspaceItems, workspaceFooter, }) {
33
+ function MenuDrawer({ open, onClose, items, activeId, renderLink, workspaceItems, workspaceFooter, sectionTabs, }) {
34
34
  const hasIconItems = items.length > 0;
35
35
  const hasWorkspaceItems = Array.isArray(workspaceItems) && workspaceItems.length > 0;
36
+ const hasSectionTabs = sectionTabs != null;
37
+ // The desktop "leading" section tabs (e.g. Home / Inbox) live under the menu
38
+ // on mobile. The tabs are router Links; clicking one navigates client-side,
39
+ // so close the drawer when any anchor inside is activated.
40
+ const handleSectionTabsClick = (event) => {
41
+ if (event.target.closest('a')) {
42
+ onClose();
43
+ }
44
+ };
36
45
  /* v8 ignore next — v8 phantom duplicate return statement for MenuDrawer */
37
- return (_jsx(Drawer, { open: open, onOpenChange: (o) => !o && onClose(), direction: "left", children: _jsxs(DrawerContent, { children: [_jsxs(DrawerHeader, { children: [_jsx(DrawerTitle, { children: "Menu" }), _jsx(DrawerDescription, { className: "sr-only", children: "Switch between primary app areas." })] }), _jsxs("div", { className: "flex flex-col gap-2 p-2", children: [hasIconItems && (_jsx("nav", { "aria-label": "Primary navigation", className: "flex flex-col gap-0.5", children: items.map((item) => (_jsx(MenuDrawerItem, { item: item, isActive: item.id === activeId, onClose: onClose, renderLink: renderLink }, item.id))) })), (hasIconItems || hasWorkspaceItems) && _jsx("div", { className: "h-px bg-border" }), _jsxs("nav", { "aria-label": "Workspace menu", className: "flex flex-col gap-0.5", children: [workspaceItems?.map((item) => (_jsxs(Fragment, { children: [_jsx(WorkspaceMenuDrawerItem, { item: item, onClose: onClose }), item.separatorAfter && _jsx("div", { className: "my-1 h-px bg-border" })] }, item.id))), _jsx(MobileThemeMenuItem, { onClose: onClose })] }), workspaceFooter] })] }) }));
46
+ return (_jsx(Drawer, { open: open, onOpenChange: (o) => !o && onClose(), direction: "left", children: _jsxs(DrawerContent, { children: [_jsxs(DrawerHeader, { children: [_jsx(DrawerTitle, { children: "Menu" }), _jsx(DrawerDescription, { className: "sr-only", children: "Switch between primary app areas." })] }), _jsxs("div", { className: "flex flex-col gap-2 p-2", children: [hasSectionTabs && (_jsxs(_Fragment, { children: [_jsx("div", { "data-meda-mobile-section-tabs": "", className: "flex h-9 items-stretch overflow-x-auto px-1", onClick: handleSectionTabsClick, children: sectionTabs }), _jsx("div", { className: "h-px bg-border" })] })), hasIconItems && (_jsx("nav", { "aria-label": "Primary navigation", className: "flex flex-col gap-0.5", children: items.map((item) => (_jsx(MenuDrawerItem, { item: item, isActive: item.id === activeId, onClose: onClose, renderLink: renderLink }, item.id))) })), (hasIconItems || hasWorkspaceItems) && _jsx("div", { className: "h-px bg-border" }), _jsxs("nav", { "aria-label": "Workspace menu", className: "flex flex-col gap-0.5", children: [workspaceItems?.map((item) => (_jsxs(Fragment, { children: [_jsx(WorkspaceMenuDrawerItem, { item: item, onClose: onClose }), item.separatorAfter && _jsx("div", { className: "my-1 h-px bg-border" })] }, item.id))), _jsx(MobileThemeMenuItem, { onClose: onClose })] }), workspaceFooter] })] }) }));
38
47
  }
39
48
  const menuItemClassName = 'flex items-center gap-2 rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground';
40
49
  function MenuDrawerItem({ item, isActive, onClose, renderLink, }) {
@@ -112,7 +121,7 @@ function MobileThemeMenuItem({ onClose }) {
112
121
  onClose();
113
122
  }, children: [_jsx(Icon, { size: 18, "aria-hidden": "true" }), _jsx("span", { children: THEME_LABEL[theme] })] }));
114
123
  }
115
- function ModuleDrawer({ open, onClose, module, renderCtx, header = 'auto', scroll = 'auto', }) {
124
+ function ModuleDrawer({ open, onClose, module, activeItemId, renderCtx, renderLink, header = 'auto', scroll = 'auto', }) {
116
125
  const items = module?.items ?? [];
117
126
  if (!module || (items.length === 0 && !module.render))
118
127
  return null;
@@ -122,7 +131,19 @@ function ModuleDrawer({ open, onClose, module, renderCtx, header = 'auto', scrol
122
131
  /* v8 ignore next — v8 phantom duplicate return statement for ModuleDrawer */
123
132
  return (_jsx(Drawer, { open: open, onOpenChange: (o) => !o && onClose(), direction: "left", children: _jsxs(DrawerContent, { children: [showHeader ? _jsx(DrawerHeader, { children: title }) : title, _jsxs("div", { "data-meda-context-rail-scroll-area": "", className: cn('min-h-0 flex-1', scroll === 'auto' ? 'overflow-y-auto overflow-x-hidden' : 'overflow-hidden'), children: [items.length > 0 && (_jsx("nav", { className: "flex flex-col gap-0.5 p-2", children: items.map((item) => {
124
133
  const Icon = item.icon;
125
- return (_jsxs("a", { href: item.to, onClick: onClose, className: "flex items-center gap-2 rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground", children: [_jsx(Icon, { size: 16, "aria-hidden": "true" }), _jsx("span", { children: item.label })] }, item.id));
134
+ const isActive = item.id === activeItemId;
135
+ const className = cn('flex items-center gap-2 rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground', isActive && 'bg-accent text-foreground');
136
+ const children = (_jsxs(_Fragment, { children: [_jsx(Icon, { size: 16, "aria-hidden": "true" }), _jsx("span", { children: item.label })] }));
137
+ const linkProps = {
138
+ href: item.to,
139
+ 'aria-current': isActive ? 'page' : undefined,
140
+ className,
141
+ children,
142
+ };
143
+ if (renderLink) {
144
+ return (_jsx(Fragment, { children: closeAfterLinkClick(renderLink({ item, isActive, className, children, linkProps }), onClose) }, item.id));
145
+ }
146
+ return (_jsx(Fragment, { children: closeAfterLinkClick(_jsx("a", { ...linkProps }), onClose) }, item.id));
126
147
  }) })), module.render?.(renderCtx)] })] }) }));
127
148
  }
128
149
  function PanelsDrawer({ open, onClose, panelViews, defaultView, renderCtx, }) {
@@ -10,6 +10,8 @@ export interface MobileHeaderProps {
10
10
  onBack?: () => void;
11
11
  /** Root mode only — renders to the right of workspace name. */
12
12
  globalActions?: ReactNode;
13
+ /** Root mode only — rendered centered in the header (e.g. clock). */
14
+ headerCenter?: ReactNode;
13
15
  className?: string;
14
16
  }
15
17
  /**
@@ -20,4 +22,4 @@ export interface MobileHeaderProps {
20
22
  *
21
23
  * Renders nothing on non-mobile viewports.
22
24
  */
23
- export declare function MobileHeader({ parentLabel, parentTo: _parentTo, title, onBack, globalActions, className, }: MobileHeaderProps): import("react/jsx-runtime").JSX.Element | null;
25
+ export declare function MobileHeader({ parentLabel, parentTo: _parentTo, title, onBack, globalActions, headerCenter, className, }: MobileHeaderProps): import("react/jsx-runtime").JSX.Element | null;
@@ -12,11 +12,11 @@ import { useShellViewport } from '../use-shell-viewport.js';
12
12
  *
13
13
  * Renders nothing on non-mobile viewports.
14
14
  */
15
- export function MobileHeader({ parentLabel, parentTo: _parentTo, title, onBack, globalActions, className, }) {
15
+ export function MobileHeader({ parentLabel, parentTo: _parentTo, title, onBack, globalActions, headerCenter, className, }) {
16
16
  const ctx = useMedaShell();
17
17
  const band = useShellViewport();
18
18
  const isNested = !!parentLabel;
19
19
  if (band !== 'mobile')
20
20
  return null;
21
- return (_jsx("header", { "data-testid": "mobile-header", "data-meda-mobile-header": isNested ? 'nested' : 'root', className: cn('flex h-[var(--shell-header-height)] items-center justify-between border-b border-border bg-card px-3', className), children: isNested ? (_jsxs("button", { type: "button", onClick: onBack, "aria-label": "Go back", className: "flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm hover:bg-accent", children: [_jsx(ChevronLeft, { size: 16, "aria-hidden": "true" }), _jsx("span", { className: "text-muted-foreground", children: parentLabel }), title && (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-muted-foreground/40", "aria-hidden": "true", children: "\u00B7" }), _jsx("span", { className: "text-foreground", children: title })] }))] })) : (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center gap-2 text-sm font-medium text-foreground", children: [_jsx("span", { "aria-hidden": "true", children: ctx.workspace.icon }), _jsx("span", { children: ctx.workspace.name })] }), globalActions && _jsx("div", { className: "flex items-center gap-1", children: globalActions })] })) }));
21
+ return (_jsx("header", { "data-testid": "mobile-header", "data-meda-mobile-header": isNested ? 'nested' : 'root', className: cn('relative flex h-[var(--shell-mobile-header-height)] items-center justify-between bg-background px-3', className), children: isNested ? (_jsxs("button", { type: "button", onClick: onBack, "aria-label": "Go back", className: "flex min-h-9 max-w-full items-center gap-1.5 rounded-lg px-2 text-sm font-medium hover:bg-accent", children: [_jsx(ChevronLeft, { size: 18, "aria-hidden": "true" }), _jsx("span", { className: "truncate text-muted-foreground", children: parentLabel }), title && (_jsxs(_Fragment, { children: [_jsx("span", { className: "text-muted-foreground/40", "aria-hidden": "true", children: "\u00B7" }), _jsx("span", { className: "truncate text-foreground", children: title })] }))] })) : (_jsxs(_Fragment, { children: [_jsxs("button", { type: "button", onClick: () => ctx.mobileDrawer.setOpen('menu-drawer'), "aria-label": "Open workspace menu", "aria-haspopup": "menu", className: "-mx-1 flex min-w-0 items-center gap-2.5 rounded-lg px-1 py-1 text-sm font-semibold text-foreground hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring", children: [_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: ctx.workspace.icon }), _jsx("span", { className: "truncate text-[15px] leading-5", children: ctx.workspace.name })] }), headerCenter && (_jsx("div", { className: "-translate-x-1/2 pointer-events-none absolute left-1/2 flex max-w-[55%] items-center justify-center", children: _jsx("div", { className: "pointer-events-auto flex min-w-0 items-center", children: headerCenter }) })), globalActions && (_jsx("div", { className: "flex shrink-0 items-center gap-1.5", children: globalActions }))] })) }));
22
22
  }
@@ -1,5 +1,5 @@
1
1
  import { type ReactNode } from 'react';
2
- import type { AppShellAppTabsConfig, WorkspaceMenuItem } from './types.js';
2
+ import type { AppShellAppTabsConfig, PanelView, WorkspaceMenuItem } from './types.js';
3
3
  export interface WorkspaceSwitcherProps {
4
4
  /**
5
5
  * Configurable dropdown items. When provided, REPLACES the default
@@ -22,7 +22,11 @@ export declare function WorkspaceSwitcher({ menuItems, menuFooter, workspaceMenu
22
22
  export interface AppTabsProps extends AppShellAppTabsConfig {
23
23
  }
24
24
  export declare function AppTabs({ renderLink }?: AppTabsProps): import("react/jsx-runtime").JSX.Element;
25
- export declare function PanelToggle(): import("react/jsx-runtime").JSX.Element;
25
+ export interface PanelToggleProps {
26
+ /** Panel views surfaced in the chevron dropdown. Empty → plain toggle. */
27
+ panelViews?: PanelView[];
28
+ }
29
+ export declare function PanelToggle({ panelViews }?: PanelToggleProps): import("react/jsx-runtime").JSX.Element;
26
30
  export interface ShellHeaderProps {
27
31
  globalActions?: ReactNode;
28
32
  /**
@@ -30,6 +34,11 @@ export interface ShellHeaderProps {
30
34
  * provided.
31
35
  */
32
36
  headerCenter?: ReactNode;
37
+ /**
38
+ * Optional leading content rendered in the LEFT header region immediately
39
+ * after the workspace switcher (separated by whitespace only, no divider).
40
+ */
41
+ headerLeading?: ReactNode;
33
42
  appTabsRenderLink?: AppShellAppTabsConfig['renderLink'];
34
43
  className?: string;
35
44
  /**
@@ -38,5 +47,8 @@ export interface ShellHeaderProps {
38
47
  */
39
48
  workspaceMenuItems?: WorkspaceMenuItem[];
40
49
  workspaceMenuFooter?: ReactNode;
50
+ showPanelToggle?: boolean;
51
+ /** Panel views forwarded to the header `<PanelToggle>` dropdown. */
52
+ panelViews?: PanelView[];
41
53
  }
42
- export declare function ShellHeader({ globalActions, headerCenter, appTabsRenderLink, className, workspaceMenuItems, workspaceMenuFooter, }?: ShellHeaderProps): import("react/jsx-runtime").JSX.Element | null;
54
+ export declare function ShellHeader({ globalActions, headerCenter, headerLeading, appTabsRenderLink, className, workspaceMenuItems, workspaceMenuFooter, showPanelToggle, panelViews, }?: ShellHeaderProps): import("react/jsx-runtime").JSX.Element | null;