@medalsocial/meda 1.3.0 → 1.5.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 (43) hide show
  1. package/README.md +63 -17
  2. package/dist/auth/auth-message.d.ts +10 -0
  3. package/dist/auth/auth-message.js +18 -0
  4. package/dist/auth/auth-provider-button.d.ts +14 -0
  5. package/dist/auth/auth-provider-button.js +26 -0
  6. package/dist/auth/auth-provider-list.d.ts +6 -0
  7. package/dist/auth/auth-provider-list.js +7 -0
  8. package/dist/auth/better-auth.d.ts +40 -0
  9. package/dist/auth/better-auth.js +92 -0
  10. package/dist/auth/index.d.ts +6 -0
  11. package/dist/auth/index.js +3 -0
  12. package/dist/auth/public.d.ts +6 -0
  13. package/dist/auth/public.js +3 -0
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.js +2 -0
  16. package/dist/lib/render-element.d.ts +9 -0
  17. package/dist/lib/render-element.js +27 -0
  18. package/dist/recipes/next.d.ts +49 -0
  19. package/dist/recipes/next.js +128 -0
  20. package/dist/shell/app-shell-workspace.d.ts +3 -2
  21. package/dist/shell/app-shell-workspace.js +2 -2
  22. package/dist/shell/app-shell.d.ts +11 -2
  23. package/dist/shell/app-shell.js +14 -2
  24. package/dist/shell/context-rail.js +9 -3
  25. package/dist/shell/icon-rail.d.ts +2 -1
  26. package/dist/shell/icon-rail.js +8 -1
  27. package/dist/shell/index.d.ts +4 -1
  28. package/dist/shell/index.js +1 -1
  29. package/dist/shell/internal/mobile-drawers.js +9 -2
  30. package/dist/shell/primitives.d.ts +9 -0
  31. package/dist/shell/primitives.js +7 -0
  32. package/dist/shell/right-panel.d.ts +12 -1
  33. package/dist/shell/right-panel.js +17 -5
  34. package/dist/shell/shell-header.d.ts +25 -3
  35. package/dist/shell/shell-header.js +39 -4
  36. package/dist/shell/types.d.ts +47 -1
  37. package/dist/styles/theme.css +20 -0
  38. package/dist/styles/theme.test.ts +29 -0
  39. package/dist/theme/index.d.ts +1 -0
  40. package/dist/theme/index.js +1 -0
  41. package/dist/theme/theme-bridge.d.ts +21 -0
  42. package/dist/theme/theme-bridge.js +52 -0
  43. package/package.json +21 -1
package/README.md CHANGED
@@ -1,16 +1,16 @@
1
1
  # @medalsocial/meda
2
2
 
3
- Shared UI shell and runtime primitives — the navigation chrome, panels, tab bars, command palette, and workbench layout that power Medal's apps. Published as Apache-2.0.
3
+ Shared UI shell and runtime primitives — the navigation chrome, panels, auth controls, recipes, theme bridges, command palette, and workbench layout that power Medal's apps. Published as Apache-2.0.
4
4
 
