@shellui/core 0.1.0 → 0.2.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/package.json +4 -2
  2. package/src/components/AppPathView.tsx +31 -0
  3. package/src/components/ContentView.tsx +14 -6
  4. package/src/components/HomeView.tsx +9 -2
  5. package/src/components/IndexRoute.tsx +37 -0
  6. package/src/components/NotFoundView.tsx +3 -2
  7. package/src/components/ViewRoute.tsx +11 -7
  8. package/src/components/ui/tooltip.tsx +52 -0
  9. package/src/constants/urls.ts +2 -0
  10. package/src/features/config/ConfigProvider.ts +20 -76
  11. package/src/features/config/shellui-config.d.ts +13 -0
  12. package/src/features/config/types.ts +14 -3
  13. package/src/features/config/useConfig.ts +1 -10
  14. package/src/features/cookieConsent/cookieConsent.ts +2 -4
  15. package/src/features/layouts/AppBarLayout.tsx +260 -0
  16. package/src/features/layouts/AppLayout.tsx +6 -0
  17. package/src/features/layouts/DefaultLayout.tsx +25 -17
  18. package/src/features/layouts/OverlayShell.tsx +19 -8
  19. package/src/features/layouts/WindowsLayout.tsx +11 -9
  20. package/src/features/layouts/utils.ts +44 -0
  21. package/src/features/sentry/initSentry.ts +82 -12
  22. package/src/features/settings/SettingsProvider.tsx +2 -1
  23. package/src/features/settings/SettingsView.tsx +79 -15
  24. package/src/features/settings/components/Advanced.tsx +17 -2
  25. package/src/features/settings/components/ApplicationSettingsPanel.tsx +25 -0
  26. package/src/features/settings/components/Develop.tsx +68 -4
  27. package/src/i18n/translations/en/common.json +5 -0
  28. package/src/i18n/translations/en/settings.json +3 -1
  29. package/src/i18n/translations/fr/common.json +5 -0
  30. package/src/i18n/translations/fr/settings.json +3 -1
  31. package/src/index.css +10 -0
  32. package/src/lib/z-index.ts +2 -0
  33. package/src/router/routes.tsx +18 -5
  34. package/tailwind.config.js +1 -1
@@ -1,4 +1,5 @@
1
1
  import { getCookieConsentAccepted } from '../cookieConsent/cookieConsent';
2
+ import shelluiConfig from '@shellui/config';
2
3
 
3
4
  const SETTINGS_KEY = 'shellui:settings';
4
5
 
@@ -19,17 +20,10 @@ function isErrorReportingEnabled(): boolean {
19
20
  }
20
21
  }
21
22
 
22
- type SentryGlobals = {
23
- __SHELLUI_SENTRY_DSN__?: string;
24
- __SHELLUI_SENTRY_ENVIRONMENT__?: string;
25
- __SHELLUI_SENTRY_RELEASE__?: string;
26
- };
27
-
28
23
  /**
29
24
  * Initialize error reporting only in production when configured and user has not disabled it.
30
25
  * Lazy-loads @sentry/react only when needed so the bundle is not loaded when Sentry is unused.
31
- * Reads DSN, environment, and release from __SHELLUI_SENTRY_DSN__, __SHELLUI_SENTRY_ENVIRONMENT__,
32
- * and __SHELLUI_SENTRY_RELEASE__ (injected at build time) and user preference from settings.
26
+ * Reads Sentry config from @shellui/config (injected at build time) and user preference from settings.
33
27
  * Exported so the settings UI can re-initialize when the user re-enables reporting.
34
28
  */
