@shellui/core 0.2.0-alpha.1 → 0.2.0-alpha.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/components/ContentView.tsx +5 -14
- package/src/components/ViewRoute.tsx +6 -9
- package/src/components/ui/sonner.tsx +1 -1
- package/src/constants/urls.ts +0 -2
- package/src/features/config/ConfigProvider.ts +3 -8
- package/src/features/config/shellui-config.d.ts +0 -4
- package/src/features/config/types.ts +1 -8
- package/src/features/layouts/AppBarLayout.tsx +21 -13
- package/src/features/layouts/DefaultLayout.tsx +23 -16
- package/src/features/layouts/OverlayShell.tsx +4 -14
- package/src/features/layouts/WindowsLayout.tsx +7 -8
- package/src/features/layouts/utils.ts +15 -23
- package/src/features/settings/SettingsProvider.tsx +156 -25
- package/src/features/settings/components/Appearance.tsx +4 -4
- package/src/features/theme/useTheme.tsx +7 -7
- package/src/router/routes.tsx +0 -12
- package/src/components/AppPathView.tsx +0 -31
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shellui/core",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.2",
|
|
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-alpha.
|
|
61
|
+
"@shellui/sdk": "0.2.0-alpha.2"
|
|
62
62
|
},
|
|
63
63
|
"peerDependencies": {
|
|
64
64
|
"react": "^18.0.0 || ^19.0.0",
|
|
@@ -16,22 +16,17 @@ const logger = getLogger('shellcore');
|
|
|
16
16
|
|
|
17
17
|
interface ContentViewProps {
|
|
18
18
|
url: string;
|
|
19
|
-
/** Base URL for this content (effective nav item URL). Used for URL sync; when omitted, falls back to navItem.url or url. */
|
|
20
|
-
baseUrl?: string;
|
|
21
19
|
pathPrefix: string;
|
|
22
20
|
ignoreMessages?: boolean;
|
|
23
|
-
|
|
24
|
-
navItem?: NavigationItem | null;
|
|
21
|
+
navItem: NavigationItem;
|
|
25
22
|
}
|
|
26
23
|
|
|
27
24
|
export const ContentView = ({
|
|
28
25
|
url,
|
|
29
|
-
baseUrl: baseUrlProp,
|
|
30
26
|
pathPrefix,
|
|
31
27
|
ignoreMessages = false,
|
|
32
28
|
navItem,
|
|
33
29
|
}: ContentViewProps) => {
|
|
34
|
-
const baseUrl = baseUrlProp ?? navItem?.url ?? url;
|
|
35
30
|
const navigate = useNavigate();
|
|
36
31
|
const iframeRef = useRef<HTMLIFrameElement>(null);
|
|
37
32
|
const isInternalNavigation = useRef(false);
|
|
@@ -63,13 +58,9 @@ export const ContentView = ({
|
|
|
63
58
|
}
|
|
64
59
|
|
|
65
60
|
const { pathname, search, hash } = data.payload as ShellUIUrlPayload;
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
? new URL(baseUrl, window.location.origin).pathname
|
|
70
|
-
: baseUrl;
|
|
71
|
-
let cleanPathname = pathname.startsWith(basePathname)
|
|
72
|
-
? pathname.slice(basePathname.length)
|
|
61
|
+
// Remove leading slash and trailing slashes from iframe pathname
|
|
62
|
+
let cleanPathname = pathname.startsWith(navItem.url)
|
|
63
|
+
? pathname.slice(navItem.url.length)
|
|
73
64
|
: pathname;
|
|
74
65
|
cleanPathname = cleanPathname.startsWith('/') ? cleanPathname.slice(1) : cleanPathname;
|
|
75
66
|
cleanPathname = cleanPathname.replace(/\/+$/, ''); // Remove trailing slashes
|
|
@@ -111,7 +102,7 @@ export const ContentView = ({
|
|
|
111
102
|
return () => {
|
|
112
103
|
cleanup();
|
|
113
104
|
};
|
|
114
|
-
}, [pathPrefix, navigate
|
|
105
|
+
}, [pathPrefix, navigate]);
|
|
115
106
|
|
|
116
107
|
// Hide loading overlay when iframe sends SHELLUI_INITIALIZED
|
|
117
108
|
useEffect(() => {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useMemo } from 'react';
|
|
2
2
|
import { Navigate, useLocation } from 'react-router';
|
|
3
|
-
import { getNavPathPrefix
|
|
3
|
+
import { getNavPathPrefix } from '../features/layouts/utils';
|
|
4
4
|
import { ContentView } from './ContentView';
|
|
5
5
|
import type { NavigationItem } from '../features/config/types';
|
|
6
6
|
|
|
@@ -32,18 +32,15 @@ export const ViewRoute = ({ navigation }: ViewRouteProps) => {
|
|
|
32
32
|
const pathPrefix = getNavPathPrefix(navItem);
|
|
33
33
|
const subPath = pathname.length > pathPrefix.length ? pathname.slice(pathPrefix.length + 1) : '';
|
|
34
34
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
finalUrl = `${base}${subPath}`;
|
|
35
|
+
// Construct the final URL for the iframe
|
|
36
|
+
let finalUrl = navItem.url;
|
|
37
|
+
if (subPath) {
|
|
38
|
+
const baseUrl = navItem.url.endsWith('/') ? navItem.url : `${navItem.url}/`;
|
|
39
|
+
finalUrl = `${baseUrl}${subPath}`;
|
|
41
40
|
}
|
|
42
|
-
|
|
43
41
|
return (
|
|
44
42
|
<ContentView
|
|
45
43
|
url={finalUrl}
|
|
46
|
-
baseUrl={baseUrl}
|
|
47
44
|
pathPrefix={navItem.path}
|
|
48
45
|
navItem={navItem}
|
|
49
46
|
/>
|
|
@@ -11,7 +11,7 @@ const Toaster = ({ ...props }: ToasterProps) => {
|
|
|
11
11
|
return (
|
|
12
12
|
<Sonner
|
|
13
13
|
position="top-center"
|
|
14
|
-
theme={settings.appearance.
|
|
14
|
+
theme={settings.appearance.colorScheme as 'light' | 'dark' | 'system'}
|
|
15
15
|
className="toaster group"
|
|
16
16
|
style={{
|
|
17
17
|
zIndex: Z_INDEX.TOAST,
|
package/src/constants/urls.ts
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
import { createContext, useState, createElement, type ReactNode } from 'react';
|
|
2
|
-
import type { ComponentType } from 'react';
|
|
3
2
|
import { getLogger } from '@shellui/sdk';
|
|
4
3
|
import type { ShellUIConfig } from './types';
|
|
5
|
-
import shelluiConfig
|
|
4
|
+
import shelluiConfig from '@shellui/config';
|
|
6
5
|
|
|
7
6
|
const logger = getLogger('shellcore');
|
|
8
7
|
|
|
9
8
|
export interface ConfigContextValue {
|
|
10
9
|
config: ShellUIConfig;
|
|
11
|
-
/** Map of nav path -> component for /__app/:path (injected when componentPath is set in config). */
|
|
12
|
-
shelluiComponents: Record<string, ComponentType>;
|
|
13
10
|
}
|
|
14
11
|
|
|
15
12
|
export const ConfigContext = createContext<ConfigContextValue | null>(null);
|
|
@@ -47,9 +44,7 @@ export function ConfigProvider(props: ConfigProviderProps): ReturnType<typeof cr
|
|
|
47
44
|
}
|
|
48
45
|
});
|
|
49
46
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
shelluiComponents: typeof shelluiComponents !== 'undefined' ? shelluiComponents : {},
|
|
53
|
-
};
|
|
47
|
+
// Always provide a value - never null - to prevent "useConfig must be used within ConfigProvider" errors
|
|
48
|
+
const value: ConfigContextValue = { config };
|
|
54
49
|
return createElement(ConfigContext.Provider, { value }, props.children);
|
|
55
50
|
}
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Type declaration for the virtual @shellui/config module.
|
|
3
3
|
* The CLI injects this module at build time via Vite (virtual:shellui-config).
|
|
4
|
-
* When nav items have componentPath, the CLI also injects shelluiComponents for /__app/:path routes.
|
|
5
4
|
*/
|
|
6
5
|
declare module '@shellui/config' {
|
|
7
|
-
import type { ComponentType } from 'react';
|
|
8
6
|
import type { ShellUIConfig } from './types';
|
|
9
7
|
export const shelluiConfig: ShellUIConfig;
|
|
10
|
-
/** Map of nav path -> component for component-based items (injected when componentPath is set). */
|
|
11
|
-
export const shelluiComponents: Record<string, ComponentType>;
|
|
12
8
|
export default shelluiConfig;
|
|
13
9
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { ComponentType } from 'react';
|
|
2
|
-
|
|
3
1
|
// Language-specific label/title
|
|
4
2
|
export type LocalizedString =
|
|
5
3
|
| string
|
|
@@ -18,12 +16,7 @@ export type LayoutType = 'sidebar' | 'fullscreen' | 'windows' | 'app-bar';
|
|
|
18
16
|
export interface NavigationItem {
|
|
19
17
|
label: string | LocalizedString;
|
|
20
18
|
path: string;
|
|
21
|
-
|
|
22
|
-
url?: string;
|
|
23
|
-
/** React component to render for this item. When set, a URL under the app-path is generated so the content can be loaded in an iframe like URL-based items. The CLI infers the component module path from your config file imports so you don't need to set componentPath. */
|
|
24
|
-
component?: ComponentType;
|
|
25
|
-
/** @internal Set by the CLI when loading config (inferred from component + config file imports). Only needed if you bypass the CLI. */
|
|
26
|
-
componentPath?: string;
|
|
19
|
+
url: string;
|
|
27
20
|
icon?: string; // Path to SVG icon file (e.g., '/icons/book-open.svg')
|
|
28
21
|
/** When true, hide this item from the sidebar and 404 page; route remains valid and item still appears in Develop settings. */
|
|
29
22
|
hidden?: boolean;
|
|
@@ -6,7 +6,7 @@ import type { NavigationItem, NavigationGroup } from '../config/types';
|
|
|
6
6
|
import {
|
|
7
7
|
filterNavigationByViewport,
|
|
8
8
|
flattenNavigationItems,
|
|
9
|
-
|
|
9
|
+
getActivePathPrefix,
|
|
10
10
|
getNavPathPrefix,
|
|
11
11
|
resolveLocalizedString as resolveNavLabel,
|
|
12
12
|
splitNavigationByPosition,
|
|
@@ -49,17 +49,22 @@ function resolveLocalizedLabel(
|
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/** End link: icon-only or first-letter badge with themed tooltip. */
|
|
52
|
-
function TopBarEndItem({
|
|
52
|
+
function TopBarEndItem({
|
|
53
|
+
item,
|
|
54
|
+
label,
|
|
55
|
+
activePathPrefix,
|
|
56
|
+
}: {
|
|
57
|
+
item: NavigationItem;
|
|
58
|
+
label: string;
|
|
59
|
+
activePathPrefix: string | null;
|
|
60
|
+
}) {
|
|
53
61
|
const pathPrefix = getNavPathPrefix(item);
|
|
54
62
|
const isOverlay = item.openIn === 'modal' || item.openIn === 'drawer';
|
|
55
63
|
const isExternal = item.openIn === 'external';
|
|
56
|
-
const location = useLocation();
|
|
57
64
|
const isActive =
|
|
58
|
-
!isOverlay &&
|
|
59
|
-
!isExternal &&
|
|
60
|
-
(location.pathname === pathPrefix || location.pathname.startsWith(`${pathPrefix}/`));
|
|
65
|
+
!isOverlay && !isExternal && pathPrefix === activePathPrefix;
|
|
61
66
|
|
|
62
|
-
const faviconUrl = isExternal && !item.icon ? getExternalFaviconUrl(
|
|
67
|
+
const faviconUrl = isExternal && !item.icon ? getExternalFaviconUrl(item.url) : null;
|
|
63
68
|
const iconSrc = item.icon ?? faviconUrl ?? null;
|
|
64
69
|
const firstLetter = label ? label.charAt(0).toUpperCase() : '?';
|
|
65
70
|
|
|
@@ -94,7 +99,7 @@ function TopBarEndItem({ item, label }: { item: NavigationItem; label: string })
|
|
|
94
99
|
return wrap(
|
|
95
100
|
<button
|
|
96
101
|
type="button"
|
|
97
|
-
onClick={() => shellui.openModal(
|
|
102
|
+
onClick={() => shellui.openModal(item.url)}
|
|
98
103
|
className={buttonClass}
|
|
99
104
|
aria-label={label}
|
|
100
105
|
>
|
|
@@ -106,7 +111,7 @@ function TopBarEndItem({ item, label }: { item: NavigationItem; label: string })
|
|
|
106
111
|
return wrap(
|
|
107
112
|
<button
|
|
108
113
|
type="button"
|
|
109
|
-
onClick={() => shellui.openDrawer({ url:
|
|
114
|
+
onClick={() => shellui.openDrawer({ url: item.url, position: item.drawerPosition })}
|
|
110
115
|
className={buttonClass}
|
|
111
116
|
aria-label={label}
|
|
112
117
|
>
|
|
@@ -117,7 +122,7 @@ function TopBarEndItem({ item, label }: { item: NavigationItem; label: string })
|
|
|
117
122
|
if (item.openIn === 'external') {
|
|
118
123
|
return wrap(
|
|
119
124
|
<a
|
|
120
|
-
href={
|
|
125
|
+
href={item.url}
|
|
121
126
|
target="_blank"
|
|
122
127
|
rel="noopener noreferrer"
|
|
123
128
|
className={buttonClass}
|
|
@@ -144,16 +149,18 @@ export function AppBarLayout({ title, logo, navigation }: AppBarLayoutProps) {
|
|
|
144
149
|
const navigate = useNavigate();
|
|
145
150
|
const currentLanguage = i18n.language || 'en';
|
|
146
151
|
|
|
147
|
-
const { endNavItems, navigationItems, displayStartItems } = useMemo(() => {
|
|
152
|
+
const { endNavItems, navigationItems, displayStartItems, activePathPrefix } = useMemo(() => {
|
|
148
153
|
const desktopNav = filterNavigationByViewport(navigation, 'desktop');
|
|
149
154
|
const { start, end } = splitNavigationByPosition(desktopNav);
|
|
150
155
|
const startItems = flattenNavigationItems(start).filter((i) => !i.hidden);
|
|
156
|
+
const flat = flattenNavigationItems(navigation);
|
|
151
157
|
return {
|
|
152
158
|
endNavItems: flattenNavigationItems(end).filter((i) => !i.hidden),
|
|
153
|
-
navigationItems:
|
|
159
|
+
navigationItems: flat,
|
|
154
160
|
displayStartItems: withHomepageWhenNoRoot(startItems),
|
|
161
|
+
activePathPrefix: getActivePathPrefix(location.pathname, flat),
|
|
155
162
|
};
|
|
156
|
-
}, [navigation]);
|
|
163
|
+
}, [navigation, location.pathname]);
|
|
157
164
|
|
|
158
165
|
useEffect(() => {
|
|
159
166
|
if (!title) return;
|
|
@@ -243,6 +250,7 @@ export function AppBarLayout({ title, logo, navigation }: AppBarLayoutProps) {
|
|
|
243
250
|
key={item.path}
|
|
244
251
|
item={item}
|
|
245
252
|
label={resolveNavLabel(item.label, currentLanguage) || item.path || ''}
|
|
253
|
+
activePathPrefix={activePathPrefix}
|
|
246
254
|
/>
|
|
247
255
|
))}
|
|
248
256
|
</div>
|
|
@@ -22,7 +22,7 @@ import {
|
|
|
22
22
|
filterNavigationByViewport,
|
|
23
23
|
filterNavigationForSidebar,
|
|
24
24
|
flattenNavigationItems,
|
|
25
|
-
|
|
25
|
+
getActivePathPrefix,
|
|
26
26
|
getNavPathPrefix,
|
|
27
27
|
HOMEPAGE_NAV_ITEM,
|
|
28
28
|
resolveLocalizedString as resolveNavLabel,
|
|
@@ -83,6 +83,12 @@ const NavigationContent = ({
|
|
|
83
83
|
});
|
|
84
84
|
}, [navigation]);
|
|
85
85
|
|
|
86
|
+
const flatItems = useMemo(() => flattenNavigationItems(navigation), [navigation]);
|
|
87
|
+
const activePathPrefix = useMemo(
|
|
88
|
+
() => getActivePathPrefix(location.pathname, flatItems),
|
|
89
|
+
[location.pathname, flatItems],
|
|
90
|
+
);
|
|
91
|
+
|
|
86
92
|
// Helper to check if an item is a group
|
|
87
93
|
const isGroup = (item: NavigationItem | NavigationGroup): item is NavigationGroup => {
|
|
88
94
|
return 'title' in item && 'items' in item;
|
|
@@ -94,11 +100,9 @@ const NavigationContent = ({
|
|
|
94
100
|
const isOverlay = navItem.openIn === 'modal' || navItem.openIn === 'drawer';
|
|
95
101
|
const isExternal = navItem.openIn === 'external';
|
|
96
102
|
const isActive =
|
|
97
|
-
!isOverlay &&
|
|
98
|
-
!isExternal &&
|
|
99
|
-
(location.pathname === pathPrefix || location.pathname.startsWith(`${pathPrefix}/`));
|
|
103
|
+
!isOverlay && !isExternal && pathPrefix === activePathPrefix;
|
|
100
104
|
const itemLabel = resolveLocalizedString(navItem.label, currentLanguage);
|
|
101
|
-
const faviconUrl = isExternal && !navItem.icon ? getExternalFaviconUrl(
|
|
105
|
+
const faviconUrl = isExternal && !navItem.icon ? getExternalFaviconUrl(navItem.url) : null;
|
|
102
106
|
const iconSrc = navItem.icon ?? faviconUrl ?? null;
|
|
103
107
|
const iconEl = iconSrc ? (
|
|
104
108
|
<img
|
|
@@ -123,7 +127,7 @@ const NavigationContent = ({
|
|
|
123
127
|
navItem.openIn === 'modal' ? (
|
|
124
128
|
<button
|
|
125
129
|
type="button"
|
|
126
|
-
onClick={() => shellui.openModal(
|
|
130
|
+
onClick={() => shellui.openModal(navItem.url)}
|
|
127
131
|
className="flex items-center gap-2 w-full cursor-pointer text-left"
|
|
128
132
|
>
|
|
129
133
|
{content}
|
|
@@ -131,14 +135,14 @@ const NavigationContent = ({
|
|
|
131
135
|
) : navItem.openIn === 'drawer' ? (
|
|
132
136
|
<button
|
|
133
137
|
type="button"
|
|
134
|
-
onClick={() => shellui.openDrawer({ url:
|
|
138
|
+
onClick={() => shellui.openDrawer({ url: navItem.url, position: navItem.drawerPosition })}
|
|
135
139
|
className="flex items-center gap-2 w-full cursor-pointer text-left"
|
|
136
140
|
>
|
|
137
141
|
{content}
|
|
138
142
|
</button>
|
|
139
143
|
) : navItem.openIn === 'external' ? (
|
|
140
144
|
<a
|
|
141
|
-
href={
|
|
145
|
+
href={navItem.url}
|
|
142
146
|
target="_blank"
|
|
143
147
|
rel="noopener noreferrer"
|
|
144
148
|
className="flex items-center gap-2 w-full"
|
|
@@ -306,7 +310,7 @@ const BottomNavItem = ({
|
|
|
306
310
|
return (
|
|
307
311
|
<button
|
|
308
312
|
type="button"
|
|
309
|
-
onClick={() => shellui.openModal(
|
|
313
|
+
onClick={() => shellui.openModal(item.url)}
|
|
310
314
|
className={baseClass}
|
|
311
315
|
>
|
|
312
316
|
{content}
|
|
@@ -317,7 +321,7 @@ const BottomNavItem = ({
|
|
|
317
321
|
return (
|
|
318
322
|
<button
|
|
319
323
|
type="button"
|
|
320
|
-
onClick={() => shellui.openDrawer({ url:
|
|
324
|
+
onClick={() => shellui.openDrawer({ url: item.url, position: item.drawerPosition })}
|
|
321
325
|
className={baseClass}
|
|
322
326
|
>
|
|
323
327
|
{content}
|
|
@@ -327,7 +331,7 @@ const BottomNavItem = ({
|
|
|
327
331
|
if (item.openIn === 'external') {
|
|
328
332
|
return (
|
|
329
333
|
<a
|
|
330
|
-
href={
|
|
334
|
+
href={item.url}
|
|
331
335
|
target="_blank"
|
|
332
336
|
rel="noopener noreferrer"
|
|
333
337
|
className={baseClass}
|
|
@@ -443,6 +447,11 @@ const MobileBottomNav = ({
|
|
|
443
447
|
const navRef = useRef<HTMLElement>(null);
|
|
444
448
|
const [rowWidth, setRowWidth] = useState(0);
|
|
445
449
|
|
|
450
|
+
const activePathPrefix = useMemo(
|
|
451
|
+
() => getActivePathPrefix(location.pathname, items),
|
|
452
|
+
[location.pathname, items],
|
|
453
|
+
);
|
|
454
|
+
|
|
446
455
|
useLayoutEffect(() => {
|
|
447
456
|
const el = navRef.current;
|
|
448
457
|
if (!el) return;
|
|
@@ -483,17 +492,15 @@ const MobileBottomNav = ({
|
|
|
483
492
|
const pathPrefix = getNavPathPrefix(item);
|
|
484
493
|
const isOverlayOrExternal =
|
|
485
494
|
item.openIn === 'modal' || item.openIn === 'drawer' || item.openIn === 'external';
|
|
486
|
-
const isActive =
|
|
487
|
-
!isOverlayOrExternal &&
|
|
488
|
-
(location.pathname === pathPrefix || location.pathname.startsWith(`${pathPrefix}/`));
|
|
495
|
+
const isActive = !isOverlayOrExternal && pathPrefix === activePathPrefix;
|
|
489
496
|
const label = resolveNavLabel(item.label, currentLanguage);
|
|
490
497
|
const faviconUrl =
|
|
491
|
-
item.openIn === 'external' && !item.icon ? getExternalFaviconUrl(
|
|
498
|
+
item.openIn === 'external' && !item.icon ? getExternalFaviconUrl(item.url) : null;
|
|
492
499
|
const iconSrc = item.icon ?? faviconUrl ?? null;
|
|
493
500
|
const applyIconTheme = iconSrc ? isAppIcon(iconSrc) : false;
|
|
494
501
|
return (
|
|
495
502
|
<BottomNavItem
|
|
496
|
-
key={`${item.path}-${
|
|
503
|
+
key={`${item.path}-${item.url}-${index}`}
|
|
497
504
|
item={item}
|
|
498
505
|
label={label}
|
|
499
506
|
isActive={isActive}
|
|
@@ -9,7 +9,7 @@ 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 {
|
|
12
|
+
import { getNavPathPrefix, resolveLocalizedString } from './utils';
|
|
13
13
|
|
|
14
14
|
interface OverlayShellProps {
|
|
15
15
|
navigationItems: NavigationItem[];
|
|
@@ -90,9 +90,7 @@ export function OverlayShell({ navigationItems, children }: OverlayShellProps) {
|
|
|
90
90
|
<>
|
|
91
91
|
<DialogTitle className="sr-only">
|
|
92
92
|
{resolveLocalizedString(
|
|
93
|
-
navigationItems.find(
|
|
94
|
-
(item) => normalizeUrlToPathname(getEffectiveUrl(item)) === normalizeUrlToPathname(modalUrl),
|
|
95
|
-
)?.label,
|
|
93
|
+
navigationItems.find((item) => item.url === modalUrl)?.label,
|
|
96
94
|
currentLanguage,
|
|
97
95
|
)}
|
|
98
96
|
</DialogTitle>
|
|
@@ -107,11 +105,7 @@ export function OverlayShell({ navigationItems, children }: OverlayShellProps) {
|
|
|
107
105
|
url={modalUrl}
|
|
108
106
|
pathPrefix="settings"
|
|
109
107
|
ignoreMessages={true}
|
|
110
|
-
navItem={
|
|
111
|
-
navigationItems.find(
|
|
112
|
-
(item) => normalizeUrlToPathname(getEffectiveUrl(item)) === normalizeUrlToPathname(modalUrl),
|
|
113
|
-
) ?? undefined
|
|
114
|
-
}
|
|
108
|
+
navItem={navigationItems.find((item) => item.url === modalUrl) as NavigationItem}
|
|
115
109
|
/>
|
|
116
110
|
</div>
|
|
117
111
|
</>
|
|
@@ -153,11 +147,7 @@ export function OverlayShell({ navigationItems, children }: OverlayShellProps) {
|
|
|
153
147
|
url={drawerUrl}
|
|
154
148
|
pathPrefix="settings"
|
|
155
149
|
ignoreMessages={true}
|
|
156
|
-
navItem={
|
|
157
|
-
navigationItems.find(
|
|
158
|
-
(item) => normalizeUrlToPathname(getEffectiveUrl(item)) === normalizeUrlToPathname(drawerUrl),
|
|
159
|
-
) ?? undefined
|
|
160
|
-
}
|
|
150
|
+
navItem={navigationItems.find((item) => item.url === drawerUrl) as NavigationItem}
|
|
161
151
|
/>
|
|
162
152
|
</div>
|
|
163
153
|
) : (
|
|
@@ -11,7 +11,6 @@ import { shellui } from '@shellui/sdk';
|
|
|
11
11
|
import type { NavigationItem, NavigationGroup } from '../config/types';
|
|
12
12
|
import {
|
|
13
13
|
flattenNavigationItems,
|
|
14
|
-
getEffectiveUrl,
|
|
15
14
|
getNavPathPrefix,
|
|
16
15
|
resolveLocalizedString as resolveNavLabel,
|
|
17
16
|
splitNavigationByPosition,
|
|
@@ -542,7 +541,7 @@ export function WindowsLayout({
|
|
|
542
541
|
const label =
|
|
543
542
|
typeof item.label === 'string' ? item.label : resolveNavLabel(item.label, currentLanguage);
|
|
544
543
|
const faviconUrl =
|
|
545
|
-
item.openIn === 'external' && !item.icon ? getExternalFaviconUrl(
|
|
544
|
+
item.openIn === 'external' && !item.icon ? getExternalFaviconUrl(item.url) : null;
|
|
546
545
|
const icon = item.icon ?? faviconUrl ?? null;
|
|
547
546
|
const id = genId();
|
|
548
547
|
const bounds = {
|
|
@@ -557,7 +556,7 @@ export function WindowsLayout({
|
|
|
557
556
|
id,
|
|
558
557
|
path: item.path,
|
|
559
558
|
pathname: getNavPathPrefix(item),
|
|
560
|
-
baseUrl:
|
|
559
|
+
baseUrl: item.url,
|
|
561
560
|
label,
|
|
562
561
|
icon,
|
|
563
562
|
bounds,
|
|
@@ -610,17 +609,17 @@ export function WindowsLayout({
|
|
|
610
609
|
const handleNavClick = useCallback(
|
|
611
610
|
(item: NavigationItem) => {
|
|
612
611
|
if (item.openIn === 'modal') {
|
|
613
|
-
shellui.openModal(
|
|
612
|
+
shellui.openModal(item.url);
|
|
614
613
|
setStartMenuOpen(false);
|
|
615
614
|
return;
|
|
616
615
|
}
|
|
617
616
|
if (item.openIn === 'drawer') {
|
|
618
|
-
shellui.openDrawer({ url:
|
|
617
|
+
shellui.openDrawer({ url: item.url, position: item.drawerPosition });
|
|
619
618
|
setStartMenuOpen(false);
|
|
620
619
|
return;
|
|
621
620
|
}
|
|
622
621
|
if (item.openIn === 'external') {
|
|
623
|
-
window.open(
|
|
622
|
+
window.open(item.url, '_blank', 'noopener,noreferrer');
|
|
624
623
|
setStartMenuOpen(false);
|
|
625
624
|
return;
|
|
626
625
|
}
|
|
@@ -710,7 +709,7 @@ export function WindowsLayout({
|
|
|
710
709
|
: resolveNavLabel(item.label, currentLanguage);
|
|
711
710
|
const icon =
|
|
712
711
|
item.icon ??
|
|
713
|
-
(item.openIn === 'external' ? getExternalFaviconUrl(
|
|
712
|
+
(item.openIn === 'external' ? getExternalFaviconUrl(item.url) : null);
|
|
714
713
|
return (
|
|
715
714
|
<button
|
|
716
715
|
key={item.path}
|
|
@@ -792,7 +791,7 @@ export function WindowsLayout({
|
|
|
792
791
|
: resolveNavLabel(item.label, currentLanguage);
|
|
793
792
|
const icon =
|
|
794
793
|
item.icon ??
|
|
795
|
-
(item.openIn === 'external' ? getExternalFaviconUrl(
|
|
794
|
+
(item.openIn === 'external' ? getExternalFaviconUrl(item.url) : null);
|
|
796
795
|
return (
|
|
797
796
|
<button
|
|
798
797
|
key={item.path}
|
|
@@ -1,33 +1,25 @@
|
|
|
1
1
|
import type { NavigationItem, NavigationGroup, LocalizedString } from '../config/types';
|
|
2
|
-
import urls from '../../constants/urls';
|
|
3
2
|
|
|
4
3
|
/** Path prefix for a nav item: "/" for root (path '' or '/'), otherwise "/{path}". */
|
|
5
4
|
export function getNavPathPrefix(item: NavigationItem): string {
|
|
6
5
|
return item.path === '/' || item.path === '' ? '/' : `/${item.path}`;
|
|
7
6
|
}
|
|
8
7
|
|
|
9
|
-
/**
|
|
10
|
-
export function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
try {
|
|
25
|
-
return new URL(s, 'http://localhost').pathname.replace(/\/+$/, '') || '/';
|
|
26
|
-
} catch {
|
|
27
|
-
return s.startsWith('/') ? s.replace(/\/+$/, '') || '/' : `/${s}`.replace(/\/+$/, '') || '/';
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
return (s.startsWith('/') ? s : `/${s}`).replace(/\/+$/, '') || '/';
|
|
8
|
+
/** Among items that match the current pathname, return the longest path prefix. Used so only one nav item is active when URLs nest (e.g. /foo and /foo/bar). */
|
|
9
|
+
export function getActivePathPrefix(
|
|
10
|
+
pathname: string,
|
|
11
|
+
items: NavigationItem[],
|
|
12
|
+
): string | null {
|
|
13
|
+
const linkItems = items.filter(
|
|
14
|
+
(i) => i.openIn !== 'modal' && i.openIn !== 'drawer' && i.openIn !== 'external',
|
|
15
|
+
);
|
|
16
|
+
const matching = linkItems
|
|
17
|
+
.map((i) => getNavPathPrefix(i))
|
|
18
|
+
.filter(
|
|
19
|
+
(p) => pathname === p || pathname.startsWith(p === '/' ? '/' : p + '/'),
|
|
20
|
+
);
|
|
21
|
+
if (matching.length === 0) return null;
|
|
22
|
+
return matching.reduce((a, b) => (a.length >= b.length ? a : b));
|
|
31
23
|
}
|
|
32
24
|
|
|
33
25
|
/** Resolve a localized string to a single string for the given language. */
|
|
@@ -5,12 +5,13 @@ import {
|
|
|
5
5
|
type ShellUIMessage,
|
|
6
6
|
type Settings,
|
|
7
7
|
type SettingsNavigationItem,
|
|
8
|
+
type Appearance,
|
|
8
9
|
} from '@shellui/sdk';
|
|
9
10
|
import { SettingsContext } from './SettingsContext';
|
|
10
11
|
import { useConfig } from '../config/useConfig';
|
|
11
12
|
import { useTranslation } from 'react-i18next';
|
|
12
|
-
import type { NavigationItem, NavigationGroup } from '../config/types';
|
|
13
|
-
import {
|
|
13
|
+
import type { NavigationItem, NavigationGroup, ShellUIConfig } from '../config/types';
|
|
14
|
+
import { getTheme, registerTheme } from '../theme/themes';
|
|
14
15
|
|
|
15
16
|
const logger = getLogger('shellcore');
|
|
16
17
|
|
|
@@ -32,18 +33,77 @@ function resolveLabel(
|
|
|
32
33
|
return value[lang] || value.en || value.fr || Object.values(value)[0] || '';
|
|
33
34
|
}
|
|
34
35
|
|
|
35
|
-
function
|
|
36
|
+
function resolveColorMode(colorScheme: 'light' | 'dark' | 'system'): 'light' | 'dark' {
|
|
37
|
+
if (colorScheme === 'system' && typeof window !== 'undefined') {
|
|
38
|
+
return window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
39
|
+
}
|
|
40
|
+
return colorScheme === 'dark' ? 'dark' : 'light';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Build the full appearance object for settings propagation so apps receive all theme
|
|
45
|
+
* variable values and can style without knowing the theme name.
|
|
46
|
+
*/
|
|
47
|
+
function getResolvedAppearanceForSettings(
|
|
48
|
+
settings: Settings,
|
|
49
|
+
config: ShellUIConfig | undefined,
|
|
50
|
+
): Appearance | undefined {
|
|
51
|
+
if (typeof window === 'undefined') return undefined;
|
|
52
|
+
config?.themes?.forEach(registerTheme);
|
|
53
|
+
const themeName =
|
|
54
|
+
settings.appearance?.name || config?.defaultTheme || 'default';
|
|
55
|
+
const themeDef = getTheme(themeName) || getTheme('default');
|
|
56
|
+
if (!themeDef) return undefined;
|
|
57
|
+
const colorScheme = settings.appearance?.colorScheme ?? 'system';
|
|
58
|
+
const mode = resolveColorMode(colorScheme);
|
|
59
|
+
return {
|
|
60
|
+
name: themeDef.name,
|
|
61
|
+
displayName: themeDef.displayName,
|
|
62
|
+
mode,
|
|
63
|
+
colorScheme,
|
|
64
|
+
colors: themeDef.colors,
|
|
65
|
+
...(themeDef.fontFamily !== undefined && { fontFamily: themeDef.fontFamily }),
|
|
66
|
+
...(themeDef.bodyFontFamily !== undefined && {
|
|
67
|
+
bodyFontFamily: themeDef.bodyFontFamily,
|
|
68
|
+
}),
|
|
69
|
+
...(themeDef.headingFontFamily !== undefined && {
|
|
70
|
+
headingFontFamily: themeDef.headingFontFamily,
|
|
71
|
+
}),
|
|
72
|
+
...(themeDef.letterSpacing !== undefined && {
|
|
73
|
+
letterSpacing: themeDef.letterSpacing,
|
|
74
|
+
}),
|
|
75
|
+
...(themeDef.textShadow !== undefined && { textShadow: themeDef.textShadow }),
|
|
76
|
+
...(themeDef.lineHeight !== undefined && { lineHeight: themeDef.lineHeight }),
|
|
77
|
+
...(themeDef.fontFiles !== undefined &&
|
|
78
|
+
themeDef.fontFiles.length > 0 && { fontFiles: themeDef.fontFiles }),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Build settings for propagation to iframes: inject navigation and full theme object
|
|
84
|
+
* so apps receive all theme variable values.
|
|
85
|
+
*/
|
|
86
|
+
function buildSettingsForPropagation(
|
|
36
87
|
settings: Settings,
|
|
37
|
-
|
|
88
|
+
config: ShellUIConfig | undefined,
|
|
38
89
|
lang: string,
|
|
39
90
|
): Settings {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
91
|
+
const appearance = getResolvedAppearanceForSettings(settings, config);
|
|
92
|
+
let result: Settings = {
|
|
93
|
+
...settings,
|
|
94
|
+
appearance: appearance ?? settings.appearance,
|
|
95
|
+
};
|
|
96
|
+
if (config?.navigation?.length) {
|
|
97
|
+
const items: SettingsNavigationItem[] = flattenNavigationItems(
|
|
98
|
+
config.navigation,
|
|
99
|
+
).map((item) => ({
|
|
100
|
+
path: item.path,
|
|
101
|
+
url: item.url,
|
|
102
|
+
label: resolveLabel(item.label, lang),
|
|
103
|
+
}));
|
|
104
|
+
result = { ...result, navigation: { items } };
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
47
107
|
}
|
|
48
108
|
|
|
49
109
|
const STORAGE_KEY = 'shellui:settings';
|
|
@@ -56,6 +116,75 @@ const getBrowserTimezone = (): string => {
|
|
|
56
116
|
return 'UTC';
|
|
57
117
|
};
|
|
58
118
|
|
|
119
|
+
const defaultAppearance: Appearance = {
|
|
120
|
+
name: 'default',
|
|
121
|
+
displayName: 'Default',
|
|
122
|
+
mode: 'light',
|
|
123
|
+
colorScheme: 'system',
|
|
124
|
+
colors: {
|
|
125
|
+
light: {
|
|
126
|
+
background: '#ffffff',
|
|
127
|
+
foreground: '#09090b',
|
|
128
|
+
card: '#ffffff',
|
|
129
|
+
cardForeground: '#09090b',
|
|
130
|
+
popover: '#ffffff',
|
|
131
|
+
popoverForeground: '#09090b',
|
|
132
|
+
primary: '#18181b',
|
|
133
|
+
primaryForeground: '#fafafa',
|
|
134
|
+
secondary: '#f4f4f5',
|
|
135
|
+
secondaryForeground: '#18181b',
|
|
136
|
+
muted: '#f4f4f5',
|
|
137
|
+
mutedForeground: '#71717a',
|
|
138
|
+
accent: '#f4f4f5',
|
|
139
|
+
accentForeground: '#18181b',
|
|
140
|
+
destructive: '#ef4444',
|
|
141
|
+
destructiveForeground: '#fafafa',
|
|
142
|
+
border: '#e4e4e7',
|
|
143
|
+
input: '#e4e4e7',
|
|
144
|
+
ring: '#18181b',
|
|
145
|
+
radius: '0.5rem',
|
|
146
|
+
sidebarBackground: '#fafafa',
|
|
147
|
+
sidebarForeground: '#09090b',
|
|
148
|
+
sidebarPrimary: '#18181b',
|
|
149
|
+
sidebarPrimaryForeground: '#fafafa',
|
|
150
|
+
sidebarAccent: '#e4e4e7',
|
|
151
|
+
sidebarAccentForeground: '#18181b',
|
|
152
|
+
sidebarBorder: '#e4e4e7',
|
|
153
|
+
sidebarRing: '#18181b',
|
|
154
|
+
},
|
|
155
|
+
dark: {
|
|
156
|
+
background: '#09090b',
|
|
157
|
+
foreground: '#fafafa',
|
|
158
|
+
card: '#09090b',
|
|
159
|
+
cardForeground: '#fafafa',
|
|
160
|
+
popover: '#09090b',
|
|
161
|
+
popoverForeground: '#fafafa',
|
|
162
|
+
primary: '#fafafa',
|
|
163
|
+
primaryForeground: '#18181b',
|
|
164
|
+
secondary: '#27272a',
|
|
165
|
+
secondaryForeground: '#fafafa',
|
|
166
|
+
muted: '#27272a',
|
|
167
|
+
mutedForeground: '#a1a1aa',
|
|
168
|
+
accent: '#27272a',
|
|
169
|
+
accentForeground: '#fafafa',
|
|
170
|
+
destructive: '#7f1d1d',
|
|
171
|
+
destructiveForeground: '#fafafa',
|
|
172
|
+
border: '#27272a',
|
|
173
|
+
input: '#27272a',
|
|
174
|
+
ring: '#d4d4d8',
|
|
175
|
+
radius: '0.5rem',
|
|
176
|
+
sidebarBackground: '#09090b',
|
|
177
|
+
sidebarForeground: '#fafafa',
|
|
178
|
+
sidebarPrimary: '#fafafa',
|
|
179
|
+
sidebarPrimaryForeground: '#18181b',
|
|
180
|
+
sidebarAccent: '#27272a',
|
|
181
|
+
sidebarAccentForeground: '#fafafa',
|
|
182
|
+
sidebarBorder: '#27272a',
|
|
183
|
+
sidebarRing: '#d4d4d8',
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
|
|
59
188
|
const defaultSettings: Settings = {
|
|
60
189
|
developerFeatures: {
|
|
61
190
|
enabled: false,
|
|
@@ -69,10 +198,7 @@ const defaultSettings: Settings = {
|
|
|
69
198
|
shellcore: false,
|
|
70
199
|
},
|
|
71
200
|
},
|
|
72
|
-
appearance:
|
|
73
|
-
theme: 'system',
|
|
74
|
-
themeName: 'default',
|
|
75
|
-
},
|
|
201
|
+
appearance: defaultAppearance,
|
|
76
202
|
language: {
|
|
77
203
|
code: 'en',
|
|
78
204
|
},
|
|
@@ -122,8 +248,13 @@ export function SettingsProvider({ children }: { children: ReactNode }) {
|
|
|
122
248
|
},
|
|
123
249
|
},
|
|
124
250
|
appearance: {
|
|
125
|
-
|
|
126
|
-
|
|
251
|
+
...defaultAppearance,
|
|
252
|
+
...parsed.appearance,
|
|
253
|
+
// Migrate from legacy theme/themeName
|
|
254
|
+
name: parsed.appearance?.name ?? parsed.appearance?.themeName ?? defaultAppearance.name,
|
|
255
|
+
colorScheme:
|
|
256
|
+
parsed.appearance?.colorScheme ?? parsed.appearance?.theme ?? defaultAppearance.colorScheme,
|
|
257
|
+
colors: parsed.appearance?.colors ?? defaultAppearance.colors,
|
|
127
258
|
},
|
|
128
259
|
language: {
|
|
129
260
|
code: parsed.language?.code || defaultSettings.language.code,
|
|
@@ -179,9 +310,9 @@ export function SettingsProvider({ children }: { children: ReactNode }) {
|
|
|
179
310
|
try {
|
|
180
311
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(newSettings));
|
|
181
312
|
// Confirm: root updated localStorage; re-inject navigation when propagating
|
|
182
|
-
const settingsToPropagate =
|
|
313
|
+
const settingsToPropagate = buildSettingsForPropagation(
|
|
183
314
|
newSettings,
|
|
184
|
-
config
|
|
315
|
+
config,
|
|
185
316
|
i18n.language || 'en',
|
|
186
317
|
);
|
|
187
318
|
logger.info('Root Parent received settings update', { message });
|
|
@@ -202,9 +333,9 @@ export function SettingsProvider({ children }: { children: ReactNode }) {
|
|
|
202
333
|
() => {
|
|
203
334
|
// Use ref to always get current settings (avoids stale closure)
|
|
204
335
|
const currentSettings = settingsRef.current ?? defaultSettings;
|
|
205
|
-
const settingsWithNav =
|
|
336
|
+
const settingsWithNav = buildSettingsForPropagation(
|
|
206
337
|
currentSettings,
|
|
207
|
-
config
|
|
338
|
+
config,
|
|
208
339
|
i18n.language || 'en',
|
|
209
340
|
);
|
|
210
341
|
shellui.propagateMessage({
|
|
@@ -244,9 +375,9 @@ export function SettingsProvider({ children }: { children: ReactNode }) {
|
|
|
244
375
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(newSettings));
|
|
245
376
|
setSettings(newSettings);
|
|
246
377
|
// Propagate to child iframes (sendMessageToParent does nothing in root)
|
|
247
|
-
const settingsWithNav =
|
|
378
|
+
const settingsWithNav = buildSettingsForPropagation(
|
|
248
379
|
newSettings,
|
|
249
|
-
config
|
|
380
|
+
config,
|
|
250
381
|
i18n.language || 'en',
|
|
251
382
|
);
|
|
252
383
|
shellui.propagateMessage({
|
|
@@ -304,9 +435,9 @@ export function SettingsProvider({ children }: { children: ReactNode }) {
|
|
|
304
435
|
// If we're in the root window, update localStorage with defaults
|
|
305
436
|
if (window.parent === window) {
|
|
306
437
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(newSettings));
|
|
307
|
-
const settingsToPropagate =
|
|
438
|
+
const settingsToPropagate = buildSettingsForPropagation(
|
|
308
439
|
newSettings,
|
|
309
|
-
config
|
|
440
|
+
config,
|
|
310
441
|
i18n.language || 'en',
|
|
311
442
|
);
|
|
312
443
|
shellui.propagateMessage({
|
|
@@ -166,8 +166,8 @@ export const Appearance = () => {
|
|
|
166
166
|
const { t } = useTranslation('settings');
|
|
167
167
|
const { settings, updateSetting } = useSettings();
|
|
168
168
|
const { config } = useConfig();
|
|
169
|
-
const currentTheme = settings.appearance?.
|
|
170
|
-
const currentThemeName = settings.appearance?.
|
|
169
|
+
const currentTheme = settings.appearance?.colorScheme ?? 'system';
|
|
170
|
+
const currentThemeName = settings.appearance?.name ?? 'default';
|
|
171
171
|
|
|
172
172
|
const [availableThemes, setAvailableThemes] = useState<ThemeDefinition[]>([]);
|
|
173
173
|
|
|
@@ -246,7 +246,7 @@ export const Appearance = () => {
|
|
|
246
246
|
key={theme.value}
|
|
247
247
|
variant={isSelected ? 'default' : 'outline'}
|
|
248
248
|
onClick={() => {
|
|
249
|
-
updateSetting('appearance', {
|
|
249
|
+
updateSetting('appearance', { colorScheme: theme.value });
|
|
250
250
|
}}
|
|
251
251
|
className={cn(
|
|
252
252
|
'h-10 px-4 transition-all flex items-center gap-2 cursor-pointer',
|
|
@@ -283,7 +283,7 @@ export const Appearance = () => {
|
|
|
283
283
|
<button
|
|
284
284
|
key={theme.name}
|
|
285
285
|
onClick={() => {
|
|
286
|
-
updateSetting('appearance', {
|
|
286
|
+
updateSetting('appearance', { name: theme.name });
|
|
287
287
|
}}
|
|
288
288
|
className={cn(
|
|
289
289
|
'text-left transition-all cursor-pointer',
|
|
@@ -27,8 +27,8 @@ function applyThemeToDocument(isDark: boolean) {
|
|
|
27
27
|
export function useTheme() {
|
|
28
28
|
const { settings } = useSettings();
|
|
29
29
|
const { config } = useConfig();
|
|
30
|
-
const
|
|
31
|
-
const themeName = settings.appearance?.
|
|
30
|
+
const colorScheme = settings.appearance?.colorScheme ?? 'system';
|
|
31
|
+
const themeName = settings.appearance?.name ?? 'default';
|
|
32
32
|
|
|
33
33
|
// Apply theme immediately on mount (synchronously) to prevent empty colors
|
|
34
34
|
// This ensures CSS variables are set before first render
|
|
@@ -47,11 +47,11 @@ export function useTheme() {
|
|
|
47
47
|
|
|
48
48
|
if (themeDefinition) {
|
|
49
49
|
const determineIsDark = () => {
|
|
50
|
-
if (
|
|
50
|
+
if (colorScheme === 'system') {
|
|
51
51
|
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
|
|
52
52
|
return mediaQuery.matches;
|
|
53
53
|
}
|
|
54
|
-
return
|
|
54
|
+
return colorScheme === 'dark';
|
|
55
55
|
};
|
|
56
56
|
const isDark = determineIsDark();
|
|
57
57
|
applyThemeToDocument(isDark);
|
|
@@ -62,10 +62,10 @@ export function useTheme() {
|
|
|
62
62
|
const defaultTheme = getTheme('default');
|
|
63
63
|
if (defaultTheme) {
|
|
64
64
|
const isDark =
|
|
65
|
-
|
|
66
|
-
(
|
|
65
|
+
colorScheme === 'dark' ||
|
|
66
|
+
(colorScheme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
|
|
67
67
|
applyTheme(defaultTheme, isDark);
|
|
68
68
|
}
|
|
69
69
|
}
|
|
70
|
-
}, [
|
|
70
|
+
}, [colorScheme, themeName, config]); // Run when colorScheme, themeName, or config changes
|
|
71
71
|
}
|
package/src/router/routes.tsx
CHANGED
|
@@ -24,9 +24,6 @@ const IndexRoute = lazy(() =>
|
|
|
24
24
|
const NotFoundView = lazy(() =>
|
|
25
25
|
import('../components/NotFoundView').then((m) => ({ default: m.NotFoundView })),
|
|
26
26
|
);
|
|
27
|
-
const AppPathView = lazy(() =>
|
|
28
|
-
import('../components/AppPathView').then((m) => ({ default: m.AppPathView })),
|
|
29
|
-
);
|
|
30
27
|
|
|
31
28
|
function RouteFallback() {
|
|
32
29
|
return (
|
|
@@ -62,15 +59,6 @@ export const createRoutes = (config: ShellUIConfig): RouteObject[] => {
|
|
|
62
59
|
</Suspense>
|
|
63
60
|
),
|
|
64
61
|
},
|
|
65
|
-
{
|
|
66
|
-
// App-path route: renders component-based nav items so they can be loaded in an iframe
|
|
67
|
-
path: `${urls.appPath.replace(/^\//, '')}/:path/*`,
|
|
68
|
-
element: (
|
|
69
|
-
<Suspense fallback={<RouteFallback />}>
|
|
70
|
-
<AppPathView />
|
|
71
|
-
</Suspense>
|
|
72
|
-
),
|
|
73
|
-
},
|
|
74
62
|
{
|
|
75
63
|
// Catch-all route
|
|
76
64
|
path: '*',
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { useParams } from 'react-router';
|
|
2
|
-
import { useConfig } from '../features/config/useConfig';
|
|
3
|
-
import { flattenNavigationItems } from '../features/layouts/utils';
|
|
4
|
-
import { NotFoundView } from './NotFoundView';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Renders the React component for a navigation item when the app is loaded at /__app/:path.
|
|
8
|
-
* Uses navItem.component (when config is not serialized) or shelluiComponents from context (injected by CLI when componentPath is set).
|
|
9
|
-
*/
|
|
10
|
-
export const AppPathView = () => {
|
|
11
|
-
const { config, shelluiComponents } = useConfig();
|
|
12
|
-
const { path: pathParam } = useParams<{ path: string }>();
|
|
13
|
-
const navigation = config?.navigation;
|
|
14
|
-
const items = navigation?.length ? flattenNavigationItems(navigation) : [];
|
|
15
|
-
const navItem = pathParam
|
|
16
|
-
? items.find(
|
|
17
|
-
(item) =>
|
|
18
|
-
item.path === pathParam ||
|
|
19
|
-
(pathParam === 'home' && (item.path === '' || item.path === '/')),
|
|
20
|
-
)
|
|
21
|
-
: null;
|
|
22
|
-
|
|
23
|
-
const pathKey = pathParam === 'home' || !pathParam ? 'home' : pathParam;
|
|
24
|
-
const Component = navItem?.component ?? shelluiComponents?.[pathKey];
|
|
25
|
-
|
|
26
|
-
if (!Component) {
|
|
27
|
-
return <NotFoundView />;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return <Component />;
|
|
31
|
-
};
|