@medalsocial/meda 1.1.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/dist/components/ui/checkbox.d.ts +8 -0
  2. package/dist/components/ui/checkbox.js +9 -0
  3. package/dist/components/ui/collapsible.d.ts +5 -0
  4. package/dist/components/ui/collapsible.js +13 -0
  5. package/dist/kanban/index.d.ts +5 -0
  6. package/dist/kanban/index.js +4 -0
  7. package/dist/kanban/kanban-board.d.ts +8 -0
  8. package/dist/kanban/kanban-board.js +197 -0
  9. package/dist/kanban/kanban-card-wrapper.d.ts +16 -0
  10. package/dist/kanban/kanban-card-wrapper.js +20 -0
  11. package/dist/kanban/kanban-collision.d.ts +16 -0
  12. package/dist/kanban/kanban-collision.js +18 -0
  13. package/dist/kanban/kanban-column.d.ts +19 -0
  14. package/dist/kanban/kanban-column.js +19 -0
  15. package/dist/kanban/kanban-drop-handler.d.ts +16 -0
  16. package/dist/kanban/kanban-drop-handler.js +84 -0
  17. package/dist/kanban/types.d.ts +47 -0
  18. package/dist/kanban/types.js +7 -0
  19. package/dist/list/index.d.ts +2 -0
  20. package/dist/list/index.js +1 -0
  21. package/dist/list/list-row.d.ts +52 -0
  22. package/dist/list/list-row.js +37 -0
  23. package/dist/shell/app-shell-workspace.js +11 -6
  24. package/dist/shell/command-palette.js +2 -1
  25. package/dist/shell/context-rail.d.ts +1 -1
  26. package/dist/shell/context-rail.js +5 -4
  27. package/dist/shell/drag-mode-banner.d.ts +31 -0
  28. package/dist/shell/drag-mode-banner.js +19 -0
  29. package/dist/shell/index.d.ts +8 -0
  30. package/dist/shell/index.js +5 -0
  31. package/dist/shell/internal/mobile-drawers.d.ts +8 -2
  32. package/dist/shell/internal/mobile-drawers.js +40 -12
  33. package/dist/shell/panel-views-provider.d.ts +15 -0
  34. package/dist/shell/panel-views-provider.js +56 -0
  35. package/dist/shell/rail-drop-slot.d.ts +41 -0
  36. package/dist/shell/rail-drop-slot.js +49 -0
  37. package/dist/shell/rail-drop-zones.d.ts +28 -0
  38. package/dist/shell/rail-drop-zones.js +17 -0
  39. package/dist/shell/right-panel.js +21 -8
  40. package/dist/shell/shell-provider.d.ts +14 -1
  41. package/dist/shell/shell-provider.js +64 -2
  42. package/dist/shell/types.d.ts +5 -1
  43. package/dist/timeline/index.d.ts +3 -0
  44. package/dist/timeline/index.js +2 -0
  45. package/dist/timeline/lane-timeline-types.d.ts +61 -0
  46. package/dist/timeline/lane-timeline-types.js +6 -0
  47. package/dist/timeline/lane-timeline.d.ts +2 -0
  48. package/dist/timeline/lane-timeline.js +85 -0
  49. package/dist/timeline/lane.d.ts +10 -0
  50. package/dist/timeline/lane.js +39 -0
  51. package/dist/timeline/public.d.ts +1 -0
  52. package/dist/timeline/time-axis.d.ts +18 -0
  53. package/dist/timeline/time-axis.js +22 -0
  54. package/package.json +24 -1
@@ -0,0 +1,37 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Checkbox } from '../components/ui/checkbox.js';
4
+ import { cn } from '../lib/utils.js';
5
+ /**
6
+ * A Linear-style list row with checkbox, hover states, and click handling.
7
+ * Compact ~40-48px height for dense, scannable lists.
8
+ */
9
+ export function ListRow({ selected, selectionActive, onSelect, onClick, onMouseEnter, onMouseLeave, onFocus, onBlur, focused, children, className, }) {
10
+ const isInteractive = !!onClick;
11
+ const interactiveProps = isInteractive
12
+ ? {
13
+ role: 'button',
14
+ tabIndex: 0,
15
+ onClick,
16
+ onKeyDown: (e) => {
17
+ if (e.key === 'Enter' || e.key === ' ') {
18
+ e.preventDefault();
19
+ onClick?.();
20
+ }
21
+ },
22
+ }
23
+ : {};
24
+ return (_jsxs("div", { ...interactiveProps, onMouseEnter: onMouseEnter, onMouseLeave: onMouseLeave, onFocus: onFocus, onBlur: onBlur, className: cn('group relative flex items-center gap-3 border-border border-b px-3 py-2.5', 'transition-all duration-150', isInteractive && 'cursor-pointer hover:bg-accent/50', isInteractive &&
25
+ "before:absolute before:inset-y-0 before:left-0 before:w-[2px] before:scale-y-0 before:bg-primary before:transition-transform before:duration-150 before:content-['']", isInteractive && 'hover:before:scale-y-100', isInteractive && 'focus-within:bg-accent/30 focus-within:before:scale-y-100', selected && isInteractive && 'before:!scale-y-100 bg-accent', selected && !isInteractive && 'bg-accent', focused &&
26
+ isInteractive &&
27
+ 'before:!scale-y-100 bg-accent/30 ring-1 ring-primary/50 ring-inset', focused && !isInteractive && 'bg-accent/30 ring-1 ring-primary/50 ring-inset', className), children: [onSelect && (_jsx("div", { className: "w-6 flex-shrink-0", onClick: (e) => e.stopPropagation(), onKeyDown: (e) => e.stopPropagation(), children: _jsx(Checkbox, { checked: selected, onClick: (e) => {
28
+ e.preventDefault();
29
+ onSelect(!selected, e.shiftKey);
30
+ }, className: cn('transition-opacity', selectionActive || selected
31
+ ? 'opacity-100'
32
+ : 'opacity-0 group-hover:opacity-100 data-[state=checked]:opacity-100') }) })), children] }));
33
+ }
34
+ export function ListCell({ width = 'flex-1', shrink = true, align = 'left', children, className, }) {
35
+ return (_jsx("div", { className: cn(width, shrink === false && 'flex-shrink-0', align === 'center' && 'text-center', align === 'right' && 'text-right', 'min-w-0', // Allow truncation
36
+ className), children: children }));
37
+ }
@@ -6,18 +6,22 @@ import { IconRail } from './icon-rail.js';
6
6
  import { MobileBottomNav } from './internal/mobile-bottom-nav.js';
