@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.
- package/dist/shell/app-shell-workspace.d.ts +9 -1
- package/dist/shell/app-shell-workspace.js +12 -3
- package/dist/shell/app-shell.d.ts +12 -0
- package/dist/shell/app-shell.js +3 -3
- package/dist/shell/context-rail.js +19 -22
- package/dist/shell/icon-rail.d.ts +3 -1
- package/dist/shell/icon-rail.js +27 -9
- package/dist/shell/index.d.ts +2 -2
- package/dist/shell/internal/mobile-bottom-nav.js +2 -2
- package/dist/shell/internal/mobile-drawers.d.ts +11 -1
- package/dist/shell/internal/mobile-drawers.js +27 -6
- package/dist/shell/internal/mobile-header.d.ts +3 -1
- package/dist/shell/internal/mobile-header.js +2 -2
- package/dist/shell/shell-header.d.ts +15 -3
- package/dist/shell/shell-header.js +32 -11
- package/dist/shell/shell-main.js +1 -1
- package/dist/shell/types.d.ts +2 -0
- package/dist/styles/theme.css +1 -0
- package/dist/styles/tokens.css +3 -1
- package/dist/workflow-builder/workflow-canvas.d.ts +0 -1
- package/dist/workflow-builder/workflow-canvas.js +0 -1
- package/package.json +10 -2
- package/skills/brand/SKILL.md +198 -0
- package/skills/components/SKILL.md +196 -0
- package/skills/shell/SKILL.md +151 -0
|
@@ -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-
|
|
43
|
-
|
|
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;
|
package/dist/shell/app-shell.js
CHANGED
|
@@ -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-
|
|
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(
|
|
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 {
|
|
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
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
|
|
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
|
|
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 })] })] })
|
|
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;
|
package/dist/shell/icon-rail.js
CHANGED
|
@@ -1,34 +1,48 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx,
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
}
|
package/dist/shell/index.d.ts
CHANGED
|
@@ -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-
|
|
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-
|
|
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
|
-
|
|
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
|
|
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
|
|
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;
|