@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.
Files changed (61) hide show
  1. package/package.json +2 -2
  2. package/src/app.tsx +2 -2
  3. package/src/components/ContentView.tsx +70 -135
  4. package/src/components/LoadingOverlay.tsx +5 -1
  5. package/src/components/ui/sidebar.tsx +2 -124
  6. package/src/constants/loading.ts +2 -0
  7. package/src/features/config/types.ts +2 -0
  8. package/src/features/layouts/AppLayout.tsx +22 -19
  9. package/src/features/layouts/LayoutFallback.tsx +8 -0
  10. package/src/features/layouts/OverlayShell.tsx +23 -9
  11. package/src/features/layouts/{AppBarLayout.tsx → appbar/AppBarLayout.tsx} +72 -78
  12. package/src/features/layouts/{FullscreenLayout.tsx → fullscreen/FullscreenLayout.tsx} +5 -11
  13. package/src/features/layouts/sidebar/BottomNavItem.tsx +88 -0
  14. package/src/features/layouts/sidebar/MobileBottomNav.tsx +168 -0
  15. package/src/features/layouts/sidebar/NavigationContent.tsx +159 -0
  16. package/src/features/layouts/sidebar/SidebarIcons.tsx +93 -0
  17. package/src/features/layouts/sidebar/SidebarInner.tsx +48 -0
  18. package/src/features/layouts/sidebar/SidebarLayout.tsx +86 -0
  19. package/src/features/layouts/sidebar/sidebarUtils.ts +23 -0
  20. package/src/features/layouts/sidebar/types.ts +8 -0
  21. package/src/features/layouts/utils.ts +29 -1
  22. package/src/features/layouts/{WindowsLayout.tsx → windows/WindowsLayout.tsx} +199 -204
  23. package/src/features/settings/SettingsView.tsx +178 -181
  24. package/src/{components → routes/components}/HomeView.tsx +1 -1
  25. package/src/{components → routes/components}/IndexRoute.tsx +4 -4
  26. package/src/routes/components/NavigationItemRoute.tsx +19 -0
  27. package/src/{components → routes/components}/NotFoundView.tsx +9 -4
  28. package/src/{components → routes/components}/RouteErrorBoundary.tsx +1 -1
  29. package/src/routes/components/RouteFallback.tsx +8 -0
  30. package/src/routes/hooks/useNavigationItems.ts +84 -0
  31. package/src/{router → routes}/routes.tsx +18 -16
  32. package/src/components/ViewRoute.tsx +0 -48
  33. package/src/dist/CookiePreferencesView.52b5aec8.js +0 -1182
  34. package/src/dist/CookiePreferencesView.52b5aec8.js.map +0 -1
  35. package/src/dist/DefaultLayout.045a82ff.js +0 -1964
  36. package/src/dist/DefaultLayout.045a82ff.js.map +0 -1
  37. package/src/dist/DefaultLayout.4454f259.js +0 -4414
  38. package/src/dist/DefaultLayout.4454f259.js.map +0 -1
  39. package/src/dist/FullscreenLayout.555c4987.js +0 -1054
  40. package/src/dist/FullscreenLayout.555c4987.js.map +0 -1
  41. package/src/dist/HomeView.ddfa7b68.js +0 -771
  42. package/src/dist/HomeView.ddfa7b68.js.map +0 -1
  43. package/src/dist/NotFoundView.c75be4f1.js +0 -811
  44. package/src/dist/NotFoundView.c75be4f1.js.map +0 -1
  45. package/src/dist/SettingsView.052b03a6.js +0 -4965
  46. package/src/dist/SettingsView.052b03a6.js.map +0 -1
  47. package/src/dist/ViewRoute.e6e3b142.js +0 -1042
  48. package/src/dist/ViewRoute.e6e3b142.js.map +0 -1
  49. package/src/dist/WindowsLayout.08724167.js +0 -1762
  50. package/src/dist/WindowsLayout.08724167.js.map +0 -1
  51. package/src/dist/esm.f0d741e6.js +0 -29520
  52. package/src/dist/esm.f0d741e6.js.map +0 -1
  53. package/src/dist/favicon.4367ac1e.svg +0 -14
  54. package/src/dist/index.parcel.36d65383.js +0 -54089
  55. package/src/dist/index.parcel.36d65383.js.map +0 -1
  56. package/src/dist/index.parcel.ca6d8a47.css +0 -3493
  57. package/src/dist/index.parcel.ca6d8a47.css.map +0 -1
  58. package/src/dist/index.parcel.html +0 -88
  59. package/src/features/layouts/DefaultLayout.tsx +0 -660
  60. package/src/features/layouts/LayoutProviders.tsx +0 -20
  61. /package/src/{router → routes}/router.tsx +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shellui/core",
