@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.
- package/README.md +17 -0
- package/dist/ContentView-CZG-ro_B.js +146 -0
- package/dist/ContentView-CZG-ro_B.js.map +1 -0
- package/dist/CookiePreferencesView-MhO9FO-4.js +213 -0
- package/dist/CookiePreferencesView-MhO9FO-4.js.map +1 -0
- package/dist/DefaultLayout-Dbb3uJED.js +394 -0
- package/dist/DefaultLayout-Dbb3uJED.js.map +1 -0
- package/dist/FullscreenLayout-1SgPHWw-.js +30 -0
- package/dist/FullscreenLayout-1SgPHWw-.js.map +1 -0
- package/dist/HomeView-DYU-O_Il.js +21 -0
- package/dist/HomeView-DYU-O_Il.js.map +1 -0
- package/dist/NotFoundView-CeYjJNg0.js +52 -0
- package/dist/NotFoundView-CeYjJNg0.js.map +1 -0
- package/dist/OverlayShell-pzbqQW25.js +642 -0
- package/dist/OverlayShell-pzbqQW25.js.map +1 -0
- package/dist/SettingsView-Bndrta44.js +2207 -0
- package/dist/SettingsView-Bndrta44.js.map +1 -0
- package/dist/ViewRoute-ChSPabOy.js +32 -0
- package/dist/ViewRoute-ChSPabOy.js.map +1 -0
- package/dist/WindowsLayout-CXGNPKoY.js +633 -0
- package/dist/WindowsLayout-CXGNPKoY.js.map +1 -0
- package/dist/app.d.ts +3 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/components/ContentView.d.ts +10 -0
- package/dist/components/ContentView.d.ts.map +1 -0
- package/dist/components/HomeView.d.ts +2 -0
- package/dist/components/HomeView.d.ts.map +1 -0
- package/dist/components/LoadingOverlay.d.ts +2 -0
- package/dist/components/LoadingOverlay.d.ts.map +1 -0
- package/dist/components/NotFoundView.d.ts +2 -0
- package/dist/components/NotFoundView.d.ts.map +1 -0
- package/dist/components/RouteErrorBoundary.d.ts +2 -0
- package/dist/components/RouteErrorBoundary.d.ts.map +1 -0
- package/dist/components/ViewRoute.d.ts +7 -0
- package/dist/components/ViewRoute.d.ts.map +1 -0
- package/dist/components/ui/alert-dialog.d.ts +32 -0
- package/dist/components/ui/alert-dialog.d.ts.map +1 -0
- package/dist/components/ui/breadcrumb.d.ts +20 -0
- package/dist/components/ui/breadcrumb.d.ts.map +1 -0
- package/dist/components/ui/button-group.d.ts +7 -0
- package/dist/components/ui/button-group.d.ts.map +1 -0
- package/dist/components/ui/button.d.ts +12 -0
- package/dist/components/ui/button.d.ts.map +1 -0
- package/dist/components/ui/dialog.d.ts +24 -0
- package/dist/components/ui/dialog.d.ts.map +1 -0
- package/dist/components/ui/drawer.d.ts +38 -0
- package/dist/components/ui/drawer.d.ts.map +1 -0
- package/dist/components/ui/select.d.ts +5 -0
- package/dist/components/ui/select.d.ts.map +1 -0
- package/dist/components/ui/sidebar.d.ts +46 -0
- package/dist/components/ui/sidebar.d.ts.map +1 -0
- package/dist/components/ui/sonner.d.ts +6 -0
- package/dist/components/ui/sonner.d.ts.map +1 -0
- package/dist/components/ui/switch.d.ts +8 -0
- package/dist/components/ui/switch.d.ts.map +1 -0
- package/dist/constants/urls.d.ts +6 -0
- package/dist/constants/urls.d.ts.map +1 -0
- package/dist/constants/urls.js +8 -0
- package/dist/constants/urls.js.map +1 -0
- package/dist/features/alertDialog/DialogContext.d.ts +12 -0
- package/dist/features/alertDialog/DialogContext.d.ts.map +1 -0
- package/dist/features/config/ConfigProvider.d.ts +15 -0
- package/dist/features/config/ConfigProvider.d.ts.map +1 -0
- package/dist/features/config/types.d.ts +177 -0
- package/dist/features/config/types.d.ts.map +1 -0
- package/dist/features/config/useConfig.d.ts +8 -0
- package/dist/features/config/useConfig.d.ts.map +1 -0
- package/dist/features/cookieConsent/CookieConsentModal.d.ts +6 -0
- package/dist/features/cookieConsent/CookieConsentModal.d.ts.map +1 -0
- package/dist/features/cookieConsent/CookiePreferencesView.d.ts +2 -0
- package/dist/features/cookieConsent/CookiePreferencesView.d.ts.map +1 -0
- package/dist/features/cookieConsent/cookieConsent.d.ts +22 -0
- package/dist/features/cookieConsent/cookieConsent.d.ts.map +1 -0
- package/dist/features/cookieConsent/useCookieConsent.d.ts +15 -0
- package/dist/features/cookieConsent/useCookieConsent.d.ts.map +1 -0
- package/dist/features/drawer/DrawerContext.d.ts +24 -0
- package/dist/features/drawer/DrawerContext.d.ts.map +1 -0
- package/dist/features/layouts/AppLayout.d.ts +12 -0
- package/dist/features/layouts/AppLayout.d.ts.map +1 -0
- package/dist/features/layouts/DefaultLayout.d.ts +10 -0
- package/dist/features/layouts/DefaultLayout.d.ts.map +1 -0
- package/dist/features/layouts/FullscreenLayout.d.ts +9 -0
- package/dist/features/layouts/FullscreenLayout.d.ts.map +1 -0
- package/dist/features/layouts/LayoutProviders.d.ts +9 -0
- package/dist/features/layouts/LayoutProviders.d.ts.map +1 -0
- package/dist/features/layouts/OverlayShell.d.ts +10 -0
- package/dist/features/layouts/OverlayShell.d.ts.map +1 -0
- package/dist/features/layouts/WindowsLayout.d.ts +24 -0
- package/dist/features/layouts/WindowsLayout.d.ts.map +1 -0
- package/dist/features/layouts/utils.d.ts +16 -0
- package/dist/features/layouts/utils.d.ts.map +1 -0
- package/dist/features/modal/ModalContext.d.ts +20 -0
- package/dist/features/modal/ModalContext.d.ts.map +1 -0
- package/dist/features/sentry/initSentry.d.ts +14 -0
- package/dist/features/sentry/initSentry.d.ts.map +1 -0
- package/dist/features/settings/SettingsContext.d.ts +10 -0
- package/dist/features/settings/SettingsContext.d.ts.map +1 -0
- package/dist/features/settings/SettingsIcons.d.ts +22 -0
- package/dist/features/settings/SettingsIcons.d.ts.map +1 -0
- package/dist/features/settings/SettingsProvider.d.ts +5 -0
- package/dist/features/settings/SettingsProvider.d.ts.map +1 -0
- package/dist/features/settings/SettingsRoutes.d.ts +7 -0
- package/dist/features/settings/SettingsRoutes.d.ts.map +1 -0
- package/dist/features/settings/SettingsView.d.ts +2 -0
- package/dist/features/settings/SettingsView.d.ts.map +1 -0
- package/dist/features/settings/components/Advanced.d.ts +2 -0
- package/dist/features/settings/components/Advanced.d.ts.map +1 -0
- package/dist/features/settings/components/Appearance.d.ts +2 -0
- package/dist/features/settings/components/Appearance.d.ts.map +1 -0
- package/dist/features/settings/components/DataPrivacy.d.ts +2 -0
- package/dist/features/settings/components/DataPrivacy.d.ts.map +1 -0
- package/dist/features/settings/components/Develop.d.ts +2 -0
- package/dist/features/settings/components/Develop.d.ts.map +1 -0
- package/dist/features/settings/components/LanguageAndRegion.d.ts +2 -0
- package/dist/features/settings/components/LanguageAndRegion.d.ts.map +1 -0
- package/dist/features/settings/components/ServiceWorker.d.ts +2 -0
- package/dist/features/settings/components/ServiceWorker.d.ts.map +1 -0
- package/dist/features/settings/components/UpdateApp.d.ts +2 -0
- package/dist/features/settings/components/UpdateApp.d.ts.map +1 -0
- package/dist/features/settings/components/develop/DialogTestButtons.d.ts +2 -0
- package/dist/features/settings/components/develop/DialogTestButtons.d.ts.map +1 -0
- package/dist/features/settings/components/develop/DrawerTestButtons.d.ts +2 -0
- package/dist/features/settings/components/develop/DrawerTestButtons.d.ts.map +1 -0
- package/dist/features/settings/components/develop/ModalTestButtons.d.ts +2 -0
- package/dist/features/settings/components/develop/ModalTestButtons.d.ts.map +1 -0
- package/dist/features/settings/components/develop/ToastTestButtons.d.ts +2 -0
- package/dist/features/settings/components/develop/ToastTestButtons.d.ts.map +1 -0
- package/dist/features/settings/hooks/useSettings.d.ts +2 -0
- package/dist/features/settings/hooks/useSettings.d.ts.map +1 -0
- package/dist/features/sonner/SonnerContext.d.ts +29 -0
- package/dist/features/sonner/SonnerContext.d.ts.map +1 -0
- package/dist/features/theme/ThemeProvider.d.ts +11 -0
- package/dist/features/theme/ThemeProvider.d.ts.map +1 -0
- package/dist/features/theme/themes.d.ts +114 -0
- package/dist/features/theme/themes.d.ts.map +1 -0
- package/dist/features/theme/useTheme.d.ts +10 -0
- package/dist/features/theme/useTheme.d.ts.map +1 -0
- package/dist/i18n/I18nProvider.d.ts +9 -0
- package/dist/i18n/I18nProvider.d.ts.map +1 -0
- package/dist/i18n/config.d.ts +23 -0
- package/dist/i18n/config.d.ts.map +1 -0
- package/dist/i18n/translations/en/common.json.d.ts +19 -0
- package/dist/i18n/translations/en/cookieConsent.json.d.ts +53 -0
- package/dist/i18n/translations/en/settings.json.d.ts +358 -0
- package/dist/i18n/translations/fr/common.json.d.ts +19 -0
- package/dist/i18n/translations/fr/cookieConsent.json.d.ts +53 -0
- package/dist/i18n/translations/fr/settings.json.d.ts +358 -0
- package/dist/index-lmRk5L6z.js +2160 -0
- package/dist/index-lmRk5L6z.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/utils.d.ts +3 -0
- package/dist/lib/utils.d.ts.map +1 -0
- package/dist/lib/z-index.d.ts +29 -0
- package/dist/lib/z-index.d.ts.map +1 -0
- package/dist/router/router.d.ts +3 -0
- package/dist/router/router.d.ts.map +1 -0
- package/dist/router/routes.d.ts +4 -0
- package/dist/router/routes.d.ts.map +1 -0
- package/dist/sidebar-ClIeZ2zb.js +303 -0
- package/dist/sidebar-ClIeZ2zb.js.map +1 -0
- package/dist/style.css +1 -0
- package/dist/switch-8SzUJz7Q.js +44 -0
- package/dist/switch-8SzUJz7Q.js.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +93 -0
- package/postcss.config.js +6 -0
- package/src/app.tsx +119 -0
- package/src/components/ContentView.tsx +258 -0
- package/src/components/HomeView.tsx +19 -0
- package/src/components/LoadingOverlay.tsx +12 -0
- package/src/components/NotFoundView.tsx +84 -0
- package/src/components/RouteErrorBoundary.tsx +95 -0
- package/src/components/ViewRoute.tsx +47 -0
- package/src/components/ui/alert-dialog.tsx +181 -0
- package/src/components/ui/breadcrumb.tsx +155 -0
- package/src/components/ui/button-group.tsx +52 -0
- package/src/components/ui/button.tsx +51 -0
- package/src/components/ui/dialog.tsx +160 -0
- package/src/components/ui/drawer.tsx +200 -0
- package/src/components/ui/select.tsx +24 -0
- package/src/components/ui/sidebar.tsx +406 -0
- package/src/components/ui/sonner.tsx +36 -0
- package/src/components/ui/switch.tsx +45 -0
- package/src/constants/urls.ts +4 -0
- package/src/features/alertDialog/DialogContext.tsx +468 -0
- package/src/features/config/ConfigProvider.ts +96 -0
- package/src/features/config/types.ts +195 -0
- package/src/features/config/useConfig.ts +15 -0
- package/src/features/cookieConsent/CookieConsentModal.tsx +122 -0
- package/src/features/cookieConsent/CookiePreferencesView.tsx +328 -0
- package/src/features/cookieConsent/cookieConsent.ts +84 -0
- package/src/features/cookieConsent/useCookieConsent.ts +39 -0
- package/src/features/drawer/DrawerContext.tsx +116 -0
- package/src/features/layouts/AppLayout.tsx +63 -0
- package/src/features/layouts/DefaultLayout.tsx +625 -0
- package/src/features/layouts/FullscreenLayout.tsx +55 -0
- package/src/features/layouts/LayoutProviders.tsx +20 -0
- package/src/features/layouts/OverlayShell.tsx +171 -0
- package/src/features/layouts/WindowsLayout.tsx +860 -0
- package/src/features/layouts/utils.ts +99 -0
- package/src/features/modal/ModalContext.tsx +112 -0
- package/src/features/sentry/initSentry.ts +72 -0
- package/src/features/settings/SettingsContext.tsx +19 -0
- package/src/features/settings/SettingsIcons.tsx +452 -0
- package/src/features/settings/SettingsProvider.tsx +341 -0
- package/src/features/settings/SettingsRoutes.tsx +66 -0
- package/src/features/settings/SettingsView.tsx +327 -0
- package/src/features/settings/components/Advanced.tsx +128 -0
- package/src/features/settings/components/Appearance.tsx +306 -0
- package/src/features/settings/components/DataPrivacy.tsx +142 -0
- package/src/features/settings/components/Develop.tsx +174 -0
- package/src/features/settings/components/LanguageAndRegion.tsx +329 -0
- package/src/features/settings/components/ServiceWorker.tsx +363 -0
- package/src/features/settings/components/UpdateApp.tsx +206 -0
- package/src/features/settings/components/develop/DialogTestButtons.tsx +137 -0
- package/src/features/settings/components/develop/DrawerTestButtons.tsx +67 -0
- package/src/features/settings/components/develop/ModalTestButtons.tsx +30 -0
- package/src/features/settings/components/develop/ToastTestButtons.tsx +179 -0
- package/src/features/settings/hooks/useSettings.tsx +10 -0
- package/src/features/sonner/SonnerContext.tsx +286 -0
- package/src/features/theme/ThemeProvider.tsx +16 -0
- package/src/features/theme/themes.ts +561 -0
- package/src/features/theme/useTheme.tsx +71 -0
- package/src/i18n/I18nProvider.tsx +32 -0
- package/src/i18n/config.ts +107 -0
- package/src/i18n/translations/en/common.json +16 -0
- package/src/i18n/translations/en/cookieConsent.json +50 -0
- package/src/i18n/translations/en/settings.json +355 -0
- package/src/i18n/translations/fr/common.json +16 -0
- package/src/i18n/translations/fr/cookieConsent.json +50 -0
- package/src/i18n/translations/fr/settings.json +355 -0
- package/src/index.css +412 -0
- package/src/index.html +100 -0
- package/src/index.ts +31 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/z-index.ts +29 -0
- package/src/main.tsx +26 -0
- package/src/router/router.tsx +8 -0
- package/src/router/routes.tsx +115 -0
- package/src/service-worker/register.ts +1199 -0
- package/src/service-worker/sw-dev.ts +87 -0
- package/src/service-worker/sw.ts +105 -0
- package/tailwind.config.js +60 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
// Language-specific label/title
|
|
2
|
+
export type LocalizedString =
|
|
3
|
+
| string
|
|
4
|
+
| {
|
|
5
|
+
en: string;
|
|
6
|
+
fr: string;
|
|
7
|
+
[key: string]: string; // Allow other language codes
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** Drawer position when opening a link in a drawer (optional, used when openIn === 'drawer'). */
|
|
11
|
+
export type DrawerPosition = 'top' | 'bottom' | 'left' | 'right';
|
|
12
|
+
|
|
13
|
+
/** Layout mode: 'sidebar' (default) shows navigation sidebar; 'fullscreen' shows only content area; 'windows' shows a taskbar with start menu and multi-window desktop. */
|
|
14
|
+
export type LayoutType = 'sidebar' | 'fullscreen' | 'windows';
|
|
15
|
+
|
|
16
|
+
export interface NavigationItem {
|
|
17
|
+
label: string | LocalizedString;
|
|
18
|
+
path: string;
|
|
19
|
+
url: string;
|
|
20
|
+
icon?: string; // Path to SVG icon file (e.g., '/icons/book-open.svg')
|
|
21
|
+
/** When true, hide this item from the sidebar and 404 page; route remains valid and item still appears in Develop settings. */
|
|
22
|
+
hidden?: boolean;
|
|
23
|
+
/** When true, hide this item on mobile (bottom nav). Has no effect if hidden is true. */
|
|
24
|
+
hiddenOnMobile?: boolean;
|
|
25
|
+
/** When true, hide this item on desktop (sidebar). Has no effect if hidden is true. */
|
|
26
|
+
hiddenOnDesktop?: boolean;
|
|
27
|
+
/** How to open this link: 'default' (navigate in main area), 'modal', 'drawer', or 'external' (target="_blank"). */
|
|
28
|
+
openIn?: 'default' | 'modal' | 'drawer' | 'external';
|
|
29
|
+
/** Optional drawer position when openIn === 'drawer'. Default is 'right' if omitted. */
|
|
30
|
+
drawerPosition?: DrawerPosition;
|
|
31
|
+
/** Sidebar position: 'start' (default) or 'end'. End items are rendered in the sidebar footer. */
|
|
32
|
+
position?: 'start' | 'end';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface NavigationGroup {
|
|
36
|
+
title: string | LocalizedString;
|
|
37
|
+
items: NavigationItem[];
|
|
38
|
+
/** Sidebar position: 'start' (default) or 'end'. End groups are rendered in the sidebar footer. */
|
|
39
|
+
position?: 'start' | 'end';
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ThemeColors {
|
|
43
|
+
light: {
|
|
44
|
+
background: string;
|
|
45
|
+
foreground: string;
|
|
46
|
+
card: string;
|
|
47
|
+
cardForeground: string;
|
|
48
|
+
popover: string;
|
|
49
|
+
popoverForeground: string;
|
|
50
|
+
primary: string;
|
|
51
|
+
primaryForeground: string;
|
|
52
|
+
secondary: string;
|
|
53
|
+
secondaryForeground: string;
|
|
54
|
+
muted: string;
|
|
55
|
+
mutedForeground: string;
|
|
56
|
+
accent: string;
|
|
57
|
+
accentForeground: string;
|
|
58
|
+
destructive: string;
|
|
59
|
+
destructiveForeground: string;
|
|
60
|
+
border: string;
|
|
61
|
+
input: string;
|
|
62
|
+
ring: string;
|
|
63
|
+
radius: string;
|
|
64
|
+
sidebarBackground: string;
|
|
65
|
+
sidebarForeground: string;
|
|
66
|
+
sidebarPrimary: string;
|
|
67
|
+
sidebarPrimaryForeground: string;
|
|
68
|
+
sidebarAccent: string;
|
|
69
|
+
sidebarAccentForeground: string;
|
|
70
|
+
sidebarBorder: string;
|
|
71
|
+
sidebarRing: string;
|
|
72
|
+
};
|
|
73
|
+
dark: {
|
|
74
|
+
background: string;
|
|
75
|
+
foreground: string;
|
|
76
|
+
card: string;
|
|
77
|
+
cardForeground: string;
|
|
78
|
+
popover: string;
|
|
79
|
+
popoverForeground: string;
|
|
80
|
+
primary: string;
|
|
81
|
+
primaryForeground: string;
|
|
82
|
+
secondary: string;
|
|
83
|
+
secondaryForeground: string;
|
|
84
|
+
muted: string;
|
|
85
|
+
mutedForeground: string;
|
|
86
|
+
accent: string;
|
|
87
|
+
accentForeground: string;
|
|
88
|
+
destructive: string;
|
|
89
|
+
destructiveForeground: string;
|
|
90
|
+
border: string;
|
|
91
|
+
input: string;
|
|
92
|
+
ring: string;
|
|
93
|
+
radius: string;
|
|
94
|
+
sidebarBackground: string;
|
|
95
|
+
sidebarForeground: string;
|
|
96
|
+
sidebarPrimary: string;
|
|
97
|
+
sidebarPrimaryForeground: string;
|
|
98
|
+
sidebarAccent: string;
|
|
99
|
+
sidebarAccentForeground: string;
|
|
100
|
+
sidebarBorder: string;
|
|
101
|
+
sidebarRing: string;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface ThemeDefinition {
|
|
106
|
+
name: string;
|
|
107
|
+
displayName: string;
|
|
108
|
+
colors: ThemeColors;
|
|
109
|
+
fontFamily?: string; // Optional custom font family (backward compatible)
|
|
110
|
+
headingFontFamily?: string; // Optional font family for headings (h1-h6)
|
|
111
|
+
bodyFontFamily?: string; // Optional font family for body text
|
|
112
|
+
fontFiles?: string[]; // Optional array of font file URLs or paths to load (e.g., Google Fonts links or local paths)
|
|
113
|
+
letterSpacing?: string; // Optional custom letter spacing (e.g., "0.02em")
|
|
114
|
+
textShadow?: string; // Optional custom text shadow (e.g., "1px 1px 2px rgba(0, 0, 0, 0.1)")
|
|
115
|
+
lineHeight?: string; // Optional custom line height (e.g., "1.6")
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Sentry error reporting configuration. Only used in production; ignored in dev. */
|
|
119
|
+
export interface SentryConfig {
|
|
120
|
+
/** Sentry DSN (Data Source Name). Required for Sentry to receive events. */
|
|
121
|
+
dsn: string;
|
|
122
|
+
/** Environment name (e.g. 'production', 'staging'). Shown in Sentry dashboard. */
|
|
123
|
+
environment?: string;
|
|
124
|
+
/** Release identifier (e.g. git SHA or version). Used for release-based grouping. */
|
|
125
|
+
release?: string;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Cookie consent categories for privacy-friendly grouping in the UI.
|
|
130
|
+
* - strict_necessary: Required for the app to work; typically no consent needed.
|
|
131
|
+
* - functional_performance: Analytics, performance, preferences.
|
|
132
|
+
* - targeting: Advertising, personalisation.
|
|
133
|
+
* - social_media_embedded: Social widgets, embedded content.
|
|
134
|
+
*/
|
|
135
|
+
export type CookieConsentCategory =
|
|
136
|
+
| 'strict_necessary'
|
|
137
|
+
| 'functional_performance'
|
|
138
|
+
| 'targeting'
|
|
139
|
+
| 'social_media_embedded';
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Definition of a cookie that can be toggled by the user via cookie consent.
|
|
143
|
+
* Host is the unique key: use it to gate features (e.g. getCookieConsentAccepted('sentry.io')).
|
|
144
|
+
*/
|
|
145
|
+
export interface CookieDefinition {
|
|
146
|
+
/** Display name for the cookie (e.g. "Sentry Error Reporting"). */
|
|
147
|
+
name: string;
|
|
148
|
+
/** Host or domain the cookie belongs to (e.g. "sentry.io", ".example.com"). Unique key for consent and feature gating. */
|
|
149
|
+
host: string;
|
|
150
|
+
/** Duration in seconds (e.g. 31536000 for 1 year). */
|
|
151
|
+
durationSeconds: number;
|
|
152
|
+
/** Type label for clarity (e.g. "first_party", "third_party", "http_only"). */
|
|
153
|
+
type: string;
|
|
154
|
+
/** Category for grouping in the consent UI. */
|
|
155
|
+
category: CookieConsentCategory;
|
|
156
|
+
/** Optional short description shown in the consent / settings UI. Can be a string or localized object with language keys. */
|
|
157
|
+
description?: LocalizedString;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Cookie consent configuration. Accepted hosts are stored in settings; store
|
|
162
|
+
* consentedCookieHosts when user submits so we can detect new cookies and re-prompt while keeping existing approvals.
|
|
163
|
+
*/
|
|
164
|
+
export interface CookieConsentConfig {
|
|
165
|
+
/** List of cookies the app may use. User consent is collected per category/cookie. */
|
|
166
|
+
cookies: CookieDefinition[];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** When set to 'tauri', disables service worker and hides its settings (Tauri uses a different caching system). */
|
|
170
|
+
export type RuntimeType = 'browser' | 'tauri';
|
|
171
|
+
|
|
172
|
+
export interface ShellUIConfig {
|
|
173
|
+
port?: number;
|
|
174
|
+
title?: string;
|
|
175
|
+
/** App version string (e.g. "1.2.0"). Shown in Settings > System > Update app. */
|
|
176
|
+
version?: string;
|
|
177
|
+
/** Set to 'tauri' when the app runs inside Tauri so service worker is disabled and hidden. */
|
|
178
|
+
runtime?: RuntimeType;
|
|
179
|
+
/** Favicon path (e.g. '/favicon.svg'). Used for the document link rel="icon". */
|
|
180
|
+
favicon?: string;
|
|
181
|
+
/** App icon path (e.g. '/favicon.svg'). Displayed before title/logo in sidebar header. */
|
|
182
|
+
appIcon?: string;
|
|
183
|
+
/** Logo path (e.g. '/logo.svg'). If defined, displayed as image in sidebar header instead of text title. */
|
|
184
|
+
logo?: string;
|
|
185
|
+
language?: string | string[]; // Single language code or array of enabled language codes (e.g., 'en' or ['en', 'fr'])
|
|
186
|
+
/** Layout mode: 'sidebar' (default) or 'fullscreen'. Fullscreen shows only content with no navigation. */
|
|
187
|
+
layout?: LayoutType;
|
|
188
|
+
navigation?: (NavigationItem | NavigationGroup)[];
|
|
189
|
+
themes?: ThemeDefinition[]; // Custom themes to register
|
|
190
|
+
defaultTheme?: string; // Default theme name to use
|
|
191
|
+
/** Sentry error reporting. Load from env (e.g. SENTRY_DSN). Only active in production builds. */
|
|
192
|
+
sentry?: SentryConfig;
|
|
193
|
+
/** Cookie consent: list of cookies by category; accepted ids are stored in settings. */
|
|
194
|
+
cookieConsent?: CookieConsentConfig;
|
|
195
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { useContext } from 'react';
|
|
2
|
+
import { ConfigContext, type ConfigContextValue } from './ConfigProvider';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook to access ShellUI configuration from ConfigProvider context.
|
|
6
|
+
* Must be used within a ConfigProvider.
|
|
7
|
+
* @returns {ConfigContextValue} Configuration object and loading state
|
|
8
|
+
*/
|
|
9
|
+
export function useConfig(): ConfigContextValue {
|
|
10
|
+
const context = useContext(ConfigContext);
|
|
11
|
+
if (context === null) {
|
|
12
|
+
throw new Error('useConfig must be used within a ConfigProvider');
|
|
13
|
+
}
|
|
14
|
+
return context;
|
|
15
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { useRef, useCallback, useEffect } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { shellui } from '@shellui/sdk';
|
|
4
|
+
import { useDialog } from '../alertDialog/DialogContext';
|
|
5
|
+
import { useConfig } from '../config/useConfig';
|
|
6
|
+
import { useSettings } from '../settings/hooks/useSettings';
|
|
7
|
+
import urls from '@/constants/urls';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Shows a friendly cookie consent modal on first visit (when user has not yet consented).
|
|
11
|
+
* Accept all / Reject all only. Closing via Escape or overlay counts as Reject.
|
|
12
|
+
*/
|
|
13
|
+
export function CookieConsentModal() {
|
|
14
|
+
const { t } = useTranslation('cookieConsent');
|
|
15
|
+
const { config } = useConfig();
|
|
16
|
+
const { settings, updateSetting } = useSettings();
|
|
17
|
+
const { dialog: showDialog } = useDialog();
|
|
18
|
+
const closedByChoiceRef = useRef(false);
|
|
19
|
+
const dialogShownRef = useRef(false);
|
|
20
|
+
|
|
21
|
+
const cookieConsent = config?.cookieConsent;
|
|
22
|
+
const cookies = cookieConsent?.cookies ?? [];
|
|
23
|
+
const consentedHosts = settings?.cookieConsent?.consentedCookieHosts ?? [];
|
|
24
|
+
const allHosts = cookies.map((c) => c.host);
|
|
25
|
+
const strictNecessaryHosts = cookies
|
|
26
|
+
.filter((c) => c.category === 'strict_necessary')
|
|
27
|
+
.map((c) => c.host);
|
|
28
|
+
|
|
29
|
+
// Check if there are new cookies that weren't in the list when user last consented
|
|
30
|
+
const hasNewCookies = allHosts.some((host) => !consentedHosts.includes(host));
|
|
31
|
+
const neverConsented = consentedHosts.length === 0;
|
|
32
|
+
const isRenewal = !neverConsented && hasNewCookies;
|
|
33
|
+
|
|
34
|
+
// Only show in root window, not in sub-apps (iframes)
|
|
35
|
+
const isRootWindow = typeof window !== 'undefined' && window.parent === window;
|
|
36
|
+
// Show if: has cookies AND (never consented OR new cookies added)
|
|
37
|
+
const shouldShow = isRootWindow && cookies.length > 0 && (neverConsented || hasNewCookies);
|
|
38
|
+
|
|
39
|
+
// Use renewal text if showing due to cookie policy update
|
|
40
|
+
const title = isRenewal ? t('titleRenewal') : t('title');
|
|
41
|
+
const description = isRenewal ? t('descriptionRenewal') : t('description');
|
|
42
|
+
|
|
43
|
+
const saveAccept = useCallback(() => {
|
|
44
|
+
updateSetting('cookieConsent', {
|
|
45
|
+
acceptedHosts: allHosts,
|
|
46
|
+
consentedCookieHosts: allHosts,
|
|
47
|
+
});
|
|
48
|
+
}, [allHosts, updateSetting]);
|
|
49
|
+
|
|
50
|
+
const saveReject = useCallback(() => {
|
|
51
|
+
updateSetting('cookieConsent', {
|
|
52
|
+
acceptedHosts: strictNecessaryHosts,
|
|
53
|
+
consentedCookieHosts: allHosts,
|
|
54
|
+
});
|
|
55
|
+
}, [strictNecessaryHosts, allHosts, updateSetting]);
|
|
56
|
+
|
|
57
|
+
const handleAccept = useCallback(() => {
|
|
58
|
+
closedByChoiceRef.current = true;
|
|
59
|
+
saveAccept();
|
|
60
|
+
}, [saveAccept]);
|
|
61
|
+
|
|
62
|
+
const handleReject = useCallback(() => {
|
|
63
|
+
closedByChoiceRef.current = true;
|
|
64
|
+
saveReject();
|
|
65
|
+
}, [saveReject]);
|
|
66
|
+
|
|
67
|
+
const handleSetPreferences = useCallback(() => {
|
|
68
|
+
closedByChoiceRef.current = true;
|
|
69
|
+
shellui.openDrawer({ url: `${urls.cookiePreferences}?initial=true`, size: '420px' });
|
|
70
|
+
}, []);
|
|
71
|
+
|
|
72
|
+
// Show/hide dialog based on shouldShow
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
if (!shouldShow) {
|
|
75
|
+
dialogShownRef.current = false;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Prevent showing dialog multiple times
|
|
80
|
+
if (dialogShownRef.current) {
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Mark as shown to prevent duplicate calls
|
|
85
|
+
dialogShownRef.current = true;
|
|
86
|
+
|
|
87
|
+
// Close any open modals/drawers when cookie consent is shown
|
|
88
|
+
shellui.propagateMessage({ type: 'SHELLUI_CLOSE_MODAL', payload: {} });
|
|
89
|
+
shellui.propagateMessage({ type: 'SHELLUI_CLOSE_DRAWER', payload: {} });
|
|
90
|
+
|
|
91
|
+
// Reset choice ref when showing dialog
|
|
92
|
+
closedByChoiceRef.current = false;
|
|
93
|
+
|
|
94
|
+
// Show dialog using DialogContext directly (for root window)
|
|
95
|
+
showDialog({
|
|
96
|
+
title,
|
|
97
|
+
description,
|
|
98
|
+
mode: 'okCancel',
|
|
99
|
+
okLabel: t('accept'),
|
|
100
|
+
cancelLabel: t('reject'),
|
|
101
|
+
position: 'bottom-left',
|
|
102
|
+
icon: 'cookie',
|
|
103
|
+
secondaryButton: {
|
|
104
|
+
label: t('setPreferences'),
|
|
105
|
+
onClick: handleSetPreferences,
|
|
106
|
+
},
|
|
107
|
+
onOk: handleAccept,
|
|
108
|
+
onCancel: handleReject,
|
|
109
|
+
});
|
|
110
|
+
}, [
|
|
111
|
+
shouldShow,
|
|
112
|
+
title,
|
|
113
|
+
description,
|
|
114
|
+
t,
|
|
115
|
+
handleAccept,
|
|
116
|
+
handleReject,
|
|
117
|
+
handleSetPreferences,
|
|
118
|
+
showDialog,
|
|
119
|
+
]);
|
|
120
|
+
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { useState, useCallback, useMemo, useEffect, useRef } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { useLocation } from 'react-router';
|
|
4
|
+
import { shellui } from '@shellui/sdk';
|
|
5
|
+
import { Button } from '@/components/ui/button';
|
|
6
|
+
import { Switch } from '@/components/ui/switch';
|
|
7
|
+
import { useConfig } from '../config/useConfig';
|
|
8
|
+
import { useSettings } from '../settings/hooks/useSettings';
|
|
9
|
+
import { resolveLocalizedString } from '../layouts/utils';
|
|
10
|
+
import type { CookieConsentCategory, CookieDefinition } from '../config/types';
|
|
11
|
+
|
|
12
|
+
/** Category display order and labels */
|
|
13
|
+
const CATEGORY_ORDER: CookieConsentCategory[] = [
|
|
14
|
+
'strict_necessary',
|
|
15
|
+
'functional_performance',
|
|
16
|
+
'targeting',
|
|
17
|
+
'social_media_embedded',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
/** Format duration in human-readable format */
|
|
21
|
+
function formatDuration(
|
|
22
|
+
seconds: number,
|
|
23
|
+
t: (key: string, options?: Record<string, unknown>) => string,
|
|
24
|
+
): string {
|
|
25
|
+
if (seconds < 60) return t('preferences.duration.seconds', { count: seconds });
|
|
26
|
+
if (seconds < 3600) return t('preferences.duration.minutes', { count: Math.floor(seconds / 60) });
|
|
27
|
+
if (seconds < 86400)
|
|
28
|
+
return t('preferences.duration.hours', { count: Math.floor(seconds / 3600) });
|
|
29
|
+
if (seconds < 31536000)
|
|
30
|
+
return t('preferences.duration.days', { count: Math.floor(seconds / 86400) });
|
|
31
|
+
return t('preferences.duration.years', { count: Math.floor(seconds / 31536000) });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function CookiePreferencesView() {
|
|
35
|
+
const { t, i18n } = useTranslation('cookieConsent');
|
|
36
|
+
const { config } = useConfig();
|
|
37
|
+
const { settings, updateSetting } = useSettings();
|
|
38
|
+
const location = useLocation();
|
|
39
|
+
const searchParams = new URLSearchParams(location.search);
|
|
40
|
+
const isInitialConsent = searchParams.get('initial') === 'true';
|
|
41
|
+
const currentLanguage = i18n.language || 'en';
|
|
42
|
+
|
|
43
|
+
const cookies = config?.cookieConsent?.cookies ?? [];
|
|
44
|
+
const allHosts = useMemo(() => cookies.map((c) => c.host), [cookies]);
|
|
45
|
+
|
|
46
|
+
// Strictly necessary hosts are always enabled
|
|
47
|
+
const strictNecessaryHosts = useMemo(
|
|
48
|
+
() => cookies.filter((c) => c.category === 'strict_necessary').map((c) => c.host),
|
|
49
|
+
[cookies],
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
const currentAcceptedHosts = settings?.cookieConsent?.acceptedHosts ?? [];
|
|
53
|
+
|
|
54
|
+
// Local state for unsaved changes (always include strict necessary)
|
|
55
|
+
const [localAcceptedHosts, setLocalAcceptedHosts] = useState<string[]>(() => [
|
|
56
|
+
...new Set([...currentAcceptedHosts, ...strictNecessaryHosts]),
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
// Track if save/accept/reject was clicked to avoid rejecting on intentional close
|
|
60
|
+
const actionClickedRef = useRef(false);
|
|
61
|
+
|
|
62
|
+
// Reset local state when drawer opens (when URL changes to include initial param)
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (isInitialConsent) {
|
|
65
|
+
// Always include strict necessary hosts
|
|
66
|
+
setLocalAcceptedHosts([...new Set([...currentAcceptedHosts, ...strictNecessaryHosts])]);
|
|
67
|
+
}
|
|
68
|
+
}, [isInitialConsent, currentAcceptedHosts, strictNecessaryHosts]);
|
|
69
|
+
|
|
70
|
+
// Handle drawer close without saving during initial consent
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (!isInitialConsent) return;
|
|
73
|
+
|
|
74
|
+
const handleDrawerClose = () => {
|
|
75
|
+
// If closing without saving during initial consent, reject all except strict necessary
|
|
76
|
+
// Check if user has never consented and no action was clicked
|
|
77
|
+
const neverConsented = (settings?.cookieConsent?.consentedCookieHosts ?? []).length === 0;
|
|
78
|
+
if (neverConsented && !actionClickedRef.current) {
|
|
79
|
+
updateSetting('cookieConsent', {
|
|
80
|
+
acceptedHosts: strictNecessaryHosts,
|
|
81
|
+
consentedCookieHosts: allHosts,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const cleanup = shellui.addMessageListener('SHELLUI_CLOSE_DRAWER', handleDrawerClose);
|
|
87
|
+
return cleanup;
|
|
88
|
+
}, [isInitialConsent, strictNecessaryHosts, allHosts, updateSetting, settings]);
|
|
89
|
+
|
|
90
|
+
// Cleanup on unmount: if initial consent and drawer closes without save, reject all
|
|
91
|
+
useEffect(() => {
|
|
92
|
+
return () => {
|
|
93
|
+
if (isInitialConsent && !actionClickedRef.current) {
|
|
94
|
+
// Check if user has never consented
|
|
95
|
+
const neverConsented = (settings?.cookieConsent?.consentedCookieHosts ?? []).length === 0;
|
|
96
|
+
if (neverConsented) {
|
|
97
|
+
updateSetting('cookieConsent', {
|
|
98
|
+
acceptedHosts: strictNecessaryHosts,
|
|
99
|
+
consentedCookieHosts: allHosts,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
}, [isInitialConsent, strictNecessaryHosts, allHosts, updateSetting, settings]);
|
|
105
|
+
|
|
106
|
+
// Group cookies by category
|
|
107
|
+
const cookiesByCategory = useMemo(() => {
|
|
108
|
+
const grouped = new Map<CookieConsentCategory, CookieDefinition[]>();
|
|
109
|
+
for (const cookie of cookies) {
|
|
110
|
+
const existing = grouped.get(cookie.category) ?? [];
|
|
111
|
+
grouped.set(cookie.category, [...existing, cookie]);
|
|
112
|
+
}
|
|
113
|
+
return grouped;
|
|
114
|
+
}, [cookies]);
|
|
115
|
+
|
|
116
|
+
// Toggle individual cookie
|
|
117
|
+
const toggleCookie = useCallback((host: string, enabled: boolean) => {
|
|
118
|
+
setLocalAcceptedHosts((prev) => (enabled ? [...prev, host] : prev.filter((h) => h !== host)));
|
|
119
|
+
}, []);
|
|
120
|
+
|
|
121
|
+
// Toggle entire category
|
|
122
|
+
const toggleCategory = useCallback(
|
|
123
|
+
(category: CookieConsentCategory, enabled: boolean) => {
|
|
124
|
+
const categoryHosts = cookiesByCategory.get(category)?.map((c) => c.host) ?? [];
|
|
125
|
+
setLocalAcceptedHosts((prev) => {
|
|
126
|
+
if (enabled) {
|
|
127
|
+
return [...new Set([...prev, ...categoryHosts])];
|
|
128
|
+
} else {
|
|
129
|
+
const hostsSet = new Set(categoryHosts);
|
|
130
|
+
return prev.filter((h) => !hostsSet.has(h));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
},
|
|
134
|
+
[cookiesByCategory],
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
// Check if category is fully or partially enabled
|
|
138
|
+
const getCategoryState = useCallback(
|
|
139
|
+
(category: CookieConsentCategory): 'all' | 'some' | 'none' => {
|
|
140
|
+
const categoryHosts = cookiesByCategory.get(category)?.map((c) => c.host) ?? [];
|
|
141
|
+
if (categoryHosts.length === 0) return 'none';
|
|
142
|
+
const enabledCount = categoryHosts.filter((h) => localAcceptedHosts.includes(h)).length;
|
|
143
|
+
if (enabledCount === categoryHosts.length) return 'all';
|
|
144
|
+
if (enabledCount > 0) return 'some';
|
|
145
|
+
return 'none';
|
|
146
|
+
},
|
|
147
|
+
[cookiesByCategory, localAcceptedHosts],
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
// Accept all
|
|
151
|
+
const handleAcceptAll = useCallback(() => {
|
|
152
|
+
actionClickedRef.current = true;
|
|
153
|
+
updateSetting('cookieConsent', {
|
|
154
|
+
acceptedHosts: allHosts,
|
|
155
|
+
consentedCookieHosts: allHosts,
|
|
156
|
+
});
|
|
157
|
+
shellui.closeDrawer();
|
|
158
|
+
}, [allHosts, updateSetting]);
|
|
159
|
+
|
|
160
|
+
// Reject all except strict necessary (which are always enabled)
|
|
161
|
+
const handleRejectAll = useCallback(() => {
|
|
162
|
+
actionClickedRef.current = true;
|
|
163
|
+
updateSetting('cookieConsent', {
|
|
164
|
+
acceptedHosts: strictNecessaryHosts,
|
|
165
|
+
consentedCookieHosts: allHosts,
|
|
166
|
+
});
|
|
167
|
+
shellui.closeDrawer();
|
|
168
|
+
}, [strictNecessaryHosts, allHosts, updateSetting]);
|
|
169
|
+
|
|
170
|
+
// Save custom preferences (always include strict necessary)
|
|
171
|
+
const handleSave = useCallback(() => {
|
|
172
|
+
actionClickedRef.current = true;
|
|
173
|
+
const hostsToSave = [...new Set([...localAcceptedHosts, ...strictNecessaryHosts])];
|
|
174
|
+
updateSetting('cookieConsent', {
|
|
175
|
+
acceptedHosts: hostsToSave,
|
|
176
|
+
consentedCookieHosts: allHosts,
|
|
177
|
+
});
|
|
178
|
+
shellui.closeDrawer();
|
|
179
|
+
}, [localAcceptedHosts, strictNecessaryHosts, allHosts, updateSetting]);
|
|
180
|
+
|
|
181
|
+
// Check if preferences have changed
|
|
182
|
+
const hasChanges = useMemo(() => {
|
|
183
|
+
if (localAcceptedHosts.length !== currentAcceptedHosts.length) return true;
|
|
184
|
+
const sortedLocal = [...localAcceptedHosts].sort();
|
|
185
|
+
const sortedCurrent = [...currentAcceptedHosts].sort();
|
|
186
|
+
return sortedLocal.some((h, i) => h !== sortedCurrent[i]);
|
|
187
|
+
}, [localAcceptedHosts, currentAcceptedHosts]);
|
|
188
|
+
|
|
189
|
+
if (cookies.length === 0) {
|
|
190
|
+
return (
|
|
191
|
+
<div className="flex items-center justify-center h-full p-6">
|
|
192
|
+
<p className="text-muted-foreground">{t('preferences.noCookies')}</p>
|
|
193
|
+
</div>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return (
|
|
198
|
+
<div className="flex flex-col h-full bg-background">
|
|
199
|
+
{/* Header */}
|
|
200
|
+
<div className="flex flex-col space-y-2 border-b border-border/60 px-6 pt-5 pb-4">
|
|
201
|
+
<h2
|
|
202
|
+
className="text-lg font-semibold leading-none tracking-tight"
|
|
203
|
+
style={{ fontFamily: 'var(--heading-font-family, inherit)' }}
|
|
204
|
+
>
|
|
205
|
+
{t('preferences.title')}
|
|
206
|
+
</h2>
|
|
207
|
+
<p className="text-sm text-muted-foreground">{t('preferences.description')}</p>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
{/* Content */}
|
|
211
|
+
<div className="flex-1 overflow-y-auto px-6 py-4">
|
|
212
|
+
{CATEGORY_ORDER.map((category) => {
|
|
213
|
+
const categoryCookies = cookiesByCategory.get(category);
|
|
214
|
+
if (!categoryCookies || categoryCookies.length === 0) return null;
|
|
215
|
+
|
|
216
|
+
const categoryState = getCategoryState(category);
|
|
217
|
+
const isStrictNecessary = category === 'strict_necessary';
|
|
218
|
+
|
|
219
|
+
return (
|
|
220
|
+
<div
|
|
221
|
+
key={category}
|
|
222
|
+
className="mb-6 last:mb-0"
|
|
223
|
+
>
|
|
224
|
+
{/* Category header with toggle */}
|
|
225
|
+
<div className="flex items-center justify-between mb-3">
|
|
226
|
+
<div className="flex-1 min-w-0">
|
|
227
|
+
<h3
|
|
228
|
+
className="text-sm font-semibold"
|
|
229
|
+
style={{ fontFamily: 'var(--heading-font-family, inherit)' }}
|
|
230
|
+
>
|
|
231
|
+
{t(`preferences.categories.${category}.title`)}
|
|
232
|
+
</h3>
|
|
233
|
+
<p className="text-xs text-muted-foreground mt-0.5">
|
|
234
|
+
{t(`preferences.categories.${category}.description`)}
|
|
235
|
+
</p>
|
|
236
|
+
</div>
|
|
237
|
+
<div className="ml-4 flex items-center gap-2">
|
|
238
|
+
{!isStrictNecessary && categoryState === 'some' && (
|
|
239
|
+
<span className="text-xs text-muted-foreground">
|
|
240
|
+
{t('preferences.partial')}
|
|
241
|
+
</span>
|
|
242
|
+
)}
|
|
243
|
+
{isStrictNecessary ? (
|
|
244
|
+
<span className="text-xs text-muted-foreground">
|
|
245
|
+
{t('preferences.alwaysOn')}
|
|
246
|
+
</span>
|
|
247
|
+
) : (
|
|
248
|
+
<Switch
|
|
249
|
+
checked={categoryState === 'all'}
|
|
250
|
+
onCheckedChange={(checked) => toggleCategory(category, checked)}
|
|
251
|
+
aria-label={t(`preferences.categories.${category}.title`)}
|
|
252
|
+
/>
|
|
253
|
+
)}
|
|
254
|
+
</div>
|
|
255
|
+
</div>
|
|
256
|
+
|
|
257
|
+
{/* Individual cookies - only show for non-strictly-necessary categories */}
|
|
258
|
+
{!isStrictNecessary && (
|
|
259
|
+
<div className="space-y-2 pl-2 border-l-2 border-border ml-1">
|
|
260
|
+
{categoryCookies.map((cookie) => {
|
|
261
|
+
const isEnabled = localAcceptedHosts.includes(cookie.host);
|
|
262
|
+
return (
|
|
263
|
+
<div
|
|
264
|
+
key={cookie.host}
|
|
265
|
+
className="flex items-start justify-between gap-3 py-2 px-3 rounded-md bg-muted/30"
|
|
266
|
+
>
|
|
267
|
+
<div className="flex-1 min-w-0">
|
|
268
|
+
<span className="text-sm font-medium truncate">{cookie.name}</span>
|
|
269
|
+
{cookie.description && (
|
|
270
|
+
<p className="text-xs text-muted-foreground mt-0.5 line-clamp-2">
|
|
271
|
+
{resolveLocalizedString(cookie.description, currentLanguage)}
|
|
272
|
+
</p>
|
|
273
|
+
)}
|
|
274
|
+
<div className="flex items-center gap-3 mt-1 text-[10px] text-muted-foreground/70">
|
|
275
|
+
<span>{cookie.host}</span>
|
|
276
|
+
<span>•</span>
|
|
277
|
+
<span>{formatDuration(cookie.durationSeconds, t)}</span>
|
|
278
|
+
<span>•</span>
|
|
279
|
+
<span className="capitalize">{cookie.type.replace('_', ' ')}</span>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
<Switch
|
|
283
|
+
checked={isEnabled}
|
|
284
|
+
onCheckedChange={(checked) => toggleCookie(cookie.host, checked)}
|
|
285
|
+
aria-label={cookie.name}
|
|
286
|
+
/>
|
|
287
|
+
</div>
|
|
288
|
+
);
|
|
289
|
+
})}
|
|
290
|
+
</div>
|
|
291
|
+
)}
|
|
292
|
+
</div>
|
|
293
|
+
);
|
|
294
|
+
})}
|
|
295
|
+
</div>
|
|
296
|
+
|
|
297
|
+
{/* Footer */}
|
|
298
|
+
<div className="mt-auto flex flex-col gap-2 border-t border-border px-6 py-4">
|
|
299
|
+
<div className="flex gap-2">
|
|
300
|
+
<Button
|
|
301
|
+
variant="outline"
|
|
302
|
+
size="sm"
|
|
303
|
+
onClick={handleRejectAll}
|
|
304
|
+
className="flex-1"
|
|
305
|
+
>
|
|
306
|
+
{t('preferences.rejectAll')}
|
|
307
|
+
</Button>
|
|
308
|
+
<Button
|
|
309
|
+
variant="outline"
|
|
310
|
+
size="sm"
|
|
311
|
+
onClick={handleAcceptAll}
|
|
312
|
+
className="flex-1"
|
|
313
|
+
>
|
|
314
|
+
{t('preferences.acceptAll')}
|
|
315
|
+
</Button>
|
|
316
|
+
</div>
|
|
317
|
+
<Button
|
|
318
|
+
size="sm"
|
|
319
|
+
onClick={handleSave}
|
|
320
|
+
disabled={!hasChanges && !isInitialConsent}
|
|
321
|
+
className="w-full"
|
|
322
|
+
>
|
|
323
|
+
{t('preferences.save')}
|
|
324
|
+
</Button>
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
);
|
|
328
|
+
}
|