@nubitio/admin 1.1.2 → 1.2.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 CHANGED
@@ -45,6 +45,40 @@ export function App() {
45
45
 
46
46
  ## Exports
47
47
 
48
+ ### Nubit workspace layout (opt-in)
49
+
50
+ The existing layout remains the default. `variant="workspace"` places the sidebar
51
+ alongside the header on large screens and retains the overlay menu on smaller
52
+ screens. Application-specific search and company/branch selection stay outside
53
+ the library:
54
+
55
+ ```tsx
56
+ <AdminShell
57
+ variant="workspace"
58
+ title="Sales"
59
+ menuItems={menu}
60
+ brand={<CompanyLogo />}
61
+ headerContent={<WorkspaceSearch />}
62
+ sidebarContext={<CompanyBranchPicker />}
63
+ bottomNavigation={<AdminBottomNavigation items={mobileDestinations} />}
64
+ >
65
+ {children}
66
+ </AdminShell>
67
+ ```
68
+
69
+ `brand` and `sidebarContext` are hidden in compact mode. The bottom-navigation
70
+ slot renders only on extra-small screens, reserves its own space (including the
71
+ device safe area), and does not cover the main content. `AdminBottomNavigation`
72
+ uses router links and marks the longest matching destination with `aria-current`.
73
+ Pass only authorized destinations. Its `label` prop names the navigation landmark.
74
+
75
+ All slots and `variant` are also available through `createNubitApp({ shell: … })`.
76
+ Set `visualStyle: 'nubit'` to opt into the Figma token foundation when using the
77
+ app factory; see the UI package README for font setup.
78
+
79
+ Preview in the demo: `/dashboard?visualStyle=nubit`. Search navigates between demo
80
+ pages; the context label is illustrative, not an ERP company selector.
81
+
48
82
  - `AdminShell` — full layout: sidebar + header + content area
49
83
  - `AdminHeader` — standalone header with action slots
50
84
  - `AdminSidebarMenu` — standalone sidebar menu
package/dist/index.cjs CHANGED
@@ -166,7 +166,7 @@ function UserMenuPopover({ renderUserMenu }) {
166
166
  })]
167
167
  });
168
168
  }
169
- const AdminHeader = ({ title, menuToggleEnabled, toggleMenu, className, actions = [], renderUserMenu, renderThemeSwitcher }) => {
169
+ const AdminHeader = ({ title, menuToggleEnabled, toggleMenu, className, actions = [], content, renderUserMenu, renderThemeSwitcher }) => {
170
170
  const strings = (0, _nubitio_ui.useUiStrings)();
171
171
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("header", {
172
172
  className: ["nb-admin-header-component", className].filter(Boolean).join(" "),
@@ -174,25 +174,32 @@ const AdminHeader = ({ title, menuToggleEnabled, toggleMenu, className, actions
174
174
  className: "nb-admin-header-toolbar",
175
175
  role: "toolbar",
176
176
  "aria-label": strings.mainToolbar,
177
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
178
- className: "nb-admin-header-toolbar__before",
179
- children: [menuToggleEnabled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.IconButton, {
180
- className: "nb-admin-menu-button",
181
- icon: "ph ph-list",
182
- label: strings.toggleMenu,
183
- onClick: toggleMenu
184
- }), title && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
185
- className: "nb-admin-header-title",
186
- children: title
187
- })]
188
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
189
- className: "nb-admin-header-toolbar__after",
190
- children: [
191
- renderThemeSwitcher?.(),
192
- actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionPopover, { action }, action.id)),
193
- renderUserMenu && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UserMenuPopover, { renderUserMenu })
194
- ]
195
- })]
177
+ children: [
178
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
179
+ className: "nb-admin-header-toolbar__before",
180
+ children: [menuToggleEnabled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.IconButton, {
181
+ className: "nb-admin-menu-button",
182
+ icon: "ph ph-list",
183
+ label: strings.toggleMenu,
184
+ onClick: toggleMenu
185
+ }), title && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
186
+ className: "nb-admin-header-title",
187
+ children: title
188
+ })]
189
+ }),
190
+ content && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
191
+ className: "nb-admin-header-content",
192
+ children: content
193
+ }),
194
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
195
+ className: "nb-admin-header-toolbar__after",
196
+ children: [
197
+ renderThemeSwitcher?.(),
198
+ actions.map((action) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionPopover, { action }, action.id)),
199
+ renderUserMenu && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(UserMenuPopover, { renderUserMenu })
200
+ ]
201
+ })
202
+ ]
196
203
  })
197
204
  });
198
205
  };
@@ -259,7 +266,7 @@ const getIconClassName = (icon) => {
259
266
  if (!icon) return "";
260
267
  return icon.includes(" ") ? icon : `ph ph-${icon}`;
261
268
  };
262
- const AdminSidebarMenu = ({ items, compactMode = false, selectedItemChanged, openMenu, footer }) => {
269
+ const AdminSidebarMenu = ({ items, compactMode = false, selectedItemChanged, openMenu, footer, brand, context }) => {
263
270
  const [expandedItemKeys, setExpandedItemKeys] = (0, react.useState)(/* @__PURE__ */ new Set());
264
271
  const strings = (0, _nubitio_ui.useUiStrings)();
265
272
  const { isLarge } = useScreenSize();
@@ -308,73 +315,84 @@ const AdminSidebarMenu = ({ items, compactMode = false, selectedItemChanged, ope
308
315
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
309
316
  className: `nb-admin-menu${compactMode ? " compact" : ""}`,
310
317
  onPointerDown: openMenu,
311
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
312
- className: "nb-admin-menu-container theme-dependent",
313
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("nav", {
314
- className: "nb-admin-menu__nav",
315
- "aria-label": strings.mainNavigation,
316
- children: items.map((item) => {
317
- const selected = isSelected(item.path);
318
- const containsSelected = parentContainsSelected(item);
319
- const itemKey = item.path || item.text;
320
- const hasChildren = (item.items?.length ?? 0) > 0;
321
- const expanded = !compactMode && hasChildren && expandedItemKeys.has(itemKey);
322
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
323
- className: `nb-admin-menu__section${containsSelected ? " has-selected-child" : ""}`,
324
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
325
- className: `nb-admin-menu__item nb-admin-menu__item--parent${selected ? " is-selected" : ""}`,
326
- type: "button",
327
- "aria-current": selected ? "page" : void 0,
328
- "aria-expanded": hasChildren ? expanded : void 0,
329
- onClick: (event) => {
330
- if (hasChildren) {
331
- toggleExpanded(itemKey);
332
- return;
333
- }
334
- handleSelect(item.path, selected, event);
335
- },
336
- children: [
337
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
338
- className: `nb-admin-menu__icon ${getIconClassName(item.icon)}`,
339
- "aria-hidden": "true"
340
- }),
341
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
342
- className: "nb-admin-menu__text",
343
- children: item.text
344
- }),
345
- hasChildren && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
346
- className: `nb-admin-menu__chevron${expanded ? " is-expanded" : ""}`,
347
- "aria-hidden": "true"
348
- })
349
- ]
350
- }), expanded && (item.items?.length ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
351
- className: "nb-admin-menu__children",
352
- children: item.items.map((subItem) => {
353
- const childSelected = isSelected(subItem.path);
354
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
355
- className: `nb-admin-menu__item nb-admin-menu__item--child${childSelected ? " is-selected" : ""}`,
356
- type: "button",
357
- "aria-current": childSelected ? "page" : void 0,
358
- onClick: (event) => handleSelect(subItem.path, childSelected, event),
359
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
318
+ children: [
319
+ brand && !compactMode && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
320
+ className: "nb-admin-menu__brand",
321
+ children: brand
322
+ }),
323
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
324
+ className: "nb-admin-menu-container theme-dependent",
325
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("nav", {
326
+ className: "nb-admin-menu__nav",
327
+ "aria-label": strings.mainNavigation,
328
+ children: items.map((item) => {
329
+ const selected = isSelected(item.path);
330
+ const containsSelected = parentContainsSelected(item);
331
+ const itemKey = item.path || item.text;
332
+ const hasChildren = (item.items?.length ?? 0) > 0;
333
+ const expanded = !compactMode && hasChildren && expandedItemKeys.has(itemKey);
334
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("section", {
335
+ className: `nb-admin-menu__section${containsSelected ? " has-selected-child" : ""}`,
336
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
337
+ className: `nb-admin-menu__item nb-admin-menu__item--parent${selected ? " is-selected" : ""}`,
338
+ type: "button",
339
+ "aria-current": selected ? "page" : void 0,
340
+ "aria-expanded": hasChildren ? expanded : void 0,
341
+ onClick: (event) => {
342
+ if (hasChildren) {
343
+ toggleExpanded(itemKey);
344
+ return;
345
+ }
346
+ handleSelect(item.path, selected, event);
347
+ },
348
+ children: [
349
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
350
+ className: `nb-admin-menu__icon ${getIconClassName(item.icon)}`,
351
+ "aria-hidden": "true"
352
+ }),
353
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
360
354
  className: "nb-admin-menu__text",
361
- children: subItem.text
355
+ children: item.text
356
+ }),
357
+ hasChildren && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
358
+ className: `nb-admin-menu__chevron${expanded ? " is-expanded" : ""}`,
359
+ "aria-hidden": "true"
362
360
  })
363
- }, subItem.path || subItem.text);
364
- })
365
- })]
366
- }, itemKey);
361
+ ]
362
+ }), expanded && (item.items?.length ?? 0) > 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
363
+ className: "nb-admin-menu__children",
364
+ children: item.items.map((subItem) => {
365
+ const childSelected = isSelected(subItem.path);
366
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
367
+ className: `nb-admin-menu__item nb-admin-menu__item--child${childSelected ? " is-selected" : ""}`,
368
+ type: "button",
369
+ "aria-current": childSelected ? "page" : void 0,
370
+ onClick: (event) => handleSelect(subItem.path, childSelected, event),
371
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
372
+ className: "nb-admin-menu__text",
373
+ children: subItem.text
374
+ })
375
+ }, subItem.path || subItem.text);
376
+ })
377
+ })]
378
+ }, itemKey);
379
+ })
367
380
  })
