@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.
- package/README.md +63 -17
- package/dist/auth/auth-message.d.ts +10 -0
- package/dist/auth/auth-message.js +18 -0
- package/dist/auth/auth-provider-button.d.ts +14 -0
- package/dist/auth/auth-provider-button.js +26 -0
- package/dist/auth/auth-provider-list.d.ts +6 -0
- package/dist/auth/auth-provider-list.js +7 -0
- package/dist/auth/better-auth.d.ts +40 -0
- package/dist/auth/better-auth.js +92 -0
- package/dist/auth/index.d.ts +6 -0
- package/dist/auth/index.js +3 -0
- package/dist/auth/public.d.ts +6 -0
- package/dist/auth/public.js +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/lib/render-element.d.ts +9 -0
- package/dist/lib/render-element.js +27 -0
- package/dist/recipes/next.d.ts +49 -0
- package/dist/recipes/next.js +128 -0
- package/dist/shell/app-shell-workspace.d.ts +3 -2
- package/dist/shell/app-shell-workspace.js +2 -2
- package/dist/shell/app-shell.d.ts +11 -2
- package/dist/shell/app-shell.js +14 -2
- package/dist/shell/context-rail.js +9 -3
- package/dist/shell/icon-rail.d.ts +2 -1
- package/dist/shell/icon-rail.js +8 -1
- package/dist/shell/index.d.ts +4 -1
- package/dist/shell/index.js +1 -1
- package/dist/shell/internal/mobile-drawers.js +9 -2
- package/dist/shell/primitives.d.ts +9 -0
- package/dist/shell/primitives.js +7 -0
- package/dist/shell/right-panel.d.ts +12 -1
- package/dist/shell/right-panel.js +17 -5
- package/dist/shell/shell-header.d.ts +25 -3
- package/dist/shell/shell-header.js +39 -4
- package/dist/shell/types.d.ts +47 -1
- package/dist/styles/theme.css +20 -0
- package/dist/styles/theme.test.ts +29 -0
- package/dist/theme/index.d.ts +1 -0
- package/dist/theme/index.js +1 -0
- package/dist/theme/theme-bridge.d.ts +21 -0
- package/dist/theme/theme-bridge.js +52 -0
- 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,
|
|
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
|

|
|
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
|
-
|
|
25
|
+
Use `AppShell` for the styled shell surface:
|
|
26
26
|
|
|
27
27
|
```tsx
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
28
|
+
import { AppShell, MedaShellProvider } from '@medalsocial/meda/shell';
|
|
29
|
+
import { Inbox } from 'lucide-react';
|
|
30
30
|
|
|
31
31
|
export function App() {
|
|
32
|
-
const
|
|
32
|
+
const workspace = { id: 'workspace', name: 'Workspace' };
|
|
33
|
+
const apps = [{ id: 'inbox', label: 'Inbox', icon: Inbox }];
|
|
33
34
|
|
|
34
35
|
return (
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
|
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
|
|
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,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';
|
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 = [];
|