@shellui/core 0.2.0-beta.0 → 0.2.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/app.tsx +1 -1
- package/src/components/ContentView.tsx +22 -61
- package/src/components/LoadingOverlay.tsx +1 -1
- package/src/components/ui/sidebar.tsx +2 -124
- package/src/features/layouts/AppLayout.tsx +22 -19
- package/src/features/layouts/LayoutFallback.tsx +8 -0
- package/src/features/layouts/OverlayShell.tsx +21 -40
- package/src/features/layouts/{AppBarLayout.tsx → appbar/AppBarLayout.tsx} +72 -78
- package/src/features/layouts/{FullscreenLayout.tsx → fullscreen/FullscreenLayout.tsx} +5 -11
- package/src/features/layouts/sidebar/BottomNavItem.tsx +88 -0
- package/src/features/layouts/sidebar/MobileBottomNav.tsx +168 -0
- package/src/features/layouts/sidebar/NavigationContent.tsx +159 -0
- package/src/features/layouts/sidebar/SidebarIcons.tsx +93 -0
- package/src/features/layouts/sidebar/SidebarInner.tsx +48 -0
- package/src/features/layouts/sidebar/SidebarLayout.tsx +86 -0
- package/src/features/layouts/sidebar/sidebarUtils.ts +23 -0
- package/src/features/layouts/sidebar/types.ts +8 -0
- package/src/features/layouts/utils.ts +1 -1
- package/src/features/layouts/{WindowsLayout.tsx → windows/WindowsLayout.tsx} +199 -204
- package/src/features/settings/SettingsView.tsx +177 -180
- package/src/{components → routes/components}/HomeView.tsx +1 -1
- package/src/{components → routes/components}/IndexRoute.tsx +4 -4
- package/src/routes/components/NavigationItemRoute.tsx +19 -0
- package/src/{components → routes/components}/NotFoundView.tsx +3 -3
- package/src/{components → routes/components}/RouteErrorBoundary.tsx +1 -1
- package/src/routes/components/RouteFallback.tsx +8 -0
- package/src/routes/hooks/useNavigationItems.ts +84 -0
- package/src/{router → routes}/routes.tsx +10 -18
- package/src/components/ViewRoute.tsx +0 -74
- package/src/dist/CookiePreferencesView.52b5aec8.js +0 -1182
- package/src/dist/CookiePreferencesView.52b5aec8.js.map +0 -1
- package/src/dist/DefaultLayout.045a82ff.js +0 -1964
- package/src/dist/DefaultLayout.045a82ff.js.map +0 -1
- package/src/dist/DefaultLayout.4454f259.js +0 -4414
- package/src/dist/DefaultLayout.4454f259.js.map +0 -1
- package/src/dist/FullscreenLayout.555c4987.js +0 -1054
- package/src/dist/FullscreenLayout.555c4987.js.map +0 -1
- package/src/dist/HomeView.ddfa7b68.js +0 -771
- package/src/dist/HomeView.ddfa7b68.js.map +0 -1
- package/src/dist/NotFoundView.c75be4f1.js +0 -811
- package/src/dist/NotFoundView.c75be4f1.js.map +0 -1
- package/src/dist/SettingsView.052b03a6.js +0 -4965
- package/src/dist/SettingsView.052b03a6.js.map +0 -1
- package/src/dist/ViewRoute.e6e3b142.js +0 -1042
- package/src/dist/ViewRoute.e6e3b142.js.map +0 -1
- package/src/dist/WindowsLayout.08724167.js +0 -1762
- package/src/dist/WindowsLayout.08724167.js.map +0 -1
- package/src/dist/esm.f0d741e6.js +0 -29520
- package/src/dist/esm.f0d741e6.js.map +0 -1
- package/src/dist/favicon.4367ac1e.svg +0 -14
- package/src/dist/index.parcel.36d65383.js +0 -54089
- package/src/dist/index.parcel.36d65383.js.map +0 -1
- package/src/dist/index.parcel.ca6d8a47.css +0 -3493
- package/src/dist/index.parcel.ca6d8a47.css.map +0 -1
- package/src/dist/index.parcel.html +0 -88
- package/src/features/layouts/DefaultLayout.tsx +0 -670
- package/src/features/layouts/LayoutProviders.tsx +0 -20
- /package/src/{constants.ts → constants/loading.ts} +0 -0
- /package/src/{router → routes}/router.tsx +0 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { cn } from '../../../lib/utils';
|
|
2
|
+
|
|
3
|
+
/** Inline SVG: external-link icon. Bundled so consumers don't need to serve static SVGs. */
|
|
4
|
+
export function ExternalLinkIcon({ className }: { className?: string }) {
|
|
5
|
+
return (
|
|
6
|
+
<svg
|
|
7
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
8
|
+
width="24"
|
|
9
|
+
height="24"
|
|
10
|
+
viewBox="0 0 24 24"
|
|
11
|
+
fill="none"
|
|
12
|
+
stroke="currentColor"
|
|
13
|
+
strokeWidth="2"
|
|
14
|
+
strokeLinecap="round"
|
|
15
|
+
strokeLinejoin="round"
|
|
16
|
+
className={cn('shrink-0', className)}
|
|
17
|
+
aria-hidden
|
|
18
|
+
>
|
|
19
|
+
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6" />
|
|
20
|
+
<polyline points="15 3 21 3 21 9" />
|
|
21
|
+
<line
|
|
22
|
+
x1="10"
|
|
23
|
+
y1="14"
|
|
24
|
+
x2="21"
|
|
25
|
+
y2="3"
|
|
26
|
+
/>
|
|
27
|
+
</svg>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Caret up: expand (show second line). */
|
|
32
|
+
export function CaretUpIcon({ className }: { className?: string }) {
|
|
33
|
+
return (
|
|
34
|
+
<svg
|
|
35
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
36
|
+
width="24"
|
|
37
|
+
height="24"
|
|
38
|
+
viewBox="0 0 24 24"
|
|
39
|
+
fill="none"
|
|
40
|
+
stroke="currentColor"
|
|
41
|
+
strokeWidth="2"
|
|
42
|
+
strokeLinecap="round"
|
|
43
|
+
strokeLinejoin="round"
|
|
44
|
+
className={cn('shrink-0', className)}
|
|
45
|
+
aria-hidden
|
|
46
|
+
>
|
|
47
|
+
<path d="m18 15-6-6-6 6" />
|
|
48
|
+
</svg>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Caret down: collapse (hide second line). */
|
|
53
|
+
export function CaretDownIcon({ className }: { className?: string }) {
|
|
54
|
+
return (
|
|
55
|
+
<svg
|
|
56
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
57
|
+
width="24"
|
|
58
|
+
height="24"
|
|
59
|
+
viewBox="0 0 24 24"
|
|
60
|
+
fill="none"
|
|
61
|
+
stroke="currentColor"
|
|
62
|
+
strokeWidth="2"
|
|
63
|
+
strokeLinecap="round"
|
|
64
|
+
strokeLinejoin="round"
|
|
65
|
+
className={cn('shrink-0', className)}
|
|
66
|
+
aria-hidden
|
|
67
|
+
>
|
|
68
|
+
<path d="m6 9 6 6 6-6" />
|
|
69
|
+
</svg>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Home icon for mobile bottom bar (same as sidebar logo action). */
|
|
74
|
+
export function HomeIcon({ className }: { className?: string }) {
|
|
75
|
+
return (
|
|
76
|
+
<svg
|
|
77
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
78
|
+
width="24"
|
|
79
|
+
height="24"
|
|
80
|
+
viewBox="0 0 24 24"
|
|
81
|
+
fill="none"
|
|
82
|
+
stroke="currentColor"
|
|
83
|
+
strokeWidth="2"
|
|
84
|
+
strokeLinecap="round"
|
|
85
|
+
strokeLinejoin="round"
|
|
86
|
+
className={cn('shrink-0', className)}
|
|
87
|
+
aria-hidden
|
|
88
|
+
>
|
|
89
|
+
<path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
|
90
|
+
<polyline points="9 22 9 12 15 12 15 22" />
|
|
91
|
+
</svg>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { Link } from 'react-router';
|
|
2
|
+
import type { NavigationItem, NavigationGroup } from '../../config/types';
|
|
3
|
+
import { SidebarHeader, SidebarContent, SidebarFooter } from '../../../components/ui/sidebar';
|
|
4
|
+
import { NavigationContent } from './NavigationContent';
|
|
5
|
+
|
|
6
|
+
/** Reusable sidebar inner: header, main nav, footer. Used in desktop Sidebar and mobile Drawer. */
|
|
7
|
+
export function SidebarInner({
|
|
8
|
+
title,
|
|
9
|
+
logo,
|
|
10
|
+
startNav,
|
|
11
|
+
endItems,
|
|
12
|
+
}: {
|
|
13
|
+
title?: string;
|
|
14
|
+
logo?: string;
|
|
15
|
+
startNav: (NavigationItem | NavigationGroup)[];
|
|
16
|
+
endItems: (NavigationItem | NavigationGroup)[];
|
|
17
|
+
}) {
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
<SidebarHeader className="border-b border-sidebar-border pb-4">
|
|
21
|
+
{(title || logo) && (
|
|
22
|
+
<Link
|
|
23
|
+
to="/"
|
|
24
|
+
className="flex items-center pl-1 pr-3 py-2 text-lg font-semibold text-sidebar-foreground hover:text-sidebar-foreground/80 transition-colors"
|
|
25
|
+
>
|
|
26
|
+
{logo && logo.trim() ? (
|
|
27
|
+
<img
|
|
28
|
+
src={logo}
|
|
29
|
+
alt={title || 'Logo'}
|
|
30
|
+
className="h-5 w-auto shrink-0 object-contain sidebar-logo"
|
|
31
|
+
/>
|
|
32
|
+
) : title ? (
|
|
33
|
+
<span className="leading-none">{title}</span>
|
|
34
|
+
) : null}
|
|
35
|
+
</Link>
|
|
36
|
+
)}
|
|
37
|
+
</SidebarHeader>
|
|
38
|
+
<SidebarContent className="gap-1">
|
|
39
|
+
<NavigationContent navigation={startNav} />
|
|
40
|
+
</SidebarContent>
|
|
41
|
+
{endItems.length > 0 && (
|
|
42
|
+
<SidebarFooter>
|
|
43
|
+
<NavigationContent navigation={endItems} />
|
|
44
|
+
</SidebarFooter>
|
|
45
|
+
)}
|
|
46
|
+
</>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Outlet } from 'react-router';
|
|
2
|
+
import { useMemo, useEffect } from 'react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { Sidebar } from '../../../components/ui/sidebar';
|
|
5
|
+
import { cn } from '../../../lib/utils';
|
|
6
|
+
import {
|
|
7
|
+
filterNavigationByViewport,
|
|
8
|
+
filterNavigationForSidebar,
|
|
9
|
+
flattenNavigationItems,
|
|
10
|
+
resolveLocalizedString as resolveLocalizedLabel,
|
|
11
|
+
splitNavigationByPosition,
|
|
12
|
+
} from '../utils';
|
|
13
|
+
import { SidebarInner } from './SidebarInner';
|
|
14
|
+
import { MobileBottomNav } from './MobileBottomNav';
|
|
15
|
+
import type { SidebarLayoutProps } from './types';
|
|
16
|
+
import { useNavigationItems } from '../../../routes/hooks/useNavigationItems';
|
|
17
|
+
|
|
18
|
+
const SidebarLayoutContent = ({ title, logo, navigation }: SidebarLayoutProps) => {
|
|
19
|
+
const { i18n } = useTranslation();
|
|
20
|
+
const { navigationItem, rootItem } = useNavigationItems();
|
|
21
|
+
|
|
22
|
+
const currentLanguage = useMemo(() => {
|
|
23
|
+
return i18n.language || 'en';
|
|
24
|
+
}, [i18n]);
|
|
25
|
+
|
|
26
|
+
const { startNav, endItems, mobileNavItems } = useMemo(() => {
|
|
27
|
+
const desktopNav = filterNavigationByViewport(navigation, 'desktop');
|
|
28
|
+
const mobileNav = filterNavigationByViewport(navigation, 'mobile');
|
|
29
|
+
const { start, end } = splitNavigationByPosition(desktopNav);
|
|
30
|
+
const mobileFlat = flattenNavigationItems(mobileNav);
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
startNav: filterNavigationForSidebar(start),
|
|
34
|
+
endItems: end,
|
|
35
|
+
mobileNavItems: mobileFlat,
|
|
36
|
+
};
|
|
37
|
+
}, [navigation]);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!title) return;
|
|
41
|
+
if (navigationItem) {
|
|
42
|
+
const label = resolveLocalizedLabel(navigationItem.label, currentLanguage);
|
|
43
|
+
document.title = `${label} | ${title}`;
|
|
44
|
+
} else {
|
|
45
|
+
document.title = title;
|
|
46
|
+
}
|
|
47
|
+
}, [navigationItem, title, currentLanguage]);
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<div>
|
|
51
|
+
<div className="flex h-screen overflow-hidden">
|
|
52
|
+
<Sidebar className={cn('hidden md:flex shrink-0')}>
|
|
53
|
+
<SidebarInner
|
|
54
|
+
title={title}
|
|
55
|
+
logo={logo}
|
|
56
|
+
startNav={startNav}
|
|
57
|
+
endItems={endItems}
|
|
58
|
+
/>
|
|
59
|
+
</Sidebar>
|
|
60
|
+
|
|
61
|
+
<main className="flex-1 flex flex-col overflow-hidden bg-background relative min-w-0">
|
|
62
|
+
<div className="flex-1 flex flex-col overflow-auto pb-16 md:pb-0">
|
|
63
|
+
<Outlet />
|
|
64
|
+
</div>
|
|
65
|
+
</main>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
<MobileBottomNav
|
|
69
|
+
items={mobileNavItems}
|
|
70
|
+
currentLanguage={currentLanguage}
|
|
71
|
+
showHomeButton={!rootItem}
|
|
72
|
+
/>
|
|
73
|
+
</div>
|
|
74
|
+
);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export function SidebarLayout({ title, appIcon, logo, navigation }: SidebarLayoutProps) {
|
|
78
|
+
return (
|
|
79
|
+
<SidebarLayoutContent
|
|
80
|
+
title={title}
|
|
81
|
+
appIcon={appIcon}
|
|
82
|
+
logo={logo}
|
|
83
|
+
navigation={navigation}
|
|
84
|
+
/>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** DuckDuckGo favicon URL for a given page URL (used when openIn === 'external' and no icon is set). */
|
|
2
|
+
export function getExternalFaviconUrl(url: string): string | null {
|
|
3
|
+
try {
|
|
4
|
+
const parsed = new URL(url);
|
|
5
|
+
const hostname = parsed.hostname;
|
|
6
|
+
if (!hostname) return null;
|
|
7
|
+
return `https://icons.duckduckgo.com/ip3/${hostname}.ico`;
|
|
8
|
+
} catch {
|
|
9
|
+
return null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Approximate width per slot (icon + label + padding) and gap for dynamic slot count. */
|
|
14
|
+
export const BOTTOM_NAV_SLOT_WIDTH = 64;
|
|
15
|
+
export const BOTTOM_NAV_GAP = 4;
|
|
16
|
+
export const BOTTOM_NAV_PX = 12;
|
|
17
|
+
/** Max slots in the row (Home + nav + optional More) to avoid overflow/duplicated wrap. */
|
|
18
|
+
export const BOTTOM_NAV_MAX_SLOTS = 6;
|
|
19
|
+
|
|
20
|
+
/** True when the icon is a local app icon (/icons/); external images (avatars, favicons) are shown as-is. */
|
|
21
|
+
export function isAppIcon(src: string): boolean {
|
|
22
|
+
return src.startsWith('/icons/');
|
|
23
|
+
}
|
|
@@ -2,7 +2,7 @@ import type { NavigationItem, NavigationGroup, LocalizedString } from '../config
|
|
|
2
2
|
|
|
3
3
|
/** Path prefix for a nav item: "/" for root (path '' or '/'), otherwise "/{path}". */
|
|
4
4
|
export function getNavPathPrefix(item: NavigationItem): string {
|
|
5
|
-
return item.path === '
|
|
5
|
+
return item.path === '' ? '/' : `/${item.path}`;
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
/** Whether a URL string uses hash-based routing (e.g. contains /#/). */
|