381
+ }),
382
+ context && !compactMode && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
383
+ className: "nb-admin-menu__context",
384
+ children: context
385
+ }),
386
+ footer && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("footer", {
387
+ className: "nb-admin-footer",
388
+ children: footer
368
389
  })
369
- }), footer && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("footer", {
370
- className: "nb-admin-footer",
371
- children: footer
372
- })]
390
+ ]
373
391
  });
374
392
  };
375
393
  //#endregion
376
394
  //#region packages/admin/AdminShell.tsx
377
- const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThemeSwitcher, footer, children }) => {
395
+ const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThemeSwitcher, footer, variant = "default", brand, headerContent, sidebarContext, bottomNavigation, children }) => {
378
396
  const navigate = (0, react_router_dom.useNavigate)();
379
397
  const strings = (0, _nubitio_ui.useUiStrings)();
380
398
  const { isXSmall, isLarge } = useScreenSize();
@@ -430,45 +448,92 @@ const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThe
430
448
  isXSmall ? "is-xsmall" : ""
431
449
  ].filter(Boolean).join(" ");
432
450
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
433
- className: "nb-admin-shell",
434
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdminHeader, {
435
- className: "nb-admin-layout-header",
436
- menuToggleEnabled: true,
437
- toggleMenu,
438
- title,
439
- actions: headerActions,
440
- renderUserMenu,
441
- renderThemeSwitcher
442
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
443
- className: bodyClassName,
444
- children: [
445
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("aside", {
446
- className: "nb-admin-shell__panel",
447
- "aria-label": strings.mainMenu,
448
- children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdminSidebarMenu, {
449
- items: menuItems,
450
- compactMode: isCompact,
451
- selectedItemChanged: onNavigationChanged,
452
- openMenu: temporaryOpenMenu,
453
- footer
451
+ className: [
452
+ "nb-admin-shell",
453
+ variant === "workspace" && "nb-admin-shell--workspace",
454
+ variant === "workspace" && isLarge && "is-workspace-desktop",
455
+ variant === "workspace" && isLarge && isCompact && "is-workspace-compact"
456
+ ].filter(Boolean).join(" "),
457
+ children: [
458
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdminHeader, {
459
+ className: "nb-admin-layout-header",
460
+ menuToggleEnabled: true,
461
+ toggleMenu,
462
+ title,
463
+ actions: headerActions,
464
+ content: headerContent,
465
+ renderUserMenu,
466
+ renderThemeSwitcher
467
+ }),
468
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
469
+ className: bodyClassName,
470
+ children: [
471
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("aside", {
472
+ className: "nb-admin-shell__panel",
473
+ "aria-label": strings.mainMenu,
474
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdminSidebarMenu, {
475
+ items: menuItems,
476
+ compactMode: isCompact,
477
+ selectedItemChanged: onNavigationChanged,
478
+ openMenu: temporaryOpenMenu,
479
+ footer,
480
+ brand,
481
+ context: sidebarContext
482
+ })
483
+ }),
484
+ !isLarge && isMenuOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
485
+ className: "nb-admin-shell__scrim",
486
+ type: "button",
487
+ "aria-label": strings.closeMenu,
488
+ onClick: closeMenuFromOverlay
489
+ }),
490
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("main", {
491
+ className: "nb-admin-shell__content content",
492
+ tabIndex: 0,
493
+ "aria-label": title ? `${title} content` : strings.mainContent,
494
+ children
454
495
  })
496
+ ]
497
+ }),
498
+ isXSmall && bottomNavigation && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
499
+ className: "nb-admin-shell__bottom-navigation",
500
+ children: bottomNavigation
501
+ })
502
+ ]
503
+ });
504
+ };
505
+ //#endregion
506
+ //#region packages/admin/AdminBottomNavigation.tsx
507
+ function AdminBottomNavigation({ items, label }) {
508
+ const { pathname } = (0, react_router_dom.useLocation)();
509
+ const strings = (0, _nubitio_ui.useUiStrings)();
510
+ const normalize = (path) => path.replace(/\/+$/, "") || "/";
511
+ const currentPath = normalize(pathname);
512
+ const activePath = items.map((item) => normalize(item.path)).filter((path) => currentPath === path || path !== "/" && currentPath.startsWith(`${path}/`)).sort((a, b) => b.length - a.length)[0];
513
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("nav", {
514
+ className: "nb-admin-bottom-nav",
515
+ "aria-label": label ?? strings.mainNavigation,
516
+ children: items.map((item) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_router_dom.Link, {
517
+ to: item.path,
518
+ className: "nb-admin-bottom-nav__item",
519
+ "aria-current": normalize(item.path) === activePath ? "page" : void 0,
520
+ children: [
521
+ item.icon && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
522
+ className: `nb-admin-bottom-nav__icon ${item.icon.includes(" ") ? item.icon : `ph ph-${item.icon}`}`,
523
+ "aria-hidden": "true"
455
524
  }),
456
- !isLarge && isMenuOpen && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
457
- className: "nb-admin-shell__scrim",
458
- type: "button",
459
- "aria-label": strings.closeMenu,
460
- onClick: closeMenuFromOverlay
525
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
526
+ className: "nb-admin-bottom-nav__label",
527
+ children: item.text
461
528
  }),
462
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("main", {
463
- className: "nb-admin-shell__content content",
464
- tabIndex: 0,
465
- "aria-label": title ? `${title} content` : strings.mainContent,
466
- children
529
+ item.badge != null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
530
+ className: "nb-admin-bottom-nav__badge",
531
+ children: item.badge
467
532
  })
468
533
  ]
469
- })]
534
+ }, item.path))
470
535
  });
471
- };
536
+ }
472
537
  //#endregion
473
538
  //#region packages/admin/auth/csrfFetch.ts
