@medalsocial/meda 1.1.0 → 1.1.1
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.
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ContextRail — spec §10
|
|
3
|
-
*
|
|
4
|
-
* Renders a resizable, collapsible, persisted context navigation rail.
|
|
5
|
-
* Width is stored per-(workspaceId, appId) via useShellLayoutState through
|
|
6
|
-
* the MedaShellProvider context.
|
|
7
|
-
*
|
|
8
|
-
* Resize integration note (Phase 9 / Pattern B):
|
|
9
|
-
* The rail uses its own pointer-events resize handle on the right edge instead
|
|
10
|
-
* of wrapping in <ResizableShell> (Pattern A). Pattern A requires migrating
|
|
11
|
-
* <AppShellBody> to a PanelGroup layout — that is Phase 11 territory.
|
|
12
|
-
* TODO(phase-11): replace pointer-events handle with <ResizableShellPanel>
|
|
13
|
-
* once <AppShellBody> ships as a ResizableShell Group.
|
|
14
|
-
*/
|
|
15
1
|
import type { ReactNode } from 'react';
|
|
16
2
|
import type { ContextModule, ShellLinkRenderArgs } from './types.js';
|
|
17
3
|
export interface ContextRailProps {
|
|
@@ -28,4 +14,4 @@ export interface ContextRailProps {
|
|
|
28
14
|
renderLink?: (args: ShellLinkRenderArgs) => ReactNode;
|
|
29
15
|
className?: string;
|
|
30
16
|
}
|
|
31
|
-
export declare function ContextRail({ appId: _appId, module, hidden, collapsible
|
|
17
|
+
export declare function ContextRail({ appId: _appId, module, hidden, collapsible, activeItemId, renderLink, className, }: ContextRailProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* ContextRail — spec §10
|
|
5
|
+
*
|
|
6
|
+
* Renders a resizable, collapsible, persisted context navigation rail.
|
|
7
|
+
* Width is stored per-(workspaceId, appId) via useShellLayoutState through
|
|
8
|
+
* the MedaShellProvider context.
|
|
9
|
+
*
|
|
10
|
+
* Resize integration note (Phase 9 / Pattern B):
|
|
11
|
+
* The rail uses its own pointer-events resize handle on the right edge instead
|
|
12
|
+
* of wrapping in <ResizableShell> (Pattern A). Pattern A requires migrating
|
|
13
|
+
* <AppShellBody> to a PanelGroup layout — that is Phase 11 territory.
|
|
14
|
+
* TODO(phase-11): replace pointer-events handle with <ResizableShellPanel>
|
|
15
|
+
* once <AppShellBody> ships as a ResizableShell Group.
|
|
16
|
+
*/
|
|
17
|
+
import { PanelLeftClose, PanelLeftOpen } from 'lucide-react';
|
|
18
|
+
import { useId, useRef, useState } from 'react';
|
|
4
19
|
import { cn } from '../lib/utils.js';
|
|
5
20
|
import { useMedaShell } from './shell-provider.js';
|
|
6
21
|
import { useShellViewport } from './use-shell-viewport.js';
|
|
@@ -41,12 +56,37 @@ function ResizeHandle({ currentWidth, onResize, onCommit }) {
|
|
|
41
56
|
return (_jsx("div", { role: "separator", "aria-orientation": "vertical", "aria-label": "Resize context rail", "aria-valuenow": currentWidth, "aria-valuemin": MIN_WIDTH, "aria-valuemax": MAX_WIDTH, tabIndex: 0, className: cn('absolute top-0 right-0 h-full w-1 cursor-col-resize', 'opacity-0 transition-opacity hover:opacity-100 hover:bg-ring'), onPointerDown: handlePointerDown, onPointerMove: handlePointerMove, onPointerUp: handlePointerUp }));
|
|
42
57
|
}
|
|
43
58
|
// ---------------------------------------------------------------------------
|
|
59
|
+
// ContextRailToggle — chevron button that flips ctx.contextRail.collapsed
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
function ContextRailToggle({ railId }) {
|
|
62
|
+
const ctx = useMedaShell();
|
|
63
|
+
const collapsed = ctx.contextRail.collapsed;
|
|
64
|
+
// Lucide PanelLeft* icons render the chevron-with-wall pattern (similar
|
|
65
|
+
// to Unifi). The wall reinforces "this is a sidebar toggle" rather than
|
|
66
|
+
// a generic navigation chevron.
|
|
67
|
+
const Icon = collapsed ? PanelLeftOpen : PanelLeftClose;
|
|
68
|
+
return (_jsx("button", { type: "button", onClick: () => ctx.contextRail.setCollapsed(!collapsed), "aria-label": collapsed ? 'Expand sidebar' : 'Collapse sidebar', "aria-expanded": !collapsed, "aria-controls": railId, "data-testid": "context-rail-toggle", className: cn(
|
|
69
|
+
// Pull-tab: 28w × 40h. Big enough to read the icon at a glance and to
|
|
70
|
+
// grab confidently — matches Unifi's reference. Flat left edge
|
|
71
|
+
// attached to the rail (no left border), rounded right. The whole tab
|
|
72
|
+
// sticks fully out of the rail's outer edge.
|
|
73
|
+
'absolute top-3 -right-7 z-20 inline-flex h-10 w-7 items-center justify-center', 'before:absolute before:-inset-1 before:content-[""]', 'rounded-r-md border border-l-0 border-border bg-card text-muted-foreground', 'shadow-[2px_0_4px_rgb(0_0_0_/_0.08)]', 'hover:bg-accent hover:text-foreground', 'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring'), children: _jsx(Icon, { size: 18, "aria-hidden": true }) }));
|
|
74
|
+
}
|
|
75
|
+
// ---------------------------------------------------------------------------
|
|
44
76
|
// ContextRail
|
|
45
77
|
// ---------------------------------------------------------------------------
|
|
46
|
-
export function ContextRail({ appId: _appId, module, hidden = false, collapsible
|
|
78
|
+
export function ContextRail({ appId: _appId, module, hidden = false, collapsible = true, activeItemId, renderLink, className, }) {
|
|
47
79
|
const band = useShellViewport();
|
|
48
80
|
const ctx = useMedaShell();
|
|
49
|
-
|
|
81
|
+
// collapsible={false} means the rail must always render expanded — even if
|
|
82
|
+
// the persisted layout state has collapsed: true (e.g. user collapsed the
|
|
83
|
+
// rail before the prop flipped). Without this guard, the rail would stay
|
|
84
|
+
// hidden forever with no in-component way to recover.
|
|
85
|
+
const collapsed = collapsible ? ctx.contextRail.collapsed : false;
|
|
86
|
+
// Per-instance id so multiple <ContextRail>s in one document don't clash on
|
|
87
|
+
// duplicate `id="..."` (HTML invalid + breaks aria-controls relationships).
|
|
88
|
+
const reactId = useId();
|
|
89
|
+
const railId = `meda-context-rail-${reactId}`;
|
|
50
90
|
// Local display width tracks pointer-move updates; ctx.contextRail.setWidth
|
|
51
91
|
// is called on pointerUp to persist via useShellLayoutState.
|
|
52
92
|
const [displayWidth, setDisplayWidth] = useState(null);
|
|
@@ -66,16 +106,22 @@ export function ContextRail({ appId: _appId, module, hidden = false, collapsible
|
|
|
66
106
|
setDisplayWidth(null);
|
|
67
107
|
ctx.contextRail.setWidth(w);
|
|
68
108
|
};
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
109
|
+
// Only animate width during collapse/expand toggles, not during manual
|
|
110
|
+
// pointer drag on the ResizeHandle. displayWidth is non-null only while
|
|
111
|
+
// the user is mid-drag — using it as the drag signal suppresses the
|
|
112
|
+
// transition then so the rail snaps to the cursor instead of lagging
|
|
113
|
+
// behind it.
|
|
114
|
+
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
|
+
const isActive = item.id === activeItemId;
|
|
117
|
+
const klass = cn('flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors', isActive
|
|
118
|
+
? 'bg-primary/10 text-primary'
|
|
119
|
+
: 'text-muted-foreground hover:bg-accent hover:text-foreground');
|
|
120
|
+
const IconComp = item.icon;
|
|
121
|
+
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 }))] }));
|
|
122
|
+
if (renderLink) {
|
|
123
|
+
return renderLink({ item, isActive, className: klass, children: inner });
|
|
124
|
+
}
|
|
125
|
+
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 }))] }));
|
|
81
127
|
}
|
|
@@ -40,7 +40,10 @@ export function createLocalStorageAdapter() {
|
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
42
|
const DEFAULTS = {
|
|
43
|
-
|
|
43
|
+
// 260 default keeps the rail snug for typical module navs (4-8 short labels
|
|
44
|
+
// like Inbox/Sent/Drafts) without feeling cramped. Consumers can drag wider
|
|
45
|
+
// up to MAX_WIDTH (420) — the value is persisted per-workspace.
|
|
46
|
+
contextRail: { width: 260, collapsed: false },
|
|
44
47
|
rightPanel: { mode: 'closed', activeView: null, width: 340 },
|
|
45
48
|
};
|
|
46
49
|
function isShellLayoutState(value) {
|