@medalsocial/meda 1.3.0 → 1.4.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.d.ts +5 -2
- package/dist/shell/app-shell.js +13 -1
- 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/types.d.ts +8 -1
- 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,12 +1,15 @@
|
|
|
1
1
|
import type { ReactNode } from 'react';
|
|
2
|
-
import type { AppShellAuthConfig, AppShellContextRailConfig, AppShellIconRailConfig, AppShellRightPanelConfig } from './types.js';
|
|
2
|
+
import type { AppShellAuthBranding, AppShellAuthConfig, AppShellContextRailConfig, AppShellIconRailConfig, AppShellRightPanelConfig } from './types.js';
|
|
3
3
|
interface AppShellBaseProps {
|
|
4
4
|
children: ReactNode;
|
|
5
5
|
className?: string;
|
|
6
6
|
}
|
|
7
7
|
export type AppShellProps = AppShellBaseProps & ({
|
|
8
8
|
variant: 'auth';
|
|
9
|
-
auth
|
|
9
|
+
auth?: AppShellAuthConfig;
|
|
10
|
+
branding?: AppShellAuthBranding;
|
|
11
|
+
preview?: ReactNode;
|
|
12
|
+
actions?: ReactNode;
|
|
10
13
|
} | {
|
|
11
14
|
variant: 'workspace';
|
|
12
15
|
iconRail?: AppShellIconRailConfig;
|
package/dist/shell/app-shell.js
CHANGED
|
@@ -14,13 +14,25 @@ export function AppShell(props) {
|
|
|
14
14
|
const wrapper = (content) => (_jsx("div", { "data-meda-app": activeAppId, "data-meda-workspace": workspace.id, "data-meda-variant": props.variant, className: cn(heightClass, 'bg-background text-foreground', props.className), children: content }));
|
|
15
15
|
switch (props.variant) {
|
|
16
16
|
case 'auth':
|
|
17
|
-
return wrapper(_jsx(AppShellAuth, { ...props
|
|
17
|
+
return wrapper(_jsx(AppShellAuth, { ...resolveAuthConfig(props), children: props.children }));
|
|
18
18
|
case 'workspace':
|
|
19
19
|
return wrapper(_jsx(AppShellWorkspace, { iconRail: props.iconRail, contextRail: props.contextRail, rightPanel: props.rightPanel, globalActions: props.globalActions, children: props.children }));
|
|
20
20
|
case 'chat':
|
|
21
21
|
return wrapper(_jsx(AppShellChat, { globalActions: props.globalActions, children: props.children }));
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
|
+
function resolveAuthConfig(props) {
|
|
25
|
+
const { auth, branding } = props;
|
|
26
|
+
return {
|
|
27
|
+
title: auth?.title ?? branding?.appName ?? branding?.brandName ?? 'Sign in',
|
|
28
|
+
description: auth?.description ?? branding?.tagline,
|
|
29
|
+
brandName: auth?.brandName ?? branding?.brandName,
|
|
30
|
+
brandMark: auth?.brandMark ?? branding?.brandMark,
|
|
31
|
+
eyebrow: auth?.eyebrow,
|
|
32
|
+
preview: auth?.preview ?? props.preview,
|
|
33
|
+
actions: auth?.actions ?? props.actions,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
24
36
|
export function AppShellBody({ children, className }) {
|
|
25
37
|
return (_jsx("div", { className: cn('relative flex h-[calc(100vh-var(--shell-header-height))] overflow-hidden', className), children: children }));
|
|
26
38
|
}
|
|
@@ -15,7 +15,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
15
15
|
* once <AppShellBody> ships as a ResizableShell Group.
|
|
16
16
|
*/
|
|
17
17
|
import { PanelLeftClose, PanelLeftOpen } from 'lucide-react';
|
|
18
|
-
import { useId, useRef, useState } from 'react';
|
|
18
|
+
import { Fragment, useId, useRef, useState } from 'react';
|
|
19
19
|
import { cn } from '../lib/utils.js';
|
|
20
20
|
import { useMedaShell } from './shell-provider.js';
|
|
21
21
|
import { useShellViewport } from './use-shell-viewport.js';
|
|
@@ -120,9 +120,15 @@ export function ContextRail({ appId, module, hidden = false, collapsible = true,
|
|
|
120
120
|
: 'text-muted-foreground hover:bg-accent hover:text-foreground');
|
|
121
121
|
const IconComp = item.icon;
|
|
122
122
|
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 }))] }));
|
|
123
|
+
const linkProps = {
|
|
124
|
+
href: item.to,
|
|
125
|
+
'aria-current': isActive ? 'page' : undefined,
|
|
126
|
+
className: klass,
|
|
127
|
+
children: inner,
|
|
128
|
+
};
|
|
123
129
|
if (renderLink) {
|
|
124
|
-
return renderLink({ item, isActive, className: klass, children: inner });
|
|
130
|
+
return (_jsx(Fragment, { children: renderLink({ item, isActive, className: klass, children: inner, linkProps }) }, item.id));
|
|
125
131
|
}
|
|
126
|
-
return
|
|
132
|
+
return _jsx("a", { ...linkProps }, item.id);
|
|
127
133
|
}) })), module.render?.({ workspaceId: ctx.workspace.id, appId })] }), !collapsed && (_jsx(ResizeHandle, { currentWidth: width, onResize: handleResize, onCommit: handleCommit }))] }));
|
|
128
134
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LucideIcon } from 'lucide-react';
|
|
2
|
-
import type { ReactNode } from 'react';
|
|
2
|
+
import type { AnchorHTMLAttributes, ReactNode } from 'react';
|
|
3
3
|
export interface IconRailItem {
|
|
4
4
|
id: string;
|
|
5
5
|
label: string;
|
|
@@ -12,6 +12,7 @@ export interface IconRailRenderLinkArgs {
|
|
|
12
12
|
isActive: boolean;
|
|
13
13
|
className: string;
|
|
14
14
|
children: ReactNode;
|
|
15
|
+
linkProps: AnchorHTMLAttributes<HTMLAnchorElement>;
|
|
15
16
|
}
|
|
16
17
|
export interface IconRailProps {
|
|
17
18
|
mainItems: IconRailItem[];
|
package/dist/shell/icon-rail.js
CHANGED
|
@@ -29,9 +29,16 @@ export function IconRail({ mainItems, utilityItems = [], footer, activeId, rende
|
|
|
29
29
|
const klass = itemClass(isActive);
|
|
30
30
|
const IconComp = item.icon;
|
|
31
31
|
const inner = (_jsxs(_Fragment, { children: [_jsx(IconComp, { size: 22, "aria-hidden": "true" }), item.badge ? _jsx("span", { className: "absolute right-1 top-1", children: item.badge }) : null] }));
|
|
32
|
+
const linkProps = {
|
|
33
|
+
href: item.to,
|
|
34
|
+
'aria-label': item.label,
|
|
35
|
+
'aria-current': isActive ? 'page' : undefined,
|
|
36
|
+
className: 'contents',
|
|
37
|
+
children: inner,
|
|
38
|
+
};
|
|
32
39
|
const linkContent = renderLink ? (
|
|
33
40
|
// renderLink consumers receive the className so they can apply it themselves
|
|
34
|
-
renderLink({ item, isActive, className: klass, children: inner })) : (_jsx("a", {
|
|
41
|
+
renderLink({ item, isActive, className: klass, children: inner, linkProps })) : (_jsx("a", { ...linkProps }));
|
|
35
42
|
return (_jsxs(Tooltip, { children: [_jsx(TooltipTrigger, { render: _jsx("span", { "data-testid": `icon-rail-trigger-${item.id}`, className: klass, children: linkContent }) }), _jsx(TooltipContent, { side: "right", children: item.label })] }, item.id));
|
|
36
43
|
};
|
|
37
44
|
return (_jsx(TooltipProvider, { children: _jsxs("nav", { "data-testid": "icon-rail", "aria-label": "Primary", className: cn('flex h-full w-[var(--shell-rail-width)] shrink-0 flex-col items-center bg-shell-rail py-3.5', className), children: [_jsx("div", { className: "flex flex-col items-center gap-1", children: mainItems.map(renderItem) }), utilityItems.length > 0 && (_jsxs(_Fragment, { children: [_jsx(RailDivider, { pinnedBottom: pinnedBottom, onToggle: () => setPinnedBottom((prev) => !prev) }), _jsx("div", { "data-testid": "utility-items-wrapper", className: cn('flex flex-col items-center gap-1', pinnedBottom && 'mt-auto'), children: utilityItems.map(renderItem) })] })), footer && (_jsx("div", { className: cn('pt-3', pinnedBottom || utilityItems.length === 0 ? 'mt-auto' : ''), children: footer }))] }) }));
|
package/dist/shell/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
export type { AuthMessageProps, AuthOneTapSlotProps, AuthProvider, AuthProviderButtonProps, AuthProviderListProps, } from '../auth/index.js';
|
|
2
|
+
export { AuthError, AuthNotice, AuthOneTapSlot, AuthProviderButton, AuthProviderList, } from '../auth/index.js';
|
|
1
3
|
export { AppShell, AppShellBody } from './app-shell.js';
|
|
2
4
|
export type { CommandGroupDefinition } from './command-palette.js';
|
|
3
5
|
export { CommandPalette, useCommandGroup, useCommands } from './command-palette.js';
|
|
@@ -5,6 +7,7 @@ export { ContextRail } from './context-rail.js';
|
|
|
5
7
|
export type { DragModeBannerProps } from './drag-mode-banner.js';
|
|
6
8
|
export { DragModeBanner } from './drag-mode-banner.js';
|
|
7
9
|
export * as Extras from './extras/index.js';
|
|
10
|
+
export type { IconRailItem, IconRailProps, IconRailRenderLinkArgs } from './icon-rail.js';
|
|
8
11
|
export { IconRail, RailDivider } from './icon-rail.js';
|
|
9
12
|
export type { ShellStorageAdapter } from './layout-state.js';
|
|
10
13
|
export { createLocalStorageAdapter } from './layout-state.js';
|
|
@@ -23,5 +26,5 @@ export type { MedaShellProviderProps } from './shell-provider.js';
|
|
|
23
26
|
export { MedaShellProvider, useMedaShell, useShellSelection } from './shell-provider.js';
|
|
24
27
|
export { DefaultThemeProvider, ThemeToggle, useTheme } from './theme.js';
|
|
25
28
|
export { NextThemesAdapter } from './theme-next-themes.js';
|
|
26
|
-
export type { AppDefinition, AppShellAuthConfig, AppShellContextRailConfig, AppShellIconRailConfig, AppShellRightPanelConfig, AppShellVariant, CommandDefinition, ContextItem, ContextModule, MobileBottomNavItem, PanelMode, PanelView, ShellLinkRenderArgs, ShellMainLayout, ShellRenderContext, ShellViewport, ThemeAdapter, WorkspaceDefinition, } from './types.js';
|
|
29
|
+
export type { AppDefinition, AppShellAuthBranding, AppShellAuthConfig, AppShellContextRailConfig, AppShellIconRailConfig, AppShellRightPanelConfig, AppShellVariant, CommandDefinition, ContextItem, ContextModule, MobileBottomNavItem, PanelMode, PanelView, ShellLinkRenderArgs, ShellMainLayout, ShellRenderContext, ShellViewport, ThemeAdapter, WorkspaceDefinition, } from './types.js';
|
|
27
30
|
export { useShellViewport } from './use-shell-viewport.js';
|
package/dist/shell/index.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
// the directive (see test/nextjs-consumer.test.ts). Each underlying component
|
|
4
4
|
// file carries its own 'use client' — Next traces through the barrel and
|
|
5
5
|
// applies them per-component. The barrel itself is just a re-exporter.
|
|
6
|
+
export { AuthError, AuthNotice, AuthOneTapSlot, AuthProviderButton, AuthProviderList, } from '../auth/index.js';
|
|
6
7
|
// Layout
|
|
7
8
|
export { AppShell, AppShellBody } from './app-shell.js';
|
|
8
9
|
// Command palette
|
|
@@ -12,7 +13,6 @@ export { ContextRail } from './context-rail.js';
|
|
|
12
13
|
export { DragModeBanner } from './drag-mode-banner.js';
|
|
13
14
|
// Extras (legacy components ported during Phase 15 — opt-in for apps that need them)
|
|
14
15
|
export * as Extras from './extras/index.js';
|
|
15
|
-
// Rails + main + panel
|
|
16
16
|
export { IconRail, RailDivider } from './icon-rail.js';
|
|
17
17
|
// Storage adapter (consumers may want to provide their own)
|
|
18
18
|
export { createLocalStorageAdapter } from './layout-state.js';
|
|
@@ -34,15 +34,22 @@ const menuItemClassName = 'flex items-center gap-2 rounded-md px-3 py-2 text-sm
|
|
|
34
34
|
function MenuDrawerItem({ item, isActive, onClose, renderLink, }) {
|
|
35
35
|
const Icon = item.icon;
|
|
36
36
|
const children = (_jsxs(_Fragment, { children: [_jsx(Icon, { size: 18, "aria-hidden": "true" }), _jsx("span", { children: item.label })] }));
|
|
37
|
+
const className = cn(menuItemClassName, isActive && 'bg-accent text-foreground');
|
|
38
|
+
const linkProps = {
|
|
39
|
+
href: item.to,
|
|
40
|
+
className,
|
|
41
|
+
children,
|
|
42
|
+
};
|
|
37
43
|
if (renderLink) {
|
|
38
44
|
return closeAfterLinkClick(renderLink({
|
|
39
45
|
item,
|
|
40
46
|
isActive,
|
|
41
|
-
className
|
|
47
|
+
className,
|
|
42
48
|
children,
|
|
49
|
+
linkProps,
|
|
43
50
|
}), onClose);
|
|
44
51
|
}
|
|
45
|
-
return (_jsx("a", {
|
|
52
|
+
return closeAfterLinkClick(_jsx("a", { ...linkProps }), onClose);
|
|
46
53
|
}
|
|
47
54
|
function closeAfterLinkClick(link, onClose) {
|
|
48
55
|
if (!isValidElement(link)) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export type { ShellStorageAdapter } from './layout-state.js';
|
|
2
|
+
export { createLocalStorageAdapter } from './layout-state.js';
|
|
3
|
+
export type { PanelViewsProviderProps } from './panel-views-provider.js';
|
|
4
|
+
export { PanelViewsProvider } from './panel-views-provider.js';
|
|
5
|
+
export { ResizableHandle, ResizableShell, ResizableShellPanel } from './resizable-shell.js';
|
|
6
|
+
export type { MedaShellProviderProps } from './shell-provider.js';
|
|
7
|
+
export { MedaShellProvider, useMedaShell, useShellSelection } from './shell-provider.js';
|
|
8
|
+
export type { AppDefinition, CommandDefinition, ContextItem, ContextModule, MobileBottomNavItem, PanelMode, PanelView, ShellLinkRenderArgs, ShellRenderContext, ShellViewport, ThemeAdapter, WorkspaceDefinition, } from './types.js';
|
|
9
|
+
export { useShellViewport } from './use-shell-viewport.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// State and layout primitives for consumers that want Meda shell behavior
|
|
2
|
+
// without importing the full styled shell barrel.
|
|
3
|
+
export { createLocalStorageAdapter } from './layout-state.js';
|
|
4
|
+
export { PanelViewsProvider } from './panel-views-provider.js';
|
|
5
|
+
export { ResizableHandle, ResizableShell, ResizableShellPanel } from './resizable-shell.js';
|
|
6
|
+
export { MedaShellProvider, useMedaShell, useShellSelection } from './shell-provider.js';
|
|
7
|
+
export { useShellViewport } from './use-shell-viewport.js';
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ButtonHTMLAttributes, ReactNode } from 'react';
|
|
1
2
|
import type { PanelMode, PanelView } from './types.js';
|
|
2
3
|
export interface RightPanelProps {
|
|
3
4
|
/** Views to render as tabs in the panel header. */
|
|
@@ -10,6 +11,16 @@ export interface RightPanelProps {
|
|
|
10
11
|
* If only ['panel'], the cycle button is hidden.
|
|
11
12
|
*/
|
|
12
13
|
modes?: PanelMode[];
|
|
14
|
+
renderTab?: (args: RightPanelTabRenderArgs) => ReactNode;
|
|
13
15
|
className?: string;
|
|
14
16
|
}
|
|
15
|
-
export
|
|
17
|
+
export interface RightPanelTabRenderArgs {
|
|
18
|
+
view: PanelView;
|
|
19
|
+
isActive: boolean;
|
|
20
|
+
buttonProps: RightPanelTabButtonProps;
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
}
|
|
23
|
+
export type RightPanelTabButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
24
|
+
'data-active'?: boolean;
|
|
25
|
+
};
|
|
26
|
+
export declare function RightPanel({ panelViews, defaultView, modes, renderTab, className, }: RightPanelProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
'use client';
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
/**
|
|
4
4
|
* RightPanel — spec §12
|
|
5
5
|
*
|
|
@@ -16,7 +16,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
16
16
|
* TODO(phase-15-refactor): swap to ResizableShell once AppShellBody is a PanelGroup
|
|
17
17
|
*/
|
|
18
18
|
import { Maximize2, Minimize2, X } from 'lucide-react';
|
|
19
|
-
import { useEffect, useRef, useState } from 'react';
|
|
19
|
+
import { Fragment, useEffect, useRef, useState } from 'react';
|
|
20
20
|
import { cn } from '../lib/utils.js';
|
|
21
21
|
import { useResolvedPanelViews } from './panel-views-provider.js';
|
|
22
22
|
import { useMedaShell } from './shell-provider.js';
|
|
@@ -63,7 +63,7 @@ function ResizeHandle({ currentWidth, onResize, onCommit }) {
|
|
|
63
63
|
// ---------------------------------------------------------------------------
|
|
64
64
|
// RightPanel
|
|
65
65
|
// ---------------------------------------------------------------------------
|
|
66
|
-
export function RightPanel({ panelViews = EMPTY_PANEL_VIEWS, defaultView, modes = ['panel', 'expanded', 'fullscreen'], className, }) {
|
|
66
|
+
export function RightPanel({ panelViews = EMPTY_PANEL_VIEWS, defaultView, modes = ['panel', 'expanded', 'fullscreen'], renderTab, className, }) {
|
|
67
67
|
const band = useShellViewport();
|
|
68
68
|
const ctx = useMedaShell();
|
|
69
69
|
const { mode, activeView, width, setMode, setActiveView, setWidth } = ctx.panel;
|
|
@@ -132,8 +132,20 @@ export function RightPanel({ panelViews = EMPTY_PANEL_VIEWS, defaultView, modes
|
|
|
132
132
|
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) => {
|
|
133
133
|
const isActive = view.id === activeView;
|
|
134
134
|
const Icon = view.icon;
|
|
135
|
-
|
|
135
|
+
const children = (_jsxs(_Fragment, { children: [_jsx(Icon, { size: 14, "aria-hidden": "true" }), _jsx("span", { children: view.label })] }));
|
|
136
|
+
const buttonProps = {
|
|
137
|
+
type: 'button',
|
|
138
|
+
'aria-current': isActive ? 'true' : undefined,
|
|
139
|
+
'data-active': isActive || undefined,
|
|
140
|
+
onClick: () => setActiveView(view.id),
|
|
141
|
+
className: cn('inline-flex items-center gap-1.5 rounded-md px-2 py-1 text-xs font-medium transition-colors', isActive
|
|
136
142
|
? 'bg-accent text-accent-foreground'
|
|
137
|
-
: 'text-muted-foreground hover:bg-accent hover:text-foreground'),
|
|
143
|
+
: 'text-muted-foreground hover:bg-accent hover:text-foreground'),
|
|
144
|
+
children,
|
|
145
|
+
};
|
|
146
|
+
if (renderTab) {
|
|
147
|
+
return (_jsx(Fragment, { children: renderTab({ view, isActive, buttonProps, children }) }, view.id));
|
|
148
|
+
}
|
|
149
|
+
return _jsx("button", { ...buttonProps }, view.id);
|
|
138
150
|
}) }), _jsxs("div", { className: "flex items-center gap-1", children: [modes.length > 1 && (_jsx("button", { type: "button", "aria-label": cycleAriaLabel, onClick: cycleOpenMode, className: "inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground", children: mode === 'fullscreen' ? (_jsx(Minimize2, { size: 14, "aria-hidden": "true" })) : (_jsx(Maximize2, { size: 14, "aria-hidden": "true" })) })), _jsx("button", { type: "button", "aria-label": "Close panel", onClick: () => setMode('closed'), className: "inline-flex h-7 w-7 items-center justify-center rounded-md text-muted-foreground hover:bg-accent hover:text-foreground", children: _jsx(X, { size: 14, "aria-hidden": "true" }) })] })] }), _jsx("div", { className: "flex-1 overflow-y-auto", children: activePanelView != null ? (activePanelView.render(renderCtx)) : (_jsx("div", { className: "p-4 text-muted-foreground text-sm", children: "No panel view selected" })) }), mode === 'panel' && (_jsx(ResizeHandle, { currentWidth: resolvedWidth, onResize: handleResize, onCommit: handleCommit }))] })) }));
|
|
139
151
|
}
|
package/dist/shell/types.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LucideIcon } from 'lucide-react';
|
|
2
|
-
import type { ReactNode } from 'react';
|
|
2
|
+
import type { AnchorHTMLAttributes, ReactNode } from 'react';
|
|
3
3
|
export interface AppDefinition {
|
|
4
4
|
id: string;
|
|
5
5
|
label: string;
|
|
@@ -49,6 +49,7 @@ export interface ShellLinkRenderArgs {
|
|
|
49
49
|
isActive: boolean;
|
|
50
50
|
className: string;
|
|
51
51
|
children: ReactNode;
|
|
52
|
+
linkProps: AnchorHTMLAttributes<HTMLAnchorElement>;
|
|
52
53
|
}
|
|
53
54
|
export interface CommandDefinition {
|
|
54
55
|
id: string;
|
|
@@ -101,3 +102,9 @@ export interface AppShellAuthConfig {
|
|
|
101
102
|
preview?: ReactNode;
|
|
102
103
|
actions?: ReactNode;
|
|
103
104
|
}
|
|
105
|
+
export interface AppShellAuthBranding {
|
|
106
|
+
brandName?: ReactNode;
|
|
107
|
+
brandMark?: ReactNode;
|
|
108
|
+
appName?: ReactNode;
|
|
109
|
+
tagline?: ReactNode;
|
|
110
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createMedaThemeCss, defineMedaTheme, type MedaThemeConfig, type MedaThemeDefinition, type MedaThemeMode, type MedaThemeModeConfig, type MedaThemeTokenMap, } from './theme-bridge.js';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createMedaThemeCss, defineMedaTheme, } from './theme-bridge.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export type MedaThemeMode = 'light' | 'dark';
|
|
2
|
+
export type MedaThemeTokenMap = Record<string, string | undefined>;
|
|
3
|
+
export interface MedaThemeModeConfig {
|
|
4
|
+
colors?: MedaThemeTokenMap;
|
|
5
|
+
fonts?: MedaThemeTokenMap;
|
|
6
|
+
tokens?: MedaThemeTokenMap;
|
|
7
|
+
}
|
|
8
|
+
export interface MedaThemeConfig extends MedaThemeModeConfig {
|
|
9
|
+
appId: string;
|
|
10
|
+
selector?: string;
|
|
11
|
+
light?: MedaThemeModeConfig;
|
|
12
|
+
dark?: MedaThemeModeConfig;
|
|
13
|
+
}
|
|
14
|
+
export interface MedaThemeDefinition {
|
|
15
|
+
appId: string;
|
|
16
|
+
selector: string;
|
|
17
|
+
light: Required<MedaThemeModeConfig>;
|
|
18
|
+
dark: Required<MedaThemeModeConfig>;
|
|
19
|
+
}
|
|
20
|
+
export declare function defineMedaTheme(config: MedaThemeConfig): MedaThemeDefinition;
|
|
21
|
+
export declare function createMedaThemeCss(theme: MedaThemeDefinition): string;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
const MODE_KEYS = ['colors', 'fonts', 'tokens'];
|
|
2
|
+
function assertAppId(appId) {
|
|
3
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(appId)) {
|
|
4
|
+
throw new Error('Meda theme appId must contain only letters, numbers, underscores, or dashes.');
|
|
5
|
+
}
|
|
6
|
+
}
|
|
7
|
+
function toCssVariableName(group, key) {
|
|
8
|
+
const normalized = key.replace(/[A-Z]/g, (match) => `-${match.toLowerCase()}`);
|
|
9
|
+
if (normalized.startsWith('--')) {
|
|
10
|
+
return normalized;
|
|
11
|
+
}
|
|
12
|
+
if (group === 'fonts') {
|
|
13
|
+
return `--font-${normalized}`;
|
|
14
|
+
}
|
|
15
|
+
return `--${normalized}`;
|
|
16
|
+
}
|
|
17
|
+
function mergeModeConfig(base, mode = {}) {
|
|
18
|
+
return {
|
|
19
|
+
colors: { ...base.colors, ...mode.colors },
|
|
20
|
+
fonts: { ...base.fonts, ...mode.fonts },
|
|
21
|
+
tokens: { ...base.tokens, ...mode.tokens },
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export function defineMedaTheme(config) {
|
|
25
|
+
assertAppId(config.appId);
|
|
26
|
+
const base = {
|
|
27
|
+
colors: config.colors ?? {},
|
|
28
|
+
fonts: config.fonts ?? {},
|
|
29
|
+
tokens: config.tokens ?? {},
|
|
30
|
+
};
|
|
31
|
+
return {
|
|
32
|
+
appId: config.appId,
|
|
33
|
+
selector: config.selector ?? `[data-meda-app="${config.appId}"]`,
|
|
34
|
+
light: mergeModeConfig(base, config.light),
|
|
35
|
+
dark: mergeModeConfig(base, config.dark),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function entriesForMode(mode) {
|
|
39
|
+
return MODE_KEYS.flatMap((group) => Object.entries(mode[group])
|
|
40
|
+
.filter((entry) => typeof entry[1] === 'string')
|
|
41
|
+
.map(([key, value]) => [toCssVariableName(group, key), value])).sort(([left], [right]) => left.localeCompare(right));
|
|
42
|
+
}
|
|
43
|
+
function renderBlock(selector, entries) {
|
|
44
|
+
const declarations = entries.map(([key, value]) => ` ${key}: ${value};`).join('\n');
|
|
45
|
+
return `${selector} {\n${declarations}\n}`;
|
|
46
|
+
}
|
|
47
|
+
export function createMedaThemeCss(theme) {
|
|
48
|
+
const lightEntries = entriesForMode(theme.light);
|
|
49
|
+
const darkEntries = entriesForMode(theme.dark);
|
|
50
|
+
const darkSelector = `${theme.selector}.dark, ${theme.selector}[data-theme="dark"]`;
|
|
51
|
+
return [renderBlock(theme.selector, lightEntries), renderBlock(darkSelector, darkEntries)].join('\n\n');
|
|
52
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@medalsocial/meda",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Shared Meda UI shell and runtime package.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -31,6 +31,14 @@
|
|
|
31
31
|
"types": "./dist/brand/index.d.ts",
|
|
32
32
|
"default": "./dist/brand/index.js"
|
|
33
33
|
},
|
|
34
|
+
"./auth": {
|
|
35
|
+
"types": "./dist/auth/index.d.ts",
|
|
36
|
+
"default": "./dist/auth/index.js"
|
|
37
|
+
},
|
|
38
|
+
"./auth/better-auth": {
|
|
39
|
+
"types": "./dist/auth/better-auth.d.ts",
|
|
40
|
+
"default": "./dist/auth/better-auth.js"
|
|
41
|
+
},
|
|
34
42
|
"./shell": {
|
|
35
43
|
"types": "./dist/shell/index.d.ts",
|
|
36
44
|
"default": "./dist/shell/index.js"
|
|
@@ -71,6 +79,18 @@
|
|
|
71
79
|
},
|
|
72
80
|
"./styles/tokens": {
|
|
73
81
|
"default": "./dist/styles/tokens.css"
|
|
82
|
+
},
|
|
83
|
+
"./shell/primitives": {
|
|
84
|
+
"types": "./dist/shell/primitives.d.ts",
|
|
85
|
+
"default": "./dist/shell/primitives.js"
|
|
86
|
+
},
|
|
87
|
+
"./recipes/next": {
|
|
88
|
+
"types": "./dist/recipes/next.d.ts",
|
|
89
|
+
"default": "./dist/recipes/next.js"
|
|
90
|
+
},
|
|
91
|
+
"./theme": {
|
|
92
|
+
"types": "./dist/theme/index.d.ts",
|
|
93
|
+
"default": "./dist/theme/index.js"
|
|
74
94
|
}
|
|
75
95
|
},
|
|
76
96
|
"sideEffects": [
|