474
539
  const MUTATING_METHODS = /* @__PURE__ */ new Set([
@@ -2146,8 +2211,7 @@ function NubitAuthenticatedApp({ config }) {
2146
2211
  const shell = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdminShell, {
2147
2212
  title: config.title,
2148
2213
  menuItems,
2149
- headerActions: config.shell?.headerActions,
2150
- footer: config.shell?.footer,
2214
+ ...config.shell,
2151
2215
  renderThemeSwitcher,
2152
2216
  renderUserMenu: ({ close }) => renderUserMenu({
2153
2217
  ...menuContext,
@@ -2246,6 +2310,7 @@ function createNubitApp(config) {
2246
2310
  client: queryClient,
2247
2311
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.ThemeProvider, {
2248
2312
  basePath: config.themeBasePath,
2313
+ visualStyle: config.visualStyle,
2249
2314
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionProvider, {
2250
2315
  apiBaseUrl,
2251
2316
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NubitAuthenticatedApp, { config })
@@ -2258,6 +2323,7 @@ function createNubitApp(config) {
2258
2323
  //#endregion
2259
2324
  exports.AcceptInvitationPage = AcceptInvitationPage;
2260
2325
  exports.AccountPage = AccountPage;
2326
+ exports.AdminBottomNavigation = AdminBottomNavigation;
2261
2327
  exports.AdminHeader = AdminHeader;
2262
2328
  exports.AdminShell = AdminShell;
2263
2329
  exports.AdminSidebarMenu = AdminSidebarMenu;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import React$1, { ReactNode } from "react";
2
- import { FeatureGateProps as FeatureGateProps$1 } from "@nubitio/ui";
2
+ import { FeatureGateProps as FeatureGateProps$1, ThemeProviderProps } from "@nubitio/ui";
3
3
  import { CoreRuntime } from "@nubitio/core";
4
4
  import { QueryClient } from "@tanstack/react-query";
5
5
  //#region packages/admin/FeatureHubLayout.d.ts
@@ -52,12 +52,14 @@ interface AdminHeaderProps {
52
52
  toggleMenu?: (e: React$1.MouseEvent<HTMLButtonElement>) => void;
53
53
  className?: string;
54
54
  actions?: AdminHeaderAction[];
55
+ /** Search, command trigger or other application-owned header content. */
56
+ content?: React$1.ReactNode;
55
57
  renderUserMenu?: (props: {
56
58
  close: () => void;
57
59
  }) => React$1.ReactNode;
58
60
  renderThemeSwitcher?: () => React$1.ReactNode;
59
61
  }
60
- declare const AdminHeader: ({ title, menuToggleEnabled, toggleMenu, className, actions, renderUserMenu, renderThemeSwitcher }: AdminHeaderProps) => React$1.JSX.Element;
62
+ declare const AdminHeader: ({ title, menuToggleEnabled, toggleMenu, className, actions, content, renderUserMenu, renderThemeSwitcher }: AdminHeaderProps) => React$1.JSX.Element;
61
63
  //#endregion
62
64
  //#region packages/admin/AdminSidebarMenu.d.ts
63
65
  interface AdminMenuSubItem {
@@ -81,8 +83,10 @@ interface AdminSidebarMenuProps {
81
83
  selectedItemChanged: (e: AdminSidebarMenuSelectEvent) => void;
82
84
  openMenu?: (e: React$1.PointerEvent) => void;
83
85
  footer?: React$1.ReactNode;
86
+ brand?: React$1.ReactNode;
87
+ context?: React$1.ReactNode;
84
88
  }
85
- declare const AdminSidebarMenu: ({ items, compactMode, selectedItemChanged, openMenu, footer }: AdminSidebarMenuProps) => React$1.JSX.Element;
89
+ declare const AdminSidebarMenu: ({ items, compactMode, selectedItemChanged, openMenu, footer, brand, context }: AdminSidebarMenuProps) => React$1.JSX.Element;
86
90
  //#endregion
87
91
  //#region packages/admin/AdminShell.d.ts
88
92
  interface AdminShellProps {
@@ -94,9 +98,31 @@ interface AdminShellProps {
94
98
  }) => React$1.ReactNode;
95
99
  renderThemeSwitcher?: () => React$1.ReactNode;
96
100
  footer?: React$1.ReactNode;
101
+ /** Workspace places the sidebar alongside the header on large screens. */
102
+ variant?: 'default' | 'workspace';
103
+ brand?: React$1.ReactNode;
104
+ headerContent?: React$1.ReactNode;
105
+ /** Application owns context selection (e.g. company/branch); the shell only hosts it. */
106
+ sidebarContext?: React$1.ReactNode;
107
+ /** Rendered below content on extra-small screens, without covering the scroll area. */
108
+ bottomNavigation?: React$1.ReactNode;
97
109
  children: React$1.ReactNode;
98
110
  }
99
- declare const AdminShell: ({ title, menuItems, headerActions, renderUserMenu, renderThemeSwitcher, footer, children }: AdminShellProps) => React$1.JSX.Element;
111
+ declare const AdminShell: ({ title, menuItems, headerActions, renderUserMenu, renderThemeSwitcher, footer, variant, brand, headerContent, sidebarContext, bottomNavigation, children }: AdminShellProps) => React$1.JSX.Element;
112
+ //#endregion
113
+ //#region packages/admin/AdminBottomNavigation.d.ts
114
+ interface AdminBottomNavigationItem {
115
+ text: string;
116
+ path: string;
117
+ icon?: string;
118
+ badge?: ReactNode;
119
+ }
120
+ interface AdminBottomNavigationProps {
121
+ /** Pass only destinations this user may access; authorization stays in the app/API. */
122
+ items: AdminBottomNavigationItem[];
123
+ label?: string;
124
+ }
125
+ declare function AdminBottomNavigation({ items, label }: AdminBottomNavigationProps): import("react").JSX.Element;
100
126
  //#endregion
101
127
  //#region packages/admin/useScreenSize.d.ts
102
128
  declare const useScreenSize: () => {
@@ -438,6 +464,7 @@ type CreateNubitAppConfig = {
438
464
  currency?: string;
439
465
  /** Base URL for generated theme CSS files. Default `/themes/`. */
440
466
  themeBasePath?: string;
467
+ visualStyle?: ThemeProviderProps['visualStyle'];
441
468
  menu: NubitAppMenuItem[] | ((ctx: NubitAppMenuContext) => NubitAppMenuItem[]);
442
469
  routes: NubitAppRoute[];
443
470
  /**
@@ -454,7 +481,7 @@ type CreateNubitAppConfig = {
454
481
  label: string;
455
482
  }[];
456
483
  };
457
- shell?: Pick<AdminShellProps, 'headerActions' | 'footer'>;
484
+ shell?: Pick<AdminShellProps, 'headerActions' | 'footer' | 'variant' | 'brand' | 'headerContent' | 'sidebarContext' | 'bottomNavigation'>;
458
485
  renderThemeSwitcher?: () => ReactNode;
459
486
  renderUserMenu?: (ctx: NubitAppUserMenuContext) => ReactNode;
460
487
  /** Wraps authenticated shell content (inside Hydra providers). */
@@ -488,4 +515,4 @@ declare function hasAnyRole(required: string | string[] | undefined, roles: stri
488
515
  */
489
516
  declare function filterMenuByRoles(items: NubitAppMenuItem[], roles: string[]): AdminMenuItem[];
490
517
  //#endregion
491
- export { AcceptInvitationPage, type AcceptInvitationPageProps, AccountPage, type AccountPageProps, AdminHeader, type AdminHeaderAction, type AdminHeaderProps, type AdminMenuItem, type AdminMenuSubItem, AdminShell, type AdminShellProps, AdminSidebarMenu, type AdminSidebarMenuProps, type AdminSidebarMenuSelectEvent, type AppProfile, type CreateNubitAppConfig, FeatureGate, type FeatureGateProps, type FeatureHubBanner, type FeatureHubDensity, FeatureHubLayout, type FeatureHubLayoutProps, type FeatureHubTab, ForgotPasswordPage, type ForgotPasswordPageProps, type LoginOidcProvider, LoginPage, type LoginPageProps, type NotificationItem, NotificationPanel, type NotificationPanelProps, type NotificationType, type NubitApp, type NubitAppMenuContext, type NubitAppMenuItem, type NubitAppMenuSubItem, type NubitAppRoute, type NubitAppUserMenuContext, NubitDevToolsPanel, type ParsedQuotaError, type PlanDefinition, PlanPanel, type PlanPanelLabels, type PlanPanelProps, QuotaUsageBanner, type QuotaUsageBannerProps, type RegisterField, type RegisterFieldType, RegisterPage, type RegisterPageProps, type RegisterSelectOption, ResetPasswordPage, type ResetPasswordPageProps, type RuntimeConfig, type RuntimeConfigState, type SessionContextValue, type SessionFeatureEntitlement, type SessionPermissionLimit, type SessionProfile, SessionProvider, type SessionProviderConfig, type SessionState, type SessionTenant, StaticSessionProvider, ToastHost, type ToastHostProps, type ToastItem, type UseNotificationsOptions, type UseQuotaUsageOptions, type UseRuntimeConfigOptions, createNubitApp, filterMenuByRoles, hasAnyRole, parseQuotaError, quotaErrorToastMessage, quotaUsageAboveGrid, useAppRuntime, useFeature, useFeatureConfig, useNotifications, useQuotaUsage, useRuntimeConfig, useScreenSize, useScreenSizeClass, useSession };
518
+ export { AcceptInvitationPage, type AcceptInvitationPageProps, AccountPage, type AccountPageProps, AdminBottomNavigation, type AdminBottomNavigationItem, type AdminBottomNavigationProps, AdminHeader, type AdminHeaderAction, type AdminHeaderProps, type AdminMenuItem, type AdminMenuSubItem, AdminShell, type AdminShellProps, AdminSidebarMenu, type AdminSidebarMenuProps, type AdminSidebarMenuSelectEvent, type AppProfile, type CreateNubitAppConfig, FeatureGate, type FeatureGateProps, type FeatureHubBanner, type FeatureHubDensity, FeatureHubLayout, type FeatureHubLayoutProps, type FeatureHubTab, ForgotPasswordPage, type ForgotPasswordPageProps, type LoginOidcProvider, LoginPage, type LoginPageProps, type NotificationItem, NotificationPanel, type NotificationPanelProps, type NotificationType, type NubitApp, type NubitAppMenuContext, type NubitAppMenuItem, type NubitAppMenuSubItem, type NubitAppRoute, type NubitAppUserMenuContext, NubitDevToolsPanel, type ParsedQuotaError, type PlanDefinition, PlanPanel, type PlanPanelLabels, type PlanPanelProps, QuotaUsageBanner, type QuotaUsageBannerProps, type RegisterField, type RegisterFieldType, RegisterPage, type RegisterPageProps, type RegisterSelectOption, ResetPasswordPage, type ResetPasswordPageProps, type RuntimeConfig, type RuntimeConfigState, type SessionContextValue, type SessionFeatureEntitlement, type SessionPermissionLimit, type SessionProfile, SessionProvider, type SessionProviderConfig, type SessionState, type SessionTenant, StaticSessionProvider, ToastHost, type ToastHostProps, type ToastItem, type UseNotificationsOptions, type UseQuotaUsageOptions, type UseRuntimeConfigOptions, createNubitApp, filterMenuByRoles, hasAnyRole, parseQuotaError, quotaErrorToastMessage, quotaUsageAboveGrid, useAppRuntime, useFeature, useFeatureConfig, useNotifications, useQuotaUsage, useRuntimeConfig, useScreenSize, useScreenSizeClass, useSession };
package/dist/index.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import React$1, { ReactNode } from "react";
2
- import { FeatureGateProps as FeatureGateProps$1 } from "@nubitio/ui";
2
+ import { FeatureGateProps as FeatureGateProps$1, ThemeProviderProps } from "@nubitio/ui";
3
3
  import { CoreRuntime } from "@nubitio/core";
4
4
  import { QueryClient } from "@tanstack/react-query";
5
5
  //#region packages/admin/FeatureHubLayout.d.ts
@@ -52,12 +52,14 @@ interface AdminHeaderProps {
52
52
  toggleMenu?: (e: React$1.MouseEvent<HTMLButtonElement>) => void;
53
53
  className?: string;
54
54
  actions?: AdminHeaderAction[];
55
+ /** Search, command trigger or other application-owned header content. */
56
+ content?: React$1.ReactNode;
55
57
  renderUserMenu?: (props: {
56
58
  close: () => void;
57
59
  }) => React$1.ReactNode;
58
60
  renderThemeSwitcher?: () => React$1.ReactNode;
59
61
  }
60
- declare const AdminHeader: ({ title, menuToggleEnabled, toggleMenu, className, actions, renderUserMenu, renderThemeSwitcher }: AdminHeaderProps) => React$1.JSX.Element;
62
+ declare const AdminHeader: ({ title, menuToggleEnabled, toggleMenu, className, actions, content, renderUserMenu, renderThemeSwitcher }: AdminHeaderProps) => React$1.JSX.Element;
61
63
  //#endregion
62
64
  //#region packages/admin/AdminSidebarMenu.d.ts
63
65
  interface AdminMenuSubItem {
@@ -81,8 +83,10 @@ interface AdminSidebarMenuProps {
81
83
  selectedItemChanged: (e: AdminSidebarMenuSelectEvent) => void;
82
84
  openMenu?: (e: React$1.PointerEvent) => void;
83
85
  footer?: React$1.ReactNode;
86
+ brand?: React$1.ReactNode;
87
+ context?: React$1.ReactNode;
84
88
  }
85
- declare const AdminSidebarMenu: ({ items, compactMode, selectedItemChanged, openMenu, footer }: AdminSidebarMenuProps) => React$1.JSX.Element;
89
+ declare const AdminSidebarMenu: ({ items, compactMode, selectedItemChanged, openMenu, footer, brand, context }: AdminSidebarMenuProps) => React$1.JSX.Element;
86
90
  //#endregion
87
91
  //#region packages/admin/AdminShell.d.ts
88
92
  interface AdminShellProps {
@@ -94,9 +98,31 @@ interface AdminShellProps {
94
98
  }) => React$1.ReactNode;
95
99
  renderThemeSwitcher?: () => React$1.ReactNode;
96
100
  footer?: React$1.ReactNode;
101
+ /** Workspace places the sidebar alongside the header on large screens. */
102
+ variant?: 'default' | 'workspace';
103
+ brand?: React$1.ReactNode;
104
+ headerContent?: React$1.ReactNode;
105
+ /** Application owns context selection (e.g. company/branch); the shell only hosts it. */
106
+ sidebarContext?: React$1.ReactNode;
107
+ /** Rendered below content on extra-small screens, without covering the scroll area. */
108
+ bottomNavigation?: React$1.ReactNode;
97
109
  children: React$1.ReactNode;
98
110
  }
99
- declare const AdminShell: ({ title, menuItems, headerActions, renderUserMenu, renderThemeSwitcher, footer, children }: AdminShellProps) => React$1.JSX.Element;
111
+ declare const AdminShell: ({ title, menuItems, headerActions, renderUserMenu, renderThemeSwitcher, footer, variant, brand, headerContent, sidebarContext, bottomNavigation, children }: AdminShellProps) => React$1.JSX.Element;
112
+ //#endregion
113
+ //#region packages/admin/AdminBottomNavigation.d.ts
114
+ interface AdminBottomNavigationItem {
115
+ text: string;
116
+ path: string;
117
+ icon?: string;
118
+ badge?: ReactNode;
119
+ }
120
+ interface AdminBottomNavigationProps {
121
+ /** Pass only destinations this user may access; authorization stays in the app/API. */
122
+ items: AdminBottomNavigationItem[];
123
+ label?: string;
124
+ }
125
+ declare function AdminBottomNavigation({ items, label }: AdminBottomNavigationProps): import("react").JSX.Element;
100
126
  //#endregion
101
127
  //#region packages/admin/useScreenSize.d.ts
102
128
  declare const useScreenSize: () => {
@@ -438,6 +464,7 @@ type CreateNubitAppConfig = {
438
464
  currency?: string;
439
465
  /** Base URL for generated theme CSS files. Default `/themes/`. */
440
466
  themeBasePath?: string;
467
+ visualStyle?: ThemeProviderProps['visualStyle'];
441
468
  menu: NubitAppMenuItem[] | ((ctx: NubitAppMenuContext) => NubitAppMenuItem[]);
442
469
  routes: NubitAppRoute[];
443
470
  /**
@@ -454,7 +481,7 @@ type CreateNubitAppConfig = {
454
481
  label: string;
455
482
  }[];
456
483
  };
457
- shell?: Pick<AdminShellProps, 'headerActions' | 'footer'>;
484
+ shell?: Pick<AdminShellProps, 'headerActions' | 'footer' | 'variant' | 'brand' | 'headerContent' | 'sidebarContext' | 'bottomNavigation'>;
458
485
  renderThemeSwitcher?: () => ReactNode;
459
486
  renderUserMenu?: (ctx: NubitAppUserMenuContext) => ReactNode;
460
487
  /** Wraps authenticated shell content (inside Hydra providers). */
@@ -488,4 +515,4 @@ declare function hasAnyRole(required: string | string[] | undefined, roles: stri
488
515
  */
489
516
  declare function filterMenuByRoles(items: NubitAppMenuItem[], roles: string[]): AdminMenuItem[];
490
517
  //#endregion
491
- export { AcceptInvitationPage, type AcceptInvitationPageProps, AccountPage, type AccountPageProps, AdminHeader, type AdminHeaderAction, type AdminHeaderProps, type AdminMenuItem, type AdminMenuSubItem, AdminShell, type AdminShellProps, AdminSidebarMenu, type AdminSidebarMenuProps, type AdminSidebarMenuSelectEvent, type AppProfile, type CreateNubitAppConfig, FeatureGate, type FeatureGateProps, type FeatureHubBanner, type FeatureHubDensity, FeatureHubLayout, type FeatureHubLayoutProps, type FeatureHubTab, ForgotPasswordPage, type ForgotPasswordPageProps, type LoginOidcProvider, LoginPage, type LoginPageProps, type NotificationItem, NotificationPanel, type NotificationPanelProps, type NotificationType, type NubitApp, type NubitAppMenuContext, type NubitAppMenuItem, type NubitAppMenuSubItem, type NubitAppRoute, type NubitAppUserMenuContext, NubitDevToolsPanel, type ParsedQuotaError, type PlanDefinition, PlanPanel, type PlanPanelLabels, type PlanPanelProps, QuotaUsageBanner, type QuotaUsageBannerProps, type RegisterField, type RegisterFieldType, RegisterPage, type RegisterPageProps, type RegisterSelectOption, ResetPasswordPage, type ResetPasswordPageProps, type RuntimeConfig, type RuntimeConfigState, type SessionContextValue, type SessionFeatureEntitlement, type SessionPermissionLimit, type SessionProfile, SessionProvider, type SessionProviderConfig, type SessionState, type SessionTenant, StaticSessionProvider, ToastHost, type ToastHostProps, type ToastItem, type UseNotificationsOptions, type UseQuotaUsageOptions, type UseRuntimeConfigOptions, createNubitApp, filterMenuByRoles, hasAnyRole, parseQuotaError, quotaErrorToastMessage, quotaUsageAboveGrid, useAppRuntime, useFeature, useFeatureConfig, useNotifications, useQuotaUsage, useRuntimeConfig, useScreenSize, useScreenSizeClass, useSession };
518
+ export { AcceptInvitationPage, type AcceptInvitationPageProps, AccountPage, type AccountPageProps, AdminBottomNavigation, type AdminBottomNavigationItem, type AdminBottomNavigationProps, AdminHeader, type AdminHeaderAction, type AdminHeaderProps, type AdminMenuItem, type AdminMenuSubItem, AdminShell, type AdminShellProps, AdminSidebarMenu, type AdminSidebarMenuProps, type AdminSidebarMenuSelectEvent, type AppProfile, type CreateNubitAppConfig, FeatureGate, type FeatureGateProps, type FeatureHubBanner, type FeatureHubDensity, FeatureHubLayout, type FeatureHubLayoutProps, type FeatureHubTab, ForgotPasswordPage, type ForgotPasswordPageProps, type LoginOidcProvider, LoginPage, type LoginPageProps, type NotificationItem, NotificationPanel, type NotificationPanelProps, type NotificationType, type NubitApp, type NubitAppMenuContext, type NubitAppMenuItem, type NubitAppMenuSubItem, type NubitAppRoute, type NubitAppUserMenuContext, NubitDevToolsPanel, type ParsedQuotaError, type PlanDefinition, PlanPanel, type PlanPanelLabels, type PlanPanelProps, QuotaUsageBanner, type QuotaUsageBannerProps, type RegisterField, type RegisterFieldType, RegisterPage, type RegisterPageProps, type RegisterSelectOption, ResetPasswordPage, type ResetPasswordPageProps, type RuntimeConfig, type RuntimeConfigState, type SessionContextValue, type SessionFeatureEntitlement, type SessionPermissionLimit, type SessionProfile, SessionProvider, type SessionProviderConfig, type SessionState, type SessionTenant, StaticSessionProvider, ToastHost, type ToastHostProps, type ToastItem, type UseNotificationsOptions, type UseQuotaUsageOptions, type UseRuntimeConfigOptions, createNubitApp, filterMenuByRoles, hasAnyRole, parseQuotaError, quotaErrorToastMessage, quotaUsageAboveGrid, useAppRuntime, useFeature, useFeatureConfig, useNotifications, useQuotaUsage, useRuntimeConfig, useScreenSize, useScreenSizeClass, useSession };
package/dist/index.mjs CHANGED
@@ -142,7 +142,7 @@ function UserMenuPopover({ renderUserMenu }) {
142
142
  })]
143
143
  });
144
144
  }
145
- const AdminHeader = ({ title, menuToggleEnabled, toggleMenu, className, actions = [], renderUserMenu, renderThemeSwitcher }) => {
145
+ const AdminHeader = ({ title, menuToggleEnabled, toggleMenu, className, actions = [], content, renderUserMenu, renderThemeSwitcher }) => {
146
146
  const strings = useUiStrings();
147
147
  return /* @__PURE__ */ jsx("header", {
148
148
  className: ["nb-admin-header-component", className].filter(Boolean).join(" "),
@@ -150,25 +150,32 @@ const AdminHeader = ({ title, menuToggleEnabled, toggleMenu, className, actions
150
150
  className: "nb-admin-header-toolbar",
151
151
  role: "toolbar",
152
152
  "aria-label": strings.mainToolbar,
153
- children: [/* @__PURE__ */ jsxs("div", {
154
- className: "nb-admin-header-toolbar__before",
155
- children: [menuToggleEnabled && /* @__PURE__ */ jsx(IconButton, {
156
- className: "nb-admin-menu-button",
157
- icon: "ph ph-list",
158
- label: strings.toggleMenu,
159
- onClick: toggleMenu
160
- }), title && /* @__PURE__ */ jsx("div", {
161
- className: "nb-admin-header-title",
162
- children: title
163
- })]
164
- }), /* @__PURE__ */ jsxs("div", {
165
- className: "nb-admin-header-toolbar__after",
166
- children: [
167
- renderThemeSwitcher?.(),
168
- actions.map((action) => /* @__PURE__ */ jsx(ActionPopover, { action }, action.id)),
169
- renderUserMenu && /* @__PURE__ */ jsx(UserMenuPopover, { renderUserMenu })
170
- ]
171
- })]
153
+ children: [
154
+ /* @__PURE__ */ jsxs("div", {
155
+ className: "nb-admin-header-toolbar__before",
156
+ children: [menuToggleEnabled && /* @__PURE__ */ jsx(IconButton, {
157
+ className: "nb-admin-menu-button",
158
+ icon: "ph ph-list",
159
+ label: strings.toggleMenu,
160
+ onClick: toggleMenu
161
+ }), title && /* @__PURE__ */ jsx("div", {
162
+ className: "nb-admin-header-title",
163
+ children: title
164
+ })]
165
+ }),
166
+ content && /* @__PURE__ */ jsx("div", {
167
+ className: "nb-admin-header-content",
168
+ children: content
169
+ }),
170
+ /* @__PURE__ */ jsxs("div", {
171
+ className: "nb-admin-header-toolbar__after",
172
+ children: [
173
+ renderThemeSwitcher?.(),
174
+ actions.map((action) => /* @__PURE__ */ jsx(ActionPopover, { action }, action.id)),
175
+ renderUserMenu && /* @__PURE__ */ jsx(UserMenuPopover, { renderUserMenu })
176
+ ]
177
+ })
178
+ ]
172
179
  })
173
180
  });
174
181
  };
@@ -235,7 +242,7 @@ const getIconClassName = (icon) => {
235
242
  if (!icon) return "";
236
243
  return icon.includes(" ") ? icon : `ph ph-${icon}`;
237
244
  };
238
- const AdminSidebarMenu = ({ items, compactMode = false, selectedItemChanged, openMenu, footer }) => {
245
+ const AdminSidebarMenu = ({ items, compactMode = false, selectedItemChanged, openMenu, footer, brand, context }) => {
239
246
  const [expandedItemKeys, setExpandedItemKeys] = useState(/* @__PURE__ */ new Set());
240
247
  const strings = useUiStrings();
241
248
  const { isLarge } = useScreenSize();
@@ -284,73 +291,84 @@ const AdminSidebarMenu = ({ items, compactMode = false, selectedItemChanged, ope
284
291
  return /* @__PURE__ */ jsxs("div", {
285
292
  className: `nb-admin-menu${compactMode ? " compact" : ""}`,
286
293
  onPointerDown: openMenu,
287
- children: [/* @__PURE__ */ jsx("div", {
288
- className: "nb-admin-menu-container theme-dependent",
289
- children: /* @__PURE__ */ jsx("nav", {
290
- className: "nb-admin-menu__nav",
291
- "aria-label": strings.mainNavigation,
292
- children: items.map((item) => {
293
- const selected = isSelected(item.path);
294
- const containsSelected = parentContainsSelected(item);
295
- const itemKey = item.path || item.text;
296
- const hasChildren = (item.items?.length ?? 0) > 0;
297
- const expanded = !compactMode && hasChildren && expandedItemKeys.has(itemKey);
298
- return /* @__PURE__ */ jsxs("section", {
299
- className: `nb-admin-menu__section${containsSelected ? " has-selected-child" : ""}`,
300
- children: [/* @__PURE__ */ jsxs("button", {
301
- className: `nb-admin-menu__item nb-admin-menu__item--parent${selected ? " is-selected" : ""}`,
302
- type: "button",
303
- "aria-current": selected ? "page" : void 0,
304
- "aria-expanded": hasChildren ? expanded : void 0,
305
- onClick: (event) => {
306
- if (hasChildren) {
307
- toggleExpanded(itemKey);
308
- return;
309
- }
310
- handleSelect(item.path, selected, event);
311
- },
312
- children: [
313
- /* @__PURE__ */ jsx("span", {
314
- className: `nb-admin-menu__icon ${getIconClassName(item.icon)}`,
315
- "aria-hidden": "true"
316
- }),
317
- /* @__PURE__ */ jsx("span", {
318
- className: "nb-admin-menu__text",
319
- children: item.text
320
- }),
321
- hasChildren && /* @__PURE__ */ jsx("span", {
322
- className: `nb-admin-menu__chevron${expanded ? " is-expanded" : ""}`,
323
- "aria-hidden": "true"
324
- })
325
- ]
326
- }), expanded && (item.items?.length ?? 0) > 0 && /* @__PURE__ */ jsx("div", {
327
- className: "nb-admin-menu__children",
328
- children: item.items.map((subItem) => {
329
- const childSelected = isSelected(subItem.path);
330
- return /* @__PURE__ */ jsx("button", {
331
- className: `nb-admin-menu__item nb-admin-menu__item--child${childSelected ? " is-selected" : ""}`,
332
- type: "button",
333
- "aria-current": childSelected ? "page" : void 0,
334
- onClick: (event) => handleSelect(subItem.path, childSelected, event),
335
- children: /* @__PURE__ */ jsx("span", {
294
+ children: [
295
+ brand && !compactMode && /* @__PURE__ */ jsx("div", {
296
+ className: "nb-admin-menu__brand",
297
+ children: brand
298
+ }),
299
+ /* @__PURE__ */ jsx("div", {
300
+ className: "nb-admin-menu-container theme-dependent",
301
+ children: /* @__PURE__ */ jsx("nav", {
302
+ className: "nb-admin-menu__nav",
303
+ "aria-label": strings.mainNavigation,
304
+ children: items.map((item) => {
305
+ const selected = isSelected(item.path);
306
+ const containsSelected = parentContainsSelected(item);
307
+ const itemKey = item.path || item.text;
308
+ const hasChildren = (item.items?.length ?? 0) > 0;
309
+ const expanded = !compactMode && hasChildren && expandedItemKeys.has(itemKey);
310
+ return /* @__PURE__ */ jsxs("section", {
311
+ className: `nb-admin-menu__section${containsSelected ? " has-selected-child" : ""}`,
312
+ children: [/* @__PURE__ */ jsxs("button", {
313
+ className: `nb-admin-menu__item nb-admin-menu__item--parent${selected ? " is-selected" : ""}`,
314
+ type: "button",
315
+ "aria-current": selected ? "page" : void 0,
316
+ "aria-expanded": hasChildren ? expanded : void 0,
317
+ onClick: (event) => {
318
+ if (hasChildren) {
319
+ toggleExpanded(itemKey);
320
+ return;
321
+ }
322
+ handleSelect(item.path, selected, event);
323
+ },
324
+ children: [
325
+ /* @__PURE__ */ jsx("span", {
326
+ className: `nb-admin-menu__icon ${getIconClassName(item.icon)}`,
327
+ "aria-hidden": "true"
328
+ }),
329
+ /* @__PURE__ */ jsx("span", {
336
330
  className: "nb-admin-menu__text",
337
- children: subItem.text
331
+ children: item.text
332
+ }),
333
+ hasChildren && /* @__PURE__ */ jsx("span", {
334
+ className: `nb-admin-menu__chevron${expanded ? " is-expanded" : ""}`,
335
+ "aria-hidden": "true"
338
336
  })
339
- }, subItem.path || subItem.text);
340
- })
341
- })]
342
- }, itemKey);
337
+ ]
338
+ }), expanded && (item.items?.length ?? 0) > 0 && /* @__PURE__ */ jsx("div", {
339
+ className: "nb-admin-menu__children",
340
+ children: item.items.map((subItem) => {
341
+ const childSelected = isSelected(subItem.path);
342
+ return /* @__PURE__ */ jsx("button", {
343
+ className: `nb-admin-menu__item nb-admin-menu__item--child${childSelected ? " is-selected" : ""}`,
344
+ type: "button",
345
+ "aria-current": childSelected ? "page" : void 0,
346
+ onClick: (event) => handleSelect(subItem.path, childSelected, event),
347
+ children: /* @__PURE__ */ jsx("span", {
348
+ className: "nb-admin-menu__text",
349
+ children: subItem.text
350
+ })
351
+ }, subItem.path || subItem.text);
352
+ })
353
+ })]
354
+ }, itemKey);
355
+ })
343
356
  })
357
+ }),
358
+ context && !compactMode && /* @__PURE__ */ jsx("div", {
359
+ className: "nb-admin-menu__context",
360
+ children: context
361
+ }),
362
+ footer && /* @__PURE__ */ jsx("footer", {
363
+ className: "nb-admin-footer",
364
+ children: footer
344
365
  })
345
- }), footer && /* @__PURE__ */ jsx("footer", {
346
- className: "nb-admin-footer",
347
- children: footer
348
- })]
366
+ ]
349
367
  });
350
368
  };
351
369
  //#endregion
352
370
  //#region packages/admin/AdminShell.tsx
353
- const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThemeSwitcher, footer, children }) => {
371
+ const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThemeSwitcher, footer, variant = "default", brand, headerContent, sidebarContext, bottomNavigation, children }) => {
354
372
  const navigate = useNavigate();
355
373
  const strings = useUiStrings();
356
374
  const { isXSmall, isLarge } = useScreenSize();
@@ -406,45 +424,92 @@ const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThe
406
424
  isXSmall ? "is-xsmall" : ""
407
425
  ].filter(Boolean).join(" ");
408
426
  return /* @__PURE__ */ jsxs("div", {
409
- className: "nb-admin-shell",
410
- children: [/* @__PURE__ */ jsx(AdminHeader, {
411
- className: "nb-admin-layout-header",
412
- menuToggleEnabled: true,
413
- toggleMenu,
414
- title,
415
- actions: headerActions,
416
- renderUserMenu,
417
- renderThemeSwitcher
418
- }), /* @__PURE__ */ jsxs("div", {
419
- className: bodyClassName,
420
- children: [
421
- /* @__PURE__ */ jsx("aside", {
422
- className: "nb-admin-shell__panel",
423
- "aria-label": strings.mainMenu,
424
- children: /* @__PURE__ */ jsx(AdminSidebarMenu, {
425
- items: menuItems,
426
- compactMode: isCompact,
427
- selectedItemChanged: onNavigationChanged,
428
- openMenu: temporaryOpenMenu,
429
- footer
427
+ className: [
428
+ "nb-admin-shell",
429
+ variant === "workspace" && "nb-admin-shell--workspace",
430
+ variant === "workspace" && isLarge && "is-workspace-desktop",
431
+ variant === "workspace" && isLarge && isCompact && "is-workspace-compact"
432
+ ].filter(Boolean).join(" "),
433
+ children: [
434
+ /* @__PURE__ */ jsx(AdminHeader, {
435
+ className: "nb-admin-layout-header",
436
+ menuToggleEnabled: true,
437
+ toggleMenu,
438
+ title,
439
+ actions: headerActions,
440
+ content: headerContent,
441
+ renderUserMenu,
442
+ renderThemeSwitcher
443
+ }),
444
+ /* @__PURE__ */ jsxs("div", {
445
+ className: bodyClassName,
446
+ children: [
447
+ /* @__PURE__ */ jsx("aside", {
448
+ className: "nb-admin-shell__panel",
449
+ "aria-label": strings.mainMenu,
450
+ children: /* @__PURE__ */ jsx(AdminSidebarMenu, {
451
+ items: menuItems,
452
+ compactMode: isCompact,
453
+ selectedItemChanged: onNavigationChanged,
454
+ openMenu: temporaryOpenMenu,
455
+ footer,
456
+ brand,
457
+ context: sidebarContext
458
+ })
459
+ }),
460
+ !isLarge && isMenuOpen && /* @__PURE__ */ jsx("button", {
461
+ className: "nb-admin-shell__scrim",
462
+ type: "button",
463
+ "aria-label": strings.closeMenu,
464
+ onClick: closeMenuFromOverlay
465
+ }),
466
+ /* @__PURE__ */ jsx("main", {
467
+ className: "nb-admin-shell__content content",
468
+ tabIndex: 0,
469
+ "aria-label": title ? `${title} content` : strings.mainContent,
470
+ children
430
471
  })
472
+ ]
473
+ }),
474
+ isXSmall && bottomNavigation && /* @__PURE__ */ jsx("div", {
475
+ className: "nb-admin-shell__bottom-navigation",
476
+ children: bottomNavigation
477
+ })
478
+ ]
479
+ });
480
+ };
481
+ //#endregion
482
+ //#region packages/admin/AdminBottomNavigation.tsx
483
+ function AdminBottomNavigation({ items, label }) {
484
+ const { pathname } = useLocation();
485
+ const strings = useUiStrings();
486
+ const normalize = (path) => path.replace(/\/+$/, "") || "/";
487
+ const currentPath = normalize(pathname);
488
+ const activePath = items.map((item) => normalize(item.path)).filter((path) => currentPath === path || path !== "/" && currentPath.startsWith(`${path}/`)).sort((a, b) => b.length - a.length)[0];
489
+ return /* @__PURE__ */ jsx("nav", {
490
+ className: "nb-admin-bottom-nav",
491
+ "aria-label": label ?? strings.mainNavigation,
492
+ children: items.map((item) => /* @__PURE__ */ jsxs(Link, {
493
+ to: item.path,
494
+ className: "nb-admin-bottom-nav__item",
495
+ "aria-current": normalize(item.path) === activePath ? "page" : void 0,
496
+ children: [
497
+ item.icon && /* @__PURE__ */ jsx("span", {
498
+ className: `nb-admin-bottom-nav__icon ${item.icon.includes(" ") ? item.icon : `ph ph-${item.icon}`}`,
499
+ "aria-hidden": "true"
431
500
  }),
432
- !isLarge && isMenuOpen && /* @__PURE__ */ jsx("button", {
433
- className: "nb-admin-shell__scrim",
434
- type: "button",
435
- "aria-label": strings.closeMenu,
436
- onClick: closeMenuFromOverlay
501
+ /* @__PURE__ */ jsx("span", {
502
+ className: "nb-admin-bottom-nav__label",
503
+ children: item.text
437
504
  }),
438
- /* @__PURE__ */ jsx("main", {
439
- className: "nb-admin-shell__content content",
440
- tabIndex: 0,
441
- "aria-label": title ? `${title} content` : strings.mainContent,
442
- children
505
+ item.badge != null && /* @__PURE__ */ jsx("span", {
506
+ className: "nb-admin-bottom-nav__badge",
507
+ children: item.badge
443
508
  })
444
509
  ]
445
- })]
510
+ }, item.path))
446
511
  });
447
- };
512
+ }
448
513
  //#endregion
449
514
  //#region packages/admin/auth/csrfFetch.ts
450
515
  const MUTATING_METHODS = /* @__PURE__ */ new Set([
@@ -2122,8 +2187,7 @@ function NubitAuthenticatedApp({ config }) {
2122
2187
  const shell = /* @__PURE__ */ jsx(AdminShell, {
2123
2188
  title: config.title,
2124
2189
  menuItems,
2125
- headerActions: config.shell?.headerActions,
2126
- footer: config.shell?.footer,
2190
+ ...config.shell,
2127
2191
  renderThemeSwitcher,
2128
2192
  renderUserMenu: ({ close }) => renderUserMenu({
2129
2193
  ...menuContext,
@@ -2222,6 +2286,7 @@ function createNubitApp(config) {
2222
2286
  client: queryClient,
2223
2287
  children: /* @__PURE__ */ jsx(ThemeProvider, {
2224
2288
  basePath: config.themeBasePath,
2289
+ visualStyle: config.visualStyle,
2225
2290
  children: /* @__PURE__ */ jsx(SessionProvider, {
2226
2291
  apiBaseUrl,
2227
2292
  children: /* @__PURE__ */ jsx(NubitAuthenticatedApp, { config })
@@ -2232,4 +2297,4 @@ function createNubitApp(config) {
2232
2297
  return { App };
2233
2298
  }
2234
2299
  //#endregion
2235
- export { AcceptInvitationPage, AccountPage, AdminHeader, AdminShell, AdminSidebarMenu, FeatureGate, FeatureHubLayout, ForgotPasswordPage, LoginPage, NotificationPanel, NubitDevToolsPanel, PlanPanel, QuotaUsageBanner, RegisterPage, ResetPasswordPage, SessionProvider, StaticSessionProvider, ToastHost, createNubitApp, filterMenuByRoles, hasAnyRole, parseQuotaError, quotaErrorToastMessage, quotaUsageAboveGrid, useAppRuntime, useFeature, useFeatureConfig, useNotifications, useQuotaUsage, useRuntimeConfig, useScreenSize, useScreenSizeClass, useSession };
2300
+ export { AcceptInvitationPage, AccountPage, AdminBottomNavigation, AdminHeader, AdminShell, AdminSidebarMenu, FeatureGate, FeatureHubLayout, ForgotPasswordPage, LoginPage, NotificationPanel, NubitDevToolsPanel, PlanPanel, QuotaUsageBanner, RegisterPage, ResetPasswordPage, SessionProvider, StaticSessionProvider, ToastHost, createNubitApp, filterMenuByRoles, hasAnyRole, parseQuotaError, quotaErrorToastMessage, quotaUsageAboveGrid, useAppRuntime, useFeature, useFeatureConfig, useNotifications, useQuotaUsage, useRuntimeConfig, useScreenSize, useScreenSizeClass, useSession };
package/dist/style.css CHANGED
@@ -314,6 +314,169 @@ html[data-density=comfortable] {
314
314
  .nb-feature-hub--compact .nb-feature-hub__content {
315
315
  padding-top: var(--space-3);
316
316
  }
317
+ .nb-admin-header-content {
318
+ flex: 0 1 320px;
319
+ min-width: 0;
320
+ }
321
+ .nb-admin-header-content > * {
322
+ max-width: 100%;
323
+ }
324
+
325
+ .nb-admin-menu__brand {
326
+ align-items: center;
327
+ display: flex;
328
+ flex: 0 0 auto;
329
+ min-height: var(--admin-header-height, 64px);
330
+ padding: var(--space-3) var(--space-6);
331
+ }
332
+
333
+ .nb-admin-menu__context {
334
+ flex: 0 0 auto;
335
+ padding: var(--space-3);
336
+ }
337
+
338
+ .nb-admin-shell--workspace .nb-admin-menu-container {
339
+ padding-bottom: 0;
340
+ }
341
+ .nb-admin-shell--workspace .nb-admin-footer {
342
+ flex: 0 0 auto;
343
+ position: static;
344
+ }
345
+ .nb-admin-shell--workspace .nb-admin-shell__content {
346
+ min-width: 0;
347
+ }
348
+
349
+ .nb-admin-shell.is-workspace-desktop {
350
+ display: grid;
351
+ grid-template-columns: var(--admin-sidebar-width, 248px) minmax(0, 1fr);
352
+ grid-template-rows: auto minmax(0, 1fr);
353
+ }
354
+ .nb-admin-shell.is-workspace-desktop > .nb-admin-layout-header {
355
+ grid-column: 2;
356
+ grid-row: 1;
357
+ }
358
+ .nb-admin-shell.is-workspace-desktop > .nb-admin-shell__body {
359
+ display: contents;
360
+ }
361
+ .nb-admin-shell.is-workspace-desktop > .nb-admin-shell__body > .nb-admin-shell__panel {
362
+ grid-column: 1;
363
+ grid-row: 1/-1;
364
+ height: auto;
365
+ width: 100%;
366
+ }
367
+ .nb-admin-shell.is-workspace-desktop > .nb-admin-shell__body > .nb-admin-shell__content {
368
+ grid-column: 2;
369
+ grid-row: 2;
370
+ }
371
+ .nb-admin-shell.is-workspace-desktop.is-workspace-compact {
372
+ grid-template-columns: var(--side-panel-min-width) minmax(0, 1fr);
373
+ }
374
+
375
+ .nb-admin-shell__bottom-navigation {
376
+ background: var(--surface-1);
377
+ flex: 0 0 auto;
378
+ padding-bottom: env(safe-area-inset-bottom, 0px);
379
+ z-index: var(--z-shell-panel);
380
+ }
381
+
382
+ .nb-admin-bottom-nav {
383
+ background: var(--surface-1);
384
+ border-top: 1px solid var(--border-color);
385
+ display: flex;
386
+ padding: var(--space-2);
387
+ }
388
+ .nb-admin-bottom-nav__item {
389
+ align-items: center;
390
+ border-radius: var(--radius-sm);
391
+ color: var(--text-secondary);
392
+ display: flex;
393
+ flex: 1 1 0;
394
+ flex-direction: column;
395
+ gap: var(--space-1);
396
+ justify-content: center;
397
+ min-height: 64px;
398
+ min-width: 0;
399
+ padding: var(--space-2) var(--space-1);
400
+ position: relative;
401
+ text-align: center;
402
+ text-decoration: none;
403
+ }
404
+ .nb-admin-bottom-nav__item[aria-current=page] {
405
+ background: var(--accent-subtle);
406
+ color: var(--accent-on-subtle);
407
+ }
408
+ .nb-admin-bottom-nav__item:hover {
409
+ background: var(--surface-2);
410
+ }
411
+ .nb-admin-bottom-nav__item:focus-visible {
412
+ outline: 2px solid var(--focus-ring-color);
413
+ outline-offset: -2px;
414
+ }
415
+ .nb-admin-bottom-nav__icon {
416
+ font-size: 24px;
417
+ }
418
+ .nb-admin-bottom-nav__label {
419
+ font-size: var(--font-size-xs);
420
+ overflow-wrap: anywhere;
421
+ }
422
+ .nb-admin-bottom-nav__badge {
423
+ background: var(--accent-color);
424
+ border-radius: var(--radius-pill);
425
+ color: var(--on-accent-color);
426
+ font-size: var(--font-size-xxs);
427
+ padding: 0 var(--space-1);
428
+ position: absolute;
429
+ right: var(--space-1);
430
+ top: 0;
431
+ }
432
+
433
+ html[data-visual-style=nubit] .nb-admin-header-toolbar {
434
+ gap: var(--space-4);
435
+ min-height: calc(var(--admin-header-height) - 1px);
436
+ padding-right: var(--space-7);
437
+ }
438
+ html[data-visual-style=nubit] .nb-admin-header-title {
439
+ font-size: var(--font-size-xl);
440
+ font-weight: var(--font-weight-semibold);
441
+ }
442
+ html[data-visual-style=nubit] .nb-admin-menu__nav {
443
+ gap: var(--space-1);
444
+ padding: var(--space-3);
445
+ }
446
+ html[data-visual-style=nubit] .nb-admin-menu__section {
447
+ border: 0;
448
+ }
449
+ html[data-visual-style=nubit] .nb-admin-menu__item {
450
+ border-left: 0;
451
+ border-radius: var(--radius-sm);
452
+ min-height: 40px;
453
+ }
454
+ html[data-visual-style=nubit] .nb-admin-menu__item.is-selected {
455
+ background: var(--accent-subtle);
456
+ color: var(--accent-on-subtle);
457
+ }
458
+ html[data-visual-style=nubit] .nb-admin-menu__item--parent {
459
+ color: var(--text-secondary);
460
+ }
461
+ html[data-visual-style=nubit] .nb-admin-menu.compact .nb-admin-menu__nav {
462
+ padding-inline: 0;
463
+ }
464
+
465
+ @media (max-width: 575.98px) {
466
+ .nb-admin-header-toolbar:has(.nb-admin-header-content) .nb-admin-header-title {
467
+ display: none;
468
+ }
469
+ .nb-admin-header-toolbar:has(.nb-admin-header-content) .nb-admin-header-toolbar__before {
470
+ flex: 0 0 auto;
471
+ }
472
+ .nb-admin-header-content {
473
+ flex: 1 1 0;
474
+ }
475
+ html[data-visual-style=nubit] .nb-admin-header-toolbar {
476
+ gap: var(--space-1);
477
+ padding-right: var(--space-2);
478
+ }
479
+ }
317
480
  .nb-admin-shell {
318
481
  display: flex;
319
482
  flex-direction: column;
@@ -340,12 +503,12 @@ html[data-density=comfortable] {
340
503
  .nb-admin-shell__panel {
341
504
  background-color: var(--side-panel-background);
342
505
  border-right: 1px solid var(--border-color);
343
- flex: 0 0 240px;
506
+ flex: 0 0 var(--admin-sidebar-width, 240px);
344
507
  height: 100%;
345
508
  overflow: hidden;
346
509
  position: relative;
347
510
  transition: flex-basis var(--transition-slow), transform var(--transition-slow), width var(--transition-slow);
348
- width: 240px;
511
+ width: var(--admin-sidebar-width, 240px);
349
512
  z-index: var(--z-shell-panel);
350
513
  }
351
514
 
@@ -399,7 +562,7 @@ html[data-density=comfortable] {
399
562
  align-items: center;
400
563
  display: flex;
401
564
  gap: var(--space-2);
402
- min-height: var(--toolbar-height);
565
+ min-height: var(--admin-header-height, var(--toolbar-height));
403
566
  padding: 0 var(--space-4) 0 0;
404
567
  }
405
568
 
@@ -480,7 +643,7 @@ html[data-density=comfortable] {
480
643
  flex-direction: column;
481
644
  height: 100%;
482
645
  min-height: 100%;
483
- width: 240px !important;
646
+ width: var(--admin-sidebar-width, 240px) !important;
484
647
  }
485
648
  .nb-admin-menu .nb-admin-menu-container {
486
649
  background-color: var(--side-panel-background);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nubitio/admin",
3
- "version": "1.1.2",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "description": "Admin shell layout components: responsive sidebar, header, and screen size utilities for Nubit apps.",
6
6
  "license": "MIT",
@@ -53,10 +53,10 @@
53
53
  "react": "^18.2.0 || ^19.0.0",
54
54
  "react-dom": "^18.2.0 || ^19.0.0",
55
55
  "react-router-dom": "^6.0.0 || ^7.0.0",
56
- "@nubitio/core": "^1.1.2",
57
- "@nubitio/crud": "^1.1.2",
58
- "@nubitio/hydra": "^1.1.2",
59
- "@nubitio/ui": "^1.1.2"
56
+ "@nubitio/core": "^1.2.0",
57
+ "@nubitio/hydra": "^1.2.0",
58
+ "@nubitio/crud": "^1.2.0",
59
+ "@nubitio/ui": "^1.2.0"
60
60
  },
61
61
  "devDependencies": {
62
62
  "react-router-dom": "^6.30.6"