@nubitio/admin 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/LICENSE +21 -0
- package/README.md +55 -0
- package/dist/index.cjs +390 -0
- package/dist/index.d.cts +98 -0
- package/dist/index.d.mts +98 -0
- package/dist/index.mjs +362 -0
- package/dist/style.css +371 -0
- package/package.json +57 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import { useCallback, useEffect, useMemo, useState } from "react";
|
|
2
|
+
import { useLocation, useNavigate } from "react-router-dom";
|
|
3
|
+
import { Badge, IconButton, useFloatingPanel } from "@nubitio/ui";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region packages/admin/AdminHeader.tsx
|
|
6
|
+
function ActionPopover({ action }) {
|
|
7
|
+
const { open, toggle, setOpen, containerRef } = useFloatingPanel();
|
|
8
|
+
if (action.onClick) return /* @__PURE__ */ jsxs("div", {
|
|
9
|
+
className: "nb-admin-messages",
|
|
10
|
+
children: [/* @__PURE__ */ jsx(IconButton, {
|
|
11
|
+
icon: action.icon,
|
|
12
|
+
label: action.label,
|
|
13
|
+
onClick: action.onClick
|
|
14
|
+
}), !!action.badge && action.badge > 0 && /* @__PURE__ */ jsx(Badge, {
|
|
15
|
+
variant: "danger",
|
|
16
|
+
size: "sm",
|
|
17
|
+
pill: true,
|
|
18
|
+
"aria-label": `${action.badge} ${action.label}`,
|
|
19
|
+
children: action.badge > 99 ? "99+" : action.badge
|
|
20
|
+
})]
|
|
21
|
+
});
|
|
22
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
23
|
+
className: "nb-admin-header-popover",
|
|
24
|
+
ref: containerRef,
|
|
25
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
26
|
+
className: "nb-admin-messages",
|
|
27
|
+
children: [/* @__PURE__ */ jsx(IconButton, {
|
|
28
|
+
icon: action.icon,
|
|
29
|
+
label: action.label,
|
|
30
|
+
"aria-expanded": open,
|
|
31
|
+
onClick: toggle
|
|
32
|
+
}), !!action.badge && action.badge > 0 && /* @__PURE__ */ jsx(Badge, {
|
|
33
|
+
variant: "danger",
|
|
34
|
+
size: "sm",
|
|
35
|
+
pill: true,
|
|
36
|
+
"aria-label": `${action.badge} ${action.label}`,
|
|
37
|
+
children: action.badge > 99 ? "99+" : action.badge
|
|
38
|
+
})]
|
|
39
|
+
}), open && action.renderPanel && /* @__PURE__ */ jsx("div", {
|
|
40
|
+
className: "nb-admin-header-popover__panel",
|
|
41
|
+
role: "dialog",
|
|
42
|
+
"aria-label": action.label,
|
|
43
|
+
children: action.renderPanel({ close: () => setOpen(false) })
|
|
44
|
+
})]
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
function UserMenuPopover({ renderUserMenu }) {
|
|
48
|
+
const { open, toggle, setOpen, containerRef } = useFloatingPanel();
|
|
49
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
50
|
+
className: "nb-admin-header-popover",
|
|
51
|
+
ref: containerRef,
|
|
52
|
+
children: [/* @__PURE__ */ jsx(IconButton, {
|
|
53
|
+
icon: "ph ph-user-circle",
|
|
54
|
+
label: "User menu",
|
|
55
|
+
"aria-expanded": open,
|
|
56
|
+
onClick: toggle
|
|
57
|
+
}), open && /* @__PURE__ */ jsx("div", {
|
|
58
|
+
className: "nb-admin-header-popover__panel",
|
|
59
|
+
role: "dialog",
|
|
60
|
+
"aria-label": "User menu",
|
|
61
|
+
children: renderUserMenu({ close: () => setOpen(false) })
|
|
62
|
+
})]
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const AdminHeader = ({ title, menuToggleEnabled, toggleMenu, className, actions = [], renderUserMenu, renderThemeSwitcher }) => /* @__PURE__ */ jsx("header", {
|
|
66
|
+
className: ["nb-admin-header-component", className].filter(Boolean).join(" "),
|
|
67
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
68
|
+
className: "nb-admin-header-toolbar",
|
|
69
|
+
role: "toolbar",
|
|
70
|
+
"aria-label": "Main toolbar",
|
|
71
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
72
|
+
className: "nb-admin-header-toolbar__before",
|
|
73
|
+
children: [menuToggleEnabled && /* @__PURE__ */ jsx(IconButton, {
|
|
74
|
+
className: "nb-admin-menu-button",
|
|
75
|
+
icon: "ph ph-list",
|
|
76
|
+
label: "Toggle menu",
|
|
77
|
+
onClick: toggleMenu
|
|
78
|
+
}), title && /* @__PURE__ */ jsx("div", {
|
|
79
|
+
className: "nb-admin-header-title",
|
|
80
|
+
children: title
|
|
81
|
+
})]
|
|
82
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
83
|
+
className: "nb-admin-header-toolbar__after",
|
|
84
|
+
children: [
|
|
85
|
+
renderThemeSwitcher?.(),
|
|
86
|
+
actions.map((action) => /* @__PURE__ */ jsx(ActionPopover, { action }, action.id)),
|
|
87
|
+
renderUserMenu && /* @__PURE__ */ jsx(UserMenuPopover, { renderUserMenu })
|
|
88
|
+
]
|
|
89
|
+
})]
|
|
90
|
+
})
|
|
91
|
+
});
|
|
92
|
+
//#endregion
|
|
93
|
+
//#region packages/admin/useScreenSize.ts
|
|
94
|
+
let handlers = [];
|
|
95
|
+
const xSmallMedia = window.matchMedia("(max-width: 575.98px)");
|
|
96
|
+
const smallMedia = window.matchMedia("(min-width: 576px) and (max-width: 991.98px)");
|
|
97
|
+
const mediumMedia = window.matchMedia("(min-width: 992px) and (max-width: 1199.98px)");
|
|
98
|
+
const largeMedia = window.matchMedia("(min-width: 1200px)");
|
|
99
|
+
let debounceTimer = null;
|
|
100
|
+
const notifyHandlers = () => {
|
|
101
|
+
if (debounceTimer !== null) clearTimeout(debounceTimer);
|
|
102
|
+
debounceTimer = setTimeout(() => {
|
|
103
|
+
handlers.forEach((handler) => handler());
|
|
104
|
+
}, 50);
|
|
105
|
+
};
|
|
106
|
+
[
|
|
107
|
+
xSmallMedia,
|
|
108
|
+
smallMedia,
|
|
109
|
+
mediumMedia,
|
|
110
|
+
largeMedia
|
|
111
|
+
].forEach((media) => {
|
|
112
|
+
media.addEventListener("change", (e) => {
|
|
113
|
+
if (e.matches) notifyHandlers();
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
const subscribe = (handler) => handlers.push(handler);
|
|
117
|
+
const unsubscribe = (handler) => {
|
|
118
|
+
handlers = handlers.filter((item) => item !== handler);
|
|
119
|
+
};
|
|
120
|
+
function getScreenSize() {
|
|
121
|
+
return {
|
|
122
|
+
isXSmall: xSmallMedia.matches,
|
|
123
|
+
isSmall: smallMedia.matches,
|
|
124
|
+
isMedium: mediumMedia.matches,
|
|
125
|
+
isLarge: largeMedia.matches
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const useScreenSize = () => {
|
|
129
|
+
const [screenSize, setScreenSize] = useState(getScreenSize());
|
|
130
|
+
const onSizeChanged = useCallback(() => setScreenSize(getScreenSize()), []);
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
subscribe(onSizeChanged);
|
|
133
|
+
return () => unsubscribe(onSizeChanged);
|
|
134
|
+
}, [onSizeChanged]);
|
|
135
|
+
return screenSize;
|
|
136
|
+
};
|
|
137
|
+
const useScreenSizeClass = () => {
|
|
138
|
+
const { isLarge, isMedium, isSmall } = useScreenSize();
|
|
139
|
+
if (isLarge) return "screen-large";
|
|
140
|
+
if (isMedium) return "screen-medium";
|
|
141
|
+
if (isSmall) return "screen-small";
|
|
142
|
+
return "screen-x-small";
|
|
143
|
+
};
|
|
144
|
+
//#endregion
|
|
145
|
+
//#region packages/admin/AdminSidebarMenu.tsx
|
|
146
|
+
const normalizeRoute = (path) => {
|
|
147
|
+
if (!path) return "";
|
|
148
|
+
const normalized = path.startsWith("/") ? path : `/${path}`;
|
|
149
|
+
return normalized.length > 1 ? normalized.replace(/\/+$/, "") : normalized;
|
|
150
|
+
};
|
|
151
|
+
const getIconClassName = (icon) => {
|
|
152
|
+
if (!icon) return "";
|
|
153
|
+
return icon.includes(" ") ? icon : `ph ph-${icon}`;
|
|
154
|
+
};
|
|
155
|
+
const AdminSidebarMenu = ({ items, compactMode = false, selectedItemChanged, openMenu, footer }) => {
|
|
156
|
+
const [expandedItemKeys, setExpandedItemKeys] = useState(/* @__PURE__ */ new Set());
|
|
157
|
+
const { isLarge } = useScreenSize();
|
|
158
|
+
const location = useLocation();
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
setExpandedItemKeys(/* @__PURE__ */ new Set());
|
|
161
|
+
}, [isLarge, items]);
|
|
162
|
+
const activePath = useMemo(() => {
|
|
163
|
+
const flattenPaths = (menuItems) => menuItems.flatMap((item) => [normalizeRoute(item.path), ...item.items?.map((sub) => normalizeRoute(sub.path)) ?? []]).filter(Boolean);
|
|
164
|
+
const requestedPath = normalizeRoute(location.pathname);
|
|
165
|
+
const allPaths = flattenPaths(items);
|
|
166
|
+
return allPaths.find((path) => path === requestedPath) ?? allPaths.filter((path) => requestedPath.startsWith(path) && path !== "/").sort((a, b) => b.length - a.length)[0];
|
|
167
|
+
}, [location.pathname, items]);
|
|
168
|
+
const isSelected = useCallback((path) => normalizeRoute(path) === activePath, [activePath]);
|
|
169
|
+
const parentContainsSelected = useCallback((item) => item.items?.some((sub) => isSelected(sub.path)) ?? false, [isSelected]);
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
const activeParent = items.find((item) => parentContainsSelected(item));
|
|
172
|
+
if (!activeParent || compactMode) return;
|
|
173
|
+
const activeParentKey = activeParent.path || activeParent.text;
|
|
174
|
+
setExpandedItemKeys((prev) => {
|
|
175
|
+
if (prev.has(activeParentKey)) return prev;
|
|
176
|
+
const next = new Set(prev);
|
|
177
|
+
next.add(activeParentKey);
|
|
178
|
+
return next;
|
|
179
|
+
});
|
|
180
|
+
}, [
|
|
181
|
+
compactMode,
|
|
182
|
+
items,
|
|
183
|
+
parentContainsSelected
|
|
184
|
+
]);
|
|
185
|
+
const toggleExpanded = useCallback((key) => {
|
|
186
|
+
setExpandedItemKeys((prev) => {
|
|
187
|
+
const next = new Set(prev);
|
|
188
|
+
if (next.has(key)) next.delete(key);
|
|
189
|
+
else next.add(key);
|
|
190
|
+
return next;
|
|
191
|
+
});
|
|
192
|
+
}, []);
|
|
193
|
+
const handleSelect = useCallback((path, selected, event) => {
|
|
194
|
+
selectedItemChanged({
|
|
195
|
+
path,
|
|
196
|
+
selected,
|
|
197
|
+
event
|
|
198
|
+
});
|
|
199
|
+
}, [selectedItemChanged]);
|
|
200
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
201
|
+
className: `nb-admin-menu${compactMode ? " compact" : ""}`,
|
|
202
|
+
onPointerDown: openMenu,
|
|
203
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
204
|
+
className: "nb-admin-menu-container theme-dependent",
|
|
205
|
+
children: /* @__PURE__ */ jsx("nav", {
|
|
206
|
+
className: "nb-admin-menu__nav",
|
|
207
|
+
"aria-label": "Main navigation",
|
|
208
|
+
children: items.map((item) => {
|
|
209
|
+
const selected = isSelected(item.path);
|
|
210
|
+
const containsSelected = parentContainsSelected(item);
|
|
211
|
+
const itemKey = item.path || item.text;
|
|
212
|
+
const hasChildren = (item.items?.length ?? 0) > 0;
|
|
213
|
+
const expanded = !compactMode && hasChildren && expandedItemKeys.has(itemKey);
|
|
214
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
215
|
+
className: `nb-admin-menu__section${containsSelected ? " has-selected-child" : ""}`,
|
|
216
|
+
children: [/* @__PURE__ */ jsxs("button", {
|
|
217
|
+
className: `nb-admin-menu__item nb-admin-menu__item--parent${selected ? " is-selected" : ""}`,
|
|
218
|
+
type: "button",
|
|
219
|
+
"aria-current": selected ? "page" : void 0,
|
|
220
|
+
"aria-expanded": hasChildren ? expanded : void 0,
|
|
221
|
+
onClick: (event) => {
|
|
222
|
+
if (hasChildren) {
|
|
223
|
+
toggleExpanded(itemKey);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
handleSelect(item.path, selected, event);
|
|
227
|
+
},
|
|
228
|
+
children: [
|
|
229
|
+
/* @__PURE__ */ jsx("span", {
|
|
230
|
+
className: `nb-admin-menu__icon ${getIconClassName(item.icon)}`,
|
|
231
|
+
"aria-hidden": "true"
|
|
232
|
+
}),
|
|
233
|
+
/* @__PURE__ */ jsx("span", {
|
|
234
|
+
className: "nb-admin-menu__text",
|
|
235
|
+
children: item.text
|
|
236
|
+
}),
|
|
237
|
+
hasChildren && /* @__PURE__ */ jsx("span", {
|
|
238
|
+
className: `nb-admin-menu__chevron${expanded ? " is-expanded" : ""}`,
|
|
239
|
+
"aria-hidden": "true"
|
|
240
|
+
})
|
|
241
|
+
]
|
|
242
|
+
}), expanded && (item.items?.length ?? 0) > 0 && /* @__PURE__ */ jsx("div", {
|
|
243
|
+
className: "nb-admin-menu__children",
|
|
244
|
+
children: item.items.map((subItem) => {
|
|
245
|
+
const childSelected = isSelected(subItem.path);
|
|
246
|
+
return /* @__PURE__ */ jsx("button", {
|
|
247
|
+
className: `nb-admin-menu__item nb-admin-menu__item--child${childSelected ? " is-selected" : ""}`,
|
|
248
|
+
type: "button",
|
|
249
|
+
"aria-current": childSelected ? "page" : void 0,
|
|
250
|
+
onClick: (event) => handleSelect(subItem.path, childSelected, event),
|
|
251
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
252
|
+
className: "nb-admin-menu__text",
|
|
253
|
+
children: subItem.text
|
|
254
|
+
})
|
|
255
|
+
}, subItem.path || subItem.text);
|
|
256
|
+
})
|
|
257
|
+
})]
|
|
258
|
+
}, itemKey);
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
}), footer && /* @__PURE__ */ jsx("footer", {
|
|
262
|
+
className: "nb-admin-footer",
|
|
263
|
+
children: footer
|
|
264
|
+
})]
|
|
265
|
+
});
|
|
266
|
+
};
|
|
267
|
+
//#endregion
|
|
268
|
+
//#region packages/admin/AdminShell.tsx
|
|
269
|
+
const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThemeSwitcher, footer, children }) => {
|
|
270
|
+
const navigate = useNavigate();
|
|
271
|
+
const { isXSmall, isLarge } = useScreenSize();
|
|
272
|
+
const [menuStatus, setMenuStatus] = useState(null);
|
|
273
|
+
const getDefaultMenuOpenState = useCallback(() => isLarge ? 2 : 1, [isLarge]);
|
|
274
|
+
const getMenuOpenState = useCallback((status) => status === null ? getDefaultMenuOpenState() : status, [getDefaultMenuOpenState]);
|
|
275
|
+
const getMenuStatus = useCallback((status) => status === getDefaultMenuOpenState() ? null : status, [getDefaultMenuOpenState]);
|
|
276
|
+
const changeMenuStatus = useCallback((reducerFn) => {
|
|
277
|
+
setMenuStatus((prev) => getMenuStatus(reducerFn(getMenuOpenState(prev)) ?? prev));
|
|
278
|
+
}, [getMenuOpenState, getMenuStatus]);
|
|
279
|
+
const toggleMenu = useCallback((event) => {
|
|
280
|
+
changeMenuStatus((prev) => prev === 1 ? 2 : 1);
|
|
281
|
+
event.stopPropagation();
|
|
282
|
+
}, [changeMenuStatus]);
|
|
283
|
+
const temporaryOpenMenu = useCallback(() => {
|
|
284
|
+
changeMenuStatus((prev) => prev === 1 ? 3 : null);
|
|
285
|
+
}, [changeMenuStatus]);
|
|
286
|
+
const closeMenuFromOverlay = useCallback(() => {
|
|
287
|
+
changeMenuStatus((prev) => prev !== 1 && !isLarge ? 1 : null);
|
|
288
|
+
}, [isLarge, changeMenuStatus]);
|
|
289
|
+
const onNavigationChanged = useCallback(({ path, event, selected }) => {
|
|
290
|
+
if (getMenuOpenState(menuStatus) === 1 || !path || selected) {
|
|
291
|
+
event?.preventDefault();
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
navigate(path);
|
|
295
|
+
if (!isLarge || menuStatus === 3) {
|
|
296
|
+
setMenuStatus(getMenuStatus(1));
|
|
297
|
+
event?.stopPropagation();
|
|
298
|
+
}
|
|
299
|
+
}, [
|
|
300
|
+
navigate,
|
|
301
|
+
menuStatus,
|
|
302
|
+
isLarge,
|
|
303
|
+
getMenuOpenState,
|
|
304
|
+
getMenuStatus
|
|
305
|
+
]);
|
|
306
|
+
useEffect(() => {
|
|
307
|
+
changeMenuStatus(() => menuStatus);
|
|
308
|
+
}, [
|
|
309
|
+
isLarge,
|
|
310
|
+
changeMenuStatus,
|
|
311
|
+
menuStatus
|
|
312
|
+
]);
|
|
313
|
+
const menuOpenState = getMenuOpenState(menuStatus);
|
|
314
|
+
const isMenuOpen = menuOpenState !== 1;
|
|
315
|
+
const isCompact = menuOpenState === 1;
|
|
316
|
+
const bodyClassName = [
|
|
317
|
+
"nb-admin-shell__body nb-admin-layout-body",
|
|
318
|
+
isMenuOpen ? "is-open" : "is-closed",
|
|
319
|
+
isCompact ? "is-compact" : "",
|
|
320
|
+
isLarge ? "is-large" : "is-overlay",
|
|
321
|
+
isXSmall ? "is-xsmall" : ""
|
|
322
|
+
].filter(Boolean).join(" ");
|
|
323
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
324
|
+
className: "nb-admin-shell",
|
|
325
|
+
children: [/* @__PURE__ */ jsx(AdminHeader, {
|
|
326
|
+
className: "nb-admin-layout-header",
|
|
327
|
+
menuToggleEnabled: true,
|
|
328
|
+
toggleMenu,
|
|
329
|
+
title,
|
|
330
|
+
actions: headerActions,
|
|
331
|
+
renderUserMenu,
|
|
332
|
+
renderThemeSwitcher
|
|
333
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
334
|
+
className: bodyClassName,
|
|
335
|
+
children: [
|
|
336
|
+
/* @__PURE__ */ jsx("aside", {
|
|
337
|
+
className: "nb-admin-shell__panel",
|
|
338
|
+
"aria-label": "Main menu",
|
|
339
|
+
children: /* @__PURE__ */ jsx(AdminSidebarMenu, {
|
|
340
|
+
items: menuItems,
|
|
341
|
+
compactMode: isCompact,
|
|
342
|
+
selectedItemChanged: onNavigationChanged,
|
|
343
|
+
openMenu: temporaryOpenMenu,
|
|
344
|
+
footer
|
|
345
|
+
})
|
|
346
|
+
}),
|
|
347
|
+
!isLarge && isMenuOpen && /* @__PURE__ */ jsx("button", {
|
|
348
|
+
className: "nb-admin-shell__scrim",
|
|
349
|
+
type: "button",
|
|
350
|
+
"aria-label": "Close menu",
|
|
351
|
+
onClick: closeMenuFromOverlay
|
|
352
|
+
}),
|
|
353
|
+
/* @__PURE__ */ jsx("main", {
|
|
354
|
+
className: "nb-admin-shell__content content",
|
|
355
|
+
children
|
|
356
|
+
})
|
|
357
|
+
]
|
|
358
|
+
})]
|
|
359
|
+
});
|
|
360
|
+
};
|
|
361
|
+
//#endregion
|
|
362
|
+
export { AdminHeader, AdminShell, AdminSidebarMenu, useScreenSize, useScreenSizeClass };
|