@cedros/admin-react 0.1.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 +38 -0
- package/dist/AdminShell.d.ts +34 -0
- package/dist/AdminShell.js +156 -0
- package/dist/ProfileDropdown.d.ts +10 -0
- package/dist/ProfileDropdown.js +54 -0
- package/dist/adminHostServices.d.ts +20 -0
- package/dist/adminHostServices.js +34 -0
- package/dist/adminShellLayout.d.ts +29 -0
- package/dist/adminShellLayout.js +46 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +5 -0
- package/dist/platform.d.ts +13 -0
- package/dist/platform.js +41 -0
- package/dist/pluginRegistry.d.ts +2 -0
- package/dist/pluginRegistry.js +34 -0
- package/dist/shell.css +440 -0
- package/dist/styles.css +1 -0
- package/dist/types.d.ts +98 -0
- package/dist/types.js +1 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# @cedros/admin-react
|
|
2
|
+
|
|
3
|
+
Shared `AdminShell` host and admin plugin contract for Cedros packages.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @cedros/admin-react react react-dom
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```tsx
|
|
14
|
+
import { AdminShell, HOST_SERVICE_IDS } from "@cedros/admin-react";
|
|
15
|
+
import "@cedros/admin-react/styles.css";
|
|
16
|
+
|
|
17
|
+
<AdminShell
|
|
18
|
+
hostContext={{
|
|
19
|
+
services: {
|
|
20
|
+
[HOST_SERVICE_IDS.cedrosLogin]: {
|
|
21
|
+
user: { id: "user-1", email: "admin@example.com" },
|
|
22
|
+
getAccessToken: () => null,
|
|
23
|
+
serverUrl: "https://auth.example.com"
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}}
|
|
27
|
+
plugins={[]}
|
|
28
|
+
/>;
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
This package is the canonical shared admin shell for `cedros-login` and `cedros-pay`.
|
|
32
|
+
|
|
33
|
+
It intentionally owns only:
|
|
34
|
+
- `AdminShell`
|
|
35
|
+
- the shared plugin types/contracts
|
|
36
|
+
- the host service-bag helpers used by login/pay
|
|
37
|
+
|
|
38
|
+
It intentionally does not own `cedros-data`'s authored-content, extension-management, or CMS runtime surface.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type JSX, type ReactNode } from "react";
|
|
2
|
+
import type { AdminPlugin, AdminSectionComponent, AdminShellSectionWrapper, HostContext, PluginContext, PluginRegistry, QualifiedSectionId, ResolvedSection } from "./types.js";
|
|
3
|
+
export interface AdminShellProps {
|
|
4
|
+
title?: string;
|
|
5
|
+
plugins?: AdminPlugin[];
|
|
6
|
+
hostContext: HostContext;
|
|
7
|
+
defaultSection?: QualifiedSectionId;
|
|
8
|
+
pageSize?: number;
|
|
9
|
+
refreshInterval?: number;
|
|
10
|
+
onSectionChange?: (section: QualifiedSectionId) => void;
|
|
11
|
+
logo?: ReactNode;
|
|
12
|
+
profileMenu?: ReactNode;
|
|
13
|
+
sidebarFooter?: ReactNode;
|
|
14
|
+
onSettingsClick?: () => void;
|
|
15
|
+
onLogoutClick?: () => void;
|
|
16
|
+
sectionWrappers?: AdminShellSectionWrapper[];
|
|
17
|
+
loadingFallback?: ReactNode;
|
|
18
|
+
className?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface AdminShellContextValue {
|
|
21
|
+
registry: PluginRegistry;
|
|
22
|
+
hostContext: HostContext;
|
|
23
|
+
activeSection: QualifiedSectionId | null;
|
|
24
|
+
setActiveSection: (section: QualifiedSectionId) => void;
|
|
25
|
+
getPluginContext: (pluginId: string) => PluginContext | null;
|
|
26
|
+
}
|
|
27
|
+
export interface CurrentSectionState {
|
|
28
|
+
Component: AdminSectionComponent;
|
|
29
|
+
plugin: AdminPlugin;
|
|
30
|
+
pluginContext: PluginContext;
|
|
31
|
+
section: ResolvedSection;
|
|
32
|
+
}
|
|
33
|
+
export declare function useAdminShell(): AdminShellContextValue;
|
|
34
|
+
export declare function AdminShell({ title, plugins: initialPlugins, hostContext, defaultSection, pageSize, refreshInterval, onSectionChange, logo, profileMenu, sidebarFooter, onSettingsClick, onLogoutClick, sectionWrappers, loadingFallback, className }: AdminShellProps): JSX.Element;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
|
|
3
|
+
import { AdminShellContent, AdminShellSidebar } from "./adminShellLayout.js";
|
|
4
|
+
import { canHostAccessAdminSection, readAdminProfile } from "./adminHostServices.js";
|
|
5
|
+
import { createPluginRegistry } from "./pluginRegistry.js";
|
|
6
|
+
import { ProfileDropdown } from "./ProfileDropdown.js";
|
|
7
|
+
const AdminShellContext = createContext(null);
|
|
8
|
+
export function useAdminShell() {
|
|
9
|
+
const context = useContext(AdminShellContext);
|
|
10
|
+
if (!context) {
|
|
11
|
+
throw new Error("useAdminShell must be used within AdminShell");
|
|
12
|
+
}
|
|
13
|
+
return context;
|
|
14
|
+
}
|
|
15
|
+
export function AdminShell({ title = "Admin", plugins: initialPlugins = [], hostContext, defaultSection, pageSize = 20, refreshInterval = 0, onSectionChange, logo, profileMenu, sidebarFooter, onSettingsClick, onLogoutClick, sectionWrappers = [], loadingFallback, className }) {
|
|
16
|
+
const [registry] = useState(() => createPluginRegistry(initialPlugins));
|
|
17
|
+
const [registeredPlugins, setRegisteredPlugins] = useState(() => registry.getAll());
|
|
18
|
+
const [requestedActiveSection, setRequestedActiveSection] = useState(() => defaultSection ?? null);
|
|
19
|
+
const [collapsedGroups, setCollapsedGroups] = useState(new Set());
|
|
20
|
+
useEffect(() => registry.subscribe(setRegisteredPlugins), [registry]);
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
syncRegistryPlugins(registry, initialPlugins);
|
|
23
|
+
setRegisteredPlugins(registry.getAll());
|
|
24
|
+
}, [initialPlugins, registry]);
|
|
25
|
+
const allSections = useMemo(() => resolveVisibleSections(registeredPlugins, hostContext), [registeredPlugins, hostContext]);
|
|
26
|
+
const groupConfigs = useMemo(() => buildGroupConfigMap(registeredPlugins), [registeredPlugins]);
|
|
27
|
+
const groupedSections = useMemo(() => groupSections(allSections, groupConfigs), [allSections, groupConfigs]);
|
|
28
|
+
const activeSection = useMemo(() => resolveActiveSection(requestedActiveSection, allSections), [requestedActiveSection, allSections]);
|
|
29
|
+
const currentSection = useMemo(() => resolveCurrentSectionState(registry, activeSection, hostContext, allSections), [registry, activeSection, hostContext, allSections]);
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (requestedActiveSection !== activeSection) {
|
|
32
|
+
setRequestedActiveSection(activeSection);
|
|
33
|
+
}
|
|
34
|
+
}, [requestedActiveSection, activeSection]);
|
|
35
|
+
const setActiveSection = useCallback((section) => {
|
|
36
|
+
setRequestedActiveSection(section);
|
|
37
|
+
onSectionChange?.(section);
|
|
38
|
+
}, [onSectionChange]);
|
|
39
|
+
const getPluginContext = useCallback((pluginId) => {
|
|
40
|
+
const plugin = registry.get(pluginId);
|
|
41
|
+
return plugin ? plugin.createPluginContext(hostContext) : null;
|
|
42
|
+
}, [registry, hostContext]);
|
|
43
|
+
const toggleGroup = useCallback((groupName) => {
|
|
44
|
+
setCollapsedGroups((previous) => {
|
|
45
|
+
const next = new Set(previous);
|
|
46
|
+
if (next.has(groupName)) {
|
|
47
|
+
next.delete(groupName);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
next.add(groupName);
|
|
51
|
+
}
|
|
52
|
+
return next;
|
|
53
|
+
});
|
|
54
|
+
}, []);
|
|
55
|
+
const contextValue = useMemo(() => ({
|
|
56
|
+
registry,
|
|
57
|
+
hostContext,
|
|
58
|
+
activeSection,
|
|
59
|
+
setActiveSection,
|
|
60
|
+
getPluginContext
|
|
61
|
+
}), [registry, hostContext, activeSection, setActiveSection, getPluginContext]);
|
|
62
|
+
const resolvedProfileMenu = profileMenu === undefined
|
|
63
|
+
? buildDefaultProfileMenu(hostContext, { onSettingsClick, onLogoutClick })
|
|
64
|
+
: profileMenu;
|
|
65
|
+
return (_jsx(AdminShellContext.Provider, { value: contextValue, children: _jsxs("div", { className: joinClassNames("cedros-admin", "cedros-admin-shell", className), children: [_jsx(AdminShellSidebar, { title: title, logo: logo, profileMenu: resolvedProfileMenu, sidebarFooter: sidebarFooter, groupedSections: groupedSections, groupConfigs: groupConfigs, activeSection: activeSection, collapsedGroups: collapsedGroups, onToggleGroup: toggleGroup, onSelectSection: setActiveSection }), _jsx(AdminShellContent, { allSections: allSections, hasRegisteredPlugins: registeredPlugins.length > 0, currentSection: currentSection, hostContext: hostContext, activeSection: activeSection, pageSize: pageSize, refreshInterval: refreshInterval, loadingFallback: loadingFallback, sectionWrappers: sectionWrappers })] }) }));
|
|
66
|
+
}
|
|
67
|
+
function buildDefaultProfileMenu(hostContext, actions) {
|
|
68
|
+
const user = readAdminProfile(hostContext);
|
|
69
|
+
if (!user) {
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
return (_jsx(ProfileDropdown, { name: user.name, email: user.email, picture: user.picture, onSettings: actions.onSettingsClick, onLogout: actions.onLogoutClick }));
|
|
73
|
+
}
|
|
74
|
+
function syncRegistryPlugins(registry, nextPlugins) {
|
|
75
|
+
const nextIds = new Set(nextPlugins.map((plugin) => plugin.id));
|
|
76
|
+
registry.getAll().forEach((plugin) => {
|
|
77
|
+
if (!nextIds.has(plugin.id)) {
|
|
78
|
+
registry.unregister(plugin.id);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
nextPlugins.forEach((plugin) => {
|
|
82
|
+
if (registry.get(plugin.id) !== plugin) {
|
|
83
|
+
registry.register(plugin);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
function resolveVisibleSections(plugins, hostContext) {
|
|
88
|
+
return plugins.flatMap((plugin) => plugin.sections
|
|
89
|
+
.filter((section) => isSectionVisible(section, plugin, hostContext))
|
|
90
|
+
.map((section) => ({
|
|
91
|
+
...section,
|
|
92
|
+
qualifiedId: `${plugin.id}:${section.id}`,
|
|
93
|
+
pluginId: plugin.id,
|
|
94
|
+
cssNamespace: plugin.cssNamespace
|
|
95
|
+
})));
|
|
96
|
+
}
|
|
97
|
+
function isSectionVisible(section, plugin, hostContext) {
|
|
98
|
+
if (section.requiredPermission && !plugin.checkPermission(section.requiredPermission, hostContext)) {
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
101
|
+
return canHostAccessAdminSection(hostContext, section.id);
|
|
102
|
+
}
|
|
103
|
+
function resolveActiveSection(requestedSection, allSections) {
|
|
104
|
+
if (requestedSection && allSections.some((section) => section.qualifiedId === requestedSection)) {
|
|
105
|
+
return requestedSection;
|
|
106
|
+
}
|
|
107
|
+
return allSections[0]?.qualifiedId ?? null;
|
|
108
|
+
}
|
|
109
|
+
function buildGroupConfigMap(plugins) {
|
|
110
|
+
const groups = new Map();
|
|
111
|
+
plugins.forEach((plugin) => {
|
|
112
|
+
plugin.groups?.forEach((group) => {
|
|
113
|
+
if (!groups.has(group.label)) {
|
|
114
|
+
groups.set(group.label, group);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
return groups;
|
|
119
|
+
}
|
|
120
|
+
function groupSections(sections, groupConfigs) {
|
|
121
|
+
const groups = new Map();
|
|
122
|
+
sections.forEach((section) => {
|
|
123
|
+
const groupName = section.group ?? "Menu";
|
|
124
|
+
groups.set(groupName, [...(groups.get(groupName) ?? []), section]);
|
|
125
|
+
});
|
|
126
|
+
return Array.from(groups.entries())
|
|
127
|
+
.sort(([left], [right]) => {
|
|
128
|
+
const leftOrder = groupConfigs.get(left)?.order ?? 99;
|
|
129
|
+
const rightOrder = groupConfigs.get(right)?.order ?? 99;
|
|
130
|
+
return leftOrder - rightOrder;
|
|
131
|
+
})
|
|
132
|
+
.map(([groupName, grouped]) => [groupName, [...grouped].sort(compareSectionsByOrder)]);
|
|
133
|
+
}
|
|
134
|
+
function compareSectionsByOrder(left, right) {
|
|
135
|
+
return (left.order ?? 0) - (right.order ?? 0);
|
|
136
|
+
}
|
|
137
|
+
function resolveCurrentSectionState(registry, activeSection, hostContext, sections) {
|
|
138
|
+
if (!activeSection) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const section = sections.find((entry) => entry.qualifiedId === activeSection);
|
|
142
|
+
const plugin = section ? registry.get(section.pluginId) : undefined;
|
|
143
|
+
const Component = section ? plugin?.components[section.id] : undefined;
|
|
144
|
+
if (!section || !plugin || !Component) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
return {
|
|
148
|
+
Component,
|
|
149
|
+
plugin,
|
|
150
|
+
pluginContext: plugin.createPluginContext(hostContext),
|
|
151
|
+
section
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function joinClassNames(...values) {
|
|
155
|
+
return values.filter(Boolean).join(" ");
|
|
156
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { type JSX } from "react";
|
|
2
|
+
export interface ProfileDropdownProps {
|
|
3
|
+
name?: string;
|
|
4
|
+
email?: string;
|
|
5
|
+
picture?: string;
|
|
6
|
+
onSettings?: () => void;
|
|
7
|
+
onLogout?: () => void;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
export declare function ProfileDropdown({ name, email, picture, onSettings, onLogout, className }: ProfileDropdownProps): JSX.Element;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
|
+
export function ProfileDropdown({ name, email, picture, onSettings, onLogout, className = "" }) {
|
|
4
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
5
|
+
const dropdownRef = useRef(null);
|
|
6
|
+
const displayName = name || "User";
|
|
7
|
+
const initial = (name?.[0] || email?.[0] || "?").toUpperCase();
|
|
8
|
+
useDismissDropdown({
|
|
9
|
+
dropdownRef,
|
|
10
|
+
isOpen,
|
|
11
|
+
onClose: () => setIsOpen(false)
|
|
12
|
+
});
|
|
13
|
+
const handleSettings = useCallback(() => {
|
|
14
|
+
setIsOpen(false);
|
|
15
|
+
onSettings?.();
|
|
16
|
+
}, [onSettings]);
|
|
17
|
+
const handleLogout = useCallback(() => {
|
|
18
|
+
setIsOpen(false);
|
|
19
|
+
onLogout?.();
|
|
20
|
+
}, [onLogout]);
|
|
21
|
+
return (_jsxs("div", { className: `cedros-profile-dropdown ${className}`.trim(), ref: dropdownRef, children: [_jsx(ProfileDropdownTrigger, { displayName: displayName, email: email, picture: picture, initial: initial, isOpen: isOpen, onToggle: () => setIsOpen((previous) => !previous) }), isOpen && _jsx(ProfileDropdownMenu, { onSettings: handleSettings, onLogout: handleLogout })] }));
|
|
22
|
+
}
|
|
23
|
+
function useDismissDropdown({ dropdownRef, isOpen, onClose }) {
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!isOpen) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
function handleClickOutside(event) {
|
|
29
|
+
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
|
|
30
|
+
onClose();
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
document.addEventListener("mousedown", handleClickOutside);
|
|
34
|
+
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
35
|
+
}, [dropdownRef, isOpen, onClose]);
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!isOpen) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
function handleEscape(event) {
|
|
41
|
+
if (event.key === "Escape") {
|
|
42
|
+
onClose();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
document.addEventListener("keydown", handleEscape);
|
|
46
|
+
return () => document.removeEventListener("keydown", handleEscape);
|
|
47
|
+
}, [isOpen, onClose]);
|
|
48
|
+
}
|
|
49
|
+
function ProfileDropdownTrigger({ displayName, email, picture, initial, isOpen, onToggle }) {
|
|
50
|
+
return (_jsxs("button", { type: "button", className: "cedros-profile-dropdown__trigger", onClick: onToggle, "aria-expanded": isOpen, "aria-haspopup": "menu", children: [_jsx("div", { className: "cedros-profile-dropdown__avatar", children: picture ? (_jsx("img", { src: picture, alt: displayName, className: "cedros-profile-dropdown__avatar-img", referrerPolicy: "no-referrer" })) : (_jsx("span", { className: "cedros-profile-dropdown__avatar-placeholder", children: initial })) }), _jsxs("div", { className: "cedros-profile-dropdown__info", children: [_jsx("span", { className: "cedros-profile-dropdown__name", children: displayName }), email && _jsx("span", { className: "cedros-profile-dropdown__email", children: email })] }), _jsx("svg", { className: `cedros-profile-dropdown__chevron ${isOpen ? "cedros-profile-dropdown__chevron--open" : ""}`.trim(), width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "m6 9 6 6 6-6" }) })] }));
|
|
51
|
+
}
|
|
52
|
+
function ProfileDropdownMenu({ onSettings, onLogout }) {
|
|
53
|
+
return (_jsxs("div", { className: "cedros-profile-dropdown__menu", role: "menu", children: [onSettings && (_jsxs("button", { type: "button", className: "cedros-profile-dropdown__item", onClick: onSettings, role: "menuitem", children: [_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" }), _jsx("circle", { cx: "12", cy: "12", r: "3" })] }), "Settings"] })), onLogout && (_jsxs("button", { type: "button", className: "cedros-profile-dropdown__item cedros-profile-dropdown__item--danger", onClick: onLogout, role: "menuitem", children: [_jsxs("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [_jsx("path", { d: "M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4" }), _jsx("polyline", { points: "16 17 21 12 16 7" }), _jsx("line", { x1: "21", x2: "9", y1: "12", y2: "12" })] }), "Log out"] }))] }));
|
|
54
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { AdminHostContext } from "./types.js";
|
|
2
|
+
export interface AdminProfileService {
|
|
3
|
+
getProfile?: () => AdminProfileSummary | null;
|
|
4
|
+
profile?: AdminProfileSummary | null;
|
|
5
|
+
user?: {
|
|
6
|
+
email?: string;
|
|
7
|
+
name?: string;
|
|
8
|
+
picture?: string;
|
|
9
|
+
} | null;
|
|
10
|
+
}
|
|
11
|
+
export interface AdminSectionAccessService {
|
|
12
|
+
canAccess: (sectionId: string) => boolean;
|
|
13
|
+
}
|
|
14
|
+
export interface AdminProfileSummary {
|
|
15
|
+
email?: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
picture?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function readAdminProfile(hostContext: AdminHostContext): AdminProfileSummary | null;
|
|
20
|
+
export declare function canHostAccessAdminSection(hostContext: AdminHostContext, sectionId: string): boolean;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { HOST_SERVICE_IDS, getHostService } from "./platform.js";
|
|
2
|
+
export function readAdminProfile(hostContext) {
|
|
3
|
+
const profileService = getHostService(hostContext, HOST_SERVICE_IDS.adminProfile);
|
|
4
|
+
if (profileService?.getProfile) {
|
|
5
|
+
return profileService.getProfile();
|
|
6
|
+
}
|
|
7
|
+
if (profileService?.profile) {
|
|
8
|
+
return profileService.profile;
|
|
9
|
+
}
|
|
10
|
+
if (profileService?.user) {
|
|
11
|
+
return {
|
|
12
|
+
email: profileService.user.email,
|
|
13
|
+
name: profileService.user.name,
|
|
14
|
+
picture: profileService.user.picture
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
const user = getHostService(hostContext, HOST_SERVICE_IDS.cedrosLogin)?.user;
|
|
18
|
+
if (!user) {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
email: user.email,
|
|
23
|
+
name: user.name,
|
|
24
|
+
picture: user.picture
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export function canHostAccessAdminSection(hostContext, sectionId) {
|
|
28
|
+
const access = getHostService(hostContext, HOST_SERVICE_IDS.adminSectionAccess);
|
|
29
|
+
if (access) {
|
|
30
|
+
return access.canAccess(sectionId);
|
|
31
|
+
}
|
|
32
|
+
const permissions = getHostService(hostContext, HOST_SERVICE_IDS.dashboardPermissions);
|
|
33
|
+
return permissions?.canAccess(sectionId) ?? true;
|
|
34
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type JSX, type ReactNode } from "react";
|
|
2
|
+
import type { AdminGroupConfig, AdminShellSectionWrapper, HostContext, QualifiedSectionId, ResolvedSection } from "./types.js";
|
|
3
|
+
import type { CurrentSectionState } from "./AdminShell.js";
|
|
4
|
+
interface AdminShellSidebarProps {
|
|
5
|
+
title: string;
|
|
6
|
+
logo?: ReactNode;
|
|
7
|
+
profileMenu?: ReactNode;
|
|
8
|
+
sidebarFooter?: ReactNode;
|
|
9
|
+
groupedSections: Array<[string, ResolvedSection[]]>;
|
|
10
|
+
groupConfigs: Map<string, AdminGroupConfig>;
|
|
11
|
+
activeSection: QualifiedSectionId | null;
|
|
12
|
+
collapsedGroups: Set<string>;
|
|
13
|
+
onToggleGroup: (groupName: string) => void;
|
|
14
|
+
onSelectSection: (section: QualifiedSectionId) => void;
|
|
15
|
+
}
|
|
16
|
+
interface AdminShellContentProps {
|
|
17
|
+
allSections: ResolvedSection[];
|
|
18
|
+
hasRegisteredPlugins: boolean;
|
|
19
|
+
currentSection: CurrentSectionState | null;
|
|
20
|
+
hostContext: HostContext;
|
|
21
|
+
activeSection: QualifiedSectionId | null;
|
|
22
|
+
pageSize: number;
|
|
23
|
+
refreshInterval: number;
|
|
24
|
+
loadingFallback?: ReactNode;
|
|
25
|
+
sectionWrappers: AdminShellSectionWrapper[];
|
|
26
|
+
}
|
|
27
|
+
export declare function AdminShellSidebar({ title, logo, profileMenu, sidebarFooter, groupedSections, groupConfigs, activeSection, collapsedGroups, onToggleGroup, onSelectSection }: AdminShellSidebarProps): JSX.Element;
|
|
28
|
+
export declare function AdminShellContent({ allSections, hasRegisteredPlugins, currentSection, hostContext, activeSection, pageSize, refreshInterval, loadingFallback, sectionWrappers }: AdminShellContentProps): JSX.Element;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Suspense } from "react";
|
|
3
|
+
export function AdminShellSidebar({ title, logo, profileMenu, sidebarFooter, groupedSections, groupConfigs, activeSection, collapsedGroups, onToggleGroup, onSelectSection }) {
|
|
4
|
+
return (_jsxs("aside", { className: "cedros-admin-shell__sidebar", children: [_jsx("div", { className: "cedros-admin-shell__sidebar-header", children: logo ?? (_jsx("div", { className: "cedros-admin-shell__logo", children: _jsx("span", { className: "cedros-admin-shell__logo-text", children: title }) })) }), _jsx("nav", { className: "cedros-admin-shell__nav", children: groupedSections.map(([groupName, sections]) => (_jsx(SectionNavigationGroup, { groupName: groupName, sections: sections, groupConfig: groupConfigs.get(groupName), activeSection: activeSection, isCollapsed: collapsedGroups.has(groupName), onToggleGroup: onToggleGroup, onSelectSection: onSelectSection }, groupName))) }), (profileMenu || sidebarFooter) && (_jsxs("div", { className: "cedros-admin-shell__sidebar-footer", children: [profileMenu, sidebarFooter] }))] }));
|
|
5
|
+
}
|
|
6
|
+
export function AdminShellContent({ allSections, hasRegisteredPlugins, currentSection, hostContext, activeSection, pageSize, refreshInterval, loadingFallback, sectionWrappers }) {
|
|
7
|
+
return (_jsx("main", { className: "cedros-admin-shell__main", children: currentSection ? (wrapSectionContent(_jsx(Suspense, { fallback: loadingFallback ?? _jsx(DefaultLoadingFallback, {}), children: _jsx("div", { className: "cedros-admin-shell__section", "data-plugin-namespace": currentSection.plugin.cssNamespace, children: _jsx(currentSection.Component, { pluginContext: currentSection.pluginContext, pageSize: pageSize, refreshInterval: refreshInterval }) }) }), sectionWrappers, hostContext, activeSection, currentSection)) : (_jsx("div", { className: "cedros-admin-shell__empty", children: buildEmptyStateMessage(allSections, hasRegisteredPlugins) })) }));
|
|
8
|
+
}
|
|
9
|
+
function SectionNavigationGroup({ groupName, sections, groupConfig, activeSection, isCollapsed, onToggleGroup, onSelectSection }) {
|
|
10
|
+
const collapsible = isGroupCollapsible(groupName, groupConfig);
|
|
11
|
+
return (_jsxs("div", { className: "cedros-admin-shell__nav-group", children: [_jsx(GroupLabel, { groupName: groupName, collapsible: collapsible, isCollapsed: isCollapsed, onToggleGroup: onToggleGroup }), (!collapsible || !isCollapsed) &&
|
|
12
|
+
sections.map((section) => (_jsx(SectionNavigationButton, { section: section, activeSection: activeSection, onSelectSection: onSelectSection }, section.qualifiedId)))] }));
|
|
13
|
+
}
|
|
14
|
+
function GroupLabel({ groupName, collapsible, isCollapsed, onToggleGroup }) {
|
|
15
|
+
if (!collapsible) {
|
|
16
|
+
return _jsx("span", { className: "cedros-admin-shell__nav-label", children: groupName });
|
|
17
|
+
}
|
|
18
|
+
return (_jsxs("button", { type: "button", className: "cedros-admin-shell__nav-label cedros-admin-shell__nav-label--collapsible", onClick: () => onToggleGroup(groupName), "aria-expanded": !isCollapsed, children: [_jsx("span", { children: groupName }), _jsx("span", { className: joinClassNames("cedros-admin-shell__nav-chevron", !isCollapsed ? "cedros-admin-shell__nav-chevron--expanded" : undefined), children: _jsx("svg", { width: "16", height: "16", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: _jsx("path", { d: "m9 18 6-6-6-6" }) }) })] }));
|
|
19
|
+
}
|
|
20
|
+
function SectionNavigationButton({ section, activeSection, onSelectSection }) {
|
|
21
|
+
return (_jsxs("button", { type: "button", className: joinClassNames("cedros-admin-shell__nav-item", activeSection === section.qualifiedId ? "cedros-admin-shell__nav-item--active" : undefined), onClick: () => onSelectSection(section.qualifiedId), "aria-current": activeSection === section.qualifiedId ? "page" : undefined, children: [_jsx("span", { className: "cedros-admin-shell__nav-icon", children: renderSectionIcon(section.icon) }), _jsx("span", { className: "cedros-admin-shell__nav-text", children: section.label }), section.badge && _jsx("span", { className: "cedros-admin-shell__nav-badge", children: section.badge })] }));
|
|
22
|
+
}
|
|
23
|
+
function renderSectionIcon(icon) {
|
|
24
|
+
if (typeof icon === "function") {
|
|
25
|
+
return icon();
|
|
26
|
+
}
|
|
27
|
+
return icon;
|
|
28
|
+
}
|
|
29
|
+
function isGroupCollapsible(groupName, groupConfig) {
|
|
30
|
+
return groupName === "Configuration" || groupConfig?.defaultCollapsed !== undefined;
|
|
31
|
+
}
|
|
32
|
+
function joinClassNames(...values) {
|
|
33
|
+
return values.filter(Boolean).join(" ");
|
|
34
|
+
}
|
|
35
|
+
function wrapSectionContent(content, wrappers, hostContext, activeSection, currentSection) {
|
|
36
|
+
return wrappers.reduceRight((children, Wrapper) => (_jsx(Wrapper, { hostContext: hostContext, activeSection: activeSection, currentPlugin: currentSection.plugin, currentSection: currentSection.section, pluginContext: currentSection.pluginContext, children: children })), content);
|
|
37
|
+
}
|
|
38
|
+
function buildEmptyStateMessage(allSections, hasRegisteredPlugins) {
|
|
39
|
+
if (allSections.length === 0) {
|
|
40
|
+
return hasRegisteredPlugins ? "No sections available" : "No plugins loaded";
|
|
41
|
+
}
|
|
42
|
+
return "Select a section from the sidebar";
|
|
43
|
+
}
|
|
44
|
+
function DefaultLoadingFallback() {
|
|
45
|
+
return (_jsxs("div", { className: "cedros-admin-shell__loading", children: [_jsx("span", { className: "cedros-admin-shell__loading-spinner", "aria-hidden": "true" }), _jsx("span", { children: "Loading..." })] }));
|
|
46
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export { AdminShell, useAdminShell } from "./AdminShell.js";
|
|
2
|
+
export type { AdminShellContextValue, AdminShellProps, CurrentSectionState } from "./AdminShell.js";
|
|
3
|
+
export type { AdminSectionComponent, AdminGroupConfig, AdminHostContext, AdminHostServiceBag, AdminPlugin, AdminSectionConfig, AdminSectionProps, AdminShellSectionWrapper, AdminShellSectionWrapperProps, HostContext, HostServiceId, PluginContext, PluginId, PluginPermission, PluginRegistry, QualifiedSectionId, ResolvedSection, SectionId } from "./types.js";
|
|
4
|
+
export { HOST_SERVICE_IDS, buildAdminHostServiceBag, createAdminHostServiceBag, getHostService, getServiceFromBag } from "./platform.js";
|
|
5
|
+
export { createPluginRegistry } from "./pluginRegistry.js";
|
|
6
|
+
export { canHostAccessAdminSection, readAdminProfile } from "./adminHostServices.js";
|
|
7
|
+
export type { AdminProfileService, AdminProfileSummary, AdminSectionAccessService } from "./adminHostServices.js";
|
|
8
|
+
export { ProfileDropdown } from "./ProfileDropdown.js";
|
|
9
|
+
export type { ProfileDropdownProps } from "./ProfileDropdown.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { AdminShell, useAdminShell } from "./AdminShell.js";
|
|
2
|
+
export { HOST_SERVICE_IDS, buildAdminHostServiceBag, createAdminHostServiceBag, getHostService, getServiceFromBag } from "./platform.js";
|
|
3
|
+
export { createPluginRegistry } from "./pluginRegistry.js";
|
|
4
|
+
export { canHostAccessAdminSection, readAdminProfile } from "./adminHostServices.js";
|
|
5
|
+
export { ProfileDropdown } from "./ProfileDropdown.js";
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { AdminHostContext, AdminHostServiceBag, HostServiceId } from "./types.js";
|
|
2
|
+
export declare const HOST_SERVICE_IDS: {
|
|
3
|
+
readonly adminProfile: "admin-profile";
|
|
4
|
+
readonly adminSectionAccess: "admin-section-access";
|
|
5
|
+
readonly cedrosLogin: "cedros-login";
|
|
6
|
+
readonly cedrosPay: "cedros-pay";
|
|
7
|
+
readonly org: "org";
|
|
8
|
+
readonly dashboardPermissions: "dashboard-permissions";
|
|
9
|
+
};
|
|
10
|
+
export declare function createAdminHostServiceBag(initialServices?: AdminHostServiceBag): AdminHostServiceBag;
|
|
11
|
+
export declare function buildAdminHostServiceBag(hostContext: AdminHostContext): AdminHostServiceBag;
|
|
12
|
+
export declare function getHostService<T>(hostContext: AdminHostContext, serviceId: HostServiceId): T | undefined;
|
|
13
|
+
export declare function getServiceFromBag<T>(services: AdminHostServiceBag, serviceId: HostServiceId): T | undefined;
|
package/dist/platform.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const HOST_SERVICE_IDS = {
|
|
2
|
+
adminProfile: "admin-profile",
|
|
3
|
+
adminSectionAccess: "admin-section-access",
|
|
4
|
+
cedrosLogin: "cedros-login",
|
|
5
|
+
cedrosPay: "cedros-pay",
|
|
6
|
+
org: "org",
|
|
7
|
+
dashboardPermissions: "dashboard-permissions"
|
|
8
|
+
};
|
|
9
|
+
export function createAdminHostServiceBag(initialServices = {}) {
|
|
10
|
+
return { ...initialServices };
|
|
11
|
+
}
|
|
12
|
+
export function buildAdminHostServiceBag(hostContext) {
|
|
13
|
+
const services = createAdminHostServiceBag(hostContext.services);
|
|
14
|
+
if (!(HOST_SERVICE_IDS.adminProfile in services) && hostContext.cedrosLogin?.user) {
|
|
15
|
+
services[HOST_SERVICE_IDS.adminProfile] = {
|
|
16
|
+
user: hostContext.cedrosLogin.user
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
if (!(HOST_SERVICE_IDS.adminSectionAccess in services) && hostContext.dashboardPermissions) {
|
|
20
|
+
services[HOST_SERVICE_IDS.adminSectionAccess] = hostContext.dashboardPermissions;
|
|
21
|
+
}
|
|
22
|
+
if (!(HOST_SERVICE_IDS.cedrosLogin in services) && hostContext.cedrosLogin) {
|
|
23
|
+
services[HOST_SERVICE_IDS.cedrosLogin] = hostContext.cedrosLogin;
|
|
24
|
+
}
|
|
25
|
+
if (!(HOST_SERVICE_IDS.cedrosPay in services) && hostContext.cedrosPay) {
|
|
26
|
+
services[HOST_SERVICE_IDS.cedrosPay] = hostContext.cedrosPay;
|
|
27
|
+
}
|
|
28
|
+
if (!(HOST_SERVICE_IDS.org in services) && hostContext.org) {
|
|
29
|
+
services[HOST_SERVICE_IDS.org] = hostContext.org;
|
|
30
|
+
}
|
|
31
|
+
if (!(HOST_SERVICE_IDS.dashboardPermissions in services) && hostContext.dashboardPermissions) {
|
|
32
|
+
services[HOST_SERVICE_IDS.dashboardPermissions] = hostContext.dashboardPermissions;
|
|
33
|
+
}
|
|
34
|
+
return services;
|
|
35
|
+
}
|
|
36
|
+
export function getHostService(hostContext, serviceId) {
|
|
37
|
+
return getServiceFromBag(buildAdminHostServiceBag(hostContext), serviceId);
|
|
38
|
+
}
|
|
39
|
+
export function getServiceFromBag(services, serviceId) {
|
|
40
|
+
return services[serviceId];
|
|
41
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
class PluginRegistryStore {
|
|
2
|
+
plugins = new Map();
|
|
3
|
+
listeners = new Set();
|
|
4
|
+
register(plugin) {
|
|
5
|
+
this.plugins.set(plugin.id, plugin);
|
|
6
|
+
this.notify();
|
|
7
|
+
}
|
|
8
|
+
unregister(pluginId) {
|
|
9
|
+
if (!this.plugins.has(pluginId)) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
this.plugins.delete(pluginId);
|
|
13
|
+
this.notify();
|
|
14
|
+
}
|
|
15
|
+
get(pluginId) {
|
|
16
|
+
return this.plugins.get(pluginId);
|
|
17
|
+
}
|
|
18
|
+
getAll() {
|
|
19
|
+
return Array.from(this.plugins.values());
|
|
20
|
+
}
|
|
21
|
+
subscribe(listener) {
|
|
22
|
+
this.listeners.add(listener);
|
|
23
|
+
return () => this.listeners.delete(listener);
|
|
24
|
+
}
|
|
25
|
+
notify() {
|
|
26
|
+
const plugins = this.getAll();
|
|
27
|
+
this.listeners.forEach((listener) => listener(plugins));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function createPluginRegistry(initialPlugins = []) {
|
|
31
|
+
const registry = new PluginRegistryStore();
|
|
32
|
+
initialPlugins.forEach((plugin) => registry.register(plugin));
|
|
33
|
+
return registry;
|
|
34
|
+
}
|
package/dist/shell.css
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
.cedros-admin {
|
|
2
|
+
--admin-bg: var(--cedros-background, #ffffff);
|
|
3
|
+
--admin-fg: var(--cedros-foreground, #0f172a);
|
|
4
|
+
--admin-muted: var(--cedros-muted-foreground, #64748b);
|
|
5
|
+
--admin-muted-bg: var(--cedros-muted, #f1f5f9);
|
|
6
|
+
--admin-border: var(--cedros-border, #e2e8f0);
|
|
7
|
+
--admin-accent: var(--cedros-accent, #f8fafc);
|
|
8
|
+
--admin-card-bg: color-mix(in srgb, var(--admin-bg) 94%, var(--admin-accent) 6%);
|
|
9
|
+
--admin-ring: var(--cedros-ring, #0f172a);
|
|
10
|
+
--admin-primary: var(--cedros-primary, #0f172a);
|
|
11
|
+
--admin-primary-fg: var(--cedros-primary-foreground, #f8fafc);
|
|
12
|
+
--admin-radius: 0.75rem;
|
|
13
|
+
--admin-font: var(
|
|
14
|
+
--cedros-font-sans,
|
|
15
|
+
"IBM Plex Sans",
|
|
16
|
+
"Avenir Next",
|
|
17
|
+
"Segoe UI",
|
|
18
|
+
sans-serif
|
|
19
|
+
);
|
|
20
|
+
--admin-font-mono: var(
|
|
21
|
+
--cedros-font-mono,
|
|
22
|
+
ui-monospace,
|
|
23
|
+
SFMono-Regular,
|
|
24
|
+
Menlo,
|
|
25
|
+
Consolas,
|
|
26
|
+
monospace
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.cedros-dark .cedros-admin,
|
|
31
|
+
.cedros-admin.cedros-admin--dark {
|
|
32
|
+
--admin-bg: var(--cedros-background, #09090b);
|
|
33
|
+
--admin-fg: var(--cedros-foreground, #fafafa);
|
|
34
|
+
--admin-muted: var(--cedros-muted-foreground, #a1a1aa);
|
|
35
|
+
--admin-muted-bg: var(--cedros-muted, #18181b);
|
|
36
|
+
--admin-border: var(--cedros-border, #27272a);
|
|
37
|
+
--admin-accent: var(--cedros-accent, #18181b);
|
|
38
|
+
--admin-card-bg: color-mix(in srgb, var(--admin-bg) 90%, var(--admin-accent) 10%);
|
|
39
|
+
--admin-ring: var(--cedros-ring, #fafafa);
|
|
40
|
+
--admin-primary: var(--cedros-primary, #fafafa);
|
|
41
|
+
--admin-primary-fg: var(--cedros-primary-foreground, #09090b);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.cedros-admin-shell {
|
|
45
|
+
display: grid;
|
|
46
|
+
grid-template-columns: 220px 1fr;
|
|
47
|
+
grid-template-rows: 1fr;
|
|
48
|
+
min-height: 400px;
|
|
49
|
+
height: 100%;
|
|
50
|
+
overflow: hidden;
|
|
51
|
+
border: 1px solid var(--admin-border);
|
|
52
|
+
border-radius: var(--admin-radius);
|
|
53
|
+
background: var(--admin-bg);
|
|
54
|
+
color: var(--admin-fg);
|
|
55
|
+
font-family: var(--admin-font);
|
|
56
|
+
font-size: 0.875rem;
|
|
57
|
+
line-height: 1.5;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.cedros-admin-shell__sidebar {
|
|
61
|
+
display: flex;
|
|
62
|
+
min-height: 0;
|
|
63
|
+
flex-direction: column;
|
|
64
|
+
border-right: 1px solid var(--admin-border);
|
|
65
|
+
background: color-mix(in srgb, var(--admin-bg) 88%, var(--admin-accent) 12%);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.cedros-admin-shell__sidebar-header {
|
|
69
|
+
padding: 1rem 0.75rem;
|
|
70
|
+
border-bottom: 1px solid var(--admin-border);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.cedros-admin-shell__logo {
|
|
74
|
+
display: flex;
|
|
75
|
+
align-items: center;
|
|
76
|
+
gap: 0.5rem;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.cedros-admin-shell__logo-text {
|
|
80
|
+
font-size: 0.875rem;
|
|
81
|
+
font-weight: 600;
|
|
82
|
+
letter-spacing: -0.01em;
|
|
83
|
+
color: var(--admin-fg);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.cedros-admin-shell__nav {
|
|
87
|
+
flex: 1;
|
|
88
|
+
overflow-y: auto;
|
|
89
|
+
padding: 0.5rem;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.cedros-admin-shell__nav-group {
|
|
93
|
+
display: flex;
|
|
94
|
+
flex-direction: column;
|
|
95
|
+
gap: 0.125rem;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.cedros-admin-shell__nav-group + .cedros-admin-shell__nav-group {
|
|
99
|
+
margin-top: 1rem;
|
|
100
|
+
padding-top: 0.5rem;
|
|
101
|
+
border-top: 1px solid var(--admin-border);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
.cedros-admin-shell__nav-label {
|
|
105
|
+
padding: 0.5rem 0.75rem 0.375rem;
|
|
106
|
+
font-size: 0.6875rem;
|
|
107
|
+
font-weight: 600;
|
|
108
|
+
letter-spacing: 0.05em;
|
|
109
|
+
text-transform: uppercase;
|
|
110
|
+
color: var(--admin-muted);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.cedros-admin-shell__nav-label--collapsible {
|
|
114
|
+
display: flex;
|
|
115
|
+
width: 100%;
|
|
116
|
+
align-items: center;
|
|
117
|
+
justify-content: space-between;
|
|
118
|
+
border: none;
|
|
119
|
+
background: none;
|
|
120
|
+
cursor: pointer;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.cedros-admin-shell__nav-chevron {
|
|
124
|
+
display: flex;
|
|
125
|
+
width: 1rem;
|
|
126
|
+
height: 1rem;
|
|
127
|
+
align-items: center;
|
|
128
|
+
justify-content: center;
|
|
129
|
+
transition: transform 160ms ease;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
.cedros-admin-shell__nav-chevron svg {
|
|
133
|
+
width: 0.625rem;
|
|
134
|
+
height: 0.625rem;
|
|
135
|
+
opacity: 0.6;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.cedros-admin-shell__nav-chevron--expanded {
|
|
139
|
+
transform: rotate(90deg);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.cedros-admin-shell__nav-item {
|
|
143
|
+
display: flex;
|
|
144
|
+
width: 100%;
|
|
145
|
+
align-items: center;
|
|
146
|
+
gap: 0.625rem;
|
|
147
|
+
border: none;
|
|
148
|
+
border-radius: calc(var(--admin-radius) - 4px);
|
|
149
|
+
background: transparent;
|
|
150
|
+
color: var(--admin-muted);
|
|
151
|
+
padding: 0.5rem 0.75rem;
|
|
152
|
+
font: inherit;
|
|
153
|
+
font-size: 0.8125rem;
|
|
154
|
+
font-weight: 500;
|
|
155
|
+
line-height: 1.5;
|
|
156
|
+
text-align: left;
|
|
157
|
+
cursor: pointer;
|
|
158
|
+
transition:
|
|
159
|
+
background 140ms ease,
|
|
160
|
+
color 140ms ease,
|
|
161
|
+
transform 140ms ease;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.cedros-admin-shell__nav-item:hover {
|
|
165
|
+
background: var(--admin-accent);
|
|
166
|
+
color: var(--admin-fg);
|
|
167
|
+
transform: translateX(1px);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.cedros-admin-shell__nav-item--active,
|
|
171
|
+
.cedros-admin-shell__nav-item--active:hover {
|
|
172
|
+
background: var(--admin-accent);
|
|
173
|
+
color: var(--admin-fg);
|
|
174
|
+
transform: none;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.cedros-admin-shell__nav-icon {
|
|
178
|
+
display: flex;
|
|
179
|
+
width: 1rem;
|
|
180
|
+
height: 1rem;
|
|
181
|
+
flex-shrink: 0;
|
|
182
|
+
align-items: center;
|
|
183
|
+
justify-content: center;
|
|
184
|
+
opacity: 0.75;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
.cedros-admin-shell__nav-item:hover .cedros-admin-shell__nav-icon,
|
|
188
|
+
.cedros-admin-shell__nav-item--active .cedros-admin-shell__nav-icon {
|
|
189
|
+
opacity: 1;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.cedros-admin-shell__nav-text {
|
|
193
|
+
white-space: nowrap;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
.cedros-admin-shell__nav-badge {
|
|
197
|
+
display: inline-flex;
|
|
198
|
+
min-width: 1.25rem;
|
|
199
|
+
height: 1.25rem;
|
|
200
|
+
align-items: center;
|
|
201
|
+
justify-content: center;
|
|
202
|
+
margin-left: auto;
|
|
203
|
+
padding: 0 0.375rem;
|
|
204
|
+
border-radius: 9999px;
|
|
205
|
+
background: var(--admin-primary);
|
|
206
|
+
color: var(--admin-primary-fg);
|
|
207
|
+
font-size: 0.6875rem;
|
|
208
|
+
font-weight: 600;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
.cedros-admin-shell__sidebar-footer {
|
|
212
|
+
display: flex;
|
|
213
|
+
flex-direction: column;
|
|
214
|
+
gap: 0.75rem;
|
|
215
|
+
margin-top: auto;
|
|
216
|
+
padding: 0.75rem;
|
|
217
|
+
border-top: 1px solid var(--admin-border);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.cedros-profile-dropdown {
|
|
221
|
+
position: relative;
|
|
222
|
+
width: 100%;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
.cedros-profile-dropdown__trigger {
|
|
226
|
+
display: flex;
|
|
227
|
+
width: 100%;
|
|
228
|
+
align-items: center;
|
|
229
|
+
gap: 0.625rem;
|
|
230
|
+
padding: 0.5rem;
|
|
231
|
+
border: none;
|
|
232
|
+
border-radius: calc(var(--admin-radius) - 4px);
|
|
233
|
+
background: var(--admin-accent);
|
|
234
|
+
color: var(--admin-fg);
|
|
235
|
+
font: inherit;
|
|
236
|
+
text-align: left;
|
|
237
|
+
cursor: pointer;
|
|
238
|
+
transition: background 150ms ease;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.cedros-profile-dropdown__trigger:hover {
|
|
242
|
+
background: color-mix(in srgb, var(--admin-accent) 80%, var(--admin-fg) 20%);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
.cedros-profile-dropdown__avatar {
|
|
246
|
+
width: 2rem;
|
|
247
|
+
height: 2rem;
|
|
248
|
+
border-radius: 9999px;
|
|
249
|
+
flex-shrink: 0;
|
|
250
|
+
overflow: hidden;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.cedros-profile-dropdown__avatar-img {
|
|
254
|
+
width: 100%;
|
|
255
|
+
height: 100%;
|
|
256
|
+
object-fit: cover;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.cedros-profile-dropdown__avatar-placeholder {
|
|
260
|
+
display: flex;
|
|
261
|
+
width: 100%;
|
|
262
|
+
height: 100%;
|
|
263
|
+
align-items: center;
|
|
264
|
+
justify-content: center;
|
|
265
|
+
background: var(--admin-muted-bg);
|
|
266
|
+
color: var(--admin-muted);
|
|
267
|
+
font-size: 0.75rem;
|
|
268
|
+
font-weight: 600;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
.cedros-profile-dropdown__info {
|
|
272
|
+
display: flex;
|
|
273
|
+
min-width: 0;
|
|
274
|
+
flex: 1;
|
|
275
|
+
flex-direction: column;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
.cedros-profile-dropdown__name {
|
|
279
|
+
overflow: hidden;
|
|
280
|
+
font-size: 0.8125rem;
|
|
281
|
+
font-weight: 500;
|
|
282
|
+
text-overflow: ellipsis;
|
|
283
|
+
white-space: nowrap;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
.cedros-profile-dropdown__email {
|
|
287
|
+
overflow: hidden;
|
|
288
|
+
color: var(--admin-muted);
|
|
289
|
+
font-size: 0.6875rem;
|
|
290
|
+
text-overflow: ellipsis;
|
|
291
|
+
white-space: nowrap;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
.cedros-profile-dropdown__chevron {
|
|
295
|
+
flex-shrink: 0;
|
|
296
|
+
opacity: 0.5;
|
|
297
|
+
transition: transform 200ms ease;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
.cedros-profile-dropdown__chevron--open {
|
|
301
|
+
transform: rotate(180deg);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
.cedros-profile-dropdown__menu {
|
|
305
|
+
position: absolute;
|
|
306
|
+
right: 0;
|
|
307
|
+
bottom: 100%;
|
|
308
|
+
left: 0;
|
|
309
|
+
z-index: 100;
|
|
310
|
+
margin-bottom: 0.25rem;
|
|
311
|
+
padding: 0.25rem;
|
|
312
|
+
border: 1px solid var(--admin-border);
|
|
313
|
+
border-radius: calc(var(--admin-radius) - 4px);
|
|
314
|
+
background: var(--admin-card-bg);
|
|
315
|
+
box-shadow: 0 4px 12px rgb(0 0 0 / 0.15);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
.cedros-profile-dropdown__item {
|
|
319
|
+
display: flex;
|
|
320
|
+
width: 100%;
|
|
321
|
+
align-items: center;
|
|
322
|
+
gap: 0.5rem;
|
|
323
|
+
padding: 0.5rem 0.625rem;
|
|
324
|
+
border: none;
|
|
325
|
+
border-radius: calc(var(--admin-radius) - 6px);
|
|
326
|
+
background: transparent;
|
|
327
|
+
color: var(--admin-fg);
|
|
328
|
+
font: inherit;
|
|
329
|
+
font-size: 0.8125rem;
|
|
330
|
+
text-align: left;
|
|
331
|
+
cursor: pointer;
|
|
332
|
+
transition: background 150ms ease;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.cedros-profile-dropdown__item:hover {
|
|
336
|
+
background: var(--admin-accent);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.cedros-profile-dropdown__item svg {
|
|
340
|
+
flex-shrink: 0;
|
|
341
|
+
opacity: 0.7;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
.cedros-profile-dropdown__item--danger {
|
|
345
|
+
color: var(--cedros-error, #ef4444);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
.cedros-profile-dropdown__item--danger:hover {
|
|
349
|
+
background: color-mix(in srgb, var(--cedros-error, #ef4444) 10%, transparent);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
.cedros-admin-shell__main {
|
|
353
|
+
display: flex;
|
|
354
|
+
min-width: 0;
|
|
355
|
+
min-height: 0;
|
|
356
|
+
flex-direction: column;
|
|
357
|
+
overflow: hidden;
|
|
358
|
+
background: var(--admin-bg);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
.cedros-admin-shell__section {
|
|
362
|
+
min-height: 100%;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
.cedros-admin-shell__section[data-plugin-namespace="cedros-dashboard"] {
|
|
366
|
+
display: flex;
|
|
367
|
+
min-height: 0;
|
|
368
|
+
flex-direction: column;
|
|
369
|
+
overflow-y: auto;
|
|
370
|
+
padding: 1.5rem;
|
|
371
|
+
scrollbar-gutter: stable;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.cedros-admin-shell__loading,
|
|
375
|
+
.cedros-admin-shell__empty {
|
|
376
|
+
display: flex;
|
|
377
|
+
min-height: 200px;
|
|
378
|
+
align-items: center;
|
|
379
|
+
justify-content: center;
|
|
380
|
+
color: var(--admin-muted);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
.cedros-admin-shell__loading {
|
|
384
|
+
flex-direction: column;
|
|
385
|
+
gap: 0.875rem;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
.cedros-admin-shell__loading-spinner {
|
|
389
|
+
width: 1.5rem;
|
|
390
|
+
height: 1.5rem;
|
|
391
|
+
border: 2px solid color-mix(in srgb, var(--admin-fg) 12%, transparent);
|
|
392
|
+
border-top-color: var(--admin-fg);
|
|
393
|
+
border-radius: 9999px;
|
|
394
|
+
animation: cedros-admin-shell-spin 800ms linear infinite;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
@keyframes cedros-admin-shell-spin {
|
|
398
|
+
to {
|
|
399
|
+
transform: rotate(360deg);
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
@media (max-width: 768px) {
|
|
404
|
+
.cedros-admin-shell {
|
|
405
|
+
grid-template-columns: 1fr;
|
|
406
|
+
grid-template-rows: auto 1fr;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
.cedros-admin-shell__sidebar {
|
|
410
|
+
border-right: none;
|
|
411
|
+
border-bottom: 1px solid var(--admin-border);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
.cedros-admin-shell__nav-group {
|
|
415
|
+
flex-direction: row;
|
|
416
|
+
overflow-x: auto;
|
|
417
|
+
gap: 0.25rem;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.cedros-admin-shell__nav-group + .cedros-admin-shell__nav-group {
|
|
421
|
+
margin-top: 0.75rem;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.cedros-admin-shell__nav-label {
|
|
425
|
+
display: none;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.cedros-admin-shell__nav-item {
|
|
429
|
+
flex-shrink: 0;
|
|
430
|
+
padding: 0.5rem 0.625rem;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
.cedros-admin-shell__nav-text {
|
|
434
|
+
display: none;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.cedros-admin-shell__sidebar-footer {
|
|
438
|
+
display: none;
|
|
439
|
+
}
|
|
440
|
+
}
|
package/dist/styles.css
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@import "./shell.css";
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export type PluginId = string;
|
|
3
|
+
export type SectionId = string;
|
|
4
|
+
export type QualifiedSectionId = `${PluginId}:${SectionId}`;
|
|
5
|
+
export type PluginPermission = string;
|
|
6
|
+
export type HostServiceId = string;
|
|
7
|
+
export type AdminSectionIcon = ReactNode | (() => ReactNode);
|
|
8
|
+
export interface AdminSectionConfig {
|
|
9
|
+
id: SectionId;
|
|
10
|
+
label: string;
|
|
11
|
+
icon: AdminSectionIcon;
|
|
12
|
+
group?: string;
|
|
13
|
+
order?: number;
|
|
14
|
+
requiredPermission?: PluginPermission;
|
|
15
|
+
badge?: ReactNode;
|
|
16
|
+
}
|
|
17
|
+
export interface AdminGroupConfig {
|
|
18
|
+
id: string;
|
|
19
|
+
label: string;
|
|
20
|
+
order: number;
|
|
21
|
+
icon?: ReactNode;
|
|
22
|
+
defaultCollapsed?: boolean;
|
|
23
|
+
}
|
|
24
|
+
export interface AdminSectionProps {
|
|
25
|
+
pluginContext: PluginContext;
|
|
26
|
+
pageSize: number;
|
|
27
|
+
refreshInterval: number;
|
|
28
|
+
}
|
|
29
|
+
export type AdminSectionComponent = (props: AdminSectionProps) => ReactNode;
|
|
30
|
+
export interface PluginContext {
|
|
31
|
+
serverUrl: string;
|
|
32
|
+
userId?: string;
|
|
33
|
+
getAccessToken: () => string | null;
|
|
34
|
+
hasPermission: (permission: PluginPermission) => boolean;
|
|
35
|
+
orgId?: string;
|
|
36
|
+
pluginData?: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
export interface HostContext {
|
|
39
|
+
cedrosLogin?: {
|
|
40
|
+
user: {
|
|
41
|
+
id: string;
|
|
42
|
+
email?: string;
|
|
43
|
+
name?: string;
|
|
44
|
+
picture?: string;
|
|
45
|
+
} | null;
|
|
46
|
+
getAccessToken: () => string | null;
|
|
47
|
+
serverUrl: string;
|
|
48
|
+
};
|
|
49
|
+
cedrosPay?: {
|
|
50
|
+
walletAddress?: string;
|
|
51
|
+
jwtToken?: string;
|
|
52
|
+
serverUrl: string;
|
|
53
|
+
};
|
|
54
|
+
org?: {
|
|
55
|
+
orgId: string;
|
|
56
|
+
role: string;
|
|
57
|
+
permissions: string[];
|
|
58
|
+
};
|
|
59
|
+
dashboardPermissions?: {
|
|
60
|
+
canAccess: (sectionId: string) => boolean;
|
|
61
|
+
};
|
|
62
|
+
services?: Record<HostServiceId, unknown>;
|
|
63
|
+
custom?: Record<string, unknown>;
|
|
64
|
+
}
|
|
65
|
+
export interface AdminShellSectionWrapperProps {
|
|
66
|
+
children: ReactNode;
|
|
67
|
+
hostContext: HostContext;
|
|
68
|
+
activeSection: QualifiedSectionId | null;
|
|
69
|
+
currentPlugin: AdminPlugin | null;
|
|
70
|
+
currentSection: ResolvedSection | null;
|
|
71
|
+
pluginContext: PluginContext | null;
|
|
72
|
+
}
|
|
73
|
+
export type AdminShellSectionWrapper = (props: AdminShellSectionWrapperProps) => ReactNode;
|
|
74
|
+
export interface AdminPlugin {
|
|
75
|
+
id: PluginId;
|
|
76
|
+
name: string;
|
|
77
|
+
version: string;
|
|
78
|
+
sections: AdminSectionConfig[];
|
|
79
|
+
groups?: AdminGroupConfig[];
|
|
80
|
+
components: Record<SectionId, AdminSectionComponent>;
|
|
81
|
+
createPluginContext: (hostContext: HostContext) => PluginContext;
|
|
82
|
+
checkPermission: (permission: PluginPermission, hostContext: HostContext) => boolean;
|
|
83
|
+
cssNamespace: string;
|
|
84
|
+
}
|
|
85
|
+
export interface PluginRegistry {
|
|
86
|
+
register(plugin: AdminPlugin): void;
|
|
87
|
+
unregister(pluginId: PluginId): void;
|
|
88
|
+
get(pluginId: PluginId): AdminPlugin | undefined;
|
|
89
|
+
getAll(): AdminPlugin[];
|
|
90
|
+
subscribe(listener: (plugins: AdminPlugin[]) => void): () => void;
|
|
91
|
+
}
|
|
92
|
+
export type AdminHostContext = HostContext;
|
|
93
|
+
export type AdminHostServiceBag = Record<HostServiceId, unknown>;
|
|
94
|
+
export interface ResolvedSection extends AdminSectionConfig {
|
|
95
|
+
qualifiedId: QualifiedSectionId;
|
|
96
|
+
pluginId: PluginId;
|
|
97
|
+
cssNamespace: string;
|
|
98
|
+
}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cedros/admin-react",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shared AdminShell host and admin UI contracts for Cedros packages",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"types": "./dist/index.d.ts",
|
|
16
|
+
"import": "./dist/index.js",
|
|
17
|
+
"require": "./dist/index.js",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./package.json": "./package.json",
|
|
21
|
+
"./styles.css": "./dist/styles.css",
|
|
22
|
+
"./shell.css": "./dist/shell.css"
|
|
23
|
+
},
|
|
24
|
+
"sideEffects": [
|
|
25
|
+
"**/*.css"
|
|
26
|
+
],
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
29
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
30
|
+
}
|
|
31
|
+
}
|