@shellui/core 0.2.0 → 0.3.0-beta.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.
Files changed (111) hide show
  1. package/package.json +9 -4
  2. package/src/app.tsx +12 -9
  3. package/src/components/ui/badge.tsx +35 -0
  4. package/src/components/ui/dropdown-menu.tsx +94 -0
  5. package/src/components/ui/sidebar.tsx +1 -1
  6. package/src/constants/urls.ts +8 -0
  7. package/src/features/admin/AdminView.tsx +154 -0
  8. package/src/features/admin/components/AdminForbiddenAccess.tsx +10 -0
  9. package/src/features/auth/AuthProvider.tsx +464 -0
  10. package/src/features/auth/backends/index.ts +41 -0
  11. package/src/features/auth/backends/shellui.ts +278 -0
  12. package/src/features/auth/backends/supabase.ts +300 -0
  13. package/src/features/auth/backends/types.ts +30 -0
  14. package/src/features/auth/components/LoginButton.tsx +360 -0
  15. package/src/features/auth/components/LoginButtonIcons.tsx +48 -0
  16. package/src/features/auth/components/LoginView.tsx +721 -0
  17. package/src/features/auth/components/OAuthCallbackView.tsx +119 -0
  18. package/src/features/auth/hooks/useAuth.tsx +37 -0
  19. package/src/features/auth/types.ts +51 -0
  20. package/src/features/auth/utils/buildSessionFromParams.spec.ts +61 -0
  21. package/src/features/auth/utils/buildSessionFromParams.ts +79 -0
  22. package/src/features/auth/utils/clearStoredAuthSession.spec.ts +23 -0
  23. package/src/features/auth/utils/clearStoredAuthSession.ts +10 -0
  24. package/src/features/auth/utils/clientLoginContext.spec.ts +83 -0
  25. package/src/features/auth/utils/clientLoginContext.ts +89 -0
  26. package/src/features/auth/utils/decodeJwtPayload.spec.ts +17 -0
  27. package/src/features/auth/utils/decodeJwtPayload.ts +24 -0
  28. package/src/features/auth/utils/formatProviderLabel.spec.ts +19 -0
  29. package/src/features/auth/utils/formatProviderLabel.ts +11 -0
  30. package/src/features/auth/utils/getOAuthProviderCandidates.spec.ts +16 -0
  31. package/src/features/auth/utils/getOAuthProviderCandidates.ts +15 -0
  32. package/src/features/auth/utils/getPreferredBackendProvider.spec.ts +15 -0
  33. package/src/features/auth/utils/getPreferredBackendProvider.ts +10 -0
  34. package/src/features/auth/utils/getProviderVisual.spec.ts +30 -0
  35. package/src/features/auth/utils/getProviderVisual.ts +83 -0
  36. package/src/features/auth/utils/getUserFromSdkSettings.spec.ts +32 -0
  37. package/src/features/auth/utils/getUserFromSdkSettings.ts +13 -0
  38. package/src/features/auth/utils/index.ts +21 -0
  39. package/src/features/auth/utils/isLoginMethod.spec.ts +18 -0
  40. package/src/features/auth/utils/isLoginMethod.ts +5 -0
  41. package/src/features/auth/utils/isSessionExpired.spec.ts +23 -0
  42. package/src/features/auth/utils/isSessionExpired.ts +5 -0
  43. package/src/features/auth/utils/normalizeAuthSettings.spec.ts +60 -0
  44. package/src/features/auth/utils/normalizeAuthSettings.ts +71 -0
  45. package/src/features/auth/utils/normalizeNextPath.spec.ts +21 -0
  46. package/src/features/auth/utils/normalizeNextPath.ts +12 -0
  47. package/src/features/auth/utils/normalizeRedirectPath.spec.ts +12 -0
  48. package/src/features/auth/utils/normalizeRedirectPath.ts +3 -0
  49. package/src/features/auth/utils/persistAuthSession.spec.ts +35 -0
  50. package/src/features/auth/utils/persistAuthSession.ts +12 -0
  51. package/src/features/auth/utils/readStoredAuthSession.spec.ts +34 -0
  52. package/src/features/auth/utils/readStoredAuthSession.ts +14 -0
  53. package/src/features/auth/utils/toAuthSessionFromSettingsUser.spec.ts +76 -0
  54. package/src/features/auth/utils/toAuthSessionFromSettingsUser.ts +36 -0
  55. package/src/features/config/types.ts +55 -0
  56. package/src/features/layouts/AppLayout.tsx +8 -6
  57. package/src/features/layouts/appbar/AppBarLayout.tsx +42 -23
  58. package/src/features/layouts/fullscreen/FullscreenLayout.tsx +3 -2
  59. package/src/features/layouts/sidebar/SidebarInner.tsx +7 -5
  60. package/src/features/layouts/sidebar/SidebarLayout.tsx +16 -3
  61. package/src/features/layouts/utils.ts +54 -0
  62. package/src/features/layouts/windows/WindowsLayout.tsx +22 -4
  63. package/src/features/legal/LegalDocumentContent.tsx +102 -0
  64. package/src/features/legal/LegalDocumentView.tsx +42 -0
  65. package/src/features/legal/LegalDocumentsIndexView.tsx +51 -0
  66. package/src/features/legal/LegalDocumentsLinks.tsx +29 -0
  67. package/src/features/legal/legalDocuments.ts +62 -0
  68. package/src/features/settings/SettingsIcons.tsx +20 -0
  69. package/src/features/settings/SettingsProvider.tsx +347 -245
  70. package/src/features/settings/SettingsRoutes.tsx +8 -0
  71. package/src/features/settings/SettingsView.tsx +43 -8
  72. package/src/features/settings/components/Develop.tsx +2 -2
  73. package/src/features/settings/components/LegalDocumentsPanel.tsx +46 -0
  74. package/src/features/settings/components/UserIcon.tsx +20 -0
  75. package/src/features/settings/components/UserSettingsPanel.tsx +438 -0
  76. package/src/features/settings/components/createUserSettingsRoute.tsx +43 -0
  77. package/src/features/settings/utils/buildSettingsForPropagation.spec.ts +167 -0
  78. package/src/features/settings/utils/buildSettingsForPropagation.ts +61 -0
  79. package/src/features/settings/utils/flattenNavigationItems.spec.ts +17 -0
  80. package/src/features/settings/utils/flattenNavigationItems.ts +12 -0
  81. package/src/features/settings/utils/getAvailableThemesForSettings.spec.ts +15 -0
  82. package/src/features/settings/utils/getAvailableThemesForSettings.ts +16 -0
  83. package/src/features/settings/utils/getBrowserTimezone.spec.ts +11 -0
  84. package/src/features/settings/utils/getBrowserTimezone.ts +7 -0
  85. package/src/features/settings/utils/getPreferenceSnapshot.spec.ts +35 -0
  86. package/src/features/settings/utils/getPreferenceSnapshot.ts +10 -0
  87. package/src/features/settings/utils/getResolvedAppearanceForSettings.spec.ts +97 -0
  88. package/src/features/settings/utils/getResolvedAppearanceForSettings.ts +48 -0
  89. package/src/features/settings/utils/index.ts +12 -0
  90. package/src/features/settings/utils/isSameUser.spec.ts +35 -0
  91. package/src/features/settings/utils/isSameUser.ts +17 -0
  92. package/src/features/settings/utils/mergePreferencesIntoSettings.spec.ts +108 -0
  93. package/src/features/settings/utils/mergePreferencesIntoSettings.ts +47 -0
  94. package/src/features/settings/utils/resolveColorMode.spec.ts +29 -0
  95. package/src/features/settings/utils/resolveColorMode.ts +6 -0
  96. package/src/features/settings/utils/resolveLabel.spec.ts +17 -0
  97. package/src/features/settings/utils/resolveLabel.ts +7 -0
  98. package/src/features/settings/utils/toAbsoluteFontUrls.spec.ts +26 -0
  99. package/src/features/settings/utils/toAbsoluteFontUrls.ts +15 -0
  100. package/src/features/settings/utils/toSettingsUser.spec.ts +49 -0
  101. package/src/features/settings/utils/toSettingsUser.ts +15 -0
  102. package/src/i18n/translations/en/common.json +14 -0
  103. package/src/i18n/translations/en/settings.json +45 -0
  104. package/src/i18n/translations/fr/common.json +14 -0
  105. package/src/i18n/translations/fr/settings.json +45 -0
  106. package/src/index.css +37 -0
  107. package/src/index.ts +6 -0
  108. package/src/routes/components/NavigationItemRoute.tsx +32 -1
  109. package/src/routes/components/NotFoundView.tsx +13 -3
  110. package/src/routes/hooks/useNavigationItems.ts +19 -4
  111. package/src/routes/routes.tsx +87 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shellui/core",