35
29
  export function initSentry(): void {
@@ -40,16 +34,32 @@ export function initSentry(): void {
40
34
  if (!isErrorReportingEnabled()) {
41
35
  return;
42
36
  }
43
- const g = globalThis as unknown as SentryGlobals;
44
- const dsn = g.__SHELLUI_SENTRY_DSN__;
37
+ const sentry = shelluiConfig?.sentry;
38
+ const dsn = sentry?.dsn;
45
39
  if (!dsn || typeof dsn !== 'string') {
46
40
  return;
47
41
  }
48
42
  void import('@sentry/react').then((Sentry) => {
43
+ const isLocalhost =
44
+ typeof window !== 'undefined' &&
45
+ (window.location.hostname === 'localhost' ||
46
+ window.location.hostname === '127.0.0.1' ||
47
+ window.location.hostname === '[::1]');
48
+
49
+ // For localhost, use tunnel through the dev server to avoid CORS issues
50
+ // The tunnel endpoint proxies requests to Sentry, bypassing browser CORS restrictions
51
+ const tunnel =
52
+ isLocalhost && typeof window !== 'undefined'
53
+ ? `${window.location.origin}/api/sentry-tunnel`
54
+ : undefined;
55
+
49
56
  Sentry.init({
50
57
  dsn,
51
- environment: g.__SHELLUI_SENTRY_ENVIRONMENT__ ?? 'production',
52
- release: g.__SHELLUI_SENTRY_RELEASE__,
58
+ environment: sentry.environment ?? 'production',
59
+ release: sentry.release,
60
+ sendDefaultPii: true,
61
+ // Use tunnel for localhost to avoid CORS issues
62
+ ...(tunnel && { tunnel }),
53
63
  });
54
64
  sentryLoaded = true;
55
65
  });
@@ -69,4 +79,64 @@ export function closeSentry(): void {
69
79
  });
70
80
  }
71
81
 
82
+ /**
83
+ * Ensure Sentry is initialized and ready, then capture an exception.
84
+ * Useful for manually triggering error reports (e.g., test button).
85
+ * This function will initialize Sentry even in dev mode if needed for testing.
86
+ * @param error - The error to capture
87
+ * @returns Promise that resolves when the error has been sent (or rejected if Sentry is unavailable)
88
+ */
89
+ export async function captureException(error: Error): Promise<void> {
90
+ const sentry = shelluiConfig?.sentry;
91
+ const dsn = sentry?.dsn;
92
+
93
+ if (!dsn || typeof dsn !== 'string') {
94
+ throw new Error('Sentry DSN not configured');
95
+ }
96
+
97
+ // If Sentry is not loaded, initialize it (even in dev mode for testing)
98
+ if (!sentryLoaded) {
99
+ // Check if error reporting is enabled
100
+ if (!isErrorReportingEnabled()) {
101
+ throw new Error('Error reporting is disabled in settings');
102
+ }
103
+
104
+ // Import and initialize Sentry (bypass dev mode check for manual testing)
105
+ const Sentry = await import('@sentry/react');
106
+ const isLocalhost =
107
+ typeof window !== 'undefined' &&
108
+ (window.location.hostname === 'localhost' ||
109
+ window.location.hostname === '127.0.0.1' ||
110
+ window.location.hostname === '[::1]');
111
+
112
+ // For localhost, use tunnel through the dev server to avoid CORS issues
113
+ // The tunnel endpoint proxies requests to Sentry, bypassing browser CORS restrictions
114
+ const tunnel =
115
+ isLocalhost && typeof window !== 'undefined'
116
+ ? `${window.location.origin}/api/sentry-tunnel`
117
+ : undefined;
118
+
119
+ Sentry.init({
120
+ dsn,
121
+ environment: sentry.environment ?? 'production',
122
+ release: sentry.release,
123
+ sendDefaultPii: true,
124
+ // Use tunnel for localhost to avoid CORS issues
125
+ ...(tunnel && { tunnel }),
126
+ });
127
+ sentryLoaded = true;
128
+
129
+ // Wait a bit for Sentry to fully initialize
130
+ await new Promise((resolve) => setTimeout(resolve, 100));
131
+ }
132
+
133
+ // Capture the exception
134
+ const Sentry = await import('@sentry/react');
135
+ if (Sentry.captureException) {
136
+ Sentry.captureException(error);
137
+ } else {
138
+ throw new Error('Sentry captureException not available');
139
+ }
140
+ }
141
+
72
142
  initSentry();
@@ -10,6 +10,7 @@ import { SettingsContext } from './SettingsContext';
10
10
  import { useConfig } from '../config/useConfig';
11
11
  import { useTranslation } from 'react-i18next';
12
12
  import type { NavigationItem, NavigationGroup } from '../config/types';
13
+ import { getEffectiveUrl } from '../layouts/utils';
13
14
 
14
15
  const logger = getLogger('shellcore');
15
16
 
