@nubitio/admin 0.5.15 → 0.5.19

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/dist/index.cjs CHANGED
@@ -26,6 +26,10 @@ react = __toESM(react, 1);
26
26
  let react_router_dom = require("react-router-dom");
27
27
  let _nubitio_ui = require("@nubitio/ui");
28
28
  let react_jsx_runtime = require("react/jsx-runtime");
29
+ let _tanstack_react_query = require("@tanstack/react-query");
30
+ let _nubitio_core = require("@nubitio/core");
31
+ let _nubitio_crud = require("@nubitio/crud");
32
+ let _nubitio_hydra = require("@nubitio/hydra");
29
33
  //#region packages/admin/AdminHeader.tsx
30
34
  function ActionPopover({ action }) {
31
35
  const { open, toggle, setOpen, containerRef } = (0, _nubitio_ui.useFloatingPanel)();
@@ -385,14 +389,14 @@ const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThe
385
389
  //#endregion
386
390
  //#region packages/admin/auth/SessionContext.tsx
387
391
  const SessionContext = (0, react.createContext)(null);
388
- function joinApiPath$1(apiBaseUrl, path) {
392
+ function joinApiPath$2(apiBaseUrl, path) {
389
393
  return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
390
394
  }
391
395
  function SessionProvider({ apiBaseUrl = "/api/", mePath = "me", logoutPath = "auth/logout", children }) {
392
396
  const [session, setSession] = (0, react.useState)({ status: "loading" });
393
397
  const refresh = (0, react.useCallback)(async () => {
394
398
  try {
395
- const response = await fetch(joinApiPath$1(apiBaseUrl, mePath), { credentials: "include" });
399
+ const response = await fetch(joinApiPath$2(apiBaseUrl, mePath), { credentials: "include" });
396
400
  if (!response.ok) {
397
401
  setSession({ status: "anonymous" });
398
402
  return;
@@ -409,7 +413,7 @@ function SessionProvider({ apiBaseUrl = "/api/", mePath = "me", logoutPath = "au
409
413
  refresh();
410
414
  }, [refresh]);
411
415
  const logout = (0, react.useCallback)(async () => {
412
- await fetch(joinApiPath$1(apiBaseUrl, logoutPath), {
416
+ await fetch(joinApiPath$2(apiBaseUrl, logoutPath), {
413
417
  method: "POST",
414
418
  credentials: "include"
415
419
  });
@@ -438,7 +442,7 @@ function useSession() {
438
442
  }
439
443
  //#endregion
440
444
  //#region packages/admin/auth/LoginPage.tsx
441
- function joinApiPath(apiBaseUrl, path) {
445
+ function joinApiPath$1(apiBaseUrl, path) {
442
446
  return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
443
447
  }
444
448
  function LoginPage({ onLoggedIn, apiBaseUrl = "/api/", loginPath = "auth/login", title = "Nubit Admin", hint, defaultUsername = "" }) {
@@ -451,7 +455,7 @@ function LoginPage({ onLoggedIn, apiBaseUrl = "/api/", loginPath = "auth/login",
451
455
  setBusy(true);
452
456
  setError(null);
453
457
  try {
454
- const response = await fetch(joinApiPath(apiBaseUrl, loginPath), {
458
+ const response = await fetch(joinApiPath$1(apiBaseUrl, loginPath), {
455
459
  method: "POST",
456
460
  headers: { "Content-Type": "application/json" },
457
461
  credentials: "include",
@@ -558,6 +562,48 @@ function useAppRuntime() {
558
562
  };
559
563
  }
560
564
  //#endregion
565
+ //#region packages/admin/runtime/useRuntimeConfig.ts
566
+ function joinApiPath(apiBaseUrl, path) {
567
+ return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
568
+ }
569
+ function useRuntimeConfig({ apiBaseUrl = "/api/", path = "runtime-config", enabled = true } = {}) {
570
+ const [state, setState] = (0, react.useState)({ status: "idle" });
571
+ const refresh = (0, react.useCallback)(async () => {
572
+ if (!enabled) {
573
+ setState({ status: "idle" });
574
+ return;
575
+ }
576
+ setState({ status: "loading" });
577
+ try {
578
+ const response = await fetch(joinApiPath(apiBaseUrl, path), { credentials: "include" });
579
+ if (!response.ok) {
580
+ setState({ status: "error" });
581
+ return;
582
+ }
583
+ setState({
584
+ status: "ready",
585
+ config: await response.json()
586
+ });
587
+ } catch {
588
+ setState({ status: "error" });
589
+ }
590
+ }, [
591
+ apiBaseUrl,
592
+ enabled,
593
+ path
594
+ ]);
595
+ (0, react.useEffect)(() => {
596
+ refresh();
597
+ }, [refresh]);
598
+ return {
599
+ state,
600
+ config: state.status === "ready" ? state.config : null,
601
+ loading: state.status === "loading",
602
+ error: state.status === "error",
603
+ refresh
604
+ };
605
+ }
606
+ //#endregion
561
607
  //#region packages/admin/runtime/ToastHost.tsx
562
608
  const TYPE_CLASS = {
563
609
  success: "nb-toast--success",
@@ -583,13 +629,185 @@ function ToastHost({ toasts, onDismiss }) {
583
629
  });
584
630
  }
585
631
  //#endregion
632
+ //#region packages/admin/app/filterMenuByRoles.ts
633
+ function hasAnyRole(required, roles) {
634
+ if (required === void 0) return true;
635
+ return (Array.isArray(required) ? required : [required]).some((role) => roles.includes(role));
636
+ }
637
+ /**
638
+ * Removes menu entries whose `roles` constraint is not satisfied. Sub-items with
639
+ * `roles` are filtered; empty parent groups are dropped.
640
+ */
641
+ function filterMenuByRoles(items, roles) {
642
+ const filtered = [];
643
+ for (const item of items) {
644
+ if (!hasAnyRole(item.roles, roles)) continue;
645
+ if (!item.items) {
646
+ filtered.push({
647
+ text: item.text,
648
+ path: item.path,
649
+ icon: item.icon
650
+ });
651
+ continue;
652
+ }
653
+ const subItems = item.items.filter((subItem) => hasAnyRole(subItem.roles, roles)).map(({ text, path }) => ({
654
+ text,
655
+ path
656
+ }));
657
+ if (subItems.length === 0) continue;
658
+ filtered.push({
659
+ text: item.text,
660
+ icon: item.icon,
661
+ path: item.path,
662
+ items: subItems
663
+ });
664
+ }
665
+ return filtered;
666
+ }
667
+ function resolveAppMenu(menu, ctx) {
668
+ return typeof menu === "function" ? menu(ctx) : menu;
669
+ }
670
+ //#endregion
671
+ //#region packages/admin/app/createNubitApp.tsx
672
+ function defaultUserMenu({ username, close, logout }) {
673
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
674
+ style: {
675
+ display: "flex",
676
+ flexDirection: "column",
677
+ gap: 8,
678
+ minWidth: 180
679
+ },
680
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
681
+ style: {
682
+ color: "var(--text-secondary)",
683
+ fontSize: "0.875rem"
684
+ },
685
+ children: username ?? "User"
686
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
687
+ type: "button",
688
+ onClick: () => {
689
+ close();
690
+ logout();
691
+ },
692
+ children: "Sign out"
693
+ })]
694
+ });
695
+ }
696
+ function buildMenuContext(session) {
697
+ const profile = session.session.status === "authenticated" ? session.session.profile : void 0;
698
+ return {
699
+ roles: session.roles,
700
+ username: session.username,
701
+ session: session.session,
702
+ appProfile: profile?.appProfile,
703
+ logout: session.logout
704
+ };
705
+ }
706
+ function resolveShellMenu(config, ctx) {
707
+ const declared = resolveAppMenu(config.menu, ctx);
708
+ const roleScoped = declared.some((item) => item.roles !== void 0 || item.items?.some((sub) => sub.roles !== void 0)) ? filterMenuByRoles(declared, ctx.roles) : declared.map(({ text, path, icon, items }) => ({
709
+ text,
710
+ path,
711
+ icon,
712
+ items
713
+ }));
714
+ return config.filterMenu ? config.filterMenu(roleScoped, ctx) : roleScoped;
715
+ }
716
+ function NubitAuthenticatedApp({ config }) {
717
+ const session = useSession();
718
+ const { runtime, toasts, dismiss } = useAppRuntime();
719
+ const apiBaseUrl = config.apiBaseUrl ?? "/api/";
720
+ const homePath = config.homePath ?? config.routes[0]?.path ?? "/";
721
+ const menuContext = (0, react.useMemo)(() => buildMenuContext(session), [session]);
722
+ const menuItems = (0, react.useMemo)(() => session.session.status === "authenticated" ? resolveShellMenu(config, menuContext) : [], [
723
+ config,
724
+ menuContext,
725
+ session.session.status
726
+ ]);
727
+ const renderThemeSwitcher = config.renderThemeSwitcher ?? (() => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.ThemeSwitcher, {}));
728
+ const renderUserMenu = config.renderUserMenu ?? defaultUserMenu;
729
+ const Wrapper = config.Wrapper ?? react.default.Fragment;
730
+ if (session.session.status === "loading") return null;
731
+ const shell = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AdminShell, {
732
+ title: config.title,
733
+ menuItems,
734
+ headerActions: config.shell?.headerActions,
735
+ footer: config.shell?.footer,
736
+ renderThemeSwitcher,
737
+ renderUserMenu: ({ close }) => renderUserMenu({
738
+ ...menuContext,
739
+ close
740
+ }),
741
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_router_dom.Routes, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_router_dom.Route, {
742
+ path: "/",
743
+ element: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_router_dom.Navigate, {
744
+ to: homePath,
745
+ replace: true
746
+ })
747
+ }), config.routes.map((route) => /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_router_dom.Route, {
748
+ path: route.path,
749
+ element: route.element
750
+ }, route.path))] })
751
+ });
752
+ const authenticated = /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_core.CoreProvider, {
753
+ http: {
754
+ baseUrl: apiBaseUrl,
755
+ refreshPath: "auth/refresh",
756
+ loginPath: "auth/login"
757
+ },
758
+ runtime,
759
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_core.CoreConfigProvider, {
760
+ apiBaseUrl,
761
+ locale: config.locale ?? "en",
762
+ timezone: config.timezone ?? "UTC",
763
+ currency: config.currency ?? "USD",
764
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(_nubitio_crud.SmartCrudRolesProvider, {
765
+ roles: session.roles,
766
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_router_dom.BrowserRouter, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Wrapper, { children: config.hydra === false ? shell : /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_core.MercureProvider, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_hydra.SchemaProvider, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_hydra.HydraResourceSchemaProvider, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_hydra.HydraResourceStoreProvider, { children: shell }) }) }) }) }) }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToastHost, {
767
+ toasts,
768
+ onDismiss: dismiss
769
+ })]
770
+ })
771
+ })
772
+ });
773
+ if (session.session.status === "authenticated") return authenticated;
774
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_router_dom.BrowserRouter, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(LoginPage, {
775
+ apiBaseUrl,
776
+ title: config.login?.title ?? config.title,
777
+ hint: config.login?.hint,
778
+ defaultUsername: config.login?.defaultUsername,
779
+ onLoggedIn: () => void session.refresh()
780
+ }) });
781
+ }
782
+ function createNubitApp(config) {
783
+ const queryClient = config.queryClient ?? new _tanstack_react_query.QueryClient({ defaultOptions: { queries: {
784
+ retry: 1,
785
+ staleTime: 3e4
786
+ } } });
787
+ const apiBaseUrl = config.apiBaseUrl ?? "/api/";
788
+ function App() {
789
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_tanstack_react_query.QueryClientProvider, {
790
+ client: queryClient,
791
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(_nubitio_ui.ThemeProvider, { children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SessionProvider, {
792
+ apiBaseUrl,
793
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NubitAuthenticatedApp, { config })
794
+ }) })
795
+ });
796
+ }
797
+ return { App };
798
+ }
799
+ //#endregion
586
800
  exports.AdminHeader = AdminHeader;
