@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.
Files changed (45) hide show
  1. package/README.md +63 -17
  2. package/dist/auth/auth-message.d.ts +10 -0
  3. package/dist/auth/auth-message.js +18 -0
  4. package/dist/auth/auth-provider-button.d.ts +14 -0
  5. package/dist/auth/auth-provider-button.js +26 -0
  6. package/dist/auth/auth-provider-list.d.ts +6 -0
  7. package/dist/auth/auth-provider-list.js +7 -0
  8. package/dist/auth/better-auth.d.ts +40 -0
  9. package/dist/auth/better-auth.js +92 -0
  10. package/dist/auth/index.d.ts +6 -0
  11. package/dist/auth/index.js +3 -0
  12. package/dist/auth/public.d.ts +6 -0
  13. package/dist/auth/public.js +3 -0
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.js +2 -0
  16. package/dist/lib/render-element.d.ts +9 -0
  17. package/dist/lib/render-element.js +27 -0
  18. package/dist/recipes/next.d.ts +49 -0
  19. package/dist/recipes/next.js +128 -0
  20. package/dist/shell/app-shell-workspace.js +11 -6
  21. package/dist/shell/app-shell.d.ts +5 -2
  22. package/dist/shell/app-shell.js +13 -1
  23. package/dist/shell/command-palette.js +2 -1
  24. package/dist/shell/context-rail.d.ts +1 -1
  25. package/dist/shell/context-rail.js +14 -7
  26. package/dist/shell/icon-rail.d.ts +2 -1
  27. package/dist/shell/icon-rail.js +8 -1
  28. package/dist/shell/index.d.ts +6 -1
  29. package/dist/shell/index.js +2 -1
  30. package/dist/shell/internal/mobile-drawers.d.ts +8 -2
  31. package/dist/shell/internal/mobile-drawers.js +47 -12
  32. package/dist/shell/panel-views-provider.d.ts +15 -0
  33. package/dist/shell/panel-views-provider.js +56 -0
  34. package/dist/shell/primitives.d.ts +9 -0
  35. package/dist/shell/primitives.js +7 -0
  36. package/dist/shell/right-panel.d.ts +12 -1
  37. package/dist/shell/right-panel.js +37 -12
  38. package/dist/shell/shell-provider.d.ts +14 -1
  39. package/dist/shell/shell-provider.js +64 -2
  40. package/dist/shell/types.d.ts +13 -2
  41. package/dist/theme/index.d.ts +1 -0
  42. package/dist/theme/index.js +1 -0
  43. package/dist/theme/theme-bridge.d.ts +21 -0
  44. package/dist/theme/theme-bridge.js +52 -0
  45. 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
  ]);
@@ -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: ContextItem[];
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.2.0",
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": [