@medalsocial/meda 3.13.0 → 3.14.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.
@@ -106,13 +106,11 @@ export function ContextRail({ appId, module, hidden = false, collapsible = true,
106
106
  setDisplayWidth(null);
107
107
  ctx.contextRail.setWidth(w);
108
108
  };
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('group/rail relative h-full shrink-0 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(ContextRailSeam, { railId: railId, currentWidth: width, onResize: handleResize, onCommit: handleCommit })), _jsxs("div", { className: "flex h-full min-w-0 flex-col overflow-hidden", "aria-hidden": collapsed, inert: collapsed || undefined, children: [showHeader && (_jsxs("div", { className: "shrink-0 border-b border-shell-border px-4 py-3", children: [_jsx("h2", { className: "text-sm font-semibold text-foreground", children: module.label }), module.description && (_jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: module.description }))] })), _jsxs("div", { "data-meda-context-rail-scroll-area": "", className: cn('min-h-0 flex-1', scroll === 'auto' ? 'overflow-y-auto overflow-x-hidden' : 'overflow-hidden'), children: [items.length > 0 && (_jsx("nav", { "aria-label": `${module.label} navigation`, className: "flex flex-col gap-0.5 p-2", children: items.map((item) => {
109
+ // The rail never animates its width. Collapse/expand is mostly ⌘B, a
110
+ // keyboard action used many times a day, and a width transition re-wraps
111
+ // the whole main column on every frame. It snaps on the keyboard, on a
112
+ // click and while dragging.
113
+ return (_jsxs("aside", { id: railId, "data-testid": "context-rail", "aria-label": module.label, className: cn('group/rail relative h-full shrink-0 bg-shell-context', collapsed && 'w-0', className), style: { width: collapsed ? 0 : width }, children: [collapsible && (_jsx(ContextRailSeam, { railId: railId, currentWidth: width, onResize: handleResize, onCommit: handleCommit })), _jsxs("div", { className: "flex h-full min-w-0 flex-col overflow-hidden", "aria-hidden": collapsed, inert: collapsed || undefined, children: [showHeader && (_jsxs("div", { className: "shrink-0 border-b border-shell-border px-4 py-3", children: [_jsx("h2", { className: "text-sm font-semibold text-foreground", children: module.label }), module.description && (_jsx("p", { className: "mt-0.5 text-xs text-muted-foreground", children: module.description }))] })), _jsxs("div", { "data-meda-context-rail-scroll-area": "", className: cn('min-h-0 flex-1', scroll === 'auto' ? 'overflow-y-auto overflow-x-hidden' : 'overflow-hidden'), children: [items.length > 0 && (_jsx("nav", { "aria-label": `${module.label} navigation`, className: "flex flex-col gap-0.5 p-2", children: items.map((item) => {
116
114
  const isActive = item.id === activeItemId;
117
115
  const klass = cn('flex items-center gap-2 rounded-md px-2 py-1.5 text-sm transition-colors', isActive
118
116
  ? 'bg-primary/10 text-primary'
@@ -25,7 +25,8 @@ export { AppTabs, PanelToggle, ShellHeader, WorkspaceSwitcher } from './shell-he
25
25
  export { ShellMain } from './shell-main.js';
26
26
  export type { MedaShellProviderProps } from './shell-provider.js';
27
27
  export { MedaShellProvider, useMedaShell, useShellSelection } from './shell-provider.js';
28
+ export { ShellViewportHintProvider, type ShellViewportHintProviderProps, } from './shell-viewport-hint.js';
28
29
  export { DefaultThemeProvider, ThemeToggle, useTheme } from './theme.js';
29
30
  export { NextThemesAdapter } from './theme-next-themes.js';
30
31
  export type { AppDefinition, AppShellAppTabsConfig, AppShellAuthBranding, AppShellAuthConfig, AppShellContextRailConfig, AppShellIconRailConfig, AppShellMobileNavConfig, AppShellRightPanelConfig, AppShellVariant, AppShellWorkspaceConfig, AppTabRenderLinkArgs, CommandDefinition, ContextItem, ContextModule, ContextRailHeader, ContextRailScroll, MobileBottomNavItem, MobileDockItem, MobileNavGroup, MobileNavItem, MobileNavLinkArgs, MobileNavTree, MobileNavView, PanelMode, PanelView, ShellHeaderLayout, ShellLinkRenderArgs, ShellMainLayout, ShellRenderContext, ShellViewport, ThemeAdapter, WorkspaceDefinition, WorkspaceMenuItem, } from './types.js';
31
- export { useShellViewport } from './use-shell-viewport.js';
32
+ export { ShellViewportHintContext, useShellViewport } from './use-shell-viewport.js';
@@ -28,7 +28,8 @@ export { AppTabs, PanelToggle, ShellHeader, WorkspaceSwitcher } from './shell-he
28
28
  export { ShellMain } from './shell-main.js';
29
29
  // Provider + hooks
30
30
  export { MedaShellProvider, useMedaShell, useShellSelection } from './shell-provider.js';
31
+ export { ShellViewportHintProvider, } from './shell-viewport-hint.js';
31
32
  // Theme
32
33
  export { DefaultThemeProvider, ThemeToggle, useTheme } from './theme.js';
33
34
  export { NextThemesAdapter } from './theme-next-themes.js';
34
- export { useShellViewport } from './use-shell-viewport.js';
35
+ export { ShellViewportHintContext, useShellViewport } from './use-shell-viewport.js';
@@ -138,7 +138,11 @@ export function RightPanel({ panelViews = EMPTY_PANEL_VIEWS, defaultView, modes
138
138
  setDisplayWidth(null);
139
139
  setWidth(w);
140
140
  };
141
- return (_jsx("aside", { "data-meda-panel-mode": mode, "aria-hidden": mode === 'closed' ? 'true' : undefined, className: cn('relative h-full shrink-0 overflow-hidden border-l border-shell-border bg-shell-panel', 'transition-[width] ease-[var(--motion-ease)] duration-[var(--motion-panel)]', mode === 'fullscreen' && 'fixed inset-0 h-screen w-screen border-none', zIndexClass, className), style: widthStyle, children: mode !== 'closed' && (_jsxs("div", { className: "flex h-full flex-col", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-shell-border px-3 py-2", children: [_jsx("div", { className: "flex items-center gap-1", children: resolvedPanelViews.map((view) => {
141
+ return (_jsx("aside", { "data-meda-panel-mode": mode, "aria-hidden": mode === 'closed' ? 'true' : undefined,
142
+ // No width transition: ⌘I / ⌘E are keyboard actions used many times a
143
+ // day, and an animated width re-wraps the main column every frame (and
144
+ // lags behind the cursor while resizing). The panel snaps.
145
+ className: cn('relative h-full shrink-0 overflow-hidden border-l border-shell-border bg-shell-panel', mode === 'fullscreen' && 'fixed inset-0 h-screen w-screen border-none', zIndexClass, className), style: widthStyle, children: mode !== 'closed' && (_jsxs("div", { className: "flex h-full flex-col", children: [_jsxs("div", { className: "flex items-center justify-between border-b border-shell-border px-3 py-2", children: [_jsx("div", { className: "flex items-center gap-1", children: resolvedPanelViews.map((view) => {
142
146
  const isActive = view.id === activeView;
143
147
  const Icon = view.icon;
144
148
  const children = (_jsxs(_Fragment, { children: [_jsx(Icon, { size: 14, "aria-hidden": "true" }), _jsx("span", { children: view.label })] }));
@@ -0,0 +1,34 @@
1
+ import type { ReactNode } from 'react';
2
+ import type { ShellViewport } from './types.js';
3
+ export interface ShellViewportHintProviderProps {
4
+ /**
5
+ * Coarse device class detected from the request, or `null` to keep the
6
+ * desktop-first default.
7
+ */
8
+ value: ShellViewport | null;
9
+ children: ReactNode;
10
+ }
11
+ /**
12
+ * Client wrapper that supplies the SSR viewport hint.
13
+ *
14
+ * React Context providers cannot be rendered from a React Server Component,
15
+ * so an App Router server component — the place where request headers are
16
+ * actually available — must render this client component instead of touching
17
+ * `ShellViewportHintContext.Provider` itself:
18
+ *
19
+ * // app/layout.tsx — server component
20
+ * import { headers } from 'next/headers';
21
+ * import { ShellViewportHintProvider } from '@medalsocial/meda/shell';
22
+ *
23
+ * export default async function Layout({ children }) {
24
+ * const isPhone = (await headers()).get('sec-ch-ua-mobile') === '?1';
25
+ * return (
26
+ * <ShellViewportHintProvider value={isPhone ? 'mobile' : null}>
27
+ * {children}
28
+ * </ShellViewportHintProvider>
29
+ * );
30
+ * }
31
+ *
32
+ * See {@link ShellViewportHintContext} for what the hint does.
33
+ */
34
+ export declare function ShellViewportHintProvider({ value, children }: ShellViewportHintProviderProps): import("react").JSX.Element;
@@ -0,0 +1,29 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { ShellViewportHintContext } from './use-shell-viewport.js';
4
+ /**
5
+ * Client wrapper that supplies the SSR viewport hint.
6
+ *
7
+ * React Context providers cannot be rendered from a React Server Component,
8
+ * so an App Router server component — the place where request headers are
9
+ * actually available — must render this client component instead of touching
10
+ * `ShellViewportHintContext.Provider` itself:
11
+ *
12
+ * // app/layout.tsx — server component
13
+ * import { headers } from 'next/headers';
14
+ * import { ShellViewportHintProvider } from '@medalsocial/meda/shell';
15
+ *
16
+ * export default async function Layout({ children }) {
17
+ * const isPhone = (await headers()).get('sec-ch-ua-mobile') === '?1';
18
+ * return (
19
+ * <ShellViewportHintProvider value={isPhone ? 'mobile' : null}>
20
+ * {children}
21
+ * </ShellViewportHintProvider>
22
+ * );
23
+ * }
24
+ *
25
+ * See {@link ShellViewportHintContext} for what the hint does.
26
+ */
27
+ export function ShellViewportHintProvider({ value, children }) {
28
+ return (_jsx(ShellViewportHintContext.Provider, { value: value, children: children }));
29
+ }
@@ -1,4 +1,6 @@
1
1
  import type { ShellViewport } from './types.js';
2
+ /** Optional request-derived band for server rendering and hydration only. */
3
+ export declare const ShellViewportHintContext: import("react").Context<ShellViewport | null>;
2
4
  /**
3
5
  * The shell's viewport band, resolved SYNCHRONOUSLY on the first client
4
6
  * render.
@@ -12,8 +14,8 @@ import type { ShellViewport } from './types.js';
12
14
  *
13
15
  * `useSyncExternalStore` reads `matchMedia` during the first client render,
14
16
  * so client-rendered apps commit the correct tree once. Server rendering
15
- * still returns 'desktop' via the server snapshot, and hydration stays
16
- * mismatch-safe (React uses the server snapshot while hydrating, then
17
- * re-reads — SSR consumers keep exactly the old behavior).
17
+ * uses the optional request-derived hint, otherwise 'desktop'. Hydration uses
18
+ * that same snapshot before switching to the live viewport. A hint lets an
19
+ * app send mobile shell HTML before its JavaScript loads.
18
20
  */
19
21
  export declare function useShellViewport(): ShellViewport;
@@ -1,5 +1,5 @@
1
1
  'use client';
2
- import { useSyncExternalStore } from 'react';
2
+ import { createContext, useContext, useSyncExternalStore } from 'react';
3
3
  const BREAKPOINTS = {
4
4
  mobile: '(max-width: 767px)',
5
5
  tablet: '(min-width: 768px) and (max-width: 1023px)',
@@ -7,6 +7,8 @@ const BREAKPOINTS = {
7
7
  wide: '(min-width: 1280px) and (max-width: 1535px)',
8
8
  ultrawide: '(min-width: 1536px)',
9
9
  };
10
+ /** Optional request-derived band for server rendering and hydration only. */
11
+ export const ShellViewportHintContext = createContext(null);
10
12
  function getMatchMedia() {
11
13
  /* v8 ignore next — SSR guard: window is always defined in jsdom test env */
12
14
  if (typeof window === 'undefined')
@@ -63,7 +65,6 @@ function getClientViewport() {
63
65
  // First render happens before subscribe — detect directly.
64
66
  return detectViewport();
65
67
  }
66
- const getServerViewport = () => 'desktop';
67
68
  /**
68
69
  * The shell's viewport band, resolved SYNCHRONOUSLY on the first client
69
70
  * render.
@@ -77,10 +78,11 @@ const getServerViewport = () => 'desktop';
77
78
  *
78
79
  * `useSyncExternalStore` reads `matchMedia` during the first client render,
79
80
  * so client-rendered apps commit the correct tree once. Server rendering
80
- * still returns 'desktop' via the server snapshot, and hydration stays
81
- * mismatch-safe (React uses the server snapshot while hydrating, then
82
- * re-reads — SSR consumers keep exactly the old behavior).
81
+ * uses the optional request-derived hint, otherwise 'desktop'. Hydration uses
82
+ * that same snapshot before switching to the live viewport. A hint lets an
83
+ * app send mobile shell HTML before its JavaScript loads.
83
84
  */
84
85
  export function useShellViewport() {
85
- return useSyncExternalStore(subscribeToViewport, getClientViewport, getServerViewport);
86
+ const hint = useContext(ShellViewportHintContext);
87
+ return useSyncExternalStore(subscribeToViewport, getClientViewport, () => hint ?? 'desktop');
86
88
  }
@@ -1,4 +1,4 @@
1
- /*! Vendored from @xyflow/react@12.11.6 dist/style.css — MIT License, (c) webkid GmbH. https://github.com/xyflow/xyflow */
1
+ /*! Vendored from @xyflow/react@12.12.0 dist/style.css — MIT License, (c) webkid GmbH. https://github.com/xyflow/xyflow */
2
2
  /* this gets exported as style.css and can be used for the default theming */
3
3
  /* these are the necessary styles for React/Svelte Flow, they get used by base.css and style.css */
4
4
  .react-flow {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@medalsocial/meda",
3
- "version": "3.13.0",
3
+ "version": "3.14.0",
4
4
  "description": "Shared Meda UI shell and runtime package.",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
@@ -210,7 +210,7 @@
210
210
  "vaul": "^1.1.2"
211
211
  },
212
212
  "devDependencies": {
213
- "@biomejs/biome": "2.5.14",
213
+ "@biomejs/biome": "2.5.15",
214
214
  "@changesets/changelog-github": "1.0.1",
215
215
  "@changesets/cli": "3.0.3",
216
216
  "@dnd-kit/core": "6.3.1",
@@ -218,48 +218,48 @@
218
218
  "@dnd-kit/utilities": "3.2.2",
219
219
  "@fontsource-variable/geist": "5.3.0",
220
220
  "@fontsource-variable/geist-mono": "5.3.0",
221
- "@react-three/fiber": "9.7.0",
222
- "@size-limit/preset-small-lib": "13.0.3",
223
- "@storybook/addon-a11y": "10.6.0",
224
- "@storybook/addon-docs": "10.6.0",
225
- "@storybook/addon-mcp": "10.6.0",
226
- "@storybook/addon-themes": "10.6.0",
227
- "@storybook/addon-vitest": "10.6.0",
228
- "@storybook/react-vite": "10.6.0",
221
+ "@react-three/fiber": "9.8.1",
222
+ "@size-limit/preset-small-lib": "13.1.1",
223
+ "@storybook/addon-a11y": "10.6.1",
224
+ "@storybook/addon-docs": "10.6.1",
225
+ "@storybook/addon-mcp": "10.6.1",
226
+ "@storybook/addon-themes": "10.6.1",
227
+ "@storybook/addon-vitest": "10.6.1",
228
+ "@storybook/react-vite": "10.6.1",
229
229
  "@tailwindcss/cli": "4.3.3",
230
230
  "@tailwindcss/vite": "4.3.3",
231
- "@tanstack/intent": "0.4.0",
231
+ "@tanstack/intent": "0.5.5",
232
232
  "@testing-library/jest-dom": "7.0.1",
233
233
  "@testing-library/react": "16.3.3",
234
234
  "@types/react": "19.3.0",
235
235
  "@types/react-dom": "19.3.0",
236
236
  "@types/three": "0.185.4",
237
- "@vitejs/plugin-react": "6.1.1",
237
+ "@vitejs/plugin-react": "6.1.2",
238
238
  "@vitest/browser-playwright": "4.1.11",
239
239
  "@vitest/coverage-istanbul": "4.1.11",
240
240
  "@vitest/coverage-v8": "4.1.11",
241
- "@xyflow/react": "12.11.6",
242
- "chromatic": "18.9.4",
241
+ "@xyflow/react": "12.12.0",
242
+ "chromatic": "18.12.0",
243
243
  "esbuild": "0.28.2",
244
244
  "fast-check": "4.10.2",
245
245
  "husky": "9.1.7",
246
- "jsdom": "30.1.0",
246
+ "jsdom": "30.1.2",
247
247
  "next-themes": "0.4.6",
248
- "playwright": "1.63.0",
248
+ "playwright": "1.64.0",
249
249
  "react": "19.2.8",
250
250
  "react-dom": "19.2.8",
251
251
  "react-markdown": "10.1.0",
252
252
  "rehype-highlight": "7.0.2",
253
253
  "remark-gfm": "4.0.1",
254
- "size-limit": "13.0.3",
255
- "storybook": "10.6.0",
254
+ "size-limit": "13.1.1",
255
+ "storybook": "10.6.1",
256
256
  "tailwindcss": "4.3.3",
257
- "three": "0.185.1",
257
+ "three": "0.186.1",
258
258
  "typescript": "~6.0.3",
259
- "vite": "8.3.0",
259
+ "vite": "8.3.4",
260
260
  "vitest": "4.1.11",
261
261
  "vitest-axe": "0.1.0",
262
- "wrangler": "4.135.0"
262
+ "wrangler": "4.149.0"
263
263
  },
264
264
  "keywords": [
265
265
  "tanstack-intent"