587
801
  exports.AdminShell = AdminShell;
588
802
  exports.AdminSidebarMenu = AdminSidebarMenu;
589
803
  exports.LoginPage = LoginPage;
590
804
  exports.SessionProvider = SessionProvider;
591
805
  exports.ToastHost = ToastHost;
806
+ exports.createNubitApp = createNubitApp;
807
+ exports.filterMenuByRoles = filterMenuByRoles;
808
+ exports.hasAnyRole = hasAnyRole;
592
809
  exports.useAppRuntime = useAppRuntime;
810
+ exports.useRuntimeConfig = useRuntimeConfig;
593
811
  exports.useScreenSize = useScreenSize;
594
812
  exports.useScreenSizeClass = useScreenSizeClass;
595
813
  exports.useSession = useSession;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,6 @@
1
- import React from "react";
1
+ import React$1, { ReactNode } from "react";
2
2
  import { CoreRuntime } from "@nubitio/core";
3
+ import { QueryClient } from "@tanstack/react-query";
3
4
 
4
5
  //#region packages/admin/AdminHeader.d.ts
5
6
  interface AdminHeaderAction {
@@ -11,18 +12,18 @@ interface AdminHeaderAction {
11
12
  onClick?: () => void;
12
13
  renderPanel?: (props: {
13
14
  close: () => void;
14
- }) => React.ReactNode;
15
+ }) => React$1.ReactNode;
15
16
  }
16
17
  interface AdminHeaderProps {
17
18
  title?: string;
18
19
  menuToggleEnabled?: boolean;
19
- toggleMenu?: (e: React.MouseEvent<HTMLButtonElement>) => void;
20
+ toggleMenu?: (e: React$1.MouseEvent<HTMLButtonElement>) => void;
20
21
  className?: string;
21
22
  actions?: AdminHeaderAction[];
22
23
  renderUserMenu?: (props: {
23
24
  close: () => void;
24
- }) => React.ReactNode;
25
- renderThemeSwitcher?: () => React.ReactNode;
25
+ }) => React$1.ReactNode;
26
+ renderThemeSwitcher?: () => React$1.ReactNode;
26
27
  }
27
28
  declare const AdminHeader: ({
28
29
  title,
@@ -32,7 +33,7 @@ declare const AdminHeader: ({
32
33
  actions,
33
34
  renderUserMenu,
34
35
  renderThemeSwitcher
35
- }: AdminHeaderProps) => React.JSX.Element;
36
+ }: AdminHeaderProps) => React$1.JSX.Element;
36
37
  //#endregion
37
38
  //#region packages/admin/AdminSidebarMenu.d.ts
38
39
  interface AdminMenuSubItem {
@@ -48,14 +49,14 @@ interface AdminMenuItem {
48
49
  interface AdminSidebarMenuSelectEvent {
49
50
  path?: string;
50
51
  selected: boolean;
51
- event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>;
52
+ event: React$1.MouseEvent<HTMLButtonElement | HTMLAnchorElement>;
52
53
  }
53
54
  interface AdminSidebarMenuProps {
54
55
  items: AdminMenuItem[];
55
56
  compactMode?: boolean;
56
57
  selectedItemChanged: (e: AdminSidebarMenuSelectEvent) => void;
57
- openMenu?: (e: React.PointerEvent) => void;
58
- footer?: React.ReactNode;
58
+ openMenu?: (e: React$1.PointerEvent) => void;
59
+ footer?: React$1.ReactNode;
59
60
  }
60
61
  declare const AdminSidebarMenu: ({
61
62
  items,
@@ -63,7 +64,7 @@ declare const AdminSidebarMenu: ({
63
64
  selectedItemChanged,
64
65
  openMenu,
65
66
  footer
66
- }: AdminSidebarMenuProps) => React.JSX.Element;
67
+ }: AdminSidebarMenuProps) => React$1.JSX.Element;
67
68
  //#endregion
68
69
  //#region packages/admin/AdminShell.d.ts
69
70
  interface AdminShellProps {
@@ -72,10 +73,10 @@ interface AdminShellProps {
72
73
  headerActions?: AdminHeaderAction[];
73
74
  renderUserMenu?: (props: {
74
75
  close: () => void;
75
- }) => React.ReactNode;
76
- renderThemeSwitcher?: () => React.ReactNode;
77
- footer?: React.ReactNode;
78
- children: React.ReactNode;
76
+ }) => React$1.ReactNode;
77
+ renderThemeSwitcher?: () => React$1.ReactNode;
78
+ footer?: React$1.ReactNode;
79
+ children: React$1.ReactNode;
79
80
  }
80
81
  declare const AdminShell: ({
81
82
  title,
@@ -85,7 +86,7 @@ declare const AdminShell: ({
85
86
  renderThemeSwitcher,
86
87
  footer,
87
88
  children
88
- }: AdminShellProps) => React.JSX.Element;
89
+ }: AdminShellProps) => React$1.JSX.Element;
89
90
  //#endregion
90
91
  //#region packages/admin/useScreenSize.d.ts
91
92
  declare const useScreenSize: () => {
@@ -97,9 +98,22 @@ declare const useScreenSize: () => {
97
98
  declare const useScreenSizeClass: () => "screen-large" | "screen-medium" | "screen-small" | "screen-x-small";
98
99
  //#endregion
99
100
  //#region packages/admin/auth/SessionContext.d.ts
101
+ type AppProfile = 'internal' | 'saas' | 'hybrid';
102
+ type SessionTenant = {
103
+ id?: number;
104
+ name?: string;
105
+ domain?: string | null;
106
+ };
107
+ type SessionFeatureEntitlement = {
108
+ enabled: boolean;
109
+ config: Record<string, unknown>;
110
+ };
100
111
  type SessionProfile = {
101
112
  username: string;
102
- roles: string[];
113
+ roles: string[]; /** Present when the backend runs nubitio/admin-bundle ≥ 0.1 session contract. */
114
+ appProfile?: AppProfile;
115
+ tenant?: SessionTenant;
116
+ features?: Record<string, SessionFeatureEntitlement>;
103
117
  };
104
118
  type SessionState = {
105
119
  status: 'loading';
@@ -130,8 +144,8 @@ declare function SessionProvider({
130
144
  logoutPath,
131
145
  children
132
146
  }: SessionProviderConfig & {
133
- children: React.ReactNode;
134
- }): React.JSX.Element;
147
+ children: React$1.ReactNode;
148
+ }): React$1.JSX.Element;
135
149
  declare function useSession(): SessionContextValue;
136
150
  //#endregion
137
151
  //#region packages/admin/auth/LoginPage.d.ts
@@ -165,6 +179,39 @@ declare function useAppRuntime(): {
165
179
  dismiss: (id: number) => void;
166
180
  };
167
181
  //#endregion
182
+ //#region packages/admin/runtime/useRuntimeConfig.d.ts
183
+ /** Free-form JSON from GET /api/runtime-config — each app defines the shape. */
184
+ type RuntimeConfig = Record<string, unknown>;
185
+ type RuntimeConfigState = {
186
+ status: 'idle';
187
+ } | {
188
+ status: 'loading';
189
+ } | {
190
+ status: 'ready';
191
+ config: RuntimeConfig;
192
+ } | {
193
+ status: 'error';
194
+ };
195
+ interface UseRuntimeConfigOptions {
196
+ /** API base URL, e.g. `/api/`. */
197
+ apiBaseUrl?: string;
198
+ /** Endpoint relative to apiBaseUrl. @default `runtime-config` */
199
+ path?: string;
200
+ /** When false, skips the fetch (e.g. endpoint not enabled). @default true */
201
+ enabled?: boolean;
202
+ }
203
+ declare function useRuntimeConfig({
204
+ apiBaseUrl,
205
+ path,
206
+ enabled
207
+ }?: UseRuntimeConfigOptions): {
208
+ state: RuntimeConfigState;
209
+ config: RuntimeConfig | null;
210
+ loading: boolean;
211
+ error: boolean;
212
+ refresh: () => Promise<void>;
213
+ };
214
+ //#endregion
168
215
  //#region packages/admin/runtime/ToastHost.d.ts
169
216
  interface ToastHostProps {
170
217
  toasts: ToastItem[];
@@ -175,4 +222,69 @@ declare function ToastHost({
175
222
  onDismiss
176
223
  }: ToastHostProps): import("react").JSX.Element | null;
177
224
  //#endregion
178
- export { AdminHeader, type AdminHeaderAction, type AdminHeaderProps, type AdminMenuItem, type AdminMenuSubItem, AdminShell, type AdminShellProps, AdminSidebarMenu, type AdminSidebarMenuProps, type AdminSidebarMenuSelectEvent, LoginPage, type LoginPageProps, type NotificationType, type SessionContextValue, type SessionProfile, SessionProvider, type SessionProviderConfig, type SessionState, ToastHost, type ToastHostProps, type ToastItem, useAppRuntime, useScreenSize, useScreenSizeClass, useSession };
225
+ //#region packages/admin/app/types.d.ts
226
+ type NubitAppRoute = {
227
+ path: string;
228
+ element: ReactNode;
229
+ };
230
+ type NubitAppMenuSubItem = AdminMenuSubItem & {
231
+ /** UX-only — hide unless the session has one of these roles. */roles?: string | string[];
232
+ };
233
+ type NubitAppMenuItem = Omit<AdminMenuItem, 'items'> & {
234
+ /** UX-only — hide unless the session has one of these roles. */roles?: string | string[];
235
+ items?: NubitAppMenuSubItem[];
236
+ };
237
+ type NubitAppMenuContext = {
238
+ roles: string[];
239
+ username: string | null;
240
+ session: SessionState;
241
+ appProfile?: AppProfile;
242
+ logout: () => Promise<void>;
243
+ };
244
+ type NubitAppUserMenuContext = NubitAppMenuContext & {
245
+ close: () => void;
246
+ };
247
+ type CreateNubitAppConfig = {
248
+ title: string;
249
+ apiBaseUrl?: string;
250
+ homePath?: string;
251
+ locale?: string;
252
+ timezone?: string;
253
+ currency?: string;
254
+ menu: NubitAppMenuItem[] | ((ctx: NubitAppMenuContext) => NubitAppMenuItem[]);
255
+ routes: NubitAppRoute[];
256
+ /**
257
+ * Final menu filter — runs after optional {@link filterMenuByRoles} when items
258
+ * declare `roles`. Use for app-specific rules (tenant features, runtime config).
259
+ */
260
+ filterMenu?: (items: AdminMenuItem[], ctx: NubitAppMenuContext) => AdminMenuItem[];
261
+ login?: {
262
+ title?: string;
263
+ hint?: string;
264
+ defaultUsername?: string;
265
+ };
266
+ shell?: Pick<AdminShellProps, 'headerActions' | 'footer'>;
267
+ renderThemeSwitcher?: () => ReactNode;
268
+ renderUserMenu?: (ctx: NubitAppUserMenuContext) => ReactNode; /** Wraps authenticated shell content (inside Hydra providers). */
269
+ Wrapper?: React.ComponentType<{
270
+ children: ReactNode;
271
+ }>;
272
+ queryClient?: QueryClient; /** Register Mercure + Hydra schema/store providers. Default true. */
273
+ hydra?: boolean;
274
+ };
275
+ type NubitApp = {
276
+ App: React.ComponentType;
277
+ };
278
+ //#endregion
279
+ //#region packages/admin/app/createNubitApp.d.ts
280
+ declare function createNubitApp(config: CreateNubitAppConfig): NubitApp;
281
+ //#endregion
282
+ //#region packages/admin/app/filterMenuByRoles.d.ts
283
+ declare function hasAnyRole(required: string | string[] | undefined, roles: string[]): boolean;
284
+ /**
285
+ * Removes menu entries whose `roles` constraint is not satisfied. Sub-items with
286
+ * `roles` are filtered; empty parent groups are dropped.
287
+ */
288
+ declare function filterMenuByRoles(items: NubitAppMenuItem[], roles: string[]): AdminMenuItem[];
289
+ //#endregion
290
+ export { AdminHeader, type AdminHeaderAction, type AdminHeaderProps, type AdminMenuItem, type AdminMenuSubItem, AdminShell, type AdminShellProps, AdminSidebarMenu, type AdminSidebarMenuProps, type AdminSidebarMenuSelectEvent, type AppProfile, type CreateNubitAppConfig, LoginPage, type LoginPageProps, type NotificationType, type NubitApp, type NubitAppMenuContext, type NubitAppMenuItem, type NubitAppMenuSubItem, type NubitAppRoute, type NubitAppUserMenuContext, type RuntimeConfig, type RuntimeConfigState, type SessionContextValue, type SessionFeatureEntitlement, type SessionProfile, SessionProvider, type SessionProviderConfig, type SessionState, type SessionTenant, ToastHost, type ToastHostProps, type ToastItem, type UseRuntimeConfigOptions, createNubitApp, filterMenuByRoles, hasAnyRole, useAppRuntime, useRuntimeConfig, useScreenSize, useScreenSizeClass, useSession };
package/dist/index.d.mts CHANGED
@@ -1,4 +1,5 @@
1
- import React from "react";
1
+ import React$1, { ReactNode } from "react";
2
+ import { QueryClient } from "@tanstack/react-query";
2
3
  import { CoreRuntime } from "@nubitio/core";
3
4
 
4
5
  //#region packages/admin/AdminHeader.d.ts
@@ -11,18 +12,18 @@ interface AdminHeaderAction {
11
12
  onClick?: () => void;
12
13
  renderPanel?: (props: {
13
14
  close: () => void;
14
- }) => React.ReactNode;
15
+ }) => React$1.ReactNode;
15
16
  }
16
17
  interface AdminHeaderProps {
17
18
  title?: string;
18
19
  menuToggleEnabled?: boolean;
19
- toggleMenu?: (e: React.MouseEvent<HTMLButtonElement>) => void;
20
+ toggleMenu?: (e: React$1.MouseEvent<HTMLButtonElement>) => void;
20
21
  className?: string;
21
22
  actions?: AdminHeaderAction[];
22
23
  renderUserMenu?: (props: {
23
24
  close: () => void;
24
- }) => React.ReactNode;
25
- renderThemeSwitcher?: () => React.ReactNode;
25
+ }) => React$1.ReactNode;
26
+ renderThemeSwitcher?: () => React$1.ReactNode;
26
27
  }
