@shellui/core 0.0.23 → 0.2.0-alpha.0

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 (35) hide show
  1. package/package.json +4 -2
  2. package/src/components/AppPathView.tsx +31 -0
  3. package/src/components/ContentView.tsx +14 -6
  4. package/src/components/HomeView.tsx +9 -2
  5. package/src/components/IndexRoute.tsx +37 -0
  6. package/src/components/NotFoundView.tsx +3 -2
  7. package/src/components/ViewRoute.tsx +11 -7
  8. package/src/components/ui/drawer.tsx +3 -2
  9. package/src/components/ui/tooltip.tsx +52 -0
  10. package/src/constants/urls.ts +2 -0
  11. package/src/features/config/ConfigProvider.ts +20 -76
  12. package/src/features/config/shellui-config.d.ts +13 -0
  13. package/src/features/config/types.ts +14 -3
  14. package/src/features/config/useConfig.ts +1 -10
  15. package/src/features/cookieConsent/cookieConsent.ts +2 -4
  16. package/src/features/layouts/AppBarLayout.tsx +260 -0
  17. package/src/features/layouts/AppLayout.tsx +6 -0
  18. package/src/features/layouts/DefaultLayout.tsx +25 -17
  19. package/src/features/layouts/OverlayShell.tsx +19 -8
  20. package/src/features/layouts/WindowsLayout.tsx +11 -9
  21. package/src/features/layouts/utils.ts +44 -0
  22. package/src/features/sentry/initSentry.ts +82 -12
  23. package/src/features/settings/SettingsProvider.tsx +2 -1
  24. package/src/features/settings/SettingsView.tsx +79 -15
  25. package/src/features/settings/components/Advanced.tsx +17 -2
  26. package/src/features/settings/components/ApplicationSettingsPanel.tsx +25 -0
  27. package/src/features/settings/components/Develop.tsx +68 -4
  28. package/src/i18n/translations/en/common.json +5 -0
  29. package/src/i18n/translations/en/settings.json +3 -1
  30. package/src/i18n/translations/fr/common.json +5 -0
  31. package/src/i18n/translations/fr/settings.json +3 -1
  32. package/src/index.css +10 -0
  33. package/src/lib/z-index.ts +2 -0
  34. package/src/router/routes.tsx +18 -5
  35. package/tailwind.config.js +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shellui/core",
3
- "version": "0.0.23",
3
+ "version": "0.2.0-alpha.0",
4
4
  "description": "ShellUI Core - Core React application runtime",
5
5
  "main": "src/index.ts",
6
6
  "type": "module",
@@ -42,6 +42,7 @@
42
42
  "@radix-ui/react-dialog": "^1.1.15",
43
43
  "@radix-ui/react-separator": "^1.1.8",
44
44
  "@radix-ui/react-slot": "^1.2.4",
45
+ "@radix-ui/react-tooltip": "^1.1.6",
45
46
  "class-variance-authority": "^0.7.1",
46
47
  "clsx": "^2.1.1",
47
48
  "i18next": "^23.15.0",
@@ -57,7 +58,7 @@
57
58
  "workbox-strategies": "^7.1.0",
58
59
  "workbox-cacheable-response": "^7.1.0",
59
60
  "workbox-expiration": "^7.1.0",
60
- "@shellui/sdk": "0.0.23"
61
+ "@shellui/sdk": "0.2.0-alpha.0"
61
62
  },
62
63
  "peerDependencies": {
63
64
  "react": "^18.0.0 || ^19.0.0",
@@ -72,6 +73,7 @@
72
73
  "@types/react-dom": "^19.2.3",
73
74
  "react": "^18.2.0",
74
75
  "react-dom": "^18.2.0",
76
+ "tailwindcss-animate": "^1.0.7",
75
77
  "typescript": "^5.0.0"
76
78
  },
77
79
  "scripts": {
@@ -0,0 +1,31 @@
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
+ };
@@ -16,17 +16,22 @@ 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;
19
21
  pathPrefix: string;
20
22
  ignoreMessages?: boolean;