3
- "version": "0.2.0",
3
+ "version": "0.3.0-beta.1",
4
4
  "description": "ShellUI Core - Core React application runtime",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -39,14 +39,18 @@
39
39
  "@radix-ui/react-alert-dialog": "^1.1.15",
40
40
  "@radix-ui/react-collapsible": "^1.1.12",
41
41
  "@radix-ui/react-dialog": "^1.1.15",
42
+ "@radix-ui/react-dropdown-menu": "^2.1.16",
42
43
  "@radix-ui/react-separator": "^1.1.8",
43
44
  "@radix-ui/react-slot": "^1.2.4",
44
45
  "@radix-ui/react-tooltip": "^1.2.8",
45
46
  "@sentry/react": "^10.39.0",
47
+ "@supabase/supabase-js": "^2.99.3",
46
48
  "class-variance-authority": "^0.7.1",
47
49
  "clsx": "^2.1.1",
48
50
  "i18next": "^25.8.11",
49
51
  "react-i18next": "^16.5.4",
52
+ "react-icons": "^5.6.0",
53
+ "react-markdown": "^10.1.0",
50
54
  "react-router": "^7.13.0",
51
55
  "roarr": "^7.21.4",
52
56
  "sonner": "^2.0.7",
@@ -58,7 +62,7 @@
58
62
  "workbox-routing": "^7.4.0",
