@medalsocial/meda 1.2.0 → 1.4.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.
Files changed (45) hide show
  1. package/README.md +63 -17
  2. package/dist/auth/auth-message.d.ts +10 -0
  3. package/dist/auth/auth-message.js +18 -0
  4. package/dist/auth/auth-provider-button.d.ts +14 -0
  5. package/dist/auth/auth-provider-button.js +26 -0
  6. package/dist/auth/auth-provider-list.d.ts +6 -0
  7. package/dist/auth/auth-provider-list.js +7 -0
  8. package/dist/auth/better-auth.d.ts +40 -0
  9. package/dist/auth/better-auth.js +92 -0
  10. package/dist/auth/index.d.ts +6 -0
  11. package/dist/auth/index.js +3 -0
  12. package/dist/auth/public.d.ts +6 -0
  13. package/dist/auth/public.js +3 -0
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.js +2 -0
  16. package/dist/lib/render-element.d.ts +9 -0
  17. package/dist/lib/render-element.js +27 -0
  18. package/dist/recipes/next.d.ts +49 -0
  19. package/dist/recipes/next.js +128 -0
  20. package/dist/shell/app-shell-workspace.js +11 -6
  21. package/dist/shell/app-shell.d.ts +5 -2
  22. package/dist/shell/app-shell.js +13 -1
  23. package/dist/shell/command-palette.js +2 -1
  24. package/dist/shell/context-rail.d.ts +1 -1
  25. package/dist/shell/context-rail.js +14 -7
  26. package/dist/shell/icon-rail.d.ts +2 -1
  27. package/dist/shell/icon-rail.js +8 -1
  28. package/dist/shell/index.d.ts +6 -1
  29. package/dist/shell/index.js +2 -1
  30. package/dist/shell/internal/mobile-drawers.d.ts +8 -2
  31. package/dist/shell/internal/mobile-drawers.js +47 -12
  32. package/dist/shell/panel-views-provider.d.ts +15 -0
  33. package/dist/shell/panel-views-provider.js +56 -0
  34. package/dist/shell/primitives.d.ts +9 -0
  35. package/dist/shell/primitives.js +7 -0
  36. package/dist/shell/right-panel.d.ts +12 -1
  37. package/dist/shell/right-panel.js +37 -12
  38. package/dist/shell/shell-provider.d.ts +14 -1
  39. package/dist/shell/shell-provider.js +64 -2
  40. package/dist/shell/types.d.ts +13 -2
  41. package/dist/theme/index.d.ts +1 -0
  42. package/dist/theme/index.js +1 -0
  43. package/dist/theme/theme-bridge.d.ts +21 -0
  44. package/dist/theme/theme-bridge.js +52 -0
  45. package/package.json +21 -1
@@ -6,18 +6,22 @@ import { IconRail } from './icon-rail.js';
6
6
  import { MobileBottomNav } from './internal/mobile-bottom-nav.js';
7
7
  import { MobileDrawers } from './internal/mobile-drawers.js';
8
8
  import { MobileHeader } from './internal/mobile-header.js';
9
+ import { useResolvedPanelViews } from './panel-views-provider.js';
9
10
  import { RightPanel } from './right-panel.js';
10
11
  import { ShellHeader } from './shell-header.js';
11
12
  import { ShellMain } from './shell-main.js';
12
13
  import { useShellViewport } from './use-shell-viewport.js';
14
+ const EMPTY_PANEL_VIEWS = [];
13
15
  export function AppShellWorkspace({ iconRail, contextRail, rightPanel, globalActions, children, }) {
14
16
  const viewport = useShellViewport();
15
17
  const isMobile = viewport === 'mobile';
18
+ const staticPanelViews = rightPanel?.panelViews ?? EMPTY_PANEL_VIEWS;
19
+ const resolvedRightPanel = useResolvedPanelViews(staticPanelViews, rightPanel?.defaultView);
16
20
  // Derive the bottom-nav items from the variant config so each button maps
17
21
  // to a drawer that actually has content. Without this filter, partial
18
22
  // configs (e.g. iconRail only) would show Module/Panels/AI buttons that
19
23
  // dispatch into the void.
20
- const navItems = buildMobileNavItems(iconRail, contextRail, rightPanel);
24
+ const navItems = buildMobileNavItems(iconRail, contextRail, resolvedRightPanel.panelViews);
21
25
  const hasDrawerContent = navItems.length > 0;
22
26
  // Mobile menu drawer needs both main and utility items — desktop IconRail
23
27
  // shows both, so dropping utilityItems here would orphan items like Help/
@@ -33,21 +37,22 @@ export function AppShellWorkspace({ iconRail, contextRail, rightPanel, globalAct
33
37
  // rendered without an explicit-height ancestor (tests, direct imports). The
34
38
  // <AppShell> wrapper already enforces h-screen for the workspace variant, so
35
39
  // nested viewport-height divs collapse cleanly — no double-scroll.
36
- return (_jsxs("div", { className: "flex h-screen flex-col", children: [isMobile ? (_jsx(MobileHeader, { globalActions: globalActions })) : (_jsx(ShellHeader, { globalActions: globalActions })), _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 })), !isMobile && contextRail && (_jsx(ContextRail, { appId: contextRail.appId, module: contextRail.module, activeItemId: contextRail.activeItemId })), _jsx(ShellMain, { layout: "workspace", children: children }), !isMobile && rightPanel && (_jsx(RightPanel, { panelViews: rightPanel.panelViews, defaultView: rightPanel.defaultView }))] }), isMobile && hasDrawerContent && _jsx(MobileBottomNav, { items: navItems }), isMobile && hasDrawerContent && (_jsx(MobileDrawers, { menuItems: mobileMenuItems, module: contextRail?.module, panelViews: rightPanel?.panelViews ?? [], defaultView: rightPanel?.defaultView }))] }));
40
+ return (_jsxs("div", { className: "flex h-screen flex-col", children: [isMobile ? (_jsx(MobileHeader, { globalActions: globalActions })) : (_jsx(ShellHeader, { globalActions: globalActions })), _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 })), _jsx(ShellMain, { layout: "workspace", 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, module: contextRail?.module, moduleAppId: contextRail?.appId, panelViews: resolvedRightPanel.panelViews, defaultView: resolvedRightPanel.defaultView }))] }));
37
41
  }