3
- "version": "0.2.0-alpha.4",
3
+ "version": "0.2.0-beta.1",
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.4"
61
+ "@shellui/sdk": "0.2.0-beta.1"
62
62
  },
63
63
  "peerDependencies": {
64
64
  "react": "^18.0.0 || ^19.0.0",
package/src/app.tsx CHANGED
@@ -3,7 +3,7 @@ import { RouterProvider } from 'react-router';
3
3
  import { shellui } from '@shellui/sdk';
4
4
  import { useConfig } from './features/config/useConfig';
5
5
  import { ConfigProvider } from './features/config/ConfigProvider';
6
- import { createAppRouter } from './router/router';
6
+ import { createAppRouter } from './routes/router';
7
7
  import { SettingsProvider } from './features/settings/SettingsProvider';
8
8
  import { ThemeProvider } from './features/theme/ThemeProvider';
9
9
  import { I18nProvider } from './i18n/I18nProvider';
@@ -33,7 +33,7 @@ const AppContent = () => {
33
33
  unregisterServiceWorker();
34
34
  return;
35
35
  }
36
- const serviceWorkerEnabled = settings?.serviceWorker?.enabled ?? true; // Default to enabled
36
+ const serviceWorkerEnabled = settings?.serviceWorker?.enabled ?? false; // Default to enabled
37
37
 
38
38
  // Don't register service worker if navigation is empty or undefined
39
39
  // This helps prevent issues in development or misconfigured apps
@@ -1,5 +1,5 @@
1
- /* eslint-disable no-console */
2
1
  import type { NavigationItem } from '../features/config/types';
2
+ import { isHashRouterNavItem, getHashPathFromUrl } from '../features/layouts/utils';
3
3
  import {
4
4
  addIframe,
5
5
  removeIframe,
@@ -10,6 +10,7 @@ import {
10
10
  } from '@shellui/sdk';
11
11
  import { useEffect, useRef, useState } from 'react';
12
12
  import { useNavigate } from 'react-router';
13
+ import { LOADING_OVERLAY_DURATION_MS } from '../constants/loading';
13
14
  import { LoadingOverlay } from './LoadingOverlay';
14
15
 
15
16
  const logger = getLogger('shellcore');
@@ -29,11 +30,21 @@ export const ContentView = ({
29
30
  }: ContentViewProps) => {
30
31
  const navigate = useNavigate();
31
32
  const iframeRef = useRef<HTMLIFrameElement>(null);
32
- const isInternalNavigation = useRef(false);
33
33
  const cancelRevealRef = useRef<(() => void) | null>(null);
34
34
  const mountTimeRef = useRef(Date.now());
35
- const [initialUrl] = useState(url);
36
- const [isLoading, setIsLoading] = useState(true);
35
+ /** Real history methods; stored once so we never restore our no-ops after a second effect run. */
36
+ const historyOriginalsRef = useRef<{
37
+ pushState: History['pushState'];
38
+ replaceState: History['replaceState'];
39
+ } | null>(null);
40
+
41
+ const [isLoading, setIsLoading] = useState(() => {
42
+ // Skip overlay when same app URL was just loaded (e.g. switching App ↔ Root with same url)
43
+ if (!ignoreMessages) return false;
44
+ return true;
45
+ });
46
+
47
+ const [iframeUrl, setIframeUrl] = useState(url);
37
48
 
38
49
  const MIN_LOADING_MS = 80; // Don't reveal before this, reduces blink from theme/layout paint
39
50
 
@@ -47,6 +58,18 @@ export const ContentView = ({
47
58
  };
48
59
  }, []);
49
60
 
61
+ useEffect(() => {
62
+ if (ignoreMessages) return;
63
+ if (isLoading) return;
64
+ if (iframeRef.current) {
65
+ setIsLoading(true);
66
+ setIframeUrl('about:blank');
67
+ setTimeout(() => {
68
+ setIframeUrl(url);
69
+ }, 60);
70
+ }
71
+ }, [navItem]);
72
+
50
73
  // Sync parent URL when iframe notifies us of a change
51
74
  useEffect(() => {
52
75
  const cleanup = shellui.addMessageListener(
@@ -56,29 +79,52 @@ export const ContentView = ({
56
79
  return;
57
80
  }
58
81
 
82
+ if (isLoading) return;
83
+
59
84
  // Ignore URL CHANGE from other than ContentView iframe
60
85
  if (event.source !== iframeRef.current?.contentWindow) {
61
86
  return;
62
87
  }
63
88
 
64
89
  const { pathname, search, hash } = data.payload as ShellUIUrlPayload;
65
- // Remove leading slash and trailing slashes from iframe pathname
66
- let cleanPathname = pathname.startsWith(navItem.url)
67
- ? pathname.slice(navItem.url.length)
68
- : pathname;
69
- cleanPathname = cleanPathname.startsWith('/') ? cleanPathname.slice(1) : cleanPathname;
70
- cleanPathname = cleanPathname.replace(/\/+$/, ''); // Remove trailing slashes
71
- // Construct the new path without trailing slashes
72
- let newShellPath = cleanPathname
73
- ? `/${pathPrefix}/${cleanPathname}${search}${hash}`
74
- : `/${pathPrefix}${search}${hash}`;
75
-
76
- // Normalize: remove trailing slashes from pathname part only (preserve query/hash)
90
+ // Shell URL is always path + search only (no hash) so it's transparent whether the sub-app uses hash routing or not
91
+ let pathSegment: string;
92
+ if (isHashRouterNavItem(navItem) && hash) {
93
+ // Hash-router app: use path relative to nav item's hash (e.g. nav #/themes, iframe #/themes → segment ''; iframe #/themes/foo → segment 'foo')
94
+ const iframeHashPath = hash.replace(/^#\/?/, '').replace(/\/+$/, '') || '';
95
+ const navHashPath = getHashPathFromUrl(navItem.url).replace(/^\/+|\/+$/g, '');
96
+ const relative = navHashPath
97
+ ? iframeHashPath === navHashPath || iframeHashPath.startsWith(`${navHashPath}/`)
98
+ ? iframeHashPath.slice(navHashPath.length).replace(/^\//, '')
99
+ : iframeHashPath
100
+ : iframeHashPath;
101
+ pathSegment = relative;
102
+ } else {
103
+ // Non-hash app: route is pathname
104
+ let cleanPathname = pathname.startsWith(navItem.url)
105
+ ? pathname.slice(navItem.url.length)
106
+ : pathname;
107
+ cleanPathname = cleanPathname.startsWith('/') ? cleanPathname.slice(1) : cleanPathname;
108
+ pathSegment = cleanPathname.replace(/\/+$/, '');
109
+ }
110
+ // Root (pathPrefix '' or '/') must produce /segment not //segment
111
+ const isRoot = pathPrefix === '' || pathPrefix === '/';
112
+ let newShellPath = isRoot
113
+ ? pathSegment
114
+ ? `/${pathSegment}${search}`
115
+ : search
116
+ ? `/${search}`
117
+ : '/'
118
+ : pathSegment
119
+ ? `/${pathPrefix}/${pathSegment}${search}`
120
+ : `/${pathPrefix}${search}`;
121
+
122
+ // Normalize: remove trailing slashes from pathname part only (preserve query)
77
123
  const urlParts = newShellPath.match(/^([^?#]*)([?#].*)?$/);
78
124
  if (urlParts) {
79
125
  const pathnamePart = urlParts[1].replace(/\/+$/, '') || '/';
80
- const queryHashPart = urlParts[2] || '';
81
- newShellPath = pathnamePart + queryHashPart;
126
+ const queryPart = urlParts[2] || '';
127
+ newShellPath = pathnamePart + queryPart;
82
128
  }
83
129
 
84
130
  // Normalize current path for comparison (remove trailing slashes from pathname)
@@ -91,14 +137,7 @@ export const ContentView = ({
91
137
  const normalizedNewPath = normalizedNewPathname + (newPathParts?.[2] || '');
92
138
 
93
139
  if (currentPath !== normalizedNewPath) {
94
- // Mark this navigation as internal so we don't try to "push" it back to the iframe
95
- isInternalNavigation.current = true;
96
140
  navigate(newShellPath, { replace: true });
97
-
98
- // Reset the flag after a short delay to allow the render cycle to complete
99
- setTimeout(() => {
100
- isInternalNavigation.current = false;
101
- }, 100);
102
141
  }
103
142
  },
104
143
  );
@@ -106,7 +145,7 @@ export const ContentView = ({
106
145
  return () => {
107
146
  cleanup();
108
147
  };
109
- }, [pathPrefix, navigate]);
148
+ }, [pathPrefix, navigate, navItem]);
110
149
 
111
150
  const scheduleReveal = (reveal: () => void) => {
112
151
  const doReveal = () => {
@@ -126,6 +165,7 @@ export const ContentView = ({
126
165
 
127
166
  // Hide loading overlay when iframe sends SHELLUI_INITIALIZED.
128
167
  // Defer reveal (double rAF + min time) so the iframe has time to apply theme and paint.
168
+ // Remember this URL so we can skip the overlay when navigating to the same app (e.g. App ↔ Root).
129
169
  useEffect(() => {
130
170
  const cleanup = shellui.addMessageListener(
131
171
  'SHELLUI_INITIALIZED',
@@ -148,9 +188,9 @@ export const ContentView = ({
148
188
  cancelRevealRef.current = null;
149
189
  cleanup();
150
190
  };
151
- }, []);
191
+ }, [ignoreMessages]);
152
192
 
153
- // Fallback: hide overlay after 400ms if SHELLUI_INITIALIZED was not received.
193
+ // Fallback: hide overlay after LOADING_OVERLAY_DURATION_MS if SHELLUI_INITIALIZED was not received.
154
194
  useEffect(() => {
155
195
  if (!isLoading) return;
156
196
  const timeoutId = setTimeout(() => {
@@ -165,115 +205,10 @@ export const ContentView = ({
165
205
  if (!cancelled) setIsLoading(false);
166
206
  cancelRevealRef.current = null;
167
207
  });
168
- }, 400);
208
+ }, LOADING_OVERLAY_DURATION_MS);
169
209
  return () => clearTimeout(timeoutId);
170
210
  }, [isLoading]);
171
211
 
172
- // Handle external URL changes (e.g. from Sidebar)
173
- useEffect(() => {
174
- if (iframeRef.current && !isInternalNavigation.current) {
175
- if (iframeRef.current.src !== url) {
176
- iframeRef.current.src = url;
177
- setIsLoading(true);
178
- mountTimeRef.current = Date.now(); // apply min delay for this load too
179
- }
180
- }
181
- }, [url]);
182
-
183
- // Inject script to prevent "Layout was forced" warning by deferring layout until stylesheets load
184
- useEffect(() => {
185
- const iframe = iframeRef.current;
186
- if (!iframe) return;
187
-
188
- const handleLoad = () => {
189
- try {
190
- const iframeWindow = iframe.contentWindow;
191
- const iframeDoc = iframe.contentDocument || iframeWindow?.document;
192
- if (!iframeDoc || !iframeWindow) return;
193
-
194
- // Inject a script that waits for stylesheets before allowing layout calculations
195
- const script = iframeDoc.createElement('script');
196
- script.textContent = `
197
- (function() {
198
- // Wait for all stylesheets to load
199
- function waitForStylesheets() {
200
- const styleSheets = Array.from(document.styleSheets);
201
- const pendingSheets = styleSheets.filter(function(sheet) {
202
- try {
203
- return sheet.cssRules === null;
204
- } catch (e) {
205
- return false; // Cross-origin stylesheets, assume loaded
206
- }
207
- });
208
-
209
- if (pendingSheets.length === 0) {
210
- // All stylesheets loaded
211
- return;
212
- }
213
-
214
- // Check again after a short delay
215
- setTimeout(waitForStylesheets, 10);
216
- }
217
-
218
- // Start checking after DOM is ready
219
- if (document.readyState === 'complete') {
220
- waitForStylesheets();
221
- } else {
222
- window.addEventListener('load', waitForStylesheets);
223
- }
224
- })();
225
- `;
226
- iframeDoc.head.appendChild(script);
227
- } catch (error) {
228
- // Cross-origin or other errors - ignore (this is expected for some iframes)
229
- logger.debug('Could not inject stylesheet wait script:', { error });
230
- }
231
- };
232
-
233
- // Wait for iframe to load before injecting script
234
- iframe.addEventListener('load', handleLoad);
235
-
236
- // Also try immediately if already loaded
237
- if (iframe.contentDocument?.readyState === 'complete') {
238
- handleLoad();
239
- }
240
-
241
- return () => {
242
- iframe.removeEventListener('load', handleLoad);
243
- };
244
- }, [initialUrl]);
245
-
246
- // Suppress browser warnings that are expected and acceptable
247
- useEffect(() => {
248
- if (process.env.NODE_ENV === 'development') {
249
- const originalWarn = console.warn;
250
- console.warn = (...args: unknown[]) => {
251
- const message = String(args[0] ?? '');
252
- // Suppress the specific sandbox warning
253
- if (
254
- message.includes('allow-scripts') &&
255
- message.includes('allow-same-origin') &&
256
- message.includes('sandbox')
257
- ) {
258
- return;
259
- }
260
- // Suppress "Layout was forced" warning from iframe content
261
- // This is a performance warning that occurs when iframe content calculates layout before stylesheets load
262
- // It's harmless and common in iframe scenarios, especially with React apps
263
- if (
264
- message.includes('Layout was forced') &&
265
- message.includes('before the page was fully loaded')
266
- ) {
267
- return;
268
- }
269
- originalWarn.apply(console, args);
270
- };
271
- return () => {
272
- console.warn = originalWarn;
273
- };
274
- }
275
- }, []);
276
-
277
212
  return (
278
213
  <div
279
214
  style={{ width: '100%', height: '100%', display: 'flex', position: 'relative' }}
@@ -289,7 +224,7 @@ export const ContentView = ({
289
224
  - Reveal is instant (no transition) after deferred double-rAF to avoid blink */}
290
225
  <iframe
291
226
  ref={iframeRef}
292
- src={initialUrl}
227
+ src={iframeUrl}
293
228
  loading="eager"
294
229
  style={{
295
230
  width: '100%',
@@ -1,10 +1,14 @@
1
+ import { LOADING_OVERLAY_DURATION_MS } from '../constants/loading';
2
+
1
3
  export function LoadingOverlay() {
2
4
  return (
3
5
  <div className="absolute inset-x-0 top-0 z-10">
4
6
  <div className="h-1 w-full overflow-hidden bg-muted/30">
5
7
  <div
6
8
  className="h-full w-0 bg-muted-foreground/50"
7
- style={{ animation: 'loading-bar-slide 400ms linear infinite' }}
9
+ style={{
10
+ animation: `loading-bar-slide ${LOADING_OVERLAY_DURATION_MS}ms linear infinite`,
11
+ }}
8
12
  />
9
13
  </div>
10
14
  </div>
@@ -1,10 +1,5 @@
1
1
  import {
2
- createContext,
3
- useContext,
4
- useState,
5
- useCallback,
6
2
  forwardRef,
7
- type ReactNode,
8
3
  type HTMLAttributes,
9
4
  type ButtonHTMLAttributes,
10
5
  type AnchorHTMLAttributes,
@@ -12,47 +7,16 @@ import {
12
7
  import { Slot } from '@radix-ui/react-slot';
13
8
  import { cva, type VariantProps } from 'class-variance-authority';
14
9
  import { cn } from '../../lib/utils';
15
- import { Z_INDEX } from '../../lib/z-index';
16
-
17
- type SidebarContextValue = {
18
- isCollapsed: boolean;
19
- toggle: () => void;
20
- };
21
-
22
- const SidebarContext = createContext<SidebarContextValue | undefined>(undefined);
23
-
24
- const useSidebar = () => {
25
- const context = useContext(SidebarContext);
26
- if (!context) {
27
- throw new Error('useSidebar must be used within a SidebarProvider');
28
- }
29
- return context;
30
- };
31
-
32
- const SidebarProvider = ({ children }: { children: ReactNode }) => {
33
- const [isCollapsed, setIsCollapsed] = useState(false);
34
-
35
- const toggle = useCallback(() => {
36
- setIsCollapsed((prev) => !prev);
37
- }, []);
38
-
39
- return (
40
- <SidebarContext.Provider value={{ isCollapsed, toggle }}>{children}</SidebarContext.Provider>
41
- );
42
- };
43
10
 
44
11
  const Sidebar = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
45
12
  ({ className, ...props }, ref) => {
46
- const { isCollapsed } = useSidebar();
47
-
48
13
  return (
49
14
  <div
50
15
  ref={ref}
51
16
  data-sidebar="sidebar"
52
- data-collapsed={isCollapsed}
53
17
  className={cn(
54
18
  'flex h-full flex-col gap-2 border-r bg-sidebar-background p-2 text-sidebar-foreground transition-all duration-300 ease-in-out overflow-hidden',
55
- isCollapsed ? 'w-0 border-r-0 p-0' : 'w-64',
19
+ 'w-64',
56
20
  className,
57
21
  )}
58
22
  {...props}
@@ -62,83 +26,6 @@ const Sidebar = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
62
26
  );
63
27
  Sidebar.displayName = 'Sidebar';
64
28
 
65
- /** Inline SVG: panel-left-open (expand sidebar) */
66
- const PanelLeftOpenIcon = () => (
67
- <svg
68
- xmlns="http://www.w3.org/2000/svg"
69
- width="24"
70
- height="24"
71
- viewBox="0 0 24 24"
72
- fill="none"
73
- stroke="currentColor"
74
- strokeWidth="2"
75
- strokeLinecap="round"
76
- strokeLinejoin="round"
77
- className="h-5 w-5 transition-transform duration-300"
78
- aria-hidden
79
- >
80
- <rect
81
- width="18"
82
- height="18"
83
- x="3"
84
- y="3"
85
- rx="2"
86
- />
87
- <path d="M9 3v18" />
88
- <path d="m14 9 3 3-3 3" />
89
- </svg>
90
- );
91
-
92
- /** Inline SVG: panel-left-close (collapse sidebar) */
93
- const PanelLeftCloseIcon = () => (
94
- <svg
95
- xmlns="http://www.w3.org/2000/svg"
96
- width="24"
97
- height="24"
98
- viewBox="0 0 24 24"
99
- fill="none"
100
- stroke="currentColor"
101
- strokeWidth="2"
102
- strokeLinecap="round"
103
- strokeLinejoin="round"
104
- className="h-5 w-5 transition-transform duration-300"
105
- aria-hidden
106
- >
107
- <rect
108
- width="18"
109
- height="18"
110
- x="3"
111
- y="3"
112
- rx="2"
113
- />
114
- <path d="M9 3v18" />
115
- <path d="m16 15-3-3 3-3" />
116
- </svg>
117
- );
118
-
119
- const SidebarTrigger = forwardRef<HTMLButtonElement, ButtonHTMLAttributes<HTMLButtonElement>>(
120
- ({ className, ...props }, ref) => {
121
- const { toggle, isCollapsed } = useSidebar();
122
-
123
- return (
124
- <button
125
- ref={ref}
126
- onClick={toggle}
127
- className={cn(
128
- 'relative flex items-center justify-center rounded-md p-2 text-foreground transition-colors hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring shadow-lg backdrop-blur-md cursor-pointer bg-background/95 border border-border',
129
- className,
130
- )}
131
- aria-label={isCollapsed ? 'Expand sidebar' : 'Collapse sidebar'}
132
- style={{ zIndex: Z_INDEX.SIDEBAR_TRIGGER }}
133
- {...props}
134
- >
135
- {isCollapsed ? <PanelLeftOpenIcon /> : <PanelLeftCloseIcon />}
136
- </button>
137
- );
138
- },
139
- );
140
- SidebarTrigger.displayName = 'SidebarTrigger';
141
-
142
29
  const SidebarHeader = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>(
143
30
  ({ className, ...props }, ref) => {
144
31
  return (
@@ -271,19 +158,13 @@ const SidebarMenuButton = forwardRef<
271
158
  isActive?: boolean;
272
159
  }
273
160
  >(({ className, variant, size, asChild = false, isActive, children, ...props }, ref) => {
274
- const { isCollapsed } = useSidebar();
275
161
  const Comp = asChild ? Slot : 'button';
276
162
 
277
163
  return (
278
164
  <Comp
279
165
  ref={ref}
280
166
  data-active={isActive}
281
- data-collapsed={isCollapsed}
282
- className={cn(
283
- sidebarMenuButtonVariants({ variant, size }),
284
- isCollapsed && 'justify-center',
285
- className,
286
- )}
167
+ className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
287
168
  {...props}
288
169
  >
289
170
  {children}
@@ -434,8 +315,6 @@ SidebarMenuSubItem.displayName = 'SidebarMenuSubItem';
434
315
 
435
316
  export {
436
317
  Sidebar,
437
- SidebarProvider,
438
- SidebarTrigger,
439
318
  SidebarHeader,
440
319
  SidebarContent,
441
320
  SidebarFooter,
@@ -452,5 +331,4 @@ export {
452
331
  SidebarMenuSub,
453
332
  SidebarMenuSubButton,
454
333
  SidebarMenuSubItem,
455
- useSidebar,
456
334
  };
@@ -0,0 +1,2 @@
1
+ /** Duration (ms) for loading overlay: fallback timeout and bar animation. */
2
+ export const LOADING_OVERLAY_DURATION_MS = 1000;
@@ -26,6 +26,8 @@ export interface NavigationItem {
26
26
  hiddenOnDesktop?: boolean;
27
27
  /** How to open this link: 'default' (navigate in main area), 'modal', 'drawer', or 'external' (target="_blank"). */
28
28
  openIn?: 'default' | 'modal' | 'drawer' | 'external';
29
+ /** When true, the app uses hash-based routing (e.g. /#/path). If omitted, inferred from url containing /#/. */
30
+ useHashRouter?: boolean;
29
31
  /** Optional drawer position when openIn === 'drawer'. Default is 'right' if omitted. */
30
32
  drawerPosition?: DrawerPosition;
31
33
  /** Sidebar position: 'start' (default) or 'end'. End items are rendered in the sidebar footer. */
@@ -1,18 +1,23 @@
1
1
  import { lazy, Suspense, type LazyExoticComponent, type ComponentType } from 'react';
2
2
  import type { LayoutType, NavigationItem, NavigationGroup } from '../config/types';
3
3
  import { useSettings } from '../settings/SettingsContext';
4
+ import { SonnerProvider } from '../sonner/SonnerContext';
5
+ import { ModalProvider } from '../modal/ModalContext';
6
+ import { DrawerProvider } from '../drawer/DrawerContext';
7
+ import { OverlayShell } from './OverlayShell';
8
+ import { LayoutFallback } from './LayoutFallback';
4
9
 
5
- const DefaultLayout = lazy(() =>
6
- import('./DefaultLayout').then((m) => ({ default: m.DefaultLayout })),
10
+ const SidebarLayout = lazy(() =>
11
+ import('./sidebar/SidebarLayout').then((m) => ({ default: m.SidebarLayout })),
7
12
  );
8
13
  const FullscreenLayout = lazy(() =>
9
- import('./FullscreenLayout').then((m) => ({ default: m.FullscreenLayout })),
14
+ import('./fullscreen/FullscreenLayout').then((m) => ({ default: m.FullscreenLayout })),
10
15
  );
11
16
  const WindowsLayout = lazy(() =>
12
- import('./WindowsLayout').then((m) => ({ default: m.WindowsLayout })),
17
+ import('./windows/WindowsLayout').then((m) => ({ default: m.WindowsLayout })),
13
18
  );
14
19
  const AppBarLayout = lazy(() =>
15
- import('./AppBarLayout').then((m) => ({ default: m.AppBarLayout })),
20
+ import('./appbar/AppBarLayout').then((m) => ({ default: m.AppBarLayout })),
16
21
  );
17
22
 
18
23
  interface AppLayoutProps {
@@ -23,15 +28,6 @@ interface AppLayoutProps {
23
28
  navigation: (NavigationItem | NavigationGroup)[];
24
29
  }
25
30
 
26
- function LayoutFallback() {
27
- return (
28
- <div
29
- className="min-h-screen bg-background"
30
- aria-hidden
31
- />
32
- );
33
- }
34
-
35
31
  /** Renders the layout based on settings.layout (override) or config.layout: 'sidebar' (default), 'fullscreen', or 'windows'. Lazy-loads only the active layout. */
36
32
  export function AppLayout({
37
33
  layout = 'sidebar',
@@ -57,13 +53,20 @@ export function AppLayout({
57
53
  LayoutComponent = AppBarLayout;
58
54
  layoutProps = { title, appIcon, logo, navigation };
59
55
  } else {
60
- LayoutComponent = DefaultLayout;
56
+ LayoutComponent = SidebarLayout;
61
57
  layoutProps = { title, appIcon, logo, navigation };
62
58
  }
63
-
64
59
  return (
65
- <Suspense fallback={<LayoutFallback />}>
66
- <LayoutComponent {...layoutProps} />
67
- </Suspense>
60
+ <ModalProvider>
61
+ <DrawerProvider>
62
+ <SonnerProvider>
63
+ <OverlayShell>
64
+ <Suspense fallback={<LayoutFallback />}>
65
+ <LayoutComponent {...layoutProps} />
66
+ </Suspense>
67
+ </OverlayShell>
68
+ </SonnerProvider>
69
+ </DrawerProvider>
70
+ </ModalProvider>
68
71
  );
69
72
  }
@@ -0,0 +1,8 @@
1
+ export function LayoutFallback() {
2
+ return (
3
+ <div
4
+ className="min-h-screen bg-background"
5
+ aria-hidden
6
+ />
7
+ );
8
+ }