21
- navItem: NavigationItem;
23
+ /** Nav item for this content; may be undefined when opening by URL (e.g. settings modal) if no matching item found. */
24
+ navItem?: NavigationItem | null;
22
25
  }
23
26
 
24
27
  export const ContentView = ({
25
28
  url,
29
+ baseUrl: baseUrlProp,
26
30
  pathPrefix,
27
31
  ignoreMessages = false,
28
32
  navItem,
29
33
  }: ContentViewProps) => {
34
+ const baseUrl = baseUrlProp ?? navItem?.url ?? url;
30
35
  const navigate = useNavigate();
31
36
  const iframeRef = useRef<HTMLIFrameElement>(null);
32
37
  const isInternalNavigation = useRef(false);
@@ -58,9 +63,13 @@ export const ContentView = ({
58
63
  }
59
64
 
60
65
  const { pathname, search, hash } = data.payload as ShellUIUrlPayload;
61
- // Remove leading slash and trailing slashes from iframe pathname
62
- let cleanPathname = pathname.startsWith(navItem.url)
63
- ? pathname.slice(navItem.url.length)
66
+ // Use pathname part of baseUrl for comparison (iframe pathname is always path-only)
67
+ const basePathname =
68
+ baseUrl.startsWith('http') || baseUrl.startsWith('//')
69
+ ? new URL(baseUrl, window.location.origin).pathname
70
+ : baseUrl;
71
+ let cleanPathname = pathname.startsWith(basePathname)
72
+ ? pathname.slice(basePathname.length)
64
73
  : pathname;
65
74
  cleanPathname = cleanPathname.startsWith('/') ? cleanPathname.slice(1) : cleanPathname;
66
75
  cleanPathname = cleanPathname.replace(/\/+$/, ''); // Remove trailing slashes
@@ -102,7 +111,7 @@ export const ContentView = ({
102
111
  return () => {
103
112
  cleanup();
104
113
  };
105
- }, [pathPrefix, navigate]);
114
+ }, [pathPrefix, navigate, baseUrl]);
106
115
 
107
116
  // Hide loading overlay when iframe sends SHELLUI_INITIALIZED
