@shellui/core 0.0.4

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 (247) hide show
  1. package/README.md +17 -0
  2. package/dist/ContentView-CZG-ro_B.js +146 -0
  3. package/dist/ContentView-CZG-ro_B.js.map +1 -0
  4. package/dist/CookiePreferencesView-MhO9FO-4.js +213 -0
  5. package/dist/CookiePreferencesView-MhO9FO-4.js.map +1 -0
  6. package/dist/DefaultLayout-Dbb3uJED.js +394 -0
  7. package/dist/DefaultLayout-Dbb3uJED.js.map +1 -0
  8. package/dist/FullscreenLayout-1SgPHWw-.js +30 -0
  9. package/dist/FullscreenLayout-1SgPHWw-.js.map +1 -0
  10. package/dist/HomeView-DYU-O_Il.js +21 -0
  11. package/dist/HomeView-DYU-O_Il.js.map +1 -0
  12. package/dist/NotFoundView-CeYjJNg0.js +52 -0
  13. package/dist/NotFoundView-CeYjJNg0.js.map +1 -0
  14. package/dist/OverlayShell-pzbqQW25.js +642 -0
  15. package/dist/OverlayShell-pzbqQW25.js.map +1 -0
  16. package/dist/SettingsView-Bndrta44.js +2207 -0
  17. package/dist/SettingsView-Bndrta44.js.map +1 -0
  18. package/dist/ViewRoute-ChSPabOy.js +32 -0
  19. package/dist/ViewRoute-ChSPabOy.js.map +1 -0
  20. package/dist/WindowsLayout-CXGNPKoY.js +633 -0
  21. package/dist/WindowsLayout-CXGNPKoY.js.map +1 -0
  22. package/dist/app.d.ts +3 -0
  23. package/dist/app.d.ts.map +1 -0
  24. package/dist/components/ContentView.d.ts +10 -0
  25. package/dist/components/ContentView.d.ts.map +1 -0
  26. package/dist/components/HomeView.d.ts +2 -0
  27. package/dist/components/HomeView.d.ts.map +1 -0
  28. package/dist/components/LoadingOverlay.d.ts +2 -0
  29. package/dist/components/LoadingOverlay.d.ts.map +1 -0
  30. package/dist/components/NotFoundView.d.ts +2 -0
  31. package/dist/components/NotFoundView.d.ts.map +1 -0
  32. package/dist/components/RouteErrorBoundary.d.ts +2 -0
  33. package/dist/components/RouteErrorBoundary.d.ts.map +1 -0
  34. package/dist/components/ViewRoute.d.ts +7 -0
  35. package/dist/components/ViewRoute.d.ts.map +1 -0
  36. package/dist/components/ui/alert-dialog.d.ts +32 -0
  37. package/dist/components/ui/alert-dialog.d.ts.map +1 -0
  38. package/dist/components/ui/breadcrumb.d.ts +20 -0
  39. package/dist/components/ui/breadcrumb.d.ts.map +1 -0
  40. package/dist/components/ui/button-group.d.ts +7 -0
  41. package/dist/components/ui/button-group.d.ts.map +1 -0
  42. package/dist/components/ui/button.d.ts +12 -0
  43. package/dist/components/ui/button.d.ts.map +1 -0
  44. package/dist/components/ui/dialog.d.ts +24 -0
  45. package/dist/components/ui/dialog.d.ts.map +1 -0
  46. package/dist/components/ui/drawer.d.ts +38 -0
  47. package/dist/components/ui/drawer.d.ts.map +1 -0
  48. package/dist/components/ui/select.d.ts +5 -0
  49. package/dist/components/ui/select.d.ts.map +1 -0
  50. package/dist/components/ui/sidebar.d.ts +46 -0
  51. package/dist/components/ui/sidebar.d.ts.map +1 -0
  52. package/dist/components/ui/sonner.d.ts +6 -0
  53. package/dist/components/ui/sonner.d.ts.map +1 -0
  54. package/dist/components/ui/switch.d.ts +8 -0
  55. package/dist/components/ui/switch.d.ts.map +1 -0
  56. package/dist/constants/urls.d.ts +6 -0
  57. package/dist/constants/urls.d.ts.map +1 -0
  58. package/dist/constants/urls.js +8 -0
  59. package/dist/constants/urls.js.map +1 -0
  60. package/dist/features/alertDialog/DialogContext.d.ts +12 -0
  61. package/dist/features/alertDialog/DialogContext.d.ts.map +1 -0
  62. package/dist/features/config/ConfigProvider.d.ts +15 -0
  63. package/dist/features/config/ConfigProvider.d.ts.map +1 -0
  64. package/dist/features/config/types.d.ts +177 -0
  65. package/dist/features/config/types.d.ts.map +1 -0
  66. package/dist/features/config/useConfig.d.ts +8 -0
  67. package/dist/features/config/useConfig.d.ts.map +1 -0
  68. package/dist/features/cookieConsent/CookieConsentModal.d.ts +6 -0
  69. package/dist/features/cookieConsent/CookieConsentModal.d.ts.map +1 -0
  70. package/dist/features/cookieConsent/CookiePreferencesView.d.ts +2 -0
  71. package/dist/features/cookieConsent/CookiePreferencesView.d.ts.map +1 -0
  72. package/dist/features/cookieConsent/cookieConsent.d.ts +22 -0
  73. package/dist/features/cookieConsent/cookieConsent.d.ts.map +1 -0
  74. package/dist/features/cookieConsent/useCookieConsent.d.ts +15 -0
  75. package/dist/features/cookieConsent/useCookieConsent.d.ts.map +1 -0
  76. package/dist/features/drawer/DrawerContext.d.ts +24 -0
  77. package/dist/features/drawer/DrawerContext.d.ts.map +1 -0
  78. package/dist/features/layouts/AppLayout.d.ts +12 -0
  79. package/dist/features/layouts/AppLayout.d.ts.map +1 -0
  80. package/dist/features/layouts/DefaultLayout.d.ts +10 -0
  81. package/dist/features/layouts/DefaultLayout.d.ts.map +1 -0
  82. package/dist/features/layouts/FullscreenLayout.d.ts +9 -0
  83. package/dist/features/layouts/FullscreenLayout.d.ts.map +1 -0
  84. package/dist/features/layouts/LayoutProviders.d.ts +9 -0
  85. package/dist/features/layouts/LayoutProviders.d.ts.map +1 -0
  86. package/dist/features/layouts/OverlayShell.d.ts +10 -0
  87. package/dist/features/layouts/OverlayShell.d.ts.map +1 -0
  88. package/dist/features/layouts/WindowsLayout.d.ts +24 -0
  89. package/dist/features/layouts/WindowsLayout.d.ts.map +1 -0
  90. package/dist/features/layouts/utils.d.ts +16 -0
  91. package/dist/features/layouts/utils.d.ts.map +1 -0
  92. package/dist/features/modal/ModalContext.d.ts +20 -0
  93. package/dist/features/modal/ModalContext.d.ts.map +1 -0
  94. package/dist/features/sentry/initSentry.d.ts +14 -0
  95. package/dist/features/sentry/initSentry.d.ts.map +1 -0
  96. package/dist/features/settings/SettingsContext.d.ts +10 -0
  97. package/dist/features/settings/SettingsContext.d.ts.map +1 -0
  98. package/dist/features/settings/SettingsIcons.d.ts +22 -0
  99. package/dist/features/settings/SettingsIcons.d.ts.map +1 -0
  100. package/dist/features/settings/SettingsProvider.d.ts +5 -0
  101. package/dist/features/settings/SettingsProvider.d.ts.map +1 -0
  102. package/dist/features/settings/SettingsRoutes.d.ts +7 -0
  103. package/dist/features/settings/SettingsRoutes.d.ts.map +1 -0
  104. package/dist/features/settings/SettingsView.d.ts +2 -0
  105. package/dist/features/settings/SettingsView.d.ts.map +1 -0
  106. package/dist/features/settings/components/Advanced.d.ts +2 -0
  107. package/dist/features/settings/components/Advanced.d.ts.map +1 -0
  108. package/dist/features/settings/components/Appearance.d.ts +2 -0
  109. package/dist/features/settings/components/Appearance.d.ts.map +1 -0
  110. package/dist/features/settings/components/DataPrivacy.d.ts +2 -0
  111. package/dist/features/settings/components/DataPrivacy.d.ts.map +1 -0
  112. package/dist/features/settings/components/Develop.d.ts +2 -0
  113. package/dist/features/settings/components/Develop.d.ts.map +1 -0
  114. package/dist/features/settings/components/LanguageAndRegion.d.ts +2 -0
  115. package/dist/features/settings/components/LanguageAndRegion.d.ts.map +1 -0
  116. package/dist/features/settings/components/ServiceWorker.d.ts +2 -0
  117. package/dist/features/settings/components/ServiceWorker.d.ts.map +1 -0
  118. package/dist/features/settings/components/UpdateApp.d.ts +2 -0
  119. package/dist/features/settings/components/UpdateApp.d.ts.map +1 -0
  120. package/dist/features/settings/components/develop/DialogTestButtons.d.ts +2 -0
  121. package/dist/features/settings/components/develop/DialogTestButtons.d.ts.map +1 -0
  122. package/dist/features/settings/components/develop/DrawerTestButtons.d.ts +2 -0
  123. package/dist/features/settings/components/develop/DrawerTestButtons.d.ts.map +1 -0
  124. package/dist/features/settings/components/develop/ModalTestButtons.d.ts +2 -0
  125. package/dist/features/settings/components/develop/ModalTestButtons.d.ts.map +1 -0
  126. package/dist/features/settings/components/develop/ToastTestButtons.d.ts +2 -0
  127. package/dist/features/settings/components/develop/ToastTestButtons.d.ts.map +1 -0
  128. package/dist/features/settings/hooks/useSettings.d.ts +2 -0
  129. package/dist/features/settings/hooks/useSettings.d.ts.map +1 -0
  130. package/dist/features/sonner/SonnerContext.d.ts +29 -0
  131. package/dist/features/sonner/SonnerContext.d.ts.map +1 -0
  132. package/dist/features/theme/ThemeProvider.d.ts +11 -0
  133. package/dist/features/theme/ThemeProvider.d.ts.map +1 -0
  134. package/dist/features/theme/themes.d.ts +114 -0
  135. package/dist/features/theme/themes.d.ts.map +1 -0
  136. package/dist/features/theme/useTheme.d.ts +10 -0
  137. package/dist/features/theme/useTheme.d.ts.map +1 -0
  138. package/dist/i18n/I18nProvider.d.ts +9 -0
  139. package/dist/i18n/I18nProvider.d.ts.map +1 -0
  140. package/dist/i18n/config.d.ts +23 -0
  141. package/dist/i18n/config.d.ts.map +1 -0
  142. package/dist/i18n/translations/en/common.json.d.ts +19 -0
  143. package/dist/i18n/translations/en/cookieConsent.json.d.ts +53 -0
  144. package/dist/i18n/translations/en/settings.json.d.ts +358 -0
  145. package/dist/i18n/translations/fr/common.json.d.ts +19 -0
  146. package/dist/i18n/translations/fr/cookieConsent.json.d.ts +53 -0
  147. package/dist/i18n/translations/fr/settings.json.d.ts +358 -0
  148. package/dist/index-lmRk5L6z.js +2160 -0
  149. package/dist/index-lmRk5L6z.js.map +1 -0
  150. package/dist/index.d.ts +7 -0
  151. package/dist/index.d.ts.map +1 -0
  152. package/dist/index.js +12 -0
  153. package/dist/index.js.map +1 -0
  154. package/dist/lib/utils.d.ts +3 -0
  155. package/dist/lib/utils.d.ts.map +1 -0
  156. package/dist/lib/z-index.d.ts +29 -0
  157. package/dist/lib/z-index.d.ts.map +1 -0
  158. package/dist/router/router.d.ts +3 -0
  159. package/dist/router/router.d.ts.map +1 -0
  160. package/dist/router/routes.d.ts +4 -0
  161. package/dist/router/routes.d.ts.map +1 -0
  162. package/dist/sidebar-ClIeZ2zb.js +303 -0
  163. package/dist/sidebar-ClIeZ2zb.js.map +1 -0
  164. package/dist/style.css +1 -0
  165. package/dist/switch-8SzUJz7Q.js +44 -0
  166. package/dist/switch-8SzUJz7Q.js.map +1 -0
  167. package/dist/types.js +2 -0
  168. package/dist/types.js.map +1 -0
  169. package/package.json +93 -0
  170. package/postcss.config.js +6 -0
  171. package/src/app.tsx +119 -0
  172. package/src/components/ContentView.tsx +258 -0
  173. package/src/components/HomeView.tsx +19 -0
  174. package/src/components/LoadingOverlay.tsx +12 -0
  175. package/src/components/NotFoundView.tsx +84 -0
  176. package/src/components/RouteErrorBoundary.tsx +95 -0
  177. package/src/components/ViewRoute.tsx +47 -0
  178. package/src/components/ui/alert-dialog.tsx +181 -0
  179. package/src/components/ui/breadcrumb.tsx +155 -0
  180. package/src/components/ui/button-group.tsx +52 -0
  181. package/src/components/ui/button.tsx +51 -0
  182. package/src/components/ui/dialog.tsx +160 -0
  183. package/src/components/ui/drawer.tsx +200 -0
  184. package/src/components/ui/select.tsx +24 -0
  185. package/src/components/ui/sidebar.tsx +406 -0
  186. package/src/components/ui/sonner.tsx +36 -0
  187. package/src/components/ui/switch.tsx +45 -0
  188. package/src/constants/urls.ts +4 -0
  189. package/src/features/alertDialog/DialogContext.tsx +468 -0
  190. package/src/features/config/ConfigProvider.ts +96 -0
  191. package/src/features/config/types.ts +195 -0
  192. package/src/features/config/useConfig.ts +15 -0
  193. package/src/features/cookieConsent/CookieConsentModal.tsx +122 -0
  194. package/src/features/cookieConsent/CookiePreferencesView.tsx +328 -0
  195. package/src/features/cookieConsent/cookieConsent.ts +84 -0
  196. package/src/features/cookieConsent/useCookieConsent.ts +39 -0
  197. package/src/features/drawer/DrawerContext.tsx +116 -0
  198. package/src/features/layouts/AppLayout.tsx +63 -0
  199. package/src/features/layouts/DefaultLayout.tsx +625 -0
  200. package/src/features/layouts/FullscreenLayout.tsx +55 -0
  201. package/src/features/layouts/LayoutProviders.tsx +20 -0
  202. package/src/features/layouts/OverlayShell.tsx +171 -0
  203. package/src/features/layouts/WindowsLayout.tsx +860 -0
  204. package/src/features/layouts/utils.ts +99 -0
  205. package/src/features/modal/ModalContext.tsx +112 -0
  206. package/src/features/sentry/initSentry.ts +72 -0
  207. package/src/features/settings/SettingsContext.tsx +19 -0
  208. package/src/features/settings/SettingsIcons.tsx +452 -0
  209. package/src/features/settings/SettingsProvider.tsx +341 -0
  210. package/src/features/settings/SettingsRoutes.tsx +66 -0
  211. package/src/features/settings/SettingsView.tsx +327 -0
  212. package/src/features/settings/components/Advanced.tsx +128 -0
  213. package/src/features/settings/components/Appearance.tsx +306 -0
  214. package/src/features/settings/components/DataPrivacy.tsx +142 -0
  215. package/src/features/settings/components/Develop.tsx +174 -0
  216. package/src/features/settings/components/LanguageAndRegion.tsx +329 -0
  217. package/src/features/settings/components/ServiceWorker.tsx +363 -0
  218. package/src/features/settings/components/UpdateApp.tsx +206 -0
  219. package/src/features/settings/components/develop/DialogTestButtons.tsx +137 -0
  220. package/src/features/settings/components/develop/DrawerTestButtons.tsx +67 -0
  221. package/src/features/settings/components/develop/ModalTestButtons.tsx +30 -0
  222. package/src/features/settings/components/develop/ToastTestButtons.tsx +179 -0
  223. package/src/features/settings/hooks/useSettings.tsx +10 -0
  224. package/src/features/sonner/SonnerContext.tsx +286 -0
  225. package/src/features/theme/ThemeProvider.tsx +16 -0
  226. package/src/features/theme/themes.ts +561 -0
  227. package/src/features/theme/useTheme.tsx +71 -0
  228. package/src/i18n/I18nProvider.tsx +32 -0
  229. package/src/i18n/config.ts +107 -0
  230. package/src/i18n/translations/en/common.json +16 -0
  231. package/src/i18n/translations/en/cookieConsent.json +50 -0
  232. package/src/i18n/translations/en/settings.json +355 -0
  233. package/src/i18n/translations/fr/common.json +16 -0
  234. package/src/i18n/translations/fr/cookieConsent.json +50 -0
  235. package/src/i18n/translations/fr/settings.json +355 -0
  236. package/src/index.css +412 -0
  237. package/src/index.html +100 -0
  238. package/src/index.ts +31 -0
  239. package/src/lib/utils.ts +6 -0
  240. package/src/lib/z-index.ts +29 -0
  241. package/src/main.tsx +26 -0
  242. package/src/router/router.tsx +8 -0
  243. package/src/router/routes.tsx +115 -0
  244. package/src/service-worker/register.ts +1199 -0
  245. package/src/service-worker/sw-dev.ts +87 -0
  246. package/src/service-worker/sw.ts +105 -0
  247. package/tailwind.config.js +60 -0