38
- function buildMobileNavItems(iconRail, contextRail, rightPanel) {
42
+ function buildMobileNavItems(iconRail, contextRail, panelViews) {
39
43
  const items = [];
40
44
  if (iconRail) {
41
45
  items.push({ id: 'menu', label: 'Menu', icon: Menu, opens: 'menu-drawer' });
42
46
  }
43
- if (contextRail?.module) {
47
+ if (contextRail?.module &&
48
+ ((contextRail.module.items ?? []).length > 0 || Boolean(contextRail.module.render))) {
44
49
  items.push({ id: 'module', label: 'Module', icon: LayoutGrid, opens: 'module-drawer' });
45
50
  }
46
51
  // Panels button only when there's an actual view to render — empty
47
52
  // panelViews would open an empty drawer (dead-end tap).
48
- if (rightPanel && rightPanel.panelViews.length > 0) {
53
+ if (panelViews.length > 0) {
49
54
  items.push({ id: 'panels', label: 'Panels', icon: PanelTop, opens: 'panels-drawer' });
50
- if (rightPanel.panelViews.some((v) => v.id === 'ai')) {
55
+ if (panelViews.some((v) => v.id === 'ai')) {
51
56
  items.push({ id: 'ai', label: 'AI', icon: Sparkles, opens: 'ai-drawer' });
52
57
  }
53
58
  }
@@ -1,12 +1,15 @@
1
1
  import type { ReactNode } from 'react';
2
- import type { AppShellAuthConfig, AppShellContextRailConfig, AppShellIconRailConfig, AppShellRightPanelConfig } from './types.js';
2
+ import type { AppShellAuthBranding, AppShellAuthConfig, AppShellContextRailConfig, AppShellIconRailConfig, AppShellRightPanelConfig } from './types.js';
3
3
  interface AppShellBaseProps {
4
4
  children: ReactNode;
5
5
  className?: string;
6
6
  }
7
7
  export type AppShellProps = AppShellBaseProps & ({
8
8
  variant: 'auth';
9
- auth: AppShellAuthConfig;
9
+ auth?: AppShellAuthConfig;
10
+ branding?: AppShellAuthBranding;
11
+ preview?: ReactNode;
12
+ actions?: ReactNode;
10
13
  } | {
11
14
  variant: 'workspace';
12
15
  iconRail?: AppShellIconRailConfig;
@@ -14,13 +14,25 @@ export function AppShell(props) {
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
- return wrapper(_jsx(AppShellAuth, { ...props.auth, children: props.children }));
17
+ return wrapper(_jsx(AppShellAuth, { ...resolveAuthConfig(props), children: props.children }));
18
18
  case 'workspace':
19
19
  return wrapper(_jsx(AppShellWorkspace, { iconRail: props.iconRail, contextRail: props.contextRail, rightPanel: props.rightPanel, globalActions: props.globalActions, children: props.children }));
20
20
  case 'chat':
21
21
  return wrapper(_jsx(AppShellChat, { globalActions: props.globalActions, children: props.children }));
22
22
  }
23
23
  }
24
+ function resolveAuthConfig(props) {
25
+ const { auth, branding } = props;
26
+ return {
27
+ title: auth?.title ?? branding?.appName ?? branding?.brandName ?? 'Sign in',
28
+ description: auth?.description ?? branding?.tagline,
29
+ brandName: auth?.brandName ?? branding?.brandName,
30
+ brandMark: auth?.brandMark ?? branding?.brandMark,
31
+ eyebrow: auth?.eyebrow,
32
+ preview: auth?.preview ?? props.preview,
33
+ actions: auth?.actions ?? props.actions,
34
+ };
35
+ }
24
36
  export function AppShellBody({ children, className }) {
25
37
  return (_jsx("div", { className: cn('relative flex h-[calc(100vh-var(--shell-header-height))] overflow-hidden', className), children: children }));
26
38
  }
@@ -109,7 +109,8 @@ export function CommandPalette({ children }) {
109
109
  // -------------------------------------------------------------------------
110
110
  return (_jsxs(CommandRegistryContext.Provider, { value: registry, children: [children, _jsx(CommandDialog, { open: open, onOpenChange: setOpen, children: _jsxs(Command, { children: [_jsx(CommandInput, { placeholder: "Search commands\u2026" }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: "No results." }), grouped.map((group) => (_jsx(CommandGroup, { heading: group.label, children: group.commands.map((cmd) => {
111
111
  const Icon = cmd.icon;
112
- return (_jsxs(CommandItem, { value: `${group.label} ${cmd.label}`, onSelect: () => handleSelect(cmd), children: [Icon && _jsx(Icon, { size: 16, "aria-hidden": "true" }), _jsx("span", { children: cmd.label }), cmd.shortcut && _jsx(CommandShortcut, { children: cmd.shortcut })] }, cmd.id));
112
+ const shortcut = cmd.shortcut ?? cmd.hotkey;
113
+ return (_jsxs(CommandItem, { value: `${group.label} ${cmd.label}`, onSelect: () => handleSelect(cmd), children: [Icon && _jsx(Icon, { size: 16, "aria-hidden": "true" }), _jsx("span", { children: cmd.label }), shortcut && _jsx(CommandShortcut, { children: shortcut })] }, cmd.id));
113
114
  }) }, group.id)))] })] }) })] }));
114
115
  }
115
116
  // ---------------------------------------------------------------------------
@@ -14,4 +14,4 @@ export interface ContextRailProps {
14
14
  renderLink?: (args: ShellLinkRenderArgs) => ReactNode;
15
15
  className?: string;
16
16
  }
17
- export declare function ContextRail({ appId: _appId, module, hidden, collapsible, activeItemId, renderLink, className, }: ContextRailProps): import("react/jsx-runtime").JSX.Element | null;
17
+ export declare function ContextRail({ appId, module, hidden, collapsible, activeItemId, renderLink, className, }: ContextRailProps): import("react/jsx-runtime").JSX.Element | null;
@@ -15,7 +15,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
15
15
  * once <AppShellBody> ships as a ResizableShell Group.
16
16
  */
17
17
  import { PanelLeftClose, PanelLeftOpen } from 'lucide-react';
18
- import { useId, useRef, useState } from 'react';
18
+ import { Fragment, useId, useRef, useState } from 'react';
19
19
  import { cn } from '../lib/utils.js';
20
20
  import { useMedaShell } from './shell-provider.js';
21
21
  import { useShellViewport } from './use-shell-viewport.js';
@@ -75,7 +75,7 @@ function ContextRailToggle({ railId }) {
75
75
  // ---------------------------------------------------------------------------
76
76
  // ContextRail
77
77
  // ---------------------------------------------------------------------------
78
- export function ContextRail({ appId: _appId, module, hidden = false, collapsible = true, activeItemId, renderLink, className, }) {
78
+ export function ContextRail({ appId, module, hidden = false, collapsible = true, activeItemId, renderLink, className, }) {
79
79
  const band = useShellViewport();
80
80
  const ctx = useMedaShell();
81
81
  // collapsible={false} means the rail must always render expanded — even if
@@ -91,12 +91,13 @@ export function ContextRail({ appId: _appId, module, hidden = false, collapsible
91
91
  // is called on pointerUp to persist via useShellLayoutState.
92
92
  const [displayWidth, setDisplayWidth] = useState(null);
93
93
  const width = displayWidth ?? ctx.contextRail.width;
94
+ const items = module?.items ?? [];
94
95
  if (band === 'mobile')
95
96
  return null;
96
97
  if (hidden) {
97
98
  return _jsx("div", { "aria-hidden": "true", className: "hidden", "data-testid": "context-rail-hidden" });
98
99
  }
99
- if (!module || module.items.length === 0) {
100
+ if (!module || (items.length === 0 && !module.render)) {
100
101
  return _jsx("div", { "aria-hidden": "true", className: "hidden", "data-testid": "context-rail-empty" });
101
102
  }
102
103
  const handleResize = (w) => {
@@ -112,16 +113,22 @@ export function ContextRail({ appId: _appId, module, hidden = false, collapsible
112
113
  // transition then so the rail snaps to the cursor instead of lagging
113
114
  // behind it.
114
115
  const isDragging = displayWidth !== null;
115
- 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: "h-full overflow-hidden", "aria-hidden": collapsed, inert: collapsed || undefined, children: [_jsxs("div", { className: "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 }))] }), _jsx("nav", { "aria-label": `${module.label} navigation`, className: "flex flex-col gap-0.5 p-2", children: module.items.map((item) => {
116
+ 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: "h-full overflow-hidden", "aria-hidden": collapsed, inert: collapsed || undefined, children: [_jsxs("div", { className: "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 }))] }), items.length > 0 && (_jsx("nav", { "aria-label": `${module.label} navigation`, className: "flex flex-col gap-0.5 p-2", children: items.map((item) => {
116
117
  const isActive = item.id === activeItemId;
117
118
  const klass = cn('flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors', isActive
118
119
  ? 'bg-primary/10 text-primary'
119
120
  : 'text-muted-foreground hover:bg-accent hover:text-foreground');
120
121
  const IconComp = item.icon;
121
122
  const inner = (_jsxs(_Fragment, { children: [_jsx(IconComp, { size: 16, "aria-hidden": "true", className: "shrink-0" }), _jsx("span", { className: "truncate", children: item.label }), item.shortcut && (_jsx("kbd", { className: "ml-auto font-mono text-[10px] text-muted-foreground", children: item.shortcut }))] }));
123
+ const linkProps = {
124
+ href: item.to,
125
+ 'aria-current': isActive ? 'page' : undefined,
126
+ className: klass,
127
+ children: inner,
128
+ };
122
129
  if (renderLink) {
123
- return renderLink({ item, isActive, className: klass, children: inner });
130
+ return (_jsx(Fragment, { children: renderLink({ item, isActive, className: klass, children: inner, linkProps }) }, item.id));
124
131
  }
125
- return (_jsx("a", { href: item.to, "aria-current": isActive ? 'page' : undefined, className: klass, children: inner }, item.id));
126
- }) })] }), !collapsed && (_jsx(ResizeHandle, { currentWidth: width, onResize: handleResize, onCommit: handleCommit }))] }));
132
+ return _jsx("a", { ...linkProps }, item.id);
133
+ }) })), module.render?.({ workspaceId: ctx.workspace.id, appId })] }), !collapsed && (_jsx(ResizeHandle, { currentWidth: width, onResize: handleResize, onCommit: handleCommit }))] }));
127
134
  }
