@shellui/core 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -0
- package/dist/ContentView-CZG-ro_B.js +146 -0
- package/dist/ContentView-CZG-ro_B.js.map +1 -0
- package/dist/CookiePreferencesView-MhO9FO-4.js +213 -0
- package/dist/CookiePreferencesView-MhO9FO-4.js.map +1 -0
- package/dist/DefaultLayout-Dbb3uJED.js +394 -0
- package/dist/DefaultLayout-Dbb3uJED.js.map +1 -0
- package/dist/FullscreenLayout-1SgPHWw-.js +30 -0
- package/dist/FullscreenLayout-1SgPHWw-.js.map +1 -0
- package/dist/HomeView-DYU-O_Il.js +21 -0
- package/dist/HomeView-DYU-O_Il.js.map +1 -0
- package/dist/NotFoundView-CeYjJNg0.js +52 -0
- package/dist/NotFoundView-CeYjJNg0.js.map +1 -0
- package/dist/OverlayShell-pzbqQW25.js +642 -0
- package/dist/OverlayShell-pzbqQW25.js.map +1 -0
- package/dist/SettingsView-Bndrta44.js +2207 -0
- package/dist/SettingsView-Bndrta44.js.map +1 -0
- package/dist/ViewRoute-ChSPabOy.js +32 -0
- package/dist/ViewRoute-ChSPabOy.js.map +1 -0
- package/dist/WindowsLayout-CXGNPKoY.js +633 -0
- package/dist/WindowsLayout-CXGNPKoY.js.map +1 -0
- package/dist/app.d.ts +3 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/components/ContentView.d.ts +10 -0
- package/dist/components/ContentView.d.ts.map +1 -0
- package/dist/components/HomeView.d.ts +2 -0
- package/dist/components/HomeView.d.ts.map +1 -0
- package/dist/components/LoadingOverlay.d.ts +2 -0
- package/dist/components/LoadingOverlay.d.ts.map +1 -0
- package/dist/components/NotFoundView.d.ts +2 -0
- package/dist/components/NotFoundView.d.ts.map +1 -0
- package/dist/components/RouteErrorBoundary.d.ts +2 -0
- package/dist/components/RouteErrorBoundary.d.ts.map +1 -0
- package/dist/components/ViewRoute.d.ts +7 -0
- package/dist/components/ViewRoute.d.ts.map +1 -0
- package/dist/components/ui/alert-dialog.d.ts +32 -0
- package/dist/components/ui/alert-dialog.d.ts.map +1 -0
- package/dist/components/ui/breadcrumb.d.ts +20 -0
- package/dist/components/ui/breadcrumb.d.ts.map +1 -0
- package/dist/components/ui/button-group.d.ts +7 -0
- package/dist/components/ui/button-group.d.ts.map +1 -0
- package/dist/components/ui/button.d.ts +12 -0
- package/dist/components/ui/button.d.ts.map +1 -0
- package/dist/components/ui/dialog.d.ts +24 -0
- package/dist/components/ui/dialog.d.ts.map +1 -0
- package/dist/components/ui/drawer.d.ts +38 -0
- package/dist/components/ui/drawer.d.ts.map +1 -0
- package/dist/components/ui/select.d.ts +5 -0
- package/dist/components/ui/select.d.ts.map +1 -0
- package/dist/components/ui/sidebar.d.ts +46 -0
- package/dist/components/ui/sidebar.d.ts.map +1 -0
- package/dist/components/ui/sonner.d.ts +6 -0
- package/dist/components/ui/sonner.d.ts.map +1 -0
- package/dist/components/ui/switch.d.ts +8 -0
- package/dist/components/ui/switch.d.ts.map +1 -0
- package/dist/constants/urls.d.ts +6 -0
- package/dist/constants/urls.d.ts.map +1 -0
- package/dist/constants/urls.js +8 -0
- package/dist/constants/urls.js.map +1 -0
- package/dist/features/alertDialog/DialogContext.d.ts +12 -0
- package/dist/features/alertDialog/DialogContext.d.ts.map +1 -0
- package/dist/features/config/ConfigProvider.d.ts +15 -0
- package/dist/features/config/ConfigProvider.d.ts.map +1 -0
- package/dist/features/config/types.d.ts +177 -0
- package/dist/features/config/types.d.ts.map +1 -0
- package/dist/features/config/useConfig.d.ts +8 -0
- package/dist/features/config/useConfig.d.ts.map +1 -0
- package/dist/features/cookieConsent/CookieConsentModal.d.ts +6 -0
- package/dist/features/cookieConsent/CookieConsentModal.d.ts.map +1 -0
- package/dist/features/cookieConsent/CookiePreferencesView.d.ts +2 -0
- package/dist/features/cookieConsent/CookiePreferencesView.d.ts.map +1 -0
- package/dist/features/cookieConsent/cookieConsent.d.ts +22 -0
- package/dist/features/cookieConsent/cookieConsent.d.ts.map +1 -0
- package/dist/features/cookieConsent/useCookieConsent.d.ts +15 -0
- package/dist/features/cookieConsent/useCookieConsent.d.ts.map +1 -0
- package/dist/features/drawer/DrawerContext.d.ts +24 -0
- package/dist/features/drawer/DrawerContext.d.ts.map +1 -0
- package/dist/features/layouts/AppLayout.d.ts +12 -0
- package/dist/features/layouts/AppLayout.d.ts.map +1 -0
- package/dist/features/layouts/DefaultLayout.d.ts +10 -0
- package/dist/features/layouts/DefaultLayout.d.ts.map +1 -0
- package/dist/features/layouts/FullscreenLayout.d.ts +9 -0
- package/dist/features/layouts/FullscreenLayout.d.ts.map +1 -0
- package/dist/features/layouts/LayoutProviders.d.ts +9 -0
- package/dist/features/layouts/LayoutProviders.d.ts.map +1 -0
- package/dist/features/layouts/OverlayShell.d.ts +10 -0
- package/dist/features/layouts/OverlayShell.d.ts.map +1 -0
- package/dist/features/layouts/WindowsLayout.d.ts +24 -0
- package/dist/features/layouts/WindowsLayout.d.ts.map +1 -0
- package/dist/features/layouts/utils.d.ts +16 -0
- package/dist/features/layouts/utils.d.ts.map +1 -0
- package/dist/features/modal/ModalContext.d.ts +20 -0
- package/dist/features/modal/ModalContext.d.ts.map +1 -0
- package/dist/features/sentry/initSentry.d.ts +14 -0
- package/dist/features/sentry/initSentry.d.ts.map +1 -0
- package/dist/features/settings/SettingsContext.d.ts +10 -0
- package/dist/features/settings/SettingsContext.d.ts.map +1 -0
- package/dist/features/settings/SettingsIcons.d.ts +22 -0
- package/dist/features/settings/SettingsIcons.d.ts.map +1 -0
- package/dist/features/settings/SettingsProvider.d.ts +5 -0
- package/dist/features/settings/SettingsProvider.d.ts.map +1 -0
- package/dist/features/settings/SettingsRoutes.d.ts +7 -0
- package/dist/features/settings/SettingsRoutes.d.ts.map +1 -0
- package/dist/features/settings/SettingsView.d.ts +2 -0
- package/dist/features/settings/SettingsView.d.ts.map +1 -0
- package/dist/features/settings/components/Advanced.d.ts +2 -0
- package/dist/features/settings/components/Advanced.d.ts.map +1 -0
- package/dist/features/settings/components/Appearance.d.ts +2 -0
- package/dist/features/settings/components/Appearance.d.ts.map +1 -0
- package/dist/features/settings/components/DataPrivacy.d.ts +2 -0
- package/dist/features/settings/components/DataPrivacy.d.ts.map +1 -0
- package/dist/features/settings/components/Develop.d.ts +2 -0
- package/dist/features/settings/components/Develop.d.ts.map +1 -0
- package/dist/features/settings/components/LanguageAndRegion.d.ts +2 -0
- package/dist/features/settings/components/LanguageAndRegion.d.ts.map +1 -0
- package/dist/features/settings/components/ServiceWorker.d.ts +2 -0
- package/dist/features/settings/components/ServiceWorker.d.ts.map +1 -0
- package/dist/features/settings/components/UpdateApp.d.ts +2 -0
- package/dist/features/settings/components/UpdateApp.d.ts.map +1 -0
- package/dist/features/settings/components/develop/DialogTestButtons.d.ts +2 -0
- package/dist/features/settings/components/develop/DialogTestButtons.d.ts.map +1 -0
- package/dist/features/settings/components/develop/DrawerTestButtons.d.ts +2 -0
- package/dist/features/settings/components/develop/DrawerTestButtons.d.ts.map +1 -0
- package/dist/features/settings/components/develop/ModalTestButtons.d.ts +2 -0
- package/dist/features/settings/components/develop/ModalTestButtons.d.ts.map +1 -0
- package/dist/features/settings/components/develop/ToastTestButtons.d.ts +2 -0
- package/dist/features/settings/components/develop/ToastTestButtons.d.ts.map +1 -0
- package/dist/features/settings/hooks/useSettings.d.ts +2 -0
- package/dist/features/settings/hooks/useSettings.d.ts.map +1 -0
- package/dist/features/sonner/SonnerContext.d.ts +29 -0
- package/dist/features/sonner/SonnerContext.d.ts.map +1 -0
- package/dist/features/theme/ThemeProvider.d.ts +11 -0
- package/dist/features/theme/ThemeProvider.d.ts.map +1 -0
- package/dist/features/theme/themes.d.ts +114 -0
- package/dist/features/theme/themes.d.ts.map +1 -0
- package/dist/features/theme/useTheme.d.ts +10 -0
- package/dist/features/theme/useTheme.d.ts.map +1 -0
- package/dist/i18n/I18nProvider.d.ts +9 -0
- package/dist/i18n/I18nProvider.d.ts.map +1 -0
- package/dist/i18n/config.d.ts +23 -0
- package/dist/i18n/config.d.ts.map +1 -0
- package/dist/i18n/translations/en/common.json.d.ts +19 -0
- package/dist/i18n/translations/en/cookieConsent.json.d.ts +53 -0
- package/dist/i18n/translations/en/settings.json.d.ts +358 -0
- package/dist/i18n/translations/fr/common.json.d.ts +19 -0
- package/dist/i18n/translations/fr/cookieConsent.json.d.ts +53 -0
- package/dist/i18n/translations/fr/settings.json.d.ts +358 -0
- package/dist/index-lmRk5L6z.js +2160 -0
- package/dist/index-lmRk5L6z.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/z-index.d.ts +29 -0
- package/dist/lib/z-index.d.ts.map +1 -0
- package/dist/router/router.d.ts +3 -0
- package/dist/router/router.d.ts.map +1 -0
- package/dist/router/routes.d.ts +4 -0
- package/dist/router/routes.d.ts.map +1 -0
- package/dist/sidebar-ClIeZ2zb.js +303 -0
- package/dist/sidebar-ClIeZ2zb.js.map +1 -0
- package/dist/style.css +1 -0
- package/dist/switch-8SzUJz7Q.js +44 -0
- package/dist/switch-8SzUJz7Q.js.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +93 -0
- package/postcss.config.js +6 -0
- package/src/app.tsx +119 -0
- package/src/components/ContentView.tsx +258 -0
- package/src/components/HomeView.tsx +19 -0
- package/src/components/LoadingOverlay.tsx +12 -0
- package/src/components/NotFoundView.tsx +84 -0
- package/src/components/RouteErrorBoundary.tsx +95 -0
- package/src/components/ViewRoute.tsx +47 -0
- package/src/components/ui/alert-dialog.tsx +181 -0
- package/src/components/ui/breadcrumb.tsx +155 -0
- package/src/components/ui/button-group.tsx +52 -0
- package/src/components/ui/button.tsx +51 -0
- package/src/components/ui/dialog.tsx +160 -0
- package/src/components/ui/drawer.tsx +200 -0
- package/src/components/ui/select.tsx +24 -0
- package/src/components/ui/sidebar.tsx +406 -0
- package/src/components/ui/sonner.tsx +36 -0
- package/src/components/ui/switch.tsx +45 -0
- package/src/constants/urls.ts +4 -0
- package/src/features/alertDialog/DialogContext.tsx +468 -0
- package/src/features/config/ConfigProvider.ts +96 -0
- package/src/features/config/types.ts +195 -0
- package/src/features/config/useConfig.ts +15 -0
- package/src/features/cookieConsent/CookieConsentModal.tsx +122 -0
- package/src/features/cookieConsent/CookiePreferencesView.tsx +328 -0
- package/src/features/cookieConsent/cookieConsent.ts +84 -0
- package/src/features/cookieConsent/useCookieConsent.ts +39 -0
- package/src/features/drawer/DrawerContext.tsx +116 -0
- package/src/features/layouts/AppLayout.tsx +63 -0
- package/src/features/layouts/DefaultLayout.tsx +625 -0
- package/src/features/layouts/FullscreenLayout.tsx +55 -0
- package/src/features/layouts/LayoutProviders.tsx +20 -0
- package/src/features/layouts/OverlayShell.tsx +171 -0
- package/src/features/layouts/WindowsLayout.tsx +860 -0
- package/src/features/layouts/utils.ts +99 -0
- package/src/features/modal/ModalContext.tsx +112 -0
- package/src/features/sentry/initSentry.ts +72 -0
- package/src/features/settings/SettingsContext.tsx +19 -0
- package/src/features/settings/SettingsIcons.tsx +452 -0
- package/src/features/settings/SettingsProvider.tsx +341 -0
- package/src/features/settings/SettingsRoutes.tsx +66 -0
- package/src/features/settings/SettingsView.tsx +327 -0
- package/src/features/settings/components/Advanced.tsx +128 -0
- package/src/features/settings/components/Appearance.tsx +306 -0
- package/src/features/settings/components/DataPrivacy.tsx +142 -0
- package/src/features/settings/components/Develop.tsx +174 -0
- package/src/features/settings/components/LanguageAndRegion.tsx +329 -0
- package/src/features/settings/components/ServiceWorker.tsx +363 -0
- package/src/features/settings/components/UpdateApp.tsx +206 -0
- package/src/features/settings/components/develop/DialogTestButtons.tsx +137 -0
- package/src/features/settings/components/develop/DrawerTestButtons.tsx +67 -0
- package/src/features/settings/components/develop/ModalTestButtons.tsx +30 -0
- package/src/features/settings/components/develop/ToastTestButtons.tsx +179 -0
- package/src/features/settings/hooks/useSettings.tsx +10 -0
- package/src/features/sonner/SonnerContext.tsx +286 -0
- package/src/features/theme/ThemeProvider.tsx +16 -0
- package/src/features/theme/themes.ts +561 -0
- package/src/features/theme/useTheme.tsx +71 -0
- package/src/i18n/I18nProvider.tsx +32 -0
- package/src/i18n/config.ts +107 -0
- package/src/i18n/translations/en/common.json +16 -0
- package/src/i18n/translations/en/cookieConsent.json +50 -0
- package/src/i18n/translations/en/settings.json +355 -0
- package/src/i18n/translations/fr/common.json +16 -0
- package/src/i18n/translations/fr/cookieConsent.json +50 -0
- package/src/i18n/translations/fr/settings.json +355 -0
- package/src/index.css +412 -0
- package/src/index.html +100 -0
- package/src/index.ts +31 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/z-index.ts +29 -0
- package/src/main.tsx +26 -0
- package/src/router/router.tsx +8 -0
- package/src/router/routes.tsx +115 -0
- package/src/service-worker/register.ts +1199 -0
- package/src/service-worker/sw-dev.ts +87 -0
- package/src/service-worker/sw.ts +105 -0
- package/tailwind.config.js +60 -0
package/package.json
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shellui/core",
|
|
3
|
+
"version": "0.0.4",
|
|
4
|
+
"description": "ShellUI Core - Core React application runtime",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./types": {
|
|
15
|
+
"types": "./dist/types.d.ts",
|
|
16
|
+
"import": "./dist/types.js",
|
|
17
|
+
"default": "./dist/types.js"
|
|
18
|
+
},
|
|
19
|
+
"./constants/urls": {
|
|
20
|
+
"types": "./dist/constants/urls.d.ts",
|
|
21
|
+
"import": "./dist/constants/urls.js",
|
|
22
|
+
"default": "./dist/constants/urls.js"
|
|
23
|
+
},
|
|
24
|
+
"./style.css": "./dist/style.css"
|
|
25
|
+
},
|
|
26
|
+
"files": [
|
|
27
|
+
"dist",
|
|
28
|
+
"src",
|
|
29
|
+
"postcss.config.js",
|
|
30
|
+
"tailwind.config.js",
|
|
31
|
+
"README.md",
|
|
32
|
+
"package.json"
|
|
33
|
+
],
|
|
34
|
+
"keywords": [
|
|
35
|
+
"shellui",
|
|
36
|
+
"core",
|
|
37
|
+
"react",
|
|
38
|
+
"microfrontend"
|
|
39
|
+
],
|
|
40
|
+
"author": "ShellUI",
|
|
41
|
+
"license": "MIT",
|
|
42
|
+
"dependencies": {
|
|
43
|
+
"@radix-ui/react-collapsible": "^1.1.12",
|
|
44
|
+
"@sentry/react": "^8.0.0",
|
|
45
|
+
"@radix-ui/react-alert-dialog": "^1.1.15",
|
|
46
|
+
"@radix-ui/react-dialog": "^1.1.15",
|
|
47
|
+
"@radix-ui/react-separator": "^1.1.8",
|
|
48
|
+
"@radix-ui/react-slot": "^1.2.4",
|
|
49
|
+
"class-variance-authority": "^0.7.1",
|
|
50
|
+
"clsx": "^2.1.1",
|
|
51
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
52
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
53
|
+
"i18next": "^23.15.0",
|
|
54
|
+
"react-i18next": "^15.1.0",
|
|
55
|
+
"react-router": "^7.12.0",
|
|
56
|
+
"roarr": "^7.21.2",
|
|
57
|
+
"sonner": "^2.0.7",
|
|
58
|
+
"tailwind-merge": "^3.4.0",
|
|
59
|
+
"vaul": "^1.1.2",
|
|
60
|
+
"workbox-window": "^7.1.0",
|
|
61
|
+
"workbox-precaching": "^7.1.0",
|
|
62
|
+
"workbox-routing": "^7.1.0",
|
|
63
|
+
"workbox-strategies": "^7.1.0",
|
|
64
|
+
"workbox-cacheable-response": "^7.1.0",
|
|
65
|
+
"workbox-expiration": "^7.1.0",
|
|
66
|
+
"@shellui/sdk": "0.0.4"
|
|
67
|
+
},
|
|
68
|
+
"peerDependencies": {
|
|
69
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
70
|
+
"react-dom": "^18.0.0 || ^19.0.0"
|
|
71
|
+
},
|
|
72
|
+
"publishConfig": {
|
|
73
|
+
"access": "public"
|
|
74
|
+
},
|
|
75
|
+
"devDependencies": {
|
|
76
|
+
"@tailwindcss/postcss": "^4.1.18",
|
|
77
|
+
"@tailwindcss/typography": "^0.5.19",
|
|
78
|
+
"@types/react": "^19.2.7",
|
|
79
|
+
"@types/react-dom": "^19.2.3",
|
|
80
|
+
"@vitejs/plugin-react": "^5.1.2",
|
|
81
|
+
"autoprefixer": "^10.4.23",
|
|
82
|
+
"postcss": "^8.5.6",
|
|
83
|
+
"tailwindcss": "^4.1.18",
|
|
84
|
+
"typescript": "^5.0.0",
|
|
85
|
+
"vite": "^7.3.1",
|
|
86
|
+
"vite-plugin-dts": "^4.5.0"
|
|
87
|
+
},
|
|
88
|
+
"scripts": {
|
|
89
|
+
"build": "vite build",
|
|
90
|
+
"clean": "rm -rf dist",
|
|
91
|
+
"test": "echo \"No tests specified for @shellui/core\""
|
|
92
|
+
}
|
|
93
|
+
}
|
package/src/app.tsx
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { useMemo, useLayoutEffect, useState, useEffect } from 'react';
|
|
2
|
+
import { RouterProvider } from 'react-router';
|
|
3
|
+
import { shellui } from '@shellui/sdk';
|
|
4
|
+
import { useConfig } from './features/config/useConfig';
|
|
5
|
+
import { ConfigProvider } from './features/config/ConfigProvider';
|
|
6
|
+
import { createAppRouter } from './router/router';
|
|
7
|
+
import { SettingsProvider } from './features/settings/SettingsProvider';
|
|
8
|
+
import { ThemeProvider } from './features/theme/ThemeProvider';
|
|
9
|
+
import { I18nProvider } from './i18n/I18nProvider';
|
|
10
|
+
import { DialogProvider } from './features/alertDialog/DialogContext';
|
|
11
|
+
import { CookieConsentModal } from './features/cookieConsent/CookieConsentModal';
|
|
12
|
+
import './features/sentry/initSentry';
|
|
13
|
+
import './i18n/config'; // Initialize i18n
|
|
14
|
+
import './index.css';
|
|
15
|
+
import { registerServiceWorker, unregisterServiceWorker, isTauri } from './service-worker/register';
|
|
16
|
+
import { useSettings } from './features/settings/hooks/useSettings';
|
|
17
|
+
|
|
18
|
+
const AppContent = () => {
|
|
19
|
+
const { config } = useConfig();
|
|
20
|
+
const { settings } = useSettings();
|
|
21
|
+
|
|
22
|
+
// Apply favicon from config when available (allows projects to override default)
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (config?.favicon) {
|
|
25
|
+
const link = document.querySelector<HTMLLinkElement>('link[rel="icon"]');
|
|
26
|
+
if (link) link.href = config.favicon;
|
|
27
|
+
}
|
|
28
|
+
}, [config?.favicon]);
|
|
29
|
+
|
|
30
|
+
// Register or unregister service worker based on setting
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
if (isTauri()) {
|
|
33
|
+
unregisterServiceWorker();
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const serviceWorkerEnabled = settings?.serviceWorker?.enabled ?? true; // Default to enabled
|
|
37
|
+
|
|
38
|
+
// Don't register service worker if navigation is empty or undefined
|
|
39
|
+
// This helps prevent issues in development or misconfigured apps
|
|
40
|
+
if (!config?.navigation || config.navigation.length === 0) {
|
|
41
|
+
if (serviceWorkerEnabled) {
|
|
42
|
+
// eslint-disable-next-line no-console
|
|
43
|
+
console.warn('[Service Worker] Disabled: No navigation items configured');
|
|
44
|
+
unregisterServiceWorker();
|
|
45
|
+
}
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (serviceWorkerEnabled) {
|
|
50
|
+
registerServiceWorker({
|
|
51
|
+
enabled: true,
|
|
52
|
+
});
|
|
53
|
+
} else {
|
|
54
|
+
unregisterServiceWorker();
|
|
55
|
+
}
|
|
56
|
+
}, [settings?.serviceWorker?.enabled, config?.navigation]);
|
|
57
|
+
|
|
58
|
+
// Create router from config using data mode
|
|
59
|
+
const router = useMemo(() => {
|
|
60
|
+
if (!config) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return createAppRouter(config);
|
|
64
|
+
}, [config]);
|
|
65
|
+
|
|
66
|
+
// If no navigation, show simple layout
|
|
67
|
+
if (!config.navigation || config.navigation.length === 0) {
|
|
68
|
+
return (
|
|
69
|
+
<>
|
|
70
|
+
<CookieConsentModal />
|
|
71
|
+
<div style={{ fontFamily: 'system-ui, sans-serif', padding: '2rem' }}>
|
|
72
|
+
<h1>{config.title || 'ShellUI'}</h1>
|
|
73
|
+
<p>No navigation items configured.</p>
|
|
74
|
+
</div>
|
|
75
|
+
</>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!router) {
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<>
|
|
85
|
+
<CookieConsentModal />
|
|
86
|
+
<RouterProvider router={router} />
|
|
87
|
+
</>
|
|
88
|
+
);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const App = () => {
|
|
92
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
93
|
+
// Initialize ShellUI SDK to support recursive nesting
|
|
94
|
+
useLayoutEffect(() => {
|
|
95
|
+
shellui.init().then(() => {
|
|
96
|
+
setIsLoading(false);
|
|
97
|
+
});
|
|
98
|
+
}, []);
|
|
99
|
+
|
|
100
|
+
if (isLoading) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return (
|
|
105
|
+
<ConfigProvider>
|
|
106
|
+
<SettingsProvider>
|
|
107
|
+
<ThemeProvider>
|
|
108
|
+
<I18nProvider>
|
|
109
|
+
<DialogProvider>
|
|
110
|
+
<AppContent />
|
|
111
|
+
</DialogProvider>
|
|
112
|
+
</I18nProvider>
|
|
113
|
+
</ThemeProvider>
|
|
114
|
+
</SettingsProvider>
|
|
115
|
+
</ConfigProvider>
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
export default App;
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/* eslint-disable no-console */
|
|
2
|
+
import type { NavigationItem } from '@/features/config/types';
|
|
3
|
+
import {
|
|
4
|
+
addIframe,
|
|
5
|
+
removeIframe,
|
|
6
|
+
shellui,
|
|
7
|
+
getLogger,
|
|
8
|
+
type ShellUIUrlPayload,
|
|
9
|
+
type ShellUIMessage,
|
|
10
|
+
} from '@shellui/sdk';
|
|
11
|
+
import { useEffect, useRef, useState } from 'react';
|
|
12
|
+
import { useNavigate } from 'react-router';
|
|
13
|
+
import { LoadingOverlay } from './LoadingOverlay';
|
|
14
|
+
|
|
15
|
+
const logger = getLogger('shellcore');
|
|
16
|
+
|
|
17
|
+
interface ContentViewProps {
|
|
18
|
+
url: string;
|
|
19
|
+
pathPrefix: string;
|
|
20
|
+
ignoreMessages?: boolean;
|
|
21
|
+
navItem: NavigationItem;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export const ContentView = ({
|
|
25
|
+
url,
|
|
26
|
+
pathPrefix,
|
|
27
|
+
ignoreMessages = false,
|
|
28
|
+
navItem,
|
|
29
|
+
}: ContentViewProps) => {
|
|
30
|
+
const navigate = useNavigate();
|
|
31
|
+
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
32
|
+
const isInternalNavigation = useRef(false);
|
|
33
|
+
const [initialUrl] = useState(url);
|
|
34
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
35
|
+
|
|
36
|
+
useEffect(() => {
|
|
37
|
+
if (!iframeRef.current) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const iframeId = addIframe(iframeRef.current);
|
|
41
|
+
return () => {
|
|
42
|
+
removeIframe(iframeId);
|
|
43
|
+
};
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
// Sync parent URL when iframe notifies us of a change
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
const cleanup = shellui.addMessageListener(
|
|
49
|
+
'SHELLUI_URL_CHANGED',
|
|
50
|
+
(data: ShellUIMessage, event: MessageEvent) => {
|
|
51
|
+
if (ignoreMessages) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Ignore URL CHANGE from other than ContentView iframe
|
|
56
|
+
if (event.source !== iframeRef.current?.contentWindow) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const { pathname, search, hash } = data.payload as ShellUIUrlPayload;
|
|
61
|
+
// Remove leading slash and trailing slashes from iframe pathname
|
|
62
|
+
let cleanPathname = pathname.startsWith(navItem.url)
|
|
63
|
+
? pathname.slice(navItem.url.length)
|
|
64
|
+
: pathname;
|
|
65
|
+
cleanPathname = cleanPathname.startsWith('/') ? cleanPathname.slice(1) : cleanPathname;
|
|
66
|
+
cleanPathname = cleanPathname.replace(/\/+$/, ''); // Remove trailing slashes
|
|
67
|
+
// Construct the new path without trailing slashes
|
|
68
|
+
let newShellPath = cleanPathname
|
|
69
|
+
? `/${pathPrefix}/${cleanPathname}${search}${hash}`
|
|
70
|
+
: `/${pathPrefix}${search}${hash}`;
|
|
71
|
+
|
|
72
|
+
// Normalize: remove trailing slashes from pathname part only (preserve query/hash)
|
|
73
|
+
const urlParts = newShellPath.match(/^([^?#]*)([?#].*)?$/);
|
|
74
|
+
if (urlParts) {
|
|
75
|
+
const pathnamePart = urlParts[1].replace(/\/+$/, '') || '/';
|
|
76
|
+
const queryHashPart = urlParts[2] || '';
|
|
77
|
+
newShellPath = pathnamePart + queryHashPart;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Normalize current path for comparison (remove trailing slashes from pathname)
|
|
81
|
+
const currentPathname = window.location.pathname.replace(/\/+$/, '') || '/';
|
|
82
|
+
const currentPath = currentPathname + window.location.search + window.location.hash;
|
|
83
|
+
|
|
84
|
+
// Normalize new path for comparison
|
|
85
|
+
const newPathParts = newShellPath.match(/^([^?#]*)([?#].*)?$/);
|
|
86
|
+
const normalizedNewPathname = newPathParts?.[1]?.replace(/\/+$/, '') || '/';
|
|
87
|
+
const normalizedNewPath = normalizedNewPathname + (newPathParts?.[2] || '');
|
|
88
|
+
|
|
89
|
+
if (currentPath !== normalizedNewPath) {
|
|
90
|
+
// Mark this navigation as internal so we don't try to "push" it back to the iframe
|
|
91
|
+
isInternalNavigation.current = true;
|
|
92
|
+
navigate(newShellPath, { replace: true });
|
|
93
|
+
|
|
94
|
+
// Reset the flag after a short delay to allow the render cycle to complete
|
|
95
|
+
setTimeout(() => {
|
|
96
|
+
isInternalNavigation.current = false;
|
|
97
|
+
}, 100);
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
return () => {
|
|
103
|
+
cleanup();
|
|
104
|
+
};
|
|
105
|
+
}, [pathPrefix, navigate]);
|
|
106
|
+
|
|
107
|
+
// Hide loading overlay when iframe sends SHELLUI_INITIALIZED
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
const cleanup = shellui.addMessageListener(
|
|
110
|
+
'SHELLUI_INITIALIZED',
|
|
111
|
+
(_data: ShellUIMessage, event: MessageEvent) => {
|
|
112
|
+
if (event.source === iframeRef.current?.contentWindow) {
|
|
113
|
+
setIsLoading(false);
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
return () => cleanup();
|
|
118
|
+
}, []);
|
|
119
|
+
|
|
120
|
+
// Fallback: hide overlay after 400ms if SHELLUI_INITIALIZED was not received
|
|
121
|
+
useEffect(() => {
|
|
122
|
+
if (!isLoading) return;
|
|
123
|
+
const timeoutId = setTimeout(() => {
|
|
124
|
+
logger.info('ContentView: Timeout expired, hiding loading overlay');
|
|
125
|
+
setIsLoading(false);
|
|
126
|
+
}, 400);
|
|
127
|
+
return () => clearTimeout(timeoutId);
|
|
128
|
+
}, [isLoading]);
|
|
129
|
+
|
|
130
|
+
// Handle external URL changes (e.g. from Sidebar)
|
|
131
|
+
useEffect(() => {
|
|
132
|
+
if (iframeRef.current && !isInternalNavigation.current) {
|
|
133
|
+
// Only update iframe src if it's actually different from its current src
|
|
134
|
+
// to avoid unnecessary reloads
|
|
135
|
+
if (iframeRef.current.src !== url) {
|
|
136
|
+
iframeRef.current.src = url;
|
|
137
|
+
setIsLoading(true);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}, [url]);
|
|
141
|
+
|
|
142
|
+
// Inject script to prevent "Layout was forced" warning by deferring layout until stylesheets load
|
|
143
|
+
useEffect(() => {
|
|
144
|
+
const iframe = iframeRef.current;
|
|
145
|
+
if (!iframe) return;
|
|
146
|
+
|
|
147
|
+
const handleLoad = () => {
|
|
148
|
+
try {
|
|
149
|
+
const iframeWindow = iframe.contentWindow;
|
|
150
|
+
const iframeDoc = iframe.contentDocument || iframeWindow?.document;
|
|
151
|
+
if (!iframeDoc || !iframeWindow) return;
|
|
152
|
+
|
|
153
|
+
// Inject a script that waits for stylesheets before allowing layout calculations
|
|
154
|
+
const script = iframeDoc.createElement('script');
|
|
155
|
+
script.textContent = `
|
|
156
|
+
(function() {
|
|
157
|
+
// Wait for all stylesheets to load
|
|
158
|
+
function waitForStylesheets() {
|
|
159
|
+
const styleSheets = Array.from(document.styleSheets);
|
|
160
|
+
const pendingSheets = styleSheets.filter(function(sheet) {
|
|
161
|
+
try {
|
|
162
|
+
return sheet.cssRules === null;
|
|
163
|
+
} catch (e) {
|
|
164
|
+
return false; // Cross-origin stylesheets, assume loaded
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
if (pendingSheets.length === 0) {
|
|
169
|
+
// All stylesheets loaded
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Check again after a short delay
|
|
174
|
+
setTimeout(waitForStylesheets, 10);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Start checking after DOM is ready
|
|
178
|
+
if (document.readyState === 'complete') {
|
|
179
|
+
waitForStylesheets();
|
|
180
|
+
} else {
|
|
181
|
+
window.addEventListener('load', waitForStylesheets);
|
|
182
|
+
}
|
|
183
|
+
})();
|
|
184
|
+
`;
|
|
185
|
+
iframeDoc.head.appendChild(script);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
// Cross-origin or other errors - ignore (this is expected for some iframes)
|
|
188
|
+
logger.debug('Could not inject stylesheet wait script:', { error });
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Wait for iframe to load before injecting script
|
|
193
|
+
iframe.addEventListener('load', handleLoad);
|
|
194
|
+
|
|
195
|
+
// Also try immediately if already loaded
|
|
196
|
+
if (iframe.contentDocument?.readyState === 'complete') {
|
|
197
|
+
handleLoad();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return () => {
|
|
201
|
+
iframe.removeEventListener('load', handleLoad);
|
|
202
|
+
};
|
|
203
|
+
}, [initialUrl]);
|
|
204
|
+
|
|
205
|
+
// Suppress browser warnings that are expected and acceptable
|
|
206
|
+
useEffect(() => {
|
|
207
|
+
if (process.env.NODE_ENV === 'development') {
|
|
208
|
+
const originalWarn = console.warn;
|
|
209
|
+
console.warn = (...args: unknown[]) => {
|
|
210
|
+
const message = String(args[0] ?? '');
|
|
211
|
+
// Suppress the specific sandbox warning
|
|
212
|
+
if (
|
|
213
|
+
message.includes('allow-scripts') &&
|
|
214
|
+
message.includes('allow-same-origin') &&
|
|
215
|
+
message.includes('sandbox')
|
|
216
|
+
) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
// Suppress "Layout was forced" warning from iframe content
|
|
220
|
+
// This is a performance warning that occurs when iframe content calculates layout before stylesheets load
|
|
221
|
+
// It's harmless and common in iframe scenarios, especially with React apps
|
|
222
|
+
if (
|
|
223
|
+
message.includes('Layout was forced') &&
|
|
224
|
+
message.includes('before the page was fully loaded')
|
|
225
|
+
) {
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
originalWarn.apply(console, args);
|
|
229
|
+
};
|
|
230
|
+
return () => {
|
|
231
|
+
console.warn = originalWarn;
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
}, []);
|
|
235
|
+
|
|
236
|
+
return (
|
|
237
|
+
<div style={{ width: '100%', height: '100%', display: 'flex', position: 'relative' }}>
|
|
238
|
+
{/* Note: allow-same-origin is required for same-origin iframe content (e.g., Vite dev server, cookies, localStorage).
|
|
239
|
+
While this allows the iframe to remove its own sandboxing, it's acceptable here because the iframe content
|
|
240
|
+
is trusted microfrontend content from the same application origin.
|
|
241
|
+
Browser security warnings about this combination cannot be suppressed programmatically. */}
|
|
242
|
+
<iframe
|
|
243
|
+
ref={iframeRef}
|
|
244
|
+
src={initialUrl}
|
|
245
|
+
style={{
|
|
246
|
+
width: '100%',
|
|
247
|
+
height: '100%',
|
|
248
|
+
border: 'none',
|
|
249
|
+
display: 'block',
|
|
250
|
+
}}
|
|
251
|
+
title="Content Frame"
|
|
252
|
+
sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox"
|
|
253
|
+
referrerPolicy="no-referrer-when-downgrade"
|
|
254
|
+
/>
|
|
255
|
+
{isLoading && <LoadingOverlay />}
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next';
|
|
2
|
+
import { useConfig } from '../features/config/useConfig';
|
|
3
|
+
|
|
4
|
+
export const HomeView = () => {
|
|
5
|
+
const { t } = useTranslation('common');
|
|
6
|
+
const { config } = useConfig();
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<div className="flex flex-col items-center justify-center h-full p-8 md:p-10">
|
|
10
|
+
<h1
|
|
11
|
+
className="m-0 text-3xl font-light text-foreground"
|
|
12
|
+
style={{ fontFamily: 'var(--heading-font-family, inherit)' }}
|
|
13
|
+
>
|
|
14
|
+
{t('welcome', { title: config.title })}
|
|
15
|
+
</h1>
|
|
16
|
+
<p className="mt-4 text-lg text-muted-foreground">{t('getStarted')}</p>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function LoadingOverlay() {
|
|
2
|
+
return (
|
|
3
|
+
<div className="absolute inset-0 z-10 flex flex-col bg-background">
|
|
4
|
+
<div className="h-1 w-full overflow-hidden bg-muted/30">
|
|
5
|
+
<div
|
|
6
|
+
className="h-full w-0 bg-muted-foreground/50"
|
|
7
|
+
style={{ animation: 'loading-bar-slide 400ms linear infinite' }}
|
|
8
|
+
/>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
);
|
|
12
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { useTranslation } from 'react-i18next';
|
|
2
|
+
import { shellui } from '@shellui/sdk';
|
|
3
|
+
import { useConfig } from '@/features/config/useConfig';
|
|
4
|
+
import type { NavigationItem, NavigationGroup } from '@/features/config/types';
|
|
5
|
+
|
|
6
|
+
const flattenNavigationItems = (
|
|
7
|
+
navigation: (NavigationItem | NavigationGroup)[],
|
|
8
|
+
): NavigationItem[] => {
|
|
9
|
+
if (navigation.length === 0) return [];
|
|
10
|
+
return navigation.flatMap((item) => {
|
|
11
|
+
if ('title' in item && 'items' in item) {
|
|
12
|
+
return (item as NavigationGroup).items;
|
|
13
|
+
}
|
|
14
|
+
return item as NavigationItem;
|
|
15
|
+
});
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const NotFoundView = () => {
|
|
19
|
+
const { config } = useConfig();
|
|
20
|
+
const { i18n } = useTranslation();
|
|
21
|
+
const currentLanguage = i18n.language || 'en';
|
|
22
|
+
|
|
23
|
+
const resolveLocalizedString = (
|
|
24
|
+
value: string | { en: string; fr: string; [key: string]: string },
|
|
25
|
+
lang: string,
|
|
26
|
+
): string => {
|
|
27
|
+
if (typeof value === 'string') return value;
|
|
28
|
+
return value[lang] || value.en || value.fr || Object.values(value)[0] || '';
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const navItems =
|
|
32
|
+
config?.navigation && config.navigation.length > 0
|
|
33
|
+
? flattenNavigationItems(config.navigation)
|
|
34
|
+
.filter((item) => !item.hidden)
|
|
35
|
+
.filter((item, index, self) => index === self.findIndex((i) => i.path === item.path))
|
|
36
|
+
: [];
|
|
37
|
+
|
|
38
|
+
const handleNavigate = (path: string) => {
|
|
39
|
+
shellui.navigate(path.startsWith('/') ? path : `/${path}`);
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="flex flex-col min-h-full">
|
|
44
|
+
<div className="flex-1 flex flex-col items-center justify-center px-6 py-12 text-muted-foreground">
|
|
45
|
+
<span className="text-6xl font-light tracking-tighter text-foreground/80 select-none">
|
|
46
|
+
404
|
|
47
|
+
</span>
|
|
48
|
+
<p className="mt-3 text-lg text-muted-foreground">Page not found</p>
|
|
49
|
+
</div>
|
|
50
|
+
|
|
51
|
+
{navItems.length > 0 && (
|
|
52
|
+
<footer className="border-t border-border py-4 px-6 mt-auto bg-muted/30">
|
|
53
|
+
<nav
|
|
54
|
+
className="flex flex-row flex-wrap justify-center items-center gap-x-2 gap-y-1 text-sm text-muted-foreground"
|
|
55
|
+
aria-label="Available pages"
|
|
56
|
+
>
|
|
57
|
+
{navItems.map((item, index) => (
|
|
58
|
+
<span
|
|
59
|
+
key={item.path}
|
|
60
|
+
className="inline-flex items-center gap-x-2"
|
|
61
|
+
>
|
|
62
|
+
{index > 0 && (
|
|
63
|
+
<span
|
|
64
|
+
className="text-border select-none"
|
|
65
|
+
aria-hidden
|
|
66
|
+
>
|
|
67
|
+
·
|
|
68
|
+
</span>
|
|
69
|
+
)}
|
|
70
|
+
<button
|
|
71
|
+
type="button"
|
|
72
|
+
onClick={() => handleNavigate(`/${item.path}`)}
|
|
73
|
+
className="text-muted-foreground hover:text-foreground hover:underline underline-offset-2 cursor-pointer bg-transparent border-0 p-0 font-normal"
|
|
74
|
+
>
|
|
75
|
+
{resolveLocalizedString(item.label, currentLanguage)}
|
|
76
|
+
</button>
|
|
77
|
+
</span>
|
|
78
|
+
))}
|
|
79
|
+
</nav>
|
|
80
|
+
</footer>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { useRouteError, isRouteErrorResponse } from 'react-router';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { shellui } from '@shellui/sdk';
|
|
4
|
+
import { Button } from '@/components/ui/button';
|
|
5
|
+
|
|
6
|
+
function isChunkLoadError(error: unknown): boolean {
|
|
7
|
+
if (error instanceof Error) {
|
|
8
|
+
const msg = error.message.toLowerCase();
|
|
9
|
+
return (
|
|
10
|
+
msg.includes('loading dynamically imported module') ||
|
|
11
|
+
msg.includes('chunk') ||
|
|
12
|
+
msg.includes('failed to fetch dynamically imported module')
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getErrorMessage(error: unknown): string {
|
|
19
|
+
if (isRouteErrorResponse(error)) {
|
|
20
|
+
return error.data?.message ?? error.statusText ?? 'Something went wrong';
|
|
21
|
+
}
|
|
22
|
+
if (error instanceof Error) {
|
|
23
|
+
return error.message;
|
|
24
|
+
}
|
|
25
|
+
return String(error);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function getErrorStack(error: unknown): string | null {
|
|
29
|
+
if (error instanceof Error && error.stack) {
|
|
30
|
+
return error.stack;
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function getErrorDetailsText(error: unknown): string {
|
|
36
|
+
const message = getErrorMessage(error);
|
|
37
|
+
const stack = getErrorStack(error);
|
|
38
|
+
if (stack) {
|
|
39
|
+
return `Message:\n${message}\n\nStack:\n${stack}`;
|
|
40
|
+
}
|
|
41
|
+
return message;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function RouteErrorBoundary() {
|
|
45
|
+
const error = useRouteError();
|
|
46
|
+
const { t } = useTranslation('common');
|
|
47
|
+
const isChunkError = isChunkLoadError(error);
|
|
48
|
+
const detailsText = getErrorDetailsText(error);
|
|
49
|
+
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
className="flex min-h-screen flex-col items-center justify-center bg-background px-4 py-12"
|
|
53
|
+
style={{ fontFamily: 'var(--heading-font-family, system-ui, sans-serif)' }}
|
|
54
|
+
>
|
|
55
|
+
<div className="w-full max-w-md space-y-6 text-center">
|
|
56
|
+
<div className="space-y-2">
|
|
57
|
+
<h1 className="text-xl font-semibold text-foreground">
|
|
58
|
+
{isChunkError ? t('errorBoundary.titleChunk') : t('errorBoundary.titleGeneric')}
|
|
59
|
+
</h1>
|
|
60
|
+
<p className="text-sm text-muted-foreground">
|
|
61
|
+
{isChunkError
|
|
62
|
+
? t('errorBoundary.descriptionChunk')
|
|
63
|
+
: t('errorBoundary.descriptionGeneric')}
|
|
64
|
+
</p>
|
|
65
|
+
</div>
|
|
66
|
+
|
|
67
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:justify-center">
|
|
68
|
+
<Button
|
|
69
|
+
variant="default"
|
|
70
|
+
onClick={() => window.location.reload()}
|
|
71
|
+
className="shrink-0"
|
|
72
|
+
>
|
|
73
|
+
{t('errorBoundary.tryAgain')}
|
|
74
|
+
</Button>
|
|
75
|
+
<Button
|
|
76
|
+
variant="outline"
|
|
77
|
+
onClick={() => shellui.navigate('/')}
|
|
78
|
+
className="shrink-0"
|
|
79
|
+
>
|
|
80
|
+
{t('errorBoundary.goToHome')}
|
|
81
|
+
</Button>
|
|
82
|
+
</div>
|
|
83
|
+
|
|
84
|
+
<details className="rounded-lg border border-border bg-muted/30 text-left">
|
|
85
|
+
<summary className="cursor-pointer px-4 py-3 text-xs font-medium text-muted-foreground hover:text-foreground">
|
|
86
|
+
{t('errorBoundary.errorDetails')}
|
|
87
|
+
</summary>
|
|
88
|
+
<pre className="max-h-64 overflow-auto whitespace-pre-wrap break-all px-4 pb-3 pt-1 text-xs text-muted-foreground font-mono">
|
|
89
|
+
{detailsText}
|
|
90
|
+
</pre>
|
|
91
|
+
</details>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|