59
63
  "workbox-strategies": "^7.4.0",
60
64
  "workbox-window": "^7.4.0",
61
- "@shellui/sdk": "0.2.0"
65
+ "@shellui/sdk": "0.3.0-beta.1"
62
66
  },
63
67
  "peerDependencies": {
64
68
  "react": "^18.0.0 || ^19.0.0",
@@ -74,9 +78,10 @@
74
78
  "react": "^19.2.4",
75
79
  "react-dom": "^19.2.4",
76
80
  "tailwindcss-animate": "^1.0.7",
77
- "typescript": "^5.9.3"
81
+ "typescript": "^5.9.3",
82
+ "vitest": "^4.0.18"
78
83
  },
79
84
  "scripts": {
80
- "test": "echo \"No tests specified for @shellui/core\""
85
+ "test": "node scripts/test.js"
81
86
  }
82
87
  }
package/src/app.tsx CHANGED
@@ -9,6 +9,7 @@ import { ThemeProvider } from './features/theme/ThemeProvider';
9
9
  import { I18nProvider } from './i18n/I18nProvider';
10
10
  import { DialogProvider } from './features/alertDialog/DialogContext';
11
11
  import { CookieConsentModal } from './features/cookieConsent/CookieConsentModal';
12
+ import { AuthProvider } from './features/auth/AuthProvider';
12
13
  import './features/sentry/initSentry';
13
14
  import './i18n/config'; // Initialize i18n
14
15
  import './index.css';
@@ -103,15 +104,17 @@ const App = () => {
103
104
 
104
105
  return (
105
106
  <ConfigProvider>
106
- <SettingsProvider>
107
- <ThemeProvider>
108
- <I18nProvider>
109
- <DialogProvider>
110
- <AppContent />
111
- </DialogProvider>
112
- </I18nProvider>
113
- </ThemeProvider>
114
- </SettingsProvider>
107
+ <AuthProvider>
108
+ <SettingsProvider>
109
+ <ThemeProvider>
110
+ <I18nProvider>
111
+ <DialogProvider>
112
+ <AppContent />
113
+ </DialogProvider>
114
+ </I18nProvider>
115
+ </ThemeProvider>
116
+ </SettingsProvider>
117
+ </AuthProvider>
115
118
  </ConfigProvider>
116
119
  );
117
120
  };