@@ -0,0 +1,84 @@
1
+ import type { ShellUIConfig } from '../config/types';
2
+
3
+ const STORAGE_KEY = 'shellui:settings';
4
+
5
+ function getConfig(): ShellUIConfig | undefined {
6
+ 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;
11
+ }
12
+
13
+ function getStoredCookieConsent(): {
14
+ acceptedHosts: string[];
15
+ consentedCookieHosts: string[];
16
+ } {
17
+ if (typeof window === 'undefined') {
18
+ return { acceptedHosts: [], consentedCookieHosts: [] };
19
+ }
20
+ try {
21
+ const stored = localStorage.getItem(STORAGE_KEY);
22
+ if (!stored) return { acceptedHosts: [], consentedCookieHosts: [] };
23
+ const parsed = JSON.parse(stored) as {
24
+ cookieConsent?: { acceptedHosts?: string[]; consentedCookieHosts?: string[] };
25
+ };
26
+ return {
27
+ acceptedHosts: Array.isArray(parsed?.cookieConsent?.acceptedHosts)
28
+ ? parsed.cookieConsent.acceptedHosts
29
+ : [],
30
+ consentedCookieHosts: Array.isArray(parsed?.cookieConsent?.consentedCookieHosts)
31
+ ? parsed.cookieConsent.consentedCookieHosts
32
+ : [],
33
+ };
34
+ } catch {
35
+ return { acceptedHosts: [], consentedCookieHosts: [] };
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Returns whether the given cookie host is accepted by the user.
41
+ * Use this outside React (e.g. in initSentry) to gate features by cookie consent.
42
+ * - If there is no cookieConsent config, returns true (no consent required).
43
+ * - If the host is not in the config list, returns true (unknown cookie, don't block).
44
+ * - Otherwise returns whether the host is in settings.cookieConsent.acceptedHosts.
45
+ */
46
+ export function getCookieConsentAccepted(host: string): boolean {
47
+ const config = getConfig();
48
+ if (!config?.cookieConsent?.cookies?.length) return true;
49
+ const knownHosts = new Set(config.cookieConsent.cookies.map((c) => c.host));
50
+ if (!knownHosts.has(host)) return true;
51
+ const { acceptedHosts } = getStoredCookieConsent();
52
+ return acceptedHosts.includes(host);
53
+ }
54
+
55
+ /**
56
+ * Returns whether the app should ask for consent again because new cookies were added to config.
57
+ * True when current config has at least one host that is not in consentedCookieHosts (the list
58
+ * stored when the user last gave consent). When re-prompting, pre-fill with acceptedHosts so
59
+ * existing approvals are kept.
60
+ */
61
+ export function getCookieConsentNeedsRenewal(): boolean {
62
+ const config = getConfig();
63
+ if (!config?.cookieConsent?.cookies?.length) return false;
64
+ const { consentedCookieHosts } = getStoredCookieConsent();
65
+ if (consentedCookieHosts.length === 0) return true; // Never consented
66
+ const currentHosts = new Set(config.cookieConsent.cookies.map((c) => c.host));
67
+ for (const host of currentHosts) {
68
+ if (!consentedCookieHosts.includes(host)) return true;
69
+ }
70
+ return false;
71
+ }
72
+
73
+ /**
74
+ * Returns the list of cookie hosts that are in the current config but were not in the list
75
+ * when the user last gave consent. Use to show "new" badges or to know which cookies need
76
+ * fresh approval while keeping existing acceptedHosts as pre-checked.
77
+ */
78
+ export function getCookieConsentNewHosts(): string[] {
79
+ const config = getConfig();
80
+ if (!config?.cookieConsent?.cookies?.length) return [];
81
+ const { consentedCookieHosts } = getStoredCookieConsent();
82
+ const consentedSet = new Set(consentedCookieHosts);
83
+ return config.cookieConsent.cookies.map((c) => c.host).filter((host) => !consentedSet.has(host));
84
+ }
@@ -0,0 +1,39 @@
1
+ import { useMemo } from 'react';
2
+ import { useConfig } from '../config/useConfig';
3
+ import { useSettings } from '../settings/hooks/useSettings';
4
+ import { getCookieConsentNeedsRenewal } from './cookieConsent';
5
+
6
+ /**
7
+ * Returns whether the given cookie host is accepted (feature may run) and whether
8
+ * consent should be re-collected. Use to gate features in React components.
9
+ *
10
+ * - isAccepted: true if cookie consent is not configured, or the host is not in the config list,
11
+ * or the user has accepted this host.
12
+ * - needsConsent: true if cookie consent is configured and the user has not yet consented,
13
+ * or new cookies were added since last consent (renewal); when showing the UI again, pre-fill
14
+ * with settings.cookieConsent.acceptedHosts to keep existing approvals.
15
+ */
16
+ export function useCookieConsent(host: string): { isAccepted: boolean; needsConsent: boolean } {
17
+ const { config } = useConfig();
18
+ const { settings } = useSettings();
19
+
20
+ return useMemo(() => {
21
+ const cookieConsent = config?.cookieConsent;
22
+ const list = cookieConsent?.cookies ?? [];
23
+ const acceptedHosts = settings?.cookieConsent?.acceptedHosts ?? [];
24
+
25
+ if (!list.length) {
26
+ return { isAccepted: true, needsConsent: false };
27
+ }
28
+
29
+ const knownHosts = new Set(list.map((c) => c.host));
30
+ if (!knownHosts.has(host)) {
31
+ return { isAccepted: true, needsConsent: false };
32
+ }
33
+
34
+ const isAccepted = acceptedHosts.includes(host);
35
+ const needsConsent = acceptedHosts.length === 0 || getCookieConsentNeedsRenewal();
36
+
37
+ return { isAccepted, needsConsent };
38
+ }, [config?.cookieConsent, settings?.cookieConsent?.acceptedHosts, host]);
39
+ }
@@ -0,0 +1,116 @@
1
+ import { shellui, type ShellUIMessage } from '@shellui/sdk';
2
+ import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from 'react';
3
+ import type { DrawerDirection } from '@/components/ui/drawer';
4
+ import { useModal } from '../modal/ModalContext';
5
+
6
+ /**
7
+ * Validates and normalizes a URL for the drawer iframe.
8
+ * Allows same-origin, localhost, and external http(s) URLs (e.g. from nav config).
9
+ */
10
+ const validateAndNormalizeUrl = (url: string | undefined | null): string | null => {
11
+ if (!url || typeof url !== 'string') {
12
+ return null;
13
+ }
14
+
15
+ try {
16
+ if (url.startsWith('http://') || url.startsWith('https://')) {
17
+ new URL(url); // validate
18
+ return url;
19
+ }
20
+
21
+ if (url.startsWith('/') || url.startsWith('./') || !url.startsWith('//')) {
22
+ const currentOrigin = typeof window !== 'undefined' ? window.location.origin : '';
23
+ const normalizedPath = url.startsWith('/') ? url : `/${url}`;
24
+ return `${currentOrigin}${normalizedPath}`;
25
+ }
26
+
27
+ return null;
28
+ } catch {
29
+ return null;
30
+ }
31
+ };
32
+
33
+ export const DEFAULT_DRAWER_POSITION: DrawerDirection = 'right';
34
+
35
+ interface OpenDrawerOptions {
36
+ url?: string;
37
+ position?: DrawerDirection;
38
+ /** CSS length: height for top/bottom (e.g. "80vh", "400px"), width for left/right (e.g. "50vw", "320px") */
39
+ size?: string;
40
+ }
41
+
42
+ interface DrawerContextValue {
43
+ isOpen: boolean;
44
+ drawerUrl: string | null;
45
+ position: DrawerDirection;
46
+ size: string | null;
47
+ openDrawer: (options?: OpenDrawerOptions) => void;
48
+ closeDrawer: () => void;
49
+ }
50
+
51
+ const DrawerContext = createContext<DrawerContextValue | undefined>(undefined);
52
+
53
+ export const useDrawer = () => {
54
+ const context = useContext(DrawerContext);
55
+ if (!context) {
56
+ throw new Error('useDrawer must be used within a DrawerProvider');
57
+ }
58
+ return context;
59
+ };
60
+
61
+ interface DrawerProviderProps {
62
+ children: ReactNode;
63
+ }
64
+
65
+ export const DrawerProvider = ({ children }: DrawerProviderProps) => {
66
+ const { closeModal } = useModal();
67
+ const [isOpen, setIsOpen] = useState(false);
68
+ const [drawerUrl, setDrawerUrl] = useState<string | null>(null);
69
+ const [position, setPosition] = useState<DrawerDirection>(DEFAULT_DRAWER_POSITION);
70
+ const [size, setSize] = useState<string | null>(null);
71
+
72
+ const openDrawer = useCallback(
73
+ (options?: OpenDrawerOptions) => {
74
+ closeModal();
75
+ const url = options?.url;
76
+ const validatedUrl = url ? validateAndNormalizeUrl(url) : null;
77
+ setDrawerUrl(validatedUrl);
78
+ setPosition(options?.position ?? DEFAULT_DRAWER_POSITION);
79
+ setSize(options?.size ?? null);
80
+ setIsOpen(true);
81
+ },
82
+ [closeModal],
83
+ );
84
+
85
+ const closeDrawer = useCallback(() => {
86
+ setIsOpen(false);
87
+ // Do not reset drawerUrl/position here — Vaul's close animation uses the current
88
+ // direction. Resetting position (e.g. to 'right') mid-animation would make
89
+ // non-right drawers jump. State is set on next openDrawer().
90
+ }, []);
91
+
92
+ useEffect(() => {
93
+ const cleanupOpen = shellui.addMessageListener(
94
+ 'SHELLUI_OPEN_DRAWER',
95
+ (data: ShellUIMessage) => {
96
+ const payload = data.payload as { url?: string; position?: DrawerDirection; size?: string };
97
+ openDrawer({ url: payload.url, position: payload.position, size: payload.size });
98
+ },
99
+ );
100
+
101
+ const cleanupClose = shellui.addMessageListener('SHELLUI_CLOSE_DRAWER', () => {
102
+ closeDrawer();
103
+ });
104
+
105
+ return () => {
106
+ cleanupOpen();
107
+ cleanupClose();
108
+ };
109
+ }, [openDrawer, closeDrawer]);
110
+
111
+ return (
112
+ <DrawerContext.Provider value={{ isOpen, drawerUrl, position, size, openDrawer, closeDrawer }}>
113
+ {children}
114
+ </DrawerContext.Provider>
115
+ );
116
+ };
@@ -0,0 +1,63 @@
1
+ import { lazy, Suspense, type LazyExoticComponent, type ComponentType } from 'react';
2
+ import type { LayoutType, NavigationItem, NavigationGroup } from '../config/types';
3
+ import { useSettings } from '../settings/SettingsContext';
4
+
5
+ const DefaultLayout = lazy(() =>
6
+ import('./DefaultLayout').then((m) => ({ default: m.DefaultLayout })),
7
+ );
8
+ const FullscreenLayout = lazy(() =>
9
+ import('./FullscreenLayout').then((m) => ({ default: m.FullscreenLayout })),
10
+ );
11
+ const WindowsLayout = lazy(() =>
12
+ import('./WindowsLayout').then((m) => ({ default: m.WindowsLayout })),
13
+ );
14
+
15
+ interface AppLayoutProps {
16
+ layout?: LayoutType;
17
+ title?: string;
18
+ appIcon?: string;
19
+ logo?: string;
20
+ navigation: (NavigationItem | NavigationGroup)[];
21
+ }
22
+
23
+ function LayoutFallback() {
24
+ return (
25
+ <div
26
+ className="min-h-screen bg-background"
27
+ aria-hidden
28
+ />
29
+ );
30
+ }
31
+
32
+ /** Renders the layout based on settings.layout (override) or config.layout: 'sidebar' (default), 'fullscreen', or 'windows'. Lazy-loads only the active layout. */
33
+ export function AppLayout({
34
+ layout = 'sidebar',
35
+ title,
36
+ appIcon,
37
+ logo,
38
+ navigation,
39
+ }: AppLayoutProps) {
40
+ const { settings } = useSettings();
41
+ const effectiveLayout: LayoutType = settings.layout ?? layout;
42
+
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
+ let LayoutComponent: LazyExoticComponent<ComponentType<any>>;
45
+ let layoutProps: Record<string, unknown>;
46
+
47
+ if (effectiveLayout === 'fullscreen') {
48
+ LayoutComponent = FullscreenLayout;
49
+ layoutProps = { title, navigation };
50
+ } else if (effectiveLayout === 'windows') {
51
+ LayoutComponent = WindowsLayout;
52
+ layoutProps = { title, appIcon, logo, navigation };
53
+ } else {
54
+ LayoutComponent = DefaultLayout;
55
+ layoutProps = { title, appIcon, logo, navigation };
56
+ }
57
+
58
+ return (
59
+ <Suspense fallback={<LayoutFallback />}>
60
+ <LayoutComponent {...layoutProps} />
61
+ </Suspense>
62
+ );
63
+ }