@medalsocial/meda 1.7.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 +2 -1
- 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,8 @@ 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
|
+
|
|
112
114
|
## Foundation primitives
|
|
113
115
|
|
|
114
116
|
Meda includes small foundation primitives for repeated loading, empty, and filtering surfaces:
|
|
@@ -126,7 +128,6 @@ npx shadcn add https://meda.medalsocial.com/r/meda-skeleton.json
|
|
|
126
128
|
npx shadcn add https://meda.medalsocial.com/r/meda-empty-state.json
|
|
127
129
|
npx shadcn add https://meda.medalsocial.com/r/meda-filter-rail.json
|
|
128
130
|
```
|
|
129
|
-
|
|
130
131
|
For app-scoped brand tokens:
|
|
131
132
|
|
|
132
133
|
```ts
|
|
@@ -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);
|