@@ -39,7 +40,7 @@ function buildSettingsWithNavigation(
39
40
  if (!navigation?.length) return settings;
40
41
  const items: SettingsNavigationItem[] = flattenNavigationItems(navigation).map((item) => ({
41
42
  path: item.path,
42
- url: item.url,
43
+ url: getEffectiveUrl(item),
43
44
  label: resolveLabel(item.label, lang),
44
45
  }));
45
46
  return { ...settings, navigation: { items } };
@@ -26,13 +26,17 @@ import { useConfig } from '../config/useConfig';
26
26
  import { isTauri } from '../../service-worker/register';
27
27
  import { Button } from '../../components/ui/button';
28
28
  import { ChevronRightIcon, ChevronLeftIcon } from './SettingsIcons';
29
+ import { flattenNavigationItems, resolveLocalizedString } from '../layouts/utils';
30
+ import { ApplicationSettingsPanel } from './components/ApplicationSettingsPanel';
31
+ import type { NavigationItem } from '../config/types';
32
+ import { cn } from '../../lib/utils';
29
33
 
30
34
  export const SettingsView = () => {
31
35
  const location = useLocation();
32
36
  const navigate = useNavigate();
33
37
  const { settings } = useSettings();
34
38
  const { config } = useConfig();
35
- const { t } = useTranslation('settings');
39
+ const { t, i18n } = useTranslation('settings');
36
40
  // Re-check isTauri after mount and after a short delay so we catch late-injected __TAURI__ in dev
37
41
  const [isTauriEnv, setIsTauriEnv] = useState(() => isTauri());
38
42
 
@@ -70,10 +74,46 @@ export const SettingsView = () => {
70
74
  );
71
75
  }, [settings.developerFeatures.enabled, routesWithoutTauriSw]);
72
76
 
77
+ // Application settings from navigation items with settings URL
78
+ const applicationRoutes = useMemo(() => {
79
+ const lang = i18n.language || 'en';
80
+ const flat = config?.navigation ? flattenNavigationItems(config.navigation) : [];
81
+ return flat
82
+ .filter((item): item is NavigationItem & { settings: string } => Boolean(item.settings))
83
+ .map((item) => {
84
+ const pathPrefix = `${urls.settings.replace(/^\/+/, '')}/app-${item.path}`;
85
+ const navItem: NavigationItem = {
86
+ ...item,
87
+ url: item.settings,
88
+ };
89
+ return {
90
+ name: resolveLocalizedString(item.label, lang),
91
+ iconSrc: item.icon ?? undefined,
92
+ path: `app-${item.path}`,
93
+ element: (
94
+ <ApplicationSettingsPanel
95
+ url={item.settings}
96
+ pathPrefix={pathPrefix}
97
+ navItem={navItem}
98
+ />
99
+ ),
100
+ };
101
+ });
102
+ }, [config?.navigation, i18n.language]);
103
+
104
+ // All routes (core + applications) for selection and routing
105
+ const allRoutes = useMemo(
106
+ () => [...filteredRoutes, ...applicationRoutes],
107
+ [filteredRoutes, applicationRoutes],
108
+ );
109
+
73
110
  // Group routes by category
74
111
  const groupedRoutes = useMemo(() => {
75
112
  const developerOnlyPaths = ['developpers', 'service-worker'];
76
113
  const groups = [
114
+ ...(applicationRoutes.length > 0
115
+ ? [{ title: t('categories.applications'), routes: applicationRoutes }]
116
+ : []),
77
117
  {
78
118
  title: t('categories.preferences'),
79
119
  routes: filteredRoutes.filter((route) =>
@@ -90,7 +130,7 @@ export const SettingsView = () => {
90
130
  },
91
131
  ];
92
132
  return groups.filter((group) => group.routes.length > 0);
93
- }, [filteredRoutes, t]);
133
+ }, [filteredRoutes, applicationRoutes, t]);
94
134
 
95
135
  // Find matching nav item by checking if URL contains or ends with the item path
