@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,45 @@
1
+ import { forwardRef, type InputHTMLAttributes, type ChangeEvent } from 'react';
2
+ import { cn } from '@/lib/utils';
3
+
4
+ export interface SwitchProps extends Omit<InputHTMLAttributes<HTMLInputElement>, 'type'> {
5
+ checked?: boolean;
6
+ onCheckedChange?: (checked: boolean) => void;
7
+ }
8
+
9
+ const Switch = forwardRef<HTMLInputElement, SwitchProps>(
10
+ ({ className, checked, onCheckedChange, ...props }, ref) => {
11
+ const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
12
+ onCheckedChange?.(e.target.checked);
13
+ };
14
+
15
+ return (
16
+ <label className="inline-flex items-center cursor-pointer">
17
+ <input
18
+ type="checkbox"
19
+ ref={ref}
20
+ checked={checked}
21
+ onChange={handleChange}
22
+ className="sr-only"
23
+ {...props}
24
+ />
25
+ <div
26
+ className={cn(
27
+ 'relative w-11 h-6 rounded-full border border-border transition-colors focus-within:outline-none focus-within:ring-2 focus-within:ring-ring',
28
+ checked ? 'bg-primary' : 'bg-muted',
29
+ className,
30
+ )}
31
+ >
32
+ <div
33
+ className={cn(
34
+ 'absolute top-[1px] left-[1px] h-5 w-5 bg-background border border-border rounded-full transition-transform',
35
+ checked ? 'translate-x-5' : 'translate-x-0',
36
+ )}
37
+ />
38
+ </div>
39
+ </label>
40
+ );
41
+ },
42
+ );
43
+ Switch.displayName = 'Switch';
44
+
45
+ export { Switch };
@@ -0,0 +1,4 @@
1
+ export default {
2
+ settings: '/__settings',
3
+ cookiePreferences: '/__cookie-preferences',
4
+ };
@@ -0,0 +1,468 @@
1
+ import { shellui, type ShellUIMessage, type DialogOptions } from '@shellui/sdk';
2
+ import {
3
+ createContext,
4
+ useContext,
5
+ useCallback,
6
+ useEffect,
7
+ useRef,
8
+ useState,
9
+ type ReactNode,
10
+ } from 'react';
11
+ import { Button } from '@/components/ui/button';
12
+ import { Z_INDEX } from '@/lib/z-index';
13
+
14
+ /** Match exit animation duration in index.css (overlay + content ~0.1s + buffer) */
15
+ const DIALOG_EXIT_ANIMATION_MS = 200;
16
+ import {
17
+ AlertDialog,
18
+ AlertDialogAction,
19
+ AlertDialogCancel,
20
+ AlertDialogContent,
21
+ AlertDialogDescription,
22
+ AlertDialogFooter,
23
+ AlertDialogHeader,
24
+ AlertDialogMedia,
25
+ AlertDialogTitle,
26
+ AlertDialogOverlay,
27
+ AlertDialogPortal,
28
+ } from '@/components/ui/alert-dialog';
29
+ import * as AlertDialogPrimitive from '@radix-ui/react-alert-dialog';
30
+
31
+ /** Trash icon (matches lucide trash-2) */
32
+ const TrashIcon = () => (
33
+ <svg
34
+ xmlns="http://www.w3.org/2000/svg"
35
+ width="20"
36
+ height="20"
37
+ viewBox="0 0 24 24"
38
+ fill="none"
39
+ stroke="currentColor"
40
+ strokeWidth="2"
41
+ strokeLinecap="round"
42
+ strokeLinejoin="round"
43
+ aria-hidden
44
+ >
45
+ <path d="M3 6h18" />
46
+ <path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6" />
47
+ <path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2" />
48
+ <line
49
+ x1="10"
50
+ x2="10"
51
+ y1="11"
52
+ y2="17"
53
+ />
54
+ <line
55
+ x1="14"
56
+ x2="14"
57
+ y1="11"
58
+ y2="17"
59
+ />
60
+ </svg>
61
+ );
62
+
63
+ /** Cookie icon for cookie consent */
64
+ const CookieIcon = ({ className }: { className?: string }) => (
65
+ <svg
66
+ xmlns="http://www.w3.org/2000/svg"
67
+ viewBox="0 0 256 256"
68
+ fill="currentColor"
69
+ className={className}
70
+ >
71
+ <path d="M164.49 163.51a12 12 0 1 1-17 0a12 12 0 0 1 17 0m-81-8a12 12 0 1 0 17 0a12 12 0 0 0-16.98 0Zm9-39a12 12 0 1 0-17 0a12 12 0 0 0 17-.02Zm48-1a12 12 0 1 0 0 17a12 12 0 0 0 0-17M232 128A104 104 0 1 1 128 24a8 8 0 0 1 8 8a40 40 0 0 0 40 40a8 8 0 0 1 8 8a40 40 0 0 0 40 40a8 8 0 0 1 8 8m-16.31 7.39A56.13 56.13 0 0 1 168.5 87.5a56.13 56.13 0 0 1-47.89-47.19a88 88 0 1 0 95.08 95.08" />
72
+ </svg>
73
+ );
74
+
75
+ interface DialogContextValue {
76
+ dialog: (options: DialogOptions) => void;
77
+ }
78
+
79
+ const DialogContext = createContext<DialogContextValue | undefined>(undefined);
80
+
81
+ export function useDialog() {
82
+ const context = useContext(DialogContext);
83
+ if (!context) {
84
+ throw new Error('useDialog must be used within a DialogProvider');
85
+ }
86
+ return context;
87
+ }
88
+
89
+ interface DialogProviderProps {
90
+ children: ReactNode;
91
+ }
92
+
93
+ interface DialogState extends Omit<DialogOptions, 'onOk' | 'onCancel' | 'icon'> {
94
+ id: string;
95
+ mode: DialogOptions['mode'];
96
+ from?: string[];
97
+ iconType?: 'cookie';
98
+ }
99
+
100
+ export const DialogProvider = ({ children }: DialogProviderProps) => {
101
+ const [dialogState, setDialogState] = useState<DialogState | null>(null);
102
+ const [isOpen, setIsOpen] = useState(false);
103
+ const unmountTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
104
+
105
+ const scheduleUnmount = useCallback(() => {
106
+ if (unmountTimeoutRef.current) clearTimeout(unmountTimeoutRef.current);
107
+ unmountTimeoutRef.current = setTimeout(() => {
108
+ unmountTimeoutRef.current = null;
109
+ setDialogState(null);
110
+ }, DIALOG_EXIT_ANIMATION_MS);
111
+ }, []);
112
+
113
+ const handleOpenChange = useCallback(
114
+ (open: boolean) => {
115
+ setIsOpen(open);
116
+ if (!open && dialogState) {
117
+ // If dialog was closed without clicking a button (X button or outside click),
118
+ // trigger cancel callback if it exists
119
+ if (dialogState.from && dialogState.from.length > 0) {
120
+ shellui.sendMessage({
121
+ type: 'SHELLUI_DIALOG_CANCEL',
122
+ payload: { id: dialogState.id },
123
+ to: dialogState.from,
124
+ });
125
+ } else if (dialogState.id) {
126
+ // Dialog opened from root window - trigger callback directly
127
+ shellui.callbackRegistry.triggerCancel(dialogState.id);
128
+ shellui.callbackRegistry.clear(dialogState.id);
129
+ }
130
+ // Unmount after exit animation finishes
131
+ scheduleUnmount();
132
+ }
133
+ },
134
+ [dialogState, scheduleUnmount],
135
+ );
136
+
137
+ const handleOk = useCallback(() => {
138
+ if (dialogState?.from && dialogState.from.length > 0) {
139
+ shellui.sendMessage({
140
+ type: 'SHELLUI_DIALOG_OK',
141
+ payload: { id: dialogState.id },
142
+ to: dialogState.from,
143
+ });
144
+ } else if (dialogState?.id) {
145
+ // Dialog opened from root window - trigger callback directly
146
+ shellui.callbackRegistry.triggerAction(dialogState.id);
147
+ shellui.callbackRegistry.clear(dialogState.id);
148
+ }
149
+ setIsOpen(false);
150
+ scheduleUnmount();
151
+ }, [dialogState, scheduleUnmount]);
152
+
153
+ const handleCancel = useCallback(() => {
154
+ if (dialogState?.from && dialogState.from.length > 0) {
155
+ shellui.sendMessage({
156
+ type: 'SHELLUI_DIALOG_CANCEL',
157
+ payload: { id: dialogState.id },
158
+ to: dialogState.from,
159
+ });
160
+ } else if (dialogState?.id) {
161
+ // Dialog opened from root window - trigger callback directly
162
+ shellui.callbackRegistry.triggerCancel(dialogState.id);
163
+ shellui.callbackRegistry.clear(dialogState.id);
164
+ }
165
+ setIsOpen(false);
166
+ scheduleUnmount();
167
+ }, [dialogState, scheduleUnmount]);
168
+
169
+ const handleSecondary = useCallback(() => {
170
+ if (dialogState?.from && dialogState.from.length > 0) {
171
+ shellui.sendMessage({
172
+ type: 'SHELLUI_DIALOG_SECONDARY',
173
+ payload: { id: dialogState.id },
174
+ to: dialogState.from,
175
+ });
176
+ } else if (dialogState?.id) {
177
+ // Dialog opened from root window - trigger callback directly
178
+ shellui.callbackRegistry.triggerSecondary(dialogState.id);
179
+ shellui.callbackRegistry.clear(dialogState.id);
180
+ }
181
+ setIsOpen(false);
182
+ scheduleUnmount();
183
+ }, [dialogState, scheduleUnmount]);
184
+
185
+ const dialog = useCallback((options: DialogOptions) => {
186
+ // Only show dialog if window is root
187
+ if (typeof window === 'undefined' || window.parent !== window) {
188
+ return;
189
+ }
190
+ if (unmountTimeoutRef.current) {
191
+ clearTimeout(unmountTimeoutRef.current);
192
+ unmountTimeoutRef.current = null;
193
+ }
194
+
195
+ const dialogId = options.id || `dialog-${Date.now()}-${Math.random()}`;
196
+
197
+ // Register callbacks for direct calls (not from iframe)
198
+ if (options.onOk || options.onCancel || options.secondaryButton?.onClick) {
199
+ shellui.callbackRegistry.register(dialogId, {
200
+ action: options.onOk,
201
+ cancel: options.onCancel,
202
+ secondary: options.secondaryButton?.onClick,
203
+ });
204
+ }
205
+
206
+ setDialogState({
207
+ id: dialogId,
208
+ title: options.title,
209
+ description: options.description,
210
+ mode: options.mode || 'ok',
211
+ okLabel: options.okLabel,
212
+ cancelLabel: options.cancelLabel,
213
+ size: options.size,
214
+ position: options.position,
215
+ secondaryButton: options.secondaryButton,
216
+ iconType: options.icon === 'cookie' ? 'cookie' : undefined,
217
+ });
218
+ setIsOpen(true);
219
+ }, []);
220
+
221
+ // Listen for postMessage events from nested iframes
222
+ useEffect(() => {
223
+ // Only listen if window is root
224
+ if (typeof window === 'undefined' || window.parent !== window) {
225
+ return;
226
+ }
227
+
228
+ const cleanupDialog = shellui.addMessageListener('SHELLUI_DIALOG', (data: ShellUIMessage) => {
229
+ if (unmountTimeoutRef.current) {
230
+ clearTimeout(unmountTimeoutRef.current);
231
+ unmountTimeoutRef.current = null;
232
+ }
233
+ const payload = data.payload as DialogOptions & { id: string };
234
+ setDialogState({
235
+ id: payload.id,
236
+ title: payload.title,
237
+ description: payload.description,
238
+ mode: payload.mode || 'ok',
239
+ okLabel: payload.okLabel,
240
+ cancelLabel: payload.cancelLabel,
241
+ size: payload.size,
242
+ position: payload.position,
243
+ secondaryButton: payload.secondaryButton,
244
+ iconType: payload.icon === 'cookie' ? 'cookie' : undefined,
245
+ from: data.from,
246
+ });
247
+ setIsOpen(true);
248
+ });
249
+
250
+ const cleanupDialogUpdate = shellui.addMessageListener(
251
+ 'SHELLUI_DIALOG_UPDATE',
252
+ (data: ShellUIMessage) => {
253
+ if (unmountTimeoutRef.current) {
254
+ clearTimeout(unmountTimeoutRef.current);
255
+ unmountTimeoutRef.current = null;
256
+ }
257
+ const payload = data.payload as DialogOptions & { id: string };
258
+ setDialogState({
259
+ id: payload.id,
260
+ title: payload.title,
261
+ description: payload.description,
262
+ mode: payload.mode || 'ok',
263
+ okLabel: payload.okLabel,
264
+ cancelLabel: payload.cancelLabel,
265
+ size: payload.size,
266
+ position: payload.position,
267
+ secondaryButton: payload.secondaryButton,
268
+ iconType: payload.icon === 'cookie' ? 'cookie' : undefined,
269
+ from: data.from,
270
+ });
271
+ setIsOpen(true);
272
+ },
273
+ );
274
+
275
+ return () => {
276
+ cleanupDialog();
277
+ cleanupDialogUpdate();
278
+ if (unmountTimeoutRef.current) clearTimeout(unmountTimeoutRef.current);
279
+ };
280
+ }, []);
281
+
282
+ const renderButtons = () => {
283
+ if (!dialogState) return null;
284
+
285
+ const { mode, okLabel, cancelLabel, secondaryButton } = dialogState;
286
+
287
+ // Cookie consent layout: secondary button on left, primary buttons on right
288
+ if (secondaryButton && dialogState.position === 'bottom-left') {
289
+ return (
290
+ <>
291
+ <Button
292
+ size="sm"
293
+ variant="ghost"
294
+ onClick={handleSecondary}
295
+ >
296
+ {secondaryButton.label}
297
+ </Button>
298
+ <div className="flex gap-2">
299
+ {mode === 'okCancel' && (
300
+ <Button
301
+ size="sm"
302
+ variant="ghost"
303
+ onClick={handleCancel}
304
+ >
305
+ {cancelLabel || 'Cancel'}
306
+ </Button>
307
+ )}
308
+ <Button
309
+ size="sm"
310
+ onClick={handleOk}
311
+ >
312
+ {okLabel || 'OK'}
313
+ </Button>
314
+ </div>
315
+ </>
316
+ );
317
+ }
318
+
319
+ switch (mode) {
320
+ case 'ok':
321
+ return <AlertDialogAction onClick={handleOk}>{okLabel || 'OK'}</AlertDialogAction>;
322
+
323
+ case 'okCancel':
324
+ return (
325
+ <>
326
+ <AlertDialogCancel
327
+ variant="outline"
328
+ onClick={handleCancel}
329
+ >
330
+ {cancelLabel || 'Cancel'}
331
+ </AlertDialogCancel>
332
+ <AlertDialogAction onClick={handleOk}>{okLabel || 'OK'}</AlertDialogAction>
333
+ </>
334
+ );
335
+
336
+ case 'delete':
337
+ return (
338
+ <>
339
+ <AlertDialogCancel
340
+ variant="outline"
341
+ onClick={handleCancel}
342
+ >
343
+ {cancelLabel || 'Cancel'}
344
+ </AlertDialogCancel>
345
+ <AlertDialogAction
346
+ variant="destructive"
347
+ onClick={handleOk}
348
+ >
349
+ {okLabel || 'Delete'}
350
+ </AlertDialogAction>
351
+ </>
352
+ );
353
+
354
+ case 'confirm':
355
+ return (
356
+ <>
357
+ <AlertDialogCancel
358
+ variant="outline"
359
+ onClick={handleCancel}
360
+ >
361
+ {cancelLabel || 'Cancel'}
362
+ </AlertDialogCancel>
363
+ <AlertDialogAction onClick={handleOk}>{okLabel || 'Confirm'}</AlertDialogAction>
364
+ </>
365
+ );
366
+
367
+ case 'onlyCancel':
368
+ return (
369
+ <AlertDialogCancel
370
+ variant="outline"
371
+ onClick={handleCancel}
372
+ >
373
+ {cancelLabel || 'Cancel'}
374
+ </AlertDialogCancel>
375
+ );
376
+
377
+ default:
378
+ return <AlertDialogAction onClick={handleOk}>{okLabel || 'OK'}</AlertDialogAction>;
379
+ }
380
+ };
381
+
382
+ // Render cookie consent dialog with custom layout
383
+ const renderCookieConsentDialog = () => {
384
+ if (!dialogState || dialogState.position !== 'bottom-left') return null;
385
+
386
+ return (
387
+ <AlertDialog
388
+ open={isOpen}
389
+ onOpenChange={handleOpenChange}
390
+ >
391
+ <AlertDialogPortal>
392
+ <AlertDialogOverlay style={{ zIndex: Z_INDEX.COOKIE_CONSENT_OVERLAY }} />
393
+ <AlertDialogPrimitive.Content
394
+ className="fixed w-[calc(100%-32px)] max-w-[520px] rounded-xl border border-border bg-background text-foreground shadow-lg sm:w-full"
395
+ style={{
396
+ bottom: 16,
397
+ left: 16,
398
+ zIndex: Z_INDEX.COOKIE_CONSENT_CONTENT,
399
+ backgroundColor: 'hsl(var(--background))',
400
+ top: 'auto',
401
+ right: 'auto',
402
+ transform: 'none',
403
+ }}
404
+ data-dialog-content
405
+ data-cookie-consent
406
+ >
407
+ <div className="flex items-start gap-4 p-6 sm:gap-5 sm:p-7">
408
+ {dialogState.iconType === 'cookie' && (
409
+ <div
410
+ className="flex h-10 w-10 shrink-0 items-center justify-center rounded-lg bg-primary/10"
411
+ aria-hidden
412
+ >
413
+ <CookieIcon className="h-5 w-5 text-primary" />
414
+ </div>
415
+ )}
416
+ <div className="flex-1 space-y-2">
417
+ <AlertDialogTitle className="text-base font-semibold leading-tight">
418
+ {dialogState.title}
419
+ </AlertDialogTitle>
420
+ {dialogState.description && (
421
+ <AlertDialogDescription className="text-sm leading-relaxed">
422
+ {dialogState.description}
423
+ </AlertDialogDescription>
424
+ )}
425
+ </div>
426
+ </div>
427
+ <div className="flex items-center justify-between rounded-b-xl border-t border-border bg-muted/50 px-6 py-4 sm:px-7">
428
+ {renderButtons()}
429
+ </div>
430
+ </AlertDialogPrimitive.Content>
431
+ </AlertDialogPortal>
432
+ </AlertDialog>
433
+ );
434
+ };
435
+
436
+ return (
437
+ <DialogContext.Provider value={{ dialog }}>
438
+ {children}
439
+ {dialogState && (
440
+ <>
441
+ {dialogState.position === 'bottom-left' ? (
442
+ renderCookieConsentDialog()
443
+ ) : (
444
+ <AlertDialog
445
+ open={isOpen}
446
+ onOpenChange={handleOpenChange}
447
+ >
448
+ <AlertDialogContent size={dialogState.size ?? 'default'}>
449
+ <AlertDialogHeader>
450
+ {dialogState.mode === 'delete' && (
451
+ <AlertDialogMedia className="bg-destructive/10 text-destructive dark:bg-destructive/20 dark:text-destructive">
452
+ <TrashIcon />
453
+ </AlertDialogMedia>
454
+ )}
455
+ <AlertDialogTitle>{dialogState.title}</AlertDialogTitle>
456
+ {dialogState.description && (
457
+ <AlertDialogDescription>{dialogState.description}</AlertDialogDescription>
458
+ )}
459
+ </AlertDialogHeader>
460
+ <AlertDialogFooter>{renderButtons()}</AlertDialogFooter>
461
+ </AlertDialogContent>
462
+ </AlertDialog>
463
+ )}
464
+ </>
465
+ )}
466
+ </DialogContext.Provider>
467
+ );
468
+ };
@@ -0,0 +1,96 @@
1
+ import { createContext, useState, createElement, type ReactNode } from 'react';
2
+ import { getLogger } from '@shellui/sdk';
3
+ import type { ShellUIConfig } from './types';
4
+
5
+ const logger = getLogger('shellcore');
6
+
7
+ export interface ConfigContextValue {
8
+ config: ShellUIConfig;
9
+ }
10
+
11
+ export const ConfigContext = createContext<ConfigContextValue | null>(null);
12
+
13
+ export interface ConfigProviderProps {
14
+ children: ReactNode;
15
+ }
16
+
17
+ // Track if config has been logged to prevent duplicate logs in dev mode
18
+ // (can happen with React StrictMode or when app renders in multiple contexts)
19
+ let configLogged = false;
20
+
21
+ /**
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.
24
+ */
25
+
26
+ export function ConfigProvider(props: ConfigProviderProps): ReturnType<typeof createElement> {
27
+ const [config] = useState<ShellUIConfig>(() => {
28
+ 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
+ const configValue: unknown = __SHELLUI_CONFIG__;
35
+
36
+ // After Vite replacement, configValue will be a JSON string
37
+ // Example: "{\"title\":\"shellui\"}" -> parse -> {title: "shellui"}
38
+ if (configValue !== undefined && configValue !== null && typeof configValue === 'string') {
39
+ try {
40
+ // Parse the JSON string to get the config object
41
+ const parsedConfig: ShellUIConfig = JSON.parse(configValue);
42
+ if (typeof window !== 'undefined' && parsedConfig.runtime === 'tauri') {
43
+ (window as Window & { __SHELLUI_TAURI__?: boolean }).__SHELLUI_TAURI__ = true;
44
+ }
45
+
46
+ // Log in dev mode to help debug (only once per page load)
47
+ if (process.env.NODE_ENV === 'development' && !configLogged) {
48
+ configLogged = true;
49
+ logger.info('Config loaded from __SHELLUI_CONFIG__', {
50
+ hasNavigation: !!parsedConfig.navigation,
51
+ navigationItems: parsedConfig.navigation?.length || 0,
52
+ });
53
+ }
54
+
55
+ return parsedConfig;
56
+ } catch (parseError) {
57
+ logger.error('Failed to parse config JSON:', { error: parseError });
58
+ logger.error('Config value (first 200 chars):', { value: configValue.substring(0, 200) });
59
+ // Fall through to return empty config
60
+ }
61
+ }
62
+
63
+ // Fallback: try to read from globalThis (for edge cases or if define didn't work)
64
+ const g = globalThis as unknown as { __SHELLUI_CONFIG__?: unknown };
65
+ if (typeof g.__SHELLUI_CONFIG__ !== 'undefined') {
66
+ const fallbackValue = g.__SHELLUI_CONFIG__;
67
+ const parsedConfig: ShellUIConfig =
68
+ typeof fallbackValue === 'string'
69
+ ? JSON.parse(fallbackValue)
70
+ : (fallbackValue as ShellUIConfig);
71
+ if (typeof window !== 'undefined' && parsedConfig.runtime === 'tauri') {
72
+ (window as Window & { __SHELLUI_TAURI__?: boolean }).__SHELLUI_TAURI__ = true;
73
+ }
74
+
75
+ if (process.env.NODE_ENV === 'development') {
76
+ logger.warn('Config loaded from globalThis fallback (define may not have worked)');
77
+ }
78
+
79
+ return parsedConfig;
80
+ }
81
+
82
+ // Return empty config if __SHELLUI_CONFIG__ is undefined (fallback for edge cases)
83
+ logger.warn(
84
+ 'Config not found. Using empty config. Make sure shellui.config.ts is properly loaded during build.',
85
+ );
86
+ return {} as ShellUIConfig;
87
+ } catch (err) {
88
+ logger.error('Failed to load ShellUI config:', { error: err });
89
+ // Don't throw - return empty config instead to prevent app crash
90
+ return {} as ShellUIConfig;
91
+ }
92
+ });
93
+
94
+ const value: ConfigContextValue = { config };
95
+ return createElement(ConfigContext.Provider, { value }, props.children);
96
+ }