@medalsocial/meda 1.2.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-workspace.js +11 -6
- package/dist/shell/app-shell.d.ts +5 -2
- package/dist/shell/app-shell.js +13 -1
- package/dist/shell/command-palette.js +2 -1
- package/dist/shell/context-rail.d.ts +1 -1
- package/dist/shell/context-rail.js +14 -7
- package/dist/shell/icon-rail.d.ts +2 -1
- package/dist/shell/icon-rail.js +8 -1
- package/dist/shell/index.d.ts +6 -1
- package/dist/shell/index.js +2 -1
- package/dist/shell/internal/mobile-drawers.d.ts +8 -2
- package/dist/shell/internal/mobile-drawers.js +47 -12
- package/dist/shell/panel-views-provider.d.ts +15 -0
- package/dist/shell/panel-views-provider.js +56 -0
- 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 +37 -12
- package/dist/shell/shell-provider.d.ts +14 -1
- package/dist/shell/shell-provider.js +64 -2
- package/dist/shell/types.d.ts +13 -2
- 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
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
2
|
import { type ShellStorageAdapter } from './layout-state.js';
|
|
3
|
-
import type { AppDefinition, MobileBottomNavItem, PanelMode, ThemeAdapter, WorkspaceDefinition } from './types.js';
|
|
3
|
+
import type { AppDefinition, MobileBottomNavItem, PanelMode, PanelView, ThemeAdapter, WorkspaceDefinition } from './types.js';
|
|
4
4
|
export type MobileDrawerKind = 'menu-drawer' | 'module-drawer' | 'panels-drawer' | 'ai-drawer' | (string & {}) | null;
|
|
5
|
+
export interface PanelViewRegistration {
|
|
6
|
+
id: string;
|
|
7
|
+
views: PanelView[];
|
|
8
|
+
defaultView?: string;
|
|
9
|
+
}
|
|
5
10
|
interface MedaShellContextValue {
|
|
6
11
|
workspace: WorkspaceDefinition;
|
|
7
12
|
workspaces: WorkspaceDefinition[];
|
|
@@ -15,6 +20,9 @@ interface MedaShellContextValue {
|
|
|
15
20
|
setMode: (m: PanelMode) => void;
|
|
16
21
|
setActiveView: (v: string | null) => void;
|
|
17
22
|
setWidth: (w: number) => void;
|
|
23
|
+
open: () => void;
|
|
24
|
+
close: () => void;
|
|
25
|
+
toggle: () => void;
|
|
18
26
|
/** Opens panel + switches to viewId in one call.
|
|
19
27
|
* Sugar for app keyboard shortcuts (e.g. Cmd+J → panel.focus('ai')).
|
|
20
28
|
* If already open in 'panel', 'expanded', or 'fullscreen', the existing
|
|
@@ -26,6 +34,7 @@ interface MedaShellContextValue {
|
|
|
26
34
|
collapsed: boolean;
|
|
27
35
|
setWidth: (w: number) => void;
|
|
28
36
|
setCollapsed: (c: boolean) => void;
|
|
37
|
+
toggle: () => void;
|
|
29
38
|
};
|
|
30
39
|
mobileBottomNav: MobileBottomNavItem[];
|
|
31
40
|
mobileDrawer: {
|
|
@@ -36,6 +45,10 @@ interface MedaShellContextValue {
|
|
|
36
45
|
open: boolean;
|
|
37
46
|
setOpen: (open: boolean) => void;
|
|
38
47
|
};
|
|
48
|
+
panelViews: {
|
|
49
|
+
registrations: PanelViewRegistration[];
|
|
50
|
+
register: (id: string, views: PanelView[], defaultView?: string) => () => void;
|
|
51
|
+
};
|
|
39
52
|
commandPaletteHotkey: string;
|
|
40
53
|
/** Selection bridge between main workspace and right panel views (spec §17). */
|
|
41
54
|
selection: unknown | null;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
3
|
import { LayoutGrid, Menu, PanelTop, Sparkles } from 'lucide-react';
|
|
4
|
-
import { createContext, lazy, Suspense, useContext, useMemo, useState, } from 'react';
|
|
4
|
+
import { createContext, lazy, Suspense, useCallback, useContext, useMemo, useState, } from 'react';
|
|
5
5
|
import { createLocalStorageAdapter, useShellLayoutState, } from './layout-state.js';
|
|
6
6
|
import { DefaultThemeProvider, ThemeCtx } from './theme.js';
|
|
7
|
+
import { useShellViewport } from './use-shell-viewport.js';
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
8
9
|
// Theme adapter wiring
|
|
9
10
|
// ---------------------------------------------------------------------------
|
|
@@ -78,11 +79,35 @@ export function MedaShellProvider(props) {
|
|
|
78
79
|
open: commandPaletteOpen,
|
|
79
80
|
setOpen: setCommandPaletteOpen,
|
|
80
81
|
}), [commandPaletteOpen]);
|
|
82
|
+
const [panelViewRegistrations, setPanelViewRegistrations] = useState([]);
|
|
83
|
+
const registerPanelViews = useCallback((id, views, defaultView) => {
|
|
84
|
+
setPanelViewRegistrations((prev) => {
|
|
85
|
+
const existing = prev.find((registration) => registration.id === id);
|
|
86
|
+
if (existing?.views === views && existing.defaultView === defaultView)
|
|
87
|
+
return prev;
|
|
88
|
+
const nextRegistration = defaultView === undefined ? { id, views } : { id, views, defaultView };
|
|
89
|
+
if (existing == null)
|
|
90
|
+
return [...prev, nextRegistration];
|
|
91
|
+
return prev.map((registration) => (registration.id === id ? nextRegistration : registration));
|
|
92
|
+
});
|
|
93
|
+
return () => {
|
|
94
|
+
setPanelViewRegistrations((prev) => prev.some((registration) => registration.id === id &&
|
|
95
|
+
registration.views === views &&
|
|
96
|
+
registration.defaultView === defaultView)
|
|
97
|
+
? prev.filter((registration) => registration.id !== id)
|
|
98
|
+
: prev);
|
|
99
|
+
};
|
|
100
|
+
}, []);
|
|
101
|
+
const panelViews = useMemo(() => ({
|
|
102
|
+
registrations: panelViewRegistrations,
|
|
103
|
+
register: registerPanelViews,
|
|
104
|
+
}), [panelViewRegistrations, registerPanelViews]);
|
|
81
105
|
const [layoutState, setLayoutState] = useShellLayoutState({
|
|
82
106
|
workspaceId: props.workspace.id,
|
|
83
107
|
appId: activeAppId,
|
|
84
108
|
storage,
|
|
85
109
|
});
|
|
110
|
+
const isMobile = useShellViewport() === 'mobile';
|
|
86
111
|
const panel = useMemo(() => ({
|
|
87
112
|
mode: layoutState.rightPanel.mode,
|
|
88
113
|
activeView: layoutState.rightPanel.activeView,
|
|
@@ -99,6 +124,37 @@ export function MedaShellProvider(props) {
|
|
|
99
124
|
...prev,
|
|
100
125
|
rightPanel: { ...prev.rightPanel, width },
|
|
101
126
|
})),
|
|
127
|
+
open: () => {
|
|
128
|
+
if (isMobile)
|
|
129
|
+
setMobileDrawerOpen('panels-drawer');
|
|
130
|
+
setLayoutState((prev) => ({
|
|
131
|
+
...prev,
|
|
132
|
+
rightPanel: {
|
|
133
|
+
...prev.rightPanel,
|
|
134
|
+
mode: prev.rightPanel.mode === 'closed' ? 'panel' : prev.rightPanel.mode,
|
|
135
|
+
},
|
|
136
|
+
}));
|
|
137
|
+
},
|
|
138
|
+
close: () => {
|
|
139
|
+
if (isMobile)
|
|
140
|
+
setMobileDrawerOpen((open) => (open === 'panels-drawer' ? null : open));
|
|
141
|
+
setLayoutState((prev) => ({
|
|
142
|
+
...prev,
|
|
143
|
+
rightPanel: { ...prev.rightPanel, mode: 'closed' },
|
|
144
|
+
}));
|
|
145
|
+
},
|
|
146
|
+
toggle: () => {
|
|
147
|
+
if (isMobile) {
|
|
148
|
+
setMobileDrawerOpen((open) => (open === 'panels-drawer' ? null : 'panels-drawer'));
|
|
149
|
+
}
|
|
150
|
+
setLayoutState((prev) => ({
|
|
151
|
+
...prev,
|
|
152
|
+
rightPanel: {
|
|
153
|
+
...prev.rightPanel,
|
|
154
|
+
mode: prev.rightPanel.mode === 'closed' ? 'panel' : 'closed',
|
|
155
|
+
},
|
|
156
|
+
}));
|
|
157
|
+
},
|
|
102
158
|
// focus(viewId) — opens panel + switches to view in one call.
|
|
103
159
|
// Only flips closed → panel; preserves expanded / fullscreen modes.
|
|
104
160
|
focus: (viewId) => setLayoutState((prev) => {
|
|
@@ -108,7 +164,7 @@ export function MedaShellProvider(props) {
|
|
|
108
164
|
rightPanel: { ...prev.rightPanel, mode: nextMode, activeView: viewId },
|
|
109
165
|
};
|
|
110
166
|
}),
|
|
111
|
-
}), [layoutState, setLayoutState]);
|
|
167
|
+
}), [isMobile, layoutState, setLayoutState]);
|
|
112
168
|
const contextRail = useMemo(() => ({
|
|
113
169
|
width: layoutState.contextRail.width,
|
|
114
170
|
collapsed: layoutState.contextRail.collapsed,
|
|
@@ -120,6 +176,10 @@ export function MedaShellProvider(props) {
|
|
|
120
176
|
...prev,
|
|
121
177
|
contextRail: { ...prev.contextRail, collapsed },
|
|
122
178
|
})),
|
|
179
|
+
toggle: () => setLayoutState((prev) => ({
|
|
180
|
+
...prev,
|
|
181
|
+
contextRail: { ...prev.contextRail, collapsed: !prev.contextRail.collapsed },
|
|
182
|
+
})),
|
|
123
183
|
}), [layoutState, setLayoutState]);
|
|
124
184
|
const value = useMemo(() => ({
|
|
125
185
|
workspace: props.workspace,
|
|
@@ -132,6 +192,7 @@ export function MedaShellProvider(props) {
|
|
|
132
192
|
mobileBottomNav: props.mobileBottomNav ?? defaultMobileBottomNav,
|
|
133
193
|
mobileDrawer,
|
|
134
194
|
commandPalette,
|
|
195
|
+
panelViews,
|
|
135
196
|
commandPaletteHotkey: props.commandPaletteHotkey ?? 'mod+k',
|
|
136
197
|
selection,
|
|
137
198
|
setSelection,
|
|
@@ -145,6 +206,7 @@ export function MedaShellProvider(props) {
|
|
|
145
206
|
props.mobileBottomNav,
|
|
146
207
|
mobileDrawer,
|
|
147
208
|
commandPalette,
|
|
209
|
+
panelViews,
|
|
148
210
|
props.commandPaletteHotkey,
|
|
149
211
|
selection,
|
|
150
212
|
]);
|
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;
|
|
@@ -14,7 +14,8 @@ export interface ContextModule {
|
|
|
14
14
|
id: string;
|
|
15
15
|
label: string;
|
|
16
16
|
description?: string;
|
|
17
|
-
items
|
|
17
|
+
items?: ContextItem[];
|
|
18
|
+
render?: (ctx: ShellRenderContext) => ReactNode;
|
|
18
19
|
}
|
|
19
20
|
export interface ContextItem {
|
|
20
21
|
id: string;
|
|
@@ -48,6 +49,7 @@ export interface ShellLinkRenderArgs {
|
|
|
48
49
|
isActive: boolean;
|
|
49
50
|
className: string;
|
|
50
51
|
children: ReactNode;
|
|
52
|
+
linkProps: AnchorHTMLAttributes<HTMLAnchorElement>;
|
|
51
53
|
}
|
|
52
54
|
export interface CommandDefinition {
|
|
53
55
|
id: string;
|
|
@@ -55,6 +57,8 @@ export interface CommandDefinition {
|
|
|
55
57
|
icon?: LucideIcon;
|
|
56
58
|
group: string;
|
|
57
59
|
shortcut?: string;
|
|
60
|
+
/** Display-only alias for `shortcut`; `shortcut` wins when both are supplied. */
|
|
61
|
+
hotkey?: string;
|
|
58
62
|
run: () => void | Promise<void>;
|
|
59
63
|
}
|
|
60
64
|
export interface ThemeAdapter {
|
|
@@ -76,6 +80,7 @@ export interface AppShellIconRailConfig {
|
|
|
76
80
|
utilityItems?: import('./icon-rail.js').IconRailItem[];
|
|
77
81
|
footer?: ReactNode;
|
|
78
82
|
activeId?: string;
|
|
83
|
+
renderLink?: import('./icon-rail.js').IconRailProps['renderLink'];
|
|
79
84
|
}
|
|
80
85
|
/** ContextRail configuration for `<AppShell variant="workspace">`. */
|
|
81
86
|
export interface AppShellContextRailConfig {
|
|
@@ -97,3 +102,9 @@ export interface AppShellAuthConfig {
|
|
|
97
102
|
preview?: ReactNode;
|
|
98
103
|
actions?: ReactNode;
|
|
99
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": [
|