@@ -1,5 +1,5 @@
1
1
  import type { LucideIcon } from 'lucide-react';
2
- import type { ReactNode } from 'react';
2
+ import type { AnchorHTMLAttributes, ReactNode } from 'react';
3
3
  export interface IconRailItem {
4
4
  id: string;
5
5
  label: string;
@@ -12,6 +12,7 @@ export interface IconRailRenderLinkArgs {
12
12
  isActive: boolean;
13
13
  className: string;
14
14
  children: ReactNode;
15
+ linkProps: AnchorHTMLAttributes<HTMLAnchorElement>;
15
16
  }
16
17
  export interface IconRailProps {
17
18
  mainItems: IconRailItem[];
@@ -29,9 +29,16 @@ export function IconRail({ mainItems, utilityItems = [], footer, activeId, rende
29
29
  const klass = itemClass(isActive);
30
30
  const IconComp = item.icon;
31
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] }));
32
+ const linkProps = {
33
+ href: item.to,
34
+ 'aria-label': item.label,
35
+ 'aria-current': isActive ? 'page' : undefined,
36
+ className: 'contents',
37
+ children: inner,
38
+ };
32
39
  const linkContent = renderLink ? (
33
40
  // renderLink consumers receive the className so they can apply it themselves
34
- renderLink({ item, isActive, className: klass, children: inner })) : (_jsx("a", { href: item.to, "aria-label": item.label, "aria-current": isActive ? 'page' : undefined, className: "contents", children: inner }));
41
+ renderLink({ item, isActive, className: klass, children: inner, linkProps })) : (_jsx("a", { ...linkProps }));
35
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));
36
43
  };
