@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.
- package/dist/components/ui/checkbox.d.ts +8 -0
- package/dist/components/ui/checkbox.js +9 -0
- package/dist/components/ui/collapsible.d.ts +5 -0
- package/dist/components/ui/collapsible.js +13 -0
- package/dist/kanban/index.d.ts +5 -0
- package/dist/kanban/index.js +4 -0
- package/dist/kanban/kanban-board.d.ts +8 -0
- package/dist/kanban/kanban-board.js +197 -0
- package/dist/kanban/kanban-card-wrapper.d.ts +16 -0
- package/dist/kanban/kanban-card-wrapper.js +20 -0
- package/dist/kanban/kanban-collision.d.ts +16 -0
- package/dist/kanban/kanban-collision.js +18 -0
- package/dist/kanban/kanban-column.d.ts +19 -0
- package/dist/kanban/kanban-column.js +19 -0
- package/dist/kanban/kanban-drop-handler.d.ts +16 -0
- package/dist/kanban/kanban-drop-handler.js +84 -0
- package/dist/kanban/types.d.ts +47 -0
- package/dist/kanban/types.js +7 -0
- package/dist/list/index.d.ts +2 -0
- package/dist/list/index.js +1 -0
- package/dist/list/list-row.d.ts +52 -0
- package/dist/list/list-row.js +37 -0
- package/dist/shell/app-shell-workspace.js +11 -6
- package/dist/shell/command-palette.js +2 -1
- package/dist/shell/context-rail.d.ts +1 -1
- package/dist/shell/context-rail.js +5 -4
- package/dist/shell/drag-mode-banner.d.ts +31 -0
- package/dist/shell/drag-mode-banner.js +19 -0
- package/dist/shell/index.d.ts +8 -0
- package/dist/shell/index.js +5 -0
- package/dist/shell/internal/mobile-drawers.d.ts +8 -2
- package/dist/shell/internal/mobile-drawers.js +40 -12
- package/dist/shell/panel-views-provider.d.ts +15 -0
- package/dist/shell/panel-views-provider.js +56 -0
- package/dist/shell/rail-drop-slot.d.ts +41 -0
- package/dist/shell/rail-drop-slot.js +49 -0
- package/dist/shell/rail-drop-zones.d.ts +28 -0
- package/dist/shell/rail-drop-zones.js +17 -0
- package/dist/shell/right-panel.js +21 -8
- package/dist/shell/shell-provider.d.ts +14 -1
- package/dist/shell/shell-provider.js +64 -2
- package/dist/shell/types.d.ts +5 -1
- package/dist/timeline/index.d.ts +3 -0
- package/dist/timeline/index.js +2 -0
- package/dist/timeline/lane-timeline-types.d.ts +61 -0
- package/dist/timeline/lane-timeline-types.js +6 -0
- package/dist/timeline/lane-timeline.d.ts +2 -0
- package/dist/timeline/lane-timeline.js +85 -0
- package/dist/timeline/lane.d.ts +10 -0
- package/dist/timeline/lane.js +39 -0
- package/dist/timeline/public.d.ts +1 -0
- package/dist/timeline/time-axis.d.ts +18 -0
- package/dist/timeline/time-axis.js +22 -0
- 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,
|
|
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 &&
|
|
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,
|
|
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 (
|
|
53
|
+
if (panelViews.length > 0) {
|
|
49
54
|
items.push({ id: 'panels', label: 'Panels', icon: PanelTop, opens: 'panels-drawer' });
|
|
50
|
-
if (
|
|
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
|
-
|
|
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
|
|
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
|
|
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 ||
|
|
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:
|
|
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';
|
package/dist/shell/index.d.ts
CHANGED
|
@@ -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';
|
package/dist/shell/index.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
-
|
|
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:
|
|
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';
|