@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.
Files changed (60) hide show
  1. package/package.json +2 -2
  2. package/src/app.tsx +1 -1
  3. package/src/components/ContentView.tsx +26 -58
  4. package/src/components/LoadingOverlay.tsx +1 -1
  5. package/src/components/ui/sidebar.tsx +2 -124
  6. package/src/features/layouts/AppLayout.tsx +22 -19
  7. package/src/features/layouts/LayoutFallback.tsx +8 -0
  8. package/src/features/layouts/OverlayShell.tsx +21 -40
  9. package/src/features/layouts/{AppBarLayout.tsx → appbar/AppBarLayout.tsx} +72 -78
  10. package/src/features/layouts/{FullscreenLayout.tsx → fullscreen/FullscreenLayout.tsx} +5 -11
  11. package/src/features/layouts/sidebar/BottomNavItem.tsx +88 -0
  12. package/src/features/layouts/sidebar/MobileBottomNav.tsx +168 -0
  13. package/src/features/layouts/sidebar/NavigationContent.tsx +159 -0
  14. package/src/features/layouts/sidebar/SidebarIcons.tsx +93 -0
  15. package/src/features/layouts/sidebar/SidebarInner.tsx +48 -0
  16. package/src/features/layouts/sidebar/SidebarLayout.tsx +86 -0
  17. package/src/features/layouts/sidebar/sidebarUtils.ts +23 -0
  18. package/src/features/layouts/sidebar/types.ts +8 -0
  19. package/src/features/layouts/utils.ts +1 -1
  20. package/src/features/layouts/{WindowsLayout.tsx → windows/WindowsLayout.tsx} +199 -204
  21. package/src/features/settings/SettingsView.tsx +177 -180
  22. package/src/{components → routes/components}/HomeView.tsx +1 -1
  23. package/src/{components → routes/components}/IndexRoute.tsx +4 -4
  24. package/src/routes/components/NavigationItemRoute.tsx +19 -0
  25. package/src/{components → routes/components}/NotFoundView.tsx +3 -3
  26. package/src/{components → routes/components}/RouteErrorBoundary.tsx +1 -1
  27. package/src/routes/components/RouteFallback.tsx +8 -0
  28. package/src/routes/hooks/useNavigationItems.ts +84 -0
  29. package/src/{router → routes}/routes.tsx +10 -18
  30. package/src/components/ViewRoute.tsx +0 -74
  31. package/src/dist/CookiePreferencesView.52b5aec8.js +0 -1182
  32. package/src/dist/CookiePreferencesView.52b5aec8.js.map +0 -1
  33. package/src/dist/DefaultLayout.045a82ff.js +0 -1964
  34. package/src/dist/DefaultLayout.045a82ff.js.map +0 -1
  35. package/src/dist/DefaultLayout.4454f259.js +0 -4414
  36. package/src/dist/DefaultLayout.4454f259.js.map +0 -1
  37. package/src/dist/FullscreenLayout.555c4987.js +0 -1054
  38. package/src/dist/FullscreenLayout.555c4987.js.map +0 -1
  39. package/src/dist/HomeView.ddfa7b68.js +0 -771
  40. package/src/dist/HomeView.ddfa7b68.js.map +0 -1
  41. package/src/dist/NotFoundView.c75be4f1.js +0 -811
  42. package/src/dist/NotFoundView.c75be4f1.js.map +0 -1
  43. package/src/dist/SettingsView.052b03a6.js +0 -4965
  44. package/src/dist/SettingsView.052b03a6.js.map +0 -1
  45. package/src/dist/ViewRoute.e6e3b142.js +0 -1042
  46. package/src/dist/ViewRoute.e6e3b142.js.map +0 -1
  47. package/src/dist/WindowsLayout.08724167.js +0 -1762
  48. package/src/dist/WindowsLayout.08724167.js.map +0 -1
  49. package/src/dist/esm.f0d741e6.js +0 -29520
  50. package/src/dist/esm.f0d741e6.js.map +0 -1
  51. package/src/dist/favicon.4367ac1e.svg +0 -14
  52. package/src/dist/index.parcel.36d65383.js +0 -54089
  53. package/src/dist/index.parcel.36d65383.js.map +0 -1
  54. package/src/dist/index.parcel.ca6d8a47.css +0 -3493
  55. package/src/dist/index.parcel.ca6d8a47.css.map +0 -1
  56. package/src/dist/index.parcel.html +0 -88
  57. package/src/features/layouts/DefaultLayout.tsx +0 -670
  58. package/src/features/layouts/LayoutProviders.tsx +0 -20
  59. /package/src/{constants.ts → constants/loading.ts} +0 -0
  60. /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.0",
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.0"
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 './router/router';
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
- const urlRef = useRef(url);
40
- urlRef.current = url;
41
- const [initialUrl] = useState(url);
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 && url === lastLoadedIframeUrl) return false;
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={initialUrl}
227
+ src={iframeUrl}
260
228
  loading="eager"
261
229
  style={{
262
230
  width: '100%',
@@ -1,4 +1,4 @@
1
- import { LOADING_OVERLAY_DURATION_MS } from '../constants';
1
+ import { LOADING_OVERLAY_DURATION_MS } from '../constants/loading';
2
2
 
3
3
  export function LoadingOverlay() {
4
4
  return (
@@ -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
- isCollapsed ? 'w-0 border-r-0 p-0' : 'w-64',
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
- data-collapsed={isCollapsed}
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 DefaultLayout = lazy(() =>
6
- import('./DefaultLayout').then((m) => ({ default: m.DefaultLayout })),
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 = DefaultLayout;
56
+ LayoutComponent = SidebarLayout;
61
57
  layoutProps = { title, appIcon, logo, navigation };
62
58
  }
63
-
64
59
  return (
65
- <Suspense fallback={<LayoutFallback />}>
66
- <LayoutComponent {...layoutProps} />
67
- </Suspense>
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
  }
@@ -0,0 +1,8 @@
1
+ export function LayoutFallback() {
2
+ return (
3
+ <div
4
+ className="min-h-screen bg-background"
5
+ aria-hidden
6
+ />
7
+ );
8
+ }
@@ -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
- getNavPathPrefix,
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 function OverlayShell({ navigationItems, children }: OverlayShellProps) {
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
- // Hash-based URL (e.g. http://localhost:5173/#/themes/foo): show non-hash path in shell, match by nav item base URL
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;