96
136
  const getSelectedItemFromUrl = useCallback(() => {
@@ -98,7 +138,7 @@ export const SettingsView = () => {
98
138
 
99
139
  // Find matching nav item by checking if pathname contains the item path
100
140
  // This works regardless of the URL structure/prefix
101
- const matchedItem = filteredRoutes.find((item) => {
141
+ const matchedItem = allRoutes.find((item) => {
102
142
  // Normalize paths for comparison (remove leading/trailing slashes)
103
143
  const normalizedPathname = pathname.replace(/^\/+|\/+$/g, '');
104
144
  const normalizedItemPath = item.path.replace(/^\/+|\/+$/g, '');
@@ -112,7 +152,7 @@ export const SettingsView = () => {
112
152
  });
113
153
 
114
154
  return matchedItem;
115
- }, [location.pathname, filteredRoutes]);
155
+ }, [location.pathname, allRoutes]);
116
156
 
117
157
  const selectedItem = useMemo(() => getSelectedItemFromUrl(), [getSelectedItemFromUrl]);
118
158
 
@@ -139,7 +179,7 @@ export const SettingsView = () => {
139
179
 
140
180
  // Pathname doesn't match settings path structure - not in settings
141
181
  return false;
142
- }, [location.pathname, filteredRoutes]);
182
+ }, [location.pathname]);
143
183
 
144
184
  // Navigate back to settings root
