@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: _collapsible, activeItemId, renderLink, className, }: ContextRailProps): import("react/jsx-runtime").JSX.Element | null;
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
- import { useRef, useState } from 'react';
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: _collapsible = true, activeItemId, renderLink, className, }) {
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
- const collapsed = ctx.contextRail.collapsed;
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
- return (_jsxs("aside", { "data-testid": "context-rail", "aria-label": module.label, className: cn('relative h-full shrink-0 overflow-hidden border-r border-shell-border bg-shell-context', collapsed && 'w-0', className), style: { width: collapsed ? 0 : width }, 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) => {
70
- const isActive = item.id === activeItemId;
71
- const klass = cn('flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors', isActive
72
- ? 'bg-primary/10 text-primary'
73
- : 'text-muted-foreground hover:bg-accent hover:text-foreground');
74
- const IconComp = item.icon;
75
- 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 }))] }));
76
- if (renderLink) {
77
- return renderLink({ item, isActive, className: klass, children: inner });
78
- }
79
- return (_jsx("a", { href: item.to, "aria-current": isActive ? 'page' : undefined, className: klass, children: inner }, item.id));
80
- }) }), _jsx(ResizeHandle, { currentWidth: width, onResize: handleResize, onCommit: handleCommit })] }));
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
- contextRail: { width: 300, collapsed: false },
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) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@medalsocial/meda",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Shared Meda UI shell and runtime package.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {