@cortejojicoy/admin-kit 0.1.8

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.
@@ -0,0 +1,950 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+ var navigation = require('next/navigation');
5
+ var NextLink = require('next/link');
6
+ var router = require('next/router');
7
+
8
+ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
9
+
10
+ var NextLink__default = /*#__PURE__*/_interopDefault(NextLink);
11
+
12
+ // src/config/defaults.ts
13
+ var DEFAULT_LOGIN_PATH = "/login";
14
+ var DEFAULT_AFTER_LOGIN = "/";
15
+ var DEFAULT_AFTER_LOGOUT = "/login";
16
+ var DEFAULT_PUBLIC_ROUTES = ["/login", "/api/auth"];
17
+ function resolveConfig(config) {
18
+ return {
19
+ ...config,
20
+ router: config.router ?? "app",
21
+ auth: {
22
+ ...config.auth,
23
+ loginPage: {
24
+ path: DEFAULT_LOGIN_PATH,
25
+ title: `Sign in to ${config.app.name}`,
26
+ ...config.auth.loginPage
27
+ },
28
+ afterLoginRedirect: config.auth.afterLoginRedirect ?? DEFAULT_AFTER_LOGIN,
29
+ afterLogoutRedirect: config.auth.afterLogoutRedirect ?? DEFAULT_AFTER_LOGOUT,
30
+ publicRoutes: config.auth.publicRoutes ?? DEFAULT_PUBLIC_ROUTES
31
+ },
32
+ layout: {
33
+ sidebarPosition: "left",
34
+ sidebarCollapsible: true,
35
+ sidebarDefaultCollapsed: false,
36
+ ...config.layout
37
+ },
38
+ theme: {
39
+ mode: "system",
40
+ ...config.theme
41
+ }
42
+ };
43
+ }
44
+
45
+ // src/auth/providers/JWTProvider.tsx
46
+ var DEFAULT_COOKIE = "admin_kit_token";
47
+ var DEFAULT_HEADER = { name: "Authorization", prefix: "Bearer " };
48
+ function readToken(cfg) {
49
+ if (typeof window === "undefined") return null;
50
+ const storage = cfg.tokenStorage ?? "cookie";
51
+ const cookieName = cfg.cookieName ?? DEFAULT_COOKIE;
52
+ if (storage === "localStorage") return window.localStorage.getItem(cookieName);
53
+ if (storage === "memory") return memoryToken;
54
+ const match = document.cookie.match(new RegExp("(?:^|; )" + cookieName + "=([^;]*)"));
55
+ return match ? decodeURIComponent(match[1]) : null;
56
+ }
57
+ var memoryToken = null;
58
+ function writeToken(cfg, token) {
59
+ if (typeof window === "undefined") return;
60
+ const storage = cfg.tokenStorage ?? "cookie";
61
+ const cookieName = cfg.cookieName ?? DEFAULT_COOKIE;
62
+ if (storage === "localStorage") {
63
+ if (token) window.localStorage.setItem(cookieName, token);
64
+ else window.localStorage.removeItem(cookieName);
65
+ return;
66
+ }
67
+ if (storage === "memory") {
68
+ memoryToken = token;
69
+ return;
70
+ }
71
+ if (token) {
72
+ document.cookie = `${cookieName}=${encodeURIComponent(token)}; path=/; SameSite=Lax`;
73
+ } else {
74
+ document.cookie = `${cookieName}=; path=/; Max-Age=0; SameSite=Lax`;
75
+ }
76
+ }
77
+ function authHeader(cfg, token) {
78
+ if (!token) return {};
79
+ const h = cfg.header ?? DEFAULT_HEADER;
80
+ return { [h.name]: `${h.prefix ?? ""}${token}` };
81
+ }
82
+ function createJWTProvider(cfg) {
83
+ const mapUser = cfg.mapUser ?? ((raw) => raw);
84
+ const mapTokens = cfg.mapTokens ?? ((raw) => {
85
+ const r = raw;
86
+ return {
87
+ accessToken: r.accessToken ?? r.token,
88
+ refreshToken: r.refreshToken,
89
+ expiresAt: r.expiresAt
90
+ };
91
+ });
92
+ async function getSession() {
93
+ const token = readToken(cfg);
94
+ if (!token) return null;
95
+ const res = await fetch(cfg.endpoints.me, {
96
+ credentials: "include",
97
+ headers: { ...authHeader(cfg, token) }
98
+ });
99
+ if (!res.ok) return null;
100
+ const raw = await res.json();
101
+ return { user: mapUser(raw), accessToken: token };
102
+ }
103
+ return {
104
+ name: "jwt",
105
+ async login(credentials) {
106
+ const res = await fetch(cfg.endpoints.login, {
107
+ method: "POST",
108
+ credentials: "include",
109
+ headers: { "Content-Type": "application/json" },
110
+ body: JSON.stringify(credentials)
111
+ });
112
+ if (!res.ok) {
113
+ const text = await res.text().catch(() => "");
114
+ throw new Error(text || `Login failed (${res.status})`);
115
+ }
116
+ const raw = await res.json();
117
+ const tokens = mapTokens(raw);
118
+ if (tokens.accessToken) writeToken(cfg, tokens.accessToken);
119
+ const session = await getSession();
120
+ if (!session) throw new Error("Could not establish session after login");
121
+ return { ...session, ...tokens };
122
+ },
123
+ async logout() {
124
+ const token = readToken(cfg);
125
+ if (cfg.endpoints.logout) {
126
+ await fetch(cfg.endpoints.logout, {
127
+ method: "POST",
128
+ credentials: "include",
129
+ headers: { ...authHeader(cfg, token) }
130
+ }).catch(() => void 0);
131
+ }
132
+ writeToken(cfg, null);
133
+ },
134
+ getSession,
135
+ refresh: cfg.endpoints.refresh ? async () => {
136
+ const token = readToken(cfg);
137
+ const res = await fetch(cfg.endpoints.refresh, {
138
+ method: "POST",
139
+ credentials: "include",
140
+ headers: { ...authHeader(cfg, token) }
141
+ });
142
+ if (!res.ok) return null;
143
+ const raw = await res.json();
144
+ const tokens = mapTokens(raw);
145
+ if (tokens.accessToken) writeToken(cfg, tokens.accessToken);
146
+ return getSession();
147
+ } : void 0
148
+ };
149
+ }
150
+
151
+ // src/auth/providers/OAuthProvider.tsx
152
+ function createOAuthProvider(cfg) {
153
+ const callbackPath = cfg.callbackPath ?? "/api/auth/callback";
154
+ function startAuthorize(providerId) {
155
+ if (typeof window === "undefined") return;
156
+ const p = cfg.providers.find((x) => x.id === providerId);
157
+ if (!p) throw new Error(`Unknown OAuth provider: ${providerId}`);
158
+ const url = new URL(p.authorizationUrl);
159
+ url.searchParams.set("client_id", p.clientId);
160
+ url.searchParams.set("response_type", "code");
161
+ url.searchParams.set(
162
+ "redirect_uri",
163
+ p.redirectUri ?? `${window.location.origin}${callbackPath}/${p.id}`
164
+ );
165
+ if (p.scopes?.length) url.searchParams.set("scope", p.scopes.join(" "));
166
+ url.searchParams.set("state", crypto.randomUUID());
167
+ window.location.assign(url.toString());
168
+ }
169
+ async function getSession() {
170
+ const res = await fetch("/api/auth/session", { credentials: "include" });
171
+ if (!res.ok) return null;
172
+ const raw = await res.json();
173
+ if (!raw?.user) return null;
174
+ return raw;
175
+ }
176
+ return {
177
+ name: "oauth",
178
+ async login(credentials) {
179
+ const providerId = credentials.provider ?? cfg.providers[0]?.id;
180
+ if (!providerId) throw new Error("No OAuth provider configured");
181
+ startAuthorize(providerId);
182
+ return new Promise(() => void 0);
183
+ },
184
+ async logout() {
185
+ await fetch("/api/auth/logout", { method: "POST", credentials: "include" }).catch(() => void 0);
186
+ },
187
+ getSession
188
+ };
189
+ }
190
+
191
+ // src/auth/providers/CustomProvider.tsx
192
+ function createCustomProvider(provider) {
193
+ if (!provider) throw new Error('config.auth.custom is required when provider is "custom"');
194
+ return provider;
195
+ }
196
+
197
+ // src/auth/AuthContext.tsx
198
+ var AuthContext = react.createContext(null);
199
+ function resolveProvider(auth) {
200
+ switch (auth.provider) {
201
+ case "jwt":
202
+ if (!auth.jwt) throw new Error('config.auth.jwt is required when provider is "jwt"');
203
+ return createJWTProvider(auth.jwt);
204
+ case "oauth":
205
+ if (!auth.oauth) throw new Error('config.auth.oauth is required when provider is "oauth"');
206
+ return createOAuthProvider(auth.oauth);
207
+ case "custom":
208
+ if (!auth.custom) throw new Error('config.auth.custom is required when provider is "custom"');
209
+ return createCustomProvider(auth.custom);
210
+ default:
211
+ throw new Error(`Unknown auth provider: ${auth.provider}`);
212
+ }
213
+ }
214
+ function AuthContextProvider({ config, children, initialSession = null }) {
215
+ const providerRef = react.useRef(null);
216
+ if (providerRef.current === null) providerRef.current = resolveProvider(config);
217
+ const provider = providerRef.current;
218
+ const [state, setState] = react.useState(() => ({
219
+ status: initialSession ? "authenticated" : "loading",
220
+ user: initialSession?.user ?? null,
221
+ session: initialSession,
222
+ error: null
223
+ }));
224
+ react.useEffect(() => {
225
+ if (initialSession) return;
226
+ let cancelled = false;
227
+ (async () => {
228
+ try {
229
+ const session = await provider.initialize?.() ?? await provider.getSession();
230
+ if (cancelled) return;
231
+ setState({
232
+ status: session ? "authenticated" : "unauthenticated",
233
+ user: session?.user ?? null,
234
+ session,
235
+ error: null
236
+ });
237
+ } catch (err) {
238
+ if (cancelled) return;
239
+ setState({
240
+ status: "unauthenticated",
241
+ user: null,
242
+ session: null,
243
+ error: err instanceof Error ? err.message : "Failed to load session"
244
+ });
245
+ }
246
+ })();
247
+ return () => {
248
+ cancelled = true;
249
+ };
250
+ }, [provider, initialSession]);
251
+ const login = react.useCallback(
252
+ async (credentials) => {
253
+ setState((s) => ({ ...s, status: "loading", error: null }));
254
+ try {
255
+ const session = await provider.login(credentials);
256
+ setState({ status: "authenticated", user: session.user, session, error: null });
257
+ return session;
258
+ } catch (err) {
259
+ const message = err instanceof Error ? err.message : "Login failed";
260
+ setState({ status: "unauthenticated", user: null, session: null, error: message });
261
+ throw err;
262
+ }
263
+ },
264
+ [provider]
265
+ );
266
+ const logout = react.useCallback(async () => {
267
+ await provider.logout();
268
+ setState({ status: "unauthenticated", user: null, session: null, error: null });
269
+ }, [provider]);
270
+ const getSession = react.useCallback(() => provider.getSession(), [provider]);
271
+ const refresh = react.useMemo(
272
+ () => provider.refresh ? async () => {
273
+ const session = await provider.refresh();
274
+ setState({
275
+ status: session ? "authenticated" : "unauthenticated",
276
+ user: session?.user ?? null,
277
+ session,
278
+ error: null
279
+ });
280
+ return session;
281
+ } : void 0,
282
+ [provider]
283
+ );
284
+ const value = react.useMemo(
285
+ () => ({
286
+ ...state,
287
+ isAuthenticated: state.status === "authenticated",
288
+ isLoading: state.status === "loading",
289
+ login,
290
+ logout,
291
+ getSession,
292
+ refresh
293
+ }),
294
+ [state, login, logout, getSession, refresh]
295
+ );
296
+ return /* @__PURE__ */ React.createElement(AuthContext.Provider, { value }, children);
297
+ }
298
+
299
+ // src/modules/registry.ts
300
+ function createModuleRegistry(modules, user) {
301
+ const active = modules.filter((m) => m.enabled ? m.enabled({ user }) : true);
302
+ const byIdMap = new Map(active.map((m) => [m.id, m]));
303
+ return {
304
+ all: active,
305
+ byId: (id) => byIdMap.get(id),
306
+ widgets: (slot) => active.filter((m) => m.widgets && slot in m.widgets).map((m) => ({ moduleId: m.id, Component: m.widgets[slot] }))
307
+ };
308
+ }
309
+
310
+ // src/modules/ModuleContext.tsx
311
+ var ModuleContext = react.createContext(null);
312
+ function ModuleProvider({ modules, user, children }) {
313
+ const registry = react.useMemo(() => createModuleRegistry(modules, user), [modules, user]);
314
+ let tree = /* @__PURE__ */ React.createElement(ModuleContext.Provider, { value: registry }, children);
315
+ for (let i = registry.all.length - 1; i >= 0; i--) {
316
+ const M = registry.all[i];
317
+ if (M.Provider) {
318
+ const Provider = M.Provider;
319
+ tree = /* @__PURE__ */ React.createElement(Provider, null, tree);
320
+ }
321
+ }
322
+ return tree;
323
+ }
324
+
325
+ // src/theme/tokens.ts
326
+ var DEFAULT_TOKENS = {
327
+ "--admin-color-bg": "#fafafa",
328
+ "--admin-color-surface": "#ffffff",
329
+ "--admin-color-border": "#e5e5e5",
330
+ "--admin-color-text": "#171717",
331
+ "--admin-color-text-muted": "#737373",
332
+ "--admin-color-primary": "#171717",
333
+ "--admin-color-primary-fg": "#ffffff",
334
+ "--admin-color-accent": "#3b82f6",
335
+ "--admin-radius": "0.5rem",
336
+ "--admin-sidebar-width": "16rem",
337
+ "--admin-sidebar-collapsed-width": "4rem",
338
+ "--admin-topbar-height": "3.5rem"
339
+ };
340
+ function tokensToStyle(tokens) {
341
+ const out = {};
342
+ for (const [k, v] of Object.entries(tokens)) {
343
+ if (v != null) out[k] = v;
344
+ }
345
+ return out;
346
+ }
347
+
348
+ // src/theme/ThemeProvider.tsx
349
+ var ThemeContext = react.createContext(null);
350
+ function resolveSystem() {
351
+ if (typeof window === "undefined") return "light";
352
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
353
+ }
354
+ function ThemeProvider({ theme, children }) {
355
+ const [mode, setMode] = react.useState(theme?.mode ?? "system");
356
+ const [systemMode, setSystemMode] = react.useState(resolveSystem);
357
+ react.useEffect(() => {
358
+ if (typeof window === "undefined") return;
359
+ const mql = window.matchMedia("(prefers-color-scheme: dark)");
360
+ const listener = () => setSystemMode(mql.matches ? "dark" : "light");
361
+ mql.addEventListener("change", listener);
362
+ return () => mql.removeEventListener("change", listener);
363
+ }, []);
364
+ const resolvedMode = mode === "system" ? systemMode : mode;
365
+ const style = react.useMemo(
366
+ () => tokensToStyle({ ...DEFAULT_TOKENS, ...theme?.tokens ?? {} }),
367
+ [theme?.tokens]
368
+ );
369
+ const value = react.useMemo(
370
+ () => ({ mode, resolvedMode, setMode }),
371
+ [mode, resolvedMode]
372
+ );
373
+ return /* @__PURE__ */ React.createElement(ThemeContext.Provider, { value }, /* @__PURE__ */ React.createElement("div", { "data-admin-kit": true, "data-theme": resolvedMode, style, className: theme?.className }, children));
374
+ }
375
+ function useAuth() {
376
+ const ctx = react.useContext(AuthContext);
377
+ if (!ctx) {
378
+ throw new Error("useAuth must be used inside <AdminProvider> / <AuthContextProvider>");
379
+ }
380
+ return ctx;
381
+ }
382
+
383
+ // src/AdminProvider.tsx
384
+ function AdminProvider({ config, initialSession, children }) {
385
+ const resolved = react.useMemo(() => resolveConfig(config), [config]);
386
+ return /* @__PURE__ */ React.createElement(ThemeProvider, { theme: resolved.theme }, /* @__PURE__ */ React.createElement(AuthContextProvider, { config: resolved.auth, initialSession }, /* @__PURE__ */ React.createElement(ModuleBridge, { modules: resolved.modules ?? [] }, children)));
387
+ }
388
+ function ModuleBridge({
389
+ modules,
390
+ children
391
+ }) {
392
+ const { user } = useAuth();
393
+ return /* @__PURE__ */ React.createElement(ModuleProvider, { modules, user }, children);
394
+ }
395
+
396
+ // src/utils/cn.ts
397
+ function cn(...values) {
398
+ const out = [];
399
+ for (const v of values) {
400
+ if (!v) continue;
401
+ if (typeof v === "string" || typeof v === "number") out.push(String(v));
402
+ else if (Array.isArray(v)) {
403
+ const inner = cn(...v);
404
+ if (inner) out.push(inner);
405
+ }
406
+ }
407
+ return out.join(" ");
408
+ }
409
+
410
+ // src/auth/components/LoginPage.tsx
411
+ function LoginPage({ config, oauthProviders, onOAuthClick, className }) {
412
+ const { login, isLoading, error } = useAuth();
413
+ const [email, setEmail] = react.useState("");
414
+ const [password, setPassword] = react.useState("");
415
+ const emailId = react.useId();
416
+ const passwordId = react.useId();
417
+ async function handleSubmit(e) {
418
+ e.preventDefault();
419
+ try {
420
+ await login({ email, password });
421
+ } catch {
422
+ }
423
+ }
424
+ return /* @__PURE__ */ React.createElement("div", { className: cn("admin-kit-login", "min-h-screen flex items-center justify-center p-6 bg-neutral-50", className) }, /* @__PURE__ */ React.createElement("div", { className: "w-full max-w-sm rounded-lg border border-neutral-200 bg-white p-6 shadow-sm" }, config?.logo ? /* @__PURE__ */ React.createElement("div", { className: "mb-4 flex justify-center" }, config.logo) : null, /* @__PURE__ */ React.createElement("h1", { className: "text-xl font-semibold text-neutral-900" }, config?.title ?? "Sign in"), config?.subtitle ? /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-neutral-500" }, config.subtitle) : null, /* @__PURE__ */ React.createElement("form", { className: "mt-6 space-y-4", onSubmit: handleSubmit }, /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", { htmlFor: emailId, className: "block text-sm font-medium text-neutral-700" }, "Email"), /* @__PURE__ */ React.createElement(
425
+ "input",
426
+ {
427
+ id: emailId,
428
+ type: "email",
429
+ autoComplete: "email",
430
+ required: true,
431
+ value: email,
432
+ onChange: (e) => setEmail(e.target.value),
433
+ className: "mt-1 w-full rounded-md border border-neutral-300 px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-neutral-900"
434
+ }
435
+ )), /* @__PURE__ */ React.createElement("div", null, /* @__PURE__ */ React.createElement("label", { htmlFor: passwordId, className: "block text-sm font-medium text-neutral-700" }, "Password"), /* @__PURE__ */ React.createElement(
436
+ "input",
437
+ {
438
+ id: passwordId,
439
+ type: "password",
440
+ autoComplete: "current-password",
441
+ required: true,
442
+ value: password,
443
+ onChange: (e) => setPassword(e.target.value),
444
+ className: "mt-1 w-full rounded-md border border-neutral-300 px-3 py-2 text-sm outline-none focus:ring-2 focus:ring-neutral-900"
445
+ }
446
+ )), error ? /* @__PURE__ */ React.createElement("p", { className: "text-sm text-red-600" }, error) : null, /* @__PURE__ */ React.createElement(
447
+ "button",
448
+ {
449
+ type: "submit",
450
+ disabled: isLoading,
451
+ className: "w-full rounded-md bg-neutral-900 px-3 py-2 text-sm font-medium text-white hover:bg-neutral-800 disabled:opacity-60"
452
+ },
453
+ isLoading ? "Signing in\u2026" : "Sign in"
454
+ )), oauthProviders?.length ? /* @__PURE__ */ React.createElement("div", { className: "mt-6" }, /* @__PURE__ */ React.createElement("div", { className: "relative my-4 text-center text-xs uppercase text-neutral-400" }, /* @__PURE__ */ React.createElement("span", { className: "bg-white px-2" }, "or continue with"), /* @__PURE__ */ React.createElement("span", { className: "absolute inset-x-0 top-1/2 -z-10 h-px bg-neutral-200" })), /* @__PURE__ */ React.createElement("div", { className: "flex flex-col gap-2" }, oauthProviders.map((p) => /* @__PURE__ */ React.createElement(
455
+ "button",
456
+ {
457
+ key: p.id,
458
+ type: "button",
459
+ onClick: () => onOAuthClick?.(p.id),
460
+ className: "flex items-center justify-center gap-2 rounded-md border border-neutral-300 bg-white px-3 py-2 text-sm font-medium text-neutral-800 hover:bg-neutral-50"
461
+ },
462
+ p.icon,
463
+ /* @__PURE__ */ React.createElement("span", null, "Continue with ", p.name)
464
+ )))) : null));
465
+ }
466
+ function useAppRouter() {
467
+ const r = navigation.useRouter();
468
+ return {
469
+ push: (href) => r.push(href),
470
+ replace: (href) => r.replace(href),
471
+ back: () => r.back()
472
+ };
473
+ }
474
+ function useAppPathname() {
475
+ return navigation.usePathname() ?? "/";
476
+ }
477
+ function useAppSearchParams() {
478
+ const sp = navigation.useSearchParams();
479
+ return new URLSearchParams(sp?.toString() ?? "");
480
+ }
481
+ var AppLink = NextLink__default.default;
482
+ var appRouterAdapter = {
483
+ useRouter: useAppRouter,
484
+ usePathname: useAppPathname,
485
+ useSearchParams: useAppSearchParams,
486
+ Link: AppLink
487
+ };
488
+
489
+ // src/auth/guards/AppRouterGuard.tsx
490
+ function AppRouterGuard({
491
+ loginPath = "/login",
492
+ publicRoutes = [],
493
+ fallback = null,
494
+ children
495
+ }) {
496
+ const { status, isAuthenticated } = useAuth();
497
+ const router = useAppRouter();
498
+ const pathname = useAppPathname();
499
+ const isPublic = publicRoutes.some((p) => pathname === p || pathname.startsWith(p + "/"));
500
+ react.useEffect(() => {
501
+ if (status === "loading" || status === "idle") return;
502
+ if (!isAuthenticated && !isPublic && pathname !== loginPath) {
503
+ const next = encodeURIComponent(pathname);
504
+ router.replace(`${loginPath}?next=${next}`);
505
+ }
506
+ }, [status, isAuthenticated, isPublic, pathname, loginPath, router]);
507
+ if (status === "loading" || status === "idle") return /* @__PURE__ */ React.createElement(React.Fragment, null, fallback);
508
+ if (!isAuthenticated && !isPublic && pathname !== loginPath) return /* @__PURE__ */ React.createElement(React.Fragment, null, fallback);
509
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, children);
510
+ }
511
+ function usePagesRouter() {
512
+ const r = router.useRouter();
513
+ return {
514
+ push: (href) => {
515
+ void r.push(href);
516
+ },
517
+ replace: (href) => {
518
+ void r.replace(href);
519
+ },
520
+ back: () => r.back()
521
+ };
522
+ }
523
+ function usePagesPathname() {
524
+ const r = router.useRouter();
525
+ return r.pathname;
526
+ }
527
+ function usePagesSearchParams() {
528
+ const r = router.useRouter();
529
+ const params = new URLSearchParams();
530
+ for (const [k, v] of Object.entries(r.query)) {
531
+ if (Array.isArray(v)) v.forEach((vv) => params.append(k, vv));
532
+ else if (v != null) params.set(k, String(v));
533
+ }
534
+ return params;
535
+ }
536
+ var PagesLink = NextLink__default.default;
537
+ var pagesRouterAdapter = {
538
+ useRouter: usePagesRouter,
539
+ usePathname: usePagesPathname,
540
+ useSearchParams: usePagesSearchParams,
541
+ Link: PagesLink
542
+ };
543
+
544
+ // src/auth/guards/PagesRouterGuard.tsx
545
+ function PagesRouterGuard({
546
+ loginPath = "/login",
547
+ publicRoutes = [],
548
+ fallback = null,
549
+ children
550
+ }) {
551
+ const { status, isAuthenticated } = useAuth();
552
+ const router = usePagesRouter();
553
+ const pathname = usePagesPathname();
554
+ const isPublic = publicRoutes.some((p) => pathname === p || pathname.startsWith(p + "/"));
555
+ react.useEffect(() => {
556
+ if (status === "loading" || status === "idle") return;
557
+ if (!isAuthenticated && !isPublic && pathname !== loginPath) {
558
+ const next = encodeURIComponent(pathname);
559
+ router.replace(`${loginPath}?next=${next}`);
560
+ }
561
+ }, [status, isAuthenticated, isPublic, pathname, loginPath, router]);
562
+ if (status === "loading" || status === "idle") return /* @__PURE__ */ React.createElement(React.Fragment, null, fallback);
563
+ if (!isAuthenticated && !isPublic && pathname !== loginPath) return /* @__PURE__ */ React.createElement(React.Fragment, null, fallback);
564
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, children);
565
+ }
566
+ var SidebarContext = react.createContext(null);
567
+ function SidebarProvider({ sections, defaultCollapsed = false, children }) {
568
+ const [collapsed, setCollapsed] = react.useState(defaultCollapsed);
569
+ const [mobileOpen, setMobileOpen] = react.useState(false);
570
+ const toggle = react.useCallback(() => setCollapsed((c) => !c), []);
571
+ const value = react.useMemo(
572
+ () => ({ sections, collapsed, setCollapsed, toggle, mobileOpen, setMobileOpen }),
573
+ [sections, collapsed, mobileOpen, toggle]
574
+ );
575
+ return /* @__PURE__ */ React.createElement(SidebarContext.Provider, { value }, children);
576
+ }
577
+ function useSidebar() {
578
+ const ctx = react.useContext(SidebarContext);
579
+ if (!ctx) throw new Error("useSidebar must be used inside <SidebarProvider>");
580
+ return ctx;
581
+ }
582
+ function SidebarItem({ item, collapsed, Link, currentPath, depth = 0 }) {
583
+ const hasChildren = (item.children?.length ?? 0) > 0;
584
+ const isActive = item.href != null && (currentPath === item.href || currentPath.startsWith(item.href + "/"));
585
+ const [open, setOpen] = react.useState(isActive);
586
+ const baseCls = cn(
587
+ "admin-kit-nav-item",
588
+ "group flex items-center gap-2 rounded-md px-3 py-2 text-sm font-medium transition-colors",
589
+ isActive ? "bg-neutral-100 text-neutral-900" : "text-neutral-600 hover:bg-neutral-50 hover:text-neutral-900",
590
+ depth > 0 && "ml-3"
591
+ );
592
+ if (hasChildren) {
593
+ return /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement(
594
+ "button",
595
+ {
596
+ type: "button",
597
+ onClick: () => setOpen((o) => !o),
598
+ className: cn(baseCls, "w-full justify-between"),
599
+ "aria-expanded": open
600
+ },
601
+ /* @__PURE__ */ React.createElement("span", { className: "flex items-center gap-2 min-w-0" }, item.icon, !collapsed && /* @__PURE__ */ React.createElement("span", { className: "truncate" }, item.label)),
602
+ !collapsed && /* @__PURE__ */ React.createElement("span", { "aria-hidden": true, className: cn("text-xs transition-transform", open && "rotate-90") }, "\u203A")
603
+ ), open && !collapsed && /* @__PURE__ */ React.createElement("ul", { className: "mt-1 space-y-0.5 border-l border-neutral-200 pl-2" }, item.children.map((child, i) => /* @__PURE__ */ React.createElement(
604
+ SidebarItem,
605
+ {
606
+ key: child.id ?? child.href ?? `${child.label}-${i}`,
607
+ item: child,
608
+ Link,
609
+ currentPath,
610
+ depth: depth + 1
611
+ }
612
+ ))));
613
+ }
614
+ if (!item.href) {
615
+ return /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement("span", { className: baseCls }, item.icon, !collapsed && /* @__PURE__ */ React.createElement("span", { className: "truncate" }, item.label)));
616
+ }
617
+ return /* @__PURE__ */ React.createElement("li", null, /* @__PURE__ */ React.createElement(
618
+ Link,
619
+ {
620
+ href: item.href,
621
+ className: baseCls,
622
+ ...item.external ? { target: "_blank", rel: "noopener noreferrer" } : {}
623
+ },
624
+ item.icon,
625
+ !collapsed && /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement("span", { className: "truncate flex-1" }, item.label), item.badge != null && /* @__PURE__ */ React.createElement("span", { className: "ml-auto inline-flex items-center rounded-full bg-neutral-200 px-2 text-xs font-medium text-neutral-700" }, item.badge))
626
+ ));
627
+ }
628
+
629
+ // src/navigation/SidebarSection.tsx
630
+ function SidebarSection({ section, collapsed, currentPath, Link }) {
631
+ return /* @__PURE__ */ React.createElement("div", { className: "admin-kit-nav-section" }, section.label && !collapsed && /* @__PURE__ */ React.createElement("h4", { className: "px-3 pb-1 pt-3 text-xs font-semibold uppercase tracking-wider text-neutral-400" }, section.label), /* @__PURE__ */ React.createElement("ul", { className: "space-y-0.5" }, section.items.map((item, i) => /* @__PURE__ */ React.createElement(
632
+ SidebarItem,
633
+ {
634
+ key: item.id ?? item.href ?? `${item.label}-${i}`,
635
+ item,
636
+ collapsed,
637
+ currentPath,
638
+ Link
639
+ }
640
+ ))));
641
+ }
642
+
643
+ // src/navigation/Sidebar.tsx
644
+ function Sidebar({ Link, currentPath, header, footer, className }) {
645
+ const { sections, collapsed, mobileOpen, setMobileOpen } = useSidebar();
646
+ return /* @__PURE__ */ React.createElement(React.Fragment, null, mobileOpen && /* @__PURE__ */ React.createElement(
647
+ "div",
648
+ {
649
+ className: "fixed inset-0 z-30 bg-black/40 md:hidden",
650
+ onClick: () => setMobileOpen(false),
651
+ "aria-hidden": true
652
+ }
653
+ ), /* @__PURE__ */ React.createElement(
654
+ "aside",
655
+ {
656
+ className: cn(
657
+ "admin-kit-sidebar",
658
+ "fixed inset-y-0 left-0 z-40 flex flex-col border-r border-neutral-200 bg-white transition-all",
659
+ collapsed ? "w-16" : "w-64",
660
+ mobileOpen ? "translate-x-0" : "-translate-x-full md:translate-x-0",
661
+ className
662
+ ),
663
+ "aria-label": "Sidebar"
664
+ },
665
+ header && /* @__PURE__ */ React.createElement("div", { className: "border-b border-neutral-200 p-3" }, header),
666
+ /* @__PURE__ */ React.createElement("nav", { className: "flex-1 overflow-y-auto p-2" }, /* @__PURE__ */ React.createElement("div", { className: "space-y-2" }, sections.map((section, i) => /* @__PURE__ */ React.createElement(
667
+ SidebarSection,
668
+ {
669
+ key: section.id ?? `section-${i}`,
670
+ section,
671
+ collapsed,
672
+ currentPath,
673
+ Link
674
+ }
675
+ )))),
676
+ footer && /* @__PURE__ */ React.createElement("div", { className: "border-t border-neutral-200 p-3" }, footer)
677
+ ));
678
+ }
679
+
680
+ // src/navigation/buildNav.ts
681
+ function buildNav(configSections, modules = []) {
682
+ const map = /* @__PURE__ */ new Map();
683
+ let anonIdx = 0;
684
+ for (const s of configSections) {
685
+ const id = s.id ?? `__anon_${anonIdx++}`;
686
+ map.set(id, { ...s, id, items: [...s.items] });
687
+ }
688
+ for (const m of modules) {
689
+ for (const s of m.navSections ?? []) {
690
+ const id = s.id ?? `__anon_${anonIdx++}`;
691
+ const existing = map.get(id);
692
+ if (existing) {
693
+ existing.items.push(...s.items);
694
+ } else {
695
+ map.set(id, { ...s, id, items: [...s.items] });
696
+ }
697
+ }
698
+ }
699
+ const sections = Array.from(map.values()).sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
700
+ for (const s of sections) {
701
+ s.items = sortItems(s.items);
702
+ }
703
+ return sections;
704
+ }
705
+ function sortItems(items) {
706
+ const sorted = items.slice().sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
707
+ return sorted.map((i) => i.children ? { ...i, children: sortItems(i.children) } : i);
708
+ }
709
+
710
+ // src/navigation/filterNav.ts
711
+ function userHasAny(user, required, key) {
712
+ if (!required || required.length === 0) return true;
713
+ const have = user?.[key] ?? [];
714
+ return required.some((r) => have.includes(r));
715
+ }
716
+ function visibleItem(item, user) {
717
+ if (item.hidden) return null;
718
+ if (!userHasAny(user, item.roles, "roles")) return null;
719
+ if (!userHasAny(user, item.permissions, "permissions")) return null;
720
+ if (item.visible && !item.visible(user)) return null;
721
+ let children;
722
+ if (item.children) {
723
+ children = item.children.map((c) => visibleItem(c, user)).filter((c) => c !== null);
724
+ if (children.length === 0 && !item.href) return null;
725
+ }
726
+ return children ? { ...item, children } : item;
727
+ }
728
+ function filterNav(sections, user) {
729
+ const out = [];
730
+ for (const section of sections) {
731
+ if (!userHasAny(user, section.roles, "roles")) continue;
732
+ if (!userHasAny(user, section.permissions, "permissions")) continue;
733
+ if (section.visible && !section.visible(user)) continue;
734
+ const items = section.items.map((i) => visibleItem(i, user)).filter((i) => i !== null);
735
+ if (items.length === 0) continue;
736
+ out.push({ ...section, items });
737
+ }
738
+ return out;
739
+ }
740
+ function useModules() {
741
+ const ctx = react.useContext(ModuleContext);
742
+ if (!ctx) throw new Error("useModules must be used inside <AdminProvider> / <ModuleProvider>");
743
+ return ctx;
744
+ }
745
+
746
+ // src/components/layout/Topbar.tsx
747
+ function Topbar({ title, actions, className }) {
748
+ const { user, logout } = useAuth();
749
+ const { toggle, setMobileOpen, collapsed } = useSidebar();
750
+ return /* @__PURE__ */ React.createElement(
751
+ "header",
752
+ {
753
+ className: cn(
754
+ "admin-kit-topbar",
755
+ "sticky top-0 z-20 flex h-14 items-center gap-3 border-b border-neutral-200 bg-white px-4",
756
+ className
757
+ )
758
+ },
759
+ /* @__PURE__ */ React.createElement(
760
+ "button",
761
+ {
762
+ type: "button",
763
+ className: "rounded p-1.5 text-neutral-600 hover:bg-neutral-100 md:hidden",
764
+ onClick: () => setMobileOpen(true),
765
+ "aria-label": "Open sidebar"
766
+ },
767
+ /* @__PURE__ */ React.createElement(Bars, null)
768
+ ),
769
+ /* @__PURE__ */ React.createElement(
770
+ "button",
771
+ {
772
+ type: "button",
773
+ className: "hidden rounded p-1.5 text-neutral-600 hover:bg-neutral-100 md:inline-flex",
774
+ onClick: toggle,
775
+ "aria-label": collapsed ? "Expand sidebar" : "Collapse sidebar"
776
+ },
777
+ /* @__PURE__ */ React.createElement(Bars, null)
778
+ ),
779
+ /* @__PURE__ */ React.createElement("div", { className: "flex-1 truncate text-sm font-medium text-neutral-800" }, title),
780
+ /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-2" }, actions),
781
+ user && /* @__PURE__ */ React.createElement("div", { className: "ml-2 flex items-center gap-2 border-l border-neutral-200 pl-3" }, /* @__PURE__ */ React.createElement("span", { className: "text-sm text-neutral-700" }, user.name ?? user.email ?? user.id), /* @__PURE__ */ React.createElement(
782
+ "button",
783
+ {
784
+ type: "button",
785
+ onClick: () => void logout(),
786
+ className: "rounded-md px-2 py-1 text-xs font-medium text-neutral-600 hover:bg-neutral-100"
787
+ },
788
+ "Sign out"
789
+ ))
790
+ );
791
+ }
792
+ function Bars() {
793
+ return /* @__PURE__ */ React.createElement("svg", { width: "18", height: "18", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", "aria-hidden": true }, /* @__PURE__ */ React.createElement("line", { x1: "3", y1: "6", x2: "21", y2: "6" }), /* @__PURE__ */ React.createElement("line", { x1: "3", y1: "12", x2: "21", y2: "12" }), /* @__PURE__ */ React.createElement("line", { x1: "3", y1: "18", x2: "21", y2: "18" }));
794
+ }
795
+
796
+ // src/components/layout/AdminLayout.tsx
797
+ function AdminLayout({
798
+ config,
799
+ Link,
800
+ currentPath,
801
+ title,
802
+ topbarActions,
803
+ children,
804
+ className
805
+ }) {
806
+ const { user } = useAuth();
807
+ const modules = useModules();
808
+ const sections = react.useMemo(
809
+ () => filterNav(buildNav(config.navigation.sections, modules.all), user),
810
+ [config.navigation.sections, modules, user]
811
+ );
812
+ const sidebarPosition = config.layout.sidebarPosition;
813
+ const Topbarish = config.layout.topbar?.component ?? Topbar;
814
+ const showTopbar = config.layout.topbar?.visible !== false;
815
+ return /* @__PURE__ */ React.createElement(SidebarProvider, { sections, defaultCollapsed: config.layout.sidebarDefaultCollapsed }, /* @__PURE__ */ React.createElement("div", { className: cn("admin-kit-root", "min-h-screen bg-neutral-50 text-neutral-900", className) }, /* @__PURE__ */ React.createElement(
816
+ Sidebar,
817
+ {
818
+ Link,
819
+ currentPath,
820
+ header: /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-2" }, typeof config.app.logo === "string" ? /* @__PURE__ */ React.createElement("img", { src: config.app.logo, alt: "", className: "h-6 w-6" }) : config.app.logo, /* @__PURE__ */ React.createElement("span", { className: "truncate font-semibold" }, config.app.name))
821
+ }
822
+ ), /* @__PURE__ */ React.createElement("div", { className: cn("flex min-h-screen flex-col transition-[padding]", sidebarPosition === "right" ? "md:pr-64" : "md:pl-64") }, showTopbar && /* @__PURE__ */ React.createElement(Topbarish, { title, actions: topbarActions }), /* @__PURE__ */ React.createElement("main", { className: "flex-1" }, children))));
823
+ }
824
+
825
+ // src/components/layout/PageContainer.tsx
826
+ function PageContainer({
827
+ title,
828
+ description,
829
+ actions,
830
+ children,
831
+ className,
832
+ contentClassName
833
+ }) {
834
+ return /* @__PURE__ */ React.createElement("div", { className: cn("admin-kit-page", "p-6", className) }, (title || description || actions) && /* @__PURE__ */ React.createElement("div", { className: "mb-6 flex items-start justify-between gap-4" }, /* @__PURE__ */ React.createElement("div", null, title && /* @__PURE__ */ React.createElement("h1", { className: "text-2xl font-semibold text-neutral-900" }, title), description && /* @__PURE__ */ React.createElement("p", { className: "mt-1 text-sm text-neutral-500" }, description)), actions && /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-2" }, actions)), /* @__PURE__ */ React.createElement("div", { className: cn("admin-kit-page-content", contentClassName) }, children));
835
+ }
836
+
837
+ // src/components/dashboard/StatCard.tsx
838
+ function StatCard({ label, value, delta, icon, className }) {
839
+ const isUp = delta ? (delta.direction ?? (delta.value >= 0 ? "up" : "down")) === "up" : false;
840
+ return /* @__PURE__ */ React.createElement("div", { className: cn("admin-kit-stat", "rounded-lg border border-neutral-200 bg-white p-4", className) }, /* @__PURE__ */ React.createElement("div", { className: "flex items-start justify-between gap-3" }, /* @__PURE__ */ React.createElement("div", { className: "text-sm font-medium text-neutral-500" }, label), icon && /* @__PURE__ */ React.createElement("div", { className: "text-neutral-400" }, icon)), /* @__PURE__ */ React.createElement("div", { className: "mt-2 text-2xl font-semibold text-neutral-900" }, value), delta && /* @__PURE__ */ React.createElement(
841
+ "div",
842
+ {
843
+ className: cn(
844
+ "mt-1 inline-flex items-center gap-1 text-xs font-medium",
845
+ isUp ? "text-emerald-600" : "text-red-600"
846
+ )
847
+ },
848
+ /* @__PURE__ */ React.createElement("span", { "aria-hidden": true }, isUp ? "\u2191" : "\u2193"),
849
+ /* @__PURE__ */ React.createElement("span", null, Math.abs(delta.value), "%"),
850
+ delta.label && /* @__PURE__ */ React.createElement("span", { className: "text-neutral-500" }, delta.label)
851
+ ));
852
+ }
853
+
854
+ // src/components/dashboard/ChartCard.tsx
855
+ function ChartCard({ title, subtitle, actions, children, className, contentClassName }) {
856
+ return /* @__PURE__ */ React.createElement("div", { className: cn("admin-kit-chart-card", "rounded-lg border border-neutral-200 bg-white", className) }, (title || subtitle || actions) && /* @__PURE__ */ React.createElement("div", { className: "flex items-start justify-between gap-3 border-b border-neutral-100 p-4" }, /* @__PURE__ */ React.createElement("div", null, title && /* @__PURE__ */ React.createElement("h3", { className: "text-sm font-semibold text-neutral-900" }, title), subtitle && /* @__PURE__ */ React.createElement("p", { className: "mt-0.5 text-xs text-neutral-500" }, subtitle)), actions && /* @__PURE__ */ React.createElement("div", { className: "flex items-center gap-2" }, actions)), /* @__PURE__ */ React.createElement("div", { className: cn("p-4", contentClassName) }, children));
857
+ }
858
+
859
+ // src/components/dashboard/ActivityFeed.tsx
860
+ function formatRelative(timestamp) {
861
+ const date = timestamp instanceof Date ? timestamp : new Date(timestamp);
862
+ const diff = Date.now() - date.getTime();
863
+ const minutes = Math.floor(diff / 6e4);
864
+ if (minutes < 1) return "just now";
865
+ if (minutes < 60) return `${minutes}m ago`;
866
+ const hours = Math.floor(minutes / 60);
867
+ if (hours < 24) return `${hours}h ago`;
868
+ const days = Math.floor(hours / 24);
869
+ if (days < 7) return `${days}d ago`;
870
+ return date.toLocaleDateString();
871
+ }
872
+ function ActivityFeed({ items, emptyState, className }) {
873
+ if (items.length === 0) {
874
+ return /* @__PURE__ */ React.createElement("div", { className: cn("admin-kit-activity-empty", "rounded-lg border border-dashed border-neutral-200 p-6 text-center text-sm text-neutral-500", className) }, emptyState ?? "No activity yet.");
875
+ }
876
+ return /* @__PURE__ */ React.createElement("ul", { className: cn("admin-kit-activity", "divide-y divide-neutral-100 rounded-lg border border-neutral-200 bg-white", className) }, items.map((item) => /* @__PURE__ */ React.createElement("li", { key: item.id, className: "flex gap-3 p-3" }, /* @__PURE__ */ React.createElement("div", { className: "mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-full bg-neutral-100 text-neutral-600" }, item.icon ?? (item.actor?.image ? /* @__PURE__ */ React.createElement("img", { src: item.actor.image, alt: "", className: "h-8 w-8 rounded-full" }) : item.actor?.name?.[0] ?? "\u2022")), /* @__PURE__ */ React.createElement("div", { className: "min-w-0 flex-1" }, /* @__PURE__ */ React.createElement("div", { className: "text-sm text-neutral-900" }, item.title), item.description && /* @__PURE__ */ React.createElement("div", { className: "text-sm text-neutral-500" }, item.description), item.timestamp && /* @__PURE__ */ React.createElement("div", { className: "mt-0.5 text-xs text-neutral-400" }, formatRelative(item.timestamp))))));
877
+ }
878
+
879
+ // src/utils/withAdminLayout.tsx
880
+ function withAdminLayout(Page, opts) {
881
+ function Wrapped(props) {
882
+ const currentPath = usePagesPathname();
883
+ return /* @__PURE__ */ React.createElement(
884
+ AdminLayout,
885
+ {
886
+ config: opts.config,
887
+ Link: PagesLink,
888
+ currentPath,
889
+ title: opts.title,
890
+ topbarActions: opts.topbarActions
891
+ },
892
+ /* @__PURE__ */ React.createElement(Page, { ...props })
893
+ );
894
+ }
895
+ Wrapped.displayName = `withAdminLayout(${Page.displayName ?? Page.name ?? "Page"})`;
896
+ return Wrapped;
897
+ }
898
+ function useTheme() {
899
+ const ctx = react.useContext(ThemeContext);
900
+ if (!ctx) throw new Error("useTheme must be used inside <ThemeProvider> / <AdminProvider>");
901
+ return ctx;
902
+ }
903
+
904
+ exports.ActivityFeed = ActivityFeed;
905
+ exports.AdminLayout = AdminLayout;
906
+ exports.AdminProvider = AdminProvider;
907
+ exports.AppLink = AppLink;
908
+ exports.AppRouterGuard = AppRouterGuard;
909
+ exports.AuthContext = AuthContext;
910
+ exports.AuthContextProvider = AuthContextProvider;
911
+ exports.ChartCard = ChartCard;
912
+ exports.DEFAULT_TOKENS = DEFAULT_TOKENS;
913
+ exports.LoginPage = LoginPage;
914
+ exports.ModuleContext = ModuleContext;
915
+ exports.ModuleProvider = ModuleProvider;
916
+ exports.PageContainer = PageContainer;
917
+ exports.PagesLink = PagesLink;
918
+ exports.PagesRouterGuard = PagesRouterGuard;
919
+ exports.Sidebar = Sidebar;
920
+ exports.SidebarContext = SidebarContext;
921
+ exports.SidebarItem = SidebarItem;
922
+ exports.SidebarProvider = SidebarProvider;
923
+ exports.SidebarSection = SidebarSection;
924
+ exports.StatCard = StatCard;
925
+ exports.ThemeContext = ThemeContext;
926
+ exports.ThemeProvider = ThemeProvider;
927
+ exports.Topbar = Topbar;
928
+ exports.appRouterAdapter = appRouterAdapter;
929
+ exports.buildNav = buildNav;
930
+ exports.cn = cn;
931
+ exports.createCustomProvider = createCustomProvider;
932
+ exports.createJWTProvider = createJWTProvider;
933
+ exports.createModuleRegistry = createModuleRegistry;
934
+ exports.createOAuthProvider = createOAuthProvider;
935
+ exports.filterNav = filterNav;
936
+ exports.pagesRouterAdapter = pagesRouterAdapter;
937
+ exports.tokensToStyle = tokensToStyle;
938
+ exports.useAppPathname = useAppPathname;
939
+ exports.useAppRouter = useAppRouter;
940
+ exports.useAppSearchParams = useAppSearchParams;
941
+ exports.useAuth = useAuth;
942
+ exports.useModules = useModules;
943
+ exports.usePagesPathname = usePagesPathname;
944
+ exports.usePagesRouter = usePagesRouter;
945
+ exports.usePagesSearchParams = usePagesSearchParams;
946
+ exports.useSidebar = useSidebar;
947
+ exports.useTheme = useTheme;
948
+ exports.withAdminLayout = withAdminLayout;
949
+ //# sourceMappingURL=client.cjs.map
950
+ //# sourceMappingURL=client.cjs.map