37
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 }))] }) }));
@@ -1,3 +1,5 @@
1
+ export type { AuthMessageProps, AuthOneTapSlotProps, AuthProvider, AuthProviderButtonProps, AuthProviderListProps, } from '../auth/index.js';
2
+ export { AuthError, AuthNotice, AuthOneTapSlot, AuthProviderButton, AuthProviderList, } from '../auth/index.js';
1
3
  export { AppShell, AppShellBody } from './app-shell.js';
2
4
  export type { CommandGroupDefinition } from './command-palette.js';
3
5
  export { CommandPalette, useCommandGroup, useCommands } from './command-palette.js';
@@ -5,10 +7,13 @@ export { ContextRail } from './context-rail.js';
5
7
  export type { DragModeBannerProps } from './drag-mode-banner.js';
6
8
  export { DragModeBanner } from './drag-mode-banner.js';
7
9
  export * as Extras from './extras/index.js';
10
+ export type { IconRailItem, IconRailProps, IconRailRenderLinkArgs } from './icon-rail.js';
8
11
  export { IconRail, RailDivider } from './icon-rail.js';
9
12
  export type { ShellStorageAdapter } from './layout-state.js';
10
13
  export { createLocalStorageAdapter } from './layout-state.js';
11
14
  export { motion } from './motion.js';
15
+ export type { PanelViewsProviderProps } from './panel-views-provider.js';
16
+ export { PanelViewsProvider } from './panel-views-provider.js';
12
17
  export type { RailDropSlotProps, RailDropSlotState } from './rail-drop-slot.js';
13
18
  export { RailDropSlot } from './rail-drop-slot.js';
14
19
  export type { RailDropZonesProps } from './rail-drop-zones.js';
@@ -21,5 +26,5 @@ export type { MedaShellProviderProps } from './shell-provider.js';
21
26
  export { MedaShellProvider, useMedaShell, useShellSelection } from './shell-provider.js';
22
27
  export { DefaultThemeProvider, ThemeToggle, useTheme } from './theme.js';
23
28
  export { NextThemesAdapter } from './theme-next-themes.js';
24
- export type { AppDefinition, AppShellAuthConfig, AppShellContextRailConfig, AppShellIconRailConfig, AppShellRightPanelConfig, AppShellVariant, CommandDefinition, ContextItem, ContextModule, MobileBottomNavItem, PanelMode, PanelView, ShellLinkRenderArgs, ShellMainLayout, ShellRenderContext, ShellViewport, ThemeAdapter, WorkspaceDefinition, } from './types.js';
29
+ export type { AppDefinition, AppShellAuthBranding, AppShellAuthConfig, AppShellContextRailConfig, AppShellIconRailConfig, AppShellRightPanelConfig, AppShellVariant, CommandDefinition, ContextItem, ContextModule, MobileBottomNavItem, PanelMode, PanelView, ShellLinkRenderArgs, ShellMainLayout, ShellRenderContext, ShellViewport, ThemeAdapter, WorkspaceDefinition, } from './types.js';
25
30
  export { useShellViewport } from './use-shell-viewport.js';
@@ -3,6 +3,7 @@
3
3
  // the directive (see test/nextjs-consumer.test.ts). Each underlying component
4
4
  // file carries its own 'use client' — Next traces through the barrel and
5
5
  // applies them per-component. The barrel itself is just a re-exporter.
6
+ export { AuthError, AuthNotice, AuthOneTapSlot, AuthProviderButton, AuthProviderList, } from '../auth/index.js';
6
7
  // Layout
7
8
  export { AppShell, AppShellBody } from './app-shell.js';
8
9
  // Command palette
@@ -12,12 +13,12 @@ export { ContextRail } from './context-rail.js';
12
13
  export { DragModeBanner } from './drag-mode-banner.js';
13
14
  // Extras (legacy components ported during Phase 15 — opt-in for apps that need them)
14
15
  export * as Extras from './extras/index.js';
