@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
@@ -15,7 +15,6 @@ import {
15
15
  SidebarMenu,
16
16
  SidebarMenuButton,
17
17
  SidebarMenuItem,
18
- SidebarProvider,
19
18
  } from '../../components/ui/sidebar';
20
19
  import { Route, Routes, useLocation, useNavigate, Navigate } from 'react-router';
21
20
  import { useTranslation } from 'react-i18next';
@@ -188,74 +187,26 @@ export const SettingsView = () => {
188
187
  }, [navigate]);
189
188
 
190
189
  return (
191
- <SidebarProvider>
192
- <div className="flex h-full w-full overflow-hidden items-start">
193
- {/* Desktop Sidebar */}
194
- <Sidebar className="hidden md:flex">
195
- <SidebarContent>
196
- {groupedRoutes.map((group) => (
197
- <SidebarGroup key={group.title}>
198
- <SidebarGroupLabel>{group.title}</SidebarGroupLabel>
199
- <SidebarGroupContent>
200
- <SidebarMenu>
201
- {group.routes.map((item) => (
202
- <SidebarMenuItem key={item.name}>
203
- <SidebarMenuButton
204
- asChild
205
- isActive={item.name === selectedItem?.name}
190
+ <div className="flex h-full w-full overflow-hidden items-start">
191
+ {/* Desktop Sidebar */}
192
+ <Sidebar className="hidden md:flex">
193
+ <SidebarContent>
194
+ {groupedRoutes.map((group) => (
195
+ <SidebarGroup key={group.title}>
196
+ <SidebarGroupLabel>{group.title}</SidebarGroupLabel>
197
+ <SidebarGroupContent>
198
+ <SidebarMenu>
199
+ {group.routes.map((item) => (
200
+ <SidebarMenuItem key={item.name}>
201
+ <SidebarMenuButton
202
+ asChild
203
+ isActive={item.name === selectedItem?.name}
204
+ >
205
+ <button
206
+ onClick={() => navigate(`${urls.settings}/${item.path}`)}
207
+ className="cursor-pointer"
206
208
  >
207
- <button
208
- onClick={() => navigate(`${urls.settings}/${item.path}`)}
209
- className="cursor-pointer"
210
- >
211
- {'icon' in item && item.icon ? (
212
- <item.icon />
213
- ) : 'iconSrc' in item && item.iconSrc ? (
214
- <img
215
- src={item.iconSrc}
216
- alt=""
217
- className="h-4 w-4 shrink-0"
218
- />
219
- ) : (
220
- <span className="h-4 w-4 shrink-0" />
221
- )}
222
- <span>{item.name}</span>
223
- </button>
224
- </SidebarMenuButton>
225
- </SidebarMenuItem>
226
- ))}
227
- </SidebarMenu>
228
- </SidebarGroupContent>
229
- </SidebarGroup>
230
- ))}
231
- </SidebarContent>
232
- </Sidebar>
233
-
234
- {/* Mobile List View */}
235
- <div className="md:hidden flex h-full w-full flex-col overflow-hidden">
236
- {isSettingsRoot ? (
237
- // Show list of settings pages
238
- <div className="flex flex-1 flex-col overflow-y-auto bg-background">
239
- <header className="flex h-16 shrink-0 items-center justify-center px-4 border-b">
240
- <h1 className="text-lg font-semibold">{t('title')}</h1>
241
- </header>
242
- <div className="flex flex-1 flex-col p-4 gap-6">
243
- {groupedRoutes.map((group) => (
244
- <div
245
- key={group.title}
246
- className="flex flex-col gap-2"
247
- >
248
- <h2
249
- className="text-xs font-semibold text-foreground/60 uppercase tracking-wider px-2"
250
- style={{ fontFamily: 'var(--heading-font-family, inherit)' }}
251
- >
252
- {group.title}
253
- </h2>
254
- <div className="flex flex-col bg-card rounded-lg overflow-hidden border border-border">
255
- {group.routes.map((item, itemIndex) => {
256
- const isLast = itemIndex === group.routes.length - 1;
257
- const iconEl =
258
- 'icon' in item && item.icon ? (
209
+ {'icon' in item && item.icon ? (
259
210
  <item.icon />
260
211
  ) : 'iconSrc' in item && item.iconSrc ? (
261
212
  <img
@@ -265,127 +216,173 @@ export const SettingsView = () => {
265
216
  />
266
217
  ) : (
267
218
  <span className="h-4 w-4 shrink-0" />
268
- );
269
- return (
270
- <div
271
- key={item.name}
272
- className="relative"
273
- >
274
- {!isLast && (
275
- <div className="absolute left-0 right-0 bottom-0 h-[1px] bg-border" />
276
- )}
277
- <button
278
- onClick={() => navigate(`${urls.settings}/${item.path}`)}
279
- className="w-full flex items-center justify-between px-4 py-3 bg-transparent hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground transition-colors cursor-pointer rounded-none"
280
- >
281
- <div className="flex items-center gap-2 flex-1 min-w-0">
282
- <div className="flex-shrink-0 text-foreground/70">{iconEl}</div>
283
- <span className="text-sm font-normal text-foreground">
284
- {item.name}
285
- </span>
286
- </div>
287
- <div className="flex-shrink-0 ml-2 text-foreground/40">
288
- <ChevronRightIcon />
289
- </div>
290
- </button>
291
- </div>
219
+ )}
220
+ <span>{item.name}</span>
221
+ </button>
222
+ </SidebarMenuButton>
223
+ </SidebarMenuItem>
224
+ ))}
225
+ </SidebarMenu>
226
+ </SidebarGroupContent>
227
+ </SidebarGroup>
228
+ ))}
229
+ </SidebarContent>
230
+ </Sidebar>
231
+
232
+ {/* Mobile List View */}
233
+ <div className="md:hidden flex h-full w-full flex-col overflow-hidden">
234
+ {isSettingsRoot ? (
235
+ // Show list of settings pages
236
+ <div className="flex flex-1 flex-col overflow-y-auto bg-background">
237
+ <header className="flex h-16 shrink-0 items-center justify-center px-4 border-b">
238
+ <h1 className="text-lg font-semibold">{t('title')}</h1>
239
+ </header>
240
+ <div className="flex flex-1 flex-col p-4 gap-6">
241
+ {groupedRoutes.map((group) => (
242
+ <div
243
+ key={group.title}
244
+ className="flex flex-col gap-2"
245
+ >
246
+ <h2
247
+ className="text-xs font-semibold text-foreground/60 uppercase tracking-wider px-2"
248
+ style={{ fontFamily: 'var(--heading-font-family, inherit)' }}
249
+ >
250
+ {group.title}
251
+ </h2>
252
+ <div className="flex flex-col bg-card rounded-lg overflow-hidden border border-border">
253
+ {group.routes.map((item, itemIndex) => {
254
+ const isLast = itemIndex === group.routes.length - 1;
255
+ const iconEl =
256
+ 'icon' in item && item.icon ? (
257
+ <item.icon />
258
+ ) : 'iconSrc' in item && item.iconSrc ? (
259
+ <img
260
+ src={item.iconSrc}
261
+ alt=""
262
+ className="h-4 w-4 shrink-0"
263
+ />
264
+ ) : (
265
+ <span className="h-4 w-4 shrink-0" />
292
266
  );
293
- })}
294
- </div>
267
+ return (
268
+ <div
269
+ key={item.name}
270
+ className="relative"
271
+ >
272
+ {!isLast && (
273
+ <div className="absolute left-0 right-0 bottom-0 h-[1px] bg-border" />
274
+ )}
275
+ <button
276
+ onClick={() => navigate(`${urls.settings}/${item.path}`)}
277
+ className="w-full flex items-center justify-between px-4 py-3 bg-transparent hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground transition-colors cursor-pointer rounded-none"
278
+ >
279
+ <div className="flex items-center gap-2 flex-1 min-w-0">
280
+ <div className="flex-shrink-0 text-foreground/70">{iconEl}</div>
281
+ <span className="text-sm font-normal text-foreground">
282
+ {item.name}
283
+ </span>
284
+ </div>
285
+ <div className="flex-shrink-0 ml-2 text-foreground/40">
286
+ <ChevronRightIcon />
287
+ </div>
288
+ </button>
289
+ </div>
290
+ );
291
+ })}
295
292
  </div>
296
- ))}
297
- </div>
293
+ </div>
294
+ ))}
298
295
  </div>
299
- ) : (
300
- // Show selected settings page with back button
301
- <div className="flex h-full flex-1 flex-col overflow-hidden">
302
- <header className="flex h-16 shrink-0 items-center gap-2 px-4 border-b">
303
- <Button
304
- variant="ghost"
305
- size="icon"
306
- onClick={handleBackToSettings}
307
- className="mr-2"
308
- >
309
- <ChevronLeftIcon />
310
- </Button>
311
- <h1 className="text-lg font-semibold">{selectedItem?.name}</h1>
312
- </header>
313
- <div className="flex flex-1 flex-col gap-4 overflow-y-auto p-4 pt-4">
314
- <Routes>
296
+ </div>
297
+ ) : (
298
+ // Show selected settings page with back button
299
+ <div className="flex h-full flex-1 flex-col overflow-hidden">
300
+ <header className="flex h-16 shrink-0 items-center gap-2 px-4 border-b">
301
+ <Button
302
+ variant="ghost"
303
+ size="icon"
304
+ onClick={handleBackToSettings}
305
+ className="mr-2"
306
+ >
307
+ <ChevronLeftIcon />
308
+ </Button>
309
+ <h1 className="text-lg font-semibold">{selectedItem?.name}</h1>
310
+ </header>
311
+ <div className="flex flex-1 flex-col gap-4 overflow-y-auto p-4 pt-4">
312
+ <Routes>
313
+ <Route
314
+ index
315
+ element={
316
+ allRoutes.length > 0 ? (
317
+ <Navigate
318
+ to={`${urls.settings}/${allRoutes[0].path}`}
319
+ replace
320
+ />
321
+ ) : null
322
+ }
323
+ />
324
+ {allRoutes.map((item) => (
315
325
  <Route
316
- index
317
- element={
318
- allRoutes.length > 0 ? (
319
- <Navigate
320
- to={`${urls.settings}/${allRoutes[0].path}`}
321
- replace
322
- />
323
- ) : null
324
- }
326
+ key={item.path}
327
+ path={item.path}
328
+ element={item.element}
325
329
  />
326
- {allRoutes.map((item) => (
327
- <Route
328
- key={item.path}
329
- path={item.path}
330
- element={item.element}
331
- />
332
- ))}
333
- </Routes>
334
- </div>
330
+ ))}
331
+ </Routes>
335
332
  </div>
336
- )}
337
- </div>
333
+ </div>
334
+ )}
335
+ </div>
338
336
 
339
- {/* Desktop Main Content */}
340
- <main className="hidden md:flex h-full flex-1 flex-col overflow-hidden">
341
- {selectedItem && (
342
- <header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear">
343
- <div className="flex items-center gap-2 px-4">
344
- <Breadcrumb>
345
- <BreadcrumbList>
346
- <BreadcrumbItem>{t('title')}</BreadcrumbItem>
347
- <BreadcrumbSeparator />
348
- <BreadcrumbItem>
349
- <BreadcrumbPage>{selectedItem.name}</BreadcrumbPage>
350
- </BreadcrumbItem>
351
- </BreadcrumbList>
352
- </Breadcrumb>
353
- </div>
354
- </header>
337
+ {/* Desktop Main Content */}
338
+ <main className="hidden md:flex h-full flex-1 flex-col overflow-hidden">
339
+ {selectedItem && (
340
+ <header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear">
341
+ <div className="flex items-center gap-2 px-4">
342
+ <Breadcrumb>
343
+ <BreadcrumbList>
344
+ <BreadcrumbItem>{t('title')}</BreadcrumbItem>
345
+ <BreadcrumbSeparator />
346
+ <BreadcrumbItem>
347
+ <BreadcrumbPage>{selectedItem.name}</BreadcrumbPage>
348
+ </BreadcrumbItem>
349
+ </BreadcrumbList>
350
+ </Breadcrumb>
351
+ </div>
352
+ </header>
353
+ )}
354
+ <div
355
+ className={cn(
356
+ 'flex flex-1 flex-col gap-4 overflow-y-auto',
357
+ !selectedItem?.path?.startsWith('app-') && 'p-4 pt-0',
355
358
  )}
356
- <div
357
- className={cn(
358
- 'flex flex-1 flex-col gap-4 overflow-y-auto',
359
- !selectedItem?.path?.startsWith('app-') && 'p-4 pt-0',
360
- )}
361
- >
362
- <Routes>
363
- <Route
364
- index
365
- element={
366
- <div className="flex flex-1 flex-col items-center justify-center p-8 text-center">
367
- <div className="max-w-md">
368
- <h2 className="text-lg font-semibold mb-2">{t('title')}</h2>
369
- <p className="text-sm text-muted-foreground">
370
- {t('selectCategory', {
371
- defaultValue: 'Select a category from the sidebar to get started.',
372
- })}
373
- </p>
374
- </div>
359
+ >
360
+ <Routes>
361
+ <Route
362
+ index
363
+ element={
364
+ <div className="flex flex-1 flex-col items-center justify-center p-8 text-center">
365
+ <div className="max-w-md">
366
+ <h2 className="text-lg font-semibold mb-2">{t('title')}</h2>
367
+ <p className="text-sm text-muted-foreground">
368
+ {t('selectCategory', {
369
+ defaultValue: 'Select a category from the sidebar to get started.',
370
+ })}
371
+ </p>
375
372
  </div>
376
- }
373
+ </div>
374
+ }
375
+ />
376
+ {allRoutes.map((item) => (
377
+ <Route
378
+ key={item.path}
379
+ path={item.path}
380
+ element={item.element}
377
381
  />
378
- {allRoutes.map((item) => (
379
- <Route
380
- key={item.path}
381
- path={item.path}
382
- element={item.element}
383
- />
384
- ))}
385
- </Routes>
386
- </div>
387
- </main>
388
- </div>
389
- </SidebarProvider>
382
+ ))}
383
+ </Routes>
384
+ </div>
385
+ </main>
386
+ </div>
390
387
  );
391
388
  };
@@ -1,5 +1,5 @@
1
1
  import { useTranslation } from 'react-i18next';
2
- import { useConfig } from '../features/config/useConfig';
2
+ import { useConfig } from '../../features/config/useConfig';
3
3
 
4
4
  export const HomeView = () => {
5
5
  const { t } = useTranslation('common');
@@ -1,8 +1,8 @@
1
1
  import { Navigate } from 'react-router';
2
- import { useConfig } from '../features/config/useConfig';
3
- import { flattenNavigationItems } from '../features/layouts/utils';
2
+ import { useConfig } from '../../features/config/useConfig';
3
+ import { flattenNavigationItems } from '../../features/layouts/utils';
4
4
  import { HomeView } from './HomeView';
5
- import { ViewRoute } from './ViewRoute';
5
+ import { NavigationItemRoute } from './NavigationItemRoute';
6
6
 
7
7
  /**
8
8
  * Renders the root path "/":
@@ -29,7 +29,7 @@ export const IndexRoute = () => {
29
29
  const navigationItems = flattenNavigationItems(navigation);
30
30
  const rootNavItem = navigationItems.find((item) => item.path === '' || item.path === '/');
31
31
  if (rootNavItem) {
32
- return <ViewRoute navigation={navigationItems} />;
32
+ return <NavigationItemRoute />;
33
33
  }
34
34
  }
35
35
 
@@ -0,0 +1,19 @@
1
+ import { ContentView } from '../../components/ContentView';
2
+ import { useNavigationItems } from '../hooks/useNavigationItems';
3
+ import { NotFoundView } from './NotFoundView';
4
+
5
+ export const NavigationItemRoute = () => {
6
+ const { url, currentItem } = useNavigationItems();
7
+
8
+ if (!currentItem) {
9
+ return <NotFoundView />;
10
+ }
11
+
12
+ return (
13
+ <ContentView
14
+ url={url}
15
+ pathPrefix={currentItem.path}
16
+ navItem={currentItem}
17
+ />
18
+ );
19
+ };
@@ -1,8 +1,8 @@
1
1
  import { useTranslation } from 'react-i18next';
2
2
  import { shellui } from '@shellui/sdk';
3
- import { useConfig } from '../features/config/useConfig';
4
- import { getNavPathPrefix } from '../features/layouts/utils';
5
- import type { NavigationItem, NavigationGroup } from '../features/config/types';
3
+ import { useConfig } from '../../features/config/useConfig';
4
+ import { getNavPathPrefix } from '../../features/layouts/utils';
5
+ import type { NavigationItem, NavigationGroup } from '../../features/config/types';
6
6
 
7
7
  const flattenNavigationItems = (
8
8
  navigation: (NavigationItem | NavigationGroup)[],
@@ -1,7 +1,7 @@
1
1
  import { useRouteError, isRouteErrorResponse } from 'react-router';
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import { shellui } from '@shellui/sdk';
4
- import { Button } from './ui/button';
4
+ import { Button } from '../../components/ui/button';
5
5
 
6
6
  function isChunkLoadError(error: unknown): boolean {
7
7
  if (error instanceof Error) {
@@ -0,0 +1,8 @@
1
+ export function RouteFallback() {
2
+ return (
3
+ <div
4
+ className="min-h-screen bg-background"
5
+ aria-hidden
6
+ />
7
+ );
8
+ }
@@ -0,0 +1,84 @@
1
+ import { useMemo } from 'react';
2
+ import { useConfig } from '../../features/config/useConfig';
3
+ import {
4
+ flattenNavigationItems,
5
+ getBaseUrlWithoutHash,
6
+ getHashPathFromUrl,
7
+ getNavPathPrefix,
8
+ isHashRouterNavItem,
9
+ } from '../../features/layouts/utils';
10
+ import { useLocation } from 'react-router';
11
+
12
+ export function useNavigationItems() {
13
+ const { config } = useConfig();
14
+ const location = useLocation();
15
+
16
+ const navigationItems = useMemo(() => {
17
+ return flattenNavigationItems(config?.navigation ?? []);
18
+ }, [config]);
19
+
20
+ const navigationItem = useMemo(() => {
21
+ return navigationItems.find((item) => {
22
+ const pathPrefix = getNavPathPrefix(item);
23
+ return location.pathname === pathPrefix || location.pathname.startsWith(`${pathPrefix}/`);
24
+ });
25
+ }, [navigationItems, location.pathname]);
26
+
27
+ // When no nav matches (e.g. /layout on refresh): use root item (path '' or '/') with pathname as hash subpath to avoid 404
28
+ const rootItem = useMemo(
29
+ () => navigationItems.find((item) => item.path === '' || item.path === '/'),
30
+ [navigationItems],
31
+ );
32
+
33
+ /**
34
+ * Constructs the final URL for the iframe based on the navigation item and the pathname.
35
+ * If the navigation item is a hash router item, it preserves the hash path and appends the subpath.
36
+ * If the navigation item is not a hash router item, it appends the subpath to the base URL.
37
+ * If the navigation item is not found, it returns an empty string.
38
+ */
39
+ const url = useMemo(() => {
40
+ const pathname = location.pathname;
41
+
42
+ const useRootFallback = !navigationItem && rootItem && pathname !== '/';
43
+ const actualNavItem = navigationItem ?? (useRootFallback ? rootItem : null);
44
+
45
+ if (!actualNavItem) {
46
+ return '';
47
+ }
48
+ const actualSubPath = useRootFallback
49
+ ? pathname.replace(/^\//, '')
50
+ : actualNavItem
51
+ ? pathname.length > getNavPathPrefix(actualNavItem).length
52
+ ? pathname.slice(getNavPathPrefix(actualNavItem).length + 1)
53
+ : ''
54
+ : '';
55
+
56
+ const subPath = actualSubPath;
57
+ // Construct the final URL for the iframe (non-hash: base + path; hash app: preserve nav url hash path + subPath)
58
+ let finalUrl: string;
59
+ if (isHashRouterNavItem(actualNavItem)) {
60
+ const base = getBaseUrlWithoutHash(actualNavItem.url).replace(/\/$/, '');
61
+ const navHashPath = getHashPathFromUrl(actualNavItem.url).replace(/^\/+|\/+$/g, '');
62
+ const segments = [navHashPath, subPath].filter(Boolean);
63
+ const fullHashPath = `/${segments.join('/')}`;
64
+ finalUrl = `${base}#${fullHashPath}`;
65
+ } else {
66
+ finalUrl = actualNavItem.url;
67
+ if (subPath) {
68
+ const baseUrl = actualNavItem.url.endsWith('/')
69
+ ? actualNavItem.url
70
+ : `${actualNavItem.url}/`;
71
+ finalUrl = `${baseUrl}${subPath}`;
72
+ }
73
+ }
74
+ return finalUrl;
75
+ }, [navigationItem, rootItem, location.pathname]);
76
+
77
+ return {
78
+ url: url,
79
+ rootItem: rootItem,
80
+ currentItem: navigationItem || rootItem,
81
+ navigationItem: navigationItem || rootItem,
82
+ navigationItems: navigationItems,
83
+ };
84
+ }
@@ -1,10 +1,11 @@
1
1
  import { lazy, Suspense } from 'react';
2
2
  import { Outlet, type RouteObject } from 'react-router';
3
3
  import type { ShellUIConfig } from '../features/config/types';
4
- import { RouteErrorBoundary } from '../components/RouteErrorBoundary';
4
+ import { RouteErrorBoundary } from './components/RouteErrorBoundary';
5
5
  import { AppLayout } from '../features/layouts/AppLayout';
6
6
  import { flattenNavigationItems } from '../features/layouts/utils';
7
7
  import urls from '../constants/urls';
8
+ import { RouteFallback } from './components/RouteFallback';
8
9
 
9
10
  // Lazy load route components
10
11
  const SettingsView = lazy(() =>
@@ -15,25 +16,16 @@ const CookiePreferencesView = lazy(() =>
15
16
  default: m.CookiePreferencesView,
16
17
  })),
17
18
  );
18
- const ViewRoute = lazy(() =>
19
- import('../components/ViewRoute').then((m) => ({ default: m.ViewRoute })),
19
+ const NavigationItemRoute = lazy(() =>
20
+ import('./components/NavigationItemRoute').then((m) => ({ default: m.NavigationItemRoute })),
20
21
  );
21
22
  const IndexRoute = lazy(() =>
22
- import('../components/IndexRoute').then((m) => ({ default: m.IndexRoute })),
23
+ import('./components/IndexRoute').then((m) => ({ default: m.IndexRoute })),
23
24
  );
24
25
  const NotFoundView = lazy(() =>
25
- import('../components/NotFoundView').then((m) => ({ default: m.NotFoundView })),
26
+ import('./components/NotFoundView').then((m) => ({ default: m.NotFoundView })),
26
27
  );
27
28
 
28
- function RouteFallback() {
29
- return (
30
- <div
31
- className="min-h-screen bg-background"
32
- aria-hidden
33
- />
34
- );
35
- }
36
-
37
29
  export const createRoutes = (config: ShellUIConfig): RouteObject[] => {
38
30
  const routes: RouteObject[] = [
39
31
  {
@@ -104,22 +96,22 @@ export const createRoutes = (config: ShellUIConfig): RouteObject[] => {
104
96
  path: `/${item.path}/*`,
105
97
  element: (
106
98
  <Suspense fallback={<RouteFallback />}>
107
- <ViewRoute navigation={navigationItems} />
99
+ <NavigationItemRoute />
108
100
  </Suspense>
109
101
  ),
110
102
  });
111
103
  });
112
- // Catch-all: no nav match (e.g. /layout) → ViewRoute can use root item with pathname as hash subpath to avoid 404
104
+ // Catch-all: no nav match (e.g. /layout) → NavigationItemRoute can use root item with pathname as hash subpath to avoid 404
113
105
  (layoutRoute.children as RouteObject[]).push({
114
106
  path: '*',
115
107
  element: (
116
108
  <Suspense fallback={<RouteFallback />}>
117
- <ViewRoute navigation={navigationItems} />
109
+ <NavigationItemRoute />
118
110
  </Suspense>
119
111
  ),
120
112
  });
121
113
  }
122
- // Layout must be before the catch-all (*) so paths like /layout are handled by layout → ViewRoute (root fallback), not 404
114
+ // Layout must be before the catch-all (*) so paths like /layout are handled by layout → NavigationItemRoute (root fallback), not 404
123
115
  (routes[0].children as RouteObject[]).unshift(layoutRoute);
124
116
 
125
117
  return routes;