@@ -0,0 +1,35 @@
1
+ import * as React from 'react';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+ import { cn } from '../../lib/utils';
4
+
5
+ const badgeVariants = cva(
6
+ 'inline-flex items-center rounded-md border px-2 py-0.5 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
7
+ {
8
+ variants: {
9
+ variant: {
10
+ default: 'border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80',
11
+ secondary:
12
+ 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
13
+ outline: 'text-foreground',
14
+ muted: 'border-border bg-muted/50 text-muted-foreground',
15
+ },
16
+ },
17
+ defaultVariants: {
18
+ variant: 'outline',
19
+ },
20
+ },
21
+ );
22
+
23
+ export interface BadgeProps
24
+ extends React.HTMLAttributes<HTMLDivElement>, VariantProps<typeof badgeVariants> {}
25
+
26
+ function Badge({ className, variant, ...props }: BadgeProps) {
27
+ return (
28
+ <div
29
+ className={cn(badgeVariants({ variant }), className)}
30
+ {...props}
31
+ />
32
+ );
33
+ }
34
+
35
+ export { Badge, badgeVariants };
@@ -0,0 +1,94 @@
1
+ import { forwardRef, type ComponentPropsWithoutRef, type ElementRef } from 'react';
2
+ import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
3
+ import { cn } from '../../lib/utils';
4
+ import { Z_INDEX } from '../../lib/z-index';
5
+
6
+ const DropdownMenu = DropdownMenuPrimitive.Root;
7
+
8
+ const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
9
+
10
+ const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
11
+
12
+ const DropdownMenuContent = forwardRef<
13
+ ElementRef<typeof DropdownMenuPrimitive.Content>,
14
+ ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
15
+ >(({ className, sideOffset = 6, forceMount, ...props }, ref) => (
16
+ <DropdownMenuPrimitive.Portal>
17
+ <DropdownMenuPrimitive.Content
18
+ ref={ref}
19
+ forceMount={forceMount}
20
+ sideOffset={sideOffset}
21
+ className={cn(
22
+ 'z-50 min-w-[12rem] overflow-hidden rounded-md border border-border bg-popover p-1 text-popover-foreground shadow-md',
23
+ 'data-[state=open]:animate-in data-[state=closed]:animate-out',
24
+ 'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
25
+ 'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
26
+ 'data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1',
27
+ 'data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1',
28
+ className,
29
+ )}
30
+ style={{ zIndex: Z_INDEX.MODAL_CONTENT }}
31
+ {...props}
32
+ />
33
+ </DropdownMenuPrimitive.Portal>
34
+ ));
35
+ DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName;
36
+
37
+ const DropdownMenuItem = forwardRef<
38
+ ElementRef<typeof DropdownMenuPrimitive.Item>,
39
+ ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item>
40
+ >(({ className, ...props }, ref) => (
41
+ <DropdownMenuPrimitive.Item
42
+ ref={ref}
43
+ className={cn(
44
+ 'relative flex items-center gap-2 cursor-pointer select-none rounded-sm px-2 py-1.5 text-sm outline-none transition-colors',
45
+ 'focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
46
+ className,
47
+ )}
48
+ {...props}
49
+ />
50
+ ));
51
+ DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName;
52
+
53
+ const DropdownMenuLabel = forwardRef<
54
+ ElementRef<typeof DropdownMenuPrimitive.Label>,
55
+ ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label>
56
+ >(({ className, ...props }, ref) => (
57
+ <DropdownMenuPrimitive.Label
58
+ ref={ref}
59
+ className={cn('px-2 py-1.5 text-sm font-semibold', className)}
60
+ {...props}
61
+ />
62
+ ));
63
+ DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName;
64
+
65
+ const DropdownMenuSeparator = forwardRef<
66
+ ElementRef<typeof DropdownMenuPrimitive.Separator>,
67
+ ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
68
+ >(({ className, ...props }, ref) => (
69
+ <DropdownMenuPrimitive.Separator
70
+ ref={ref}
71
+ className={cn('-mx-1 my-1 h-px bg-border', className)}
72
+ {...props}
73
+ />
74
+ ));
75
+ DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName;
76
+
77
+ const DropdownMenuShortcut = ({ className, ...props }: ComponentPropsWithoutRef<'span'>) => (
78
+ <span
79
+ className={cn('ml-auto text-xs tracking-widest text-muted-foreground', className)}
80
+ {...props}
81
+ />
82
+ );
83
+ DropdownMenuShortcut.displayName = 'DropdownMenuShortcut';
84
+
85
+ export {
86
+ DropdownMenu,
87
+ DropdownMenuPortal,
88
+ DropdownMenuTrigger,
89
+ DropdownMenuContent,
90
+ DropdownMenuLabel,
91
+ DropdownMenuItem,
92
+ DropdownMenuSeparator,
93
+ DropdownMenuShortcut,
94
+ };
@@ -129,7 +129,7 @@ const SidebarGroupContent = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivEle
129
129
  SidebarGroupContent.displayName = 'SidebarGroupContent';
