@specscreen/backoffice-core 0.1.1

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.js ADDED
@@ -0,0 +1,1565 @@
1
+ import * as React11 from 'react';
2
+ import React11__default, { createContext, useReducer, useEffect, useCallback, useMemo, useContext, useState } from 'react';
3
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
+ import { ChevronDown, Check, Loader2, LayoutPanelTop, EllipsisVertical, ChevronRight, PanelLeft, Plus, ShieldOff, X, Settings, CircleDot, UserCog, MoreHorizontal, ChevronLeft, ChevronsLeft, ChevronsRight } from 'lucide-react';
5
+ import { Slot } from '@radix-ui/react-slot';
6
+ import { cva } from 'class-variance-authority';
7
+ import { clsx } from 'clsx';
8
+ import { twMerge } from 'tailwind-merge';
9
+ import * as LabelPrimitive from '@radix-ui/react-label';
10
+ import * as SelectPrimitive from '@radix-ui/react-select';
11
+ import { createPortal } from 'react-dom';
12
+ import { useSensors, useSensor, PointerSensor, KeyboardSensor, DndContext, closestCenter } from '@dnd-kit/core';
13
+ import { sortableKeyboardCoordinates, arrayMove, SortableContext, verticalListSortingStrategy, useSortable } from '@dnd-kit/sortable';
14
+ import { CSS } from '@dnd-kit/utilities';
15
+ import { useReactTable, getCoreRowModel, flexRender } from '@tanstack/react-table';
16
+
17
+ // src/core/auth/AuthContext.tsx
18
+
19
+ // src/core/rbac/evaluator.ts
20
+ function canAccessResource(user, meta) {
21
+ if (!meta) return true;
22
+ const { requiredRoles, requiredPermissions } = meta;
23
+ const hasRoleConstraint = requiredRoles && requiredRoles.length > 0;
24
+ const hasPermissionConstraint = requiredPermissions && requiredPermissions.length > 0;
25
+ if (!hasRoleConstraint && !hasPermissionConstraint) return true;
26
+ if (!user) return false;
27
+ if (hasRoleConstraint) {
28
+ const userRoles = user.roles ?? [];
29
+ const rolesPassed = requiredRoles.some((role) => userRoles.includes(role));
30
+ if (!rolesPassed) return false;
31
+ }
32
+ if (hasPermissionConstraint) {
33
+ const userPermissions = user.permissions ?? [];
34
+ const permissionsPassed = requiredPermissions.every(
35
+ (perm) => userPermissions.includes(perm)
36
+ );
37
+ if (!permissionsPassed) return false;
38
+ }
39
+ return true;
40
+ }
41
+ function evaluateCan(user, permission) {
42
+ if (!user) return false;
43
+ return (user.permissions ?? []).includes(permission);
44
+ }
45
+ function evaluateHasRole(user, role) {
46
+ if (!user) return false;
47
+ return (user.roles ?? []).includes(role);
48
+ }
49
+ function evaluateCanAny(user, permissions) {
50
+ if (!user || permissions.length === 0) return false;
51
+ const userPermissions = user.permissions ?? [];
52
+ return permissions.some((perm) => userPermissions.includes(perm));
53
+ }
54
+ function evaluateCanAll(user, permissions) {
55
+ if (!user || permissions.length === 0) return false;
56
+ const userPermissions = user.permissions ?? [];
57
+ return permissions.every((perm) => userPermissions.includes(perm));
58
+ }
59
+ function authReducer(state, action) {
60
+ switch (action.type) {
61
+ case "LOADING":
62
+ return { ...state, isLoading: true, error: null };
63
+ case "AUTHENTICATED":
64
+ return {
65
+ isLoading: false,
66
+ isAuthenticated: true,
67
+ user: action.user,
68
+ error: null
69
+ };
70
+ case "UNAUTHENTICATED":
71
+ return {
72
+ isLoading: false,
73
+ isAuthenticated: false,
74
+ user: null,
75
+ error: null
76
+ };
77
+ case "ERROR":
78
+ return {
79
+ isLoading: false,
80
+ isAuthenticated: false,
81
+ user: null,
82
+ error: action.error
83
+ };
84
+ case "USER_UPDATED":
85
+ return { ...state, user: action.user };
86
+ default:
87
+ return state;
88
+ }
89
+ }
90
+ var initialState = {
91
+ isLoading: true,
92
+ isAuthenticated: false,
93
+ user: null,
94
+ error: null
95
+ };
96
+ var AuthContext = createContext(null);
97
+ function AuthContextProvider({
98
+ authProvider,
99
+ children
100
+ }) {
101
+ const [state, dispatch] = useReducer(authReducer, initialState);
102
+ useEffect(() => {
103
+ let cancelled = false;
104
+ async function boot() {
105
+ dispatch({ type: "LOADING" });
106
+ try {
107
+ const isAuthenticated = await authProvider.checkAuth();
108
+ if (cancelled) return;
109
+ if (!isAuthenticated) {
110
+ dispatch({ type: "UNAUTHENTICATED" });
111
+ return;
112
+ }
113
+ const user = await authProvider.getUser();
114
+ if (cancelled) return;
115
+ if (user) {
116
+ dispatch({ type: "AUTHENTICATED", user });
117
+ } else {
118
+ dispatch({ type: "UNAUTHENTICATED" });
119
+ }
120
+ } catch {
121
+ if (!cancelled) dispatch({ type: "UNAUTHENTICATED" });
122
+ }
123
+ }
124
+ boot();
125
+ return () => {
126
+ cancelled = true;
127
+ };
128
+ }, [authProvider]);
129
+ const login = useCallback(
130
+ async (params) => {
131
+ dispatch({ type: "LOADING" });
132
+ await authProvider.login(params);
133
+ const user = await authProvider.getUser();
134
+ if (user) {
135
+ dispatch({ type: "AUTHENTICATED", user });
136
+ } else {
137
+ dispatch({ type: "ERROR", error: "Failed to retrieve user after login." });
138
+ }
139
+ },
140
+ [authProvider]
141
+ );
142
+ const logout = useCallback(async () => {
143
+ try {
144
+ await authProvider.logout();
145
+ } catch (err) {
146
+ console.error("[BackofficeApp] Logout error:", err);
147
+ } finally {
148
+ dispatch({ type: "UNAUTHENTICATED" });
149
+ }
150
+ }, [authProvider]);
151
+ const refreshUser = useCallback(async () => {
152
+ const user = await authProvider.getUser();
153
+ if (user) dispatch({ type: "USER_UPDATED", user });
154
+ }, [authProvider]);
155
+ const can = useCallback(
156
+ (permission) => evaluateCan(state.user, permission),
157
+ [state.user]
158
+ );
159
+ const canAny = useCallback(
160
+ (permissions) => evaluateCanAny(state.user, permissions),
161
+ [state.user]
162
+ );
163
+ const canAll = useCallback(
164
+ (permissions) => evaluateCanAll(state.user, permissions),
165
+ [state.user]
166
+ );
167
+ const hasRole = useCallback(
168
+ (role) => evaluateHasRole(state.user, role),
169
+ [state.user]
170
+ );
171
+ const value = useMemo(
172
+ () => ({
173
+ ...state,
174
+ login,
175
+ logout,
176
+ refreshUser,
177
+ can,
178
+ canAny,
179
+ canAll,
180
+ hasRole
181
+ }),
182
+ [state, login, logout, refreshUser, can, canAny, canAll, hasRole]
183
+ );
184
+ return /* @__PURE__ */ jsx(AuthContext.Provider, { value, children });
185
+ }
186
+ function useAuthContext() {
187
+ const ctx = useContext(AuthContext);
188
+ if (!ctx) {
189
+ throw new Error("useAuthContext must be used inside <BackofficeApp>.");
190
+ }
191
+ return ctx;
192
+ }
193
+ function LoadingScreen({ message }) {
194
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-h-screen w-full flex-col items-center justify-center gap-3 bg-background", children: [
195
+ /* @__PURE__ */ jsx(Loader2, { className: "size-8 animate-spin text-muted-foreground" }),
196
+ message && /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: message })
197
+ ] });
198
+ }
199
+ function cn(...inputs) {
200
+ return twMerge(clsx(inputs));
201
+ }
202
+ var buttonVariants = cva(
203
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 disabled:pointer-events-none disabled:opacity-50 cursor-pointer",
204
+ {
205
+ variants: {
206
+ variant: {
207
+ default: "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
208
+ destructive: "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90",
209
+ outline: "border border-input bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
210
+ secondary: "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
211
+ ghost: "hover:bg-accent hover:text-accent-foreground",
212
+ link: "text-primary underline-offset-4 hover:underline"
213
+ },
214
+ size: {
215
+ default: "h-9 px-4 py-2",
216
+ sm: "h-8 rounded-md px-3 text-xs",
217
+ lg: "h-10 rounded-lg px-6",
218
+ icon: "size-8"
219
+ }
220
+ },
221
+ defaultVariants: {
222
+ variant: "default",
223
+ size: "default"
224
+ }
225
+ }
226
+ );
227
+ var Button = React11.forwardRef(
228
+ ({ className, variant, size, asChild = false, ...props }, ref) => {
229
+ const Comp = asChild ? Slot : "button";
230
+ return /* @__PURE__ */ jsx(
231
+ Comp,
232
+ {
233
+ className: cn(buttonVariants({ variant, size, className })),
234
+ ref,
235
+ ...props
236
+ }
237
+ );
238
+ }
239
+ );
240
+ Button.displayName = "Button";
241
+ var Input = React11.forwardRef(({ className, type, ...props }, ref) => {
242
+ return /* @__PURE__ */ jsx(
243
+ "input",
244
+ {
245
+ type,
246
+ className: cn(
247
+ "flex h-9 w-full rounded-lg border border-input bg-background px-3 py-1 text-base shadow-xs transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50",
248
+ className
249
+ ),
250
+ ref,
251
+ ...props
252
+ }
253
+ );
254
+ });
255
+ Input.displayName = "Input";
256
+ var Label = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
257
+ LabelPrimitive.Root,
258
+ {
259
+ ref,
260
+ className: cn(
261
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
262
+ className
263
+ ),
264
+ ...props
265
+ }
266
+ ));
267
+ Label.displayName = LabelPrimitive.Root.displayName;
268
+ function LoginPage({
269
+ appName = "backoffice",
270
+ logo,
271
+ onLoginSuccess,
272
+ onForgotPassword,
273
+ description
274
+ }) {
275
+ const { login, isLoading, error } = useAuthContext();
276
+ const [email, setEmail] = useState("");
277
+ const [password, setPassword] = useState("");
278
+ const [localError, setLocalError] = useState(null);
279
+ const [attempts, setAttempts] = useState(0);
280
+ const MAX_ATTEMPTS = 10;
281
+ async function handleSubmit(e) {
282
+ e.preventDefault();
283
+ setLocalError(null);
284
+ if (!email.trim() || !password) {
285
+ setLocalError("Please enter your email and password.");
286
+ return;
287
+ }
288
+ setAttempts((n) => n + 1);
289
+ if (attempts >= MAX_ATTEMPTS) {
290
+ setLocalError("Too many failed attempts. Please wait before trying again.");
291
+ return;
292
+ }
293
+ try {
294
+ await login({ email: email.trim(), password });
295
+ setAttempts(0);
296
+ onLoginSuccess?.();
297
+ } catch (err) {
298
+ console.error("[BackofficeApp] Login error:", err);
299
+ setLocalError("Invalid email or password.");
300
+ }
301
+ }
302
+ const displayError = localError ?? (error ? "Authentication error. Please try again." : null);
303
+ return /* @__PURE__ */ jsx("div", { className: "min-h-screen w-full bg-background flex items-center justify-center p-4", children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-sm rounded-xl border border-border bg-background shadow-xs overflow-hidden p-10", children: [
304
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col items-center gap-2 pb-8", children: [
305
+ logo ? /* @__PURE__ */ jsx("div", { className: "size-6", children: logo }) : /* @__PURE__ */ jsx("div", { className: "size-6 rounded-full bg-foreground flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "text-background text-xs font-bold", children: "B" }) }),
306
+ /* @__PURE__ */ jsxs("h1", { className: "text-xl font-semibold text-foreground text-center", children: [
307
+ "Welcome to ",
308
+ appName
309
+ ] }),
310
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground text-center", children: description ?? "Enter your email below to login to your account" })
311
+ ] }),
312
+ /* @__PURE__ */ jsxs("form", { onSubmit: handleSubmit, className: "flex flex-col gap-6", children: [
313
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
314
+ /* @__PURE__ */ jsx(Label, { htmlFor: "email", children: "Email" }),
315
+ /* @__PURE__ */ jsx(
316
+ Input,
317
+ {
318
+ id: "email",
319
+ type: "email",
320
+ placeholder: "m@example.com",
321
+ autoComplete: "email",
322
+ required: true,
323
+ value: email,
324
+ onChange: (e) => setEmail(e.target.value),
325
+ disabled: isLoading
326
+ }
327
+ )
328
+ ] }),
329
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-3", children: [
330
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between", children: [
331
+ /* @__PURE__ */ jsx(Label, { htmlFor: "password", children: "Password" }),
332
+ onForgotPassword && /* @__PURE__ */ jsx(
333
+ "button",
334
+ {
335
+ type: "button",
336
+ onClick: onForgotPassword,
337
+ className: "text-sm text-foreground hover:underline",
338
+ children: "Forgot password?"
339
+ }
340
+ )
341
+ ] }),
342
+ /* @__PURE__ */ jsx(
343
+ Input,
344
+ {
345
+ id: "password",
346
+ type: "password",
347
+ autoComplete: "current-password",
348
+ required: true,
349
+ value: password,
350
+ onChange: (e) => setPassword(e.target.value),
351
+ disabled: isLoading
352
+ }
353
+ )
354
+ ] }),
355
+ displayError && /* @__PURE__ */ jsx("p", { className: "text-sm text-destructive text-center", role: "alert", children: displayError }),
356
+ /* @__PURE__ */ jsx(Button, { type: "submit", className: "w-full", disabled: isLoading, children: isLoading ? "Logging in\u2026" : "Log in" })
357
+ ] }),
358
+ /* @__PURE__ */ jsx("p", { className: "mt-4 text-sm text-muted-foreground text-center", children: "Don't have an account? Ask an admin for access." })
359
+ ] }) });
360
+ }
361
+ function AuthGuard({
362
+ children,
363
+ loginPageProps,
364
+ renderLogin,
365
+ renderLoading,
366
+ onLoginSuccess
367
+ }) {
368
+ const { isLoading, isAuthenticated, error } = useAuthContext();
369
+ if (isLoading) {
370
+ return renderLoading ? /* @__PURE__ */ jsx(Fragment, { children: renderLoading() }) : /* @__PURE__ */ jsx(LoadingScreen, {});
371
+ }
372
+ if (!isAuthenticated || error) {
373
+ if (renderLogin) return /* @__PURE__ */ jsx(Fragment, { children: renderLogin() });
374
+ return /* @__PURE__ */ jsx(
375
+ LoginPage,
376
+ {
377
+ ...loginPageProps,
378
+ onLoginSuccess
379
+ }
380
+ );
381
+ }
382
+ return /* @__PURE__ */ jsx(Fragment, { children });
383
+ }
384
+ var DefaultNavLink = ({
385
+ href,
386
+ children,
387
+ className,
388
+ onClick
389
+ }) => /* @__PURE__ */ jsx("a", { href, className, onClick, children });
390
+ function SidebarItem({
391
+ label,
392
+ path,
393
+ icon: Icon2,
394
+ isActive,
395
+ NavLink
396
+ }) {
397
+ return /* @__PURE__ */ jsxs(
398
+ NavLink,
399
+ {
400
+ href: path,
401
+ isActive,
402
+ className: cn(
403
+ "flex h-8 w-full items-center gap-2 rounded-lg px-2 text-sm text-sidebar-foreground transition-colors",
404
+ "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
405
+ isActive && "bg-sidebar-accent text-sidebar-accent-foreground font-medium"
406
+ ),
407
+ children: [
408
+ Icon2 && /* @__PURE__ */ jsx(Icon2, { className: "size-4 shrink-0" }),
409
+ /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: label })
410
+ ]
411
+ }
412
+ );
413
+ }
414
+ function Sidebar({
415
+ config,
416
+ currentPath,
417
+ NavLink = DefaultNavLink,
418
+ onLogout
419
+ }) {
420
+ const { user } = useAuthContext();
421
+ const [userMenuOpen, setUserMenuOpen] = useState(false);
422
+ const headerName = config.companyName ?? config.appName;
423
+ const websiteLabel = config.goToWebsite?.label ?? "Go to website";
424
+ const WebsiteIcon = config.goToWebsite?.icon ?? LayoutPanelTop;
425
+ const websiteTarget = config.goToWebsite?.target ?? "_blank";
426
+ const groups = config.sidebarGroups ?? (config.resources ? [{ label: "", resources: config.resources }] : []);
427
+ return /* @__PURE__ */ jsxs("aside", { className: "flex h-full w-[255px] shrink-0 flex-col border-r border-sidebar-border bg-sidebar", children: [
428
+ /* @__PURE__ */ jsx("div", { className: "flex items-center gap-2 border-b border-sidebar-border px-2 py-3", children: /* @__PURE__ */ jsxs("div", { className: "flex h-8 w-full items-center gap-2 px-1.5", children: [
429
+ typeof config.logo === "string" ? /* @__PURE__ */ jsx(
430
+ "img",
431
+ {
432
+ src: config.logo,
433
+ alt: headerName,
434
+ className: "size-5 shrink-0"
435
+ }
436
+ ) : config.logo ? /* @__PURE__ */ jsx(config.logo, { className: "size-5 shrink-0" }) : /* @__PURE__ */ jsx("div", { className: "size-5 shrink-0 rounded-sm bg-foreground flex items-center justify-center", children: /* @__PURE__ */ jsx("span", { className: "text-background text-[10px] font-bold leading-none", children: headerName.charAt(0).toUpperCase() }) }),
437
+ /* @__PURE__ */ jsx("span", { className: "flex-1 truncate text-base font-semibold text-foreground", children: headerName })
438
+ ] }) }),
439
+ /* @__PURE__ */ jsx("div", { className: "flex flex-1 flex-col gap-2 overflow-y-auto", children: groups.map((group, gi) => /* @__PURE__ */ jsx(
440
+ FilteredGroup,
441
+ {
442
+ group,
443
+ currentPath,
444
+ NavLink,
445
+ user
446
+ },
447
+ group.label || gi
448
+ )) }),
449
+ config.sidebarFooterLinks && config.sidebarFooterLinks.length > 0 || config.goToWebsite ? /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-1 p-2", children: [
450
+ config.sidebarFooterLinks?.map((link) => /* @__PURE__ */ jsxs(
451
+ NavLink,
452
+ {
453
+ href: link.path,
454
+ isActive: currentPath === link.path,
455
+ className: cn(
456
+ "flex h-8 w-full items-center gap-2 rounded-lg px-2 text-sm text-sidebar-foreground transition-colors",
457
+ "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
458
+ currentPath === link.path && "bg-sidebar-accent text-sidebar-accent-foreground"
459
+ ),
460
+ children: [
461
+ link.icon && /* @__PURE__ */ jsx(link.icon, { className: "size-4 shrink-0" }),
462
+ /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: link.label })
463
+ ]
464
+ },
465
+ link.path
466
+ )),
467
+ config.goToWebsite && /* @__PURE__ */ jsxs(
468
+ "a",
469
+ {
470
+ href: config.goToWebsite.url,
471
+ target: websiteTarget,
472
+ rel: websiteTarget === "_blank" ? "noreferrer noopener" : void 0,
473
+ className: cn(
474
+ "flex h-8 w-full items-center gap-2 rounded-lg px-2 text-sm text-sidebar-foreground transition-colors",
475
+ "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground"
476
+ ),
477
+ children: [
478
+ /* @__PURE__ */ jsx(WebsiteIcon, { className: "size-4 shrink-0" }),
479
+ /* @__PURE__ */ jsx("span", { className: "flex-1 truncate", children: websiteLabel })
480
+ ]
481
+ }
482
+ )
483
+ ] }) : null,
484
+ user && /* @__PURE__ */ jsx("div", { className: "flex flex-col p-2", children: /* @__PURE__ */ jsxs("div", { className: "relative flex items-center gap-2 p-2", children: [
485
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col overflow-hidden", children: [
486
+ /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-semibold text-sidebar-foreground", children: user.name ?? user.email.split("@")[0] }),
487
+ /* @__PURE__ */ jsx("span", { className: "truncate text-xs text-muted-foreground", children: user.email })
488
+ ] }),
489
+ /* @__PURE__ */ jsx(
490
+ "button",
491
+ {
492
+ type: "button",
493
+ className: "size-4 shrink-0 text-muted-foreground hover:text-foreground",
494
+ onClick: () => setUserMenuOpen((v) => !v),
495
+ "aria-label": "User menu",
496
+ children: /* @__PURE__ */ jsx(EllipsisVertical, { className: "size-4" })
497
+ }
498
+ ),
499
+ userMenuOpen && /* @__PURE__ */ jsx("div", { className: "absolute bottom-full right-0 mb-1 w-40 rounded-lg border border-border bg-background shadow-sm py-1 z-50", children: onLogout && /* @__PURE__ */ jsx(
500
+ "button",
501
+ {
502
+ type: "button",
503
+ className: "w-full px-3 py-1.5 text-left text-sm text-foreground hover:bg-accent",
504
+ onClick: () => {
505
+ setUserMenuOpen(false);
506
+ onLogout();
507
+ },
508
+ children: "Log out"
509
+ }
510
+ ) })
511
+ ] }) })
512
+ ] });
513
+ }
514
+ function FilteredGroup({
515
+ group,
516
+ currentPath,
517
+ NavLink,
518
+ user
519
+ }) {
520
+ const visibleResources = group.resources.filter((r) => {
521
+ const hideIfUnauthorized = r.meta?.hideIfUnauthorized ?? true;
522
+ if (!hideIfUnauthorized) return true;
523
+ return canAccessResource(user, r.meta);
524
+ });
525
+ if (visibleResources.length === 0) return null;
526
+ return /* @__PURE__ */ jsxs("div", { className: "flex flex-col p-2", children: [
527
+ group.label && /* @__PURE__ */ jsx("div", { className: "mb-1 flex h-8 items-center px-2 opacity-70", children: /* @__PURE__ */ jsx("span", { className: "text-xs text-sidebar-foreground", children: group.label }) }),
528
+ /* @__PURE__ */ jsx("div", { className: "flex flex-col gap-1", children: visibleResources.map((resource) => /* @__PURE__ */ jsx(
529
+ SidebarItem,
530
+ {
531
+ resource,
532
+ label: resource.label,
533
+ path: resource.path,
534
+ icon: resource.icon,
535
+ isActive: currentPath === resource.path || currentPath?.startsWith(resource.path + "/"),
536
+ NavLink
537
+ },
538
+ resource.name
539
+ )) })
540
+ ] });
541
+ }
542
+ function SidebarToggle({ collapsed, onToggle }) {
543
+ return /* @__PURE__ */ jsx(
544
+ "button",
545
+ {
546
+ type: "button",
547
+ onClick: onToggle,
548
+ className: "flex size-7 items-center justify-center rounded-lg hover:bg-accent transition-colors",
549
+ "aria-label": collapsed ? "Expand sidebar" : "Collapse sidebar",
550
+ children: collapsed ? /* @__PURE__ */ jsx(ChevronRight, { className: "size-4" }) : /* @__PURE__ */ jsx(PanelLeft, { className: "size-4" })
551
+ }
552
+ );
553
+ }
554
+ function AppShell({
555
+ config,
556
+ currentPath,
557
+ NavLink,
558
+ headerActionLabel,
559
+ onHeaderAction,
560
+ children
561
+ }) {
562
+ const { logout } = useAuthContext();
563
+ const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
564
+ const allResources = config.resources ?? config.sidebarGroups?.flatMap((g) => g.resources) ?? [];
565
+ const activeResource = allResources.find(
566
+ (r) => currentPath === r.path || currentPath?.startsWith(r.path + "/")
567
+ );
568
+ const pageTitle = activeResource?.label ?? config.appName;
569
+ return /* @__PURE__ */ jsxs("div", { className: "flex h-screen w-full overflow-hidden bg-background", children: [
570
+ !sidebarCollapsed && /* @__PURE__ */ jsx(
571
+ Sidebar,
572
+ {
573
+ config,
574
+ currentPath,
575
+ NavLink,
576
+ onLogout: logout
577
+ }
578
+ ),
579
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-1 flex-col overflow-hidden min-w-0", children: [
580
+ /* @__PURE__ */ jsxs("header", { className: "flex shrink-0 items-center justify-between border-b border-border px-6 py-3", children: [
581
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
582
+ /* @__PURE__ */ jsx(
583
+ "button",
584
+ {
585
+ type: "button",
586
+ onClick: () => setSidebarCollapsed((v) => !v),
587
+ className: "flex size-7 items-center justify-center rounded-lg hover:bg-accent transition-colors",
588
+ "aria-label": "Toggle sidebar",
589
+ children: /* @__PURE__ */ jsx(PanelLeft, { className: "size-4 text-foreground" })
590
+ }
591
+ ),
592
+ /* @__PURE__ */ jsx("div", { className: "mx-2 h-[17px] w-px bg-border", "aria-hidden": true }),
593
+ /* @__PURE__ */ jsx("h1", { className: "text-base font-semibold text-foreground", children: pageTitle })
594
+ ] }),
595
+ headerActionLabel && onHeaderAction && /* @__PURE__ */ jsxs(
596
+ "button",
597
+ {
598
+ type: "button",
599
+ onClick: onHeaderAction,
600
+ className: cn(
601
+ "inline-flex items-center gap-1.5 rounded-lg bg-foreground px-2.5 py-1.5",
602
+ "text-sm font-medium text-background shadow-xs transition-opacity hover:opacity-90"
603
+ ),
604
+ children: [
605
+ /* @__PURE__ */ jsx(Plus, { className: "size-4" }),
606
+ headerActionLabel
607
+ ]
608
+ }
609
+ )
610
+ ] }),
611
+ /* @__PURE__ */ jsx("main", { className: "flex-1 overflow-auto p-6", children })
612
+ ] })
613
+ ] });
614
+ }
615
+ function BackofficeApp({
616
+ config,
617
+ authProvider,
618
+ NavLink,
619
+ currentPath,
620
+ loginPageProps,
621
+ renderLogin,
622
+ renderLoading,
623
+ headerActionLabel,
624
+ onHeaderAction,
625
+ onLoginSuccess,
626
+ children
627
+ }) {
628
+ return /* @__PURE__ */ jsx(AuthContextProvider, { authProvider, children: /* @__PURE__ */ jsx(
629
+ AuthGuard,
630
+ {
631
+ loginPageProps,
632
+ renderLogin,
633
+ renderLoading,
634
+ onLoginSuccess,
635
+ children: /* @__PURE__ */ jsx(
636
+ AppShell,
637
+ {
638
+ config,
639
+ currentPath,
640
+ NavLink,
641
+ headerActionLabel,
642
+ onHeaderAction,
643
+ children: children ?? config.children
644
+ }
645
+ )
646
+ }
647
+ ) });
648
+ }
649
+
650
+ // src/hooks/useAuth.ts
651
+ function useAuth() {
652
+ const { isLoading, isAuthenticated, user, error, login, logout, refreshUser } = useAuthContext();
653
+ return { isLoading, isAuthenticated, user, error, login, logout, refreshUser };
654
+ }
655
+
656
+ // src/hooks/usePermissions.ts
657
+ function usePermissions() {
658
+ const { user, can, canAny, canAll, hasRole } = useAuthContext();
659
+ return {
660
+ roles: user?.roles ?? [],
661
+ permissions: user?.permissions ?? [],
662
+ can,
663
+ canAny,
664
+ canAll,
665
+ hasRole
666
+ };
667
+ }
668
+ function AccessDenied({
669
+ message = "You do not have permission to access this page.",
670
+ actionLabel,
671
+ onAction
672
+ }) {
673
+ return /* @__PURE__ */ jsxs("div", { className: "flex min-h-full w-full flex-col items-center justify-center gap-6 p-10 text-center", children: [
674
+ /* @__PURE__ */ jsx("div", { className: "flex size-14 items-center justify-center rounded-full bg-muted", children: /* @__PURE__ */ jsx(ShieldOff, { className: "size-7 text-muted-foreground" }) }),
675
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-2", children: [
676
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold text-foreground", children: "Access Denied" }),
677
+ /* @__PURE__ */ jsx("p", { className: "max-w-sm text-sm text-muted-foreground", children: message })
678
+ ] }),
679
+ actionLabel && onAction && /* @__PURE__ */ jsx(Button, { variant: "outline", onClick: onAction, children: actionLabel })
680
+ ] });
681
+ }
682
+ function ResourceGuard({ meta, children, fallback }) {
683
+ const { user, isLoading } = useAuthContext();
684
+ if (isLoading) return null;
685
+ if (!canAccessResource(user, meta)) {
686
+ return fallback !== void 0 ? /* @__PURE__ */ jsx(Fragment, { children: fallback }) : /* @__PURE__ */ jsx(AccessDenied, {});
687
+ }
688
+ return /* @__PURE__ */ jsx(Fragment, { children });
689
+ }
690
+ function Can({ permission, role, permissions, children, fallback = null }) {
691
+ const { can, hasRole, canAll } = useAuthContext();
692
+ let granted = true;
693
+ if (permission) {
694
+ granted = can(permission);
695
+ } else if (role) {
696
+ granted = hasRole(role);
697
+ } else if (permissions && permissions.length > 0) {
698
+ granted = canAll(permissions);
699
+ }
700
+ return granted ? /* @__PURE__ */ jsx(Fragment, { children }) : /* @__PURE__ */ jsx(Fragment, { children: fallback });
701
+ }
702
+ var Select = SelectPrimitive.Root;
703
+ var SelectGroup = SelectPrimitive.Group;
704
+ var SelectValue = SelectPrimitive.Value;
705
+ var SelectTrigger = React11.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
706
+ SelectPrimitive.Trigger,
707
+ {
708
+ ref,
709
+ className: cn(
710
+ "flex h-9 w-full items-center justify-between rounded-lg border border-input bg-background px-3 py-2 text-base shadow-xs placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
711
+ className
712
+ ),
713
+ ...props,
714
+ children: [
715
+ children,
716
+ /* @__PURE__ */ jsx(SelectPrimitive.Icon, { asChild: true, children: /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4 opacity-50" }) })
717
+ ]
718
+ }
719
+ ));
720
+ SelectTrigger.displayName = SelectPrimitive.Trigger.displayName;
721
+ var SelectScrollUpButton = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
722
+ SelectPrimitive.ScrollUpButton,
723
+ {
724
+ ref,
725
+ className: cn(
726
+ "flex cursor-pointer items-center justify-center py-1",
727
+ className
728
+ ),
729
+ ...props,
730
+ children: /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4" })
731
+ }
732
+ ));
733
+ SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName;
734
+ var SelectScrollDownButton = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
735
+ SelectPrimitive.ScrollDownButton,
736
+ {
737
+ ref,
738
+ className: cn(
739
+ "flex cursor-pointer items-center justify-center py-1",
740
+ className
741
+ ),
742
+ ...props,
743
+ children: /* @__PURE__ */ jsx(ChevronDown, { className: "h-4 w-4" })
744
+ }
745
+ ));
746
+ SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName;
747
+ var SelectContent = React11.forwardRef(({ className, children, position = "popper", ...props }, ref) => /* @__PURE__ */ jsx(SelectPrimitive.Portal, { children: /* @__PURE__ */ jsx(
748
+ SelectPrimitive.Content,
749
+ {
750
+ ref,
751
+ className: cn(
752
+ "relative z-50 max-h-96 min-w-32 overflow-hidden rounded-lg border border-border bg-background shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
753
+ position === "popper" && "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
754
+ className
755
+ ),
756
+ position,
757
+ ...props,
758
+ children: /* @__PURE__ */ jsx(
759
+ SelectPrimitive.Viewport,
760
+ {
761
+ className: cn(
762
+ "p-1",
763
+ position === "popper" && "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
764
+ ),
765
+ children
766
+ }
767
+ )
768
+ }
769
+ ) }));
770
+ SelectContent.displayName = SelectPrimitive.Content.displayName;
771
+ var SelectItem = React11.forwardRef(({ className, children, ...props }, ref) => /* @__PURE__ */ jsxs(
772
+ SelectPrimitive.Item,
773
+ {
774
+ ref,
775
+ className: cn(
776
+ "relative flex w-full cursor-pointer select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-base outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
777
+ className
778
+ ),
779
+ ...props,
780
+ children: [
781
+ /* @__PURE__ */ jsx("span", { className: "absolute left-2 flex h-3.5 w-3.5 items-center justify-center", children: /* @__PURE__ */ jsx(SelectPrimitive.ItemIndicator, { children: /* @__PURE__ */ jsx(Check, { className: "h-4 w-4" }) }) }),
782
+ /* @__PURE__ */ jsx(SelectPrimitive.ItemText, { children })
783
+ ]
784
+ }
785
+ ));
786
+ SelectItem.displayName = SelectPrimitive.Item.displayName;
787
+ var SelectSeparator = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
788
+ SelectPrimitive.Separator,
789
+ {
790
+ ref,
791
+ className: cn("-mx-1 my-1 h-px bg-muted", className),
792
+ ...props
793
+ }
794
+ ));
795
+ SelectSeparator.displayName = SelectPrimitive.Separator.displayName;
796
+ var WIDTH_CLASS_MAP = {
797
+ sm: "max-w-md",
798
+ md: "max-w-lg",
799
+ lg: "max-w-2xl",
800
+ xl: "max-w-4xl",
801
+ "2xl": "max-w-6xl",
802
+ full: "max-w-[95vw]"
803
+ };
804
+ function RenderModal({
805
+ open,
806
+ onOpenChange,
807
+ title,
808
+ description,
809
+ children,
810
+ renderContent,
811
+ footer,
812
+ width = "lg",
813
+ closeOnOverlayClick = true,
814
+ showCloseButton = true,
815
+ overlayClassName,
816
+ panelClassName,
817
+ contentClassName
818
+ }) {
819
+ React11__default.useEffect(() => {
820
+ if (!open) return;
821
+ function handleEscape(event) {
822
+ if (event.key === "Escape") {
823
+ onOpenChange(false);
824
+ }
825
+ }
826
+ window.addEventListener("keydown", handleEscape);
827
+ return () => window.removeEventListener("keydown", handleEscape);
828
+ }, [open, onOpenChange]);
829
+ if (!open) return null;
830
+ const hasHeader = Boolean(title || description || showCloseButton);
831
+ const content = renderContent ? renderContent() : children;
832
+ return createPortal(
833
+ /* @__PURE__ */ jsx(
834
+ "div",
835
+ {
836
+ className: cn(
837
+ "fixed inset-0 z-[2147483647] flex items-center justify-center bg-black/30 p-4 backdrop-blur-[1px]",
838
+ overlayClassName
839
+ ),
840
+ onMouseDown: (event) => {
841
+ if (!closeOnOverlayClick) return;
842
+ if (event.target === event.currentTarget) {
843
+ onOpenChange(false);
844
+ }
845
+ },
846
+ children: /* @__PURE__ */ jsxs(
847
+ "div",
848
+ {
849
+ role: "dialog",
850
+ "aria-modal": "true",
851
+ className: cn(
852
+ "w-full overflow-hidden rounded-xl border border-border bg-background shadow-xl",
853
+ WIDTH_CLASS_MAP[width],
854
+ panelClassName
855
+ ),
856
+ children: [
857
+ hasHeader && /* @__PURE__ */ jsxs("header", { className: "flex items-start justify-between gap-4 px-5 py-4", children: [
858
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
859
+ title && /* @__PURE__ */ jsx("h2", { className: "truncate text-lg font-semibold text-foreground", children: title }),
860
+ description && /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-muted-foreground", children: description })
861
+ ] }),
862
+ showCloseButton && /* @__PURE__ */ jsx(
863
+ "button",
864
+ {
865
+ type: "button",
866
+ "aria-label": "Close modal",
867
+ className: "inline-flex size-8 shrink-0 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
868
+ onClick: () => onOpenChange(false),
869
+ children: /* @__PURE__ */ jsx(X, { className: "size-4" })
870
+ }
871
+ )
872
+ ] }),
873
+ /* @__PURE__ */ jsx(
874
+ "div",
875
+ {
876
+ className: cn(
877
+ "max-h-[80vh] overflow-auto px-5 py-4",
878
+ contentClassName
879
+ ),
880
+ children: content
881
+ }
882
+ ),
883
+ footer && /* @__PURE__ */ jsx("footer", { className: "px-5 py-4", children: footer })
884
+ ]
885
+ }
886
+ )
887
+ }
888
+ ),
889
+ document.body
890
+ );
891
+ }
892
+ var DEFAULT_ACCENT_OPTIONS = [
893
+ { value: "neutral", label: "Neutral", previewClassName: "bg-foreground" },
894
+ { value: "blue", label: "Blue", previewClassName: "bg-blue-600" },
895
+ { value: "green", label: "Green", previewClassName: "bg-emerald-600" },
896
+ { value: "orange", label: "Orange", previewClassName: "bg-orange-500" }
897
+ ];
898
+ function SettingsModal({
899
+ open,
900
+ onOpenChange,
901
+ mode,
902
+ onModeChange,
903
+ accent,
904
+ onAccentChange,
905
+ accentOptions = DEFAULT_ACCENT_OPTIONS,
906
+ personalizationContent,
907
+ usersContent,
908
+ defaultSection = "general",
909
+ width = "xl",
910
+ closeOnOverlayClick = true,
911
+ overlayClassName,
912
+ panelClassName,
913
+ contentClassName,
914
+ sidebarClassName,
915
+ bodyClassName
916
+ }) {
917
+ const [section, setSection] = React11__default.useState(defaultSection);
918
+ React11__default.useEffect(() => {
919
+ if (open) {
920
+ setSection(defaultSection);
921
+ }
922
+ }, [open, defaultSection]);
923
+ if (!open) return null;
924
+ return /* @__PURE__ */ jsx(
925
+ RenderModal,
926
+ {
927
+ open,
928
+ onOpenChange,
929
+ width,
930
+ closeOnOverlayClick,
931
+ showCloseButton: false,
932
+ overlayClassName,
933
+ contentClassName: cn(
934
+ "relative h-full overflow-hidden p-0",
935
+ contentClassName
936
+ ),
937
+ panelClassName: cn("h-[500px] max-w-[800px]", panelClassName),
938
+ children: /* @__PURE__ */ jsxs("div", { className: "flex h-full", children: [
939
+ /* @__PURE__ */ jsx(
940
+ "button",
941
+ {
942
+ type: "button",
943
+ onClick: () => onOpenChange(false),
944
+ className: "absolute right-4 top-4 z-10 inline-flex size-8 items-center justify-center rounded-md text-foreground/70 transition-colors hover:bg-accent hover:text-foreground",
945
+ "aria-label": "Close settings",
946
+ children: /* @__PURE__ */ jsx(X, { className: "size-4" })
947
+ }
948
+ ),
949
+ /* @__PURE__ */ jsxs(
950
+ "aside",
951
+ {
952
+ className: cn(
953
+ "flex h-full w-64 shrink-0 flex-col gap-1 overflow-y-auto bg-sidebar p-2",
954
+ sidebarClassName
955
+ ),
956
+ children: [
957
+ /* @__PURE__ */ jsx(
958
+ SettingsSectionButton,
959
+ {
960
+ icon: Settings,
961
+ label: "General",
962
+ active: section === "general",
963
+ onClick: () => setSection("general")
964
+ }
965
+ ),
966
+ /* @__PURE__ */ jsx(
967
+ SettingsSectionButton,
968
+ {
969
+ icon: CircleDot,
970
+ label: "Personalization",
971
+ active: section === "personalization",
972
+ onClick: () => setSection("personalization")
973
+ }
974
+ ),
975
+ /* @__PURE__ */ jsx(
976
+ SettingsSectionButton,
977
+ {
978
+ icon: UserCog,
979
+ label: "Users",
980
+ active: section === "users",
981
+ onClick: () => setSection("users")
982
+ }
983
+ )
984
+ ]
985
+ }
986
+ ),
987
+ /* @__PURE__ */ jsxs("div", { className: cn("flex min-w-0 flex-1 flex-col", bodyClassName), children: [
988
+ /* @__PURE__ */ jsx("header", { className: "flex h-16 items-center pr-12", children: /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 px-4", children: [
989
+ /* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: "Settings" }),
990
+ /* @__PURE__ */ jsx(ChevronRight, { className: "size-3.5 text-muted-foreground" }),
991
+ /* @__PURE__ */ jsx("span", { className: "text-sm text-foreground capitalize", children: section })
992
+ ] }) }),
993
+ /* @__PURE__ */ jsxs("div", { className: "flex min-h-0 flex-1 flex-col overflow-y-auto px-4 pb-4", children: [
994
+ /* @__PURE__ */ jsx("div", { className: "h-px w-full bg-border" }),
995
+ section === "general" && /* @__PURE__ */ jsxs("div", { className: "flex flex-col", children: [
996
+ /* @__PURE__ */ jsx(SettingsRow, { label: "Appearance", children: /* @__PURE__ */ jsxs("div", { className: "relative inline-flex items-center", children: [
997
+ /* @__PURE__ */ jsxs(
998
+ "select",
999
+ {
1000
+ value: mode,
1001
+ onChange: (e) => onModeChange(e.target.value),
1002
+ className: "h-8 appearance-none rounded-lg px-2.5 pr-7 text-sm font-medium text-foreground outline-none transition-colors hover:bg-accent",
1003
+ children: [
1004
+ /* @__PURE__ */ jsx("option", { value: "light", children: "Light" }),
1005
+ /* @__PURE__ */ jsx("option", { value: "dark", children: "Dark" })
1006
+ ]
1007
+ }
1008
+ ),
1009
+ /* @__PURE__ */ jsx(ChevronDown, { className: "pointer-events-none absolute right-2 size-4 text-foreground" })
1010
+ ] }) }),
1011
+ /* @__PURE__ */ jsx(SettingsRow, { label: "Accent color", children: /* @__PURE__ */ jsxs("div", { className: "relative inline-flex items-center", children: [
1012
+ /* @__PURE__ */ jsx(
1013
+ "select",
1014
+ {
1015
+ value: accent,
1016
+ onChange: (e) => onAccentChange(e.target.value),
1017
+ className: "h-8 appearance-none rounded-lg pl-8 pr-7 text-sm font-medium text-foreground outline-none transition-colors hover:bg-accent",
1018
+ children: accentOptions.map((option) => /* @__PURE__ */ jsx("option", { value: option.value, children: option.label }, option.value))
1019
+ }
1020
+ ),
1021
+ /* @__PURE__ */ jsx(
1022
+ "span",
1023
+ {
1024
+ className: cn(
1025
+ "pointer-events-none absolute left-2.5 inline-flex size-2 rounded-full",
1026
+ accentOptions.find((o) => o.value === accent)?.previewClassName ?? "bg-foreground"
1027
+ )
1028
+ }
1029
+ ),
1030
+ /* @__PURE__ */ jsx(ChevronDown, { className: "pointer-events-none absolute right-2 size-4 text-foreground" })
1031
+ ] }) })
1032
+ ] }),
1033
+ section === "personalization" && /* @__PURE__ */ jsx("div", { className: "pt-3", children: personalizationContent }),
1034
+ section === "users" && /* @__PURE__ */ jsx("div", { className: "pt-3", children: usersContent })
1035
+ ] })
1036
+ ] })
1037
+ ] })
1038
+ }
1039
+ );
1040
+ }
1041
+ function SettingsSectionButton({
1042
+ icon: Icon2,
1043
+ label,
1044
+ active,
1045
+ onClick
1046
+ }) {
1047
+ return /* @__PURE__ */ jsxs(
1048
+ "button",
1049
+ {
1050
+ type: "button",
1051
+ onClick,
1052
+ className: cn(
1053
+ "flex h-8 w-full items-center gap-2 rounded-md px-2 text-left text-sm text-sidebar-foreground",
1054
+ "transition-colors hover:bg-sidebar-accent",
1055
+ active && "bg-sidebar-accent font-medium"
1056
+ ),
1057
+ children: [
1058
+ /* @__PURE__ */ jsx(Icon2, { className: "size-4 shrink-0" }),
1059
+ /* @__PURE__ */ jsx("span", { className: "truncate", children: label })
1060
+ ]
1061
+ }
1062
+ );
1063
+ }
1064
+ function SettingsRow({ label, children }) {
1065
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1066
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between py-3", children: [
1067
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-foreground", children: label }),
1068
+ /* @__PURE__ */ jsx("div", { className: "flex items-center", children })
1069
+ ] }),
1070
+ /* @__PURE__ */ jsx("div", { className: "h-px w-full bg-border" })
1071
+ ] });
1072
+ }
1073
+ function TableActionMenu({
1074
+ items,
1075
+ ariaLabel = "Open actions"
1076
+ }) {
1077
+ const [open, setOpen] = React11__default.useState(false);
1078
+ const [confirmingItem, setConfirmingItem] = React11__default.useState(null);
1079
+ const [menuPosition, setMenuPosition] = React11__default.useState({ top: 0, left: 0 });
1080
+ const dropdownRef = React11__default.useRef(null);
1081
+ const triggerRef = React11__default.useRef(null);
1082
+ const getConfirmOptions = React11__default.useCallback(
1083
+ (item) => {
1084
+ if (item.confirm === false) return null;
1085
+ if (typeof item.confirm === "object") return item.confirm;
1086
+ if (item.confirm || item.variant === "destructive") {
1087
+ return {};
1088
+ }
1089
+ return null;
1090
+ },
1091
+ []
1092
+ );
1093
+ const confirmOptions = confirmingItem ? getConfirmOptions(confirmingItem) : null;
1094
+ const confirmTitle = confirmOptions?.title ?? (confirmingItem?.variant === "destructive" ? "Confirm deletion" : "Confirm action");
1095
+ const confirmDescription = confirmOptions?.description ?? (confirmingItem?.variant === "destructive" ? "This action cannot be undone." : "Please confirm that you want to continue.");
1096
+ const confirmLabel = confirmOptions?.confirmLabel ?? "OK";
1097
+ const cancelLabel = confirmOptions?.cancelLabel ?? "Cancel";
1098
+ const handleItemSelect = React11__default.useCallback(
1099
+ (item) => {
1100
+ const nextConfirmOptions = getConfirmOptions(item);
1101
+ if (nextConfirmOptions) {
1102
+ setConfirmingItem(item);
1103
+ setOpen(false);
1104
+ return;
1105
+ }
1106
+ item.onSelect();
1107
+ setOpen(false);
1108
+ },
1109
+ [getConfirmOptions]
1110
+ );
1111
+ const handleConfirm = React11__default.useCallback(() => {
1112
+ if (!confirmingItem) return;
1113
+ confirmingItem.onSelect();
1114
+ setConfirmingItem(null);
1115
+ }, [confirmingItem]);
1116
+ const updateMenuPosition = React11__default.useCallback(() => {
1117
+ if (!triggerRef.current) return;
1118
+ const rect = triggerRef.current.getBoundingClientRect();
1119
+ setMenuPosition({
1120
+ top: rect.bottom + 4,
1121
+ left: rect.right
1122
+ });
1123
+ }, []);
1124
+ React11__default.useEffect(() => {
1125
+ function handleOutsideClick(event) {
1126
+ const target = event.target;
1127
+ if (dropdownRef.current?.contains(target)) return;
1128
+ if (triggerRef.current?.contains(target)) return;
1129
+ setOpen(false);
1130
+ }
1131
+ function handleEscape(event) {
1132
+ if (event.key === "Escape") {
1133
+ if (confirmingItem) {
1134
+ setConfirmingItem(null);
1135
+ return;
1136
+ }
1137
+ setOpen(false);
1138
+ }
1139
+ }
1140
+ if (open || confirmingItem) {
1141
+ updateMenuPosition();
1142
+ document.addEventListener("mousedown", handleOutsideClick);
1143
+ window.addEventListener("resize", updateMenuPosition);
1144
+ window.addEventListener("scroll", updateMenuPosition, true);
1145
+ document.addEventListener("keydown", handleEscape);
1146
+ }
1147
+ return () => {
1148
+ document.removeEventListener("mousedown", handleOutsideClick);
1149
+ window.removeEventListener("resize", updateMenuPosition);
1150
+ window.removeEventListener("scroll", updateMenuPosition, true);
1151
+ document.removeEventListener("keydown", handleEscape);
1152
+ };
1153
+ }, [confirmingItem, open, updateMenuPosition]);
1154
+ const visibleItems = items.filter(Boolean);
1155
+ if (visibleItems.length === 0) return null;
1156
+ return /* @__PURE__ */ jsxs("div", { className: "relative inline-flex", children: [
1157
+ /* @__PURE__ */ jsx(
1158
+ "button",
1159
+ {
1160
+ ref: triggerRef,
1161
+ type: "button",
1162
+ onClick: () => setOpen((v) => !v),
1163
+ className: "inline-flex size-8 items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground",
1164
+ "aria-haspopup": "menu",
1165
+ "aria-expanded": open,
1166
+ "aria-label": ariaLabel,
1167
+ children: /* @__PURE__ */ jsx(MoreHorizontal, { className: "size-4" })
1168
+ }
1169
+ ),
1170
+ open && createPortal(
1171
+ /* @__PURE__ */ jsx(
1172
+ "div",
1173
+ {
1174
+ ref: dropdownRef,
1175
+ role: "menu",
1176
+ className: "fixed z-[2147483647] min-w-[120px] -translate-x-full rounded-lg border border-border bg-popover p-1 shadow-md",
1177
+ style: { top: menuPosition.top, left: menuPosition.left },
1178
+ children: visibleItems.map((item) => /* @__PURE__ */ jsx(
1179
+ "button",
1180
+ {
1181
+ type: "button",
1182
+ role: "menuitem",
1183
+ disabled: item.disabled,
1184
+ onClick: () => handleItemSelect(item),
1185
+ className: cn(
1186
+ "flex w-full items-center rounded-md px-2 py-1.5 text-left text-sm transition-colors",
1187
+ "hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50",
1188
+ item.variant === "destructive" ? "text-destructive" : "text-popover-foreground"
1189
+ ),
1190
+ children: item.label
1191
+ },
1192
+ item.key
1193
+ ))
1194
+ }
1195
+ ),
1196
+ document.body
1197
+ ),
1198
+ confirmingItem && createPortal(
1199
+ /* @__PURE__ */ jsx("div", { className: "fixed inset-0 z-[2147483647] flex items-center justify-center bg-neutral-950/30 p-4 backdrop-blur-[1.5px]", children: /* @__PURE__ */ jsxs("div", { className: "w-full max-w-md rounded-xl border border-border bg-background p-6 shadow-lg", children: [
1200
+ /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
1201
+ /* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold text-foreground", children: confirmTitle }),
1202
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: confirmDescription })
1203
+ ] }),
1204
+ /* @__PURE__ */ jsxs("div", { className: "mt-6 flex justify-end gap-2", children: [
1205
+ /* @__PURE__ */ jsx(
1206
+ Button,
1207
+ {
1208
+ variant: "outline",
1209
+ onClick: () => setConfirmingItem(null),
1210
+ children: cancelLabel
1211
+ }
1212
+ ),
1213
+ /* @__PURE__ */ jsx(
1214
+ Button,
1215
+ {
1216
+ variant: confirmingItem.variant === "destructive" ? "destructive" : "default",
1217
+ onClick: handleConfirm,
1218
+ children: confirmLabel
1219
+ }
1220
+ )
1221
+ ] })
1222
+ ] }) }),
1223
+ document.body
1224
+ )
1225
+ ] });
1226
+ }
1227
+ var toneClasses = {
1228
+ neutral: "bg-secondary text-secondary-foreground",
1229
+ success: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-300",
1230
+ danger: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-300",
1231
+ warning: "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-300"
1232
+ };
1233
+ function TableBadge({
1234
+ children,
1235
+ tone = "neutral",
1236
+ className
1237
+ }) {
1238
+ return /* @__PURE__ */ jsx(
1239
+ "span",
1240
+ {
1241
+ className: cn(
1242
+ "inline-flex items-center rounded-full px-2 py-0.5 text-xs font-medium",
1243
+ toneClasses[tone],
1244
+ className
1245
+ ),
1246
+ children
1247
+ }
1248
+ );
1249
+ }
1250
+ var Table = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("div", { className: "relative w-full overflow-auto", children: /* @__PURE__ */ jsx(
1251
+ "table",
1252
+ {
1253
+ ref,
1254
+ className: cn("w-full caption-bottom text-sm bg-background", className),
1255
+ ...props
1256
+ }
1257
+ ) }));
1258
+ Table.displayName = "Table";
1259
+ var TableHeader = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1260
+ "thead",
1261
+ {
1262
+ ref,
1263
+ className: cn("bg-muted/50 [&_tr]:border-b", className),
1264
+ ...props
1265
+ }
1266
+ ));
1267
+ TableHeader.displayName = "TableHeader";
1268
+ var TableBody = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1269
+ "tbody",
1270
+ {
1271
+ ref,
1272
+ className: cn("[&_tr:last-child]:border-0", className),
1273
+ ...props
1274
+ }
1275
+ ));
1276
+ TableBody.displayName = "TableBody";
1277
+ var TableFooter = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1278
+ "tfoot",
1279
+ {
1280
+ ref,
1281
+ className: cn(
1282
+ "border-t bg-muted/50 font-medium [&>tr]:last:border-b-0",
1283
+ className
1284
+ ),
1285
+ ...props
1286
+ }
1287
+ ));
1288
+ TableFooter.displayName = "TableFooter";
1289
+ var TableRow = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1290
+ "tr",
1291
+ {
1292
+ ref,
1293
+ className: cn("h-[53px] border-b transition-colors hover:bg-muted/30 data-[state=selected]:bg-muted", className),
1294
+ ...props
1295
+ }
1296
+ ));
1297
+ TableRow.displayName = "TableRow";
1298
+ var TableHead = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1299
+ "th",
1300
+ {
1301
+ ref,
1302
+ className: cn(
1303
+ "h-10 px-2 text-left align-middle text-sm font-medium text-foreground",
1304
+ className
1305
+ ),
1306
+ ...props
1307
+ }
1308
+ ));
1309
+ TableHead.displayName = "TableHead";
1310
+ var TableCell = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1311
+ "td",
1312
+ {
1313
+ ref,
1314
+ className: cn("px-2 py-2 align-middle text-sm", className),
1315
+ ...props
1316
+ }
1317
+ ));
1318
+ TableCell.displayName = "TableCell";
1319
+ var TableCaption = React11.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx(
1320
+ "caption",
1321
+ {
1322
+ ref,
1323
+ className: cn("mt-4 text-sm text-muted-foreground", className),
1324
+ ...props
1325
+ }
1326
+ ));
1327
+ TableCaption.displayName = "TableCaption";
1328
+ function SortableDataTableRow({
1329
+ row,
1330
+ rowId
1331
+ }) {
1332
+ const {
1333
+ attributes,
1334
+ listeners,
1335
+ setNodeRef,
1336
+ transform,
1337
+ transition,
1338
+ isDragging
1339
+ } = useSortable({ id: rowId });
1340
+ const verticalOnlyTransform = transform ? {
1341
+ ...transform,
1342
+ x: 0
1343
+ } : null;
1344
+ return /* @__PURE__ */ jsx(
1345
+ TableRow,
1346
+ {
1347
+ ref: setNodeRef,
1348
+ "data-state": row.getIsSelected() && "selected",
1349
+ className: "cursor-grab active:cursor-grabbing",
1350
+ style: {
1351
+ transform: CSS.Transform.toString(verticalOnlyTransform),
1352
+ transition,
1353
+ opacity: isDragging ? 0.5 : 1,
1354
+ position: "relative",
1355
+ zIndex: isDragging ? 1 : 0,
1356
+ willChange: "transform",
1357
+ touchAction: "none"
1358
+ },
1359
+ ...attributes,
1360
+ ...listeners,
1361
+ children: row.getVisibleCells().map((cell) => /* @__PURE__ */ jsx(TableCell, { children: flexRender(cell.column.columnDef.cell, cell.getContext()) }, cell.id))
1362
+ }
1363
+ );
1364
+ }
1365
+ function DataTable({
1366
+ columns,
1367
+ data,
1368
+ className,
1369
+ emptyMessage = "No results.",
1370
+ rowDragOptions
1371
+ }) {
1372
+ const sensors = useSensors(
1373
+ useSensor(PointerSensor, {
1374
+ activationConstraint: {
1375
+ distance: 6
1376
+ }
1377
+ }),
1378
+ useSensor(KeyboardSensor, {
1379
+ coordinateGetter: sortableKeyboardCoordinates
1380
+ })
1381
+ );
1382
+ const table = useReactTable({
1383
+ data,
1384
+ columns,
1385
+ getCoreRowModel: getCoreRowModel()
1386
+ });
1387
+ const rowIds = React11.useMemo(
1388
+ () => rowDragOptions ? data.map((row) => rowDragOptions.getRowId(row)) : [],
1389
+ [data, rowDragOptions]
1390
+ );
1391
+ const handleDragEnd = React11.useCallback(
1392
+ (event) => {
1393
+ const { active, over } = event;
1394
+ if (!rowDragOptions || !over || active.id === over.id) {
1395
+ return;
1396
+ }
1397
+ const oldIndex = data.findIndex(
1398
+ (row) => rowDragOptions.getRowId(row) === String(active.id)
1399
+ );
1400
+ const newIndex = data.findIndex(
1401
+ (row) => rowDragOptions.getRowId(row) === String(over.id)
1402
+ );
1403
+ if (oldIndex < 0 || newIndex < 0 || oldIndex === newIndex) {
1404
+ return;
1405
+ }
1406
+ rowDragOptions.onReorder(arrayMove(data, oldIndex, newIndex));
1407
+ },
1408
+ [data, rowDragOptions]
1409
+ );
1410
+ return /* @__PURE__ */ jsx(
1411
+ DndContext,
1412
+ {
1413
+ sensors: rowDragOptions ? sensors : void 0,
1414
+ collisionDetection: closestCenter,
1415
+ onDragEnd: rowDragOptions ? handleDragEnd : void 0,
1416
+ children: /* @__PURE__ */ jsx("div", { className: cn("rounded-md border", className), children: /* @__PURE__ */ jsxs(Table, { children: [
1417
+ /* @__PURE__ */ jsx(TableHeader, { children: table.getHeaderGroups().map((headerGroup) => /* @__PURE__ */ jsx(TableRow, { children: headerGroup.headers.map((header) => /* @__PURE__ */ jsx(TableHead, { children: header.isPlaceholder ? null : flexRender(
1418
+ header.column.columnDef.header,
1419
+ header.getContext()
1420
+ ) }, header.id)) }, headerGroup.id)) }),
1421
+ /* @__PURE__ */ jsx(TableBody, { children: table.getRowModel().rows.length ? rowDragOptions ? /* @__PURE__ */ jsx(
1422
+ SortableContext,
1423
+ {
1424
+ items: rowIds,
1425
+ strategy: verticalListSortingStrategy,
1426
+ children: table.getRowModel().rows.map((row) => {
1427
+ const rowId = rowDragOptions.getRowId(row.original);
1428
+ return /* @__PURE__ */ jsx(
1429
+ SortableDataTableRow,
1430
+ {
1431
+ row,
1432
+ rowId
1433
+ },
1434
+ rowId
1435
+ );
1436
+ })
1437
+ }
1438
+ ) : table.getRowModel().rows.map((row) => /* @__PURE__ */ jsx(
1439
+ TableRow,
1440
+ {
1441
+ "data-state": row.getIsSelected() && "selected",
1442
+ children: row.getVisibleCells().map((cell) => /* @__PURE__ */ jsx(TableCell, { children: flexRender(
1443
+ cell.column.columnDef.cell,
1444
+ cell.getContext()
1445
+ ) }, cell.id))
1446
+ },
1447
+ row.id
1448
+ )) : /* @__PURE__ */ jsx(TableRow, { children: /* @__PURE__ */ jsx(
1449
+ TableCell,
1450
+ {
1451
+ colSpan: columns.length,
1452
+ className: "h-24 text-center",
1453
+ children: emptyMessage
1454
+ }
1455
+ ) }) })
1456
+ ] }) })
1457
+ }
1458
+ );
1459
+ }
1460
+ function TablePagination({
1461
+ totalRows,
1462
+ selectedRows,
1463
+ page,
1464
+ pageSize,
1465
+ pageSizeOptions = [10, 20, 50, 100, "all"],
1466
+ onPageChange,
1467
+ onPageSizeChange,
1468
+ className
1469
+ }) {
1470
+ const safePageSize = Math.max(1, pageSize);
1471
+ const totalPages = Math.max(1, Math.ceil(totalRows / safePageSize));
1472
+ const safePage = Math.min(Math.max(1, page), totalPages);
1473
+ const canGoPrev = safePage > 1;
1474
+ const canGoNext = safePage < totalPages;
1475
+ const hasAllOption = pageSizeOptions.includes("all");
1476
+ const currentPageSizeValue = hasAllOption && totalRows > 0 && safePageSize === totalRows ? "all" : String(safePageSize);
1477
+ const selectedCount = selectedRows ?? 0;
1478
+ const rowsText = `${selectedCount} of ${totalRows} row(s) selected.`;
1479
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex h-9 w-full items-center justify-between gap-2", className), children: [
1480
+ /* @__PURE__ */ jsx("p", { className: "text-sm text-muted-foreground", children: rowsText }),
1481
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-8 text-foreground", children: [
1482
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1483
+ /* @__PURE__ */ jsx("span", { className: "pr-2 text-sm font-medium text-foreground", children: "Rows per page" }),
1484
+ /* @__PURE__ */ jsx(
1485
+ "select",
1486
+ {
1487
+ value: currentPageSizeValue,
1488
+ onChange: (event) => {
1489
+ const nextValue = event.target.value;
1490
+ if (nextValue === "all") {
1491
+ onPageSizeChange(Math.max(1, totalRows));
1492
+ return;
1493
+ }
1494
+ onPageSizeChange(Number(nextValue));
1495
+ },
1496
+ className: "h-9 w-20 rounded-md border border-input bg-background px-3 text-sm shadow-sm",
1497
+ "aria-label": "Rows per page",
1498
+ children: pageSizeOptions.map((option) => /* @__PURE__ */ jsx("option", { value: String(option), children: option === "all" ? "All" : option }, String(option)))
1499
+ }
1500
+ )
1501
+ ] }),
1502
+ /* @__PURE__ */ jsxs("span", { className: "pr-2 text-sm font-medium", children: [
1503
+ "Page ",
1504
+ safePage,
1505
+ " of ",
1506
+ totalPages
1507
+ ] }),
1508
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
1509
+ /* @__PURE__ */ jsx(
1510
+ IconButton,
1511
+ {
1512
+ label: "Previous page",
1513
+ disabled: !canGoPrev,
1514
+ onClick: () => onPageChange(safePage - 1),
1515
+ icon: /* @__PURE__ */ jsx(ChevronLeft, { className: "size-4" })
1516
+ }
1517
+ ),
1518
+ /* @__PURE__ */ jsx(
1519
+ IconButton,
1520
+ {
1521
+ label: "First page",
1522
+ disabled: !canGoPrev,
1523
+ onClick: () => onPageChange(1),
1524
+ icon: /* @__PURE__ */ jsx(ChevronsLeft, { className: "size-4" })
1525
+ }
1526
+ ),
1527
+ /* @__PURE__ */ jsx(
1528
+ IconButton,
1529
+ {
1530
+ label: "Next page",
1531
+ disabled: !canGoNext,
1532
+ onClick: () => onPageChange(safePage + 1),
1533
+ icon: /* @__PURE__ */ jsx(ChevronRight, { className: "size-4" })
1534
+ }
1535
+ ),
1536
+ /* @__PURE__ */ jsx(
1537
+ IconButton,
1538
+ {
1539
+ label: "Last page",
1540
+ disabled: !canGoNext,
1541
+ onClick: () => onPageChange(totalPages),
1542
+ icon: /* @__PURE__ */ jsx(ChevronsRight, { className: "size-4" })
1543
+ }
1544
+ )
1545
+ ] })
1546
+ ] })
1547
+ ] });
1548
+ }
1549
+ function IconButton({ label, icon, disabled, onClick }) {
1550
+ return /* @__PURE__ */ jsx(
1551
+ "button",
1552
+ {
1553
+ type: "button",
1554
+ onClick,
1555
+ disabled,
1556
+ "aria-label": label,
1557
+ className: "inline-flex size-8 items-center justify-center rounded-md border border-input bg-background text-foreground shadow-sm transition-colors hover:bg-accent disabled:cursor-not-allowed disabled:opacity-50",
1558
+ children: icon
1559
+ }
1560
+ );
1561
+ }
1562
+
1563
+ export { AccessDenied as SPC_AccessDenied, AppShell as SPC_AppShell, AuthContext as SPC_AuthContext, AuthContextProvider as SPC_AuthContextProvider, AuthGuard as SPC_AuthGuard, BackofficeApp as SPC_BackofficeApp, Button as SPC_Button, Can as SPC_Can, DataTable as SPC_DataTable, Input as SPC_Input, Label as SPC_Label, LoadingScreen as SPC_LoadingScreen, LoginPage as SPC_LoginPage, RenderModal as SPC_RenderModal, ResourceGuard as SPC_ResourceGuard, Select as SPC_Select, SelectContent as SPC_SelectContent, SelectGroup as SPC_SelectGroup, SelectItem as SPC_SelectItem, SelectScrollDownButton as SPC_SelectScrollDownButton, SelectScrollUpButton as SPC_SelectScrollUpButton, SelectSeparator as SPC_SelectSeparator, SelectTrigger as SPC_SelectTrigger, SelectValue as SPC_SelectValue, SettingsModal as SPC_SettingsModal, Sidebar as SPC_Sidebar, SidebarToggle as SPC_SidebarToggle, Table as SPC_Table, TableActionMenu as SPC_TableActionMenu, TableBadge as SPC_TableBadge, TableBody as SPC_TableBody, TableCaption as SPC_TableCaption, TableCell as SPC_TableCell, TableFooter as SPC_TableFooter, TableHead as SPC_TableHead, TableHeader as SPC_TableHeader, TablePagination as SPC_TablePagination, TableRow as SPC_TableRow, buttonVariants as SPC_buttonVariants, canAccessResource as SPC_canAccessResource, cn as SPC_cn, evaluateCan as SPC_evaluateCan, evaluateCanAll as SPC_evaluateCanAll, evaluateCanAny as SPC_evaluateCanAny, evaluateHasRole as SPC_evaluateHasRole, useAuth as SPC_useAuth, useAuthContext as SPC_useAuthContext, usePermissions as SPC_usePermissions };
1564
+ //# sourceMappingURL=index.js.map
1565
+ //# sourceMappingURL=index.js.map