@shellui/core 0.2.0-beta.0 → 0.2.0-beta.2
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/package.json +2 -2
- package/src/app.tsx +1 -1
- package/src/components/ContentView.tsx +22 -61
- package/src/components/LoadingOverlay.tsx +1 -1
- package/src/components/ui/sidebar.tsx +2 -124
- package/src/features/layouts/AppLayout.tsx +22 -19
- package/src/features/layouts/LayoutFallback.tsx +8 -0
- package/src/features/layouts/OverlayShell.tsx +21 -40
- package/src/features/layouts/{AppBarLayout.tsx → appbar/AppBarLayout.tsx} +72 -78
- package/src/features/layouts/{FullscreenLayout.tsx → fullscreen/FullscreenLayout.tsx} +5 -11
- package/src/features/layouts/sidebar/BottomNavItem.tsx +88 -0
- package/src/features/layouts/sidebar/MobileBottomNav.tsx +168 -0
- package/src/features/layouts/sidebar/NavigationContent.tsx +159 -0
- package/src/features/layouts/sidebar/SidebarIcons.tsx +93 -0
- package/src/features/layouts/sidebar/SidebarInner.tsx +48 -0
- package/src/features/layouts/sidebar/SidebarLayout.tsx +86 -0
- package/src/features/layouts/sidebar/sidebarUtils.ts +23 -0
- package/src/features/layouts/sidebar/types.ts +8 -0
- package/src/features/layouts/utils.ts +1 -1
- package/src/features/layouts/{WindowsLayout.tsx → windows/WindowsLayout.tsx} +199 -204
- package/src/features/settings/SettingsView.tsx +177 -180
- package/src/{components → routes/components}/HomeView.tsx +1 -1
- package/src/{components → routes/components}/IndexRoute.tsx +4 -4
- package/src/routes/components/NavigationItemRoute.tsx +19 -0
- package/src/{components → routes/components}/NotFoundView.tsx +3 -3
- package/src/{components → routes/components}/RouteErrorBoundary.tsx +1 -1
- package/src/routes/components/RouteFallback.tsx +8 -0
- package/src/routes/hooks/useNavigationItems.ts +84 -0
- package/src/{router → routes}/routes.tsx +10 -18
- package/src/components/ViewRoute.tsx +0 -74
- package/src/dist/CookiePreferencesView.52b5aec8.js +0 -1182
- package/src/dist/CookiePreferencesView.52b5aec8.js.map +0 -1
- package/src/dist/DefaultLayout.045a82ff.js +0 -1964
- package/src/dist/DefaultLayout.045a82ff.js.map +0 -1
- package/src/dist/DefaultLayout.4454f259.js +0 -4414
- package/src/dist/DefaultLayout.4454f259.js.map +0 -1
- package/src/dist/FullscreenLayout.555c4987.js +0 -1054
- package/src/dist/FullscreenLayout.555c4987.js.map +0 -1
- package/src/dist/HomeView.ddfa7b68.js +0 -771
- package/src/dist/HomeView.ddfa7b68.js.map +0 -1
- package/src/dist/NotFoundView.c75be4f1.js +0 -811
- package/src/dist/NotFoundView.c75be4f1.js.map +0 -1
- package/src/dist/SettingsView.052b03a6.js +0 -4965
- package/src/dist/SettingsView.052b03a6.js.map +0 -1
- package/src/dist/ViewRoute.e6e3b142.js +0 -1042
- package/src/dist/ViewRoute.e6e3b142.js.map +0 -1
- package/src/dist/WindowsLayout.08724167.js +0 -1762
- package/src/dist/WindowsLayout.08724167.js.map +0 -1
- package/src/dist/esm.f0d741e6.js +0 -29520
- package/src/dist/esm.f0d741e6.js.map +0 -1
- package/src/dist/favicon.4367ac1e.svg +0 -14
- package/src/dist/index.parcel.36d65383.js +0 -54089
- package/src/dist/index.parcel.36d65383.js.map +0 -1
- package/src/dist/index.parcel.ca6d8a47.css +0 -3493
- package/src/dist/index.parcel.ca6d8a47.css.map +0 -1
- package/src/dist/index.parcel.html +0 -88
- package/src/features/layouts/DefaultLayout.tsx +0 -670
- package/src/features/layouts/LayoutProviders.tsx +0 -20
- /package/src/{constants.ts → constants/loading.ts} +0 -0
- /package/src/{router → routes}/router.tsx +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shellui/core",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.2",
|
|
4
4
|
"description": "ShellUI Core - Core React application runtime",
|
|
5
5
|
"main": "src/index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
"workbox-strategies": "^7.1.0",
|
|
59
59
|
"workbox-cacheable-response": "^7.1.0",
|
|
60
60
|
"workbox-expiration": "^7.1.0",
|
|
61
|
-
"@shellui/sdk": "0.2.0-beta.
|
|
61
|
+
"@shellui/sdk": "0.2.0-beta.2"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"react": "^18.0.0 || ^19.0.0",
|
package/src/app.tsx
CHANGED
|
@@ -3,7 +3,7 @@ import { RouterProvider } from 'react-router';
|
|
|
3
3
|
import { shellui } from '@shellui/sdk';
|
|
4
4
|
import { useConfig } from './features/config/useConfig';
|
|
5
5
|
import { ConfigProvider } from './features/config/ConfigProvider';
|
|
6
|
-
import { createAppRouter } from './
|
|
6
|
+
import { createAppRouter } from './routes/router';
|
|
7
7
|
import { SettingsProvider } from './features/settings/SettingsProvider';
|
|
8
8
|
import { ThemeProvider } from './features/theme/ThemeProvider';
|
|
9
9
|
import { I18nProvider } from './i18n/I18nProvider';
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* eslint-disable no-console */
|
|
2
1
|
import type { NavigationItem } from '../features/config/types';
|
|
3
2
|
import { isHashRouterNavItem, getHashPathFromUrl } from '../features/layouts/utils';
|
|
4
3
|
import {
|
|
@@ -9,16 +8,13 @@ import {
|
|
|
9
8
|
type ShellUIUrlPayload,
|
|
10
9
|
type ShellUIMessage,
|
|
11
10
|
} from '@shellui/sdk';
|
|
12
|
-
import { useEffect, useRef, useState } from 'react';
|
|
11
|
+
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
13
12
|
import { useNavigate } from 'react-router';
|
|
14
|
-
import { LOADING_OVERLAY_DURATION_MS } from '../constants';
|
|
13
|
+
import { LOADING_OVERLAY_DURATION_MS } from '../constants/loading';
|
|
15
14
|
import { LoadingOverlay } from './LoadingOverlay';
|
|
16
15
|
|
|
17
16
|
const logger = getLogger('shellcore');
|
|
18
17
|
|
|
19
|
-
/** URL of the last main-content iframe that sent SHELLUI_INITIALIZED. Used to skip the loading overlay when navigating between nav items that point to the same app URL. */
|
|
20
|
-
let lastLoadedIframeUrl: string | null = null;
|
|
21
|
-
|
|
22
18
|
interface ContentViewProps {
|
|
23
19
|
url: string;
|
|
24
20
|
pathPrefix: string;
|
|
@@ -36,18 +32,18 @@ export const ContentView = ({
|
|
|
36
32
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
37
33
|
const cancelRevealRef = useRef<(() => void) | null>(null);
|
|
38
34
|
const mountTimeRef = useRef(Date.now());
|
|
39
|
-
|
|
40
|
-
urlRef.current = url;
|
|
41
|
-
const [initialUrl] = useState(url);
|
|
35
|
+
|
|
42
36
|
const [isLoading, setIsLoading] = useState(() => {
|
|
43
37
|
// Skip overlay when same app URL was just loaded (e.g. switching App ↔ Root with same url)
|
|
44
|
-
if (!ignoreMessages
|
|
38
|
+
if (!ignoreMessages) return false;
|
|
45
39
|
return true;
|
|
46
40
|
});
|
|
47
41
|
|
|
42
|
+
const [iframeUrl, setIframeUrl] = useState(url);
|
|
43
|
+
|
|
48
44
|
const MIN_LOADING_MS = 80; // Don't reveal before this, reduces blink from theme/layout paint
|
|
49
45
|
|
|
50
|
-
|
|
46
|
+
useLayoutEffect(() => {
|
|
51
47
|
if (!iframeRef.current) {
|
|
52
48
|
return;
|
|
53
49
|
}
|
|
@@ -55,7 +51,16 @@ export const ContentView = ({
|
|
|
55
51
|
return () => {
|
|
56
52
|
removeIframe(iframeId);
|
|
57
53
|
};
|
|
58
|
-
}, []);
|
|
54
|
+
}, [iframeUrl, navItem.path]);
|
|
55
|
+
|
|
56
|
+
useLayoutEffect(() => {
|
|
57
|
+
if (ignoreMessages) return;
|
|
58
|
+
if (isLoading) return;
|
|
59
|
+
if (iframeRef.current) {
|
|
60
|
+
setIsLoading(true);
|
|
61
|
+
setIframeUrl(url);
|
|
62
|
+
}
|
|
63
|
+
}, [navItem]);
|
|
59
64
|
|
|
60
65
|
// Sync parent URL when iframe notifies us of a change
|
|
61
66
|
useEffect(() => {
|
|
@@ -66,6 +71,8 @@ export const ContentView = ({
|
|
|
66
71
|
return;
|
|
67
72
|
}
|
|
68
73
|
|
|
74
|
+
if (isLoading) return;
|
|
75
|
+
|
|
69
76
|
// Ignore URL CHANGE from other than ContentView iframe
|
|
70
77
|
if (event.source !== iframeRef.current?.contentWindow) {
|
|
71
78
|
return;
|
|
@@ -122,7 +129,7 @@ export const ContentView = ({
|
|
|
122
129
|
const normalizedNewPath = normalizedNewPathname + (newPathParts?.[2] || '');
|
|
123
130
|
|
|
124
131
|
if (currentPath !== normalizedNewPath) {
|
|
125
|
-
navigate(newShellPath);
|
|
132
|
+
navigate(newShellPath, { replace: true });
|
|
126
133
|
}
|
|
127
134
|
},
|
|
128
135
|
);
|
|
@@ -156,7 +163,6 @@ export const ContentView = ({
|
|
|
156
163
|
'SHELLUI_INITIALIZED',
|
|
157
164
|
(_data: ShellUIMessage, event: MessageEvent) => {
|
|
158
165
|
if (event.source !== iframeRef.current?.contentWindow) return;
|
|
159
|
-
if (!ignoreMessages) lastLoadedIframeUrl = urlRef.current;
|
|
160
166
|
cancelRevealRef.current?.();
|
|
161
167
|
let cancelled = false;
|
|
162
168
|
cancelRevealRef.current = () => {
|
|
@@ -195,52 +201,6 @@ export const ContentView = ({
|
|
|
195
201
|
return () => clearTimeout(timeoutId);
|
|
196
202
|
}, [isLoading]);
|
|
197
203
|
|
|
198
|
-
// Handle external URL changes (e.g. from Sidebar)
|
|
199
|
-
useEffect(() => {
|
|
200
|
-
if (iframeRef.current) {
|
|
201
|
-
if (iframeRef.current.src !== url) {
|
|
202
|
-
iframeRef.current.src = url;
|
|
203
|
-
// Skip overlay when switching to the same app URL (e.g. App ↔ Root); different app still shows overlay
|
|
204
|
-
const sameAppAlreadyLoaded = !ignoreMessages;
|
|
205
|
-
if (!sameAppAlreadyLoaded) {
|
|
206
|
-
setIsLoading(true);
|
|
207
|
-
mountTimeRef.current = Date.now(); // apply min delay for this load too
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
}, [url, ignoreMessages]);
|
|
212
|
-
|
|
213
|
-
// Suppress browser warnings that are expected and acceptable
|
|
214
|
-
useEffect(() => {
|
|
215
|
-
if (process.env.NODE_ENV === 'development') {
|
|
216
|
-
const originalWarn = console.warn;
|
|
217
|
-
console.warn = (...args: unknown[]) => {
|
|
218
|
-
const message = String(args[0] ?? '');
|
|
219
|
-
// Suppress the specific sandbox warning
|
|
220
|
-
if (
|
|
221
|
-
message.includes('allow-scripts') &&
|
|
222
|
-
message.includes('allow-same-origin') &&
|
|
223
|
-
message.includes('sandbox')
|
|
224
|
-
) {
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
// Suppress "Layout was forced" warning from iframe content
|
|
228
|
-
// This is a performance warning that occurs when iframe content calculates layout before stylesheets load
|
|
229
|
-
// It's harmless and common in iframe scenarios, especially with React apps
|
|
230
|
-
if (
|
|
231
|
-
message.includes('Layout was forced') &&
|
|
232
|
-
message.includes('before the page was fully loaded')
|
|
233
|
-
) {
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
originalWarn.apply(console, args);
|
|
237
|
-
};
|
|
238
|
-
return () => {
|
|
239
|
-
console.warn = originalWarn;
|
|
240
|
-
};
|
|
241
|
-
}
|
|
242
|
-
}, []);
|
|
243
|
-
|
|
244
204
|
return (
|
|
245
205
|
<div
|
|
246
206
|
style={{ width: '100%', height: '100%', display: 'flex', position: 'relative' }}
|
|
@@ -256,7 +216,8 @@ export const ContentView = ({
|
|
|
256
216
|
- Reveal is instant (no transition) after deferred double-rAF to avoid blink */}
|
|
257
217
|
<iframe
|
|
258
218
|
ref={iframeRef}
|
|
259
|
-
src={
|
|
219
|
+
src={iframeUrl}
|
|
220
|
+
key={iframeUrl + navItem.path}
|
|
260
221
|
loading="eager"
|
|
261
222
|
style={{
|
|
262
223
|
width: '100%',
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
|
-
createContext,
|
|
3
|
-
useContext,
|
|
4
|
-
useState,
|
|
5
|
-
useCallback,
|
|
6
2
|
forwardRef,
|
|
7
|
-
type ReactNode,
|
|
8
3
|
type HTMLAttributes,
|
|
9
4
|
type ButtonHTMLAttributes,
|
|
10
5
|
type AnchorHTMLAttributes,
|
|
@@ -12,47 +7,16 @@ import {
|
|
|
12
7
|
import { Slot } from '@radix-ui/react-slot';
|
|
13
8
|
import { cva, type VariantProps } from 'class-variance-authority';
|
|
14
9
|
import { cn } from '../../lib/utils';
|
|
15
|
-
import { Z_INDEX } from '../../lib/z-index';
|
|
16
|
-
|
|
17
|
-
type SidebarContextValue = {
|
|
18
|
-
isCollapsed: boolean;
|
|
19
|
-
toggle: () => void;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
const SidebarContext = createContext<SidebarContextValue | undefined>(undefined);
|
|
23
|
-
|
|
24
|
-
const useSidebar = () => {
|
|
25
|
-
const context = useContext(SidebarContext);
|
|
26
|
-
if (!context) {
|
|
27
|
-
throw new Error('useSidebar must be used within a SidebarProvider');
|
|
28
|
-
}
|
|
29
|
-
return context;
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
const SidebarProvider = ({ children }: { children: ReactNode }) => {
|
|
33
|
-
const [isCollapsed, setIsCollapsed] = useState(false);
|
|
34
|
-
|
|
35
|
-
const toggle = useCallback(() => {
|
|
36
|
-
setIsCollapsed((prev) => !prev);
|
|
37
|
-
}, []);
|
|
38
|
-
|
|
39
|
-
return (
|
|
40
|
-
<SidebarContext.Provider value={{ isCollapsed, toggle }}>{children}</SidebarContext.Provider>
|
|
41
|
-
);
|
|
42
|
-
};
|
|
43
10
|
|
|
44
11
|
const Sidebar = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
45
12
|
({ className, ...props }, ref) => {
|
|
46
|
-
const { isCollapsed } = useSidebar();
|
|
47
|
-
|
|
48
13
|
return (
|
|
49
14
|
<div
|
|
50
15
|
ref={ref}
|
|
51
16
|
data-sidebar="sidebar"
|
|
52
|
-
data-collapsed={isCollapsed}
|
|
53
17
|
className={cn(
|
|
54
18
|
'flex h-full flex-col gap-2 border-r bg-sidebar-background p-2 text-sidebar-foreground transition-all duration-300 ease-in-out overflow-hidden',
|
|
55
|
-
|
|
19
|
+
'w-64',
|
|
56
20
|
className,
|
|
57
21
|
)}
|
|
58
22
|
{...props}
|
|
@@ -62,83 +26,6 @@ const Sidebar = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
|
62
26
|
);
|
|
63
27
|
Sidebar.displayName = 'Sidebar';
|
|
64
28
|
|
|
65
|
-
/** Inline SVG: panel-left-open (expand sidebar) */
|
|
66
|
-
const PanelLeftOpenIcon = () => (
|
|
67
|
-
<svg
|
|
68
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
69
|
-
width="24"
|
|
70
|
-
height="24"
|
|
71
|
-
viewBox="0 0 24 24"
|
|
72
|
-
fill="none"
|
|
73
|
-
stroke="currentColor"
|
|
74
|
-
strokeWidth="2"
|
|
75
|
-
strokeLinecap="round"
|
|
76
|
-
strokeLinejoin="round"
|
|
77
|
-
className="h-5 w-5 transition-transform duration-300"
|
|
78
|
-
aria-hidden
|
|
79
|
-
>
|
|
80
|
-
<rect
|
|
81
|
-
width="18"
|
|
82
|
-
height="18"
|
|
83
|
-
x="3"
|
|
84
|
-
y="3"
|
|
85
|
-
rx="2"
|
|
86
|
-
/>
|
|
87
|
-
<path d="M9 3v18" />
|
|
88
|
-
<path d="m14 9 3 3-3 3" />
|
|
89
|
-
</svg>
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
/** Inline SVG: panel-left-close (collapse sidebar) */
|
|
93
|
-
const PanelLeftCloseIcon = () => (
|
|
94
|
-
<svg
|
|
95
|
-
xmlns="http://www.w3.org/2000/svg"
|
|
96
|
-
width="24"
|
|
97
|
-
height="24"
|
|
98
|
-
viewBox="0 0 24 24"
|
|
99
|
-
fill="none"
|
|
100
|
-
stroke="currentColor"
|
|
101
|
-
strokeWidth="2"
|
|
102
|
-
strokeLinecap="round"
|
|
103
|
-
strokeLinejoin="round"
|
|
104
|
-
className="h-5 w-5 transition-transform duration-300"
|
|
105
|
-
aria-hidden
|
|
106
|
-
>
|
|
107
|
-
<rect
|
|
108
|
-
width="18"
|
|
109
|
-
height="18"
|
|
110
|
-
x="3"
|
|
111
|
-
y="3"
|
|
112
|
-
rx="2"
|
|
113
|
-
/>
|
|
114
|
-
<path d="M9 3v18" />
|
|
115
|
-
<path d="m16 15-3-3 3-3" />
|
|
116
|
-
</svg>
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
const SidebarTrigger = forwardRef<HTMLButtonElement, ButtonHTMLAttributes<HTMLButtonElement>>(
|
|
120
|
-
({ className, ...props }, ref) => {
|
|
121
|
-
const { toggle, isCollapsed } = useSidebar();
|
|
122
|
-
|
|
123
|
-
return (
|
|
124
|
-
<button
|
|
125
|
-
ref={ref}
|
|
126
|
-
onClick={toggle}
|
|
127
|
-
className={cn(
|
|
128
|
-
'relative flex items-center justify-center rounded-md p-2 text-foreground transition-colors hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring shadow-lg backdrop-blur-md cursor-pointer bg-background/95 border border-border',
|
|
129
|
-
className,
|
|
130
|
-
)}
|
|
131
|
-
aria-label={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
|
132
|
-
style={{ zIndex: Z_INDEX.SIDEBAR_TRIGGER }}
|
|
133
|
-
{...props}
|
|
134
|
-
>
|
|
135
|
-
{isCollapsed ? <PanelLeftOpenIcon /> : <PanelLeftCloseIcon />}
|
|
136
|
-
</button>
|
|
137
|
-
);
|
|
138
|
-
},
|
|
139
|
-
);
|
|
140
|
-
SidebarTrigger.displayName = 'SidebarTrigger';
|
|
141
|
-
|
|
142
29
|
const SidebarHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
|
|
143
30
|
({ className, ...props }, ref) => {
|
|
144
31
|
return (
|
|
@@ -271,19 +158,13 @@ const SidebarMenuButton = forwardRef<
|
|
|
271
158
|
isActive?: boolean;
|
|
272
159
|
}
|
|
273
160
|
>(({ className, variant, size, asChild = false, isActive, children, ...props }, ref) => {
|
|
274
|
-
const { isCollapsed } = useSidebar();
|
|
275
161
|
const Comp = asChild ? Slot : 'button';
|
|
276
162
|
|
|
277
163
|
return (
|
|
278
164
|
<Comp
|
|
279
165
|
ref={ref}
|
|
280
166
|
data-active={isActive}
|
|
281
|
-
|
|
282
|
-
className={cn(
|
|
283
|
-
sidebarMenuButtonVariants({ variant, size }),
|
|
284
|
-
isCollapsed && 'justify-center',
|
|
285
|
-
className,
|
|
286
|
-
)}
|
|
167
|
+
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
|
|
287
168
|
{...props}
|
|
288
169
|
>
|
|
289
170
|
{children}
|
|
@@ -434,8 +315,6 @@ SidebarMenuSubItem.displayName = 'SidebarMenuSubItem';
|
|
|
434
315
|
|
|
435
316
|
export {
|
|
436
317
|
Sidebar,
|
|
437
|
-
SidebarProvider,
|
|
438
|
-
SidebarTrigger,
|
|
439
318
|
SidebarHeader,
|
|
440
319
|
SidebarContent,
|
|
441
320
|
SidebarFooter,
|
|
@@ -452,5 +331,4 @@ export {
|
|
|
452
331
|
SidebarMenuSub,
|
|
453
332
|
SidebarMenuSubButton,
|
|
454
333
|
SidebarMenuSubItem,
|
|
455
|
-
useSidebar,
|
|
456
334
|
};
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import { lazy, Suspense, type LazyExoticComponent, type ComponentType } from 'react';
|
|
2
2
|
import type { LayoutType, NavigationItem, NavigationGroup } from '../config/types';
|
|
3
3
|
import { useSettings } from '../settings/SettingsContext';
|
|
4
|
+
import { SonnerProvider } from '../sonner/SonnerContext';
|
|
5
|
+
import { ModalProvider } from '../modal/ModalContext';
|
|
6
|
+
import { DrawerProvider } from '../drawer/DrawerContext';
|
|
7
|
+
import { OverlayShell } from './OverlayShell';
|
|
8
|
+
import { LayoutFallback } from './LayoutFallback';
|
|
4
9
|
|
|
5
|
-
const
|
|
6
|
-
import('./
|
|
10
|
+
const SidebarLayout = lazy(() =>
|
|
11
|
+
import('./sidebar/SidebarLayout').then((m) => ({ default: m.SidebarLayout })),
|
|
7
12
|
);
|
|
8
13
|
const FullscreenLayout = lazy(() =>
|
|
9
|
-
import('./FullscreenLayout').then((m) => ({ default: m.FullscreenLayout })),
|
|
14
|
+
import('./fullscreen/FullscreenLayout').then((m) => ({ default: m.FullscreenLayout })),
|
|
10
15
|
);
|
|
11
16
|
const WindowsLayout = lazy(() =>
|
|
12
|
-
import('./WindowsLayout').then((m) => ({ default: m.WindowsLayout })),
|
|
17
|
+
import('./windows/WindowsLayout').then((m) => ({ default: m.WindowsLayout })),
|
|
13
18
|
);
|
|
14
19
|
const AppBarLayout = lazy(() =>
|
|
15
|
-
import('./AppBarLayout').then((m) => ({ default: m.AppBarLayout })),
|
|
20
|
+
import('./appbar/AppBarLayout').then((m) => ({ default: m.AppBarLayout })),
|
|
16
21
|
);
|
|
17
22
|
|
|
18
23
|
interface AppLayoutProps {
|
|
@@ -23,15 +28,6 @@ interface AppLayoutProps {
|
|
|
23
28
|
navigation: (NavigationItem | NavigationGroup)[];
|
|
24
29
|
}
|
|
25
30
|
|
|
26
|
-
function LayoutFallback() {
|
|
27
|
-
return (
|
|
28
|
-
<div
|
|
29
|
-
className="min-h-screen bg-background"
|
|
30
|
-
aria-hidden
|
|
31
|
-
/>
|
|
32
|
-
);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
31
|
/** Renders the layout based on settings.layout (override) or config.layout: 'sidebar' (default), 'fullscreen', or 'windows'. Lazy-loads only the active layout. */
|
|
36
32
|
export function AppLayout({
|
|
37
33
|
layout = 'sidebar',
|
|
@@ -57,13 +53,20 @@ export function AppLayout({
|
|
|
57
53
|
LayoutComponent = AppBarLayout;
|
|
58
54
|
layoutProps = { title, appIcon, logo, navigation };
|
|
59
55
|
} else {
|
|
60
|
-
LayoutComponent =
|
|
56
|
+
LayoutComponent = SidebarLayout;
|
|
61
57
|
layoutProps = { title, appIcon, logo, navigation };
|
|
62
58
|
}
|
|
63
|
-
|
|
64
59
|
return (
|
|
65
|
-
<
|
|
66
|
-
<
|
|
67
|
-
|
|
60
|
+
<ModalProvider>
|
|
61
|
+
<DrawerProvider>
|
|
62
|
+
<SonnerProvider>
|
|
63
|
+
<OverlayShell>
|
|
64
|
+
<Suspense fallback={<LayoutFallback />}>
|
|
65
|
+
<LayoutComponent {...layoutProps} />
|
|
66
|
+
</Suspense>
|
|
67
|
+
</OverlayShell>
|
|
68
|
+
</SonnerProvider>
|
|
69
|
+
</DrawerProvider>
|
|
70
|
+
</ModalProvider>
|
|
68
71
|
);
|
|
69
72
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { useEffect, type ReactNode } from 'react';
|
|
2
|
-
import { useNavigate } from 'react-router';
|
|
1
|
+
import { useEffect, useRef, type ReactNode } from 'react';
|
|
2
|
+
import { useLocation, useNavigate } from 'react-router';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import { shellui } from '@shellui/sdk';
|
|
5
5
|
import type { NavigationItem } from '../config/types';
|
|
@@ -9,21 +9,18 @@ import { Toaster } from '../../components/ui/sonner';
|
|
|
9
9
|
import { ContentView } from '../../components/ContentView';
|
|
10
10
|
import { useModal } from '../modal/ModalContext';
|
|
11
11
|
import { useDrawer } from '../drawer/DrawerContext';
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
getBaseUrlWithoutHash,
|
|
15
|
-
isHashRouterNavItem,
|
|
16
|
-
resolveLocalizedString,
|
|
17
|
-
} from './utils';
|
|
12
|
+
import { useNavigationItems } from '../../routes/hooks/useNavigationItems';
|
|
13
|
+
import { getNavPathPrefix, resolveLocalizedString } from './utils';
|
|
18
14
|
|
|
19
15
|
interface OverlayShellProps {
|
|
20
|
-
navigationItems: NavigationItem[];
|
|
21
16
|
children: ReactNode;
|
|
22
17
|
}
|
|
23
18
|
|
|
24
19
|
/** Renders modal, drawer and toaster overlays and handles SHELLUI_OPEN_MODAL / SHELLUI_NAVIGATE. */
|
|
25
|
-
export
|
|
20
|
+
export const OverlayShell = ({ children }: OverlayShellProps) => {
|
|
21
|
+
const location = useLocation();
|
|
26
22
|
const navigate = useNavigate();
|
|
23
|
+
const { navigationItems } = useNavigationItems();
|
|
27
24
|
const { isOpen, modalUrl, closeModal } = useModal();
|
|
28
25
|
const {
|
|
29
26
|
isOpen: isDrawerOpen,
|
|
@@ -35,6 +32,17 @@ export function OverlayShell({ navigationItems, children }: OverlayShellProps) {
|
|
|
35
32
|
const { t, i18n } = useTranslation('common');
|
|
36
33
|
const currentLanguage = i18n.language || 'en';
|
|
37
34
|
|
|
35
|
+
// Close modal and drawer when app URL changes (navigation, back button) so overlay content stays url-specific
|
|
36
|
+
const locationKeyRef = useRef(location.pathname + location.search + location.hash);
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const currentKey = location.pathname + location.search + location.hash;
|
|
39
|
+
if (locationKeyRef.current !== currentKey) {
|
|
40
|
+
closeModal();
|
|
41
|
+
closeDrawer();
|
|
42
|
+
locationKeyRef.current = currentKey;
|
|
43
|
+
}
|
|
44
|
+
}, [location.pathname, location.search, location.hash, closeModal, closeDrawer]);
|
|
45
|
+
|
|
38
46
|
useEffect(() => {
|
|
39
47
|
const cleanup = shellui.addMessageListener('SHELLUI_OPEN_MODAL', () => {
|
|
40
48
|
closeDrawer();
|
|
@@ -53,34 +61,7 @@ export function OverlayShell({ navigationItems, children }: OverlayShellProps) {
|
|
|
53
61
|
|
|
54
62
|
let pathname: string;
|
|
55
63
|
|
|
56
|
-
|
|
57
|
-
if (rawUrl.includes('/#/')) {
|
|
58
|
-
try {
|
|
59
|
-
const parsed = new URL(rawUrl);
|
|
60
|
-
const hashPart = parsed.hash.slice(1); // strip leading #
|
|
61
|
-
const hashPath = hashPart.startsWith('/') ? hashPart : `/${hashPart}`;
|
|
62
|
-
const baseUrl = getBaseUrlWithoutHash(rawUrl);
|
|
63
|
-
const navItem = navigationItems.find(
|
|
64
|
-
(item) => isHashRouterNavItem(item) && getBaseUrlWithoutHash(item.url) === baseUrl,
|
|
65
|
-
);
|
|
66
|
-
if (!navItem) {
|
|
67
|
-
shellui.toast({
|
|
68
|
-
type: 'error',
|
|
69
|
-
title: t('navigationError') ?? 'Navigation error',
|
|
70
|
-
description:
|
|
71
|
-
t('navigationNotAllowed') ?? 'This URL is not configured in the app navigation.',
|
|
72
|
-
});
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
const pathPrefix = getNavPathPrefix(navItem);
|
|
76
|
-
pathname =
|
|
77
|
-
hashPath === '/' || hashPath === ''
|
|
78
|
-
? pathPrefix
|
|
79
|
-
: `${pathPrefix.replace(/\/$/, '')}${hashPath}`;
|
|
80
|
-
} catch {
|
|
81
|
-
pathname = rawUrl.startsWith('/') ? rawUrl : `/${rawUrl}`;
|
|
82
|
-
}
|
|
83
|
-
} else if (rawUrl.startsWith('http://') || rawUrl.startsWith('https://')) {
|
|
64
|
+
if (rawUrl.startsWith('http://') || rawUrl.startsWith('https://')) {
|
|
84
65
|
try {
|
|
85
66
|
pathname = new URL(rawUrl).pathname;
|
|
86
67
|
} catch {
|
|
@@ -110,7 +91,6 @@ export function OverlayShell({ navigationItems, children }: OverlayShellProps) {
|
|
|
110
91
|
});
|
|
111
92
|
return () => cleanup();
|
|
112
93
|
}, [navigate, closeModal, closeDrawer, navigationItems, t]);
|
|
113
|
-
|
|
114
94
|
return (
|
|
115
95
|
<>
|
|
116
96
|
{children}
|
|
@@ -202,4 +182,5 @@ export function OverlayShell({ navigationItems, children }: OverlayShellProps) {
|
|
|
202
182
|
<Toaster />
|
|
203
183
|
</>
|
|
204
184
|
);
|
|
205
|
-
}
|
|
185
|
+
};
|
|
186
|
+
export default OverlayShell;
|