130
130
 
131
131
  const sidebarMenuButtonVariants = cva(
132
- 'flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:bg-sidebar-accent focus-visible:text-sidebar-accent-foreground focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground',
132
+ 'flex w-full appearance-none items-center gap-2 overflow-hidden rounded-md bg-transparent p-2 text-left text-sm transition-colors hover:bg-sidebar-accent hover:text-sidebar-accent-foreground hover:filter-none focus-visible:bg-sidebar-accent focus-visible:text-sidebar-accent-foreground focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground',
133
133
  {
134
134
  variants: {
135
135
  variant: {
@@ -1,4 +1,12 @@
1
1
  export default {
2
2
  settings: '/__settings',
3
3
  cookiePreferences: '/__cookie-preferences',
4
+ login: '/login',
5
+ loginCallback: '/login/callback',
6
+ admin: '/admin',
7
+ legalDocuments: '/legal',
8
+ legalPrivacyPolicy: '/legal/privacy-policy',
9
+ legalTermsOfService: '/legal/terms-of-service',
10
+ legalNotice: '/legal/legal-notice',
11
+ legalDataProcessingAgreement: '/legal/data-processing-agreement',
4
12
  };
@@ -0,0 +1,154 @@
1
+ import { useEffect, useMemo, useRef } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Outlet, Route, Routes, useLocation, useNavigate } from 'react-router';
4
+ import { Button } from '../../components/ui/button';
5
+ import urls from '../../constants/urls';
6
+ import { ContentView } from '../../components/ContentView';
7
+ import { LoginButton } from '../auth/components/LoginButton';
8
+ import { useAuth } from '../auth/hooks/useAuth';
9
+ import { useConfig } from '../config/useConfig';
10
+ import type { NavigationItem } from '../config/types';
11
+ import { getBaseUrlWithoutHash } from '../layouts/utils';
12
+ import { AdminForbiddenAccess } from './components/AdminForbiddenAccess';
13
+ import { AppLayout } from '../layouts/AppLayout';
14
+
15
+ /** Admin microfrontend uses hash routes (e.g. createHashRouter); sync shell `/admin/...` with iframe `#/...`. */
16
+ function buildAdminIframeSrc(
17
+ baseAdminContentUrl: string,
18
+ normalizedAdminPath: string,
19
+ pathname: string,
20
+ search: string,
21
+ ): string {
22
+ const pathAfterAdmin = pathname.startsWith(normalizedAdminPath)
23
+ ? pathname.slice(normalizedAdminPath.length)
24
+ : '';
25
+ const segment = pathAfterAdmin.replace(/^\/+|\/+$/g, '');
26
+ const hashRoute = segment ? `/${segment}` : '/';
27
+ const originBase = getBaseUrlWithoutHash(baseAdminContentUrl).replace(/\/+$/, '');
28
+ return `${originBase}/#${hashRoute}${search}`;
29
+ }
30
+
31
+ const AdminAccessGuard = ({ allow }: { allow: boolean }) => {
32
+ if (!allow) {
33
+ return <AdminForbiddenAccess />;
34
+ }
35
+ return <Outlet />;
36
+ };
37
+
38
+ export const AdminView = () => {
39
+ const { t } = useTranslation('common');
40
+ const navigate = useNavigate();
41
+ const location = useLocation();
42
+ const { config } = useConfig();
43
+ const { user } = useAuth();
44
+ const isStaff = Boolean(user?.isStaff);
45
+ const canOpenAdminPanel = Boolean(user?.isStaff || user?.isCompanyOwner);
46
+ const djangoAdminHref = useMemo(() => {
47
+ if (config.backend?.type !== 'shellui' || !config.backend.url?.trim()) {
48
+ return null;
49
+ }
50
+ return `${config.backend.url.replace(/\/+$/, '')}/admin`;
51
+ }, [config.backend?.type, config.backend?.url]);
52
+ const configuredAdminPathname = config.backend?.adminPathname?.trim();
53
+ const adminPath =
54
+ configuredAdminPathname && configuredAdminPathname.startsWith('/')
55
+ ? configuredAdminPathname
56
+ : urls.admin;
57
+ const baseAdminContentUrl = config.backend?.adminUrl?.trim() || urls.settings;
58
+ const initialAdminContentUrlRef = useRef<string | null>(null);
59
+ if (!initialAdminContentUrlRef.current) {
60
+ const normalizedAdminPath = adminPath.replace(/\/+$/, '');
61
+ initialAdminContentUrlRef.current = buildAdminIframeSrc(
62
+ baseAdminContentUrl,
63
+ normalizedAdminPath,
64
+ location.pathname,
65
+ location.search,
66
+ );
67
+ }
68
+ const initialAdminContentUrl = initialAdminContentUrlRef.current;
69
+ const adminContentItem = useMemo<NavigationItem>(
70
+ () => ({
71
+ label: 'Settings',
72
+ path: adminPath.replace(/^\/+/, ''),
73
+ url: baseAdminContentUrl,
74
+ /** Required so ContentView maps iframe hash → shell path (`/admin`, `/admin/users`, …). */
75
+ useHashRouter: true,
76
+ }),
77
+ [adminPath, baseAdminContentUrl],
78
+ );
79
+
80
+ useEffect(() => {
81
+ if (config?.title) {
82
+ document.title = `Administration | ${config.title}`;
83
+ } else {
84
+ document.title = 'Administration';
85
+ }
86
+ }, [config?.title]);
87
+
88
+ return (
89
+ <AppLayout>
90
+ <div className="flex h-full w-full flex-col overflow-hidden bg-background">
91
+ <header className="flex h-12 shrink-0 items-center justify-between border-b bg-card px-2 md:px-3">
92
+ <Button
93
+ type="button"
94
+ variant="ghost"
95
+ size="sm"
96
+ onClick={() => navigate('/')}
97
+ >
98
+ <svg
99
+ xmlns="http://www.w3.org/2000/svg"
100
+ viewBox="0 0 24 24"
101
+ fill="none"
102
+ stroke="currentColor"
103
+ strokeWidth="2"
104
+ strokeLinecap="round"
105
+ strokeLinejoin="round"
106
+ className="h-4 w-4"
107
+ aria-hidden
108
+ >
109
+ <path d="m15 18-6-6 6-6" />
110
+ </svg>
111
+ Back to home
112
+ </Button>
113
+ <div className="flex items-center gap-2">
114
+ {isStaff && djangoAdminHref ? (
115
+ <Button
116
+ variant="outline"
117
+ size="sm"
118
+ asChild
119
+ >
120
+ <a
121
+ href={djangoAdminHref}
122
+ target="_blank"
123
+ rel="noopener noreferrer"
124
+ >
125
+ {t('adminShell.djangoAdmin')}
126
+ </a>
127
+ </Button>
128
+ ) : null}
129
+ <LoginButton
130
+ variant="appbar"
131
+ logoutOnly
132
+ />
133
+ </div>
134
+ </header>
135
+ <main className="flex min-h-0 flex-1">
136
+ <Routes>
137
+ <Route element={<AdminAccessGuard allow={canOpenAdminPanel} />}>
138
+ <Route
139
+ path="*"
140
+ element={
141
+ <ContentView
142
+ url={initialAdminContentUrl}
143
+ pathPrefix={adminPath.replace(/^\/+/, '')}
144
+ navItem={adminContentItem}
145
+ />
146
+ }
147
+ />
148
+ </Route>
149
+ </Routes>
150
+ </main>
151
+ </div>
152
+ </AppLayout>
153
+ );
154
+ };
@@ -0,0 +1,10 @@
1
+ export const AdminForbiddenAccess = () => {
2
+ return (
3
+ <div className="flex h-full w-full items-center justify-center p-6">
4
+ <div className="max-w-md text-center">
5
+ <h1 className="text-xl font-semibold">Access forbidden</h1>
6
+ <p className="mt-2 text-sm text-muted-foreground">Contact your administrator.</p>
7
+ </div>
8
+ </div>
9
+ );
10
+ };