@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
|
@@ -1,17 +1,26 @@
|
|
|
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, AppShellWorkspaceConfig } 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;
|
|
13
16
|
contextRail?: AppShellContextRailConfig;
|
|
14
17
|
rightPanel?: AppShellRightPanelConfig;
|
|
18
|
+
/**
|
|
19
|
+
* Configurable workspace dropdown — replaces the package-default
|
|
20
|
+
* "Manage workspaces / Settings / Profile / Sign out" entries when
|
|
21
|
+
* `menuItems` is provided. The theme toggle is preserved automatically.
|
|
22
|
+
*/
|
|
23
|
+
workspace?: AppShellWorkspaceConfig;
|
|
15
24
|
globalActions?: ReactNode;
|
|
16
25
|
} | {
|
|
17
26
|
variant: 'chat';
|
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
|
-
return wrapper(_jsx(AppShellWorkspace, { iconRail: props.iconRail, contextRail: props.contextRail, rightPanel: props.rightPanel, globalActions: props.globalActions, children: props.children }));
|
|
19
|
+
return wrapper(_jsx(AppShellWorkspace, { iconRail: props.iconRail, contextRail: props.contextRail, rightPanel: props.rightPanel, workspace: props.workspace, 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, AppShellWorkspaceConfig, CommandDefinition, ContextItem, ContextModule, MobileBottomNavItem, PanelMode, PanelView, ShellLinkRenderArgs, ShellMainLayout, ShellRenderContext, ShellViewport, ThemeAdapter, WorkspaceDefinition, WorkspaceMenuItem, } 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
|
}
|
|
@@ -1,12 +1,34 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import type { WorkspaceMenuItem } from './types.js';
|
|
2
3
|
export interface WorkspaceSwitcherProps {
|
|
4
|
+
/**
|
|
5
|
+
* Configurable dropdown items. When provided, REPLACES the default
|
|
6
|
+
* "Manage workspaces / Settings / Profile / Sign out" entries. The theme
|
|
7
|
+
* toggle is still inserted automatically by meda — between the items and
|
|
8
|
+
* the footer — so consumers don't have to reimplement theme cycling.
|
|
9
|
+
*
|
|
10
|
+
* When omitted, the package-default items render (1.x behavior).
|
|
11
|
+
*/
|
|
12
|
+
menuItems?: WorkspaceMenuItem[];
|
|
13
|
+
/**
|
|
14
|
+
* Extra content rendered after all items and the theme toggle. Backwards-
|
|
15
|
+
* compatible alias for the legacy `workspaceMenuFooter` prop.
|
|
16
|
+
*/
|
|
17
|
+
menuFooter?: ReactNode;
|
|
18
|
+
/** @deprecated Use `menuFooter` instead. */
|
|
3
19
|
workspaceMenuFooter?: ReactNode;
|
|
4
20
|
}
|
|
5
|
-
export declare function WorkspaceSwitcher({ workspaceMenuFooter }?: WorkspaceSwitcherProps): import("react/jsx-runtime").JSX.Element;
|
|
21
|
+
export declare function WorkspaceSwitcher({ menuItems, menuFooter, workspaceMenuFooter, }?: WorkspaceSwitcherProps): import("react/jsx-runtime").JSX.Element;
|
|
6
22
|
export declare function AppTabs(): import("react/jsx-runtime").JSX.Element;
|
|
7
23
|
export declare function PanelToggle(): import("react/jsx-runtime").JSX.Element;
|
|
8
24
|
export interface ShellHeaderProps {
|
|
9
25
|
globalActions?: ReactNode;
|
|
10
26
|
className?: string;
|
|
27
|
+
/**
|
|
28
|
+
* Forwarded to the internal `<WorkspaceSwitcher>`. See
|
|
29
|
+
* `WorkspaceSwitcherProps` for the full shape.
|
|
30
|
+
*/
|
|
31
|
+
workspaceMenuItems?: WorkspaceMenuItem[];
|
|
32
|
+
workspaceMenuFooter?: ReactNode;
|
|
11
33
|
}
|
|
12
|
-
export declare function ShellHeader({ globalActions, className }?: ShellHeaderProps): import("react/jsx-runtime").JSX.Element | null;
|
|
34
|
+
export declare function ShellHeader({ globalActions, className, workspaceMenuItems, workspaceMenuFooter, }?: ShellHeaderProps): import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
3
|
import { ChevronDown, Monitor, Moon, PanelRightClose, PanelRightOpen, Sun } from 'lucide-react';
|
|
4
|
+
import { createElement, Fragment, isValidElement } from 'react';
|
|
4
5
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '../components/ui/dropdown-menu.js';
|
|
5
6
|
import { cn } from '../lib/utils.js';
|
|
6
7
|
import { useMedaShell } from './shell-provider.js';
|
|
@@ -18,9 +19,43 @@ function ThemeToggleMenuItem() {
|
|
|
18
19
|
const Icon = THEME_ICON[theme];
|
|
19
20
|
return (_jsxs(DropdownMenuItem, { onClick: () => setTheme(NEXT_THEME[theme]), children: [_jsx(Icon, { size: 16, "aria-hidden": "true" }), THEME_LABEL[theme]] }));
|
|
20
21
|
}
|
|
21
|
-
|
|
22
|
+
function renderConfiguredIcon(icon) {
|
|
23
|
+
if (icon == null)
|
|
24
|
+
return null;
|
|
25
|
+
if (isValidElement(icon))
|
|
26
|
+
return icon;
|
|
27
|
+
if (typeof icon === 'function') {
|
|
28
|
+
return createElement(icon, { size: 16, 'aria-hidden': true });
|
|
29
|
+
}
|
|
30
|
+
// forwardRef/memo components are objects carrying a `$$typeof` symbol —
|
|
31
|
+
// treat them like Lucide components. Plain ReactNode objects (arrays,
|
|
32
|
+
// iterables, promises) have no `$$typeof` and are rendered as-is so they
|
|
33
|
+
// don't crash createElement with "Element type is invalid".
|
|
34
|
+
if (typeof icon === 'object' && icon !== null) {
|
|
35
|
+
const candidate = icon;
|
|
36
|
+
if (candidate.$$typeof != null) {
|
|
37
|
+
return createElement(icon, {
|
|
38
|
+
size: 16,
|
|
39
|
+
'aria-hidden': true,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return icon;
|
|
44
|
+
}
|
|
45
|
+
function renderConfiguredItem(item) {
|
|
46
|
+
const handleSelect = () => item.onClick?.();
|
|
47
|
+
// Childless anchor — Base UI merges the DropdownMenuItem's children into the
|
|
48
|
+
// cloned render element. Passing children here would override the icon +
|
|
49
|
+
// label children below and configured icons would silently disappear.
|
|
50
|
+
// biome-ignore lint/a11y/useAnchorContent: children are injected at render time by Base UI's `render` prop
|
|
51
|
+
const renderLink = item.href != null ? _jsx("a", { href: item.href }) : undefined;
|
|
52
|
+
return (_jsxs(DropdownMenuItem, { render: renderLink, "data-variant": item.variant ?? 'default', className: item.variant === 'destructive' ? 'text-destructive' : undefined, onClick: handleSelect, children: [renderConfiguredIcon(item.icon), item.label] }));
|
|
53
|
+
}
|
|
54
|
+
export function WorkspaceSwitcher({ menuItems, menuFooter, workspaceMenuFooter, } = {}) {
|
|
22
55
|
const { workspace, workspaces } = useMedaShell();
|
|
23
|
-
|
|
56
|
+
const resolvedFooter = menuFooter ?? workspaceMenuFooter;
|
|
57
|
+
const useConfiguredItems = Array.isArray(menuItems);
|
|
58
|
+
return (_jsxs(DropdownMenu, { children: [_jsxs(DropdownMenuTrigger, { render: _jsx("button", { type: "button", className: "flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm font-medium hover:bg-accent" }), children: [workspace.icon != null && (_jsx("span", { className: "shrink-0", "aria-hidden": "true", children: workspace.icon })), _jsx("span", { children: workspace.name }), _jsx(ChevronDown, { size: 14, "aria-hidden": "true" })] }), _jsxs(DropdownMenuContent, { className: "min-w-[200px]", children: [workspaces.length > 0 && (_jsxs(_Fragment, { children: [workspaces.map((ws) => (_jsxs(DropdownMenuItem, { children: [ws.icon != null && _jsx("span", { "aria-hidden": "true", children: ws.icon }), ws.name] }, ws.id))), _jsx(DropdownMenuSeparator, {})] })), useConfiguredItems ? (menuItems.map((item) => (_jsxs(Fragment, { children: [renderConfiguredItem(item), item.separatorAfter && _jsx(DropdownMenuSeparator, {})] }, item.id)))) : (_jsxs(_Fragment, { children: [_jsx(DropdownMenuItem, { children: "Manage workspaces" }), _jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { children: "Settings" }), _jsx(DropdownMenuItem, { children: "Profile" })] })), _jsx(DropdownMenuSeparator, {}), _jsx(ThemeToggleMenuItem, {}), !useConfiguredItems && (_jsxs(_Fragment, { children: [_jsx(DropdownMenuSeparator, {}), _jsx(DropdownMenuItem, { children: "Sign out" })] })), resolvedFooter] })] }));
|
|
24
59
|
}
|
|
25
60
|
// ---------------------------------------------------------------------------
|
|
26
61
|
// AppTabs
|
|
@@ -50,9 +85,9 @@ export function PanelToggle() {
|
|
|
50
85
|
};
|
|
51
86
|
return (_jsx("button", { type: "button", "aria-label": isOpen ? 'Close right panel' : 'Open right panel', onClick: handleClick, className: cn('inline-flex h-8 w-8 items-center justify-center rounded-md transition-colors', isOpen ? 'bg-accent text-accent-foreground' : 'text-muted-foreground hover:bg-accent'), children: isOpen ? (_jsx(PanelRightClose, { size: 18, "aria-hidden": "true" })) : (_jsx(PanelRightOpen, { size: 18, "aria-hidden": "true" })) }));
|
|
52
87
|
}
|
|
53
|
-
export function ShellHeader({ globalActions, className } = {}) {
|
|
88
|
+
export function ShellHeader({ globalActions, className, workspaceMenuItems, workspaceMenuFooter, } = {}) {
|
|
54
89
|
const band = useShellViewport();
|
|
55
90
|
if (band === 'mobile')
|
|
56
91
|
return null;
|
|
57
|
-
return (_jsxs("header", { className: cn('flex h-[var(--shell-header-height)] w-full items-center justify-between', 'border-b border-border bg-background px-3', className), children: [_jsxs("div", { className: "flex items-center", children: [_jsx(WorkspaceSwitcher, {}), _jsx(AppTabs, {})] }), _jsxs("div", { className: "flex items-center gap-2", children: [globalActions, _jsx(PanelToggle, {})] })] }));
|
|
92
|
+
return (_jsxs("header", { className: cn('flex h-[var(--shell-header-height)] w-full items-center justify-between', 'border-b border-border bg-background px-3', className), children: [_jsxs("div", { className: "flex items-center", children: [_jsx(WorkspaceSwitcher, { menuItems: workspaceMenuItems, menuFooter: workspaceMenuFooter }), _jsx(AppTabs, {})] }), _jsxs("div", { className: "flex items-center gap-2", children: [globalActions, _jsx(PanelToggle, {})] })] }));
|
|
58
93
|
}
|
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;
|
|
@@ -92,6 +93,44 @@ export interface AppShellRightPanelConfig {
|
|
|
92
93
|
panelViews: PanelView[];
|
|
93
94
|
defaultView?: string;
|
|
94
95
|
}
|
|
96
|
+
/**
|
|
97
|
+
* A single configurable item in the WorkspaceSwitcher dropdown.
|
|
98
|
+
* Either `href` (renders as a link) or `onClick` (renders as a button) MUST
|
|
99
|
+
* be provided; supplying both makes the item a button that fires `onClick`
|
|
100
|
+
* AND navigates to `href` afterwards.
|
|
101
|
+
*/
|
|
102
|
+
interface WorkspaceMenuItemBase {
|
|
103
|
+
id: string;
|
|
104
|
+
label: ReactNode;
|
|
105
|
+
icon?: LucideIcon | ReactNode;
|
|
106
|
+
/** Insert a `<DropdownMenuSeparator />` immediately after this item. */
|
|
107
|
+
separatorAfter?: boolean;
|
|
108
|
+
/** Visual intent — `destructive` paints with the destructive token. */
|
|
109
|
+
variant?: 'default' | 'destructive';
|
|
110
|
+
}
|
|
111
|
+
export type WorkspaceMenuItem = WorkspaceMenuItemBase & ({
|
|
112
|
+
href: string;
|
|
113
|
+
onClick?: () => void;
|
|
114
|
+
} | {
|
|
115
|
+
onClick: () => void;
|
|
116
|
+
href?: string;
|
|
117
|
+
});
|
|
118
|
+
/**
|
|
119
|
+
* Workspace dropdown configuration for `<AppShell variant="workspace">`.
|
|
120
|
+
*
|
|
121
|
+
* When `menuItems` is provided, it REPLACES the package-default menu
|
|
122
|
+
* ("Manage workspaces", "Settings", "Profile", "Sign out"). The theme toggle
|
|
123
|
+
* is still inserted automatically by meda — between the items and the footer
|
|
124
|
+
* — so consumers don't have to reimplement theme cycling.
|
|
125
|
+
*
|
|
126
|
+
* When `menuItems` is omitted, the package defaults render unchanged (the
|
|
127
|
+
* existing 1.x behavior). This is fully additive and non-breaking.
|
|
128
|
+
*/
|
|
129
|
+
export interface AppShellWorkspaceConfig {
|
|
130
|
+
menuItems?: WorkspaceMenuItem[];
|
|
131
|
+
/** Extra slot rendered after all items and the theme toggle. */
|
|
132
|
+
menuFooter?: ReactNode;
|
|
133
|
+
}
|
|
95
134
|
export interface AppShellAuthConfig {
|
|
96
135
|
title: ReactNode;
|
|
97
136
|
description?: ReactNode;
|
|
@@ -101,3 +140,10 @@ export interface AppShellAuthConfig {
|
|
|
101
140
|
preview?: ReactNode;
|
|
102
141
|
actions?: ReactNode;
|
|
103
142
|
}
|
|
143
|
+
export interface AppShellAuthBranding {
|
|
144
|
+
brandName?: ReactNode;
|
|
145
|
+
brandMark?: ReactNode;
|
|
146
|
+
appName?: ReactNode;
|
|
147
|
+
tagline?: ReactNode;
|
|
148
|
+
}
|
|
149
|
+
export {};
|
package/dist/styles/theme.css
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
1
|
@import "./tokens.css";
|
|
2
2
|
|
|
3
|
+
/* Tell Tailwind v4 to scan meda's compiled component output so utilities
|
|
4
|
+
like `h-full`, `mt-auto`, `py-3.5`, and `bg-shell-rail` (used by IconRail,
|
|
5
|
+
AppShellWorkspace, etc.) are generated even when the consumer app doesn't
|
|
6
|
+
reference them itself. Without this, consumers see broken layout where
|
|
7
|
+
meda components rely on classes the app doesn't otherwise use. Path is
|
|
8
|
+
resolved relative to this CSS file at build time, so it points to the
|
|
9
|
+
sibling component output regardless of where the package is installed. */
|
|
10
|
+
@source "../**/*.js";
|
|
11
|
+
|
|
3
12
|
@custom-variant dark (&:is(.dark *, [data-theme="dark"] *));
|
|
4
13
|
|
|
14
|
+
@layer base {
|
|
15
|
+
:root {
|
|
16
|
+
color-scheme: light;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.dark,
|
|
20
|
+
[data-theme="dark"] {
|
|
21
|
+
color-scheme: dark;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
5
25
|
@theme inline {
|
|
6
26
|
/* Color primitives — exposed as Tailwind color scales */
|
|
7
27
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
|
|
3
|
+
// Read theme.css via Node's runtime APIs without pulling in @types/node.
|
|
4
|
+
// Declared locally so the package-level tsconfig (which doesn't include
|
|
5
|
+
// node typings) still typechecks cleanly under jsdom Vitest runs.
|
|
6
|
+
declare const require: (id: string) => unknown;
|
|
7
|
+
declare const __dirname: string;
|
|
8
|
+
|
|
9
|
+
const fs = require('node:fs') as { readFileSync(p: string, enc: string): string };
|
|
10
|
+
const path = require('node:path') as { join(...parts: string[]): string };
|
|
11
|
+
const themeCss = fs.readFileSync(path.join(__dirname, 'theme.css'), 'utf8');
|
|
12
|
+
|
|
13
|
+
describe('theme.css', () => {
|
|
14
|
+
// Tailwind v4 only generates utility classes for class names it sees in
|
|
15
|
+
// scanned files. Consumer apps don't reference every utility used by meda
|
|
16
|
+
// components (e.g. `h-full`, `mt-auto`, `py-3.5`, `bg-shell-rail` on
|
|
17
|
+
// IconRail), so without an @source directive pointing at meda's compiled
|
|
18
|
+
// dist files, those classes silently disappear and component layouts
|
|
19
|
+
// collapse — most visibly, the IconRail's utility items fail to pin to
|
|
20
|
+
// the bottom of the viewport because `mt-auto` never gets generated.
|
|
21
|
+
//
|
|
22
|
+
// The directive is resolved relative to this CSS file at build time. Since
|
|
23
|
+
// src/styles/theme.css is copied verbatim to dist/styles/theme.css by the
|
|
24
|
+
// build script, `../**/*.js` from the dist location scans every emitted
|
|
25
|
+
// component module — exactly the surface consumers import from.
|
|
26
|
+
it('declares an @source directive that scans meda component output', () => {
|
|
27
|
+
expect(themeCss).toMatch(/@source\s+["']\.\.\/\*\*\/\*\.js["']/);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -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
|
+
}
|