15
- // Rails + main + panel
16
16
  export { IconRail, RailDivider } from './icon-rail.js';
17
17
  // Storage adapter (consumers may want to provide their own)
18
18
  export { createLocalStorageAdapter } from './layout-state.js';
19
19
  // Hooks + tokens
20
20
  export { motion } from './motion.js';
21
+ export { PanelViewsProvider } from './panel-views-provider.js';
21
22
  // Resize primitives
22
23
  export { RailDropSlot } from './rail-drop-slot.js';
23
24
  export { RailDropZones } from './rail-drop-zones.js';
@@ -1,11 +1,17 @@
1
1
  import { type ReactNode } from 'react';
2
- import type { IconRailItem } from '../icon-rail.js';
2
+ import type { IconRailItem, IconRailProps } from '../icon-rail.js';
3
3
  import type { ContextModule, PanelView } from '../types.js';
4
4
  export interface MobileDrawersProps {
5
5
  /** Menu drawer source (icon-rail items). */
6
6
  menuItems?: IconRailItem[];
7
+ /** Active menu item id, sourced from icon rail config. */
8
+ menuActiveId?: string;
9
+ /** Custom menu link renderer, sourced from icon rail config. */
10
+ menuRenderLink?: IconRailProps['renderLink'];
7
11
  /** Module drawer source (current app's context-rail module). */
8
12
  module?: ContextModule;
13
+ /** App id used when rendering module custom content. */
14
+ moduleAppId?: string;
9
15
  /** Panels drawer source. */
10
16
  panelViews?: PanelView[];
11
17
  /**
@@ -23,4 +29,4 @@ export interface MobileDrawersProps {
23
29
  * any custom-content drawers. Mount once near the AppShell root; drawers
24
30
  * open/close via `ctx.mobileDrawer.open` provider state.
25
31
  */
26
- export declare function MobileDrawers({ menuItems, module, panelViews, defaultView, customContent, }: MobileDrawersProps): import("react/jsx-runtime").JSX.Element;
32
+ export declare function MobileDrawers({ menuItems, menuActiveId, menuRenderLink, module, moduleAppId, panelViews, defaultView, customContent, }: MobileDrawersProps): import("react/jsx-runtime").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useEffect } from 'react';
3
+ import { cloneElement, isValidElement, useEffect } from 'react';
4
4
  import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle, } from '../../components/ui/drawer.js';
5
5
  import { cn } from '../../lib/utils.js';
6
6
  import { useMedaShell } from '../shell-provider.js';
@@ -9,7 +9,7 @@ import { useMedaShell } from '../shell-provider.js';
9
9
  * any custom-content drawers. Mount once near the AppShell root; drawers
10
10
  * open/close via `ctx.mobileDrawer.open` provider state.
11
11
  */
12
- export function MobileDrawers({ menuItems = [], module, panelViews = [], defaultView, customContent = {}, }) {
12
+ export function MobileDrawers({ menuItems = [], menuActiveId, menuRenderLink, module, moduleAppId, panelViews = [], defaultView, customContent = {}, }) {
13
13
  const ctx = useMedaShell();
14
14
  const open = ctx.mobileDrawer.open;
15
15
  const setOpen = ctx.mobileDrawer.setOpen;
@@ -18,24 +18,59 @@ export function MobileDrawers({ menuItems = [], module, panelViews = [], default
18
18
  workspaceId: ctx.workspace.id,
19
19
  appId: ctx.activeAppId,
20
20
  };
21
- return (_jsxs(_Fragment, { children: [_jsx(MenuDrawer, { open: open === 'menu-drawer', onClose: close, items: menuItems }), _jsx(ModuleDrawer, { open: open === 'module-drawer', onClose: close, module: module }), _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)))] }));
21
+ const moduleRenderCtx = {
22
+ workspaceId: ctx.workspace.id,
23
+ appId: moduleAppId ?? ctx.activeAppId,
24
+ };
25
+ return (_jsxs(_Fragment, { children: [_jsx(MenuDrawer, { open: open === 'menu-drawer', onClose: close, items: menuItems, activeId: menuActiveId, renderLink: menuRenderLink }), _jsx(ModuleDrawer, { open: open === 'module-drawer', onClose: close, module: module, renderCtx: moduleRenderCtx }), _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)))] }));
22
26
  }
23
27
  // ---------------------------------------------------------------------------
24
28
  // Internal sub-drawers
25
29
  // ---------------------------------------------------------------------------
26
- function MenuDrawer({ open, onClose, items, }) {
27
- 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." })] }), _jsx("nav", { className: "flex flex-col gap-0.5 p-2", children: items.map((item) => {
28
- const Icon = item.icon;
29
- 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: 18, "aria-hidden": "true" }), _jsx("span", { children: item.label })] }, item.id));
30
- }) })] }) }));
30
+ function MenuDrawer({ open, onClose, items, activeId, renderLink, }) {
31
+ 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." })] }), _jsx("nav", { className: "flex flex-col gap-0.5 p-2", children: items.map((item) => (_jsx(MenuDrawerItem, { item: item, isActive: item.id === activeId, onClose: onClose, renderLink: renderLink }, item.id))) })] }) }));
32
+ }
33
+ const menuItemClassName = 'flex items-center gap-2 rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground';
34
+ function MenuDrawerItem({ item, isActive, onClose, renderLink, }) {
35
+ const Icon = item.icon;
36
+ const children = (_jsxs(_Fragment, { children: [_jsx(Icon, { size: 18, "aria-hidden": "true" }), _jsx("span", { children: item.label })] }));
37
+ const className = cn(menuItemClassName, isActive && 'bg-accent text-foreground');
38
+ const linkProps = {
39
+ href: item.to,
40
+ className,
41
+ children,
42
+ };
43
+ if (renderLink) {
44
+ return closeAfterLinkClick(renderLink({
45
+ item,
46
+ isActive,
47
+ className,
48
+ children,
49
+ linkProps,
50
+ }), onClose);
51
+ }
52
+ return closeAfterLinkClick(_jsx("a", { ...linkProps }), onClose);
53
+ }
54
+ function closeAfterLinkClick(link, onClose) {
55
+ if (!isValidElement(link)) {
56
+ return link;
57
+ }
58
+ const originalOnClick = link.props.onClick;
59
+ return cloneElement(link, {
60
+ onClick: (event) => {
61
+ originalOnClick?.(event);
62
+ onClose();
63
+ },
64
+ });
31
65
  }
