@shellui/core 0.2.0-beta.0 → 0.2.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.
- package/package.json +2 -2
- package/src/app.tsx +1 -1
- package/src/components/ContentView.tsx +26 -58
- 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.1",
|
|
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.1"
|
|
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 {
|
|
@@ -11,14 +10,11 @@ import {
|
|
|
11
10
|
} from '@shellui/sdk';
|
|
12
11
|
import { useEffect, 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,15 +32,20 @@ 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
|
-
|
|
41
|
-
|
|
35
|
+
/** Real history methods; stored once so we never restore our no-ops after a second effect run. */
|
|
36
|
+
const historyOriginalsRef = useRef<{
|
|
37
|
+
pushState: History['pushState'];
|
|
38
|
+
replaceState: History['replaceState'];
|
|
39
|
+
} | null>(null);
|
|
40
|
+
|
|
42
41
|
const [isLoading, setIsLoading] = useState(() => {
|
|
43
42
|
// Skip overlay when same app URL was just loaded (e.g. switching App ↔ Root with same url)
|
|
44
|
-
if (!ignoreMessages
|
|
43
|
+
if (!ignoreMessages) return false;
|
|
45
44
|
return true;
|
|
46
45
|
});
|
|
47
46
|
|
|
47
|
+
const [iframeUrl, setIframeUrl] = useState(url);
|
|
48
|
+
|
|
48
49
|
const MIN_LOADING_MS = 80; // Don't reveal before this, reduces blink from theme/layout paint
|
|
49
50
|
|
|
50
51
|
useEffect(() => {
|
|
@@ -57,6 +58,18 @@ export const ContentView = ({
|
|
|
57
58
|
};
|
|
58
59
|
}, []);
|
|
59
60
|
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (ignoreMessages) return;
|
|
63
|
+
if (isLoading) return;
|
|
64
|
+
if (iframeRef.current) {
|
|
65
|
+
setIsLoading(true);
|
|
66
|
+
setIframeUrl('about:blank');
|
|
67
|
+
setTimeout(() => {
|
|
68
|
+
setIframeUrl(url);
|
|
69
|
+
}, 60);
|
|
70
|
+
}
|
|
71
|
+
}, [navItem]);
|
|
72
|
+
|
|
60
73
|
// Sync parent URL when iframe notifies us of a change
|
|
61
74
|
useEffect(() => {
|
|
62
75
|
const cleanup = shellui.addMessageListener(
|
|
@@ -66,6 +79,8 @@ export const ContentView = ({
|
|
|
66
79
|
return;
|
|
67
80
|
}
|
|
68
81
|
|
|
82
|
+
if (isLoading) return;
|
|
83
|
+
|
|
69
84
|
// Ignore URL CHANGE from other than ContentView iframe
|
|
70
85
|
if (event.source !== iframeRef.current?.contentWindow) {
|
|
71
86
|
return;
|
|
@@ -122,7 +137,7 @@ export const ContentView = ({
|
|
|
122
137
|
const normalizedNewPath = normalizedNewPathname + (newPathParts?.[2] || '');
|
|
123
138
|
|
|
124
139
|
if (currentPath !== normalizedNewPath) {
|
|
125
|
-
navigate(newShellPath);
|
|
140
|
+
navigate(newShellPath, { replace: true });
|
|
126
141
|
}
|
|
127
142
|
},
|
|
128
143
|
);
|
|
@@ -156,7 +171,6 @@ export const ContentView = ({
|
|
|
156
171
|
'SHELLUI_INITIALIZED',
|
|
157
172
|
(_data: ShellUIMessage, event: MessageEvent) => {
|
|
158
173
|
if (event.source !== iframeRef.current?.contentWindow) return;
|
|
159
|
-
if (!ignoreMessages) lastLoadedIframeUrl = urlRef.current;
|
|
160
174
|
cancelRevealRef.current?.();
|
|
161
175
|
let cancelled = false;
|
|
162
176
|
cancelRevealRef.current = () => {
|
|
@@ -195,52 +209,6 @@ export const ContentView = ({
|
|
|
195
209
|
return () => clearTimeout(timeoutId);
|
|
196
210
|
}, [isLoading]);
|
|
197
211
|
|
|
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
212
|
return (
|
|
245
213
|
<div
|
|
246
214
|
style={{ width: '100%', height: '100%', display: 'flex', position: 'relative' }}
|
|
@@ -256,7 +224,7 @@ export const ContentView = ({
|
|
|
256
224
|
- Reveal is instant (no transition) after deferred double-rAF to avoid blink */}
|
|
257
225
|
<iframe
|
|
258
226
|
ref={iframeRef}
|
|
259
|
-
src={
|
|
227
|
+
src={iframeUrl}
|
|
260
228
|
loading="eager"
|
|
261
229
|
style={{
|
|
262
230
|
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;
|