108
117
  useEffect(() => {
@@ -248,7 +257,6 @@ export const ContentView = ({
248
257
  border: 'none',
249
258
  display: 'block',
250
259
  }}
251
- title="Content Frame"
252
260
  sandbox="allow-same-origin allow-scripts allow-forms allow-popups allow-popups-to-escape-sandbox"
253
261
  referrerPolicy="no-referrer-when-downgrade"
254
262
  />
@@ -6,14 +6,21 @@ export const HomeView = () => {
6
6
  const { config } = useConfig();
7
7
 
8
8
  return (
9
- <div className="flex flex-col items-center justify-center h-full p-8 md:p-10">
9
+ <div className="flex flex-col items-center justify-center h-full p-8 md:p-10 max-w-2xl mx-auto text-center">
10
10
  <h1
11
11
  className="m-0 text-3xl font-light text-foreground"
12
12
  style={{ fontFamily: 'var(--heading-font-family, inherit)' }}
13
13
  >
14
- {t('welcome', { title: config.title })}
14
+ {t('welcome', { title: config?.title ?? 'ShellUI' })}
15
15
  </h1>
16
16
  <p className="mt-4 text-lg text-muted-foreground">{t('getStarted')}</p>
17
+ <div className="mt-8 p-4 rounded-lg bg-muted/50 border border-border text-left">
18
+ <p className="text-sm font-medium text-foreground mb-2">{t('homeConfig.intro')}</p>
19
+ <ul className="text-sm text-muted-foreground list-disc list-inside space-y-1">
20
+ <li>{t('homeConfig.startUrl')}</li>
21
+ <li>{t('homeConfig.rootNav')}</li>
22
+ </ul>
23
+ </div>
17
24
  </div>
18
25
  );
19
26
  };
@@ -0,0 +1,37 @@
1
+ import { Navigate } from 'react-router';
2
+ import { useConfig } from '../features/config/useConfig';
3
+ import { flattenNavigationItems } from '../features/layouts/utils';
4
+ import { HomeView } from './HomeView';
5
+ import { ViewRoute } from './ViewRoute';
6
+
7
+ /**
8
+ * Renders the root path "/":
9
+ * - If start_url is set in config, redirects to start_url.
10
+ * - Else if a navigation item has path "" or "/", shows that item's content.
11
+ * - Otherwise shows the default HomeView.
12
+ */
13
+ export const IndexRoute = () => {
14
+ const { config } = useConfig();
15
+
16
+ const startUrl = config?.start_url?.trim();
17
+ if (startUrl) {
18
+ const to = startUrl.startsWith('/') ? startUrl : `/${startUrl}`;
19
+ return (
20
+ <Navigate
21
+ to={to}
22
+ replace
23
+ />
24
+ );
25
+ }
26
+
27
+ const navigation = config?.navigation;
28
+ if (navigation?.length) {
29
+ const navigationItems = flattenNavigationItems(navigation);
30
+ const rootNavItem = navigationItems.find((item) => item.path === '' || item.path === '/');
31
+ if (rootNavItem) {
32
+ return <ViewRoute navigation={navigationItems} />;
33
+ }
34
+ }
35
+
36
+ return <HomeView />;
37
+ };
@@ -1,6 +1,7 @@
1
1
  import { useTranslation } from 'react-i18next';
2
2
  import { shellui } from '@shellui/sdk';
3
3
  import { useConfig } from '../features/config/useConfig';
4
+ import { getNavPathPrefix } from '../features/layouts/utils';
4
5
  import type { NavigationItem, NavigationGroup } from '../features/config/types';
5
6
 
6
7
  const flattenNavigationItems = (
@@ -56,7 +57,7 @@ export const NotFoundView = () => {
56
57
  >
57
58
  {navItems.map((item, index) => (
58
59
  <span
59
- key={item.path}
60
+ key={item.path || 'root'}
60
61
  className="inline-flex items-center gap-x-2"
61
62
  >
62
63
  {index > 0 && (
@@ -69,7 +70,7 @@ export const NotFoundView = () => {
69
70
  )}
70
71
  <button
71
72
  type="button"
72
- onClick={() => handleNavigate(`/${item.path}`)}
73
+ onClick={() => handleNavigate(getNavPathPrefix(item))}
73
74
  className="text-muted-foreground hover:text-foreground hover:underline underline-offset-2 cursor-pointer bg-transparent border-0 p-0 font-normal"
74
75
  >
75
76
  {resolveLocalizedString(item.label, currentLanguage)}
@@ -1,5 +1,6 @@
1
1
  import { useMemo } from 'react';
2
2
  import { Navigate, useLocation } from 'react-router';
3
+ import { getNavPathPrefix, getEffectiveUrl } from '../features/layouts/utils';
3
4
  import { ContentView } from './ContentView';
4
5
  import type { NavigationItem } from '../features/config/types';
5
6
 
@@ -13,7 +14,7 @@ export const ViewRoute = ({ navigation }: ViewRouteProps) => {
13
14
 
14
15
  const navItem = useMemo(() => {
15
16
  return navigation.find((item) => {
16
- const pathPrefix = `/${item.path}`;
17
+ const pathPrefix = getNavPathPrefix(item);
17
18
  return pathname === pathPrefix || pathname.startsWith(`${pathPrefix}/`);
18
19
  });
19
20
  }, [navigation, pathname]);
@@ -28,18 +29,21 @@ export const ViewRoute = ({ navigation }: ViewRouteProps) => {
28
29
  }
29
30
  // Calculate the relative path from the navItem.path
30
31
  // e.g. if item.path is "docs" and pathname is "/docs/intro", subPath is "intro"
31
- const pathPrefix = `/${navItem.path}`;
32
+ const pathPrefix = getNavPathPrefix(navItem);
32
33
  const subPath = pathname.length > pathPrefix.length ? pathname.slice(pathPrefix.length + 1) : '';
33
34
 
34
- // Construct the final URL for the iframe
35
- let finalUrl = navItem.url;
36
- if (subPath) {
37
- const baseUrl = navItem.url.endsWith('/') ? navItem.url : `${navItem.url}/`;
38
- finalUrl = `${baseUrl}${subPath}`;
35
+ // Effective URL: url string or app-path URL when item uses a component
36
+ const baseUrl = getEffectiveUrl(navItem);
37
+ let finalUrl = baseUrl;
38
+ if (!navItem.component && subPath && navItem.url) {
39
+ const base = navItem.url.endsWith('/') ? navItem.url : `${navItem.url}/`;
40
+ finalUrl = `${base}${subPath}`;
39
41
  }
42
+
40
43
  return (
41
44
  <ContentView
42
45
  url={finalUrl}
46
+ baseUrl={baseUrl}
43
47
  pathPrefix={navItem.path}
44
48
  navItem={navItem}
45
49
  />
@@ -79,9 +79,10 @@ const DrawerContent = forwardRef<ComponentRef<typeof VaulDrawer.Content>, Drawer
79
79
  const isVertical = pos === 'top' || pos === 'bottom';
80
80
  // Set dimension via inline style; use both width/height and max so Vaul/defaults don't override.
81
81
  const effectiveSize = size?.trim() || (isVertical ? '80vh' : '80vw');
82
+ // Use min() to ensure drawer doesn't exceed viewport on mobile devices
82
83
  const sizeStyle = isVertical
83
- ? { height: effectiveSize, maxHeight: effectiveSize }
84
- : { width: effectiveSize, maxWidth: effectiveSize };
84
+ ? { height: effectiveSize, maxHeight: `min(${effectiveSize}, 100vh)` }
85
+ : { width: effectiveSize, maxWidth: `min(${effectiveSize}, 100%)` };
85
86
  return (
86
87
  <DrawerPortal>
87
88
  <DrawerOverlay />
@@ -0,0 +1,52 @@
1
+ import { type ComponentPropsWithoutRef, type ElementRef, forwardRef, type ReactNode } from 'react';
2
+ import * as TooltipPrimitive from '@radix-ui/react-tooltip';
3
+ import { cn } from '../../lib/utils';
4
+ import { Z_INDEX } from '../../lib/z-index';
5
+
6
+ const TooltipProvider = TooltipPrimitive.Provider;
7
+
8
+ const Tooltip = TooltipPrimitive.Root;
9
+
10
+ const TooltipTrigger = TooltipPrimitive.Trigger;
11
+
12
+ const TooltipContent = forwardRef<
13
+ ElementRef<typeof TooltipPrimitive.Content>,
14
+ ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
15
+ >(({ className, sideOffset = 6, ...props }, ref) => (
16
+ <TooltipPrimitive.Portal>
17
+ <TooltipPrimitive.Content
18
+ ref={ref}
19
+ sideOffset={sideOffset}
20
+ className={cn(
21
+ 'overflow-hidden rounded-md border border-border bg-popover px-2.5 py-1.5 text-xs text-popover-foreground shadow-md',
22
+ 'animate-none',
23
+ className,
24
+ )}
25
+ style={{ zIndex: Z_INDEX.TOOLTIP }}
26
+ {...props}
27
+ />
28
+ </TooltipPrimitive.Portal>
29
+ ));
30
+ TooltipContent.displayName = TooltipPrimitive.Content.displayName;
31
+
32
+ export interface AppBarTooltipProps {
33
+ label: string;
34
+ children: ReactNode;
35
+ }
36
+
37
+ /** Themed tooltip for app-bar end buttons: no arrow, no animation. */
38
+ function AppBarTooltip({ label, children }: AppBarTooltipProps) {
39
+ return (
40
+ <Tooltip delayDuration={200}>
41
+ <TooltipTrigger asChild>{children}</TooltipTrigger>
42
+ <TooltipContent
43
+ side="bottom"
44
+ align="center"
45
+ >
46
+ {label}
47
+ </TooltipContent>
48
+ </Tooltip>
49
+ );
50
+ }
51
+
52
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider, AppBarTooltip };
@@ -1,4 +1,6 @@
1
1
  export default {
2
2
  settings: '/__settings',
3
3
  cookiePreferences: '/__cookie-preferences',
4
+ /** Base path for navigation items that use a React component; full URL is /__app/{path}. */
5
+ appPath: '/__app',
4
6
  };
@@ -1,11 +1,15 @@
1
1
  import { createContext, useState, createElement, type ReactNode } from 'react';
2
+ import type { ComponentType } from 'react';
2
3
  import { getLogger } from '@shellui/sdk';
3
4
  import type { ShellUIConfig } from './types';
5
+ import shelluiConfig, { shelluiComponents } from '@shellui/config';
4
6
 
5
7
  const logger = getLogger('shellcore');
6
8
 
7
9
  export interface ConfigContextValue {
8
10
  config: ShellUIConfig;
11
+ /** Map of nav path -> component for /__app/:path (injected when componentPath is set in config). */
12
+ shelluiComponents: Record<string, ComponentType>;
9
13
  }
10
14
 
11
15
  export const ConfigContext = createContext<ConfigContextValue | null>(null);
@@ -19,93 +23,33 @@ export interface ConfigProviderProps {
19
23
  let configLogged = false;
20
24
 
21
25
  /**
22
- * Loads ShellUI config from __SHELLUI_CONFIG__ (injected by Vite at build time)
23
- * and provides it via context. Children can use useConfig() to read config.
26
+ * Provides ShellUI config (loaded from @shellui/config at build time) via context.
27
+ * Children can use useConfig() to read config.
24
28
  */
25
-
26
29
  export function ConfigProvider(props: ConfigProviderProps): ReturnType<typeof createElement> {
27
30
  const [config] = useState<ShellUIConfig>(() => {
28
31
  try {
29
- // __SHELLUI_CONFIG__ is replaced by Vite at build time via define
30
- // After replacement, it will be a JSON string like: "{\"title\":\"shellui\",...}"
31
- // Vite's define inserts the string value directly, so we only need to parse once
32
- // Access it directly (no typeof check) so Vite can statically analyze and replace it
33
- // @ts-expect-error - __SHELLUI_CONFIG__ is injected by Vite at build time
34
- let configValue: unknown = __SHELLUI_CONFIG__;
35
-
36
- // In development, if __SHELLUI_CONFIG__ is undefined, it might not have been replaced by Vite
37
- // This can happen if the Vite define configuration isn't working properly
38
- if (configValue === undefined && typeof window !== 'undefined') {
39
- // Try to get it from window (fallback for dev mode issues)
40
- const g = window as unknown as { __SHELLUI_CONFIG__?: unknown };
41
- if (g.__SHELLUI_CONFIG__ !== undefined) {
42
- configValue = g.__SHELLUI_CONFIG__;
43
- }
44
- }
45
-
46
- // After Vite replacement, configValue will be a JSON string
47
- // Example: "{\"title\":\"shellui\"}" -> parse -> {title: "shellui"}
48
- if (configValue !== undefined && configValue !== null && typeof configValue === 'string') {
49
- try {
50
- // Parse the JSON string to get the config object
51
- const parsedConfig: ShellUIConfig = JSON.parse(configValue);
52
- if (typeof window !== 'undefined' && parsedConfig.runtime === 'tauri') {
53
- (window as Window & { __SHELLUI_TAURI__?: boolean }).__SHELLUI_TAURI__ = true;
54
- }
55
-
56
- // Log in dev mode to help debug (only once per page load)
57
- if (process.env.NODE_ENV === 'development' && !configLogged) {
58
- configLogged = true;
59
- logger.info('Config loaded from __SHELLUI_CONFIG__', {
60
- hasNavigation: !!parsedConfig.navigation,
61
- navigationItems: parsedConfig.navigation?.length || 0,
62
- });
63
- }
64
-
65
- return parsedConfig;
66
- } catch (parseError) {
67
- logger.error('Failed to parse config JSON:', { error: parseError });
68
- logger.error('Config value (first 200 chars):', { value: configValue.substring(0, 200) });
69
- // Fall through to return empty config
70
- }
32
+ const resolved = shelluiConfig ?? ({} as ShellUIConfig);
33
+ if (typeof window !== 'undefined' && resolved.runtime === 'tauri') {
34
+ (window as Window & { __SHELLUI_TAURI__?: boolean }).__SHELLUI_TAURI__ = true;
71
35
  }
72
-
73
- // Fallback: try to read from globalThis (for edge cases or if define didn't work)
74
- // This handles cases where Vite's define didn't work or config needs to be injected at runtime
75
- const g = globalThis as unknown as { __SHELLUI_CONFIG__?: unknown };
76
- if (typeof g.__SHELLUI_CONFIG__ !== 'undefined' && configValue === undefined) {
77
- const fallbackValue = g.__SHELLUI_CONFIG__;
78
- const parsedConfig: ShellUIConfig =
79
- typeof fallbackValue === 'string'
80
- ? JSON.parse(fallbackValue)
81
- : (fallbackValue as ShellUIConfig);
82
- if (typeof window !== 'undefined' && parsedConfig.runtime === 'tauri') {
83
- (window as Window & { __SHELLUI_TAURI__?: boolean }).__SHELLUI_TAURI__ = true;
84
- }
85
-
86
- if (process.env.NODE_ENV === 'development') {
87
- logger.warn('Config loaded from globalThis fallback (define may not have worked)');
88
- }
89
-
90
- return parsedConfig;
36
+ if (process.env.NODE_ENV === 'development' && !configLogged) {
37
+ configLogged = true;
38
+ logger.info('Config loaded from @shellui/config', {
39
+ hasNavigation: !!resolved.navigation,
40
+ navigationItems: resolved.navigation?.length || 0,
41
+ });
91
42
  }
92
-
93
- // Return empty config if __SHELLUI_CONFIG__ is undefined (fallback for edge cases)
94
- // This ensures the provider always provides a value, preventing "useConfig must be used within ConfigProvider" errors
95
- if (process.env.NODE_ENV === 'development') {
96
- logger.warn(
97
- 'Config not found. Using empty config. Make sure shellui.config.ts is properly loaded and Vite define is configured correctly.',
98
- );
99
- }
100
- return {} as ShellUIConfig;
43
+ return resolved;
101
44
  } catch (err) {
102
45
  logger.error('Failed to load ShellUI config:', { error: err });
103
- // Don't throw - return empty config instead to prevent app crash
104
46
  return {} as ShellUIConfig;
105
47
  }
106
48
  });
107
49
 
108
- // Always provide a value - never null - to prevent "useConfig must be used within ConfigProvider" errors
109
- const value: ConfigContextValue = { config };
50
+ const value: ConfigContextValue = {
51
+ config,
52
+ shelluiComponents: typeof shelluiComponents !== 'undefined' ? shelluiComponents : {},
53
+ };
110
54
  return createElement(ConfigContext.Provider, { value }, props.children);
111
55
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Type declaration for the virtual @shellui/config module.
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
+ */
6
+ declare module '@shellui/config' {
7
+ import type { ComponentType } from 'react';
8
+ import type { ShellUIConfig } from './types';
9
+ 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
+ export default shelluiConfig;
13
+ }
@@ -1,3 +1,5 @@
1
+ import type { ComponentType } from 'react';
2
+
1
3
  // Language-specific label/title
2
4
  export type LocalizedString =
3
5
  | string
@@ -10,13 +12,18 @@ export type LocalizedString =
10
12
  /** Drawer position when opening a link in a drawer (optional, used when openIn === 'drawer'). */
11
13
  export type DrawerPosition = 'top' | 'bottom' | 'left' | 'right';
12
14
 
13
- /** Layout mode: 'sidebar' (default) shows navigation sidebar; 'fullscreen' shows only content area; 'windows' shows a taskbar with start menu and multi-window desktop. */
14
- export type LayoutType = 'sidebar' | 'fullscreen' | 'windows';
15
+ /** Layout mode: 'sidebar' (default) shows navigation sidebar; 'fullscreen' shows only content area; 'windows' shows a taskbar with start menu and multi-window desktop; 'app-bar' shows a compact top bar with select menu for start links and icon-only end links. */
16
+ export type LayoutType = 'sidebar' | 'fullscreen' | 'windows' | 'app-bar';
15
17
 
16
18
  export interface NavigationItem {
17
19
  label: string | LocalizedString;
18
20
  path: string;
19
- url: string;
21
+ /** URL to load in the content area (iframe). Omit when using component; then the app-path URL is used. */
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;
20
27
  icon?: string; // Path to SVG icon file (e.g., '/icons/book-open.svg')
21
28
  /** When true, hide this item from the sidebar and 404 page; route remains valid and item still appears in Develop settings. */
22
29
  hidden?: boolean;
@@ -30,6 +37,8 @@ export interface NavigationItem {
30
37
  drawerPosition?: DrawerPosition;
31
38
  /** Sidebar position: 'start' (default) or 'end'. End items are rendered in the sidebar footer. */
32
39
  position?: 'start' | 'end';
40
+ /** URL to display as a settings panel in Settings > Applications. When set, the nav item appears in the Applications group. */
41
+ settings?: string;
33
42
  }
34
43
 
35
44
  export interface NavigationGroup {
@@ -185,6 +194,8 @@ export interface ShellUIConfig {
185
194
  language?: string | string[]; // Single language code or array of enabled language codes (e.g., 'en' or ['en', 'fr'])
186
195
  /** Layout mode: 'sidebar' (default) or 'fullscreen'. Fullscreen shows only content with no navigation. */
187
196
  layout?: LayoutType;
197
+ /** When set, opening the app at "/" redirects to this path (e.g. "/playground"). */
198
+ start_url?: string;
188
199
  navigation?: (NavigationItem | NavigationGroup)[];
189
200
  themes?: ThemeDefinition[]; // Custom themes to register
190
201
  defaultTheme?: string; // Default theme name to use
@@ -10,22 +10,13 @@ export function useConfig(): ConfigContextValue {
10
10
  const context = useContext(ConfigContext);
11
11
  if (context === null) {
12
12
  // This error should never happen if ConfigProvider is properly wrapping the app
13
- // If you see this error, check that:
14
- // 1. Your component is rendered inside a <ConfigProvider> tree
15
- // 2. The ConfigProvider is mounted before components that use useConfig()
16
- // 3. Vite's define configuration is working correctly (check __SHELLUI_CONFIG__)
17
13
  const error = new Error(
18
14
  'useConfig must be used within a ConfigProvider. ' +
19
- 'Make sure your app is wrapped with <ConfigProvider> and that Vite define is configured correctly.',
15
+ 'Make sure your app is wrapped with <ConfigProvider>.',
20
16
  );
21
17
  if (typeof window !== 'undefined' && process.env.NODE_ENV === 'development') {
22
18
  // eslint-disable-next-line no-console
23
19
  console.error('[ShellUI] ConfigProvider error:', error);
24
- // eslint-disable-next-line no-console
25
- console.error(
26
- '[ShellUI] Check that __SHELLUI_CONFIG__ is defined:',
27
- typeof (window as unknown as { __SHELLUI_CONFIG__?: unknown }).__SHELLUI_CONFIG__,
28
- );
29
20
  }
30
21
  throw error;
31
22
  }
@@ -1,13 +1,11 @@
1
1
  import type { ShellUIConfig } from '../config/types';
2
+ import shelluiConfig from '@shellui/config';
2
3
 
3
4
  const STORAGE_KEY = 'shellui:settings';
4
5
 
5
6
  function getConfig(): ShellUIConfig | undefined {
6
7
  if (typeof globalThis === 'undefined') return undefined;
7
- const g = globalThis as unknown as { __SHELLUI_CONFIG__?: string | ShellUIConfig };
8
- const raw = g.__SHELLUI_CONFIG__;
9
- if (raw === null || raw === undefined) return undefined;
10
- return typeof raw === 'string' ? (JSON.parse(raw) as ShellUIConfig) : raw;
8
+ return shelluiConfig ?? undefined;
11
9
  }
12
10
 
13
11
  function getStoredCookieConsent(): {