@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,329 @@
1
+ import { useTranslation } from 'react-i18next';
2
+ import { useSettings } from '../hooks/useSettings';
3
+ import { useConfig } from '@/features/config/useConfig';
4
+ import { getSupportedLanguages } from '@/i18n/config';
5
+ import { Button } from '@/components/ui/button';
6
+ import { ButtonGroup } from '@/components/ui/button-group';
7
+ import { Select } from '@/components/ui/select';
8
+ import { cn } from '@/lib/utils';
9
+ import { useState, useEffect } from 'react';
10
+
11
+ const GlobeIcon = () => (
12
+ <svg
13
+ xmlns="http://www.w3.org/2000/svg"
14
+ width="16"
15
+ height="16"
16
+ viewBox="0 0 24 24"
17
+ fill="none"
18
+ stroke="currentColor"
19
+ strokeWidth="2"
20
+ strokeLinecap="round"
21
+ strokeLinejoin="round"
22
+ >
23
+ <circle
24
+ cx="12"
25
+ cy="12"
26
+ r="10"
27
+ />
28
+ <line
29
+ x1="2"
30
+ x2="22"
31
+ y1="12"
32
+ y2="12"
33
+ />
34
+ <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
35
+ </svg>
36
+ );
37
+
38
+ const ClockIcon = () => (
39
+ <svg
40
+ xmlns="http://www.w3.org/2000/svg"
41
+ width="16"
42
+ height="16"
43
+ viewBox="0 0 24 24"
44
+ fill="none"
45
+ stroke="currentColor"
46
+ strokeWidth="2"
47
+ strokeLinecap="round"
48
+ strokeLinejoin="round"
49
+ >
50
+ <circle
51
+ cx="12"
52
+ cy="12"
53
+ r="10"
54
+ />
55
+ <polyline points="12 6 12 12 16 14" />
56
+ </svg>
57
+ );
58
+
59
+ // Timezones organized by region
60
+ const TIMEZONE_GROUPS = [
61
+ {
62
+ label: 'UTC',
63
+ timezones: [{ value: 'UTC', label: 'UTC (Coordinated Universal Time)' }],
64
+ },
65
+ {
66
+ label: 'North America',
67
+ timezones: [
68
+ { value: 'America/New_York', label: 'Eastern Time (US & Canada)' },
69
+ { value: 'America/Chicago', label: 'Central Time (US & Canada)' },
70
+ { value: 'America/Denver', label: 'Mountain Time (US & Canada)' },
71
+ { value: 'America/Los_Angeles', label: 'Pacific Time (US & Canada)' },
72
+ { value: 'America/Toronto', label: 'Toronto' },
73
+ { value: 'America/Vancouver', label: 'Vancouver' },
74
+ { value: 'America/Mexico_City', label: 'Mexico City' },
75
+ ],
76
+ },
77
+ {
78
+ label: 'South America',
79
+ timezones: [
80
+ { value: 'America/Sao_Paulo', label: 'São Paulo' },
81
+ { value: 'America/Buenos_Aires', label: 'Buenos Aires' },
82
+ ],
83
+ },
84
+ {
85
+ label: 'Europe',
86
+ timezones: [
87
+ { value: 'Europe/London', label: 'London' },
88
+ { value: 'Europe/Paris', label: 'Paris' },
89
+ { value: 'Europe/Berlin', label: 'Berlin' },
90
+ { value: 'Europe/Rome', label: 'Rome' },
91
+ { value: 'Europe/Madrid', label: 'Madrid' },
92
+ { value: 'Europe/Amsterdam', label: 'Amsterdam' },
93
+ { value: 'Europe/Stockholm', label: 'Stockholm' },
94
+ { value: 'Europe/Zurich', label: 'Zurich' },
95
+ ],
96
+ },
97
+ {
98
+ label: 'Asia',
99
+ timezones: [
100
+ { value: 'Asia/Tokyo', label: 'Tokyo' },
101
+ { value: 'Asia/Shanghai', label: 'Shanghai' },
102
+ { value: 'Asia/Hong_Kong', label: 'Hong Kong' },
103
+ { value: 'Asia/Singapore', label: 'Singapore' },
104
+ { value: 'Asia/Dubai', label: 'Dubai' },
105
+ { value: 'Asia/Kolkata', label: 'Mumbai, New Delhi' },
106
+ { value: 'Asia/Bangkok', label: 'Bangkok' },
107
+ ],
108
+ },
109
+ {
110
+ label: 'Australia & Pacific',
111
+ timezones: [
112
+ { value: 'Australia/Sydney', label: 'Sydney' },
113
+ { value: 'Australia/Melbourne', label: 'Melbourne' },
114
+ { value: 'Pacific/Auckland', label: 'Auckland' },
115
+ ],
116
+ },
117
+ ];
118
+
119
+ // Format date based on timezone and language
120
+ const formatDate = (date: Date, timezone: string, lang: string): string => {
121
+ try {
122
+ return new Intl.DateTimeFormat(lang, {
123
+ timeZone: timezone,
124
+ weekday: 'long',
125
+ year: 'numeric',
126
+ month: 'long',
127
+ day: 'numeric',
128
+ }).format(date);
129
+ } catch {
130
+ return date.toLocaleDateString(lang);
131
+ }
132
+ };
133
+
134
+ // Format time based on timezone and language
135
+ const formatTime = (date: Date, timezone: string, lang: string): string => {
136
+ try {
137
+ return new Intl.DateTimeFormat(lang, {
138
+ timeZone: timezone,
139
+ hour: '2-digit',
140
+ minute: '2-digit',
141
+ second: '2-digit',
142
+ hour12: false,
143
+ }).format(date);
144
+ } catch {
145
+ return date.toLocaleTimeString(lang);
146
+ }
147
+ };
148
+
149
+ // Get browser's current timezone
150
+ const getBrowserTimezone = (): string => {
151
+ if (typeof Intl !== 'undefined') {
152
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
153
+ }
154
+ return 'UTC';
155
+ };
156
+
157
+ // Get human-readable timezone name
158
+ const getTimezoneDisplayName = (timezone: string, lang = 'en'): string => {
159
+ try {
160
+ // Try to get a friendly name from Intl
161
+ const formatter = new Intl.DateTimeFormat(lang, {
162
+ timeZone: timezone,
163
+ timeZoneName: 'long',
164
+ });
165
+ const parts = formatter.formatToParts(new Date());
166
+ const timeZoneName = parts.find((part) => part.type === 'timeZoneName')?.value;
167
+
168
+ if (timeZoneName) {
169
+ return timeZoneName;
170
+ }
171
+
172
+ // Fallback: try to get city name from timezone string
173
+ const cityName = timezone.split('/').pop()?.replace(/_/g, ' ') || timezone;
174
+ return cityName;
175
+ } catch {
176
+ // Final fallback: use the timezone code
177
+ return timezone;
178
+ }
179
+ };
180
+
181
+ export const LanguageAndRegion = () => {
182
+ const { t } = useTranslation('settings');
183
+ const { settings, updateSetting } = useSettings();
184
+ const { config } = useConfig();
185
+ const currentLanguage = settings.language?.code || 'en';
186
+ const browserTimezone = getBrowserTimezone();
187
+ const currentTimezone = settings.region?.timezone || browserTimezone;
188
+ const isUsingBrowserTimezone = currentTimezone === browserTimezone;
189
+
190
+ // Get supported languages based on config
191
+ const supportedLanguages = getSupportedLanguages(config?.language);
192
+
193
+ const handleResetRegion = () => {
194
+ updateSetting('region', { timezone: browserTimezone });
195
+ };
196
+
197
+ // State for current date/time
198
+ const [currentDateTime, setCurrentDateTime] = useState<{ date: string; time: string }>(() => {
199
+ const now = new Date();
200
+ return {
201
+ date: formatDate(now, currentTimezone, currentLanguage),
202
+ time: formatTime(now, currentTimezone, currentLanguage),
203
+ };
204
+ });
205
+
206
+ // Update date/time every second and when timezone/language changes
207
+ useEffect(() => {
208
+ const updateDateTime = () => {
209
+ const now = new Date();
210
+ setCurrentDateTime({
211
+ date: formatDate(now, currentTimezone, currentLanguage),
212
+ time: formatTime(now, currentTimezone, currentLanguage),
213
+ });
214
+ };
215
+
216
+ // Update immediately when timezone or language changes
217
+ updateDateTime();
218
+
219
+ // Then update every second
220
+ const interval = setInterval(updateDateTime, 1000);
221
+
222
+ return () => clearInterval(interval);
223
+ }, [currentTimezone, currentLanguage]);
224
+
225
+ return (
226
+ <div className="space-y-6">
227
+ <div className="space-y-2">
228
+ <label
229
+ className="text-sm font-medium leading-none"
230
+ style={{ fontFamily: 'var(--heading-font-family, inherit)' }}
231
+ >
232
+ {t('languageAndRegion.language')}
233
+ </label>
234
+ <div className="mt-2">
235
+ <ButtonGroup>
236
+ {supportedLanguages.map((lang) => {
237
+ const isSelected = currentLanguage === lang.code;
238
+ return (
239
+ <Button
240
+ key={lang.code}
241
+ variant={isSelected ? 'default' : 'outline'}
242
+ onClick={() => {
243
+ updateSetting('language', { code: lang.code });
244
+ }}
245
+ className={cn(
246
+ 'h-10 px-4 transition-all flex items-center gap-2',
247
+ isSelected && ['shadow-md', 'font-semibold'],
248
+ !isSelected && ['bg-background hover:bg-accent/50', 'text-muted-foreground'],
249
+ )}
250
+ aria-label={lang.nativeName}
251
+ title={lang.nativeName}
252
+ >
253
+ <GlobeIcon />
254
+ <span className="text-sm font-medium">{lang.nativeName}</span>
255
+ </Button>
256
+ );
257
+ })}
258
+ </ButtonGroup>
259
+ </div>
260
+ </div>
261
+
262
+ <div className="space-y-2">
263
+ <div className="flex items-center justify-between">
264
+ <label
265
+ className="text-sm font-medium leading-none"
266
+ style={{ fontFamily: 'var(--heading-font-family, inherit)' }}
267
+ >
268
+ {t('languageAndRegion.region')}
269
+ </label>
270
+ {!isUsingBrowserTimezone && (
271
+ <Button
272
+ variant="ghost"
273
+ size="sm"
274
+ onClick={handleResetRegion}
275
+ className="h-8 text-xs"
276
+ >
277
+ {t('languageAndRegion.resetToBrowser')}
278
+ </Button>
279
+ )}
280
+ </div>
281
+ <div className="mt-2 space-y-3">
282
+ <Select
283
+ value={currentTimezone}
284
+ onChange={(e) => {
285
+ updateSetting('region', { timezone: e.target.value });
286
+ }}
287
+ className="w-full"
288
+ >
289
+ {TIMEZONE_GROUPS.map((group) => (
290
+ <optgroup
291
+ key={group.label}
292
+ label={group.label}
293
+ >
294
+ {group.timezones.map((tz) => {
295
+ const isBrowserTimezone = tz.value === browserTimezone;
296
+ return (
297
+ <option
298
+ key={tz.value}
299
+ value={tz.value}
300
+ >
301
+ {tz.label}
302
+ {isBrowserTimezone ? ` (${t('languageAndRegion.defaultBrowser')})` : ''}
303
+ </option>
304
+ );
305
+ })}
306
+ </optgroup>
307
+ ))}
308
+ </Select>
309
+ <div className="flex items-center gap-2 px-3 py-2 rounded-md bg-muted/60 border border-border/50">
310
+ <ClockIcon />
311
+ <div className="flex items-baseline gap-2">
312
+ <span className="text-lg font-semibold tabular-nums">{currentDateTime.time}</span>
313
+ <span className="text-xs text-muted-foreground">{currentDateTime.date}</span>
314
+ </div>
315
+ </div>
316
+ <div className="text-xs text-muted-foreground flex items-center gap-1">
317
+ <span>{t('languageAndRegion.timezoneLabel')}:</span>
318
+ <span className="font-medium">
319
+ {getTimezoneDisplayName(currentTimezone, currentLanguage)}
320
+ </span>
321
+ {isUsingBrowserTimezone && (
322
+ <span className="ml-1">({t('languageAndRegion.defaultBrowser')})</span>
323
+ )}
324
+ </div>
325
+ </div>
326
+ </div>
327
+ </div>
328
+ );
329
+ };
@@ -0,0 +1,363 @@
1
+ import { useTranslation } from 'react-i18next';
2
+ import { useSettings } from '../hooks/useSettings';
3
+ import { Button } from '@/components/ui/button';
4
+ import { Switch } from '@/components/ui/switch';
5
+ import {
6
+ isServiceWorkerRegistered,
7
+ updateServiceWorker,
8
+ getServiceWorkerStatus,
9
+ addStatusListener,
10
+ serviceWorkerFileExists,
11
+ } from '@/service-worker/register';
12
+ import { shellui } from '@shellui/sdk';
13
+ import { useState, useEffect } from 'react';
14
+
15
+ export const ServiceWorker = () => {
16
+ const { t } = useTranslation('settings');
17
+ const { settings, updateSetting } = useSettings();
18
+ const [isRegistered, setIsRegistered] = useState(false);
19
+ const [updateAvailable, setUpdateAvailable] = useState(false);
20
+ const [isLoading, setIsLoading] = useState(true);
21
+ const [swFileExists, setSwFileExists] = useState(true); // Track if service worker file exists
22
+
23
+ const serviceWorkerEnabled = settings?.serviceWorker?.enabled ?? true;
24
+
25
+ useEffect(() => {
26
+ // Don't check service worker status if disabled
27
+ if (!serviceWorkerEnabled) {
28
+ // Immediately clear all state and hide error messages
29
+ setIsRegistered(false);
30
+ setUpdateAvailable(false);
31
+ setIsLoading(false);
32
+ setSwFileExists(true); // Set to true to prevent error messages from showing
33
+ return;
34
+ }
35
+
36
+ // Initial check with loading state
37
+ const initialCheck = async () => {
38
+ // Double-check service worker is still enabled before proceeding
39
+ const stillEnabled = settings?.serviceWorker?.enabled ?? true;
40
+ if (!stillEnabled) {
41
+ setIsLoading(false);
42
+ return;
43
+ }
44
+
45
+ setIsLoading(true);
46
+
47
+ // First check if service worker file exists
48
+ const exists = await serviceWorkerFileExists();
49
+
50
+ // Check again if service worker was disabled during the async operation
51
+ const currentEnabled = settings?.serviceWorker?.enabled ?? true;
52
+ if (!currentEnabled) {
53
+ setIsLoading(false);
54
+ return;
55
+ }
56
+
57
+ setSwFileExists(exists);
58
+
59
+ if (exists) {
60
+ // Check service worker status only if file exists
61
+ const status = await getServiceWorkerStatus();
62
+
63
+ // Final check before updating state
64
+ const finalCheck = settings?.serviceWorker?.enabled ?? true;
65
+ if (finalCheck) {
66
+ setIsRegistered(status.registered);
67
+ setUpdateAvailable(status.updateAvailable);
68
+ }
69
+ } else {
70
+ // File doesn't exist, so service worker can't be registered
71
+ // Only update if service worker is still enabled
72
+ const finalCheck = settings?.serviceWorker?.enabled ?? true;
73
+ if (finalCheck) {
74
+ setIsRegistered(false);
75
+ setUpdateAvailable(false);
76
+ }
77
+ }
78
+
79
+ setIsLoading(false);
80
+ };
81
+
82
+ // Background refresh without affecting loading state
83
+ const refreshStatus = async () => {
84
+ // Skip if service worker was disabled (check current setting value)
85
+ const currentEnabled = settings?.serviceWorker?.enabled ?? true;
86
+ if (!currentEnabled) {
87
+ return;
88
+ }
89
+
90
+ // Check if file exists first
91
+ const exists = await serviceWorkerFileExists();
92
+
93
+ // Check again if service worker was disabled during the async operation
94
+ const currentEnabledAfter = settings?.serviceWorker?.enabled ?? true;
95
+ if (!currentEnabledAfter) {
96
+ return;
97
+ }
98
+
99
+ setSwFileExists(exists);
100
+
101
+ if (exists) {
102
+ // Check service worker status only if file exists
103
+ const status = await getServiceWorkerStatus();
104
+
105
+ // Final check before updating state
106
+ const finalCheck = settings?.serviceWorker?.enabled ?? true;
107
+ if (finalCheck) {
108
+ setIsRegistered(status.registered);
109
+ setUpdateAvailable(status.updateAvailable);
110
+ }
111
+ } else {
112
+ // File doesn't exist, so service worker can't be registered
113
+ // Only update if service worker is still enabled
114
+ const finalCheck = settings?.serviceWorker?.enabled ?? true;
115
+ if (finalCheck) {
116
+ setIsRegistered(false);
117
+ setUpdateAvailable(false);
118
+ }
119
+ }
120
+ };
121
+
122
+ // Initial check immediately
123
+ initialCheck();
124
+
125
+ // Listen for status changes
126
+ const unsubscribe = addStatusListener((status) => {
127
+ // Only update if service worker is still enabled (check current setting value)
128
+ const currentEnabled = settings?.serviceWorker?.enabled ?? true;
129
+ if (currentEnabled) {
130
+ setIsRegistered(status.registered);
131
+ setUpdateAvailable(status.updateAvailable);
132
+ setIsLoading(false);
133
+ }
134
+ });
135
+
136
+ // Also check periodically as fallback (background refresh) - less frequent to avoid excessive fetches
137
+ const interval = setInterval(refreshStatus, 30000); // Check every 30 seconds instead of 5
138
+
139
+ return () => {
140
+ unsubscribe();
141
+ clearInterval(interval);
142
+ };
143
+ }, [serviceWorkerEnabled]);
144
+
145
+ const handleToggleServiceWorker = async (enabled: boolean) => {
146
+ // Immediately clear all state before updating setting to prevent error flashes
147
+ if (!enabled) {
148
+ setIsRegistered(false);
149
+ setUpdateAvailable(false);
150
+ setSwFileExists(true); // Set to true to hide error messages immediately
151
+ setIsLoading(false);
152
+ }
153
+
154
+ updateSetting('serviceWorker', { enabled });
155
+
156
+ if (!enabled) {
157
+ // Already cleared above, just return
158
+ return;
159
+ }
160
+
161
+ // Give it a moment to register/unregister
162
+ setTimeout(async () => {
163
+ // Double-check service worker is still enabled before updating state
164
+ const currentEnabled = settings?.serviceWorker?.enabled ?? true;
165
+ if (enabled && currentEnabled) {
166
+ const registered = await isServiceWorkerRegistered();
167
+ setIsRegistered(registered);
168
+ }
169
+ }, 1000);
170
+ };
171
+
172
+ const handleUpdateNow = async () => {
173
+ try {
174
+ await updateServiceWorker();
175
+ // Don't show toast here - updateServiceWorker() will handle the reload
176
+ // and the update process already provides feedback through the UI
177
+ } catch (_error) {
178
+ shellui.toast({
179
+ title: t('caching.updateError.title'),
180
+ description: t('caching.updateError.description'),
181
+ type: 'error',
182
+ });
183
+ }
184
+ };
185
+
186
+ const handleResetToLatest = async () => {
187
+ try {
188
+ // Clear all caches and reload
189
+ if ('caches' in window) {
190
+ const cacheNames = await caches.keys();
191
+ await Promise.all(cacheNames.map((name) => caches.delete(name)));
192
+ }
193
+
194
+ shellui.toast({
195
+ title: t('caching.resetSuccess.title'),
196
+ description: t('caching.resetSuccess.description'),
197
+ type: 'success',
198
+ });
199
+
200
+ // Reload the app using shellUI refresh message (refreshes entire app, not just iframe)
201
+ setTimeout(() => {
202
+ const sent = shellui.sendMessageToParent({
203
+ type: 'SHELLUI_REFRESH_PAGE',
204
+ payload: {},
205
+ });
206
+ if (!sent) {
207
+ // Fallback to window.location.reload if message can't be sent
208
+ window.location.reload();
209
+ }
210
+ }, 1000);
211
+ } catch (_error) {
212
+ shellui.toast({
213
+ title: t('caching.resetError.title'),
214
+ description: t('caching.resetError.description'),
215
+ type: 'error',
216
+ });
217
+ }
218
+ };
219
+
220
+ return (
221
+ <div className="space-y-6">
222
+ {isLoading ? (
223
+ <div className="text-sm text-muted-foreground">{t('caching.loading')}</div>
224
+ ) : null}
225
+
226
+ {!isLoading && (
227
+ <div className="space-y-6">
228
+ {/* Enable/Disable service worker */}
229
+ <div className="space-y-2">
230
+ <div className="flex items-center justify-between">
231
+ <div className="space-y-0.5">
232
+ <label
233
+ className="text-sm font-medium leading-none"
234
+ style={{ fontFamily: 'var(--heading-font-family, inherit)' }}
235
+ >
236
+ {t('caching.enabled.title')}
237
+ </label>
238
+ <p className="text-sm text-muted-foreground">
239
+ {serviceWorkerEnabled
240
+ ? t('caching.enabled.descriptionEnabled')
241
+ : t('caching.enabled.descriptionDisabled')}
242
+ </p>
243
+ </div>
244
+ <Switch
245
+ checked={serviceWorkerEnabled}
246
+ onCheckedChange={handleToggleServiceWorker}
247
+ />
248
+ </div>
249
+ </div>
250
+
251
+ {/* Status – always visible; shows Disabled when off */}
252
+ <div className="space-y-2">
253
+ <label
254
+ className="text-sm font-medium leading-none"
255
+ style={{ fontFamily: 'var(--heading-font-family, inherit)' }}
256
+ >
257
+ {t('caching.status.title')}
258
+ </label>
259
+ <div className="flex items-center gap-2 text-sm mt-2">
260
+ {!serviceWorkerEnabled ? (
261
+ <span className="text-muted-foreground">○ {t('caching.status.disabled')}</span>
262
+ ) : !swFileExists ? (
263
+ <span className="text-red-600 dark:text-red-400">
264
+ ✗ {t('caching.status.fileNotFound')}
265
+ </span>
266
+ ) : (
267
+ <>
268
+ <span
269
+ className={
270
+ isRegistered
271
+ ? 'text-green-600 dark:text-green-400'
272
+ : 'text-orange-600 dark:text-orange-400'
273
+ }
274
+ >
275
+ {isRegistered ? '●' : '○'}{' '}
276
+ {isRegistered ? t('caching.status.registered') : t('caching.status.notRunning')}
277
+ </span>
278
+ {updateAvailable && (
279
+ <>
280
+ <span className="text-muted-foreground/50">|</span>
281
+ <span className="text-blue-600 dark:text-blue-400">
282
+ ● {t('caching.status.updateAvailable')}
283
+ </span>
284
+ </>
285
+ )}
286
+ </>
287
+ )}
288
+ </div>
289
+ </div>
290
+
291
+ {serviceWorkerEnabled && (
292
+ <>
293
+ {/* Error Message when file missing */}
294
+ {!swFileExists && (
295
+ <div className="rounded-lg border border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-950/30 p-4 space-y-3">
296
+ <div className="space-y-1">
297
+ <h3
298
+ className="text-sm font-medium leading-none text-red-600 dark:text-red-400"
299
+ style={{ fontFamily: 'var(--heading-font-family, inherit)' }}
300
+ >
301
+ {t('caching.status.fileNotFound')}
302
+ </h3>
303
+ <p className="text-sm text-red-700 dark:text-red-300">
304
+ {t('caching.status.fileNotFoundDescription')}
305
+ </p>
306
+ </div>
307
+ </div>
308
+ )}
309
+
310
+ {/* Update Available */}
311
+ {updateAvailable && (
312
+ <div className="space-y-2">
313
+ <div className="space-y-0.5">
314
+ <label
315
+ className="text-sm font-medium leading-none"
316
+ style={{ fontFamily: 'var(--heading-font-family, inherit)' }}
317
+ >
318
+ {t('caching.update.title')}
319
+ </label>
320
+ <p className="text-sm text-muted-foreground">
321
+ {t('caching.update.description')}
322
+ </p>
323
+ </div>
324
+ <div className="mt-2">
325
+ <Button
326
+ variant="outline"
327
+ onClick={handleUpdateNow}
328
+ className="w-full sm:w-auto"
329
+ >
330
+ {t('caching.update.button')}
331
+ </Button>
332
+ </div>
333
+ </div>
334
+ )}
335
+
336
+ {/* Reset Cache */}
337
+ <div className="space-y-2">
338
+ <div className="space-y-0.5">
339
+ <label
340
+ className="text-sm font-medium leading-none"
341
+ style={{ fontFamily: 'var(--heading-font-family, inherit)' }}
342
+ >
343
+ {t('caching.reset.title')}
344
+ </label>
345
+ <p className="text-sm text-muted-foreground">{t('caching.reset.description')}</p>
346
+ </div>
347
+ <div className="mt-2">
348
+ <Button
349
+ variant="outline"
350
+ onClick={handleResetToLatest}
351
+ className="w-full sm:w-auto"
352
+ >
353
+ {t('caching.reset.button')}
354
+ </Button>
355
+ </div>
356
+ </div>
357
+ </>
358
+ )}
359
+ </div>
360
+ )}
361
+ </div>
362
+ );
363
+ };