@shellui/core 0.2.0-alpha.4 → 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 +2 -2
- package/src/components/ContentView.tsx +70 -135
- package/src/components/LoadingOverlay.tsx +5 -1
- package/src/components/ui/sidebar.tsx +2 -124
- package/src/constants/loading.ts +2 -0
- package/src/features/config/types.ts +2 -0
- package/src/features/layouts/AppLayout.tsx +22 -19
- package/src/features/layouts/LayoutFallback.tsx +8 -0
- package/src/features/layouts/OverlayShell.tsx +23 -9
- 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 +29 -1
- package/src/features/layouts/{WindowsLayout.tsx → windows/WindowsLayout.tsx} +199 -204
- package/src/features/settings/SettingsView.tsx +178 -181
- 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 +9 -4
- 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 +18 -16
- package/src/components/ViewRoute.tsx +0 -48
- 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 -660
- package/src/features/layouts/LayoutProviders.tsx +0 -20
- /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';
|
|
@@ -184,78 +183,30 @@ export const SettingsView = () => {
|
|
|
184
183
|
// Navigate back to settings root
|
|
185
184
|
const handleBackToSettings = useCallback(() => {
|
|
186
185
|
// Navigate to settings root, replacing current history entry
|
|
187
|
-
navigate(urls.settings
|
|
186
|
+
navigate(urls.settings);
|
|
188
187
|
}, [navigate]);
|
|
189
188
|
|
|
190
189
|
return (
|
|
191
|
-
<
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
<
|
|
195
|
-
|
|
196
|
-
{
|
|
197
|
-
<
|
|
198
|
-
|
|
199
|
-
<
|
|
200
|
-
|
|
201
|
-
{
|
|
202
|
-
<
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
-
|
|
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
|
-
|
|
293
|
+
</div>
|
|
294
|
+
))}
|
|
298
295
|
</div>
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
</
|
|
313
|
-
|
|
314
|
-
|
|
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
|
-
|
|
317
|
-
|
|
318
|
-
|
|
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
|
-
|
|
327
|
-
|
|
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
|
-
|
|
333
|
+
</div>
|
|
334
|
+
)}
|
|
335
|
+
</div>
|
|
338
336
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
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
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
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,8 +1,8 @@
|
|
|
1
1
|
import { Navigate } from 'react-router';
|
|
2
|
-
import { useConfig } from '
|
|
3
|
-
import { flattenNavigationItems } from '
|
|
2
|
+
import { useConfig } from '../../features/config/useConfig';
|
|
3
|
+
import { flattenNavigationItems } from '../../features/layouts/utils';
|
|
4
4
|
import { HomeView } from './HomeView';
|
|
5
|
-
import {
|
|
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 <
|
|
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 '
|
|
4
|
-
import { getNavPathPrefix } from '
|
|
5
|
-
import type { NavigationItem, NavigationGroup } from '
|
|
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)[],
|
|
@@ -37,7 +37,12 @@ export const NotFoundView = () => {
|
|
|
37
37
|
: [];
|
|
38
38
|
|
|
39
39
|
const handleNavigate = (path: string) => {
|
|
40
|
-
|
|
40
|
+
const targetPath = path.startsWith('/') ? path : `/${path}`;
|
|
41
|
+
if (window.self !== window.top) {
|
|
42
|
+
shellui.navigate(targetPath);
|
|
43
|
+
} else {
|
|
44
|
+
window.location.href = targetPath;
|
|
45
|
+
}
|
|
41
46
|
};
|
|
42
47
|
|
|
43
48
|
return (
|
|
@@ -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 '
|
|
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,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 '
|
|
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
|
|
19
|
-
import('
|
|
19
|
+
const NavigationItemRoute = lazy(() =>
|
|
20
|
+
import('./components/NavigationItemRoute').then((m) => ({ default: m.NavigationItemRoute })),
|
|
20
21
|
);
|
|
21
22
|
const IndexRoute = lazy(() =>
|
|
22
|
-
import('
|
|
23
|
+
import('./components/IndexRoute').then((m) => ({ default: m.IndexRoute })),
|
|
23
24
|
);
|
|
24
25
|
const NotFoundView = lazy(() =>
|
|
25
|
-
import('
|
|
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,13 +96,23 @@ export const createRoutes = (config: ShellUIConfig): RouteObject[] => {
|
|
|
104
96
|
path: `/${item.path}/*`,
|
|
105
97
|
element: (
|
|
106
98
|
<Suspense fallback={<RouteFallback />}>
|
|
107
|
-
<
|
|
99
|
+
<NavigationItemRoute />
|
|
108
100
|
</Suspense>
|
|
109
101
|
),
|
|
110
102
|
});
|
|
111
103
|
});
|
|
104
|
+
// Catch-all: no nav match (e.g. /layout) → NavigationItemRoute can use root item with pathname as hash subpath to avoid 404
|
|
105
|
+
(layoutRoute.children as RouteObject[]).push({
|
|
106
|
+
path: '*',
|
|
107
|
+
element: (
|
|
108
|
+
<Suspense fallback={<RouteFallback />}>
|
|
109
|
+
<NavigationItemRoute />
|
|
110
|
+
</Suspense>
|
|
111
|
+
),
|
|
112
|
+
});
|
|
112
113
|
}
|
|
113
|
-
(
|
|
114
|
+
// Layout must be before the catch-all (*) so paths like /layout are handled by layout → NavigationItemRoute (root fallback), not 404
|
|
115
|
+
(routes[0].children as RouteObject[]).unshift(layoutRoute);
|
|
114
116
|
|
|
115
117
|
return routes;
|
|
116
118
|
};
|