27
28
  declare const AdminHeader: ({
28
29
  title,
@@ -32,7 +33,7 @@ declare const AdminHeader: ({
32
33
  actions,
33
34
  renderUserMenu,
34
35
  renderThemeSwitcher
35
- }: AdminHeaderProps) => React.JSX.Element;
36
+ }: AdminHeaderProps) => React$1.JSX.Element;
36
37
  //#endregion
37
38
  //#region packages/admin/AdminSidebarMenu.d.ts
38
39
  interface AdminMenuSubItem {
@@ -48,14 +49,14 @@ interface AdminMenuItem {
48
49
  interface AdminSidebarMenuSelectEvent {
49
50
  path?: string;
50
51
  selected: boolean;
51
- event: React.MouseEvent<HTMLButtonElement | HTMLAnchorElement>;
52
+ event: React$1.MouseEvent<HTMLButtonElement | HTMLAnchorElement>;
52
53
  }
53
54
  interface AdminSidebarMenuProps {
54
55
  items: AdminMenuItem[];
55
56
  compactMode?: boolean;
56
57
  selectedItemChanged: (e: AdminSidebarMenuSelectEvent) => void;
57
- openMenu?: (e: React.PointerEvent) => void;
58
- footer?: React.ReactNode;
58
+ openMenu?: (e: React$1.PointerEvent) => void;
59
+ footer?: React$1.ReactNode;
59
60
  }
60
61
  declare const AdminSidebarMenu: ({
61
62
  items,
@@ -63,7 +64,7 @@ declare const AdminSidebarMenu: ({
63
64
  selectedItemChanged,
64
65
  openMenu,
65
66
  footer
66
- }: AdminSidebarMenuProps) => React.JSX.Element;
67
+ }: AdminSidebarMenuProps) => React$1.JSX.Element;
67
68
  //#endregion
68
69
  //#region packages/admin/AdminShell.d.ts
69
70
  interface AdminShellProps {
@@ -72,10 +73,10 @@ interface AdminShellProps {
72
73
  headerActions?: AdminHeaderAction[];
73
74
  renderUserMenu?: (props: {
74
75
  close: () => void;
75
- }) => React.ReactNode;
76
- renderThemeSwitcher?: () => React.ReactNode;
77
- footer?: React.ReactNode;
78
- children: React.ReactNode;
76
+ }) => React$1.ReactNode;
77
+ renderThemeSwitcher?: () => React$1.ReactNode;
78
+ footer?: React$1.ReactNode;
79
+ children: React$1.ReactNode;
79
80
  }
80
81
  declare const AdminShell: ({
81
82
  title,
@@ -85,7 +86,7 @@ declare const AdminShell: ({
85
86
  renderThemeSwitcher,
86
87
  footer,
87
88
  children
88
- }: AdminShellProps) => React.JSX.Element;
89
+ }: AdminShellProps) => React$1.JSX.Element;
89
90
  //#endregion
90
91
  //#region packages/admin/useScreenSize.d.ts
91
92
  declare const useScreenSize: () => {
@@ -97,9 +98,22 @@ declare const useScreenSize: () => {
97
98
  declare const useScreenSizeClass: () => "screen-large" | "screen-medium" | "screen-small" | "screen-x-small";
98
99
  //#endregion
99
100
  //#region packages/admin/auth/SessionContext.d.ts
101
+ type AppProfile = 'internal' | 'saas' | 'hybrid';
102
+ type SessionTenant = {
103
+ id?: number;
104
+ name?: string;
105
+ domain?: string | null;
106
+ };
107
+ type SessionFeatureEntitlement = {
108
+ enabled: boolean;
109
+ config: Record<string, unknown>;
110
+ };
100
111
  type SessionProfile = {
101
112
  username: string;
102
- roles: string[];
113
+ roles: string[]; /** Present when the backend runs nubitio/admin-bundle ≥ 0.1 session contract. */
114
+ appProfile?: AppProfile;
115
+ tenant?: SessionTenant;
116
+ features?: Record<string, SessionFeatureEntitlement>;
103
117
  };
104
118
  type SessionState = {
105
119
  status: 'loading';
@@ -130,8 +144,8 @@ declare function SessionProvider({
130
144
  logoutPath,
131
145
  children
132
146
  }: SessionProviderConfig & {
133
- children: React.ReactNode;
134
- }): React.JSX.Element;
147
+ children: React$1.ReactNode;
148
+ }): React$1.JSX.Element;
135
149
  declare function useSession(): SessionContextValue;
136
150
  //#endregion
137
151
  //#region packages/admin/auth/LoginPage.d.ts
@@ -165,6 +179,39 @@ declare function useAppRuntime(): {
165
179
  dismiss: (id: number) => void;
166
180
  };
167
181
  //#endregion
182
+ //#region packages/admin/runtime/useRuntimeConfig.d.ts
183
+ /** Free-form JSON from GET /api/runtime-config — each app defines the shape. */
184
+ type RuntimeConfig = Record<string, unknown>;
185
+ type RuntimeConfigState = {
186
+ status: 'idle';
187
+ } | {
188
+ status: 'loading';
189
+ } | {
190
+ status: 'ready';
191
+ config: RuntimeConfig;
192
+ } | {
193
+ status: 'error';
194
+ };
195
+ interface UseRuntimeConfigOptions {
196
+ /** API base URL, e.g. `/api/`. */
197
+ apiBaseUrl?: string;
198
+ /** Endpoint relative to apiBaseUrl. @default `runtime-config` */
199
+ path?: string;
200
+ /** When false, skips the fetch (e.g. endpoint not enabled). @default true */
201
+ enabled?: boolean;
202
+ }
203
+ declare function useRuntimeConfig({
204
+ apiBaseUrl,
205
+ path,
206
+ enabled
207
+ }?: UseRuntimeConfigOptions): {
208
+ state: RuntimeConfigState;
209
+ config: RuntimeConfig | null;
210
+ loading: boolean;
211
+ error: boolean;
212
+ refresh: () => Promise<void>;
213
+ };
214
+ //#endregion
168
215
  //#region packages/admin/runtime/ToastHost.d.ts
169
216
  interface ToastHostProps {
170
217
  toasts: ToastItem[];
@@ -175,4 +222,69 @@ declare function ToastHost({
175
222
  onDismiss
176
223
  }: ToastHostProps): import("react").JSX.Element | null;
177
224
  //#endregion
178
- export { AdminHeader, type AdminHeaderAction, type AdminHeaderProps, type AdminMenuItem, type AdminMenuSubItem, AdminShell, type AdminShellProps, AdminSidebarMenu, type AdminSidebarMenuProps, type AdminSidebarMenuSelectEvent, LoginPage, type LoginPageProps, type NotificationType, type SessionContextValue, type SessionProfile, SessionProvider, type SessionProviderConfig, type SessionState, ToastHost, type ToastHostProps, type ToastItem, useAppRuntime, useScreenSize, useScreenSizeClass, useSession };
225
+ //#region packages/admin/app/types.d.ts
226
+ type NubitAppRoute = {
227
+ path: string;
228
+ element: ReactNode;
229
+ };
230
+ type NubitAppMenuSubItem = AdminMenuSubItem & {
231
+ /** UX-only — hide unless the session has one of these roles. */roles?: string | string[];
232
+ };
233
+ type NubitAppMenuItem = Omit<AdminMenuItem, 'items'> & {
234
+ /** UX-only — hide unless the session has one of these roles. */roles?: string | string[];
235
+ items?: NubitAppMenuSubItem[];
236
+ };
237
+ type NubitAppMenuContext = {
238
+ roles: string[];
239
+ username: string | null;
240
+ session: SessionState;
241
+ appProfile?: AppProfile;
242
+ logout: () => Promise<void>;
243
+ };
244
+ type NubitAppUserMenuContext = NubitAppMenuContext & {
245
+ close: () => void;
246
+ };
247
+ type CreateNubitAppConfig = {
248
+ title: string;
249
+ apiBaseUrl?: string;
250
+ homePath?: string;
251
+ locale?: string;
252
+ timezone?: string;
253
+ currency?: string;
254
+ menu: NubitAppMenuItem[] | ((ctx: NubitAppMenuContext) => NubitAppMenuItem[]);
255
+ routes: NubitAppRoute[];
256
+ /**
257
+ * Final menu filter — runs after optional {@link filterMenuByRoles} when items
258
+ * declare `roles`. Use for app-specific rules (tenant features, runtime config).
259
+ */
260
+ filterMenu?: (items: AdminMenuItem[], ctx: NubitAppMenuContext) => AdminMenuItem[];
261
+ login?: {
262
+ title?: string;
263
+ hint?: string;
264
+ defaultUsername?: string;
265
+ };
266
+ shell?: Pick<AdminShellProps, 'headerActions' | 'footer'>;
267
+ renderThemeSwitcher?: () => ReactNode;
268
+ renderUserMenu?: (ctx: NubitAppUserMenuContext) => ReactNode; /** Wraps authenticated shell content (inside Hydra providers). */
269
+ Wrapper?: React.ComponentType<{
270
+ children: ReactNode;
271
+ }>;
272
+ queryClient?: QueryClient; /** Register Mercure + Hydra schema/store providers. Default true. */
273
+ hydra?: boolean;
274
+ };
275
+ type NubitApp = {
276
+ App: React.ComponentType;
277
+ };
278
+ //#endregion
279
+ //#region packages/admin/app/createNubitApp.d.ts
280
+ declare function createNubitApp(config: CreateNubitAppConfig): NubitApp;
281
+ //#endregion
282
+ //#region packages/admin/app/filterMenuByRoles.d.ts
283
+ declare function hasAnyRole(required: string | string[] | undefined, roles: string[]): boolean;
284
+ /**
285
+ * Removes menu entries whose `roles` constraint is not satisfied. Sub-items with
286
+ * `roles` are filtered; empty parent groups are dropped.
287
+ */
288
+ declare function filterMenuByRoles(items: NubitAppMenuItem[], roles: string[]): AdminMenuItem[];
289
+ //#endregion
290
+ export { AdminHeader, type AdminHeaderAction, type AdminHeaderProps, type AdminMenuItem, type AdminMenuSubItem, AdminShell, type AdminShellProps, AdminSidebarMenu, type AdminSidebarMenuProps, type AdminSidebarMenuSelectEvent, type AppProfile, type CreateNubitAppConfig, LoginPage, type LoginPageProps, type NotificationType, type NubitApp, type NubitAppMenuContext, type NubitAppMenuItem, type NubitAppMenuSubItem, type NubitAppRoute, type NubitAppUserMenuContext, type RuntimeConfig, type RuntimeConfigState, type SessionContextValue, type SessionFeatureEntitlement, type SessionProfile, SessionProvider, type SessionProviderConfig, type SessionState, type SessionTenant, ToastHost, type ToastHostProps, type ToastItem, type UseRuntimeConfigOptions, createNubitApp, filterMenuByRoles, hasAnyRole, useAppRuntime, useRuntimeConfig, useScreenSize, useScreenSizeClass, useSession };
package/dist/index.mjs CHANGED
@@ -1,7 +1,11 @@
1
- import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
2
- import { useLocation, useNavigate } from "react-router-dom";
3
- import { Badge, Button, Card, IconButton, TextField, useFloatingPanel } from "@nubitio/ui";
1
+ import React, { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react";
2
+ import { BrowserRouter, Navigate, Route, Routes, useLocation, useNavigate } from "react-router-dom";
3
+ import { Badge, Button, Card, IconButton, TextField, ThemeProvider, ThemeSwitcher, useFloatingPanel } from "@nubitio/ui";
4
4
  import { jsx, jsxs } from "react/jsx-runtime";
5
+ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
6
+ import { CoreConfigProvider, CoreProvider, MercureProvider } from "@nubitio/core";
7
+ import { SmartCrudRolesProvider } from "@nubitio/crud";
8
+ import { HydraResourceSchemaProvider, HydraResourceStoreProvider, SchemaProvider } from "@nubitio/hydra";
5
9
  //#region packages/admin/AdminHeader.tsx
6
10
  function ActionPopover({ action }) {
7
11
  const { open, toggle, setOpen, containerRef } = useFloatingPanel();
@@ -361,14 +365,14 @@ const AdminShell = ({ title, menuItems, headerActions, renderUserMenu, renderThe
361
365
  //#endregion
362
366
  //#region packages/admin/auth/SessionContext.tsx
363
367
  const SessionContext = createContext(null);
364
- function joinApiPath$1(apiBaseUrl, path) {
368
+ function joinApiPath$2(apiBaseUrl, path) {
365
369
  return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
366
370
  }
367
371
  function SessionProvider({ apiBaseUrl = "/api/", mePath = "me", logoutPath = "auth/logout", children }) {
368
372
  const [session, setSession] = useState({ status: "loading" });
369
373
  const refresh = useCallback(async () => {
370
374
  try {
371
- const response = await fetch(joinApiPath$1(apiBaseUrl, mePath), { credentials: "include" });
375
+ const response = await fetch(joinApiPath$2(apiBaseUrl, mePath), { credentials: "include" });
372
376
  if (!response.ok) {
373
377
  setSession({ status: "anonymous" });
374
378
  return;
@@ -385,7 +389,7 @@ function SessionProvider({ apiBaseUrl = "/api/", mePath = "me", logoutPath = "au
385
389
  refresh();
386
390
  }, [refresh]);
387
391
  const logout = useCallback(async () => {
388
- await fetch(joinApiPath$1(apiBaseUrl, logoutPath), {
392
+ await fetch(joinApiPath$2(apiBaseUrl, logoutPath), {
389
393
  method: "POST",
390
394
  credentials: "include"
391
395
  });
@@ -414,7 +418,7 @@ function useSession() {
414
418
  }
415
419
  //#endregion
416
420
  //#region packages/admin/auth/LoginPage.tsx
417
- function joinApiPath(apiBaseUrl, path) {
421
+ function joinApiPath$1(apiBaseUrl, path) {
418
422
  return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
419
423
  }
420
424
  function LoginPage({ onLoggedIn, apiBaseUrl = "/api/", loginPath = "auth/login", title = "Nubit Admin", hint, defaultUsername = "" }) {
@@ -427,7 +431,7 @@ function LoginPage({ onLoggedIn, apiBaseUrl = "/api/", loginPath = "auth/login",
427
431
  setBusy(true);
428
432
  setError(null);
429
433
  try {
430
- const response = await fetch(joinApiPath(apiBaseUrl, loginPath), {
434
+ const response = await fetch(joinApiPath$1(apiBaseUrl, loginPath), {
431
435
  method: "POST",
432
436
  headers: { "Content-Type": "application/json" },
433
437
  credentials: "include",
@@ -534,6 +538,48 @@ function useAppRuntime() {
534
538
  };
535
539
  }
536
540
  //#endregion
541
+ //#region packages/admin/runtime/useRuntimeConfig.ts
542
+ function joinApiPath(apiBaseUrl, path) {
543
+ return `${apiBaseUrl.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`;
544
+ }
545
+ function useRuntimeConfig({ apiBaseUrl = "/api/", path = "runtime-config", enabled = true } = {}) {
546
+ const [state, setState] = useState({ status: "idle" });
547
+ const refresh = useCallback(async () => {
548
+ if (!enabled) {
549
+ setState({ status: "idle" });
550
+ return;
551
+ }
552
+ setState({ status: "loading" });
553
+ try {
554
+ const response = await fetch(joinApiPath(apiBaseUrl, path), { credentials: "include" });
555
+ if (!response.ok) {
556
+ setState({ status: "error" });
557
+ return;
558
+ }
559
+ setState({
560
+ status: "ready",
561
+ config: await response.json()
562
+ });
563
+ } catch {
564
+ setState({ status: "error" });
565
+ }
566
+ }, [
567
+ apiBaseUrl,
568
+ enabled,
569
+ path
570
+ ]);
571
+ useEffect(() => {
572
+ refresh();
573
+ }, [refresh]);
574
+ return {
575
+ state,
576
+ config: state.status === "ready" ? state.config : null,
577
+ loading: state.status === "loading",
578
+ error: state.status === "error",
579
+ refresh
580
+ };
581
+ }
582
+ //#endregion
537
583
  //#region packages/admin/runtime/ToastHost.tsx
538
584
  const TYPE_CLASS = {
539
585
  success: "nb-toast--success",
@@ -559,4 +605,172 @@ function ToastHost({ toasts, onDismiss }) {
559
605
  });
560
606
  }
561
607
  //#endregion
562
- export { AdminHeader, AdminShell, AdminSidebarMenu, LoginPage, SessionProvider, ToastHost, useAppRuntime, useScreenSize, useScreenSizeClass, useSession };
608
+ //#region packages/admin/app/filterMenuByRoles.ts
609
+ function hasAnyRole(required, roles) {
610
+ if (required === void 0) return true;
611
+ return (Array.isArray(required) ? required : [required]).some((role) => roles.includes(role));
612
+ }
613
+ /**
614
+ * Removes menu entries whose `roles` constraint is not satisfied. Sub-items with
615
+ * `roles` are filtered; empty parent groups are dropped.
616
+ */
617
+ function filterMenuByRoles(items, roles) {
618
+ const filtered = [];
619
+ for (const item of items) {
620
+ if (!hasAnyRole(item.roles, roles)) continue;
621
+ if (!item.items) {
622
+ filtered.push({
623
+ text: item.text,
624
+ path: item.path,
625
+ icon: item.icon
626
+ });
627
+ continue;
628
+ }
629
+ const subItems = item.items.filter((subItem) => hasAnyRole(subItem.roles, roles)).map(({ text, path }) => ({
630
+ text,
631
+ path
632
+ }));
633
+ if (subItems.length === 0) continue;
634
+ filtered.push({
635
+ text: item.text,
636
+ icon: item.icon,
637
+ path: item.path,
638
+ items: subItems
639
+ });
640
+ }
641
+ return filtered;
642
+ }
643
+ function resolveAppMenu(menu, ctx) {
644
+ return typeof menu === "function" ? menu(ctx) : menu;
645
+ }
646
+ //#endregion
647
+ //#region packages/admin/app/createNubitApp.tsx
648
+ function defaultUserMenu({ username, close, logout }) {
649
+ return /* @__PURE__ */ jsxs("div", {
650
+ style: {
651
+ display: "flex",
652
+ flexDirection: "column",
653
+ gap: 8,
654
+ minWidth: 180
655
+ },
656
+ children: [/* @__PURE__ */ jsx("span", {
657
+ style: {
658
+ color: "var(--text-secondary)",
659
+ fontSize: "0.875rem"
660
+ },
661
+ children: username ?? "User"
662
+ }), /* @__PURE__ */ jsx("button", {
663
+ type: "button",
664
+ onClick: () => {
665
+ close();
666
+ logout();
667
+ },
668
+ children: "Sign out"
669
+ })]
670
+ });
671
+ }
672
+ function buildMenuContext(session) {
673
+ const profile = session.session.status === "authenticated" ? session.session.profile : void 0;
674
+ return {
675
+ roles: session.roles,
676
+ username: session.username,
677
+ session: session.session,
678
+ appProfile: profile?.appProfile,
679
+ logout: session.logout
680
+ };
681
+ }
682
+ function resolveShellMenu(config, ctx) {
683
+ const declared = resolveAppMenu(config.menu, ctx);
684
+ const roleScoped = declared.some((item) => item.roles !== void 0 || item.items?.some((sub) => sub.roles !== void 0)) ? filterMenuByRoles(declared, ctx.roles) : declared.map(({ text, path, icon, items }) => ({
685
+ text,
686
+ path,
687
+ icon,
688
+ items
689
+ }));
690
+ return config.filterMenu ? config.filterMenu(roleScoped, ctx) : roleScoped;
691
+ }
692
+ function NubitAuthenticatedApp({ config }) {
693
+ const session = useSession();
694
+ const { runtime, toasts, dismiss } = useAppRuntime();
695
+ const apiBaseUrl = config.apiBaseUrl ?? "/api/";
696
+ const homePath = config.homePath ?? config.routes[0]?.path ?? "/";
697
+ const menuContext = useMemo(() => buildMenuContext(session), [session]);
698
+ const menuItems = useMemo(() => session.session.status === "authenticated" ? resolveShellMenu(config, menuContext) : [], [
699
+ config,
700
+ menuContext,
701
+ session.session.status
702
+ ]);
703
+ const renderThemeSwitcher = config.renderThemeSwitcher ?? (() => /* @__PURE__ */ jsx(ThemeSwitcher, {}));
704
+ const renderUserMenu = config.renderUserMenu ?? defaultUserMenu;
705
+ const Wrapper = config.Wrapper ?? React.Fragment;
706
+ if (session.session.status === "loading") return null;
707
+ const shell = /* @__PURE__ */ jsx(AdminShell, {
708
+ title: config.title,
709
+ menuItems,
710
+ headerActions: config.shell?.headerActions,
711
+ footer: config.shell?.footer,
712
+ renderThemeSwitcher,
713
+ renderUserMenu: ({ close }) => renderUserMenu({
714
+ ...menuContext,
715
+ close
716
+ }),
717
+ children: /* @__PURE__ */ jsxs(Routes, { children: [/* @__PURE__ */ jsx(Route, {
718
+ path: "/",
719
+ element: /* @__PURE__ */ jsx(Navigate, {
720
+ to: homePath,
721
+ replace: true
722
+ })
723
+ }), config.routes.map((route) => /* @__PURE__ */ jsx(Route, {
724
+ path: route.path,
725
+ element: route.element
726
+ }, route.path))] })
727
+ });
728
+ const authenticated = /* @__PURE__ */ jsx(CoreProvider, {
729
+ http: {
730
+ baseUrl: apiBaseUrl,
731
+ refreshPath: "auth/refresh",
732
+ loginPath: "auth/login"
733
+ },
734
+ runtime,
735
+ children: /* @__PURE__ */ jsx(CoreConfigProvider, {
736
+ apiBaseUrl,
737
+ locale: config.locale ?? "en",
738
+ timezone: config.timezone ?? "UTC",
739
+ currency: config.currency ?? "USD",
740
+ children: /* @__PURE__ */ jsxs(SmartCrudRolesProvider, {
741
+ roles: session.roles,
742
+ children: [/* @__PURE__ */ jsx(BrowserRouter, { children: /* @__PURE__ */ jsx(Wrapper, { children: config.hydra === false ? shell : /* @__PURE__ */ jsx(MercureProvider, { children: /* @__PURE__ */ jsx(SchemaProvider, { children: /* @__PURE__ */ jsx(HydraResourceSchemaProvider, { children: /* @__PURE__ */ jsx(HydraResourceStoreProvider, { children: shell }) }) }) }) }) }), /* @__PURE__ */ jsx(ToastHost, {
743
+ toasts,
744
+ onDismiss: dismiss
745
+ })]
746
+ })
747
+ })
748
+ });
749
+ if (session.session.status === "authenticated") return authenticated;
750
+ return /* @__PURE__ */ jsx(BrowserRouter, { children: /* @__PURE__ */ jsx(LoginPage, {
751
+ apiBaseUrl,
752
+ title: config.login?.title ?? config.title,
753
+ hint: config.login?.hint,
754
+ defaultUsername: config.login?.defaultUsername,
755
+ onLoggedIn: () => void session.refresh()
756
+ }) });
757
+ }
758
+ function createNubitApp(config) {
759
+ const queryClient = config.queryClient ?? new QueryClient({ defaultOptions: { queries: {
760
+ retry: 1,
761
+ staleTime: 3e4
762
+ } } });
763
+ const apiBaseUrl = config.apiBaseUrl ?? "/api/";
764
+ function App() {
765
+ return /* @__PURE__ */ jsx(QueryClientProvider, {
766
+ client: queryClient,
767
+ children: /* @__PURE__ */ jsx(ThemeProvider, { children: /* @__PURE__ */ jsx(SessionProvider, {
768
+ apiBaseUrl,
769
+ children: /* @__PURE__ */ jsx(NubitAuthenticatedApp, { config })
770
+ }) })
771
+ });
772
+ }
773
+ return { App };
774
+ }
775
+ //#endregion
776
+ export { AdminHeader, AdminShell, AdminSidebarMenu, LoginPage, SessionProvider, ToastHost, createNubitApp, filterMenuByRoles, hasAnyRole, useAppRuntime, useRuntimeConfig, useScreenSize, useScreenSizeClass, useSession };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nubitio/admin",
3
- "version": "0.5.15",
3
+ "version": "0.5.19",
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",
@@ -49,10 +49,13 @@
49
49
  "access": "public"
50
50
  },
51
51
  "peerDependencies": {
52
+ "@tanstack/react-query": "^5.0.0",
52
53
  "react": "^19.0.0",
53
54
  "react-dom": "^19.0.0",
54
55
  "react-router-dom": "^6.0.0",
55
- "@nubitio/ui": "^0.5.15",
56
- "@nubitio/core": "^0.5.15"
56
+ "@nubitio/core": "^0.5.19",
57
+ "@nubitio/crud": "^0.5.19",
58
+ "@nubitio/hydra": "^0.5.19",
59
+ "@nubitio/ui": "^0.5.19"
57
60
  }
58
61
  }