145
185
  const handleBackToSettings = useCallback(() => {
@@ -168,7 +208,17 @@ export const SettingsView = () => {
168
208
  onClick={() => navigate(`${urls.settings}/${item.path}`)}
169
209
  className="cursor-pointer"
170
210
  >
171
- <item.icon />
211
+ {'icon' in item && item.icon ? (
212
+ <item.icon />
213
+ ) : 'iconSrc' in item && item.iconSrc ? (
214
+ <img
215
+ src={item.iconSrc}
216
+ alt=""
217
+ className="h-4 w-4 shrink-0"
218
+ />
219
+ ) : (
220
+ <span className="h-4 w-4 shrink-0" />
221
+ )}
172
222
  <span>{item.name}</span>
173
223
  </button>
174
224
  </SidebarMenuButton>
@@ -203,8 +253,19 @@ export const SettingsView = () => {
203
253
  </h2>
204
254
  <div className="flex flex-col bg-card rounded-lg overflow-hidden border border-border">
205
255
  {group.routes.map((item, itemIndex) => {
206
- const Icon = item.icon;
207
256
  const isLast = itemIndex === group.routes.length - 1;
257
+ const iconEl =
258
+ 'icon' in item && item.icon ? (
259
+ <item.icon />
260
+ ) : 'iconSrc' in item && item.iconSrc ? (
261
+ <img
262
+ src={item.iconSrc}
263
+ alt=""
264
+ className="h-4 w-4 shrink-0"
265
+ />
266
+ ) : (
267
+ <span className="h-4 w-4 shrink-0" />
268
+ );
208
269
  return (
209
270
  <div
210
271
  key={item.name}
@@ -218,9 +279,7 @@ export const SettingsView = () => {
218
279
  className="w-full flex items-center justify-between px-4 py-3 bg-transparent hover:bg-sidebar-accent hover:text-sidebar-accent-foreground active:bg-sidebar-accent active:text-sidebar-accent-foreground transition-colors cursor-pointer rounded-none"
219
280
  >
220
281
  <div className="flex items-center gap-2 flex-1 min-w-0">
221
- <div className="flex-shrink-0 text-foreground/70">
222
- <Icon />
223
- </div>
282
+ <div className="flex-shrink-0 text-foreground/70">{iconEl}</div>
224
283
  <span className="text-sm font-normal text-foreground">
225
284
  {item.name}
226
285
  </span>
@@ -256,15 +315,15 @@ export const SettingsView = () => {
256
315
  <Route
257
316
  index
258
317
  element={
259
- filteredRoutes.length > 0 ? (
318
+ allRoutes.length > 0 ? (
260
319
  <Navigate
261
- to={`${urls.settings}/${filteredRoutes[0].path}`}
320
+ to={`${urls.settings}/${allRoutes[0].path}`}
262
321
  replace
263
322
  />
264
323
  ) : null
265
324
  }
266
325
  />
267
- {filteredRoutes.map((item) => (
326
+ {allRoutes.map((item) => (
268
327
  <Route
269
328
  key={item.path}
270
329
  path={item.path}
@@ -294,7 +353,12 @@ export const SettingsView = () => {
294
353
  </div>
295
354
  </header>
296
355
  )}
297
- <div className="flex flex-1 flex-col gap-4 overflow-y-auto p-4 pt-0">
356
+ <div
357
+ className={cn(
358
+ 'flex flex-1 flex-col gap-4 overflow-y-auto',
359
+ !selectedItem?.path?.startsWith('app-') && 'p-4 pt-0',
360
+ )}
361
+ >
298
362
  <Routes>
299
363
  <Route
300
364
  index
@@ -311,7 +375,7 @@ export const SettingsView = () => {
311
375
  </div>
312
376
  }
313
377
  />
314
- {filteredRoutes.map((item) => (
378
+ {allRoutes.map((item) => (
315
379
  <Route
316
380
  key={item.path}
317
381
  path={item.path}
@@ -5,14 +5,26 @@ import { closeSentry, initSentry } from '../../sentry/initSentry';
5
5
  import { useSettings } from '../hooks/useSettings';
6
6
  import { Button } from '../../../components/ui/button';
7
7
  import { shellui } from '@shellui/sdk';
8
+ import { useCookieConsent } from '../../cookieConsent/useCookieConsent';
8
9
 
9
10
  export const Advanced = () => {
10
11
  const { t } = useTranslation('settings');
11
12
  const { config } = useConfig();
12
13
  const { settings, updateSetting, resetAllData } = useSettings();
13
14
  const errorReportingConfigured = Boolean(config?.sentry?.dsn);
15
+ const { isAccepted: sentryConsentAccepted } = useCookieConsent('sentry.io');
14
16
 
15
17
  const handleErrorReportingChange = (checked: boolean) => {
18
+ // Don't allow enabling if cookie consent hasn't been approved
19
+ if (checked && !sentryConsentAccepted) {
20
+ shellui.toast({
21
+ title: 'Cookie consent required',
22
+ description:
23
+ 'Please approve Sentry cookie consent in Data Privacy settings to enable error reporting.',
24
+ type: 'error',
25
+ });
26
+ return;
27
+ }
16
28
  updateSetting('errorReporting', { enabled: checked });
17
29
  if (checked) {
18
30
  initSentry();
@@ -56,15 +68,18 @@ export const Advanced = () => {
56
68
  </span>
57
69
  <p className="text-sm text-muted-foreground">
58
70
  {errorReportingConfigured
59
- ? t('advanced.errorReporting.statusConfigured')
71
+ ? sentryConsentAccepted
72
+ ? t('advanced.errorReporting.statusConfigured')
73
+ : 'Cookie consent required to enable error reporting'
60
74
  : t('advanced.errorReporting.statusNotConfigured')}
61
75
  </p>
62
76
  </div>
63
77
  {errorReportingConfigured && (
64
78
  <Switch
65
79
  id="error-reporting"
66
- checked={settings.errorReporting.enabled}
80
+ checked={sentryConsentAccepted && settings.errorReporting.enabled}
67
81
  onCheckedChange={handleErrorReportingChange}
82
+ disabled={!sentryConsentAccepted}
68
83
  />
69
84
  )}
70
85
  </div>
@@ -0,0 +1,25 @@
1
+ import type { NavigationItem } from '../../config/types';
2
+ import { ContentView } from '../../../components/ContentView';
3
+
4
+ interface ApplicationSettingsPanelProps {
5
+ url: string;
6
+ pathPrefix: string;
7
+ navItem: NavigationItem;
8
+ }
9
+
10
+ export const ApplicationSettingsPanel = ({
11
+ url,
12
+ pathPrefix,
13
+ navItem,
14
+ }: ApplicationSettingsPanelProps) => {
15
+ return (
16
+ <div className="m-0 p-0 flex flex-1 flex-col min-h-0 w-full overflow-hidden bg-background">
17
+ <ContentView
18
+ url={url}
19
+ pathPrefix={pathPrefix}
20
+ navItem={navItem}
21
+ ignoreMessages={true}
22
+ />
23
+ </div>
24
+ );
25
+ };
@@ -2,7 +2,11 @@ import { useTranslation } from 'react-i18next';
2
2
  import { shellui } from '@shellui/sdk';
3
3
  import { useSettings } from '../hooks/useSettings';
4
4
  import { useConfig } from '../../config/useConfig';
5
- import { flattenNavigationItems, resolveLocalizedString } from '../../layouts/utils';
5
+ import {
6
+ flattenNavigationItems,
7
+ getNavPathPrefix,
8
+ resolveLocalizedString,
9
+ } from '../../layouts/utils';
6
10
  import { Switch } from '../../../components/ui/switch';
7
11
  import { Select } from '../../../components/ui/select';
8
12
  import { Button } from '../../../components/ui/button';
@@ -10,6 +14,8 @@ import { ToastTestButtons } from './develop/ToastTestButtons';
10
14
  import { DialogTestButtons } from './develop/DialogTestButtons';
11
15
  import { ModalTestButtons } from './develop/ModalTestButtons';
12
16
  import { DrawerTestButtons } from './develop/DrawerTestButtons';
17
+ import { captureException } from '../../sentry/initSentry';
18
+ import { useCookieConsent } from '../../cookieConsent/useCookieConsent';
13
19
  import type { LayoutType } from '../../config/types';
14
20
 
15
21
  export const Develop = () => {
@@ -23,6 +29,8 @@ export const Develop = () => {
23
29
  (item, index, self) => index === self.findIndex((i) => i.path === item.path),
24
30
  )
25
31
  : [];
32
+ const errorReportingConfigured = Boolean(config?.sentry?.dsn);
33
+ const { isAccepted: sentryConsentAccepted } = useCookieConsent('sentry.io');
26
34
 
27
35
  return (
28
36
  <div className="space-y-6">
@@ -120,8 +128,8 @@ export const Develop = () => {
120
128
  <option value="">{t('develop.navigation.placeholder')}</option>
121
129
  {navItems.map((item) => (
122
130
  <option
123
- key={item.path}
124
- value={`/${item.path}`}
131
+ key={item.path || 'root'}
132
+ value={getNavPathPrefix(item)}
125
133
  >
126
134
  {resolveLocalizedString(item.label, currentLanguage) || item.path}
127
135
  </option>
@@ -142,7 +150,7 @@ export const Develop = () => {
142
150
  {t('develop.layout.title')}
143
151
  </h3>
144
152
  <div className="flex flex-wrap gap-2">
145
- {(['sidebar', 'windows'] as const).map((layoutMode) => (
153
+ {(['sidebar', 'app-bar', 'windows'] as const).map((layoutMode) => (
146
154
  <Button
147
155
  key={layoutMode}
148
156
  variant={effectiveLayout === layoutMode ? 'default' : 'outline'}
@@ -167,6 +175,62 @@ export const Develop = () => {
167
175
  <DialogTestButtons />
168
176
  <ModalTestButtons />
169
177
  <DrawerTestButtons />
178
+ {errorReportingConfigured && (
179
+ <div className="flex items-center justify-between">
180
+ <div className="space-y-0.5">
181
+ <span
182
+ className="text-sm font-medium leading-none"
183
+ style={{ fontFamily: 'var(--heading-font-family, inherit)' }}
184
+ >
185
+ Test Error Reporting
186
+ </span>
187
+ <p className="text-sm text-muted-foreground">
188
+ Trigger a test error to verify Sentry is working correctly
189
+ </p>
190
+ </div>
191
+ <Button
192
+ variant="destructive"
193
+ onClick={async () => {
194
+ if (!sentryConsentAccepted) {
195
+ shellui.toast({
196
+ title: 'Cookie consent required',
197
+ description:
198
+ 'Please approve Sentry cookie consent in Data Privacy settings to test error reporting.',
199
+ type: 'error',
200
+ });
201
+ return;
202
+ }
203
+
204
+ const testError = new Error('This is your first error!');
205
+
206
+ try {
207
+ // Ensure Sentry is initialized and capture the error
208
+ await captureException(testError);
209
+
210
+ // Show success toast
211
+ shellui.toast({
212
+ title: 'Test error triggered',
213
+ description: 'The error has been sent to Sentry for tracking.',
214
+ type: 'success',
215
+ });
216
+ } catch (err) {
217
+ // If Sentry capture failed, show error toast
218
+ shellui.toast({
219
+ title: 'Failed to send error to Sentry',
220
+ description:
221
+ err instanceof Error
222
+ ? err.message
223
+ : 'Sentry is not configured or not available.',
224
+ type: 'error',
225
+ });
226
+ }
227
+ }}
228
+ disabled={!sentryConsentAccepted}
229
+ >
230
+ Break the world
231
+ </Button>
232
+ </div>
233
+ )}
170
234
  </div>
171
235
  </div>
172
236
  </div>
@@ -2,6 +2,11 @@
2
2
  "settings": "Settings",
3
3
  "welcome": "Welcome to {{title}}",
4
4
  "getStarted": "Select a navigation item to get started.",
5
+ "homeConfig": {
6
+ "intro": "To show custom content on the home page:",
7
+ "startUrl": "Set start_url in shellui.config to redirect \"/\" to another path (e.g. start_url: '/playground').",
8
+ "rootNav": "Or add a navigation item with path \"\" or \"/\" to display that item at \"/\"."
9
+ },
5
10
  "navigationError": "Navigation error",
6
11
  "navigationNotAllowed": "This URL is not configured in the app navigation.",
7
12
  "errorBoundary": {
@@ -2,6 +2,7 @@
2
2
  "title": "Settings",
3
3
  "categories": {
4
4
  "preferences": "Preferences",
5
+ "applications": "Applications",
5
6
  "system": "System",
6
7
  "developer": "Developer"
7
8
  },
@@ -199,7 +200,8 @@
199
200
  "title": "Layout",
200
201
  "description": "Switch between sidebar layout and windows (taskbar) layout. This overrides the app config for this session.",
201
202
  "sidebar": "Sidebar",
202
- "windows": "Windows"
203
+ "windows": "Windows (experimental)",
204
+ "app-bar": "App bar"
203
205
  },
204
206
  "logging": {
205
207
  "title": "Logging",
@@ -2,6 +2,11 @@
2
2
  "settings": "Paramètres",
3
3
  "welcome": "Bienvenue sur {{title}}",
4
4
  "getStarted": "Sélectionnez un élément de navigation pour commencer.",
5
+ "homeConfig": {
6
+ "intro": "Pour afficher du contenu personnalisé sur la page d'accueil :",
7
+ "startUrl": "Définissez start_url dans shellui.config pour rediriger \"/\" vers un autre chemin (ex. start_url: '/playground').",
8
+ "rootNav": "Ou ajoutez un élément de navigation avec path \"\" ou \"/\" pour l'afficher à \"/\"."
9
+ },
5
10
  "navigationError": "Erreur de navigation",
6
11
  "navigationNotAllowed": "Cette URL n'est pas configurée dans la navigation de l'application.",
7
12
  "errorBoundary": {
@@ -2,6 +2,7 @@
2
2
  "title": "Paramètres",
3
3
  "categories": {
4
4
  "preferences": "Préférences",
5
+ "applications": "Applications",
5
6
  "system": "Système",
6
7
  "developer": "Développeur"
7
8
  },
@@ -199,7 +200,8 @@
199
200
  "title": "Disposition",
200
201
  "description": "Basculer entre la disposition avec barre latérale et la disposition fenêtres (barre des tâches). Remplace la config de l'app pour cette session.",
201
202
  "sidebar": "Barre latérale",
202
- "windows": "Fenêtres"
203
+ "windows": "Fenêtres (expérimental)",
204
+ "app-bar": "Barre d'app"
203
205
  },
204
206
  "logging": {
205
207
  "title": "Journalisation",
package/src/index.css CHANGED
@@ -318,6 +318,16 @@
318
318
  filter: brightness(0) invert(1) saturate(100%);
319
319
  opacity: 0.9;
320
320
  }
321
+
322
+ /* App bar layout logo - same as sidebar: dark in light mode, white in dark mode */
323
+ [data-layout='app-bar'] img.app-bar-logo {
324
+ filter: brightness(0) saturate(100%);
325
+ opacity: 0.9;
326
+ }
327
+ .dark [data-layout='app-bar'] img.app-bar-logo {
328
+ filter: brightness(0) invert(1) saturate(100%);
329
+ opacity: 0.9;
330
+ }
321
331
  }
322
332
 
323
333
  /* Dialog / modal overlay and content animations */
@@ -9,6 +9,8 @@ export const Z_INDEX = {
9
9
  /** Modal overlay and content (settings panel, etc.) */
10
10
  MODAL_OVERLAY: 10000,
11
11
  MODAL_CONTENT: 10001,
12
+ /** Tooltip content (above modals so tooltips show over dialogs if needed) */
13
+ TOOLTIP: 10002,
12
14
  /** Drawer overlay and content (slide-out panels; same level as modal) */
13
15
  DRAWER_OVERLAY: 10000,
14
16
  DRAWER_CONTENT: 10001,