@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/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Johan Guerreros
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# @nubitio/admin
|
|
2
|
+
|
|
3
|
+
Admin shell layout for Nubit apps: responsive sidebar with nested menus, header with action slots, and screen-size utilities.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @nubitio/admin
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Peer dependencies
|
|
12
|
+
|
|
13
|
+
```json
|
|
14
|
+
"react": "^19",
|
|
15
|
+
"react-dom": "^19",
|
|
16
|
+
"react-router-dom": "^6"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { AdminShell } from '@nubitio/admin';
|
|
23
|
+
import '@nubitio/admin/style.css';
|
|
24
|
+
|
|
25
|
+
const menu = [
|
|
26
|
+
{ text: 'Dashboard', icon: 'ph ph-house', path: '/' },
|
|
27
|
+
{
|
|
28
|
+
text: 'Catalog',
|
|
29
|
+
icon: 'ph ph-package',
|
|
30
|
+
items: [
|
|
31
|
+
{ text: 'Products', path: '/products' },
|
|
32
|
+
{ text: 'Categories', path: '/categories' },
|
|
33
|
+
],
|
|
34
|
+
},
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export function App() {
|
|
38
|
+
return (
|
|
39
|
+
<AdminShell title="My Admin" menuItems={menu}>
|
|
40
|
+
{/* routed content */}
|
|
41
|
+
</AdminShell>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Exports
|
|
47
|
+
|
|
48
|
+
- `AdminShell` — full layout: sidebar + header + content area
|
|
49
|
+
- `AdminHeader` — standalone header with action slots
|
|
50
|
+
- `AdminSidebarMenu` — standalone sidebar menu
|
|
51
|
+
- `useScreenSize` / `useScreenSizeClass` — responsive breakpoint helpers
|
|
52
|
+
|
|
53
|
+
## License
|
|
54
|
+
|
|
55
|
+
MIT
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
//#region \0rolldown/runtime.js
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
11
|
+
key = keys[i];
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
13
|
+
get: ((k) => from[k]).bind(null, key),
|
|
14
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
20
|
+
value: mod,
|
|
21
|
+
enumerable: true
|
|
22
|
+
}) : target, mod));
|
|
23
|
+
//#endregion
|
|
24
|
+
let react = require("react");
|
|
25
|
+
react = __toESM(react, 1);
|
|
26
|
+
let react_router_dom = require("react-router-dom");
|
|
27
|
+
let _nubitio_ui = require("@nubitio/ui");
|
|
28
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
29
|
+
//#region packages/admin/AdminHeader.tsx
|
|
30
|
+
function ActionPopover({ action }) {
|
|
31
|
+
const { open, toggle, setOpen, containerRef } = (0, _nubitio_ui.useFloatingPanel)();
|
|
32
|
+
if (action.onClick) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
33
|
+
className: "nb-admin-messages",
|
|
34
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.IconButton, {
|
|
35
|
+
icon: action.icon,
|
|
36
|
+
label: action.label,
|
|
37
|
+
onClick: action.onClick
|
|
38
|
+
}), !!action.badge && action.badge > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Badge, {
|
|
39
|
+
variant: "danger",
|
|
40
|
+
size: "sm",
|
|
41
|
+
pill: true,
|
|
42
|
+
"aria-label": `${action.badge} ${action.label}`,
|
|
43
|
+
children: action.badge > 99 ? "99+" : action.badge
|
|
44
|
+
})]
|
|
45
|
+
});
|
|
46
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
47
|
+
className: "nb-admin-header-popover",
|
|
48
|
+
ref: containerRef,
|
|
49
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
50
|
+
className: "nb-admin-messages",
|
|
51
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.IconButton, {
|
|
52
|
+
icon: action.icon,
|
|
53
|
+
label: action.label,
|
|
54
|
+
"aria-expanded": open,
|
|
55
|
+
onClick: toggle
|
|
56
|
+
}), !!action.badge && action.badge > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.Badge, {
|
|
57
|
+
variant: "danger",
|
|
58
|
+
size: "sm",
|
|
59
|
+
pill: true,
|
|
60
|
+
"aria-label": `${action.badge} ${action.label}`,
|
|
61
|
+
children: action.badge > 99 ? "99+" : action.badge
|
|
62
|
+
})]
|
|
63
|
+
}), open && action.renderPanel && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
64
|
+
className: "nb-admin-header-popover__panel",
|
|
65
|
+
role: "dialog",
|
|
66
|
+
"aria-label": action.label,
|
|
67
|
+
children: action.renderPanel({ close: () => setOpen(false) })
|
|
68
|
+
})]
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function UserMenuPopover({ renderUserMenu }) {
|
|
72
|
+
const { open, toggle, setOpen, containerRef } = (0, _nubitio_ui.useFloatingPanel)();
|
|
73
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
74
|
+
className: "nb-admin-header-popover",
|
|
75
|
+
ref: containerRef,
|
|
76
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.IconButton, {
|
|
77
|
+
icon: "ph ph-user-circle",
|
|
78
|
+
label: "User menu",
|
|
79
|
+
"aria-expanded": open,
|
|
80
|
+
onClick: toggle
|
|
81
|
+
}), open && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
82
|
+
className: "nb-admin-header-popover__panel",
|
|
83
|
+
role: "dialog",
|
|
84
|
+
"aria-label": "User menu",
|
|
85
|
+
children: renderUserMenu({ close: () => setOpen(false) })
|
|
86
|
+
})]
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
const AdminHeader = ({ title, menuToggleEnabled, toggleMenu, className, actions = [], renderUserMenu, renderThemeSwitcher }) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)("header", {
|
|
90
|
+
className: ["nb-admin-header-component", className].filter(Boolean).join(" "),
|
|
91
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
92
|
+
className: "nb-admin-header-toolbar",
|
|
93
|
+
role: "toolbar",
|
|
94
|
+
"aria-label": "Main toolbar",
|
|
95
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
96
|
+
className: "nb-admin-header-toolbar__before",
|
|
97
|
+
children: [menuToggleEnabled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.IconButton, {
|
|
98
|
+
className: "nb-admin-menu-button",
|
|
99
|
+
icon: "ph ph-list",
|
|
100
|
+
label: "Toggle menu",
|
|
101
|
+
onClick: toggleMenu
|
|
102
|
+
}), title && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
103
|
+
className: "nb-admin-header-title",
|
|
104
|
+
children: title
|
|
105
|
+
})]
|
|
106
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
107
|
+
className: "nb-admin-header-toolbar__after",
|
|
108
|
+
children: [
|
|
109
|
+
renderThemeSwitcher?.(),
|
|
110
|
+
actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionPopover, { action }, action.id)),
|
|
111
|
+
renderUserMenu && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UserMenuPopover, { renderUserMenu })
|
|
112
|
+
]
|
|
113
|
+
})]
|
|
114
|
+
})
|
|
115
|
+
});
|
|
116
|
+
//#endregion
|
|
117
|
+
//#region packages/admin/useScreenSize.ts
|
|
118
|
+
let handlers = [];
|
|
119
|
+
const xSmallMedia = window.matchMedia("(max-width: 575.98px)");
|
|
120
|
+
const smallMedia = window.matchMedia("(min-width: 576px) and (max-width: 991.98px)");
|
|
121
|
+
const mediumMedia = window.matchMedia("(min-width: 992px) and (max-width: 1199.98px)");
|
|
122
|
+
const largeMedia = window.matchMedia("(min-width: 1200px)");
|
|
123
|
+
let debounceTimer = null;
|
|
124
|
+
const notifyHandlers = () => {
|
|
125
|
+
if (debounceTimer !== null) clearTimeout(debounceTimer);
|
|
126
|
+
debounceTimer = setTimeout(() => {
|
|
127
|
+
handlers.forEach((handler) => handler());
|
|
128
|
+
}, 50);
|
|
129
|
+
};
|
|
130
|
+
[
|
|
131
|
+
xSmallMedia,
|
|
132
|
+
smallMedia,
|
|
133
|
+
mediumMedia,
|
|
134
|
+
largeMedia
|
|
135
|
+
].forEach((media) => {
|
|
136
|
+
media.addEventListener("change", (e) => {
|
|
137
|
+
if (e.matches) notifyHandlers();
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
const subscribe = (handler) => handlers.push(handler);
|
|
141
|
+
const unsubscribe = (handler) => {
|
|
142
|
+
handlers = handlers.filter((item) => item !== handler);
|
|
143
|
+
};
|
|
144
|
+
function getScreenSize() {
|
|
145
|
+
return {
|
|
146
|
+
isXSmall: xSmallMedia.matches,
|
|
147
|
+
isSmall: smallMedia.matches,
|
|
148
|
+
isMedium: mediumMedia.matches,
|
|
149
|
+
isLarge: largeMedia.matches
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const useScreenSize = () => {
|
|
153
|
+
const [screenSize, setScreenSize] = (0, react.useState)(getScreenSize());
|
|
154
|
+
const onSizeChanged = (0, react.useCallback)(() => setScreenSize(getScreenSize()), []);
|
|
155
|
+
(0, react.useEffect)(() => {
|
|
156
|
+
subscribe(onSizeChanged);
|
|
157
|
+
return () => unsubscribe(onSizeChanged);
|
|
158
|
+
}, [onSizeChanged]);
|
|
159
|
+
return screenSize;
|
|
160
|
+
};
|
|
161
|
+
const useScreenSizeClass = () => {
|
|
162
|
+
const { isLarge, isMedium, isSmall } = useScreenSize();
|
|
163
|
+
if (isLarge) return "screen-large";
|
|
164
|
+
if (isMedium) return "screen-medium";
|
|
165
|
+
if (isSmall) return "screen-small";
|
|
166
|
+
return "screen-x-small";
|
|
167
|
+
};
|
|
168
|
+
//#endregion
|
|
169
|
+
//#region packages/admin/AdminSidebarMenu.tsx
|
|
170
|
+
const normalizeRoute = (path) => {
|
|
171
|
+
if (!path) return "";
|
|
172
|
+
const normalized = path.startsWith("/") ? path : `/${path}`;
|
|
173
|
+
return normalized.length > 1 ? normalized.replace(/\/+$/, "") : normalized;
|
|
174
|
+
};
|
|
175
|
+
const getIconClassName = (icon) => {
|
|
176
|
+
if (!icon) return "";
|
|
177
|
+
return icon.includes(" ") ? icon : `ph ph-${icon}`;
|
|
178
|
+
};
|
|
179
|
+
const AdminSidebarMenu = ({ items, compactMode = false, selectedItemChanged, openMenu, footer }) => {
|
|
180
|
+
const [expandedItemKeys, setExpandedItemKeys] = (0, react.useState)(/* @__PURE__ */ new Set());
|
|
181
|
+
const { isLarge } = useScreenSize();
|
|
182
|
+
const location = (0, react_router_dom.useLocation)();
|
|
183
|
+
(0, react.useEffect)(() => {
|
|
184
|
+
setExpandedItemKeys(/* @__PURE__ */ new Set());
|
|
185
|
+
}, [isLarge, items]);
|
|
186
|
+
const activePath = (0, react.useMemo)(() => {
|
|
187
|
+
const flattenPaths = (menuItems) => menuItems.flatMap((item) => [normalizeRoute(item.path), ...item.items?.map((sub) => normalizeRoute(sub.path)) ?? []]).filter(Boolean);
|
|
188
|
+
const requestedPath = normalizeRoute(location.pathname);
|
|
189
|
+
const allPaths = flattenPaths(items);
|
|
190
|
+
return allPaths.find((path) => path === requestedPath) ?? allPaths.filter((path) => requestedPath.startsWith(path) && path !== "/").sort((a, b) => b.length - a.length)[0];
|
|
191
|
+
}, [location.pathname, items]);
|
|
192
|
+
const isSelected = (0, react.useCallback)((path) => normalizeRoute(path) === activePath, [activePath]);
|
|
193
|
+
const parentContainsSelected = (0, react.useCallback)((item) => item.items?.some((sub) => isSelected(sub.path)) ?? false, [isSelected]);
|
|
194
|
+
(0, react.useEffect)(() => {
|
|
195
|
+
const activeParent = items.find((item) => parentContainsSelected(item));
|
|
196
|
+
if (!activeParent || compactMode) return;
|
|
197
|
+
const activeParentKey = activeParent.path || activeParent.text;
|
|
198
|
+
setExpandedItemKeys((prev) => {
|
|
199
|
+
if (prev.has(activeParentKey)) return prev;
|
|
200
|
+
const next = new Set(prev);
|
|
201
|
+
next.add(activeParentKey);
|
|
202
|
+
return next;
|
|
203
|
+
});
|
|
204
|
+
}, [
|
|
205
|
+
compactMode,
|
|
206
|
+
items,
|
|
207
|
+
parentContainsSelected
|
|
208
|
+
]);
|
|
209
|
+
const toggleExpanded = (0, react.useCallback)((key) => {
|
|
210
|
+
setExpandedItemKeys((prev) => {
|
|
211
|
+
const next = new Set(prev);
|
|
212
|
+
if (next.has(key)) next.delete(key);
|
|
213
|
+
else next.add(key);
|
|
214
|
+
return next;
|
|
215
|
+
});
|
|
216
|
+
}, []);
|
|
217
|
+
const handleSelect = (0, react.useCallback)((path, selected, event) => {
|
|
218
|
+
selectedItemChanged({
|
|
219
|
+
path,
|
|
220
|
+
selected,
|
|
221
|
+
event
|
|
222
|
+
});
|
|
223
|
+
}, [selectedItemChanged]);
|
|
224
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
225
|
+
className: `nb-admin-menu${compactMode ? " compact" : ""}`,
|
|
226
|
+
onPointerDown: openMenu,
|
|
227
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
228
|
+
className: "nb-admin-menu-container theme-dependent",
|
|
229
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("nav", {
|
|
230
|
+
className: "nb-admin-menu__nav",
|
|
231
|
+
"aria-label": "Main navigation",
|
|
232
|
+
children: items.map((item) => {
|
|
233
|
+
const selected = isSelected(item.path);
|
|
234
|
+
const containsSelected = parentContainsSelected(item);
|
|
235
|
+
const itemKey = item.path || item.text;
|
|
236
|
+
const hasChildren = (item.items?.length ?? 0) > 0;
|
|
237
|
+
const expanded = !compactMode && hasChildren && expandedItemKeys.has(itemKey);
|
|
238
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
|
|
239
|
+
className: `nb-admin-menu__section${containsSelected ? " has-selected-child" : ""}`,
|
|
240
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
|
|
241
|
+
className: `nb-admin-menu__item nb-admin-menu__item--parent${selected ? " is-selected" : ""}`,
|
|
242
|
+
type: "button",
|
|
243
|
+
"aria-current": selected ? "page" : void 0,
|
|
244
|
+
"aria-expanded": hasChildren ? expanded : void 0,
|
|
245
|
+
onClick: (event) => {
|
|
246
|
+
if (hasChildren) {
|
|
247
|
+
toggleExpanded(itemKey);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
handleSelect(item.path, selected, event);
|
|
251
|
+
},
|
|
252
|
+
children: [
|
|
253
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
254
|
+
className: `nb-admin-menu__icon ${getIconClassName(item.icon)}`,
|
|
255
|
+
"aria-hidden": "true"
|
|
256
|
+
}),
|
|
257
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
258
|
+
className: "nb-admin-menu__text",
|
|
259
|
+
children: item.text
|
|
260
|
+
}),
|
|
261
|
+
hasChildren && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
262
|
+
className: `nb-admin-menu__chevron${expanded ? " is-expanded" : ""}`,
|
|
263
|
+
"aria-hidden": "true"
|
|
264
|
+
})
|
|
265
|
+
]
|
|
266
|
+
}), expanded && (item.items?.length ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
267
|
+
className: "nb-admin-menu__children",
|
|
268
|
+
children: item.items.map((subItem) => {
|
|
269
|
+
const childSelected = isSelected(subItem.path);
|
|
270
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
271
|
+
className: `nb-admin-menu__item nb-admin-menu__item--child${childSelected ? " is-selected" : ""}`,
|
|
272
|
+
type: "button",
|
|
273
|
+
"aria-current": childSelected ? "page" : void 0,
|
|
274
|
+
onClick: (event) => handleSelect(subItem.path, childSelected, event),
|
|
275
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
276
|
+
className: "nb-admin-menu__text",
|
|
277
|
+
children: subItem.text
|
|
278
|
+
})
|
|
279
|
+
}, subItem.path || subItem.text);
|
|
280
|
+
})
|
|
281
|
+
})]
|
|
282
|
+
}, itemKey);
|
|
283
|
+
})
|
|
284
|
+
})
|
|
285
|
+
}), footer && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("footer", {
|
|
286
|
+
className: "nb-admin-footer",
|
|
287
|
+
children: footer
|
|
288
|
+
})]
|
|
289
|
+
});
|
|
290
|
+
};
|
|
291
|
+
//#endregion
|
|
292
|
+
//#region packages/admin/AdminShell.tsx
|
|
293
|
+
const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThemeSwitcher, footer, children }) => {
|
|
294
|
+
const navigate = (0, react_router_dom.useNavigate)();
|
|
295
|
+
const { isXSmall, isLarge } = useScreenSize();
|
|
296
|
+
const [menuStatus, setMenuStatus] = (0, react.useState)(null);
|
|
297
|
+
const getDefaultMenuOpenState = (0, react.useCallback)(() => isLarge ? 2 : 1, [isLarge]);
|
|
298
|
+
const getMenuOpenState = (0, react.useCallback)((status) => status === null ? getDefaultMenuOpenState() : status, [getDefaultMenuOpenState]);
|
|
299
|
+
const getMenuStatus = (0, react.useCallback)((status) => status === getDefaultMenuOpenState() ? null : status, [getDefaultMenuOpenState]);
|
|
300
|
+
const changeMenuStatus = (0, react.useCallback)((reducerFn) => {
|
|
301
|
+
setMenuStatus((prev) => getMenuStatus(reducerFn(getMenuOpenState(prev)) ?? prev));
|
|
302
|
+
}, [getMenuOpenState, getMenuStatus]);
|
|
303
|
+
const toggleMenu = (0, react.useCallback)((event) => {
|
|
304
|
+
changeMenuStatus((prev) => prev === 1 ? 2 : 1);
|
|
305
|
+
event.stopPropagation();
|
|
306
|
+
}, [changeMenuStatus]);
|
|
307
|
+
const temporaryOpenMenu = (0, react.useCallback)(() => {
|
|
308
|
+
changeMenuStatus((prev) => prev === 1 ? 3 : null);
|
|
309
|
+
}, [changeMenuStatus]);
|
|
310
|
+
const closeMenuFromOverlay = (0, react.useCallback)(() => {
|
|
311
|
+
changeMenuStatus((prev) => prev !== 1 && !isLarge ? 1 : null);
|
|
312
|
+
}, [isLarge, changeMenuStatus]);
|
|
313
|
+
const onNavigationChanged = (0, react.useCallback)(({ path, event, selected }) => {
|
|
314
|
+
if (getMenuOpenState(menuStatus) === 1 || !path || selected) {
|
|
315
|
+
event?.preventDefault();
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
navigate(path);
|
|
319
|
+
if (!isLarge || menuStatus === 3) {
|
|
320
|
+
setMenuStatus(getMenuStatus(1));
|
|
321
|
+
event?.stopPropagation();
|
|
322
|
+
}
|
|
323
|
+
}, [
|
|
324
|
+
navigate,
|
|
325
|
+
menuStatus,
|
|
326
|
+
isLarge,
|
|
327
|
+
getMenuOpenState,
|
|
328
|
+
getMenuStatus
|
|
329
|
+
]);
|
|
330
|
+
(0, react.useEffect)(() => {
|
|
331
|
+
changeMenuStatus(() => menuStatus);
|
|
332
|
+
}, [
|
|
333
|
+
isLarge,
|
|
334
|
+
changeMenuStatus,
|
|
335
|
+
menuStatus
|
|
336
|
+
]);
|
|
337
|
+
const menuOpenState = getMenuOpenState(menuStatus);
|
|
338
|
+
const isMenuOpen = menuOpenState !== 1;
|
|
339
|
+
const isCompact = menuOpenState === 1;
|
|
340
|
+
const bodyClassName = [
|
|
341
|
+
"nb-admin-shell__body nb-admin-layout-body",
|
|
342
|
+
isMenuOpen ? "is-open" : "is-closed",
|
|
343
|
+
isCompact ? "is-compact" : "",
|
|
344
|
+
isLarge ? "is-large" : "is-overlay",
|
|
345
|
+
isXSmall ? "is-xsmall" : ""
|
|
346
|
+
].filter(Boolean).join(" ");
|
|
347
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
348
|
+
className: "nb-admin-shell",
|
|
349
|
+
children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdminHeader, {
|
|
350
|
+
className: "nb-admin-layout-header",
|
|
351
|
+
menuToggleEnabled: true,
|
|
352
|
+
toggleMenu,
|
|
353
|
+
title,
|
|
354
|
+
actions: headerActions,
|
|
355
|
+
renderUserMenu,
|
|
356
|
+
renderThemeSwitcher
|
|
357
|
+
}), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
|
|
358
|
+
className: bodyClassName,
|
|
359
|
+
children: [
|
|
360
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("aside", {
|
|
361
|
+
className: "nb-admin-shell__panel",
|
|
362
|
+
"aria-label": "Main menu",
|
|
363
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdminSidebarMenu, {
|
|
364
|
+
items: menuItems,
|
|
365
|
+
compactMode: isCompact,
|
|
366
|
+
selectedItemChanged: onNavigationChanged,
|
|
367
|
+
openMenu: temporaryOpenMenu,
|
|
368
|
+
footer
|
|
369
|
+
})
|
|
370
|
+
}),
|
|
371
|
+
!isLarge && isMenuOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
372
|
+
className: "nb-admin-shell__scrim",
|
|
373
|
+
type: "button",
|
|
374
|
+
"aria-label": "Close menu",
|
|
375
|
+
onClick: closeMenuFromOverlay
|
|
376
|
+
}),
|
|
377
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("main", {
|
|
378
|
+
className: "nb-admin-shell__content content",
|
|
379
|
+
children
|
|
380
|
+
})
|
|
381
|
+
]
|
|
382
|
+
})]
|
|
383
|
+
});
|
|
384
|
+
};
|
|
385
|
+
//#endregion
|
|
386
|
+
exports.AdminHeader = AdminHeader;
|
|
387
|
+
exports.AdminShell = AdminShell;
|
|
388
|
+
exports.AdminSidebarMenu = AdminSidebarMenu;
|
|
389
|
+
exports.useScreenSize = useScreenSize;
|
|
390
|
+
exports.useScreenSizeClass = useScreenSizeClass;
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
//#region packages/admin/AdminHeader.d.ts
|
|
4
|
+
interface AdminHeaderAction {
|
|
5
|
+
id: string;
|
|
6
|
+
icon: string;
|
|
7
|
+
label: string;
|
|
8
|
+
badge?: number;
|
|
9
|
+
/** When provided, clicking the button calls this instead of opening a panel. */
|
|
10
|
+
onClick?: () => void;
|
|
11
|
+
renderPanel?: (props: {
|
|
12
|
+
close: () => void;
|
|
13
|
+
}) => React.ReactNode;
|
|
14
|
+
}
|
|
15
|
+
interface AdminHeaderProps {
|
|
16
|
+
title?: string;
|
|
17
|
+
menuToggleEnabled?: boolean;
|
|
18
|
+
toggleMenu?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
|
19
|
+
className?: string;
|
|
20
|
+
actions?: AdminHeaderAction[];
|
|
21
|
+
renderUserMenu?: (props: {
|
|
22
|
+
close: () => void;
|
|
23
|
+
}) => React.ReactNode;
|
|
24
|
+
renderThemeSwitcher?: () => React.ReactNode;
|
|
25
|
+
}
|
|
26
|
+
declare const AdminHeader: ({
|
|
27
|
+
title,
|
|
28
|
+
menuToggleEnabled,
|
|
29
|
+
toggleMenu,
|
|
30
|
+
className,
|
|
31
|
+
actions,
|
|
32
|
+
renderUserMenu,
|
|
33
|
+
renderThemeSwitcher
|
|
34
|
+
}: AdminHeaderProps) => React.JSX.Element;
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region packages/admin/AdminSidebarMenu.d.ts
|
|
37
|
+
interface AdminMenuSubItem {
|
|
38
|
+
text: string;
|
|
39
|
+
path: string;
|
|
40
|
+
}
|
|
41
|
+
interface AdminMenuItem {
|
|
42
|
+
text: string;
|
|
43
|
+
path?: string;
|
|
44
|
+
icon?: string;
|
|
45
|
+
items?: AdminMenuSubItem[];
|
|
46
|
+
}
|
|
47
|
+
interface AdminSidebarMenuSelectEvent {
|
|
48
|
+
path?: string;
|
|
49
|
+
selected: boolean;
|
|
50
|
+
event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>;
|
|
51
|
+
}
|
|
52
|
+
interface AdminSidebarMenuProps {
|
|
53
|
+
items: AdminMenuItem[];
|
|
54
|
+
compactMode?: boolean;
|
|
55
|
+
selectedItemChanged: (e: AdminSidebarMenuSelectEvent) => void;
|
|
56
|
+
openMenu?: (e: React.PointerEvent) => void;
|
|
57
|
+
footer?: React.ReactNode;
|
|
58
|
+
}
|
|
59
|
+
declare const AdminSidebarMenu: ({
|
|
60
|
+
items,
|
|
61
|
+
compactMode,
|
|
62
|
+
selectedItemChanged,
|
|
63
|
+
openMenu,
|
|
64
|
+
footer
|
|
65
|
+
}: AdminSidebarMenuProps) => React.JSX.Element;
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region packages/admin/AdminShell.d.ts
|
|
68
|
+
interface AdminShellProps {
|
|
69
|
+
title?: string;
|
|
70
|
+
menuItems: AdminMenuItem[];
|
|
71
|
+
headerActions?: AdminHeaderAction[];
|
|
72
|
+
renderUserMenu?: (props: {
|
|
73
|
+
close: () => void;
|
|
74
|
+
}) => React.ReactNode;
|
|
75
|
+
renderThemeSwitcher?: () => React.ReactNode;
|
|
76
|
+
footer?: React.ReactNode;
|
|
77
|
+
children: React.ReactNode;
|
|
78
|
+
}
|
|
79
|
+
declare const AdminShell: ({
|
|
80
|
+
title,
|
|
81
|
+
menuItems,
|
|
82
|
+
headerActions,
|
|
83
|
+
renderUserMenu,
|
|
84
|
+
renderThemeSwitcher,
|
|
85
|
+
footer,
|
|
86
|
+
children
|
|
87
|
+
}: AdminShellProps) => React.JSX.Element;
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region packages/admin/useScreenSize.d.ts
|
|
90
|
+
declare const useScreenSize: () => {
|
|
91
|
+
isXSmall: boolean;
|
|
92
|
+
isSmall: boolean;
|
|
93
|
+
isMedium: boolean;
|
|
94
|
+
isLarge: boolean;
|
|
95
|
+
};
|
|
96
|
+
declare const useScreenSizeClass: () => "screen-large" | "screen-medium" | "screen-small" | "screen-x-small";
|
|
97
|
+
//#endregion
|
|
98
|
+
export { AdminHeader, type AdminHeaderAction, type AdminHeaderProps, type AdminMenuItem, type AdminMenuSubItem, AdminShell, type AdminShellProps, AdminSidebarMenu, type AdminSidebarMenuProps, type AdminSidebarMenuSelectEvent, useScreenSize, useScreenSizeClass };
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
|
|
3
|
+
//#region packages/admin/AdminHeader.d.ts
|
|
4
|
+
interface AdminHeaderAction {
|
|
5
|
+
id: string;
|
|
6
|
+
icon: string;
|
|
7
|
+
label: string;
|
|
8
|
+
badge?: number;
|
|
9
|
+
/** When provided, clicking the button calls this instead of opening a panel. */
|
|
10
|
+
onClick?: () => void;
|
|
11
|
+
renderPanel?: (props: {
|
|
12
|
+
close: () => void;
|
|
13
|
+
}) => React.ReactNode;
|
|
14
|
+
}
|
|
15
|
+
interface AdminHeaderProps {
|
|
16
|
+
title?: string;
|
|
17
|
+
menuToggleEnabled?: boolean;
|
|
18
|
+
toggleMenu?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
|
19
|
+
className?: string;
|
|
20
|
+
actions?: AdminHeaderAction[];
|
|
21
|
+
renderUserMenu?: (props: {
|
|
22
|
+
close: () => void;
|
|
23
|
+
}) => React.ReactNode;
|
|
24
|
+
renderThemeSwitcher?: () => React.ReactNode;
|
|
25
|
+
}
|
|
26
|
+
declare const AdminHeader: ({
|
|
27
|
+
title,
|
|
28
|
+
menuToggleEnabled,
|
|
29
|
+
toggleMenu,
|
|
30
|
+
className,
|
|
31
|
+
actions,
|
|
32
|
+
renderUserMenu,
|
|
33
|
+
renderThemeSwitcher
|
|
34
|
+
}: AdminHeaderProps) => React.JSX.Element;
|
|
35
|
+
//#endregion
|
|
36
|
+
//#region packages/admin/AdminSidebarMenu.d.ts
|
|
37
|
+
interface AdminMenuSubItem {
|
|
38
|
+
text: string;
|
|
39
|
+
path: string;
|
|
40
|
+
}
|
|
41
|
+
interface AdminMenuItem {
|
|
42
|
+
text: string;
|
|
43
|
+
path?: string;
|
|
44
|
+
icon?: string;
|
|
45
|
+
items?: AdminMenuSubItem[];
|
|
46
|
+
}
|
|
47
|
+
interface AdminSidebarMenuSelectEvent {
|
|
48
|
+
path?: string;
|
|
49
|
+
selected: boolean;
|
|
50
|
+
event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>;
|
|
51
|
+
}
|
|
52
|
+
interface AdminSidebarMenuProps {
|
|
53
|
+
items: AdminMenuItem[];
|
|
54
|
+
compactMode?: boolean;
|
|
55
|
+
selectedItemChanged: (e: AdminSidebarMenuSelectEvent) => void;
|
|
56
|
+
openMenu?: (e: React.PointerEvent) => void;
|
|
57
|
+
footer?: React.ReactNode;
|
|
58
|
+
}
|
|
59
|
+
declare const AdminSidebarMenu: ({
|
|
60
|
+
items,
|
|
61
|
+
compactMode,
|
|
62
|
+
selectedItemChanged,
|
|
63
|
+
openMenu,
|
|
64
|
+
footer
|
|
65
|
+
}: AdminSidebarMenuProps) => React.JSX.Element;
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region packages/admin/AdminShell.d.ts
|
|
68
|
+
interface AdminShellProps {
|
|
69
|
+
title?: string;
|
|
70
|
+
menuItems: AdminMenuItem[];
|
|
71
|
+
headerActions?: AdminHeaderAction[];
|
|
72
|
+
renderUserMenu?: (props: {
|
|
73
|
+
close: () => void;
|
|
74
|
+
}) => React.ReactNode;
|
|
75
|
+
renderThemeSwitcher?: () => React.ReactNode;
|
|
76
|
+
footer?: React.ReactNode;
|
|
77
|
+
children: React.ReactNode;
|
|
78
|
+
}
|
|
79
|
+
declare const AdminShell: ({
|
|
80
|
+
title,
|
|
81
|
+
menuItems,
|
|
82
|
+
headerActions,
|
|
83
|
+
renderUserMenu,
|
|
84
|
+
renderThemeSwitcher,
|
|
85
|
+
footer,
|
|
86
|
+
children
|
|
87
|
+
}: AdminShellProps) => React.JSX.Element;
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region packages/admin/useScreenSize.d.ts
|
|
90
|
+
declare const useScreenSize: () => {
|
|
91
|
+
isXSmall: boolean;
|
|
92
|
+
isSmall: boolean;
|
|
93
|
+
isMedium: boolean;
|
|
94
|
+
isLarge: boolean;
|
|
95
|
+
};
|
|
96
|
+
declare const useScreenSizeClass: () => "screen-large" | "screen-medium" | "screen-small" | "screen-x-small";
|
|
97
|
+
//#endregion
|
|
98
|
+
export { AdminHeader, type AdminHeaderAction, type AdminHeaderProps, type AdminMenuItem, type AdminMenuSubItem, AdminShell, type AdminShellProps, AdminSidebarMenu, type AdminSidebarMenuProps, type AdminSidebarMenuSelectEvent, useScreenSize, useScreenSizeClass };
|