@medalsocial/meda 1.6.0 → 2.0.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/README.md +19 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/primitives/empty-state.d.ts +11 -0
- package/dist/primitives/empty-state.js +23 -0
- package/dist/primitives/filter-rail.d.ts +20 -0
- package/dist/primitives/filter-rail.js +16 -0
- package/dist/primitives/index.d.ts +6 -0
- package/dist/primitives/index.js +3 -0
- package/dist/primitives/skeleton.d.ts +3 -0
- package/dist/primitives/skeleton.js +6 -0
- package/dist/shell/app-shell-workspace.js +1 -1
- package/dist/shell/context-rail.d.ts +4 -2
- package/dist/shell/context-rail.js +27 -19
- package/dist/shell/index.d.ts +1 -1
- package/dist/shell/internal/mobile-drawers.d.ts +6 -2
- package/dist/shell/internal/mobile-drawers.js +10 -7
- package/dist/shell/shell-provider.js +69 -53
- package/dist/shell/types.d.ts +4 -0
- package/dist/shell/use-shell-viewport.js +14 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -109,6 +109,25 @@ Workspace shells also expose chrome-level composition slots:
|
|
|
109
109
|
|
|
110
110
|
`workspace.menuItems`, `workspace.menuFooter`, and the theme toggle are available from the mobile Menu drawer. Use `mainLayout`/`mainClassName` when a workspace shell needs the same mobile chrome but a custom main scroll region, such as a full-bleed marketing page. `useCommands()` works from workspace descendants without manually mounting `CommandPalette`; lower-level primitive compositions can still mount `CommandPalette` directly.
|
|
111
111
|
|
|
112
|
+
`ContextRail` is usable for both navigation rails and custom rendered rails. Navigation rails show Meda's label header by default. Custom rendered rails hide the automatic visible header by default so consumers can render their own heading without duplication. Rail bodies scroll vertically by default; use `contextRail={{ header: "visible" }}` or `contextRail={{ scroll: "none" }}` when you need explicit control.
|
|
113
|
+
|
|
114
|
+
## Foundation primitives
|
|
115
|
+
|
|
116
|
+
Meda includes small foundation primitives for repeated loading, empty, and filtering surfaces:
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
import { EmptyState, FilterRail, Skeleton } from '@medalsocial/meda';
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`Skeleton` mirrors shadcn's simple loading placeholder shape. `EmptyState` standardizes zero/error states across panels and content areas. `FilterRail` provides a dense filter surface while leaving selected values, URL syncing, and query logic in the consuming app.
|
|
123
|
+
|
|
124
|
+
The same primitives are available from the shadcn-compatible registry when an app wants local source ownership:
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
npx shadcn add https://meda.medalsocial.com/r/meda-skeleton.json
|
|
128
|
+
npx shadcn add https://meda.medalsocial.com/r/meda-empty-state.json
|
|
129
|
+
npx shadcn add https://meda.medalsocial.com/r/meda-filter-rail.json
|
|
130
|
+
```
|
|
112
131
|
For app-scoped brand tokens:
|
|
113
132
|
|
|
114
133
|
```ts
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export * from './brand/public.js';
|
|
|
3
3
|
export * from './chat/public.js';
|
|
4
4
|
export * from './marketing/public.js';
|
|
5
5
|
export * from './panel/public.js';
|
|
6
|
+
export * from './primitives/index.js';
|
|
6
7
|
export * from './shell/index.js';
|
|
7
8
|
export * from './theme/index.js';
|
|
8
9
|
export * from './timeline/public.js';
|
package/dist/index.js
CHANGED
|
@@ -3,6 +3,7 @@ export * from './brand/public.js';
|
|
|
3
3
|
export * from './chat/public.js';
|
|
4
4
|
export * from './marketing/public.js';
|
|
5
5
|
export * from './panel/public.js';
|
|
6
|
+
export * from './primitives/index.js';
|
|
6
7
|
export * from './shell/index.js'; // v2 surface
|
|
7
8
|
export * from './theme/index.js';
|
|
8
9
|
export * from './timeline/public.js';
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { LucideIcon } from 'lucide-react';
|
|
2
|
+
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
3
|
+
export type EmptyStateVariant = 'default' | 'panel' | 'inline';
|
|
4
|
+
export interface EmptyStateProps extends Omit<ComponentPropsWithoutRef<'div'>, 'title'> {
|
|
5
|
+
icon?: LucideIcon | ReactNode;
|
|
6
|
+
title: ReactNode;
|
|
7
|
+
description?: ReactNode;
|
|
8
|
+
action?: ReactNode;
|
|
9
|
+
variant?: EmptyStateVariant;
|
|
10
|
+
}
|
|
11
|
+
export declare function EmptyState({ icon, title, description, action, variant, className, ...props }: EmptyStateProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { isValidElement } from 'react';
|
|
4
|
+
import { cn } from '../lib/utils.js';
|
|
5
|
+
function isIconComponent(icon) {
|
|
6
|
+
if (typeof icon === 'function')
|
|
7
|
+
return true;
|
|
8
|
+
if (typeof icon !== 'object')
|
|
9
|
+
return false;
|
|
10
|
+
return '$$typeof' in icon && !isValidElement(icon);
|
|
11
|
+
}
|
|
12
|
+
function renderIcon(icon) {
|
|
13
|
+
if (!icon)
|
|
14
|
+
return null;
|
|
15
|
+
if (isIconComponent(icon)) {
|
|
16
|
+
const Icon = icon;
|
|
17
|
+
return _jsx(Icon, { "data-testid": "empty-state-icon", className: "size-10", "aria-hidden": "true" });
|
|
18
|
+
}
|
|
19
|
+
return (_jsx("span", { "data-testid": "empty-state-icon", "aria-hidden": "true", className: "inline-flex", children: icon }));
|
|
20
|
+
}
|
|
21
|
+
export function EmptyState({ icon, title, description, action, variant = 'default', className, ...props }) {
|
|
22
|
+
return (_jsxs("div", { "data-slot": "empty-state", "data-variant": variant, className: cn('flex flex-col items-center justify-center text-center', variant === 'default' && 'px-6 py-16', variant === 'panel' && 'px-4 py-10', variant === 'inline' && 'px-3 py-6', className), ...props, children: [icon ? (_jsx("div", { "data-slot": "empty-state-icon", className: cn('mb-4 inline-flex items-center justify-center rounded-md text-muted-foreground', variant === 'inline' ? 'size-9' : 'size-12'), children: renderIcon(icon) })) : null, _jsx("h3", { "data-slot": "empty-state-title", className: cn('font-semibold text-foreground', variant === 'default' && 'text-lg', variant === 'panel' && 'text-base', variant === 'inline' && 'text-sm'), children: title }), description ? (_jsx("p", { "data-slot": "empty-state-description", className: cn('mt-1 max-w-sm text-muted-foreground', variant === 'inline' ? 'text-xs' : 'text-sm'), children: description })) : null, action ? (_jsx("div", { "data-slot": "empty-state-action", className: "mt-5", children: action })) : null] }));
|
|
23
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type ComponentPropsWithoutRef, type ReactNode } from 'react';
|
|
2
|
+
export interface FilterRailProps extends Omit<ComponentPropsWithoutRef<'aside'>, 'title'> {
|
|
3
|
+
title?: ReactNode;
|
|
4
|
+
description?: ReactNode;
|
|
5
|
+
search?: ReactNode;
|
|
6
|
+
actions?: ReactNode;
|
|
7
|
+
footer?: ReactNode;
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
}
|
|
10
|
+
export interface FilterRailGroupProps extends Omit<ComponentPropsWithoutRef<'fieldset'>, 'title'> {
|
|
11
|
+
title?: ReactNode;
|
|
12
|
+
description?: ReactNode;
|
|
13
|
+
children?: ReactNode;
|
|
14
|
+
}
|
|
15
|
+
declare function FilterRailGroup({ title, description, children, className, ...props }: FilterRailGroupProps): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
declare function FilterRailRoot({ title, description, search, actions, footer, children, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...props }: FilterRailProps): import("react/jsx-runtime").JSX.Element;
|
|
17
|
+
export declare const FilterRail: typeof FilterRailRoot & {
|
|
18
|
+
Group: typeof FilterRailGroup;
|
|
19
|
+
};
|
|
20
|
+
export {};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useId } from 'react';
|
|
4
|
+
import { cn } from '../lib/utils.js';
|
|
5
|
+
function FilterRailGroup({ title, description, children, className, ...props }) {
|
|
6
|
+
return (_jsxs("fieldset", { "data-slot": "filter-rail-group", className: cn('space-y-2 border-0 p-0', className), ...props, children: [title ? (_jsx("legend", { "data-slot": "filter-rail-group-title", className: "text-xs font-semibold text-foreground", children: title })) : null, description ? (_jsx("p", { "data-slot": "filter-rail-group-description", className: "text-xs text-muted-foreground", children: description })) : null, _jsx("div", { "data-slot": "filter-rail-group-content", className: "space-y-1.5", children: children })] }));
|
|
7
|
+
}
|
|
8
|
+
function FilterRailRoot({ title, description, search, actions, footer, children, className, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledBy, ...props }) {
|
|
9
|
+
const titleId = useId();
|
|
10
|
+
const label = ariaLabel ?? (ariaLabelledBy ? undefined : typeof title === 'string' ? title : undefined);
|
|
11
|
+
const labelledBy = ariaLabelledBy ?? (label ? undefined : title ? titleId : undefined);
|
|
12
|
+
return (_jsxs("aside", { "data-slot": "filter-rail", "aria-label": label, "aria-labelledby": labelledBy, className: cn('flex min-h-0 w-full flex-col border-border bg-card text-card-foreground', className), ...props, children: [title || description || actions ? (_jsxs("div", { "data-slot": "filter-rail-header", className: "flex shrink-0 items-start justify-between gap-3 border-b border-border px-4 py-3", children: [_jsxs("div", { className: "min-w-0", children: [title ? (_jsx("h2", { id: titleId, "data-slot": "filter-rail-title", className: "text-sm font-semibold text-foreground", children: title })) : null, description ? (_jsx("p", { "data-slot": "filter-rail-description", className: "mt-0.5 text-xs text-muted-foreground", children: description })) : null] }), actions ? (_jsx("div", { "data-slot": "filter-rail-actions", className: "shrink-0", children: actions })) : null] })) : null, search ? (_jsx("div", { "data-slot": "filter-rail-search", className: "shrink-0 border-b border-border p-3", children: search })) : null, _jsx("div", { "data-slot": "filter-rail-content", className: "min-h-0 flex-1 space-y-5 overflow-y-auto p-4", children: children }), footer ? (_jsx("div", { "data-slot": "filter-rail-footer", className: "shrink-0 border-t border-border p-3", children: footer })) : null] }));
|
|
13
|
+
}
|
|
14
|
+
export const FilterRail = Object.assign(FilterRailRoot, {
|
|
15
|
+
Group: FilterRailGroup,
|
|
16
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { EmptyStateProps, EmptyStateVariant } from './empty-state.js';
|
|
2
|
+
export { EmptyState } from './empty-state.js';
|
|
3
|
+
export type { FilterRailGroupProps, FilterRailProps } from './filter-rail.js';
|
|
4
|
+
export { FilterRail } from './filter-rail.js';
|
|
5
|
+
export type { SkeletonProps } from './skeleton.js';
|
|
6
|
+
export { Skeleton } from './skeleton.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
+
import { cn } from '../lib/utils.js';
|
|
4
|
+
export function Skeleton({ className, 'aria-hidden': ariaHidden = true, ...props }) {
|
|
5
|
+
return (_jsx("div", { "data-slot": "skeleton", "aria-hidden": ariaHidden, className: cn('animate-pulse rounded-md bg-muted', className), ...props }));
|
|
6
|
+
}
|
|
@@ -39,7 +39,7 @@ export function AppShellWorkspace({ iconRail, contextRail, rightPanel, workspace
|
|
|
39
39
|
// <AppShell> wrapper already enforces h-screen for the workspace variant, so
|
|
40
40
|
// nested viewport-height divs collapse cleanly — no double-scroll.
|
|
41
41
|
const commandRegistry = useContext(CommandRegistryContext);
|
|
42
|
-
const shell = (_jsxs("div", { className: "flex h-screen flex-col", children: [isMobile ? (_jsx(MobileHeader, { globalActions: globalActions })) : (_jsx(ShellHeader, { globalActions: globalActions, headerCenter: headerCenter, appTabsRenderLink: appTabs?.renderLink, workspaceMenuItems: workspace?.menuItems, workspaceMenuFooter: workspace?.menuFooter })), banners ? (_jsx("div", { "data-meda-banners": "", className: "flex-shrink-0", children: banners })) : (false), _jsxs("div", { className: "relative flex flex-1 overflow-hidden", children: [!isMobile && iconRail && (_jsx(IconRail, { mainItems: iconRail.mainItems, utilityItems: iconRail.utilityItems, footer: iconRail.footer, activeId: iconRail.activeId, renderLink: iconRail.renderLink })), !isMobile && contextRail && (_jsx(ContextRail, { appId: contextRail.appId, module: contextRail.module, activeItemId: contextRail.activeItemId })), _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, panelViews: resolvedRightPanel.panelViews, defaultView: resolvedRightPanel.defaultView }))] }));
|
|
42
|
+
const shell = (_jsxs("div", { className: "flex h-screen flex-col", children: [isMobile ? (_jsx(MobileHeader, { globalActions: globalActions })) : (_jsx(ShellHeader, { globalActions: globalActions, headerCenter: headerCenter, appTabsRenderLink: appTabs?.renderLink, workspaceMenuItems: workspace?.menuItems, workspaceMenuFooter: workspace?.menuFooter })), banners ? (_jsx("div", { "data-meda-banners": "", className: "flex-shrink-0", children: banners })) : (false), _jsxs("div", { className: "relative flex flex-1 overflow-hidden", children: [!isMobile && iconRail && (_jsx(IconRail, { mainItems: iconRail.mainItems, utilityItems: iconRail.utilityItems, footer: iconRail.footer, activeId: iconRail.activeId, renderLink: iconRail.renderLink })), !isMobile && contextRail && (_jsx(ContextRail, { appId: contextRail.appId, module: contextRail.module, activeItemId: contextRail.activeItemId, header: contextRail.header, scroll: contextRail.scroll })), _jsx(ShellMain, { layout: mainLayout ?? 'workspace', className: mainClassName, children: children }), !isMobile && resolvedRightPanel.panelViews.length > 0 && (_jsx(RightPanel, { panelViews: staticPanelViews, defaultView: rightPanel?.defaultView }))] }), isMobile && hasDrawerContent && _jsx(MobileBottomNav, { items: navItems }), isMobile && hasDrawerContent && (_jsx(MobileDrawers, { menuItems: mobileMenuItems, menuActiveId: iconRail?.activeId, menuRenderLink: iconRail?.renderLink, workspaceMenuItems: workspace?.menuItems, workspaceMenuFooter: workspace?.menuFooter, module: contextRail?.module, moduleAppId: contextRail?.appId, moduleHeader: contextRail?.header, moduleScroll: contextRail?.scroll, panelViews: resolvedRightPanel.panelViews, defaultView: resolvedRightPanel.defaultView }))] }));
|
|
43
43
|
return commandRegistry ? shell : _jsx(CommandPalette, { children: shell });
|
|
44
44
|
}
|
|
45
45
|
function buildMobileNavItems(contextRail, panelViews) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import type { ContextModule, ShellLinkRenderArgs } from './types.js';
|
|
2
|
+
import type { ContextModule, ContextRailHeader, ContextRailScroll, ShellLinkRenderArgs } from './types.js';
|
|
3
3
|
export interface ContextRailProps {
|
|
4
4
|
/** Drives the persistence key for layout state. */
|
|
5
5
|
appId: string;
|
|
@@ -12,6 +12,8 @@ export interface ContextRailProps {
|
|
|
12
12
|
/** Optional active item id for nav active state (preferred over useMedaShell().selection). */
|
|
13
13
|
activeItemId?: string;
|
|
14
14
|
renderLink?: (args: ShellLinkRenderArgs) => ReactNode;
|
|
15
|
+
header?: ContextRailHeader;
|
|
16
|
+
scroll?: ContextRailScroll;
|
|
15
17
|
className?: string;
|
|
16
18
|
}
|
|
17
|
-
export declare function ContextRail({ appId, module, hidden, collapsible, activeItemId, renderLink, className, }: ContextRailProps): import("react/jsx-runtime").JSX.Element | null;
|
|
19
|
+
export declare function ContextRail({ appId, module, hidden, collapsible, activeItemId, renderLink, header, scroll, className, }: ContextRailProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -75,7 +75,7 @@ function ContextRailToggle({ railId }) {
|
|
|
75
75
|
// ---------------------------------------------------------------------------
|
|
76
76
|
// ContextRail
|
|
77
77
|
// ---------------------------------------------------------------------------
|
|
78
|
-
export function ContextRail({ appId, module, hidden = false, collapsible = true, activeItemId, renderLink, className, }) {
|
|
78
|
+
export function ContextRail({ appId, module, hidden = false, collapsible = true, activeItemId, renderLink, header = 'auto', scroll = 'auto', className, }) {
|
|
79
79
|
const band = useShellViewport();
|
|
80
80
|
const ctx = useMedaShell();
|
|
81
81
|
// collapsible={false} means the rail must always render expanded — even if
|
|
@@ -92,6 +92,8 @@ export function ContextRail({ appId, module, hidden = false, collapsible = true,
|
|
|
92
92
|
const [displayWidth, setDisplayWidth] = useState(null);
|
|
93
93
|
const width = displayWidth ?? ctx.contextRail.width;
|
|
94
94
|
const items = module?.items ?? [];
|
|
95
|
+
const hasRender = typeof module?.render === 'function';
|
|
96
|
+
const showHeader = header === 'visible' || (header === 'auto' && items.length > 0 && !hasRender);
|
|
95
97
|
if (band === 'mobile')
|
|
96
98
|
return null;
|
|
97
99
|
if (hidden) {
|
|
@@ -113,22 +115,28 @@ export function ContextRail({ appId, module, hidden = false, collapsible = true,
|
|
|
113
115
|
// transition then so the rail snaps to the cursor instead of lagging
|
|
114
116
|
// behind it.
|
|
115
117
|
const isDragging = displayWidth !== null;
|
|
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) => {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
118
|
+
return (_jsxs("aside", { id: railId, "data-testid": "context-rail", "aria-label": module.label, className: cn('relative h-full shrink-0 border-r border-shell-border bg-shell-context', !isDragging && 'transition-[width] duration-200 ease-in-out motion-reduce:transition-none', collapsed && 'w-0', className), style: { width: collapsed ? 0 : width }, children: [collapsible && _jsx(ContextRailToggle, { railId: railId }), _jsxs("div", { className: "flex h-full min-w-0 flex-col overflow-hidden", "aria-hidden": collapsed, inert: collapsed || undefined, children: [showHeader && (_jsxs("div", { className: "shrink-0 border-b border-shell-border px-4 py-3", children: [_jsx("h2", { className: "text-sm font-semibold text-foreground", children: module.label }), module.description && (_jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: module.description }))] })), _jsxs("div", { "data-meda-context-rail-scroll-area": "", className: cn('min-h-0 flex-1', scroll === 'auto' ? 'overflow-y-auto overflow-x-hidden' : 'overflow-hidden'), children: [items.length > 0 && (_jsx("nav", { "aria-label": `${module.label} navigation`, className: "flex flex-col gap-0.5 p-2", children: items.map((item) => {
|
|
119
|
+
const isActive = item.id === activeItemId;
|
|
120
|
+
const klass = cn('flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors', isActive
|
|
121
|
+
? 'bg-primary/10 text-primary'
|
|
122
|
+
: 'text-muted-foreground hover:bg-accent hover:text-foreground');
|
|
123
|
+
const IconComp = item.icon;
|
|
124
|
+
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 }))] }));
|
|
125
|
+
const linkProps = {
|
|
126
|
+
href: item.to,
|
|
127
|
+
'aria-current': isActive ? 'page' : undefined,
|
|
128
|
+
className: klass,
|
|
129
|
+
children: inner,
|
|
130
|
+
};
|
|
131
|
+
if (renderLink) {
|
|
132
|
+
return (_jsx(Fragment, { children: renderLink({
|
|
133
|
+
item,
|
|
134
|
+
isActive,
|
|
135
|
+
className: klass,
|
|
136
|
+
children: inner,
|
|
137
|
+
linkProps,
|
|
138
|
+
}) }, item.id));
|
|
139
|
+
}
|
|
140
|
+
return _jsx("a", { ...linkProps }, item.id);
|
|
141
|
+
}) })), module.render?.({ workspaceId: ctx.workspace.id, appId })] })] }), !collapsed && (_jsx(ResizeHandle, { currentWidth: width, onResize: handleResize, onCommit: handleCommit }))] }));
|
|
134
142
|
}
|
package/dist/shell/index.d.ts
CHANGED
|
@@ -27,5 +27,5 @@ export type { MedaShellProviderProps } from './shell-provider.js';
|
|
|
27
27
|
export { MedaShellProvider, useMedaShell, useShellSelection } from './shell-provider.js';
|
|
28
28
|
export { DefaultThemeProvider, ThemeToggle, useTheme } from './theme.js';
|
|
29
29
|
export { NextThemesAdapter } from './theme-next-themes.js';
|
|
30
|
-
export type { AppDefinition, AppShellAppTabsConfig, AppShellAuthBranding, AppShellAuthConfig, AppShellContextRailConfig, AppShellIconRailConfig, AppShellRightPanelConfig, AppShellVariant, AppShellWorkspaceConfig, AppTabRenderLinkArgs, CommandDefinition, ContextItem, ContextModule, MobileBottomNavItem, PanelMode, PanelView, ShellLinkRenderArgs, ShellMainLayout, ShellRenderContext, ShellViewport, ThemeAdapter, WorkspaceDefinition, WorkspaceMenuItem, } from './types.js';
|
|
30
|
+
export type { AppDefinition, AppShellAppTabsConfig, AppShellAuthBranding, AppShellAuthConfig, AppShellContextRailConfig, AppShellIconRailConfig, AppShellRightPanelConfig, AppShellVariant, AppShellWorkspaceConfig, AppTabRenderLinkArgs, CommandDefinition, ContextItem, ContextModule, ContextRailHeader, ContextRailScroll, MobileBottomNavItem, PanelMode, PanelView, ShellLinkRenderArgs, ShellMainLayout, ShellRenderContext, ShellViewport, ThemeAdapter, WorkspaceDefinition, WorkspaceMenuItem, } from './types.js';
|
|
31
31
|
export { useShellViewport } from './use-shell-viewport.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
2
|
import type { IconRailItem, IconRailProps } from '../icon-rail.js';
|
|
3
|
-
import type { ContextModule, PanelView, WorkspaceMenuItem } from '../types.js';
|
|
3
|
+
import type { ContextModule, ContextRailHeader, ContextRailScroll, PanelView, WorkspaceMenuItem } from '../types.js';
|
|
4
4
|
export interface MobileDrawersProps {
|
|
5
5
|
/** Menu drawer source (icon-rail items). */
|
|
6
6
|
menuItems?: IconRailItem[];
|
|
@@ -16,6 +16,10 @@ export interface MobileDrawersProps {
|
|
|
16
16
|
module?: ContextModule;
|
|
17
17
|
/** App id used when rendering module custom content. */
|
|
18
18
|
moduleAppId?: string;
|
|
19
|
+
/** Header behavior mirrored from the desktop ContextRail. */
|
|
20
|
+
moduleHeader?: ContextRailHeader;
|
|
21
|
+
/** Scroll behavior mirrored from the desktop ContextRail. */
|
|
22
|
+
moduleScroll?: ContextRailScroll;
|
|
19
23
|
/** Panels drawer source. */
|
|
20
24
|
panelViews?: PanelView[];
|
|
21
25
|
/**
|
|
@@ -33,4 +37,4 @@ export interface MobileDrawersProps {
|
|
|
33
37
|
* any custom-content drawers. Mount once near the AppShell root; drawers
|
|
34
38
|
* open/close via `ctx.mobileDrawer.open` provider state.
|
|
35
39
|
*/
|
|
36
|
-
export declare function MobileDrawers({ menuItems, menuActiveId, menuRenderLink, workspaceMenuItems, workspaceMenuFooter, module, moduleAppId, panelViews, defaultView, customContent, }: MobileDrawersProps): import("react/jsx-runtime").JSX.Element;
|
|
40
|
+
export declare function MobileDrawers({ menuItems, menuActiveId, menuRenderLink, workspaceMenuItems, workspaceMenuFooter, module, moduleAppId, moduleHeader, moduleScroll, panelViews, defaultView, customContent, }: MobileDrawersProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -11,7 +11,7 @@ import { useTheme } from '../theme.js';
|
|
|
11
11
|
* any custom-content drawers. Mount once near the AppShell root; drawers
|
|
12
12
|
* open/close via `ctx.mobileDrawer.open` provider state.
|
|
13
13
|
*/
|
|
14
|
-
export function MobileDrawers({ menuItems = [], menuActiveId, menuRenderLink, workspaceMenuItems, workspaceMenuFooter, module, moduleAppId, panelViews = [], defaultView, customContent = {}, }) {
|
|
14
|
+
export function MobileDrawers({ menuItems = [], menuActiveId, menuRenderLink, workspaceMenuItems, workspaceMenuFooter, module, moduleAppId, moduleHeader, moduleScroll, panelViews = [], defaultView, customContent = {}, }) {
|
|
15
15
|
const ctx = useMedaShell();
|
|
16
16
|
const open = ctx.mobileDrawer.open;
|
|
17
17
|
const setOpen = ctx.mobileDrawer.setOpen;
|
|
@@ -24,7 +24,7 @@ export function MobileDrawers({ menuItems = [], menuActiveId, menuRenderLink, wo
|
|
|
24
24
|
workspaceId: ctx.workspace.id,
|
|
25
25
|
appId: moduleAppId ?? ctx.activeAppId,
|
|
26
26
|
};
|
|
27
|
-
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 }), _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)))] }));
|
|
27
|
+
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
28
|
}
|
|
29
29
|
// ---------------------------------------------------------------------------
|
|
30
30
|
// Internal sub-drawers
|
|
@@ -110,14 +110,17 @@ function MobileThemeMenuItem({ onClose }) {
|
|
|
110
110
|
onClose();
|
|
111
111
|
}, children: [_jsx(Icon, { size: 18, "aria-hidden": "true" }), _jsx("span", { children: THEME_LABEL[theme] })] }));
|
|
112
112
|
}
|
|
113
|
-
function ModuleDrawer({ open, onClose, module, renderCtx, }) {
|
|
113
|
+
function ModuleDrawer({ open, onClose, module, renderCtx, header = 'auto', scroll = 'auto', }) {
|
|
114
114
|
const items = module?.items ?? [];
|
|
115
115
|
if (!module || (items.length === 0 && !module.render))
|
|
116
116
|
return null;
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
const hasRender = typeof module.render === 'function';
|
|
118
|
+
const showHeader = header === 'visible' || (header === 'auto' && items.length > 0 && !hasRender);
|
|
119
|
+
const title = (_jsxs(_Fragment, { children: [_jsx(DrawerTitle, { className: showHeader ? undefined : 'sr-only', children: module.label }), module.description && (_jsx(DrawerDescription, { className: showHeader ? 'text-muted-foreground text-xs' : 'sr-only', children: module.description }))] }));
|
|
120
|
+
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) => {
|
|
121
|
+
const Icon = item.icon;
|
|
122
|
+
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));
|
|
123
|
+
}) })), module.render?.(renderCtx)] })] }) }));
|
|
121
124
|
}
|
|
122
125
|
function PanelsDrawer({ open, onClose, panelViews, defaultView, renderCtx, }) {
|
|
123
126
|
const ctx = useMedaShell();
|
|
@@ -107,63 +107,79 @@ export function MedaShellProvider(props) {
|
|
|
107
107
|
storage,
|
|
108
108
|
});
|
|
109
109
|
const isMobile = useShellViewport() === 'mobile';
|
|
110
|
+
const setPanelMode = useCallback((mode) => setLayoutState((prev) => ({
|
|
111
|
+
...prev,
|
|
112
|
+
rightPanel: { ...prev.rightPanel, mode },
|
|
113
|
+
})), [setLayoutState]);
|
|
114
|
+
const setPanelActiveView = useCallback((activeView) => setLayoutState((prev) => ({
|
|
115
|
+
...prev,
|
|
116
|
+
rightPanel: { ...prev.rightPanel, activeView },
|
|
117
|
+
})), [setLayoutState]);
|
|
118
|
+
const setPanelWidth = useCallback((width) => setLayoutState((prev) => ({
|
|
119
|
+
...prev,
|
|
120
|
+
rightPanel: { ...prev.rightPanel, width },
|
|
121
|
+
})), [setLayoutState]);
|
|
122
|
+
const openPanel = useCallback(() => {
|
|
123
|
+
if (isMobile)
|
|
124
|
+
setMobileDrawerOpen('panels-drawer');
|
|
125
|
+
setLayoutState((prev) => ({
|
|
126
|
+
...prev,
|
|
127
|
+
rightPanel: {
|
|
128
|
+
...prev.rightPanel,
|
|
129
|
+
mode: prev.rightPanel.mode === 'closed' ? 'panel' : prev.rightPanel.mode,
|
|
130
|
+
},
|
|
131
|
+
}));
|
|
132
|
+
}, [isMobile, setLayoutState]);
|
|
133
|
+
const closePanel = useCallback(() => {
|
|
134
|
+
if (isMobile)
|
|
135
|
+
setMobileDrawerOpen((open) => (open === 'panels-drawer' ? null : open));
|
|
136
|
+
setLayoutState((prev) => ({
|
|
137
|
+
...prev,
|
|
138
|
+
rightPanel: { ...prev.rightPanel, mode: 'closed' },
|
|
139
|
+
}));
|
|
140
|
+
}, [isMobile, setLayoutState]);
|
|
141
|
+
const togglePanel = useCallback(() => {
|
|
142
|
+
if (isMobile) {
|
|
143
|
+
setMobileDrawerOpen((open) => (open === 'panels-drawer' ? null : 'panels-drawer'));
|
|
144
|
+
}
|
|
145
|
+
setLayoutState((prev) => ({
|
|
146
|
+
...prev,
|
|
147
|
+
rightPanel: {
|
|
148
|
+
...prev.rightPanel,
|
|
149
|
+
mode: prev.rightPanel.mode === 'closed' ? 'panel' : 'closed',
|
|
150
|
+
},
|
|
151
|
+
}));
|
|
152
|
+
}, [isMobile, setLayoutState]);
|
|
153
|
+
const focusPanel = useCallback((viewId) => setLayoutState((prev) => {
|
|
154
|
+
const nextMode = prev.rightPanel.mode === 'closed' ? 'panel' : prev.rightPanel.mode;
|
|
155
|
+
return {
|
|
156
|
+
...prev,
|
|
157
|
+
rightPanel: { ...prev.rightPanel, mode: nextMode, activeView: viewId },
|
|
158
|
+
};
|
|
159
|
+
}), [setLayoutState]);
|
|
110
160
|
const panel = useMemo(() => ({
|
|
111
161
|
mode: layoutState.rightPanel.mode,
|
|
112
162
|
activeView: layoutState.rightPanel.activeView,
|
|
113
163
|
width: layoutState.rightPanel.width,
|
|
114
|
-
setMode:
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
mode: prev.rightPanel.mode === 'closed' ? 'panel' : prev.rightPanel.mode,
|
|
134
|
-
},
|
|
135
|
-
}));
|
|
136
|
-
},
|
|
137
|
-
close: () => {
|
|
138
|
-
if (isMobile)
|
|
139
|
-
setMobileDrawerOpen((open) => (open === 'panels-drawer' ? null : open));
|
|
140
|
-
setLayoutState((prev) => ({
|
|
141
|
-
...prev,
|
|
142
|
-
rightPanel: { ...prev.rightPanel, mode: 'closed' },
|
|
143
|
-
}));
|
|
144
|
-
},
|
|
145
|
-
toggle: () => {
|
|
146
|
-
if (isMobile) {
|
|
147
|
-
setMobileDrawerOpen((open) => (open === 'panels-drawer' ? null : 'panels-drawer'));
|
|
148
|
-
}
|
|
149
|
-
setLayoutState((prev) => ({
|
|
150
|
-
...prev,
|
|
151
|
-
rightPanel: {
|
|
152
|
-
...prev.rightPanel,
|
|
153
|
-
mode: prev.rightPanel.mode === 'closed' ? 'panel' : 'closed',
|
|
154
|
-
},
|
|
155
|
-
}));
|
|
156
|
-
},
|
|
157
|
-
// focus(viewId) — opens panel + switches to view in one call.
|
|
158
|
-
// Only flips closed → panel; preserves expanded / fullscreen modes.
|
|
159
|
-
focus: (viewId) => setLayoutState((prev) => {
|
|
160
|
-
const nextMode = prev.rightPanel.mode === 'closed' ? 'panel' : prev.rightPanel.mode;
|
|
161
|
-
return {
|
|
162
|
-
...prev,
|
|
163
|
-
rightPanel: { ...prev.rightPanel, mode: nextMode, activeView: viewId },
|
|
164
|
-
};
|
|
165
|
-
}),
|
|
166
|
-
}), [isMobile, layoutState, setLayoutState]);
|
|
164
|
+
setMode: setPanelMode,
|
|
165
|
+
setActiveView: setPanelActiveView,
|
|
166
|
+
setWidth: setPanelWidth,
|
|
167
|
+
open: openPanel,
|
|
168
|
+
close: closePanel,
|
|
169
|
+
toggle: togglePanel,
|
|
170
|
+
focus: focusPanel,
|
|
171
|
+
}), [
|
|
172
|
+
layoutState.rightPanel.mode,
|
|
173
|
+
layoutState.rightPanel.activeView,
|
|
174
|
+
layoutState.rightPanel.width,
|
|
175
|
+
setPanelMode,
|
|
176
|
+
setPanelActiveView,
|
|
177
|
+
setPanelWidth,
|
|
178
|
+
openPanel,
|
|
179
|
+
closePanel,
|
|
180
|
+
togglePanel,
|
|
181
|
+
focusPanel,
|
|
182
|
+
]);
|
|
167
183
|
const contextRail = useMemo(() => ({
|
|
168
184
|
width: layoutState.contextRail.width,
|
|
169
185
|
collapsed: layoutState.contextRail.collapsed,
|
package/dist/shell/types.d.ts
CHANGED
|
@@ -50,6 +50,8 @@ export interface PanelView {
|
|
|
50
50
|
export type PanelMode = 'closed' | 'panel' | 'expanded' | 'fullscreen';
|
|
51
51
|
export type ShellMainLayout = 'workspace' | 'centered' | 'fullbleed';
|
|
52
52
|
export type ShellViewport = 'mobile' | 'tablet' | 'desktop' | 'wide' | 'ultrawide';
|
|
53
|
+
export type ContextRailHeader = 'auto' | 'visible' | 'hidden';
|
|
54
|
+
export type ContextRailScroll = 'auto' | 'none';
|
|
53
55
|
export interface MobileBottomNavItem {
|
|
54
56
|
id: string;
|
|
55
57
|
label: string | (() => string);
|
|
@@ -103,6 +105,8 @@ export interface AppShellContextRailConfig {
|
|
|
103
105
|
appId: string;
|
|
104
106
|
module: ContextModule;
|
|
105
107
|
activeItemId?: string;
|
|
108
|
+
header?: ContextRailHeader;
|
|
109
|
+
scroll?: ContextRailScroll;
|
|
106
110
|
}
|
|
107
111
|
/** RightPanel configuration for `<AppShell variant="workspace">`. */
|
|
108
112
|
export interface AppShellRightPanelConfig {
|
|
@@ -7,11 +7,18 @@ const BREAKPOINTS = {
|
|
|
7
7
|
wide: '(min-width: 1280px) and (max-width: 1535px)',
|
|
8
8
|
ultrawide: '(min-width: 1536px)',
|
|
9
9
|
};
|
|
10
|
-
function
|
|
10
|
+
function getMatchMedia() {
|
|
11
11
|
if (typeof window === 'undefined')
|
|
12
|
+
return null;
|
|
13
|
+
if (typeof window.matchMedia !== 'function')
|
|
14
|
+
return null;
|
|
15
|
+
return window.matchMedia.bind(window);
|
|
16
|
+
}
|
|
17
|
+
function detectViewport(matchMedia = getMatchMedia()) {
|
|
18
|
+
if (!matchMedia)
|
|
12
19
|
return 'desktop';
|
|
13
20
|
for (const [band, query] of Object.entries(BREAKPOINTS)) {
|
|
14
|
-
if (
|
|
21
|
+
if (matchMedia(query).matches)
|
|
15
22
|
return band;
|
|
16
23
|
}
|
|
17
24
|
return 'desktop';
|
|
@@ -21,9 +28,12 @@ export function useShellViewport() {
|
|
|
21
28
|
// client's first paint — actual band resolves in the post-mount effect.
|
|
22
29
|
const [viewport, setViewport] = useState('desktop');
|
|
23
30
|
useEffect(() => {
|
|
24
|
-
|
|
31
|
+
const matchMedia = getMatchMedia();
|
|
32
|
+
setViewport(detectViewport(matchMedia));
|
|
33
|
+
if (!matchMedia)
|
|
34
|
+
return;
|
|
25
35
|
const cleanups = Object.entries(BREAKPOINTS).map(([band, query]) => {
|
|
26
|
-
const mql =
|
|
36
|
+
const mql = matchMedia(query);
|
|
27
37
|
const onChange = () => {
|
|
28
38
|
if (mql.matches)
|
|
29
39
|
setViewport(band);
|