5
5
  ![npm](https://img.shields.io/npm/v/@medalsocial/meda)
6
6
 
7
7
  ## Install
8
8
 
9
9
  ```bash
10
- pnpm add @medalsocial/meda
10
+ pnpm add @medalsocial/meda lucide-react
11
11
  ```
12
12
 
13
- Peer deps: `react >= 19`, `react-dom >= 19`.
13
+ Peer deps: `react >= 19`, `react-dom >= 19`, and `lucide-react`.
14
14
 
15
15
  ## Tailwind CSS v4 setup
16
16
 
@@ -22,36 +22,81 @@ Meda ships a `styles.css` with its design tokens. Import it once in your entry s
22
22
 
23
23
  ## Usage
24
24
 
25
- `ShellStateProvider` stores panel/selection state in URL search params and is router-agnostic. You provide a `ShellSearchParamsAdapter` so it can read and update them however your router prefers. A minimal, in-memory adapter:
25
+ Use `AppShell` for the styled shell surface:
26
26
 
27
27
  ```tsx
28
- import { useState } from 'react';
29
- import { ShellStateProvider, ShellFrame } from '@medalsocial/meda';
28
+ import { AppShell, MedaShellProvider } from '@medalsocial/meda/shell';
29
+ import { Inbox } from 'lucide-react';
30
30
 
31
31
  export function App() {
32
- const [searchParams, setSearchParams] = useState(() => new URLSearchParams());
32
+ const workspace = { id: 'workspace', name: 'Workspace' };
33
+ const apps = [{ id: 'inbox', label: 'Inbox', icon: Inbox }];
33
34
 
34
35
  return (
35
- <ShellStateProvider
36
- adapter={{
37
- searchParams,
38
- setSearchParams: (updater) => setSearchParams((current) => updater(current)),
39
- }}
40
- >
41
- <ShellFrame>{/* your app */}</ShellFrame>
42
- </ShellStateProvider>
36
+ <MedaShellProvider workspace={workspace} apps={apps}>
37
+ <AppShell
38
+ variant="workspace"
39
+ iconRail={{
40
+ mainItems: [{ id: 'inbox', label: 'Inbox', icon: Inbox, to: '/inbox' }],
41
+ }}
42
+ >
43
+ {/* your app */}
44
+ </AppShell>
45
+ </MedaShellProvider>
43
46
  );
44
47
  }
45
48
  ```
46
49
 
47
- For a real app, wire the adapter to your router (e.g. TanStack Router's `useSearch`/`useNavigate`, React Router's `useSearchParams`) so URL changes persist state.
50
+ For framework routing, pass a render callback and forward Meda props:
51
+
52
+ ```tsx
53
+ import Link from 'next/link';
54
+
55
+ <AppShell
56
+ variant="workspace"
57
+ iconRail={{
58
+ mainItems,
59
+ renderLink: ({ item, linkProps }) => <Link {...linkProps} href={item.to} prefetch />,
60
+ }}
61
+ >
62
+ {children}
63
+ </AppShell>;
64
+ ```
65
+
66
+ For app-scoped brand tokens:
67
+
68
+ ```ts
69
+ import { createMedaThemeCss, defineMedaTheme } from '@medalsocial/meda/theme';
70
+
71
+ const css = createMedaThemeCss(
72
+ defineMedaTheme({
73
+ appId: 'auto',
74
+ colors: {
75
+ primary: 'var(--hb-brand-500)',
76
+ background: 'var(--hb-base-50)',
77
+ },
78
+ dark: {
79
+ colors: {
80
+ background: 'var(--hb-base-900)',
81
+ },
82
+ },
83
+ })
84
+ );
85
+ ```
86
+
87
+ Lower-level `ShellStateProvider` and layout parts remain available from `@medalsocial/meda/shell/primitives` for apps that need to own more composition.
48
88
 
49
89
  See the [demo app](./demo) for a live playground.
50
90
 
51
91
  ## Exports
52
92
 
53
93
  - `@medalsocial/meda` — curated public API (components + helpers)
54
- - `@medalsocial/meda/shell` — shell-only subpath
94
+ - `@medalsocial/meda/shell` — styled shell components and hooks
95
+ - `@medalsocial/meda/shell/primitives` — lower-level shell state and layout primitives
96
+ - `@medalsocial/meda/auth` — provider-neutral auth controls
97
+ - `@medalsocial/meda/auth/better-auth` — optional better-auth adapter
98
+ - `@medalsocial/meda/recipes/next` — copyable Next.js adoption recipe metadata
99
+ - `@medalsocial/meda/theme` — app-scoped token bridge helpers
55
100
  - `@medalsocial/meda/marketing` — marketing sections and campaign blocks
56
101
  - `@medalsocial/meda/styles.css` — design tokens + base styles
57
102
 
@@ -64,6 +109,7 @@ Prefer to copy source into your project instead of installing? The shadcn-compat
64
109
  npx shadcn add https://meda.medalsocial.com/r/meda-shell.json
65
110
  npx shadcn add https://meda.medalsocial.com/r/meda-shell-state.json
66
111
  npx shadcn add https://meda.medalsocial.com/r/meda-workbench-layout.json
112
+ npx shadcn add https://meda.medalsocial.com/r/meda-next-app-shell.json
67
113
  ```
68
114
 
69
115
  The registry index is at `https://meda.medalsocial.com/registry.json`. Source JSON files live under [`./registry`](./registry) in this repo and are deployed as static assets via Cloudflare Workers — see `wrangler.toml` and `.github/workflows/deploy-worker.yml`.
@@ -0,0 +1,10 @@
1
+ import type { HTMLAttributes, ReactNode } from 'react';
2
+ export interface AuthMessageProps extends HTMLAttributes<HTMLDivElement> {
3
+ children?: ReactNode;
4
+ }
5
+ export declare function AuthError({ children, className, ...props }: AuthMessageProps): import("react/jsx-runtime").JSX.Element | null;
6
+ export declare function AuthNotice({ children, className, ...props }: AuthMessageProps): import("react/jsx-runtime").JSX.Element | null;
7
+ export interface AuthOneTapSlotProps extends HTMLAttributes<HTMLDivElement> {
8
+ children?: ReactNode;
9
+ }
10
+ export declare function AuthOneTapSlot({ children, className, ...props }: AuthOneTapSlotProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,18 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { cn } from '../lib/utils.js';
4
+ export function AuthError({ children, className, ...props }) {
5
+ if (!children) {
6
+ return null;
7
+ }
8
+ return (_jsx("div", { role: "alert", className: cn('rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive', className), ...props, children: children }));
9
+ }
10
+ export function AuthNotice({ children, className, ...props }) {
11
+ if (!children) {
12
+ return null;
13
+ }
14
+ return (_jsx("div", { className: cn('rounded-md border border-border bg-muted/60 px-3 py-2 text-sm text-muted-foreground', className), ...props, children: children }));
15
+ }
16
+ export function AuthOneTapSlot({ children, className, ...props }) {
17
+ return (_jsx("div", { "data-meda-auth-one-tap-slot": "", className: cn('contents', className), ...props, children: children }));
18
+ }
@@ -0,0 +1,14 @@
1
+ import type { ButtonHTMLAttributes, ReactNode } from 'react';
2
+ import { type RenderElement } from '../lib/render-element.js';
3
+ export type AuthProvider = 'google' | (string & {});
4
+ export interface AuthProviderButtonProps extends Omit<ButtonHTMLAttributes<HTMLButtonElement>, 'children'> {
5
+ provider?: AuthProvider;
6
+ label: ReactNode;
7
+ icon?: ReactNode;
8
+ loading?: boolean;
9
+ loadingLabel?: ReactNode;
10
+ lastUsed?: boolean;
11
+ lastUsedLabel?: ReactNode;
12
+ render?: RenderElement<ButtonHTMLAttributes<HTMLButtonElement>>;
13
+ }
14
+ export declare function AuthProviderButton({ provider, label, icon, loading, loadingLabel, lastUsed, lastUsedLabel, render, disabled, className, type, ...props }: AuthProviderButtonProps): string | number | bigint | boolean | Iterable<ReactNode> | Promise<string | number | bigint | boolean | import("react").ReactPortal | import("react").ReactElement<unknown, string | import("react").JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | import("react/jsx-runtime").JSX.Element | null | undefined;
@@ -0,0 +1,26 @@
1
+ 'use client';
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { renderElement } from '../lib/render-element.js';
4
+ import { cn } from '../lib/utils.js';
5
+ export function AuthProviderButton({ provider, label, icon, loading = false, loadingLabel = 'Loading...', lastUsed = false, lastUsedLabel = 'Last used', render, disabled, className, type = 'button', ...props }) {
6
+ const resolvedIcon = icon ?? (provider === 'google' ? _jsx(GoogleIcon, {}) : null);
7
+ const isDisabled = disabled || loading;
8
+ const children = (_jsxs(_Fragment, { children: [resolvedIcon && (_jsx("span", { className: "flex size-5 shrink-0 items-center justify-center", "aria-hidden": "true", children: resolvedIcon })), _jsx("span", { className: "min-w-0 truncate", children: loading ? loadingLabel : label }), lastUsed && !loading && (_jsx("span", { "aria-hidden": "true", className: "ml-auto shrink-0 rounded-full bg-muted px-2 py-0.5 text-[11px] font-medium text-muted-foreground", children: lastUsedLabel }))] }));
9
+ const buttonProps = {
10
+ type,
11
+ disabled: isDisabled,
12
+ 'aria-busy': loading || undefined,
13
+ 'data-provider': provider,
14
+ 'data-last-used': lastUsed || undefined,
15
+ className: cn('relative inline-flex min-h-11 w-full items-center justify-center gap-3 rounded-md border border-border bg-background px-4 py-2.5 text-sm font-medium text-foreground shadow-sm transition-colors hover:bg-accent hover:text-accent-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-60', className),
16
+ ...props,
17
+ children,
18
+ };
19
+ if (render) {
20
+ return renderElement(render, buttonProps);
21
+ }
22
+ return _jsx("button", { ...buttonProps });
23
+ }
24
+ function GoogleIcon() {
25
+ return (_jsxs("svg", { viewBox: "0 0 24 24", className: "size-5", "aria-hidden": "true", children: [_jsx("path", { fill: "#4285F4", d: "M21.6 12.23c0-.82-.07-1.42-.22-2.05H12v3.72h5.51c-.11.92-.71 2.31-2.04 3.24l-.02.12 2.96 2.29.2.02c1.86-1.72 2.99-4.25 2.99-7.34z" }), _jsx("path", { fill: "#34A853", d: "M12 22c2.66 0 4.89-.88 6.52-2.39l-3.11-2.42c-.83.58-1.95.99-3.41.99a5.93 5.93 0 0 1-5.6-4.1l-.12.01-3.08 2.39-.04.11A9.85 9.85 0 0 0 12 22z" }), _jsx("path", { fill: "#FBBC05", d: "M6.4 14.08A6.17 6.17 0 0 1 6.07 12c0-.72.12-1.42.32-2.08l-.01-.13-3.12-2.42-.1.05A9.93 9.93 0 0 0 2.1 12c0 1.64.39 3.19 1.07 4.57l3.23-2.49z" }), _jsx("path", { fill: "#EA4335", d: "M12 5.82c1.85 0 3.1.8 3.81 1.47l2.78-2.72C16.88 2.98 14.66 2 12 2a9.85 9.85 0 0 0-8.84 5.42l3.22 2.5A5.96 5.96 0 0 1 12 5.82z" })] }));
26
+ }
@@ -0,0 +1,6 @@
1
+ import { type HTMLAttributes, type ReactNode } from 'react';
2
+ export interface AuthProviderListProps extends HTMLAttributes<HTMLUListElement> {
3
+ children: ReactNode;
4
+ label?: string;
5
+ }
6
+ export declare function AuthProviderList({ children, className, label, ...props }: AuthProviderListProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,7 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { Children, isValidElement } from 'react';
4
+ import { cn } from '../lib/utils.js';
5
+ export function AuthProviderList({ children, className, label = 'Authentication providers', ...props }) {
6
+ return (_jsx("ul", { "aria-label": label, className: cn('flex w-full flex-col gap-3', className), ...props, children: Children.toArray(children).map((child, index) => (_jsx("li", { className: "contents", children: child }, isValidElement(child) && child.key != null ? child.key : index))) }));
7
+ }
@@ -0,0 +1,40 @@
1
+ import { type AuthProviderButtonProps } from './auth-provider-button.js';
2
+ export interface BetterAuthSocialArgs {
3
+ provider: string;
4
+ callbackURL?: string;
5
+ errorCallbackURL?: string;
6
+ }
7
+ export interface BetterAuthOneTapArgs {
8
+ callbackURL?: string;
9
+ }
10
+ export interface BetterAuthClientLike {
11
+ signIn?: {
12
+ social?: (args: BetterAuthSocialArgs) => Promise<unknown> | unknown;
13
+ };
14
+ oneTap?: (args: BetterAuthOneTapArgs) => Promise<unknown> | unknown;
15
+ getLastUsedLoginMethod?: () => Promise<string | null | undefined> | string | null | undefined;
16
+ }
17
+ export interface BetterAuthProviderButtonProps extends Omit<AuthProviderButtonProps, 'onClick' | 'provider'> {
18
+ authClient: BetterAuthClientLike;
19
+ provider?: string;
20
+ callbackURL?: string;
21
+ errorCallbackURL?: string;
22
+ onPendingChange?: (pending: boolean) => void;
23
+ onError?: (error: unknown) => void;
24
+ onSuccess?: (result: unknown) => void;
25
+ }
26
+ export declare function BetterAuthProviderButton({ authClient, provider, callbackURL, errorCallbackURL, onPendingChange, onError, onSuccess, loading, ...props }: BetterAuthProviderButtonProps): import("react/jsx-runtime").JSX.Element;
27
+ export declare function useBetterAuthLastLoginMethod(authClient: BetterAuthClientLike): string | null;
28
+ export interface BetterAuthOneTapProps {
29
+ authClient: BetterAuthClientLike;
30
+ enabled?: boolean;
31
+ callbackURL?: string;
32
+ onError?: (error: unknown) => void;
33
+ onSuccess?: (result: unknown) => void;
34
+ }
35
+ export declare function BetterAuthOneTap({ authClient, enabled, callbackURL, onError, onSuccess, }: BetterAuthOneTapProps): null;
36
+ export declare function createBetterAuthAdapter(authClient: BetterAuthClientLike): {
37
+ BetterAuthProviderButton: (props: Omit<BetterAuthProviderButtonProps, "authClient">) => import("react/jsx-runtime").JSX.Element;
38
+ BetterAuthOneTap: (props: Omit<BetterAuthOneTapProps, "authClient">) => import("react/jsx-runtime").JSX.Element;
39
+ useBetterAuthLastLoginMethod: () => string | null;
40
+ };
@@ -0,0 +1,92 @@
1
+ 'use client';
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useState } from 'react';
4
+ import { AuthProviderButton } from './auth-provider-button.js';
5
+ export function BetterAuthProviderButton({ authClient, provider = 'google', callbackURL, errorCallbackURL, onPendingChange, onError, onSuccess, loading, ...props }) {
6
+ const [internalPending, setInternalPending] = useState(false);
7
+ const pending = Boolean(loading || internalPending);
8
+ return (_jsx(AuthProviderButton, { ...props, provider: provider, loading: pending, onClick: async () => {
9
+ const social = authClient.signIn?.social;
10
+ if (!social) {
11
+ onError?.(new Error('better-auth social sign-in is unavailable.'));
12
+ return;
13
+ }
14
+ setInternalPending(true);
15
+ onPendingChange?.(true);
16
+ try {
17
+ const result = await social({ provider, callbackURL, errorCallbackURL });
18
+ onSuccess?.(result);
19
+ }
20
+ catch (error) {
21
+ onError?.(error);
22
+ }
23
+ finally {
24
+ setInternalPending(false);
25
+ onPendingChange?.(false);
26
+ }
27
+ } }));
28
+ }
29
+ export function useBetterAuthLastLoginMethod(authClient) {
30
+ const [method, setMethod] = useState(null);
31
+ useEffect(() => {
32
+ let active = true;
33
+ async function loadLastMethod() {
34
+ try {
35
+ const getter = authClient.getLastUsedLoginMethod;
36
+ const nextMethod = getter ? await getter() : null;
37
+ if (active) {
38
+ setMethod(nextMethod ?? null);
39
+ }
40
+ }
41
+ catch {
42
+ if (active) {
43
+ setMethod(null);
44
+ }
45
+ }
46
+ }
47
+ void loadLastMethod();
48
+ return () => {
49
+ active = false;
50
+ };
51
+ }, [authClient]);
52
+ return method;
53
+ }
54
+ export function BetterAuthOneTap({ authClient, enabled = true, callbackURL, onError, onSuccess, }) {
55
+ const onErrorRef = useRef(onError);
56
+ const onSuccessRef = useRef(onSuccess);
57
+ useEffect(() => {
58
+ onErrorRef.current = onError;
59
+ onSuccessRef.current = onSuccess;
60
+ }, [onError, onSuccess]);
61
+ useEffect(() => {
62
+ if (!enabled || !authClient.oneTap) {
63
+ return;
64
+ }
65
+ let active = true;
66
+ async function mountOneTap() {
67
+ try {
68
+ const result = await authClient.oneTap?.({ callbackURL });
69
+ if (active) {
70
+ onSuccessRef.current?.(result);
71
+ }
72
+ }
73
+ catch (error) {
74
+ if (active) {
75
+ onErrorRef.current?.(error);
76
+ }
77
+ }
78
+ }
79
+ void mountOneTap();
80
+ return () => {
81
+ active = false;
82
+ };
83
+ }, [authClient, callbackURL, enabled]);
84
+ return null;
85
+ }
86
+ export function createBetterAuthAdapter(authClient) {
87
+ return {
88
+ BetterAuthProviderButton: (props) => (_jsx(BetterAuthProviderButton, { authClient: authClient, ...props })),
89
+ BetterAuthOneTap: (props) => (_jsx(BetterAuthOneTap, { authClient: authClient, ...props })),
90
+ useBetterAuthLastLoginMethod: () => useBetterAuthLastLoginMethod(authClient),
91
+ };
92
+ }
@@ -0,0 +1,6 @@
1
+ export type { AuthMessageProps, AuthOneTapSlotProps } from './auth-message.js';
2
+ export { AuthError, AuthNotice, AuthOneTapSlot } from './auth-message.js';
3
+ export type { AuthProvider, AuthProviderButtonProps } from './auth-provider-button.js';
4
+ export { AuthProviderButton } from './auth-provider-button.js';
5
+ export type { AuthProviderListProps } from './auth-provider-list.js';
6
+ export { AuthProviderList } from './auth-provider-list.js';
@@ -0,0 +1,3 @@
1
+ export { AuthError, AuthNotice, AuthOneTapSlot } from './auth-message.js';
2
+ export { AuthProviderButton } from './auth-provider-button.js';
3
+ export { AuthProviderList } from './auth-provider-list.js';
@@ -0,0 +1,6 @@
1
+ export type { AuthMessageProps, AuthOneTapSlotProps } from './auth-message.js';
2
+ export { AuthError, AuthNotice, AuthOneTapSlot } from './auth-message.js';
3
+ export type { AuthProvider, AuthProviderButtonProps } from './auth-provider-button.js';
4
+ export { AuthProviderButton } from './auth-provider-button.js';
5
+ export type { AuthProviderListProps } from './auth-provider-list.js';
6
+ export { AuthProviderList } from './auth-provider-list.js';
@@ -0,0 +1,3 @@
1
+ export { AuthError, AuthNotice, AuthOneTapSlot } from './auth-message.js';
2
+ export { AuthProviderButton } from './auth-provider-button.js';
3
+ export { AuthProviderList } from './auth-provider-list.js';
package/dist/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
+ export * from './auth/public.js';
1
2
  export * from './brand/public.js';
2
3
  export * from './chat/public.js';
3
4
  export * from './marketing/public.js';
4
5
  export * from './panel/public.js';
5
6
  export * from './shell/index.js';
7
+ export * from './theme/index.js';
6
8
  export * from './timeline/public.js';
package/dist/index.js CHANGED
@@ -1,6 +1,8 @@
1
+ export * from './auth/public.js';
1
2
  export * from './brand/public.js';
2
3
  export * from './chat/public.js';
3
4
  export * from './marketing/public.js';
4
5
  export * from './panel/public.js';
5
6
  export * from './shell/index.js'; // v2 surface
7
+ export * from './theme/index.js';
6
8
  export * from './timeline/public.js';
@@ -0,0 +1,9 @@
1
+ import { type ReactElement, type ReactNode } from 'react';
2
+ export type RenderElement<TProps extends {
3
+ className?: string;
4
+ children?: ReactNode;
5
+ }> = ReactElement<Partial<TProps>>;
6
+ export declare function renderElement<TProps extends {
7
+ className?: string;
8
+ children?: ReactNode;
9
+ }>(render: RenderElement<TProps>, props: TProps): ReactNode;
@@ -0,0 +1,27 @@
1
+ import { cloneElement, isValidElement, } from 'react';
2
+ import { cn } from './utils.js';
3
+ function composeEventHandlers(consumerHandler, medaHandler) {
4
+ if (!consumerHandler)
5
+ return medaHandler;
6
+ if (!medaHandler)
7
+ return consumerHandler;
8
+ return (event) => {
9
+ consumerHandler(event);
10
+ if (!event.defaultPrevented) {
11
+ medaHandler(event);
12
+ }
13
+ };
14
+ }
15
+ export function renderElement(render, props) {
16
+ if (!isValidElement(render))
17
+ return null;
18
+ const renderProps = render.props;
19
+ const medaProps = props;
20
+ return cloneElement(render, {
21
+ ...renderProps,
22
+ ...medaProps,
23
+ className: cn(renderProps.className, medaProps.className),
24
+ onClick: composeEventHandlers(renderProps.onClick, medaProps.onClick),
25
+ onKeyDown: composeEventHandlers(renderProps.onKeyDown, medaProps.onKeyDown),
26
+ });
27
+ }
@@ -0,0 +1,49 @@
1
+ export interface MedaRecipeFile {
2
+ path: string;
3
+ target: string;
4
+ type: 'registry:block' | 'registry:component' | 'registry:hook' | 'registry:lib';
5
+ content: string;
6
+ }
7
+ export interface MedaRecipe {
8
+ name: string;
9
+ title: string;
10
+ description: string;
11
+ dependencies: string[];
12
+ peerDependencies: string[];
13
+ cssVars: string[];
14
+ files: MedaRecipeFile[];
15
+ accessibility: string[];
16
+ composition: string[];
17
+ }
18
+ export declare const nextAppShellRecipe: {
19
+ name: string;
20
+ title: string;
21
+ description: string;
22
+ dependencies: string[];
23
+ peerDependencies: string[];
24
+ cssVars: string[];
25
+ files: {
26
+ path: string;
27
+ target: string;
28
+ type: "registry:block";
29
+ content: string;
30
+ }[];
31
+ accessibility: string[];
32
+ composition: string[];
33
+ };
34
+ export declare const nextRecipes: {
35
+ name: string;
36
+ title: string;
37
+ description: string;
38
+ dependencies: string[];
39
+ peerDependencies: string[];
40
+ cssVars: string[];
41
+ files: {
42
+ path: string;
43
+ target: string;
44
+ type: "registry:block";
45
+ content: string;
46
+ }[];
47
+ accessibility: string[];
48
+ composition: string[];
49
+ }[];
@@ -0,0 +1,128 @@
1
+ export const nextAppShellRecipe = {
2
+ name: 'meda-next-app-shell',
3
+ title: 'Meda Next AppShell',
4
+ description: 'Copyable Next.js App Router shell adapter with next/link routing, route-owned panel views, and auth controls.',
5
+ dependencies: ['@medalsocial/meda', 'lucide-react'],
6
+ peerDependencies: ['next', 'react', 'react-dom'],
7
+ cssVars: ['@medalsocial/meda/styles.css'],
8
+ files: [
9
+ {
10
+ path: 'registry/meda/meda-next-app-shell/meda-next-app-shell.tsx',
11
+ target: 'components/meda/meda-next-app-shell.tsx',
12
+ type: 'registry:block',
13
+ content: `\
14
+ 'use client'
15
+
16
+ import Link from 'next/link'
17
+ import type { ComponentProps, ReactNode } from 'react'
18
+ import {
19
+ AppShell,
20
+ AuthError,
21
+ AuthNotice,
22
+ AuthOneTapSlot,
23
+ AuthProviderButton,
24
+ AuthProviderList,
25
+ MedaShellProvider,
26
+ PanelViewsProvider,
27
+ type AppDefinition,
28
+ type IconRailItem,
29
+ type PanelView,
30
+ type WorkspaceDefinition,
31
+ } from '@medalsocial/meda/shell'
32
+
33
+ export function MedaNextWorkspaceShell({
34
+ workspace,
35
+ apps,
36
+ iconItems,
37
+ activeIconId,
38
+ panelViews = [],
39
+ defaultPanelView,
40
+ children,
41
+ }: {
42
+ workspace: WorkspaceDefinition
43
+ apps: AppDefinition[]
44
+ iconItems: IconRailItem[]
45
+ activeIconId?: string
46
+ panelViews?: PanelView[]
47
+ defaultPanelView?: string
48
+ children: ReactNode
49
+ }) {
50
+ return (
51
+ <MedaShellProvider workspace={workspace} apps={apps}>
52
+ <AppShell
53
+ variant="workspace"
54
+ iconRail={{
55
+ mainItems: iconItems,
56
+ activeId: activeIconId,
57
+ renderLink: ({ item, linkProps }) => (
58
+ <Link {...linkProps} href={item.to} prefetch />
59
+ ),
60
+ }}
61
+ rightPanel={{ panelViews, defaultView: defaultPanelView }}
62
+ >
63
+ <PanelViewsProvider views={panelViews} defaultView={defaultPanelView}>
64
+ {children}
65
+ </PanelViewsProvider>
66
+ </AppShell>
67
+ </MedaShellProvider>
68
+ )
69
+ }
70
+
71
+ export function MedaNextAuthShell({
72
+ brandName,
73
+ brandMark,
74
+ appName,
75
+ tagline,
76
+ preview,
77
+ error,
78
+ notice,
79
+ onGoogleSignIn,
80
+ }: {
81
+ brandName: ReactNode
82
+ brandMark?: ReactNode
83
+ appName?: ReactNode
84
+ tagline?: ReactNode
85
+ preview?: ReactNode
86
+ error?: ReactNode
87
+ notice?: ReactNode
88
+ onGoogleSignIn: ComponentProps<typeof AuthProviderButton>['onClick']
89
+ }) {
90
+ return (
91
+ <MedaShellProvider workspace={{ id: 'auth', name: 'Auth', icon: brandMark }} apps={[]}>
92
+ <AppShell
93
+ variant="auth"
94
+ branding={{ brandName, brandMark, appName, tagline }}
95
+ preview={preview}
96
+ >
97
+ <AuthProviderList>
98
+ <AuthProviderButton
99
+ provider="google"
100
+ label="Continue with Google"
101
+ onClick={onGoogleSignIn}
102
+ />
103
+ </AuthProviderList>
104
+ <AuthNotice>{notice}</AuthNotice>
105
+ <AuthError>{error}</AuthError>
106
+ <AuthOneTapSlot />
107
+ </AppShell>
108
+ </MedaShellProvider>
109
+ )
110
+ }
111
+ `,
112
+ },
113
+ ],
114
+ accessibility: [
115
+ 'Every drawer and panel keeps its accessible name from AppShell and RightPanel.',
116
+ 'Custom link renderers must forward all linkProps to preserve aria-current, labels, handlers, and className.',
117
+ 'Auth provider buttons keep the visible provider affordance separate from the accessible button name.',
118
+ 'Route-owned panel views should expose headings inside their rendered panel content.',
119
+ 'Reduced-motion behavior remains delegated to Meda shell motion tokens.',
120
+ ],
121
+ composition: [
122
+ 'MedaShellProvider owns workspace and app context for the copied shell adapter.',
123
+ 'AppShell receives route-owned rightPanel views on first render to avoid delayed panel UI.',
124
+ 'PanelViewsProvider wraps children with the same panelViews and defaultPanelView for nested route registrations.',
125
+ 'renderLink composes Next Link by forwarding Meda linkProps before setting framework-specific props.',
126
+ ],
127
+ };
128
+ export const nextRecipes = [nextAppShellRecipe];
@@ -1,10 +1,11 @@
1
1
  import type { ReactNode } from 'react';
2
- import type { AppShellContextRailConfig, AppShellIconRailConfig, AppShellRightPanelConfig } from './types.js';
2
+ import type { AppShellContextRailConfig, AppShellIconRailConfig, AppShellRightPanelConfig, AppShellWorkspaceConfig } from './types.js';
3
3
  export interface AppShellWorkspaceProps {
4
4
  iconRail?: AppShellIconRailConfig;
5
5
  contextRail?: AppShellContextRailConfig;
6
6
  rightPanel?: AppShellRightPanelConfig;
7
+ workspace?: AppShellWorkspaceConfig;
7
8
  globalActions?: ReactNode;
8
9
  children: ReactNode;
9
10
  }
10
- export declare function AppShellWorkspace({ iconRail, contextRail, rightPanel, globalActions, children, }: AppShellWorkspaceProps): import("react/jsx-runtime").JSX.Element;
11
+ export declare function AppShellWorkspace({ iconRail, contextRail, rightPanel, workspace, globalActions, children, }: AppShellWorkspaceProps): import("react/jsx-runtime").JSX.Element;
@@ -12,7 +12,7 @@ import { ShellHeader } from './shell-header.js';
12
12
  import { ShellMain } from './shell-main.js';
13
13
  import { useShellViewport } from './use-shell-viewport.js';
14
14
  const EMPTY_PANEL_VIEWS = [];
15
- export function AppShellWorkspace({ iconRail, contextRail, rightPanel, globalActions, children, }) {
15
+ export function AppShellWorkspace({ iconRail, contextRail, rightPanel, workspace, globalActions, children, }) {
16
16
  const viewport = useShellViewport();
17
17
  const isMobile = viewport === 'mobile';
18
18
  const staticPanelViews = rightPanel?.panelViews ?? EMPTY_PANEL_VIEWS;
@@ -37,7 +37,7 @@ export function AppShellWorkspace({ iconRail, contextRail, rightPanel, globalAct
37
37
  // rendered without an explicit-height ancestor (tests, direct imports). The
38
38
  // <AppShell> wrapper already enforces h-screen for the workspace variant, so
39
39
  // nested viewport-height divs collapse cleanly — no double-scroll.
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 }))] }));
40
+ return (_jsxs("div", { className: "flex h-screen flex-col", children: [isMobile ? (_jsx(MobileHeader, { globalActions: globalActions })) : (_jsx(ShellHeader, { globalActions: globalActions, workspaceMenuItems: workspace?.menuItems, workspaceMenuFooter: workspace?.menuFooter })), _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 }))] }));
41
41
  }
42
42
  function buildMobileNavItems(iconRail, contextRail, panelViews) {
43
43
  const items = [];