32
- function ModuleDrawer({ open, onClose, module, }) {
33
- if (!module)
66
+ function ModuleDrawer({ open, onClose, module, renderCtx, }) {
67
+ const items = module?.items ?? [];
68
+ if (!module || (items.length === 0 && !module.render))
34
69
  return null;
35
- return (_jsx(Drawer, { open: open, onOpenChange: (o) => !o && onClose(), direction: "left", children: _jsxs(DrawerContent, { children: [_jsxs(DrawerHeader, { children: [_jsx(DrawerTitle, { children: module.label }), module.description && (_jsx(DrawerDescription, { className: "text-muted-foreground text-xs", children: module.description }))] }), _jsx("nav", { className: "flex flex-col gap-0.5 p-2", children: module.items.map((item) => {
70
+ return (_jsx(Drawer, { open: open, onOpenChange: (o) => !o && onClose(), direction: "left", children: _jsxs(DrawerContent, { children: [_jsxs(DrawerHeader, { children: [_jsx(DrawerTitle, { children: module.label }), module.description && (_jsx(DrawerDescription, { className: "text-muted-foreground text-xs", children: module.description }))] }), items.length > 0 && (_jsx("nav", { className: "flex flex-col gap-0.5 p-2", children: items.map((item) => {
36
71
  const Icon = item.icon;
37
72
  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));
38
- }) })] }) }));
73
+ }) })), module.render?.(renderCtx)] }) }));
39
74
  }