7
7
  import { MobileDrawers } from './internal/mobile-drawers.js';
8
8
  import { MobileHeader } from './internal/mobile-header.js';
9
+ import { useResolvedPanelViews } from './panel-views-provider.js';
9
10
  import { RightPanel } from './right-panel.js';
10
11
  import { ShellHeader } from './shell-header.js';
11
12
  import { ShellMain } from './shell-main.js';
12
13
  import { useShellViewport } from './use-shell-viewport.js';
14
+ const EMPTY_PANEL_VIEWS = [];
13
15
  export function AppShellWorkspace({ iconRail, contextRail, rightPanel, globalActions, children, }) {
14
16
  const viewport = useShellViewport();
15
17
  const isMobile = viewport === 'mobile';
18
+ const staticPanelViews = rightPanel?.panelViews ?? EMPTY_PANEL_VIEWS;
19
+ const resolvedRightPanel = useResolvedPanelViews(staticPanelViews, rightPanel?.defaultView);
16
20
  // Derive the bottom-nav items from the variant config so each button maps
17
21
  // to a drawer that actually has content. Without this filter, partial
18
22
  // configs (e.g. iconRail only) would show Module/Panels/AI buttons that
19
23
  // dispatch into the void.
20
- const navItems = buildMobileNavItems(iconRail, contextRail, rightPanel);
24
+ const navItems = buildMobileNavItems(iconRail, contextRail, resolvedRightPanel.panelViews);
21
25
  const hasDrawerContent = navItems.length > 0;
22
26
  // Mobile menu drawer needs both main and utility items — desktop IconRail
23
27
  // shows both, so dropping utilityItems here would orphan items like Help/
@@ -33,21 +37,22 @@ export function AppShellWorkspace({ iconRail, contextRail, rightPanel, globalAct
33
37
  // rendered without an explicit-height ancestor (tests, direct imports). The
34
38
  // <AppShell> wrapper already enforces h-screen for the workspace variant, so
35
39
  // nested viewport-height divs collapse cleanly — no double-scroll.
36
- return (_jsxs("div", { className: "flex h-screen flex-col", children: [isMobile ? (_jsx(MobileHeader, { globalActions: globalActions })) : (_jsx(ShellHeader, { globalActions: globalActions })), _jsxs("div", { className: "relative flex flex-1 overflow-hidden", children: [!isMobile && iconRail && (_jsx(IconRail, { mainItems: iconRail.mainItems, utilityItems: iconRail.utilityItems, footer: iconRail.footer, activeId: iconRail.activeId })), !isMobile && contextRail && (_jsx(ContextRail, { appId: contextRail.appId, module: contextRail.module, activeItemId: contextRail.activeItemId })), _jsx(ShellMain, { layout: "workspace", children: children }), !isMobile && rightPanel && (_jsx(RightPanel, { panelViews: rightPanel.panelViews, defaultView: rightPanel.defaultView }))] }), isMobile && hasDrawerContent && _jsx(MobileBottomNav, { items: navItems }), isMobile && hasDrawerContent && (_jsx(MobileDrawers, { menuItems: mobileMenuItems, module: contextRail?.module, panelViews: rightPanel?.panelViews ?? [], defaultView: rightPanel?.defaultView }))] }));
40
+ return (_jsxs("div", { className: "flex h-screen flex-col", children: [isMobile ? (_jsx(MobileHeader, { globalActions: globalActions })) : (_jsx(ShellHeader, { globalActions: globalActions })), _jsxs("div", { className: "relative flex flex-1 overflow-hidden", children: [!isMobile && iconRail && (_jsx(IconRail, { mainItems: iconRail.mainItems, utilityItems: iconRail.utilityItems, footer: iconRail.footer, activeId: iconRail.activeId, renderLink: iconRail.renderLink })), !isMobile && contextRail && (_jsx(ContextRail, { appId: contextRail.appId, module: contextRail.module, activeItemId: contextRail.activeItemId })), _jsx(ShellMain, { layout: "workspace", children: children }), !isMobile && resolvedRightPanel.panelViews.length > 0 && (_jsx(RightPanel, { panelViews: staticPanelViews, defaultView: rightPanel?.defaultView }))] }), isMobile && hasDrawerContent && _jsx(MobileBottomNav, { items: navItems }), isMobile && hasDrawerContent && (_jsx(MobileDrawers, { menuItems: mobileMenuItems, menuActiveId: iconRail?.activeId, menuRenderLink: iconRail?.renderLink, module: contextRail?.module, moduleAppId: contextRail?.appId, panelViews: resolvedRightPanel.panelViews, defaultView: resolvedRightPanel.defaultView }))] }));
37
41
  }
38
- function buildMobileNavItems(iconRail, contextRail, rightPanel) {
42
+ function buildMobileNavItems(iconRail, contextRail, panelViews) {
39
43
  const items = [];
40
44
  if (iconRail) {
41
45
  items.push({ id: 'menu', label: 'Menu', icon: Menu, opens: 'menu-drawer' });
42
46
  }
43
- if (contextRail?.module) {
47
+ if (contextRail?.module &&
48
+ ((contextRail.module.items ?? []).length > 0 || Boolean(contextRail.module.render))) {
44
49
  items.push({ id: 'module', label: 'Module', icon: LayoutGrid, opens: 'module-drawer' });
45
50
  }
46
51
  // Panels button only when there's an actual view to render — empty
47
52
  // panelViews would open an empty drawer (dead-end tap).
48
- if (rightPanel && rightPanel.panelViews.length > 0) {
53
+ if (panelViews.length > 0) {
49
54
  items.push({ id: 'panels', label: 'Panels', icon: PanelTop, opens: 'panels-drawer' });
50
- if (rightPanel.panelViews.some((v) => v.id === 'ai')) {
55
+ if (panelViews.some((v) => v.id === 'ai')) {
51
56
  items.push({ id: 'ai', label: 'AI', icon: Sparkles, opens: 'ai-drawer' });
52
57
  }
53
58
  }
@@ -109,7 +109,8 @@ export function CommandPalette({ children }) {
109
109
  // -------------------------------------------------------------------------
110
110
  return (_jsxs(CommandRegistryContext.Provider, { value: registry, children: [children, _jsx(CommandDialog, { open: open, onOpenChange: setOpen, children: _jsxs(Command, { children: [_jsx(CommandInput, { placeholder: "Search commands\u2026" }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: "No results." }), grouped.map((group) => (_jsx(CommandGroup, { heading: group.label, children: group.commands.map((cmd) => {
111
111
  const Icon = cmd.icon;
112
- return (_jsxs(CommandItem, { value: `${group.label} ${cmd.label}`, onSelect: () => handleSelect(cmd), children: [Icon && _jsx(Icon, { size: 16, "aria-hidden": "true" }), _jsx("span", { children: cmd.label }), cmd.shortcut && _jsx(CommandShortcut, { children: cmd.shortcut })] }, cmd.id));
112
+ const shortcut = cmd.shortcut ?? cmd.hotkey;
113
+ return (_jsxs(CommandItem, { value: `${group.label} ${cmd.label}`, onSelect: () => handleSelect(cmd), children: [Icon && _jsx(Icon, { size: 16, "aria-hidden": "true" }), _jsx("span", { children: cmd.label }), shortcut && _jsx(CommandShortcut, { children: shortcut })] }, cmd.id));
113
114
  }) }, group.id)))] })] }) })] }));
114
115
  }
115
116
  // ---------------------------------------------------------------------------
@@ -14,4 +14,4 @@ export interface ContextRailProps {
14
14
  renderLink?: (args: ShellLinkRenderArgs) => ReactNode;
15
15
  className?: string;
16
16
  }
17
- export declare function ContextRail({ appId: _appId, module, hidden, collapsible, activeItemId, renderLink, className, }: ContextRailProps): import("react/jsx-runtime").JSX.Element | null;
17
+ export declare function ContextRail({ appId, module, hidden, collapsible, activeItemId, renderLink, className, }: ContextRailProps): import("react/jsx-runtime").JSX.Element | null;
@@ -75,7 +75,7 @@ function ContextRailToggle({ railId }) {
75
75
  // ---------------------------------------------------------------------------
76
76
  // ContextRail
77
77
  // ---------------------------------------------------------------------------
78
- export function ContextRail({ appId: _appId, module, hidden = false, collapsible = true, activeItemId, renderLink, className, }) {
78
+ export function ContextRail({ appId, module, hidden = false, collapsible = true, activeItemId, renderLink, className, }) {
79
79
  const band = useShellViewport();
80
80
  const ctx = useMedaShell();
81
81
  // collapsible={false} means the rail must always render expanded — even if
@@ -91,12 +91,13 @@ export function ContextRail({ appId: _appId, module, hidden = false, collapsible
91
91
  // is called on pointerUp to persist via useShellLayoutState.
92
92
  const [displayWidth, setDisplayWidth] = useState(null);
93
93
  const width = displayWidth ?? ctx.contextRail.width;
94
+ const items = module?.items ?? [];
94
95
  if (band === 'mobile')
95
96
  return null;
96
97
  if (hidden) {
97
98
  return _jsx("div", { "aria-hidden": "true", className: "hidden", "data-testid": "context-rail-hidden" });
98
99
  }
99
- if (!module || module.items.length === 0) {
100
+ if (!module || (items.length === 0 && !module.render)) {
100
101
  return _jsx("div", { "aria-hidden": "true", className: "hidden", "data-testid": "context-rail-empty" });
101
102
  }
102
103
  const handleResize = (w) => {
@@ -112,7 +113,7 @@ export function ContextRail({ appId: _appId, module, hidden = false, collapsible
112
113
  // transition then so the rail snaps to the cursor instead of lagging
113
114
  // behind it.
114
115
  const isDragging = displayWidth !== null;
115
- return (_jsxs("aside", { id: railId, "data-testid": "context-rail", "aria-label": module.label, className: cn('relative h-full shrink-0 border-r border-shell-border bg-shell-context', !isDragging && 'transition-[width] duration-200 ease-in-out motion-reduce:transition-none', collapsed && 'w-0', className), style: { width: collapsed ? 0 : width }, children: [collapsible && _jsx(ContextRailToggle, { railId: railId }), _jsxs("div", { className: "h-full overflow-hidden", "aria-hidden": collapsed, inert: collapsed || undefined, children: [_jsxs("div", { className: "border-b border-shell-border px-4 py-3", children: [_jsx("h2", { className: "text-sm font-semibold text-foreground", children: module.label }), module.description && (_jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: module.description }))] }), _jsx("nav", { "aria-label": `${module.label} navigation`, className: "flex flex-col gap-0.5 p-2", children: module.items.map((item) => {
116
+ return (_jsxs("aside", { id: railId, "data-testid": "context-rail", "aria-label": module.label, className: cn('relative h-full shrink-0 border-r border-shell-border bg-shell-context', !isDragging && 'transition-[width] duration-200 ease-in-out motion-reduce:transition-none', collapsed && 'w-0', className), style: { width: collapsed ? 0 : width }, children: [collapsible && _jsx(ContextRailToggle, { railId: railId }), _jsxs("div", { className: "h-full overflow-hidden", "aria-hidden": collapsed, inert: collapsed || undefined, children: [_jsxs("div", { className: "border-b border-shell-border px-4 py-3", children: [_jsx("h2", { className: "text-sm font-semibold text-foreground", children: module.label }), module.description && (_jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: module.description }))] }), items.length > 0 && (_jsx("nav", { "aria-label": `${module.label} navigation`, className: "flex flex-col gap-0.5 p-2", children: items.map((item) => {
116
117
  const isActive = item.id === activeItemId;
117
118
  const klass = cn('flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors', isActive
118
119
  ? 'bg-primary/10 text-primary'
@@ -123,5 +124,5 @@ export function ContextRail({ appId: _appId, module, hidden = false, collapsible
123
124
  return renderLink({ item, isActive, className: klass, children: inner });
124
125
  }
125
126
  return (_jsx("a", { href: item.to, "aria-current": isActive ? 'page' : undefined, className: klass, children: inner }, item.id));
126
- }) })] }), !collapsed && (_jsx(ResizeHandle, { currentWidth: width, onResize: handleResize, onCommit: handleCommit }))] }));
127
+ }) })), module.render?.({ workspaceId: ctx.workspace.id, appId })] }), !collapsed && (_jsx(ResizeHandle, { currentWidth: width, onResize: handleResize, onCommit: handleCommit }))] }));
127
128
  }
@@ -0,0 +1,31 @@
1
+ import { type ReactNode } from 'react';
2
+ /**
3
+ * Props for DragModeBanner.
4
+ *
5
+ * **Important:** This component uses `useDndMonitor` internally and must be
6
+ * rendered inside the same `<DndContext>` tree it is monitoring.
7
+ *
8
+ * **Known limitation:** The ESC chip is decorative only. Keyboard-initiated
9
+ * drags cancel naturally via dnd-kit's KeyboardSensor; pointer-initiated drags
10
+ * require the user to release outside any drop zone — a manual ESC handler for
11
+ * pointer drags is not yet implemented.
12
+ */
13
+ export interface DragModeBannerProps {
14
+ /** Banner message shown while a drag is active. */
15
+ message: ReactNode;
16
+ /**
17
+ * When provided, render an ESC chip indicating the user can press Escape to
18
+ * cancel. See known limitation note above.
19
+ */
20
+ cancelKey?: 'ESC';
21
+ /**
22
+ * Optional predicate; when supplied only activates the banner for matching
23
+ * drag ids. Receives the `active.id` of the current drag.
24
+ */
25
+ isActive?: (activeId: string | number) => boolean;
26
+ className?: string;
27
+ }
28
+ export declare function DragModeBanner({ message, cancelKey, isActive, className }: DragModeBannerProps): import("react/jsx-runtime").JSX.Element | null;
29
+ export declare namespace DragModeBanner {
30
+ var displayName: string;
31
+ }
@@ -0,0 +1,19 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useDndMonitor } from '@dnd-kit/core';
4
+ import { useState } from 'react';
5
+ import { cn } from '../lib/utils.js';
6
+ export function DragModeBanner({ message, cancelKey, isActive, className }) {
7
+ const [activeId, setActiveId] = useState(null);
8
+ useDndMonitor({
9
+ onDragStart: (e) => setActiveId(e.active.id),
10
+ onDragEnd: () => setActiveId(null),
11
+ onDragCancel: () => setActiveId(null),
12
+ });
13
+ if (activeId == null)
14
+ return null;
15
+ if (isActive && !isActive(activeId))
16
+ return null;
17
+ return (_jsxs("div", { role: "status", "aria-live": "polite", className: cn('pointer-events-none flex items-center justify-center gap-3 px-4 py-2 text-sm text-muted-foreground', className), children: [_jsx("span", { children: message }), cancelKey === 'ESC' && (_jsx("kbd", { className: "rounded border border-border bg-muted px-1.5 py-0.5 font-medium font-mono text-xs", children: "ESC" }))] }));
18
+ }
19
+ DragModeBanner.displayName = 'DragModeBanner';
@@ -2,11 +2,19 @@ export { AppShell, AppShellBody } from './app-shell.js';
2
2
  export type { CommandGroupDefinition } from './command-palette.js';
3
3
  export { CommandPalette, useCommandGroup, useCommands } from './command-palette.js';
4
4
  export { ContextRail } from './context-rail.js';
5
+ export type { DragModeBannerProps } from './drag-mode-banner.js';
6
+ export { DragModeBanner } from './drag-mode-banner.js';
5
7
  export * as Extras from './extras/index.js';
6
8
  export { IconRail, RailDivider } from './icon-rail.js';
7
9
  export type { ShellStorageAdapter } from './layout-state.js';
8
10
  export { createLocalStorageAdapter } from './layout-state.js';
9
11
  export { motion } from './motion.js';
12
+ export type { PanelViewsProviderProps } from './panel-views-provider.js';
13
+ export { PanelViewsProvider } from './panel-views-provider.js';
14
+ export type { RailDropSlotProps, RailDropSlotState } from './rail-drop-slot.js';
15
+ export { RailDropSlot } from './rail-drop-slot.js';
16
+ export type { RailDropZonesProps } from './rail-drop-zones.js';
17
+ export { RailDropZones } from './rail-drop-zones.js';
10
18
  export { ResizableHandle, ResizableShell, ResizableShellPanel } from './resizable-shell.js';
11
19
  export { RightPanel } from './right-panel.js';
12
20
  export { AppTabs, PanelToggle, ShellHeader, WorkspaceSwitcher } from './shell-header.js';
@@ -8,6 +8,8 @@ export { AppShell, AppShellBody } from './app-shell.js';
8
8
  // Command palette
9
9
  export { CommandPalette, useCommandGroup, useCommands } from './command-palette.js';
10
10
  export { ContextRail } from './context-rail.js';
11
+ // Drag-and-drop utilities
12
+ export { DragModeBanner } from './drag-mode-banner.js';
11
13
  // Extras (legacy components ported during Phase 15 — opt-in for apps that need them)
12
14
  export * as Extras from './extras/index.js';
13
15
  // Rails + main + panel
@@ -16,7 +18,10 @@ export { IconRail, RailDivider } from './icon-rail.js';
16
18
  export { createLocalStorageAdapter } from './layout-state.js';
17
19
  // Hooks + tokens
18
20
  export { motion } from './motion.js';
21
+ export { PanelViewsProvider } from './panel-views-provider.js';
19
22
  // Resize primitives
23
+ export { RailDropSlot } from './rail-drop-slot.js';
24
+ export { RailDropZones } from './rail-drop-zones.js';
20
25
  export { ResizableHandle, ResizableShell, ResizableShellPanel } from './resizable-shell.js';
21
26
  export { RightPanel } from './right-panel.js';
22
27
  // Header (and its individual children for advanced composition)
@@ -1,11 +1,17 @@
1
1
  import { type ReactNode } from 'react';
2
- import type { IconRailItem } from '../icon-rail.js';
2
+ import type { IconRailItem, IconRailProps } from '../icon-rail.js';
3
3
  import type { ContextModule, PanelView } from '../types.js';
4
4
  export interface MobileDrawersProps {
5
5
  /** Menu drawer source (icon-rail items). */
6
6
  menuItems?: IconRailItem[];
7
+ /** Active menu item id, sourced from icon rail config. */
8
+ menuActiveId?: string;
9
+ /** Custom menu link renderer, sourced from icon rail config. */
10
+ menuRenderLink?: IconRailProps['renderLink'];
7
11
  /** Module drawer source (current app's context-rail module). */
8
12
  module?: ContextModule;
13
+ /** App id used when rendering module custom content. */
14
+ moduleAppId?: string;
9
15
  /** Panels drawer source. */
10
16
  panelViews?: PanelView[];
11
17
  /**
@@ -23,4 +29,4 @@ export interface MobileDrawersProps {
23
29
  * any custom-content drawers. Mount once near the AppShell root; drawers
24
30
  * open/close via `ctx.mobileDrawer.open` provider state.
25
31
  */
26
- export declare function MobileDrawers({ menuItems, module, panelViews, defaultView, customContent, }: MobileDrawersProps): import("react/jsx-runtime").JSX.Element;
32
+ export declare function MobileDrawers({ menuItems, menuActiveId, menuRenderLink, module, moduleAppId, panelViews, defaultView, customContent, }: MobileDrawersProps): import("react/jsx-runtime").JSX.Element;
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
- import { useEffect } from 'react';
3
+ import { cloneElement, isValidElement, useEffect } from 'react';
4
4
  import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle, } from '../../components/ui/drawer.js';
5
5
  import { cn } from '../../lib/utils.js';
6
6
  import { useMedaShell } from '../shell-provider.js';
@@ -9,7 +9,7 @@ import { useMedaShell } from '../shell-provider.js';
9
9
  * any custom-content drawers. Mount once near the AppShell root; drawers
10
10
  * open/close via `ctx.mobileDrawer.open` provider state.
11
11
  */
12
- export function MobileDrawers({ menuItems = [], module, panelViews = [], defaultView, customContent = {}, }) {
12
+ export function MobileDrawers({ menuItems = [], menuActiveId, menuRenderLink, module, moduleAppId, panelViews = [], defaultView, customContent = {}, }) {
13
13
  const ctx = useMedaShell();
14
14
  const open = ctx.mobileDrawer.open;
15
15
  const setOpen = ctx.mobileDrawer.setOpen;
@@ -18,24 +18,52 @@ export function MobileDrawers({ menuItems = [], module, panelViews = [], default
18
18
  workspaceId: ctx.workspace.id,
19
19
  appId: ctx.activeAppId,
20
20
  };
21
- return (_jsxs(_Fragment, { children: [_jsx(MenuDrawer, { open: open === 'menu-drawer', onClose: close, items: menuItems }), _jsx(ModuleDrawer, { open: open === 'module-drawer', onClose: close, module: module }), _jsx(PanelsDrawer, { open: open === 'panels-drawer', onClose: close, panelViews: panelViews, defaultView: defaultView, renderCtx: renderCtx }), _jsx(AiDrawer, { open: open === 'ai-drawer', onClose: close, panelViews: panelViews, renderCtx: renderCtx }), Object.entries(customContent).map(([id, renderFn]) => (_jsx(Drawer, { open: open === id, onOpenChange: (o) => !o && close(), direction: "bottom", children: _jsx(DrawerContent, { children: renderFn(close) }) }, id)))] }));
21
+ const moduleRenderCtx = {
22
+ workspaceId: ctx.workspace.id,
23
+ appId: moduleAppId ?? ctx.activeAppId,
24
+ };
25
+ return (_jsxs(_Fragment, { children: [_jsx(MenuDrawer, { open: open === 'menu-drawer', onClose: close, items: menuItems, activeId: menuActiveId, renderLink: menuRenderLink }), _jsx(ModuleDrawer, { open: open === 'module-drawer', onClose: close, module: module, renderCtx: moduleRenderCtx }), _jsx(PanelsDrawer, { open: open === 'panels-drawer', onClose: close, panelViews: panelViews, defaultView: defaultView, renderCtx: renderCtx }), _jsx(AiDrawer, { open: open === 'ai-drawer', onClose: close, panelViews: panelViews, renderCtx: renderCtx }), Object.entries(customContent).map(([id, renderFn]) => (_jsx(Drawer, { open: open === id, onOpenChange: (o) => !o && close(), direction: "bottom", children: _jsx(DrawerContent, { children: renderFn(close) }) }, id)))] }));
22
26
  }
23
27
  // ---------------------------------------------------------------------------
24
28
  // Internal sub-drawers
25
29
  // ---------------------------------------------------------------------------
26
- function MenuDrawer({ open, onClose, items, }) {
27
- return (_jsx(Drawer, { open: open, onOpenChange: (o) => !o && onClose(), direction: "left", children: _jsxs(DrawerContent, { children: [_jsxs(DrawerHeader, { children: [_jsx(DrawerTitle, { children: "Menu" }), _jsx(DrawerDescription, { className: "sr-only", children: "Switch between primary app areas." })] }), _jsx("nav", { className: "flex flex-col gap-0.5 p-2", children: items.map((item) => {
28
- const Icon = item.icon;
29
- return (_jsxs("a", { href: item.to, onClick: onClose, className: "flex items-center gap-2 rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground", children: [_jsx(Icon, { size: 18, "aria-hidden": "true" }), _jsx("span", { children: item.label })] }, item.id));
30
- }) })] }) }));
30
+ function MenuDrawer({ open, onClose, items, activeId, renderLink, }) {
31
+ return (_jsx(Drawer, { open: open, onOpenChange: (o) => !o && onClose(), direction: "left", children: _jsxs(DrawerContent, { children: [_jsxs(DrawerHeader, { children: [_jsx(DrawerTitle, { children: "Menu" }), _jsx(DrawerDescription, { className: "sr-only", children: "Switch between primary app areas." })] }), _jsx("nav", { className: "flex flex-col gap-0.5 p-2", children: items.map((item) => (_jsx(MenuDrawerItem, { item: item, isActive: item.id === activeId, onClose: onClose, renderLink: renderLink }, item.id))) })] }) }));
32
+ }
33
+ const menuItemClassName = 'flex items-center gap-2 rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground';
34
+ function MenuDrawerItem({ item, isActive, onClose, renderLink, }) {
35
+ const Icon = item.icon;
36
+ const children = (_jsxs(_Fragment, { children: [_jsx(Icon, { size: 18, "aria-hidden": "true" }), _jsx("span", { children: item.label })] }));
37
+ if (renderLink) {
38
+ return closeAfterLinkClick(renderLink({
39
+ item,
40
+ isActive,
41
+ className: cn(menuItemClassName, isActive && 'bg-accent text-foreground'),
42
+ children,
43
+ }), onClose);
44
+ }
45
+ return (_jsx("a", { href: item.to, onClick: onClose, className: menuItemClassName, children: children }));
46
+ }
47
+ function closeAfterLinkClick(link, onClose) {
48
+ if (!isValidElement(link)) {
49
+ return link;
50
+ }
51
+ const originalOnClick = link.props.onClick;
52
+ return cloneElement(link, {
53
+ onClick: (event) => {
54
+ originalOnClick?.(event);
55
+ onClose();
56
+ },
57
+ });
31
58
  }
32
- function ModuleDrawer({ open, onClose, module, }) {
33
- if (!module)
59
+ function ModuleDrawer({ open, onClose, module, renderCtx, }) {
60
+ const items = module?.items ?? [];
61
+ if (!module || (items.length === 0 && !module.render))
34
62
  return null;
35
- return (_jsx(Drawer, { open: open, onOpenChange: (o) => !o && onClose(), direction: "left", children: _jsxs(DrawerContent, { children: [_jsxs(DrawerHeader, { children: [_jsx(DrawerTitle, { children: module.label }), module.description && (_jsx(DrawerDescription, { className: "text-muted-foreground text-xs", children: module.description }))] }), _jsx("nav", { className: "flex flex-col gap-0.5 p-2", children: module.items.map((item) => {
63
+ return (_jsx(Drawer, { open: open, onOpenChange: (o) => !o && onClose(), direction: "left", children: _jsxs(DrawerContent, { children: [_jsxs(DrawerHeader, { children: [_jsx(DrawerTitle, { children: module.label }), module.description && (_jsx(DrawerDescription, { className: "text-muted-foreground text-xs", children: module.description }))] }), items.length > 0 && (_jsx("nav", { className: "flex flex-col gap-0.5 p-2", children: items.map((item) => {
36
64
  const Icon = item.icon;
37
65
  return (_jsxs("a", { href: item.to, onClick: onClose, className: "flex items-center gap-2 rounded-md px-3 py-2 text-sm text-muted-foreground hover:bg-accent hover:text-foreground", children: [_jsx(Icon, { size: 16, "aria-hidden": "true" }), _jsx("span", { children: item.label })] }, item.id));
38
- }) })] }) }));
66
+ }) })), module.render?.(renderCtx)] }) }));
39
67
  }
40
68
  function PanelsDrawer({ open, onClose, panelViews, defaultView, renderCtx, }) {
41
69
  const ctx = useMedaShell();
@@ -0,0 +1,15 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type PanelViewRegistration } from './shell-provider.js';
3
+ import type { PanelView } from './types.js';
4
+ export interface PanelViewsProviderProps {
5
+ views: PanelView[];
6
+ defaultView?: string;
7
+ children: ReactNode;
8
+ }
9
+ export interface ResolvedPanelViews {
10
+ panelViews: PanelView[];
11
+ defaultView?: string;
12
+ }
13
+ export declare function mergePanelViews(staticViews: PanelView[] | undefined, registrations: PanelViewRegistration[], staticDefaultView?: string): ResolvedPanelViews;
14
+ export declare function useResolvedPanelViews(staticViews?: PanelView[], staticDefaultView?: string): ResolvedPanelViews;
15
+ export declare function PanelViewsProvider({ views, defaultView, children }: PanelViewsProviderProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,56 @@
1
+ 'use client';
2
+ import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
3
+ import { useId, useLayoutEffect, useMemo, useRef } from 'react';
4
+ import { useMedaShell } from './shell-provider.js';
5
+ export function mergePanelViews(staticViews = [], registrations, staticDefaultView) {
6
+ const panelViews = [];
7
+ for (const view of staticViews) {
8
+ panelViews.push(view);
9
+ }
10
+ let defaultView = staticDefaultView;
11
+ for (const registration of registrations) {
12
+ for (const view of registration.views) {
13
+ const previousIndex = panelViews.findIndex((existingView) => existingView.id === view.id);
14
+ if (previousIndex >= 0) {
15
+ panelViews.splice(previousIndex, 1);
16
+ }
17
+ panelViews.push(view);
18
+ }
19
+ if (registration.defaultView !== undefined) {
20
+ defaultView = registration.defaultView;
21
+ }
22
+ }
23
+ return defaultView === undefined ? { panelViews } : { panelViews, defaultView };
24
+ }
25
+ export function useResolvedPanelViews(staticViews = [], staticDefaultView) {
26
+ const { panelViews } = useMedaShell();
27
+ return useMemo(() => mergePanelViews(staticViews, panelViews.registrations, staticDefaultView), [staticViews, panelViews.registrations, staticDefaultView]);
28
+ }
29
+ function getPanelViewsSignature(views) {
30
+ return views.map((view) => `${view.id}\u001f${view.label}`).join('\u001e');
31
+ }
32
+ export function PanelViewsProvider({ views, defaultView, children }) {
33
+ const { register } = useMedaShell().panelViews;
34
+ const registrationId = useId();
35
+ const latestViewsRef = useRef(views);
36
+ latestViewsRef.current = views;
37
+ const viewsSignature = getPanelViewsSignature(views);
38
+ const registeredViewsRef = useRef(null);
39
+ if (registeredViewsRef.current?.signature !== viewsSignature) {
40
+ registeredViewsRef.current = {
41
+ signature: viewsSignature,
42
+ views: views.map((view) => ({
43
+ id: view.id,
44
+ label: view.label,
45
+ icon: view.icon,
46
+ render: (ctx) => latestViewsRef.current.find((currentView) => currentView.id === view.id)?.render(ctx) ??
47
+ null,
48
+ })),
49
+ };
50
+ }
51
+ const registeredViews = registeredViewsRef.current.views;
52
+ useLayoutEffect(() => {
53
+ return register(registrationId, registeredViews, defaultView);
54
+ }, [register, registrationId, registeredViews, defaultView]);
55
+ return _jsx(_Fragment, { children: children });
56
+ }
@@ -0,0 +1,41 @@
1
+ import { type ReactNode, type Ref } from 'react';
2
+ /**
3
+ * Describes the slot's current relation to an active drag operation.
4
+ *
5
+ * - `'idle'` — no drag in progress.
6
+ * - `'active'` — a drag is in progress and this slot would accept it, but
7
+ * the cursor is not currently over this slot.
8
+ * - `'over'` — the dragged item is hovering over this slot and it would
9
+ * accept the drop.
10
+ * - `'rejected'` — the dragged item is hovering over this slot but the slot
11
+ * cannot accept it (disabled or `accepts` returns false), OR
12
+ * the slot is `disabled` while any drag is active.
13
+ */
14
+ export type RailDropSlotState = 'idle' | 'active' | 'over' | 'rejected';
15
+ export interface RailDropSlotProps {
16
+ /** Stable id used by dnd-kit; also rendered as `data-slot-id`. */
17
+ id: string;
18
+ /** Decide whether this slot accepts the dragged card. */
19
+ accepts?: (activeId: string) => boolean;
20
+ /** Whether this slot can currently receive a drop (e.g. capacity, online). */
21
+ disabled?: boolean;
22
+ /**
23
+ * Static content. Mutually exclusive with `render`.
24
+ * Use `render` when you need state-aware content.
25
+ */
26
+ children?: ReactNode;
27
+ /**
28
+ * State-aware content factory. Receives the slot's current `RailDropSlotState`
29
+ * so you can render different UI for idle / active / over / rejected states.
30
+ * Mutually exclusive with `children`.
31
+ */
32
+ render?: (state: RailDropSlotState) => ReactNode;
33
+ className?: string;
34
+ /** Visible label for assistive tech. */
35
+ ariaLabel?: string;
36
+ ref?: Ref<HTMLElement>;
37
+ }
38
+ export declare function RailDropSlot({ id, accepts, disabled, children, render, className, ariaLabel, ref, }: RailDropSlotProps): import("react/jsx-runtime").JSX.Element;
39
+ export declare namespace RailDropSlot {
40
+ var displayName: string;
41
+ }
@@ -0,0 +1,49 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useDndMonitor, useDroppable } from '@dnd-kit/core';
4
+ import { useState } from 'react';
5
+ import { cn } from '../lib/utils.js';
6
+ export function RailDropSlot({ id, accepts, disabled, children, render, className, ariaLabel, ref, }) {
7
+ const { isOver, active, setNodeRef } = useDroppable({
8
+ id,
9
+ disabled,
10
+ data: { type: 'rail-drop-slot' },
11
+ });
12
+ const [globalDragActive, setGlobalDragActive] = useState(false);
13
+ useDndMonitor({
14
+ onDragStart: () => setGlobalDragActive(true),
15
+ onDragEnd: () => setGlobalDragActive(false),
16
+ onDragCancel: () => setGlobalDragActive(false),
17
+ });
18
+ const wouldAccept = !disabled && (!accepts || (active?.id != null && accepts(String(active.id))));
19
+ let state;
20
+ if (isOver && wouldAccept) {
21
+ state = 'over';
22
+ }
23
+ else if (isOver && !wouldAccept) {
24
+ state = 'rejected';
25
+ }
26
+ else if (globalDragActive && disabled) {
27
+ state = 'rejected';
28
+ }
29
+ else if (globalDragActive && wouldAccept) {
30
+ state = 'active';
31
+ }
32
+ else {
33
+ state = 'idle';
34
+ }
35
+ // Legacy boolean aliases kept for backward-compat data attributes
36
+ const showHover = state === 'over';
37
+ const showRejected = state === 'rejected';
38
+ return (_jsx("section", { ref: (el) => {
39
+ setNodeRef(el);
40
+ if (typeof ref === 'function')
41
+ ref(el);
42
+ else if (ref)
43
+ ref.current = el;
44
+ }, "data-slot-id": id, "data-state": state, "aria-label": ariaLabel, "aria-disabled": disabled || undefined, className: cn('relative rounded-lg border border-border bg-card transition-all duration-150', state === 'idle' && '', state === 'active' && 'border-dashed border-primary/60 bg-primary/5', state === 'over' &&
45
+ 'border-primary bg-primary/5 ring-2 ring-primary/30 shadow-[0_0_0_4px_hsl(var(--primary)/0.1)]', state === 'rejected' && 'border-destructive/40 opacity-60',
46
+ // Legacy classes — keep working for consumers using data-state selectors
47
+ showHover && '', showRejected && '', disabled && state === 'idle' && 'opacity-60', className), children: render ? render(state) : children }));
48
+ }
49
+ RailDropSlot.displayName = 'RailDropSlot';
@@ -0,0 +1,28 @@
1
+ import { type ReactNode } from 'react';
2
+ export interface RailDropZonesProps {
3
+ /** Section title — uppercased in the header. Defaults to "Drop zones". */
4
+ title?: string;
5
+ /** Subtitle shown under the title (e.g. "Eligible machines for ENG-405 · claude"). */
6
+ subtitle?: ReactNode;
7
+ /** Count badge rendered in the header corner, e.g. "3 / 4". */
8
+ count?: ReactNode;
9
+ /** Optional icon rendered at the start of the header. */
10
+ icon?: ReactNode;
11
+ /**
12
+ * When set, overrides the auto-detected drag-active state. Useful for
13
+ * Storybook stories that want to demonstrate the active visual without a
14
+ * real drag in progress.
15
+ */
16
+ forceActive?: boolean;
17
+ /**
18
+ * Optional predicate; when supplied only activates the rail outline for
19
+ * matching drag ids. Receives `active.id` of the current drag.
20
+ */
21
+ isActive?: (activeId: string | number) => boolean;
22
+ children: ReactNode;
23
+ className?: string;
24
+ }
25
+ export declare function RailDropZones({ title, subtitle, count, icon, forceActive, isActive, children, className, }: RailDropZonesProps): import("react/jsx-runtime").JSX.Element;
26
+ export declare namespace RailDropZones {
27
+ var displayName: string;
28
+ }
@@ -0,0 +1,17 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useDndMonitor } from '@dnd-kit/core';
4
+ import { useState } from 'react';
5
+ import { cn } from '../lib/utils.js';
6
+ export function RailDropZones({ title = 'Drop zones', subtitle, count, icon, forceActive, isActive, children, className, }) {
7
+ const [activeId, setActiveId] = useState(null);
8
+ useDndMonitor({
9
+ onDragStart: (e) => setActiveId(e.active.id),
10
+ onDragEnd: () => setActiveId(null),
11
+ onDragCancel: () => setActiveId(null),
12
+ });
13
+ const auto = activeId != null && (!isActive || isActive(activeId));
14
+ const active = forceActive ?? auto;
15
+ return (_jsxs("section", { "data-active": active || undefined, className: cn('flex flex-col gap-3 rounded-lg border border-transparent p-3 transition-colors', active && 'border-2 border-dashed border-primary/60 bg-primary/5', className), children: [(title || subtitle || count) && (_jsxs("header", { className: "flex items-start justify-between gap-3", children: [_jsxs("div", { className: "flex items-start gap-2", children: [icon && _jsx("span", { className: "text-primary", children: icon }), _jsxs("div", { className: "flex flex-col gap-0.5", children: [title && (_jsx("span", { className: "font-medium text-muted-foreground text-xs uppercase tracking-wider", children: title })), subtitle && _jsx("span", { className: "text-muted-foreground text-xs", children: subtitle })] })] }), count && _jsx("span", { className: "text-muted-foreground text-xs", children: count })] })), _jsx("div", { className: "flex flex-col gap-2", children: children })] }));
16
+ }
17
+ RailDropZones.displayName = 'RailDropZones';