40
75
  function PanelsDrawer({ open, onClose, panelViews, defaultView, renderCtx, }) {
41
76
  const ctx = useMedaShell();
@@ -0,0 +1,15 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type PanelViewRegistration } from './shell-provider.js';
3
+ import type { PanelView } from './types.js';
4
+ export interface PanelViewsProviderProps {
5
+ views: PanelView[];
6
+ defaultView?: string;
7
+ children: ReactNode;
8
+ }
9
+ export interface ResolvedPanelViews {
10
+ panelViews: PanelView[];
11
+ defaultView?: string;
12
+ }
13
+ export declare function mergePanelViews(staticViews: PanelView[] | undefined, registrations: PanelViewRegistration[], staticDefaultView?: string): ResolvedPanelViews;
14
+ export declare function useResolvedPanelViews(staticViews?: PanelView[], staticDefaultView?: string): ResolvedPanelViews;
15
+ export declare function PanelViewsProvider({ views, defaultView, children }: PanelViewsProviderProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,56 @@
1
+ 'use client';
2
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
3
+ import { useId, useLayoutEffect, useMemo, useRef } from 'react';
4
+ import { useMedaShell } from './shell-provider.js';
5
+ export function mergePanelViews(staticViews = [], registrations, staticDefaultView) {
6
+ const panelViews = [];
7
+ for (const view of staticViews) {
8
+ panelViews.push(view);
9
+ }
10
+ let defaultView = staticDefaultView;
11
+ for (const registration of registrations) {
12
+ for (const view of registration.views) {
13
+ const previousIndex = panelViews.findIndex((existingView) => existingView.id === view.id);
14
+ if (previousIndex >= 0) {
15
+ panelViews.splice(previousIndex, 1);
16
+ }
17
+ panelViews.push(view);
18
+ }
19
+ if (registration.defaultView !== undefined) {
20
+ defaultView = registration.defaultView;
21
+ }
22
+ }
23
+ return defaultView === undefined ? { panelViews } : { panelViews, defaultView };
24
+ }
25
+ export function useResolvedPanelViews(staticViews = [], staticDefaultView) {
26
+ const { panelViews } = useMedaShell();
27
+ return useMemo(() => mergePanelViews(staticViews, panelViews.registrations, staticDefaultView), [staticViews, panelViews.registrations, staticDefaultView]);
28
+ }
29
+ function getPanelViewsSignature(views) {
30
+ return views.map((view) => `${view.id}\u001f${view.label}`).join('\u001e');
31
+ }
32
+ export function PanelViewsProvider({ views, defaultView, children }) {
33
+ const { register } = useMedaShell().panelViews;
34
+ const registrationId = useId();
35
+ const latestViewsRef = useRef(views);
36
+ latestViewsRef.current = views;
37
+ const viewsSignature = getPanelViewsSignature(views);
38
+ const registeredViewsRef = useRef(null);
39
+ if (registeredViewsRef.current?.signature !== viewsSignature) {
40
+ registeredViewsRef.current = {
41
+ signature: viewsSignature,
42
+ views: views.map((view) => ({
43
+ id: view.id,
44
+ label: view.label,
45
+ icon: view.icon,
46
+ render: (ctx) => latestViewsRef.current.find((currentView) => currentView.id === view.id)?.render(ctx) ??
47
+ null,
48
+ })),
49
+ };
50
+ }
51
+ const registeredViews = registeredViewsRef.current.views;
52
+ useLayoutEffect(() => {
53
+ return register(registrationId, registeredViews, defaultView);
54
+ }, [register, registrationId, registeredViews, defaultView]);
55
+ return _jsx(_Fragment, { children: children });
56
+ }
@@ -0,0 +1,9 @@
1
+ export type { ShellStorageAdapter } from './layout-state.js';
2
+ export { createLocalStorageAdapter } from './layout-state.js';
3
+ export type { PanelViewsProviderProps } from './panel-views-provider.js';
4
+ export { PanelViewsProvider } from './panel-views-provider.js';
5
+ export { ResizableHandle, ResizableShell, ResizableShellPanel } from './resizable-shell.js';
6
+ export type { MedaShellProviderProps } from './shell-provider.js';
7
+ export { MedaShellProvider, useMedaShell, useShellSelection } from './shell-provider.js';
8
+ export type { AppDefinition, CommandDefinition, ContextItem, ContextModule, MobileBottomNavItem, PanelMode, PanelView, ShellLinkRenderArgs, ShellRenderContext, ShellViewport, ThemeAdapter, WorkspaceDefinition, } from './types.js';
9
+ export { useShellViewport } from './use-shell-viewport.js';
@@ -0,0 +1,7 @@
1
+ // State and layout primitives for consumers that want Meda shell behavior
2
+ // without importing the full styled shell barrel.
3
+ export { createLocalStorageAdapter } from './layout-state.js';
4
+ export { PanelViewsProvider } from './panel-views-provider.js';
5
+ export { ResizableHandle, ResizableShell, ResizableShellPanel } from './resizable-shell.js';
6
+ export { MedaShellProvider, useMedaShell, useShellSelection } from './shell-provider.js';
7
+ export { useShellViewport } from './use-shell-viewport.js';
@@ -1,3 +1,4 @@
1
+ import type { ButtonHTMLAttributes, ReactNode } from 'react';
1
2
  import type { PanelMode, PanelView } from './types.js';
2
3
  export interface RightPanelProps {
3
4
  /** Views to render as tabs in the panel header. */
@@ -10,6 +11,16 @@ export interface RightPanelProps {
10
11
  * If only ['panel'], the cycle button is hidden.
11
12
  */
12
13
  modes?: PanelMode[];
14
+ renderTab?: (args: RightPanelTabRenderArgs) => ReactNode;
13
15
  className?: string;
14
16
  }
15
- export declare function RightPanel({ panelViews, defaultView, modes, className, }: RightPanelProps): import("react/jsx-runtime").JSX.Element | null;
17
+ export interface RightPanelTabRenderArgs {
18
+ view: PanelView;
19
+ isActive: boolean;
20
+ buttonProps: RightPanelTabButtonProps;
21
+ children: ReactNode;
22
+ }
23
+ export type RightPanelTabButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
24
+ 'data-active'?: boolean;
25
+ };
26
+ export declare function RightPanel({ panelViews, defaultView, modes, renderTab, className, }: RightPanelProps): import("react/jsx-runtime").JSX.Element | null;
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  /**
4
4
  * RightPanel — spec §12
5
5
  *
@@ -16,8 +16,9 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
16
16
  * TODO(phase-15-refactor): swap to ResizableShell once AppShellBody is a PanelGroup
17
17
  */
18
18
  import { Maximize2, Minimize2, X } from 'lucide-react';
19
- import { useEffect, useRef, useState } from 'react';
19
+ import { Fragment, useEffect, useRef, useState } from 'react';
20
20
  import { cn } from '../lib/utils.js';
21
+ import { useResolvedPanelViews } from './panel-views-provider.js';
21
22
  import { useMedaShell } from './shell-provider.js';
22
23
  import { useShellViewport } from './use-shell-viewport.js';
23
24
  // ---------------------------------------------------------------------------
@@ -25,6 +26,7 @@ import { useShellViewport } from './use-shell-viewport.js';
25
26
  // ---------------------------------------------------------------------------
26
27
  const MIN_WIDTH = 300;
27
28
  const MAX_WIDTH = 520;
29
+ const EMPTY_PANEL_VIEWS = [];
28
30
  function ResizeHandle({ currentWidth, onResize, onCommit }) {
29
31
  const startWidthRef = useRef(currentWidth);
30
32
  const startXRef = useRef(0);
@@ -61,17 +63,28 @@ function ResizeHandle({ currentWidth, onResize, onCommit }) {
61
63
  // ---------------------------------------------------------------------------
62
64
  // RightPanel
63
65
  // ---------------------------------------------------------------------------
64
- export function RightPanel({ panelViews = [], defaultView, modes = ['panel', 'expanded', 'fullscreen'], className, }) {
66
+ export function RightPanel({ panelViews = EMPTY_PANEL_VIEWS, defaultView, modes = ['panel', 'expanded', 'fullscreen'], renderTab, className, }) {
65
67
  const band = useShellViewport();
66
68
  const ctx = useMedaShell();
67
69
  const { mode, activeView, width, setMode, setActiveView, setWidth } = ctx.panel;
68
- // Hydrate defaultView on mount if no active view set
69
- // biome-ignore lint/correctness/useExhaustiveDependencies: intentional one-time mount effect
70
+ const { panelViews: resolvedPanelViews, defaultView: resolvedDefaultView } = useResolvedPanelViews(panelViews, defaultView);
71
+ const hydratedDefaultRef = useRef(null);
72
+ // Hydrate a default when static or route-registered views become available.
70
73
  useEffect(() => {
71
- if (!activeView && defaultView) {
72
- setActiveView(defaultView);
74
+ const defaultExists = resolvedDefaultView != null &&
75
+ resolvedPanelViews.some((view) => view.id === resolvedDefaultView);
76
+ if (!defaultExists)
77
+ return;
78
+ const source = resolvedDefaultView !== defaultView ? 'registered' : 'static';
79
+ const canReplaceStaticDefault = activeView === hydratedDefaultRef.current?.id &&
80
+ hydratedDefaultRef.current.source === 'static' &&
81
+ source === 'registered' &&
82
+ activeView !== resolvedDefaultView;
83
+ if (!activeView || canReplaceStaticDefault) {
84
+ hydratedDefaultRef.current = { id: resolvedDefaultView, source };
85
+ setActiveView(resolvedDefaultView);
73
86
  }
74
- }, []);
87
+ }, [activeView, defaultView, resolvedDefaultView, resolvedPanelViews, setActiveView]);
75
88
  // Local display width tracks pointer-move updates live;
76
89
  // setWidth (persist) is called on pointerUp.
77
90
  const [displayWidth, setDisplayWidth] = useState(null);
@@ -109,18 +122,30 @@ export function RightPanel({ panelViews = [], defaultView, modes = ['panel', 'ex
109
122
  })();
110
123
  // Z-index escalation: fullscreen covers header + rails
111
124
  const zIndexClass = mode === 'fullscreen' ? 'z-[var(--z-shell-fullscreen)]' : 'z-[var(--z-shell-panel)]';
112
- const activePanelView = panelViews.find((v) => v.id === activeView);
125
+ const activePanelView = resolvedPanelViews.find((v) => v.id === activeView);
113
126
  const cycleAriaLabel = mode === 'panel' ? 'Expand panel' : mode === 'expanded' ? 'Maximize panel' : 'Restore panel';
114
127
  const handleResize = (w) => setDisplayWidth(w);
115
128
  const handleCommit = (w) => {
116
129
  setDisplayWidth(null);
117
130
  setWidth(w);
118
131
  };
119
- return (_jsx("aside", { "data-meda-panel-mode": mode, "aria-hidden": mode === 'closed' ? 'true' : undefined, className: cn('relative h-full shrink-0 overflow-hidden border-l border-shell-border bg-shell-panel', 'transition-[width] ease-[var(--motion-ease)] duration-[var(--motion-panel)]', mode === 'fullscreen' && 'fixed inset-0 h-screen w-screen border-none', zIndexClass, className), style: widthStyle, children: mode !== 'closed' && (_jsxs("div", { className: "flex h-full flex-col", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-shell-border px-3 py-2", children: [_jsx("div", { className: "flex items-center gap-1", children: panelViews.map((view) => {
132
+ return (_jsx("aside", { "data-meda-panel-mode": mode, "aria-hidden": mode === 'closed' ? 'true' : undefined, className: cn('relative h-full shrink-0 overflow-hidden border-l border-shell-border bg-shell-panel', 'transition-[width] ease-[var(--motion-ease)] duration-[var(--motion-panel)]', mode === 'fullscreen' && 'fixed inset-0 h-screen w-screen border-none', zIndexClass, className), style: widthStyle, children: mode !== 'closed' && (_jsxs("div", { className: "flex h-full flex-col", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-shell-border px-3 py-2", children: [_jsx("div", { className: "flex items-center gap-1", children: resolvedPanelViews.map((view) => {
120
133
  const isActive = view.id === activeView;
121
134
  const Icon = view.icon;
122
- return (_jsxs("button", { type: "button", "aria-current": isActive ? 'true' : undefined, onClick: () => setActiveView(view.id), className: cn('inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium transition-colors', isActive
135
+ const children = (_jsxs(_Fragment, { children: [_jsx(Icon, { size: 14, "aria-hidden": "true" }), _jsx("span", { children: view.label })] }));
136
+ const buttonProps = {
137
+ type: 'button',
138
+ 'aria-current': isActive ? 'true' : undefined,
139
+ 'data-active': isActive || undefined,
140
+ onClick: () => setActiveView(view.id),
141
+ className: cn('inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium transition-colors', isActive
123
142
  ? 'bg-accent text-accent-foreground'
124
- : 'text-muted-foreground hover:bg-accent hover:text-foreground'), children: [_jsx(Icon, { size: 14, "aria-hidden": "true" }), _jsx("span", { children: view.label })] }, view.id));
143
+ : 'text-muted-foreground hover:bg-accent hover:text-foreground'),
144
+ children,
145
+ };
146
+ if (renderTab) {
147
+ return (_jsx(Fragment, { children: renderTab({ view, isActive, buttonProps, children }) }, view.id));
148
+ }
149
+ return _jsx("button", { ...buttonProps }, view.id);
125
150
  }) }), _jsxs("div", { className: "flex items-center gap-1", children: [modes.length > 1 && (_jsx("button", { type: "button", "aria-label": cycleAriaLabel, onClick: cycleOpenMode, className: "inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground", children: mode === 'fullscreen' ? (_jsx(Minimize2, { size: 14, "aria-hidden": "true" })) : (_jsx(Maximize2, { size: 14, "aria-hidden": "true" })) })), _jsx("button", { type: "button", "aria-label": "Close panel", onClick: () => setMode('closed'), className: "inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground", children: _jsx(X, { size: 14, "aria-hidden": "true" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto", children: activePanelView != null ? (activePanelView.render(renderCtx)) : (_jsx("div", { className: "p-4 text-muted-foreground text-sm", children: "No panel view selected" })) }), mode === 'panel' && (_jsx(ResizeHandle, { currentWidth: resolvedWidth, onResize: handleResize, onCommit: handleCommit }))] })) }));
126
151
  }